Merge branch 'develop' into account-portal-auth-api-testing-2

This commit is contained in:
Mitch-Budibase 2023-08-30 15:14:48 +01:00
commit 178b807573
296 changed files with 6283 additions and 3584 deletions

View File

@ -18,6 +18,8 @@ env:
BRANCH: ${{ github.event.pull_request.head.ref }} BRANCH: ${{ github.event.pull_request.head.ref }}
BASE_BRANCH: ${{ github.event.pull_request.base.ref}} BASE_BRANCH: ${{ github.event.pull_request.base.ref}}
PERSONAL_ACCESS_TOKEN: ${{ secrets.PERSONAL_ACCESS_TOKEN }} PERSONAL_ACCESS_TOKEN: ${{ secrets.PERSONAL_ACCESS_TOKEN }}
NX_BASE_BRANCH: origin/${{ github.base_ref }}
USE_NX_AFFECTED: ${{ github.event_name == 'pull_request' && github.base_ref != 'master'}}
jobs: jobs:
lint: lint:
@ -25,20 +27,20 @@ jobs:
steps: steps:
- name: Checkout repo and submodules - name: Checkout repo and submodules
uses: actions/checkout@v3 uses: actions/checkout@v3
if: github.repository == github.event.pull_request.head.repo.full_name if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'Budibase/budibase'
with: with:
submodules: true submodules: true
token: ${{ secrets.PERSONAL_ACCESS_TOKEN || github.token }} token: ${{ secrets.PERSONAL_ACCESS_TOKEN || github.token }}
- name: Checkout repo only - name: Checkout repo only
uses: actions/checkout@v3 uses: actions/checkout@v3
if: github.repository != github.event.pull_request.head.repo.full_name if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name != 'Budibase/budibase'
- name: Use Node.js 14.x - name: Use Node.js 18.x
uses: actions/setup-node@v3 uses: actions/setup-node@v3
with: with:
node-version: 14.x node-version: 18.x
cache: "yarn" cache: "yarn"
- run: yarn - run: yarn --frozen-lockfile
- run: yarn lint - run: yarn lint
build: build:
@ -46,45 +48,66 @@ jobs:
steps: steps:
- name: Checkout repo and submodules - name: Checkout repo and submodules
uses: actions/checkout@v3 uses: actions/checkout@v3
if: github.repository == github.event.pull_request.head.repo.full_name if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'Budibase/budibase'
with: with:
submodules: true submodules: true
token: ${{ secrets.PERSONAL_ACCESS_TOKEN || github.token }} token: ${{ secrets.PERSONAL_ACCESS_TOKEN || github.token }}
fetch-depth: 0
- name: Checkout repo only - name: Checkout repo only
uses: actions/checkout@v3 uses: actions/checkout@v3
if: github.repository != github.event.pull_request.head.repo.full_name if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name != 'Budibase/budibase'
with:
fetch-depth: 0
- name: Use Node.js 14.x - name: Use Node.js 18.x
uses: actions/setup-node@v3 uses: actions/setup-node@v3
with: with:
node-version: 14.x node-version: 18.x
cache: "yarn" cache: "yarn"
- run: yarn - run: yarn --frozen-lockfile
# Run build all the projects # Run build all the projects
- run: yarn build - name: Build
run: |
yarn build
# Check the types of the projects built via esbuild # Check the types of the projects built via esbuild
- run: yarn check:types - name: Check types
run: |
if ${{ env.USE_NX_AFFECTED }}; then
yarn check:types --since=${{ env.NX_BASE_BRANCH }}
else
yarn check:types
fi
test-libraries: test-libraries:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout repo and submodules - name: Checkout repo and submodules
uses: actions/checkout@v3 uses: actions/checkout@v3
if: github.repository == github.event.pull_request.head.repo.full_name if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'Budibase/budibase'
with: with:
submodules: true submodules: true
token: ${{ secrets.PERSONAL_ACCESS_TOKEN || github.token }} token: ${{ secrets.PERSONAL_ACCESS_TOKEN || github.token }}
fetch-depth: 0
- name: Checkout repo only - name: Checkout repo only
uses: actions/checkout@v3 uses: actions/checkout@v3
if: github.repository != github.event.pull_request.head.repo.full_name if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name != 'Budibase/budibase'
with:
fetch-depth: 0
- name: Use Node.js 14.x - name: Use Node.js 18.x
uses: actions/setup-node@v3 uses: actions/setup-node@v3
with: with:
node-version: 14.x node-version: 18.x
cache: "yarn" cache: "yarn"
- run: yarn - run: yarn --frozen-lockfile
- run: yarn test --ignore=@budibase/worker --ignore=@budibase/server --ignore=@budibase/pro - name: Test
run: |
if ${{ env.USE_NX_AFFECTED }}; then
yarn test --ignore=@budibase/worker --ignore=@budibase/server --ignore=@budibase/pro --since=${{ env.NX_BASE_BRANCH }}
else
yarn test --ignore=@budibase/worker --ignore=@budibase/server --ignore=@budibase/pro
fi
- uses: codecov/codecov-action@v3 - uses: codecov/codecov-action@v3
with: with:
token: ${{ secrets.CODECOV_TOKEN }} # not required for public repos token: ${{ secrets.CODECOV_TOKEN }} # not required for public repos
@ -96,21 +119,31 @@ jobs:
steps: steps:
- name: Checkout repo and submodules - name: Checkout repo and submodules
uses: actions/checkout@v3 uses: actions/checkout@v3
if: github.repository == github.event.pull_request.head.repo.full_name if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'Budibase/budibase'
with: with:
submodules: true submodules: true
token: ${{ secrets.PERSONAL_ACCESS_TOKEN || github.token }} token: ${{ secrets.PERSONAL_ACCESS_TOKEN || github.token }}
fetch-depth: 0
- name: Checkout repo only - name: Checkout repo only
uses: actions/checkout@v3 uses: actions/checkout@v3
if: github.repository != github.event.pull_request.head.repo.full_name if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name != 'Budibase/budibase'
with:
fetch-depth: 0
- name: Use Node.js 14.x - name: Use Node.js 18.x
uses: actions/setup-node@v3 uses: actions/setup-node@v3
with: with:
node-version: 14.x node-version: 18.x
cache: "yarn" cache: "yarn"
- run: yarn - run: yarn --frozen-lockfile
- run: yarn test --scope=@budibase/worker --scope=@budibase/server - name: Test worker and server
run: |
if ${{ env.USE_NX_AFFECTED }}; then
yarn test --scope=@budibase/worker --scope=@budibase/server --since=${{ env.NX_BASE_BRANCH }}
else
yarn test --scope=@budibase/worker --scope=@budibase/server
fi
- uses: codecov/codecov-action@v3 - uses: codecov/codecov-action@v3
with: with:
token: ${{ secrets.CODECOV_TOKEN || github.token }} # not required for public repos token: ${{ secrets.CODECOV_TOKEN || github.token }} # not required for public repos
@ -119,42 +152,50 @@ jobs:
test-pro: test-pro:
runs-on: ubuntu-latest runs-on: ubuntu-latest
if: github.repository == github.event.pull_request.head.repo.full_name if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'Budibase/budibase'
steps: steps:
- name: Checkout repo and submodules - name: Checkout repo and submodules
uses: actions/checkout@v3 uses: actions/checkout@v3
with: with:
submodules: true submodules: true
token: ${{ secrets.PERSONAL_ACCESS_TOKEN || github.token }} token: ${{ secrets.PERSONAL_ACCESS_TOKEN || github.token }}
fetch-depth: 0
- name: Use Node.js 14.x - name: Use Node.js 18.x
uses: actions/setup-node@v3 uses: actions/setup-node@v3
with: with:
node-version: 14.x node-version: 18.x
cache: "yarn" cache: "yarn"
- run: yarn - run: yarn --frozen-lockfile
- run: yarn test --scope=@budibase/pro - name: Test
run: |
if ${{ env.USE_NX_AFFECTED }}; then
yarn test --scope=@budibase/pro --since=${{ env.NX_BASE_BRANCH }}
else
yarn test --scope=@budibase/pro
fi
integration-test: integration-test:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout repo and submodules - name: Checkout repo and submodules
uses: actions/checkout@v3 uses: actions/checkout@v3
if: github.repository == github.event.pull_request.head.repo.full_name if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'Budibase/budibase'
with: with:
submodules: true submodules: true
token: ${{ secrets.PERSONAL_ACCESS_TOKEN || github.token }} token: ${{ secrets.PERSONAL_ACCESS_TOKEN || github.token }}
- name: Checkout repo only - name: Checkout repo only
uses: actions/checkout@v3 uses: actions/checkout@v3
if: github.repository != github.event.pull_request.head.repo.full_name if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name != 'Budibase/budibase'
- name: Use Node.js 14.x - name: Use Node.js 18.x
uses: actions/setup-node@v3 uses: actions/setup-node@v3
with: with:
node-version: 14.x node-version: 18.x
cache: "yarn" cache: "yarn"
- run: yarn - run: yarn --frozen-lockfile
- run: yarn build --projects=@budibase/server,@budibase/worker,@budibase/client - name: Build packages
run: yarn build --scope @budibase/server --scope @budibase/worker --scope @budibase/client --scope @budibase/backend-core
- name: Run tests - name: Run tests
run: | run: |
cd qa-core cd qa-core
@ -166,13 +207,12 @@ jobs:
check-pro-submodule: check-pro-submodule:
runs-on: ubuntu-latest runs-on: ubuntu-latest
if: github.repository == github.event.pull_request.head.repo.full_name if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'Budibase/budibase'
steps: steps:
- name: Checkout repo and submodules - name: Checkout repo and submodules
uses: actions/checkout@v3 uses: actions/checkout@v3
with: with:
submodules: true submodules: true
fetch-depth: 0
token: ${{ secrets.PERSONAL_ACCESS_TOKEN || github.token }} token: ${{ secrets.PERSONAL_ACCESS_TOKEN || github.token }}
- name: Check pro commit - name: Check pro commit
@ -190,6 +230,8 @@ jobs:
base_commit=$(git rev-parse origin/develop) base_commit=$(git rev-parse origin/develop)
fi fi
echo "target_branch=$branch"
echo "target_branch=$branch" >> "$GITHUB_OUTPUT"
echo "pro_commit=$pro_commit" echo "pro_commit=$pro_commit"
echo "pro_commit=$pro_commit" >> "$GITHUB_OUTPUT" echo "pro_commit=$pro_commit" >> "$GITHUB_OUTPUT"
echo "base_commit=$base_commit" echo "base_commit=$base_commit"
@ -204,7 +246,7 @@ jobs:
const baseCommit = '${{ steps.get_pro_commits.outputs.base_commit }}'; const baseCommit = '${{ steps.get_pro_commits.outputs.base_commit }}';
if (submoduleCommit !== baseCommit) { if (submoduleCommit !== baseCommit) {
console.error('Submodule commit does not match the latest commit on the develop branch.'); console.error('Submodule commit does not match the latest commit on the "${{ steps.get_pro_commits.outputs.target_branch }}"" branch.');
console.error('Refer to the pro repo to merge your changes: https://github.com/Budibase/budibase-pro/blob/develop/docs/getting_started.md') console.error('Refer to the pro repo to merge your changes: https://github.com/Budibase/budibase-pro/blob/develop/docs/getting_started.md')
process.exit(1); process.exit(1);
} else { } else {

View File

@ -0,0 +1,29 @@
name: check_unreleased_changes
on:
pull_request:
branches:
- master
jobs:
check_unreleased:
runs-on: ubuntu-latest
steps:
- name: Check for unreleased changes
env:
REPO: "Budibase/budibase"
TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
RELEASE_TIMESTAMP=$(curl -s -H "Authorization: token $TOKEN" \
"https://api.github.com/repos/$REPO/releases/latest" | \
jq -r .published_at)
COMMIT_TIMESTAMP=$(curl -s -H "Authorization: token $TOKEN" \
"https://api.github.com/repos/$REPO/commits/master" | \
jq -r .commit.committer.date)
RELEASE_SECONDS=$(date --date="$RELEASE_TIMESTAMP" "+%s")
COMMIT_SECONDS=$(date --date="$COMMIT_TIMESTAMP" "+%s")
if (( COMMIT_SECONDS > RELEASE_SECONDS )); then
echo "There are unreleased changes. Please release these changes before merging."
exit 1
fi
echo "No unreleased changes detected."

View File

@ -0,0 +1,19 @@
name: deploy-featurebranch
on:
pull_request:
branches:
- develop
jobs:
release:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: passeidireto/trigger-external-workflow-action@main
env:
BRANCH: ${{ github.head_ref }}
with:
repository: budibase/budibase-deploys
event: featurebranch-qa-deploy
github_pat: ${{ secrets.GH_ACCESS_TOKEN }}

View File

@ -44,7 +44,7 @@ jobs:
- uses: actions/setup-node@v1 - uses: actions/setup-node@v1
with: with:
node-version: 14.x node-version: 18.x
- run: yarn install --frozen-lockfile - run: yarn install --frozen-lockfile
- name: Update versions - name: Update versions

View File

@ -60,9 +60,9 @@ jobs:
- name: "Get Current tag" - name: "Get Current tag"
id: currenttag id: currenttag
run: | run: |
version=v$(./scripts/getCurrentVersion.sh) version=$(./scripts/getCurrentVersion.sh)
echo 'Using tag $version' echo "Using tag $version"
echo "::set-output name=tag::$resversionult" echo "version=$version" >> "$GITHUB_OUTPUT"
- name: Build/release Docker images - name: Build/release Docker images
run: | run: |
@ -71,7 +71,7 @@ jobs:
env: env:
DOCKER_USER: ${{ secrets.DOCKER_USERNAME }} DOCKER_USER: ${{ secrets.DOCKER_USERNAME }}
DOCKER_PASSWORD: ${{ secrets.DOCKER_API_KEY }} DOCKER_PASSWORD: ${{ secrets.DOCKER_API_KEY }}
BUDIBASE_RELEASE_VERSION: ${{ steps.currenttag.outputs.tag }} BUDIBASE_RELEASE_VERSION: ${{ steps.currenttag.outputs.version }}
release-helm-chart: release-helm-chart:
needs: [release-images] needs: [release-images]

View File

@ -1,4 +1,4 @@
name: release-singleimage name: Deploy Budibase Single Container Image to DockerHub
on: on:
workflow_dispatch: workflow_dispatch:
@ -8,13 +8,20 @@ env:
PERSONAL_ACCESS_TOKEN: ${{ secrets.PERSONAL_ACCESS_TOKEN }} PERSONAL_ACCESS_TOKEN: ${{ secrets.PERSONAL_ACCESS_TOKEN }}
REGISTRY_URL: registry.hub.docker.com REGISTRY_URL: registry.hub.docker.com
jobs: jobs:
build-amd64: build:
name: "build-amd64" name: "build"
runs-on: ubuntu-latest runs-on: ubuntu-latest
strategy: strategy:
matrix: matrix:
node-version: [14.x] node-version: [14.x]
steps: steps:
- name: Maximize build space
uses: easimon/maximize-build-space@master
with:
root-reserve-mb: 35000
swap-size-mb: 1024
remove-android: 'true'
remove-dotnet: 'true'
- name: Fail if not a tag - name: Fail if not a tag
run: | run: |
if [[ $GITHUB_REF != refs/tags/* ]]; then if [[ $GITHUB_REF != refs/tags/* ]]; then
@ -27,12 +34,14 @@ jobs:
submodules: true submodules: true
token: ${{ secrets.PERSONAL_ACCESS_TOKEN }} token: ${{ secrets.PERSONAL_ACCESS_TOKEN }}
fetch-depth: 0 fetch-depth: 0
- name: Fail if tag is not in master - name: Fail if tag is not in master
run: | run: |
if ! git merge-base --is-ancestor ${{ github.sha }} origin/master; then if ! git merge-base --is-ancestor ${{ github.sha }} origin/master; then
echo "Tag is not in master. This pipeline can only execute tags that are present on the master branch" echo "Tag is not in master. This pipeline can only execute tags that are present on the master branch"
exit 1 exit 1
fi fi
- name: Use Node.js ${{ matrix.node-version }} - name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v1 uses: actions/setup-node@v1
with: with:
@ -68,139 +77,9 @@ jobs:
with: with:
context: . context: .
push: true push: true
platforms: linux/amd64 platforms: linux/amd64,linux/arm64
tags: budibase/budibase,budibase/budibase:v${{ env.RELEASE_VERSION }} tags: budibase/budibase,budibase/budibase:${{ env.RELEASE_VERSION }}
file: ./hosting/single/Dockerfile file: ./hosting/single/Dockerfile
- name: Tag and release Budibase Azure App Service docker image
uses: docker/build-push-action@v2
with:
context: .
push: true
platforms: linux/amd64
build-args: TARGETBUILD=aas
tags: budibase/budibase-aas,budibase/budibase-aas:v${{ env.RELEASE_VERSION }}
file: ./hosting/single/Dockerfile
build-arm64:
name: "build-arm64"
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [14.x]
steps:
- name: Fail if not a tag
run: |
if [[ $GITHUB_REF != refs/tags/* ]]; then
echo "Workflow Dispatch can only be run on tags"
exit 1
fi
- name: "Checkout"
uses: actions/checkout@v2
with:
submodules: true
token: ${{ secrets.PERSONAL_ACCESS_TOKEN }}
fetch-depth: 0
- name: Fail if tag is not in master
run: |
if ! git merge-base --is-ancestor ${{ github.sha }} origin/master; then
echo "Tag is not in master. This pipeline can only execute tags that are present on the master branch"
exit 1
fi
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v1
with:
node-version: ${{ matrix.node-version }}
- name: Setup QEMU
uses: docker/setup-qemu-action@v1
- name: Setup Docker Buildx
id: buildx
uses: docker/setup-buildx-action@v1
- name: Run Yarn
run: yarn
- name: Update versions
run: ./scripts/updateVersions.sh
- name: Runt Yarn Lint
run: yarn lint
- name: Update versions
run: ./scripts/updateVersions.sh
- name: Run Yarn Build
run: yarn build:docker:pre
- name: Login to Docker Hub
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_API_KEY }}
- name: Get the latest release version
id: version
run: |
release_version=$(cat lerna.json | jq -r '.version')
echo $release_version
echo "RELEASE_VERSION=$release_version" >> $GITHUB_ENV
- name: Tag and release Budibase service docker image
uses: docker/build-push-action@v2
with:
context: .
push: true
platforms: linux/arm64
tags: budibase/budibase,budibase/budibase:v${{ env.RELEASE_VERSION }}
file: ./hosting/single/Dockerfile
build-aas:
name: "build-aas"
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [14.x]
steps:
- name: Fail if not a tag
run: |
if [[ $GITHUB_REF != refs/tags/* ]]; then
echo "Workflow Dispatch can only be run on tags"
exit 1
fi
- name: "Checkout"
uses: actions/checkout@v2
with:
submodules: true
token: ${{ secrets.PERSONAL_ACCESS_TOKEN }}
fetch-depth: 0
- name: Fail if tag is not in master
run: |
if ! git merge-base --is-ancestor ${{ github.sha }} origin/master; then
echo "Tag is not in master. This pipeline can only execute tags that are present on the master branch"
exit 1
fi
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v1
with:
node-version: ${{ matrix.node-version }}
- name: Setup QEMU
uses: docker/setup-qemu-action@v1
- name: Setup Docker Buildx
id: buildx
uses: docker/setup-buildx-action@v1
- name: Run Yarn
run: yarn
- name: Update versions
run: ./scripts/updateVersions.sh
- name: Runt Yarn Lint
run: yarn lint
- name: Update versions
run: ./scripts/updateVersions.sh
- name: Run Yarn Build
run: yarn build:docker:pre
- name: Login to Docker Hub
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_API_KEY }}
- name: Get the latest release version
id: version
run: |
release_version=$(cat lerna.json | jq -r '.version')
echo $release_version
echo "RELEASE_VERSION=$release_version" >> $GITHUB_ENV
- name: Tag and release Budibase Azure App Service docker image - name: Tag and release Budibase Azure App Service docker image
uses: docker/build-push-action@v2 uses: docker/build-push-action@v2
with: with:

2
.nvmrc
View File

@ -1 +1 @@
v14.20.1 v18.17.0

View File

@ -1,3 +1,3 @@
nodejs 14.21.3 nodejs 18.17.0
python 3.10.0 python 3.10.0
yarn 1.22.19 yarn 1.22.19

23
.vscode/launch.json vendored
View File

@ -1,3 +1,4 @@
{ {
// Use IntelliSense to learn about possible attributes. // Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes. // Hover to view descriptions of existing attributes.
@ -8,30 +9,18 @@
"name": "Budibase Server", "name": "Budibase Server",
"type": "node", "type": "node",
"request": "launch", "request": "launch",
"runtimeArgs": [ "runtimeArgs": ["--nolazy", "-r", "ts-node/register/transpile-only"],
"--nolazy", "args": ["${workspaceFolder}/packages/server/src/index.ts"],
"-r",
"ts-node/register/transpile-only"
],
"args": [
"${workspaceFolder}/packages/server/src/index.ts"
],
"cwd": "${workspaceFolder}/packages/server" "cwd": "${workspaceFolder}/packages/server"
}, },
{ {
"name": "Budibase Worker", "name": "Budibase Worker",
"type": "node", "type": "node",
"request": "launch", "request": "launch",
"runtimeArgs": [ "runtimeArgs": ["--nolazy", "-r", "ts-node/register/transpile-only"],
"--nolazy", "args": ["${workspaceFolder}/packages/worker/src/index.ts"],
"-r",
"ts-node/register/transpile-only"
],
"args": [
"${workspaceFolder}/packages/worker/src/index.ts"
],
"cwd": "${workspaceFolder}/packages/worker" "cwd": "${workspaceFolder}/packages/worker"
}, }
], ],
"compounds": [ "compounds": [
{ {

View File

@ -120,6 +120,8 @@ spec:
{{ end }} {{ end }}
- name: MULTI_TENANCY - name: MULTI_TENANCY
value: {{ .Values.globals.multiTenancy | quote }} value: {{ .Values.globals.multiTenancy | quote }}
- name: OFFLINE_MODE
value: {{ .Values.globals.offlineMode | quote }}
- name: LOG_LEVEL - name: LOG_LEVEL
value: {{ .Values.services.apps.logLevel | quote }} value: {{ .Values.services.apps.logLevel | quote }}
- name: REDIS_PASSWORD - name: REDIS_PASSWORD

View File

@ -116,6 +116,8 @@ spec:
value: {{ .Values.services.worker.port | quote }} value: {{ .Values.services.worker.port | quote }}
- name: MULTI_TENANCY - name: MULTI_TENANCY
value: {{ .Values.globals.multiTenancy | quote }} value: {{ .Values.globals.multiTenancy | quote }}
- name: OFFLINE_MODE
value: {{ .Values.globals.offlineMode | quote }}
- name: LOG_LEVEL - name: LOG_LEVEL
value: {{ .Values.services.worker.logLevel | quote }} value: {{ .Values.services.worker.logLevel | quote }}
- name: REDIS_PASSWORD - name: REDIS_PASSWORD

View File

@ -82,6 +82,7 @@ globals:
posthogToken: "phc_bIjZL7oh2GEUd2vqvTBH8WvrX0fWTFQMs6H5KQxiUxU" posthogToken: "phc_bIjZL7oh2GEUd2vqvTBH8WvrX0fWTFQMs6H5KQxiUxU"
selfHosted: "1" # set to 0 for budibase cloud environment, set to 1 for self-hosted setup selfHosted: "1" # set to 0 for budibase cloud environment, set to 1 for self-hosted setup
multiTenancy: "0" # set to 0 to disable multiple orgs, set to 1 to enable multiple orgs multiTenancy: "0" # set to 0 to disable multiple orgs, set to 1 to enable multiple orgs
offlineMode: "0" # set to 1 to enable offline mode
accountPortalUrl: "" accountPortalUrl: ""
accountPortalApiKey: "" accountPortalApiKey: ""
cookieDomain: "" cookieDomain: ""
@ -136,7 +137,6 @@ services:
path: /health path: /health
port: 10000 port: 10000
scheme: HTTP scheme: HTTP
enabled: true
periodSeconds: 3 periodSeconds: 3
failureThreshold: 1 failureThreshold: 1
livenessProbe: livenessProbe:
@ -169,7 +169,6 @@ services:
path: /health path: /health
port: 4002 port: 4002
scheme: HTTP scheme: HTTP
enabled: true
periodSeconds: 3 periodSeconds: 3
failureThreshold: 1 failureThreshold: 1
livenessProbe: livenessProbe:
@ -203,7 +202,6 @@ services:
path: /health path: /health
port: 4003 port: 4003
scheme: HTTP scheme: HTTP
enabled: true
periodSeconds: 3 periodSeconds: 3
failureThreshold: 1 failureThreshold: 1
livenessProbe: livenessProbe:
@ -410,14 +408,12 @@ couchdb:
## Ref: https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-probes/#configure-probes ## Ref: https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-probes/#configure-probes
# FOR COUCHDB # FOR COUCHDB
livenessProbe: livenessProbe:
enabled: true
failureThreshold: 3 failureThreshold: 3
initialDelaySeconds: 0 initialDelaySeconds: 0
periodSeconds: 10 periodSeconds: 10
successThreshold: 1 successThreshold: 1
timeoutSeconds: 1 timeoutSeconds: 1
readinessProbe: readinessProbe:
enabled: true
failureThreshold: 3 failureThreshold: 3
initialDelaySeconds: 0 initialDelaySeconds: 0
periodSeconds: 10 periodSeconds: 10

View File

@ -90,7 +90,7 @@ Component libraries are collections of components as well as the definition of t
#### 1. Prerequisites #### 1. Prerequisites
- NodeJS version `14.x.x` - NodeJS version `18.x.x`
- Python version `3.x` - Python version `3.x`
### Using asdf (recommended) ### Using asdf (recommended)

View File

@ -5,11 +5,11 @@ ENV COUCHDB_PASSWORD admin
EXPOSE 5984 EXPOSE 5984
RUN apt-get update && apt-get install -y --no-install-recommends software-properties-common wget unzip curl && \ RUN apt-get update && apt-get install -y --no-install-recommends software-properties-common wget unzip curl && \
wget -qO - https://adoptopenjdk.jfrog.io/adoptopenjdk/api/gpg/key/public | apt-key add - && \ wget -O - https://packages.adoptium.net/artifactory/api/gpg/key/public | sudo apt-key add - && \
apt-add-repository 'deb http://security.debian.org/debian-security bullseye-security/updates main' && \ apt-add-repository 'deb http://security.debian.org/debian-security bullseye-security/updates main' && \
apt-add-repository 'deb http://archive.debian.org/debian stretch-backports main' && \ apt-add-repository 'deb http://archive.debian.org/debian stretch-backports main' && \
apt-add-repository --yes https://adoptopenjdk.jfrog.io/adoptopenjdk/deb/ && \ apt-add-repository 'deb https://packages.adoptium.net/artifactory/deb bullseye main' && \
apt-get update && apt-get install -y --no-install-recommends adoptopenjdk-8-hotspot && \ apt-get update && apt-get install -y --no-install-recommends temurin-8-jdk && \
rm -rf /var/lib/apt/lists/ rm -rf /var/lib/apt/lists/
# setup clouseau # setup clouseau

View File

@ -1,47 +0,0 @@
version: "3"
# optional ports are specified throughout for more advanced use cases.
services:
minio-service:
restart: on-failure
# Last version that supports the "fs" backend
image: minio/minio:RELEASE.2022-10-24T18-35-07Z
ports:
- "9000"
- "9001"
environment:
MINIO_ACCESS_KEY: ${MINIO_ACCESS_KEY}
MINIO_SECRET_KEY: ${MINIO_SECRET_KEY}
command: server /data --console-address ":9001"
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:9000/minio/health/live"]
interval: 30s
timeout: 20s
retries: 3
couchdb-service:
# platform: linux/amd64
restart: on-failure
image: budibase/couchdb
environment:
- COUCHDB_PASSWORD=${COUCH_DB_PASSWORD}
- COUCHDB_USER=${COUCH_DB_USER}
ports:
- "5984"
- "4369"
- "9100"
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:5984/_up"]
interval: 30s
timeout: 20s
retries: 3
redis-service:
restart: on-failure
image: redis
command: redis-server --requirepass ${REDIS_PASSWORD}
ports:
- "6379"
healthcheck:
test: ["CMD", "redis-cli", "ping"]

View File

@ -27,6 +27,7 @@ services:
BB_ADMIN_USER_EMAIL: ${BB_ADMIN_USER_EMAIL} BB_ADMIN_USER_EMAIL: ${BB_ADMIN_USER_EMAIL}
BB_ADMIN_USER_PASSWORD: ${BB_ADMIN_USER_PASSWORD} BB_ADMIN_USER_PASSWORD: ${BB_ADMIN_USER_PASSWORD}
PLUGINS_DIR: ${PLUGINS_DIR} PLUGINS_DIR: ${PLUGINS_DIR}
OFFLINE_MODE: ${OFFLINE_MODE}
depends_on: depends_on:
- worker-service - worker-service
- redis-service - redis-service
@ -54,6 +55,7 @@ services:
INTERNAL_API_KEY: ${INTERNAL_API_KEY} INTERNAL_API_KEY: ${INTERNAL_API_KEY}
REDIS_URL: redis-service:6379 REDIS_URL: redis-service:6379
REDIS_PASSWORD: ${REDIS_PASSWORD} REDIS_PASSWORD: ${REDIS_PASSWORD}
OFFLINE_MODE: ${OFFLINE_MODE}
depends_on: depends_on:
- redis-service - redis-service
- minio-service - minio-service

View File

@ -1,7 +1,7 @@
FROM node:14-slim as build FROM node:18-slim as build
# install node-gyp dependencies # install node-gyp dependencies
RUN apt-get update && apt-get upgrade -y && apt-get install -y --no-install-recommends apt-utils cron g++ make python RUN apt-get update && apt-get upgrade -y && apt-get install -y --no-install-recommends apt-utils cron g++ make python3
# add pin script # add pin script
WORKDIR / WORKDIR /

View File

@ -1,9 +1,16 @@
module.exports = () => { module.exports = () => {
return { return {
dockerCompose: { couchdb: {
composeFilePath: "../../hosting", image: "budibase/couchdb",
composeFile: "docker-compose.test.yaml", ports: [5984],
startupTimeout: 10000, env: {
COUCHDB_PASSWORD: "budibase",
COUCHDB_USER: "budibase",
}, },
wait: {
type: "ports",
timeout: 20000,
}
}
} }
} }

View File

@ -1,5 +1,5 @@
{ {
"version": "2.8.29-alpha.15", "version": "2.9.33-alpha.5",
"npmClient": "yarn", "npmClient": "yarn",
"packages": [ "packages": [
"packages/*" "packages/*"

View File

@ -34,7 +34,7 @@
"preinstall": "node scripts/syncProPackage.js", "preinstall": "node scripts/syncProPackage.js",
"setup": "git config submodule.recurse true && git submodule update && node ./hosting/scripts/setup.js && yarn && yarn build && yarn dev", "setup": "git config submodule.recurse true && git submodule update && node ./hosting/scripts/setup.js && yarn && yarn build && yarn dev",
"bootstrap": "./scripts/link-dependencies.sh && echo '***BOOTSTRAP ONLY REQUIRED FOR USE WITH ACCOUNT PORTAL***'", "bootstrap": "./scripts/link-dependencies.sh && echo '***BOOTSTRAP ONLY REQUIRED FOR USE WITH ACCOUNT PORTAL***'",
"build": "yarn nx run-many -t=build", "build": "lerna run build --stream",
"build:dev": "lerna run --stream prebuild && yarn nx run-many --target=build --output-style=dynamic --watch --preserveWatchOutput", "build:dev": "lerna run --stream prebuild && yarn nx run-many --target=build --output-style=dynamic --watch --preserveWatchOutput",
"check:types": "lerna run check:types", "check:types": "lerna run check:types",
"backend:bootstrap": "./scripts/scopeBackend.sh && yarn run bootstrap", "backend:bootstrap": "./scripts/scopeBackend.sh && yarn run bootstrap",
@ -109,7 +109,7 @@
"@budibase/types": "0.0.0" "@budibase/types": "0.0.0"
}, },
"engines": { "engines": {
"node": ">=14.0.0 <15.0.0" "node": ">=18.0.0 <19.0.0"
}, },
"dependencies": {} "dependencies": {}
} }

View File

@ -0,0 +1,4 @@
*
!dist/**/*
dist/tsconfig.build.tsbuildinfo
!package.json

View File

@ -2,11 +2,11 @@
"name": "@budibase/backend-core", "name": "@budibase/backend-core",
"version": "0.0.0", "version": "0.0.0",
"description": "Budibase backend core libraries used in server and worker", "description": "Budibase backend core libraries used in server and worker",
"main": "dist/src/index.js", "main": "dist/index.js",
"types": "dist/src/index.d.ts", "types": "dist/src/index.d.ts",
"exports": { "exports": {
".": "./dist/src/index.js", ".": "./dist/index.js",
"./tests": "./dist/tests/index.js", "./tests": "./dist/tests.js",
"./*": "./dist/*.js" "./*": "./dist/*.js"
}, },
"author": "Budibase", "author": "Budibase",
@ -14,7 +14,7 @@
"scripts": { "scripts": {
"prebuild": "rimraf dist/", "prebuild": "rimraf dist/",
"prepack": "cp package.json dist", "prepack": "cp package.json dist",
"build": "tsc -p tsconfig.build.json", "build": "node ./scripts/build.js && tsc -p tsconfig.build.json --emitDeclarationOnly --paths null",
"build:dev": "yarn prebuild && tsc --build --watch --preserveWatchOutput", "build:dev": "yarn prebuild && tsc --build --watch --preserveWatchOutput",
"check:types": "tsc -p tsconfig.json --noEmit --paths null", "check:types": "tsc -p tsconfig.json --noEmit --paths null",
"test": "bash scripts/test.sh", "test": "bash scripts/test.sh",
@ -88,5 +88,20 @@
"ts-node": "10.8.1", "ts-node": "10.8.1",
"tsconfig-paths": "4.0.0", "tsconfig-paths": "4.0.0",
"typescript": "4.7.3" "typescript": "4.7.3"
},
"nx": {
"targets": {
"build": {
"dependsOn": [
{
"projects": [
"@budibase/shared-core",
"@budibase/types"
],
"target": "build"
}
]
}
}
} }
} }

View File

@ -1 +0,0 @@
export * from "./src/plugin"

View File

@ -0,0 +1,6 @@
#!/usr/bin/node
const coreBuild = require("../../../scripts/build")
coreBuild("./src/plugin/index.ts", "./dist/plugins.js")
coreBuild("./src/index.ts", "./dist/index.js")
coreBuild("./tests/index.ts", "./dist/tests.js")

View File

@ -4,6 +4,8 @@ import * as context from "../context"
import * as platform from "../platform" import * as platform from "../platform"
import env from "../environment" import env from "../environment"
import * as accounts from "../accounts" import * as accounts from "../accounts"
import { UserDB } from "../users"
import { sdk } from "@budibase/shared-core"
const EXPIRY_SECONDS = 3600 const EXPIRY_SECONDS = 3600
@ -60,6 +62,18 @@ export async function getUser(
// make sure the tenant ID is always correct/set // make sure the tenant ID is always correct/set
user.tenantId = tenantId user.tenantId = tenantId
} }
// if has groups, could have builder permissions granted by a group
if (user.userGroups && !sdk.users.isGlobalBuilder(user)) {
await context.doInTenant(tenantId, async () => {
const appIds = await UserDB.getGroupBuilderAppIds(user)
if (appIds.length) {
const existing = user.builder?.apps || []
user.builder = {
apps: [...new Set(existing.concat(appIds))],
}
}
})
}
return user return user
} }

View File

@ -8,6 +8,7 @@ import {
DatabasePutOpts, DatabasePutOpts,
DatabaseCreateIndexOpts, DatabaseCreateIndexOpts,
DatabaseDeleteIndexOpts, DatabaseDeleteIndexOpts,
DocExistsResponse,
Document, Document,
isDocument, isDocument,
} from "@budibase/types" } from "@budibase/types"
@ -120,6 +121,19 @@ export class DatabaseImpl implements Database {
return this.updateOutput(() => db.get(id)) return this.updateOutput(() => db.get(id))
} }
async docExists(docId: string): Promise<DocExistsResponse> {
const db = await this.checkSetup()
let _rev, exists
try {
const { etag } = await db.head(docId)
_rev = etag
exists = true
} catch (err) {
exists = false
}
return { _rev, exists }
}
async remove(idOrDoc: string | Document, rev?: string) { async remove(idOrDoc: string | Document, rev?: string) {
const db = await this.checkSetup() const db = await this.checkSetup()
let _id: string let _id: string

View File

@ -1,7 +1,6 @@
import fetch from "node-fetch" import fetch from "node-fetch"
import { getCouchInfo } from "./couch" import { getCouchInfo } from "./couch"
import { SearchFilters, Row } from "@budibase/types" import { SearchFilters, Row, EmptyFilterOption } from "@budibase/types"
import { createUserIndex } from "./searchIndexes/searchIndexes"
const QUERY_START_REGEX = /\d[0-9]*:/g const QUERY_START_REGEX = /\d[0-9]*:/g
@ -65,6 +64,7 @@ export class QueryBuilder<T> {
this.#index = index this.#index = index
this.#query = { this.#query = {
allOr: false, allOr: false,
onEmptyFilter: EmptyFilterOption.RETURN_ALL,
string: {}, string: {},
fuzzy: {}, fuzzy: {},
range: {}, range: {},
@ -218,6 +218,10 @@ export class QueryBuilder<T> {
this.#query.allOr = true this.#query.allOr = true
} }
setOnEmptyFilter(value: EmptyFilterOption) {
this.#query.onEmptyFilter = value
}
handleSpaces(input: string) { handleSpaces(input: string) {
if (this.#noEscaping) { if (this.#noEscaping) {
return input return input
@ -289,8 +293,9 @@ export class QueryBuilder<T> {
const builder = this const builder = this
let allOr = this.#query && this.#query.allOr let allOr = this.#query && this.#query.allOr
let query = allOr ? "" : "*:*" let query = allOr ? "" : "*:*"
let allFiltersEmpty = true
const allPreProcessingOpts = { escape: true, lowercase: true, wrap: true } const allPreProcessingOpts = { escape: true, lowercase: true, wrap: true }
let tableId let tableId: string = ""
if (this.#query.equal!.tableId) { if (this.#query.equal!.tableId) {
tableId = this.#query.equal!.tableId tableId = this.#query.equal!.tableId
delete this.#query.equal!.tableId delete this.#query.equal!.tableId
@ -305,7 +310,7 @@ export class QueryBuilder<T> {
} }
const contains = (key: string, value: any, mode = "AND") => { const contains = (key: string, value: any, mode = "AND") => {
if (Array.isArray(value) && value.length === 0) { if (!value || (Array.isArray(value) && value.length === 0)) {
return null return null
} }
if (!Array.isArray(value)) { if (!Array.isArray(value)) {
@ -384,6 +389,12 @@ export class QueryBuilder<T> {
built += ` ${mode} ` built += ` ${mode} `
} }
built += expression built += expression
if (
(typeof value !== "string" && value != null) ||
(typeof value === "string" && value !== tableId && value !== "")
) {
allFiltersEmpty = false
}
} }
if (opts?.returnBuilt) { if (opts?.returnBuilt) {
return built return built
@ -463,6 +474,13 @@ export class QueryBuilder<T> {
allOr = false allOr = false
build({ tableId }, equal) build({ tableId }, equal)
} }
if (allFiltersEmpty) {
if (this.#query.onEmptyFilter === EmptyFilterOption.RETURN_NONE) {
return ""
} else if (this.#query?.allOr) {
return query.replace("()", "(*:*)")
}
}
return query return query
} }

View File

@ -1,6 +1,6 @@
import { newid } from "../../docIds/newid" import { newid } from "../../docIds/newid"
import { getDB } from "../db" import { getDB } from "../db"
import { Database } from "@budibase/types" import { Database, EmptyFilterOption } from "@budibase/types"
import { QueryBuilder, paginatedSearch, fullSearch } from "../lucene" import { QueryBuilder, paginatedSearch, fullSearch } from "../lucene"
const INDEX_NAME = "main" const INDEX_NAME = "main"
@ -156,6 +156,76 @@ describe("lucene", () => {
expect(resp.rows.length).toBe(2) expect(resp.rows.length).toBe(2)
}) })
describe("empty filters behaviour", () => {
it("should return all rows by default", async () => {
const builder = new QueryBuilder(dbName, INDEX_NAME)
builder.addEqual("property", "")
builder.addEqual("number", null)
builder.addString("property", "")
builder.addFuzzy("property", "")
builder.addNotEqual("number", undefined)
builder.addOneOf("number", null)
builder.addContains("array", undefined)
builder.addNotContains("array", null)
builder.addContainsAny("array", null)
const resp = await builder.run()
expect(resp.rows.length).toBe(3)
})
it("should return all rows when onEmptyFilter is ALL", async () => {
const builder = new QueryBuilder(dbName, INDEX_NAME)
builder.setOnEmptyFilter(EmptyFilterOption.RETURN_ALL)
builder.setAllOr()
builder.addEqual("property", "")
builder.addEqual("number", null)
builder.addString("property", "")
builder.addFuzzy("property", "")
builder.addNotEqual("number", undefined)
builder.addOneOf("number", null)
builder.addContains("array", undefined)
builder.addNotContains("array", null)
builder.addContainsAny("array", null)
const resp = await builder.run()
expect(resp.rows.length).toBe(3)
})
it("should return no rows when onEmptyFilter is NONE", async () => {
const builder = new QueryBuilder(dbName, INDEX_NAME)
builder.setOnEmptyFilter(EmptyFilterOption.RETURN_NONE)
builder.addEqual("property", "")
builder.addEqual("number", null)
builder.addString("property", "")
builder.addFuzzy("property", "")
builder.addNotEqual("number", undefined)
builder.addOneOf("number", null)
builder.addContains("array", undefined)
builder.addNotContains("array", null)
builder.addContainsAny("array", null)
const resp = await builder.run()
expect(resp.rows.length).toBe(0)
})
it("should return all matching rows when onEmptyFilter is NONE, but a filter value is provided", async () => {
const builder = new QueryBuilder(dbName, INDEX_NAME)
builder.setOnEmptyFilter(EmptyFilterOption.RETURN_NONE)
builder.addEqual("property", "")
builder.addEqual("number", 1)
builder.addString("property", "")
builder.addFuzzy("property", "")
builder.addNotEqual("number", undefined)
builder.addOneOf("number", null)
builder.addContains("array", undefined)
builder.addNotContains("array", null)
builder.addContainsAny("array", null)
const resp = await builder.run()
expect(resp.rows.length).toBe(1)
})
})
describe("skip", () => { describe("skip", () => {
const skipDbName = `db-${newid()}` const skipDbName = `db-${newid()}`
let docs: { let docs: {

View File

@ -1,5 +1,6 @@
import env from "../environment" import env from "../environment"
import * as context from "../context" import * as context from "../context"
export * from "./installation"
/** /**
* Read the TENANT_FEATURE_FLAGS env var and return an array of features flags for each tenant. * Read the TENANT_FEATURE_FLAGS env var and return an array of features flags for each tenant.

View File

@ -0,0 +1,17 @@
export function processFeatureEnvVar<T>(
fullList: string[],
featureList?: string
) {
let list
if (!featureList) {
list = fullList
} else {
list = featureList.split(",")
}
for (let feature of list) {
if (!fullList.includes(feature)) {
throw new Error(`Feature: ${feature} is not an allowed option`)
}
}
return list as unknown as T[]
}

View File

@ -6,7 +6,8 @@ export * as roles from "./security/roles"
export * as permissions from "./security/permissions" export * as permissions from "./security/permissions"
export * as accounts from "./accounts" export * as accounts from "./accounts"
export * as installation from "./installation" export * as installation from "./installation"
export * as featureFlags from "./featureFlags" export * as featureFlags from "./features"
export * as features from "./features/installation"
export * as sessions from "./security/sessions" export * as sessions from "./security/sessions"
export * as platform from "./platform" export * as platform from "./platform"
export * as auth from "./auth" export * as auth from "./auth"

View File

@ -5,7 +5,8 @@ import env from "../environment"
export default async (ctx: UserCtx, next: any) => { export default async (ctx: UserCtx, next: any) => {
const appId = getAppId() const appId = getAppId()
const builderFn = env.isWorker() const builderFn =
env.isWorker() || !appId
? hasBuilderPermissions ? hasBuilderPermissions
: env.isApps() : env.isApps()
? isBuilder ? isBuilder

View File

@ -5,7 +5,8 @@ import env from "../environment"
export default async (ctx: UserCtx, next: any) => { export default async (ctx: UserCtx, next: any) => {
const appId = getAppId() const appId = getAppId()
const builderFn = env.isWorker() const builderFn =
env.isWorker() || !appId
? hasBuilderPermissions ? hasBuilderPermissions
: env.isApps() : env.isApps()
? isBuilder ? isBuilder

View File

@ -78,7 +78,6 @@ export const BUILTIN_PERMISSIONS = {
permissions: [ permissions: [
new Permission(PermissionType.QUERY, PermissionLevel.READ), new Permission(PermissionType.QUERY, PermissionLevel.READ),
new Permission(PermissionType.TABLE, PermissionLevel.READ), new Permission(PermissionType.TABLE, PermissionLevel.READ),
new Permission(PermissionType.VIEW, PermissionLevel.READ),
], ],
}, },
WRITE: { WRITE: {
@ -87,7 +86,6 @@ export const BUILTIN_PERMISSIONS = {
permissions: [ permissions: [
new Permission(PermissionType.QUERY, PermissionLevel.WRITE), new Permission(PermissionType.QUERY, PermissionLevel.WRITE),
new Permission(PermissionType.TABLE, PermissionLevel.WRITE), new Permission(PermissionType.TABLE, PermissionLevel.WRITE),
new Permission(PermissionType.VIEW, PermissionLevel.READ),
new Permission(PermissionType.AUTOMATION, PermissionLevel.EXECUTE), new Permission(PermissionType.AUTOMATION, PermissionLevel.EXECUTE),
], ],
}, },
@ -98,7 +96,6 @@ export const BUILTIN_PERMISSIONS = {
new Permission(PermissionType.TABLE, PermissionLevel.WRITE), new Permission(PermissionType.TABLE, PermissionLevel.WRITE),
new Permission(PermissionType.USER, PermissionLevel.READ), new Permission(PermissionType.USER, PermissionLevel.READ),
new Permission(PermissionType.AUTOMATION, PermissionLevel.EXECUTE), new Permission(PermissionType.AUTOMATION, PermissionLevel.EXECUTE),
new Permission(PermissionType.VIEW, PermissionLevel.READ),
new Permission(PermissionType.WEBHOOK, PermissionLevel.READ), new Permission(PermissionType.WEBHOOK, PermissionLevel.READ),
], ],
}, },
@ -109,7 +106,6 @@ export const BUILTIN_PERMISSIONS = {
new Permission(PermissionType.TABLE, PermissionLevel.ADMIN), new Permission(PermissionType.TABLE, PermissionLevel.ADMIN),
new Permission(PermissionType.USER, PermissionLevel.ADMIN), new Permission(PermissionType.USER, PermissionLevel.ADMIN),
new Permission(PermissionType.AUTOMATION, PermissionLevel.ADMIN), new Permission(PermissionType.AUTOMATION, PermissionLevel.ADMIN),
new Permission(PermissionType.VIEW, PermissionLevel.ADMIN),
new Permission(PermissionType.WEBHOOK, PermissionLevel.READ), new Permission(PermissionType.WEBHOOK, PermissionLevel.READ),
new Permission(PermissionType.QUERY, PermissionLevel.ADMIN), new Permission(PermissionType.QUERY, PermissionLevel.ADMIN),
], ],

View File

@ -1,30 +1,32 @@
import env from "../environment" import env from "../environment"
import * as eventHelpers from "./events" import * as eventHelpers from "./events"
import * as accounts from "../accounts" import * as accounts from "../accounts"
import * as accountSdk from "../accounts"
import * as cache from "../cache" import * as cache from "../cache"
import { getIdentity, getTenantId, getGlobalDB } from "../context" import { getGlobalDB, getIdentity, getTenantId } from "../context"
import * as dbUtils from "../db" import * as dbUtils from "../db"
import { EmailUnavailableError, HTTPError } from "../errors" import { EmailUnavailableError, HTTPError } from "../errors"
import * as platform from "../platform" import * as platform from "../platform"
import * as sessions from "../security/sessions" import * as sessions from "../security/sessions"
import * as usersCore from "./users" import * as usersCore from "./users"
import { import {
Account,
AllDocsResponse, AllDocsResponse,
BulkUserCreated, BulkUserCreated,
BulkUserDeleted, BulkUserDeleted,
isSSOAccount,
isSSOUser,
RowResponse, RowResponse,
SaveUserOpts, SaveUserOpts,
User, User,
Account,
isSSOUser,
isSSOAccount,
UserStatus, UserStatus,
UserGroup,
ContextUser,
} from "@budibase/types" } from "@budibase/types"
import * as accountSdk from "../accounts"
import { import {
validateUniqueUser,
getAccountHolderFromUserIds, getAccountHolderFromUserIds,
isAdmin, isAdmin,
validateUniqueUser,
} from "./utils" } from "./utils"
import { searchExistingEmails } from "./lookup" import { searchExistingEmails } from "./lookup"
import { hash } from "../utils" import { hash } from "../utils"
@ -32,8 +34,14 @@ import { hash } from "../utils"
type QuotaUpdateFn = (change: number, cb?: () => Promise<any>) => Promise<any> type QuotaUpdateFn = (change: number, cb?: () => Promise<any>) => Promise<any>
type GroupUpdateFn = (groupId: string, userIds: string[]) => Promise<any> type GroupUpdateFn = (groupId: string, userIds: string[]) => Promise<any>
type FeatureFn = () => Promise<Boolean> type FeatureFn = () => Promise<Boolean>
type GroupGetFn = (ids: string[]) => Promise<UserGroup[]>
type GroupBuildersFn = (user: User) => Promise<string[]>
type QuotaFns = { addUsers: QuotaUpdateFn; removeUsers: QuotaUpdateFn } type QuotaFns = { addUsers: QuotaUpdateFn; removeUsers: QuotaUpdateFn }
type GroupFns = { addUsers: GroupUpdateFn } type GroupFns = {
addUsers: GroupUpdateFn
getBulk: GroupGetFn
getGroupBuilderAppIds: GroupBuildersFn
}
type FeatureFns = { isSSOEnforced: FeatureFn; isAppBuildersEnabled: FeatureFn } type FeatureFns = { isSSOEnforced: FeatureFn; isAppBuildersEnabled: FeatureFn }
const bulkDeleteProcessing = async (dbUser: User) => { const bulkDeleteProcessing = async (dbUser: User) => {
@ -179,6 +187,14 @@ export class UserDB {
return user return user
} }
static async bulkGet(userIds: string[]) {
return await usersCore.bulkGetGlobalUsersById(userIds)
}
static async bulkUpdate(users: User[]) {
return await usersCore.bulkUpdateGlobalUsers(users)
}
static async save(user: User, opts: SaveUserOpts = {}): Promise<User> { static async save(user: User, opts: SaveUserOpts = {}): Promise<User> {
// default booleans to true // default booleans to true
if (opts.hashPassword == null) { if (opts.hashPassword == null) {
@ -457,4 +473,12 @@ export class UserDB {
await cache.user.invalidateUser(userId) await cache.user.invalidateUser(userId)
await sessions.invalidateSessions(userId, { reason: "deletion" }) await sessions.invalidateSessions(userId, { reason: "deletion" })
} }
static async getGroups(groupIds: string[]) {
return await this.groups.getBulk(groupIds)
}
static async getGroupBuilderAppIds(user: User) {
return await this.groups.getGroupBuilderAppIds(user)
}
} }

View File

@ -86,6 +86,10 @@ export const useAuditLogs = () => {
return useFeature(Feature.AUDIT_LOGS) return useFeature(Feature.AUDIT_LOGS)
} }
export const usePublicApiUserRoles = () => {
return useFeature(Feature.USER_ROLE_PUBLIC_API)
}
export const useScimIntegration = () => { export const useScimIntegration = () => {
return useFeature(Feature.SCIM) return useFeature(Feature.SCIM)
} }
@ -98,6 +102,10 @@ export const useAppBuilders = () => {
return useFeature(Feature.APP_BUILDERS) return useFeature(Feature.APP_BUILDERS)
} }
export const useViewPermissions = () => {
return useFeature(Feature.VIEW_PERMISSIONS)
}
// QUOTAS // QUOTAS
export const setAutomationLogsQuota = (value: number) => { export const setAutomationLogsQuota = (value: number) => {

View File

@ -32,8 +32,8 @@ function getTestContainerSettings(
): string | null { ): string | null {
const entry = Object.entries(global).find( const entry = Object.entries(global).find(
([k]) => ([k]) =>
k.includes(`_${serverName.toUpperCase()}`) && k.includes(`${serverName.toUpperCase()}`) &&
k.includes(`_${key.toUpperCase()}__`) k.includes(`${key.toUpperCase()}`)
) )
if (!entry) { if (!entry) {
return null return null
@ -67,27 +67,14 @@ function getContainerInfo(containerName: string, port: number) {
} }
function getCouchConfig() { function getCouchConfig() {
return getContainerInfo("couchdb-service", 5984) return getContainerInfo("couchdb", 5984)
}
function getMinioConfig() {
return getContainerInfo("minio-service", 9000)
}
function getRedisConfig() {
return getContainerInfo("redis-service", 6379)
} }
export function setupEnv(...envs: any[]) { export function setupEnv(...envs: any[]) {
const couch = getCouchConfig(), const couch = getCouchConfig()
minio = getCouchConfig(),
redis = getRedisConfig()
const configs = [ const configs = [
{ key: "COUCH_DB_PORT", value: couch.port }, { key: "COUCH_DB_PORT", value: couch.port },
{ key: "COUCH_DB_URL", value: couch.url }, { key: "COUCH_DB_URL", value: couch.url },
{ key: "MINIO_PORT", value: minio.port },
{ key: "MINIO_URL", value: minio.url },
{ key: "REDIS_URL", value: redis.url },
] ]
for (const config of configs.filter(x => !!x.value)) { for (const config of configs.filter(x => !!x.value)) {

View File

@ -12,7 +12,11 @@
"declaration": true, "declaration": true,
"types": ["node", "jest"], "types": ["node", "jest"],
"outDir": "dist", "outDir": "dist",
"skipLibCheck": true "skipLibCheck": true,
"paths": {
"@budibase/types": ["../types/src"],
"@budibase/shared-core": ["../shared-core/src"]
}
}, },
"include": ["**/*.js", "**/*.ts"], "include": ["**/*.js", "**/*.ts"],
"exclude": [ "exclude": [

View File

@ -1,12 +1,4 @@
{ {
"extends": "./tsconfig.build.json", "extends": "./tsconfig.build.json",
"compilerOptions": {
"composite": true,
"baseUrl": ".",
"paths": {
"@budibase/types": ["../types/src"],
"@budibase/shared-core": ["../shared-core/src"]
}
},
"exclude": ["node_modules", "dist"] "exclude": ["node_modules", "dist"]
} }

View File

@ -98,8 +98,7 @@
{ {
"projects": [ "projects": [
"@budibase/string-templates", "@budibase/string-templates",
"@budibase/shared-core", "@budibase/shared-core"
"@budibase/types"
], ],
"target": "build" "target": "build"
} }

View File

@ -17,6 +17,7 @@ export default function positionDropdown(element, opts) {
maxWidth, maxWidth,
useAnchorWidth, useAnchorWidth,
offset = 5, offset = 5,
customUpdate,
} = opts } = opts
if (!anchor) { if (!anchor) {
return return
@ -33,10 +34,16 @@ export default function positionDropdown(element, opts) {
top: null, top: null,
} }
if (typeof customUpdate === "function") {
styles = customUpdate(anchorBounds, elementBounds, styles)
} else {
// Determine vertical styles // Determine vertical styles
if (align === "right-outside") { if (align === "right-outside") {
styles.top = anchorBounds.top styles.top = anchorBounds.top
} else if (window.innerHeight - anchorBounds.bottom < 100) { } else if (
window.innerHeight - anchorBounds.bottom <
(maxHeight || 100)
) {
styles.top = anchorBounds.top - elementBounds.height - offset styles.top = anchorBounds.top - elementBounds.height - offset
styles.maxHeight = maxHeight || 240 styles.maxHeight = maxHeight || 240
} else { } else {
@ -53,7 +60,8 @@ export default function positionDropdown(element, opts) {
styles.minWidth = anchorBounds.width styles.minWidth = anchorBounds.width
} }
if (align === "right") { if (align === "right") {
styles.left = anchorBounds.left + anchorBounds.width - elementBounds.width styles.left =
anchorBounds.left + anchorBounds.width - elementBounds.width
} else if (align === "right-outside") { } else if (align === "right-outside") {
styles.left = anchorBounds.right + offset styles.left = anchorBounds.right + offset
} else if (align === "left-outside") { } else if (align === "left-outside") {
@ -61,6 +69,7 @@ export default function positionDropdown(element, opts) {
} else { } else {
styles.left = anchorBounds.left styles.left = anchorBounds.left
} }
}
// Apply styles // Apply styles
Object.entries(styles).forEach(([style, value]) => { Object.entries(styles).forEach(([style, value]) => {

View File

@ -1,8 +1,8 @@
<script> <script>
import Popover from "../Popover/Popover.svelte"
import Layout from "../Layout/Layout.svelte"
import { createEventDispatcher } from "svelte" import { createEventDispatcher } from "svelte"
import "@spectrum-css/popover/dist/index-vars.css" import "@spectrum-css/popover/dist/index-vars.css"
import clickOutside from "../Actions/click_outside"
import { fly } from "svelte/transition"
import Icon from "../Icon/Icon.svelte" import Icon from "../Icon/Icon.svelte"
import Input from "../Form/Input.svelte" import Input from "../Form/Input.svelte"
import { capitalise } from "../helpers" import { capitalise } from "../helpers"
@ -10,9 +10,11 @@
export let value export let value
export let size = "M" export let size = "M"
export let spectrumTheme export let spectrumTheme
export let alignRight = false export let offset
export let align
let open = false let dropdown
let preview
$: customValue = getCustomValue(value) $: customValue = getCustomValue(value)
$: checkColor = getCheckColor(value) $: checkColor = getCheckColor(value)
@ -82,7 +84,7 @@
const onChange = value => { const onChange = value => {
dispatch("change", value) dispatch("change", value)
open = false dropdown.hide()
} }
const getCustomValue = value => { const getCustomValue = value => {
@ -119,30 +121,25 @@
return "var(--spectrum-global-color-static-gray-900)" return "var(--spectrum-global-color-static-gray-900)"
} }
const handleOutsideClick = event => {
if (open) {
event.stopPropagation()
open = false
}
}
</script> </script>
<div class="container"> <div
<div class="preview size--{size || 'M'}" on:click={() => (open = true)}> bind:this={preview}
class="preview size--{size || 'M'}"
on:click={() => {
dropdown.toggle()
}}
>
<div <div
class="fill {spectrumTheme || ''}" class="fill {spectrumTheme || ''}"
style={value ? `background: ${value};` : ""} style={value ? `background: ${value};` : ""}
class:placeholder={!value} class:placeholder={!value}
/> />
</div> </div>
{#if open}
<div <Popover bind:this={dropdown} anchor={preview} maxHeight={320} {offset} {align}>
use:clickOutside={handleOutsideClick} <Layout paddingX="XL" paddingY="L">
transition:fly|local={{ y: -20, duration: 200 }} <div class="container">
class="spectrum-Popover spectrum-Popover--bottom spectrum-Picker-popover is-open"
class:spectrum-Popover--align-right={alignRight}
>
{#each categories as category} {#each categories as category}
<div class="category"> <div class="category">
<div class="heading">{category.label}</div> <div class="heading">{category.label}</div>
@ -187,8 +184,8 @@
</div> </div>
</div> </div>
</div> </div>
{/if} </Layout>
</div> </Popover>
<style> <style>
.container { .container {
@ -248,20 +245,6 @@
width: 48px; width: 48px;
height: 48px; height: 48px;
} }
.spectrum-Popover {
width: 210px;
z-index: 999;
top: 100%;
padding: var(--spacing-l) var(--spacing-xl);
display: flex;
flex-direction: column;
justify-content: flex-start;
align-items: stretch;
gap: var(--spacing-xl);
}
.spectrum-Popover--align-right {
right: 0;
}
.colors { .colors {
display: grid; display: grid;
grid-template-columns: 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr; grid-template-columns: 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr;
@ -297,7 +280,11 @@
.category--custom .heading { .category--custom .heading {
margin-bottom: var(--spacing-xs); margin-bottom: var(--spacing-xs);
} }
.container {
display: flex;
flex-direction: column;
gap: var(--spacing-xl);
}
.spectrum-wrapper { .spectrum-wrapper {
background-color: transparent; background-color: transparent;
} }

View File

@ -44,7 +44,9 @@
align-items: stretch; align-items: stretch;
border-bottom: var(--border-light); border-bottom: var(--border-light);
} }
.property-group-container:last-child {
border-bottom: 0px;
}
.property-group-name { .property-group-name {
cursor: pointer; cursor: pointer;
display: flex; display: flex;

View File

@ -4,6 +4,8 @@
import Body from "../Typography/Body.svelte" import Body from "../Typography/Body.svelte"
import Heading from "../Typography/Heading.svelte" import Heading from "../Typography/Heading.svelte"
import { setContext } from "svelte" import { setContext } from "svelte"
import { createEventDispatcher } from "svelte"
import { generate } from "shortid"
export let title export let title
export let fillWidth export let fillWidth
@ -11,13 +13,17 @@
export let width = "calc(100% - 626px)" export let width = "calc(100% - 626px)"
export let headless = false export let headless = false
const dispatch = createEventDispatcher()
let visible = false let visible = false
let drawerId = generate()
export function show() { export function show() {
if (visible) { if (visible) {
return return
} }
visible = true visible = true
dispatch("drawerShow", drawerId)
} }
export function hide() { export function hide() {
@ -25,6 +31,7 @@
return return
} }
visible = false visible = false
dispatch("drawerHide", drawerId)
} }
setContext("drawer-actions", { setContext("drawer-actions", {

View File

@ -2,8 +2,8 @@
import "@spectrum-css/inputgroup/dist/index-vars.css" import "@spectrum-css/inputgroup/dist/index-vars.css"
import "@spectrum-css/popover/dist/index-vars.css" import "@spectrum-css/popover/dist/index-vars.css"
import "@spectrum-css/menu/dist/index-vars.css" import "@spectrum-css/menu/dist/index-vars.css"
import { fly } from "svelte/transition"
import { createEventDispatcher } from "svelte" import { createEventDispatcher } from "svelte"
import clickOutside from "../../Actions/click_outside"
export let value = null export let value = null
export let id = null export let id = null
@ -80,10 +80,11 @@
</svg> </svg>
</button> </button>
{#if open} {#if open}
<div class="overlay" on:mousedown|self={() => (open = false)} />
<div <div
transition:fly|local={{ y: -20, duration: 200 }}
class="spectrum-Popover spectrum-Popover--bottom is-open" class="spectrum-Popover spectrum-Popover--bottom is-open"
use:clickOutside={() => {
open = false
}}
> >
<ul class="spectrum-Menu" role="listbox"> <ul class="spectrum-Menu" role="listbox">
{#if options && Array.isArray(options)} {#if options && Array.isArray(options)}
@ -125,14 +126,6 @@
.spectrum-Textfield-input { .spectrum-Textfield-input {
width: 0; width: 0;
} }
.overlay {
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
z-index: 999;
}
.spectrum-Popover { .spectrum-Popover {
max-height: 240px; max-height: 240px;
width: 100%; width: 100%;

View File

@ -23,6 +23,10 @@
export let animate = true export let animate = true
export let customZindex export let customZindex
export let handlePostionUpdate
export let showPopover = true
export let clickOutsideOverride = false
$: target = portalTarget || getContext(Context.PopoverRoot) || ".spectrum" $: target = portalTarget || getContext(Context.PopoverRoot) || ".spectrum"
export const show = () => { export const show = () => {
@ -35,7 +39,18 @@
open = false open = false
} }
export const toggle = () => {
if (!open) {
show()
} else {
hide()
}
}
const handleOutsideClick = e => { const handleOutsideClick = e => {
if (clickOutsideOverride) {
return
}
if (open) { if (open) {
// Stop propagation if the source is the anchor // Stop propagation if the source is the anchor
let node = e.target let node = e.target
@ -54,6 +69,9 @@
} }
function handleEscape(e) { function handleEscape(e) {
if (!clickOutsideOverride) {
return
}
if (open && e.key === "Escape") { if (open && e.key === "Escape") {
hide() hide()
} }
@ -71,6 +89,7 @@
maxWidth, maxWidth,
useAnchorWidth, useAnchorWidth,
offset, offset,
customUpdate: handlePostionUpdate,
}} }}
use:clickOutside={{ use:clickOutside={{
callback: dismissible ? handleOutsideClick : () => {}, callback: dismissible ? handleOutsideClick : () => {},
@ -79,6 +98,7 @@
on:keydown={handleEscape} on:keydown={handleEscape}
class="spectrum-Popover is-open" class="spectrum-Popover is-open"
class:customZindex class:customZindex
class:hide-popover={open && !showPopover}
role="presentation" role="presentation"
style="height: {customHeight}; --customZindex: {customZindex};" style="height: {customHeight}; --customZindex: {customZindex};"
transition:fly|local={{ y: -20, duration: animate ? 200 : 0 }} transition:fly|local={{ y: -20, duration: animate ? 200 : 0 }}
@ -89,6 +109,10 @@
{/if} {/if}
<style> <style>
.hide-popover {
display: contents;
}
.spectrum-Popover { .spectrum-Popover {
min-width: var(--spectrum-global-dimension-size-2000); min-width: var(--spectrum-global-dimension-size-2000);
border-color: var(--spectrum-global-color-gray-300); border-color: var(--spectrum-global-color-gray-300);

View File

@ -215,7 +215,7 @@
const nameA = getDisplayName(a) const nameA = getDisplayName(a)
const nameB = getDisplayName(b) const nameB = getDisplayName(b)
if (orderA !== orderB) { if (orderA !== orderB) {
return orderA < orderB ? orderA : orderB return orderA < orderB ? a : b
} }
return nameA < nameB ? a : b return nameA < nameB ? a : b
}) })

View File

@ -133,9 +133,7 @@
"dependsOn": [ "dependsOn": [
{ {
"projects": [ "projects": [
"@budibase/shared-core", "@budibase/string-templates"
"@budibase/string-templates",
"@budibase/types"
], ],
"target": "build" "target": "build"
} }
@ -145,9 +143,7 @@
"dependsOn": [ "dependsOn": [
{ {
"projects": [ "projects": [
"@budibase/shared-core", "@budibase/string-templates"
"@budibase/string-templates",
"@budibase/types"
], ],
"target": "build" "target": "build"
} }
@ -157,9 +153,7 @@
"dependsOn": [ "dependsOn": [
{ {
"projects": [ "projects": [
"@budibase/shared-core", "@budibase/string-templates"
"@budibase/string-templates",
"@budibase/types"
], ],
"target": "build" "target": "build"
} }

View File

@ -6,7 +6,7 @@ import {
findComponentPath, findComponentPath,
getComponentSettings, getComponentSettings,
} from "./componentUtils" } from "./componentUtils"
import { store } from "builderStore" import { store, currentAsset } from "builderStore"
import { import {
queries as queriesStores, queries as queriesStores,
tables as tablesStore, tables as tablesStore,
@ -22,6 +22,7 @@ import { TableNames } from "../constants"
import { JSONUtils } from "@budibase/frontend-core" import { JSONUtils } from "@budibase/frontend-core"
import ActionDefinitions from "components/design/settings/controls/ButtonActionEditor/manifest.json" import ActionDefinitions from "components/design/settings/controls/ButtonActionEditor/manifest.json"
import { environment, licensing } from "stores/portal" import { environment, licensing } from "stores/portal"
import { convertOldFieldFormat } from "components/design/settings/controls/FieldConfiguration/utils"
// Regex to match all instances of template strings // Regex to match all instances of template strings
const CAPTURE_VAR_INSIDE_TEMPLATE = /{{([^}]+)}}/g const CAPTURE_VAR_INSIDE_TEMPLATE = /{{([^}]+)}}/g
@ -328,7 +329,7 @@ const getProviderContextBindings = (asset, dataProviders) => {
if (context.type === "form") { if (context.type === "form") {
// Forms do not need table schemas // Forms do not need table schemas
// Their schemas are built from their component field names // Their schemas are built from their component field names
schema = buildFormSchema(component) schema = buildFormSchema(component, asset)
readablePrefix = "Fields" readablePrefix = "Fields"
} else if (context.type === "static") { } else if (context.type === "static") {
// Static contexts are fully defined by the components // Static contexts are fully defined by the components
@ -370,6 +371,11 @@ const getProviderContextBindings = (asset, dataProviders) => {
if (runtimeSuffix) { if (runtimeSuffix) {
providerId += `-${runtimeSuffix}` providerId += `-${runtimeSuffix}`
} }
if (!filterCategoryByContext(component, context)) {
return
}
const safeComponentId = makePropSafe(providerId) const safeComponentId = makePropSafe(providerId)
// Create bindable properties for each schema field // Create bindable properties for each schema field
@ -387,6 +393,12 @@ const getProviderContextBindings = (asset, dataProviders) => {
} }
readableBinding += `.${fieldSchema.name || key}` readableBinding += `.${fieldSchema.name || key}`
const bindingCategory = getComponentBindingCategory(
component,
context,
def
)
// Create the binding object // Create the binding object
bindings.push({ bindings.push({
type: "context", type: "context",
@ -399,8 +411,8 @@ const getProviderContextBindings = (asset, dataProviders) => {
// Table ID is used by JSON fields to know what table the field is in // Table ID is used by JSON fields to know what table the field is in
tableId: table?._id, tableId: table?._id,
component: component._component, component: component._component,
category: component._instanceName, category: bindingCategory.category,
icon: def.icon, icon: bindingCategory.icon,
display: { display: {
name: fieldSchema.name || key, name: fieldSchema.name || key,
type: fieldSchema.type, type: fieldSchema.type,
@ -413,6 +425,40 @@ const getProviderContextBindings = (asset, dataProviders) => {
return bindings return bindings
} }
// Exclude a data context based on the component settings
const filterCategoryByContext = (component, context) => {
const { _component } = component
if (_component.endsWith("formblock")) {
if (
(component.actionType == "Create" && context.type === "schema") ||
(component.actionType == "View" && context.type === "form")
) {
return false
}
}
return true
}
const getComponentBindingCategory = (component, context, def) => {
let icon = def.icon
let category = component._instanceName
if (component._component.endsWith("formblock")) {
let contextCategorySuffix = {
form: "Fields",
schema: "Row",
}
category = `${component._instanceName} - ${
contextCategorySuffix[context.type]
}`
icon = context.type === "form" ? "Form" : "Data"
}
return {
icon,
category,
}
}
/** /**
* Gets all bindable properties from the logged in user. * Gets all bindable properties from the logged in user.
*/ */
@ -507,6 +553,7 @@ const getSelectedRowsBindings = asset => {
)}.${makePropSafe("selectedRows")}`, )}.${makePropSafe("selectedRows")}`,
readableBinding: `${block._instanceName}.Selected rows`, readableBinding: `${block._instanceName}.Selected rows`,
category: "Selected rows", category: "Selected rows",
icon: "ViewRow",
display: { name: block._instanceName }, display: { name: block._instanceName },
})) }))
) )
@ -582,24 +629,36 @@ const getRoleBindings = () => {
} }
/** /**
* Gets all bindable properties exposed in an event action flow up until * Gets all bindable event context properties provided in the component
* the specified action ID, as well as context provided for the action * setting
* setting as a whole by the component.
*/ */
export const getEventContextBindings = ( export const getEventContextBindings = ({
asset,
componentId,
settingKey, settingKey,
actions, componentInstance,
actionId componentId,
) => { componentDefinition,
asset,
}) => {
let bindings = [] let bindings = []
const selectedAsset = asset ?? get(currentAsset)
// Check if any context bindings are provided by the component for this // Check if any context bindings are provided by the component for this
// setting // setting
const component = findComponent(asset.props, componentId) const component =
const def = store.actions.components.getDefinition(component?._component) componentInstance ?? findComponent(selectedAsset.props, componentId)
if (!component) {
return bindings
}
const definition =
componentDefinition ??
store.actions.components.getDefinition(component?._component)
const settings = getComponentSettings(component?._component) const settings = getComponentSettings(component?._component)
const eventSetting = settings.find(setting => setting.key === settingKey) const eventSetting = settings.find(setting => setting.key === settingKey)
if (eventSetting?.context?.length) { if (eventSetting?.context?.length) {
eventSetting.context.forEach(contextEntry => { eventSetting.context.forEach(contextEntry => {
bindings.push({ bindings.push({
@ -608,14 +667,23 @@ export const getEventContextBindings = (
contextEntry.key contextEntry.key
)}`, )}`,
category: component._instanceName, category: component._instanceName,
icon: def.icon, icon: definition.icon,
display: { display: {
name: contextEntry.label, name: contextEntry.label,
}, },
}) })
}) })
} }
return bindings
}
/**
* Gets all bindable properties exposed in an event action flow up until
* the specified action ID, as well as context provided for the action
* setting as a whole by the component.
*/
export const getActionBindings = (actions, actionId) => {
let bindings = []
// Get the steps leading up to this value // Get the steps leading up to this value
const index = actions?.findIndex(action => action.id === actionId) const index = actions?.findIndex(action => action.id === actionId)
if (index == null || index === -1) { if (index == null || index === -1) {
@ -642,7 +710,6 @@ export const getEventContextBindings = (
}) })
} }
}) })
return bindings return bindings
} }
@ -835,18 +902,36 @@ export const getSchemaForDatasource = (asset, datasource, options) => {
* Builds a form schema given a form component. * Builds a form schema given a form component.
* A form schema is a schema of all the fields nested anywhere within a form. * A form schema is a schema of all the fields nested anywhere within a form.
*/ */
export const buildFormSchema = component => { export const buildFormSchema = (component, asset) => {
let schema = {} let schema = {}
if (!component) { if (!component) {
return schema return schema
} }
// If this is a form block, simply use the fields setting
if (component._component.endsWith("formblock")) { if (component._component.endsWith("formblock")) {
let schema = {} let schema = {}
component.fields?.forEach(field => {
schema[field] = { type: "string" } const datasource = getDatasourceForProvider(asset, component)
const info = getSchemaForDatasource(component, datasource)
if (!component.fields) {
Object.values(info?.schema)
.filter(
({ autocolumn, name }) =>
!autocolumn && !["_rev", "_id"].includes(name)
)
.forEach(({ name }) => {
schema[name] = { type: info?.schema[name].type }
}) })
} else {
// Field conversion
const patched = convertOldFieldFormat(component.fields || [])
patched?.forEach(({ field, active }) => {
if (!active) return
schema[field] = { type: info?.schema[field].type }
})
}
return schema return schema
} }
@ -862,7 +947,7 @@ export const buildFormSchema = component => {
} }
} }
component._children?.forEach(child => { component._children?.forEach(child => {
const childSchema = buildFormSchema(child) const childSchema = buildFormSchema(child, asset)
schema = { ...schema, ...childSchema } schema = { ...schema, ...childSchema }
}) })
return schema return schema

View File

@ -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 } from "svelte/store" import { derived, writable } 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"
@ -61,6 +61,12 @@ export const selectedLayout = derived(store, $store => {
export const selectedComponent = derived( export const selectedComponent = derived(
[store, selectedScreen], [store, selectedScreen],
([$store, $selectedScreen]) => { ([$store, $selectedScreen]) => {
if (
$selectedScreen &&
$store.selectedComponentId?.startsWith(`${$selectedScreen._id}-`)
) {
return $selectedScreen?.props
}
if (!$selectedScreen || !$store.selectedComponentId) { if (!$selectedScreen || !$store.selectedComponentId) {
return null return null
} }
@ -141,3 +147,5 @@ 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")

View File

@ -111,6 +111,7 @@ export const getFrontendStore = () => {
} }
let clone = cloneDeep(screen) let clone = cloneDeep(screen)
const result = patchFn(clone) const result = patchFn(clone)
if (result === false) { if (result === false) {
return return
} }
@ -225,7 +226,6 @@ export const getFrontendStore = () => {
// Select new screen // Select new screen
store.update(state => { store.update(state => {
state.selectedScreenId = screen._id state.selectedScreenId = screen._id
state.selectedComponentId = screen.props?._id
return state return state
}) })
}, },
@ -769,9 +769,13 @@ export const getFrontendStore = () => {
else { else {
await store.actions.screens.patch(screen => { await store.actions.screens.patch(screen => {
// Find the selected component // Find the selected component
let selectedComponentId = state.selectedComponentId
if (selectedComponentId.startsWith(`${screen._id}-`)) {
selectedComponentId = screen?.props._id
}
const currentComponent = findComponent( const currentComponent = findComponent(
screen.props, screen.props,
state.selectedComponentId selectedComponentId
) )
if (!currentComponent) { if (!currentComponent) {
return false return false
@ -834,6 +838,7 @@ export const getFrontendStore = () => {
return return
} }
const patchScreen = screen => { const patchScreen = screen => {
// findComponent looks in the tree not comp.settings[0]
let component = findComponent(screen.props, componentId) let component = findComponent(screen.props, componentId)
if (!component) { if (!component) {
return false return false
@ -994,12 +999,20 @@ export const getFrontendStore = () => {
const componentId = state.selectedComponentId const componentId = state.selectedComponentId
const screen = get(selectedScreen) const screen = get(selectedScreen)
const parent = findComponentParent(screen.props, componentId) const parent = findComponentParent(screen.props, componentId)
// Check we aren't right at the top of the tree
const index = parent?._children.findIndex(x => x._id === componentId) const index = parent?._children.findIndex(x => x._id === componentId)
if (!parent || componentId === screen.props._id) {
// Check for screen and navigation component edge cases
const screenComponentId = `${screen._id}-screen`
const navComponentId = `${screen._id}-navigation`
if (componentId === screenComponentId) {
return null return null
} }
if (componentId === navComponentId) {
return screenComponentId
}
if (parent._id === screen.props._id && index === 0) {
return navComponentId
}
// If we have siblings above us, choose the sibling or a descendant // If we have siblings above us, choose the sibling or a descendant
if (index > 0) { if (index > 0) {
@ -1021,12 +1034,20 @@ export const getFrontendStore = () => {
return parent._id return parent._id
}, },
getNext: () => { getNext: () => {
const state = get(store)
const component = get(selectedComponent) const component = get(selectedComponent)
const componentId = component?._id const componentId = component?._id
const screen = get(selectedScreen) const screen = get(selectedScreen)
const parent = findComponentParent(screen.props, componentId) const parent = findComponentParent(screen.props, componentId)
const index = parent?._children.findIndex(x => x._id === componentId) const index = parent?._children.findIndex(x => x._id === componentId)
// Check for screen and navigation component edge cases
const screenComponentId = `${screen._id}-screen`
const navComponentId = `${screen._id}-navigation`
if (state.selectedComponentId === screenComponentId) {
return navComponentId
}
// If we have children, select first child // If we have children, select first child
if (component._children?.length) { if (component._children?.length) {
return component._children[0]._id return component._children[0]._id
@ -1207,7 +1228,12 @@ export const getFrontendStore = () => {
}) })
}, },
updateSetting: async (name, value) => { updateSetting: async (name, value) => {
await store.actions.components.patch(component => { await store.actions.components.patch(
store.actions.components.updateComponentSetting(name, value)
)
},
updateComponentSetting: (name, value) => {
return component => {
if (!name || !component) { if (!name || !component) {
return false return false
} }
@ -1235,9 +1261,8 @@ export const getFrontendStore = () => {
component[key] = columnNames component[key] = columnNames
}) })
} }
component[name] = value component[name] = value
}) }
}, },
requestEjectBlock: componentId => { requestEjectBlock: componentId => {
store.actions.preview.sendEvent("eject-block", componentId) store.actions.preview.sendEvent("eject-block", componentId)

View File

@ -108,7 +108,10 @@
/****************************************************/ /****************************************************/
const getInputData = (testData, blockInputs) => { const getInputData = (testData, blockInputs) => {
let newInputData = testData || blockInputs // Test data is not cloned for reactivity
let newInputData = testData || cloneDeep(blockInputs)
// Ensures the app action fields are populated
if (block.event === "app:trigger" && !newInputData?.fields) { if (block.event === "app:trigger" && !newInputData?.fields) {
newInputData = cloneDeep(blockInputs) newInputData = cloneDeep(blockInputs)
} }

View File

@ -50,6 +50,7 @@
type="string" type="string"
{bindings} {bindings}
fillWidth={true} fillWidth={true}
updateOnChange={false}
/> />
{/each} {/each}
</div> </div>

View File

@ -6,13 +6,15 @@
Select, Select,
Toggle, Toggle,
RadioGroup, RadioGroup,
Icon,
DatePicker, DatePicker,
Modal, Modal,
notifications, notifications,
OptionSelectDnD, OptionSelectDnD,
Layout, Layout,
AbsTooltip,
} from "@budibase/bbui" } from "@budibase/bbui"
import { createEventDispatcher, getContext } from "svelte" import { createEventDispatcher, getContext, onMount } from "svelte"
import { cloneDeep } from "lodash/fp" import { cloneDeep } from "lodash/fp"
import { tables, datasources } from "stores/backend" import { tables, datasources } from "stores/backend"
import { TableNames, UNEDITABLE_USER_FIELDS } from "constants" import { TableNames, UNEDITABLE_USER_FIELDS } from "constants"
@ -47,6 +49,7 @@
export let field export let field
let mounted = false
let fieldDefinitions = cloneDeep(FIELDS) let fieldDefinitions = cloneDeep(FIELDS)
let originalName let originalName
let linkEditDisabled let linkEditDisabled
@ -413,16 +416,22 @@
} }
return newError return newError
} }
onMount(() => {
mounted = true
})
</script> </script>
<Layout noPadding gap="S"> <Layout noPadding gap="S">
{#if mounted}
<Input <Input
autofocus
bind:value={editableColumn.name} bind:value={editableColumn.name}
disabled={uneditable || disabled={uneditable ||
(linkEditDisabled && editableColumn.type === LINK_TYPE)} (linkEditDisabled && editableColumn.type === LINK_TYPE)}
error={errors?.name} error={errors?.name}
/> />
{/if}
<Select <Select
disabled={!typeEnabled} disabled={!typeEnabled}
bind:value={editableColumn.type} bind:value={editableColumn.type}
@ -452,12 +461,17 @@
/> />
{:else if editableColumn.type === "longform"} {:else if editableColumn.type === "longform"}
<div> <div>
<Label <div class="tooltip-alignment">
size="M" <Label size="M">Formatting</Label>
tooltip="Rich text includes support for images, links, tables, lists and more" <AbsTooltip
position="top"
type="info"
text={"Rich text includes support for images, link"}
> >
Formatting <Icon size="XS" name="InfoOutline" />
</Label> </AbsTooltip>
</div>
<Toggle <Toggle
bind:value={editableColumn.useRichText} bind:value={editableColumn.useRichText}
text="Enable rich text support (markdown)" text="Enable rich text support (markdown)"
@ -488,13 +502,18 @@
</div> </div>
{#if datasource?.source !== "ORACLE" && datasource?.source !== "SQL_SERVER"} {#if datasource?.source !== "ORACLE" && datasource?.source !== "SQL_SERVER"}
<div> <div>
<Label <div>
tooltip={isCreating <Label>Time zones</Label>
<AbsTooltip
position="top"
type="info"
text={isCreating
? null ? null
: "We recommend not changing how timezones are handled for existing columns, as existing data will not be updated"} : "We recommend not changing how timezones are handled for existing columns, as existing data will not be updated"}
> >
Time zones <Icon size="XS" name="InfoOutline" />
</Label> </AbsTooltip>
</div>
<Toggle <Toggle
bind:value={editableColumn.ignoreTimezones} bind:value={editableColumn.ignoreTimezones}
text="Ignore time zones" text="Ignore time zones"
@ -671,6 +690,12 @@
align-items: center; align-items: center;
} }
.tooltip-alignment {
display: flex;
align-items: center;
gap: var(--spacing-xs);
}
.label-length { .label-length {
flex-basis: 40%; flex-basis: 40%;
} }

View File

@ -121,7 +121,9 @@
type: "Screen", type: "Screen",
name: screen.routing.route, name: screen.routing.route,
icon: "WebPage", icon: "WebPage",
action: () => $goto(`./design/${screen._id}/components`), action: () => {
$goto(`./design/${screen._id}/${screen._id}-screen`)
},
})), })),
...($automationStore?.automations?.map(automation => ({ ...($automationStore?.automations?.map(automation => ({
type: "Automation", type: "Automation",

View File

@ -21,6 +21,7 @@
export let id export let id
export let showTooltip = false export let showTooltip = false
export let selectedBy = null export let selectedBy = null
export let compact = false
const scrollApi = getContext("scroll") const scrollApi = getContext("scroll")
const dispatch = createEventDispatcher() const dispatch = createEventDispatcher()
@ -80,8 +81,9 @@
{#if withArrow} {#if withArrow}
<div <div
class:opened class:opened
class:relative={indentLevel === 0} class:relative={indentLevel === 0 && !compact}
class:absolute={indentLevel > 0} class:absolute={indentLevel > 0 && !compact}
class:compact
class="icon arrow" class="icon arrow"
on:click={onIconClick} on:click={onIconClick}
> >
@ -194,10 +196,21 @@
padding: 8px; padding: 8px;
margin-left: -8px; margin-left: -8px;
} }
.compact {
position: absolute;
left: 6px;
padding: 8px;
margin-left: -8px;
}
.icon.arrow :global(svg) { .icon.arrow :global(svg) {
width: 12px; width: 12px;
height: 12px; height: 12px;
} }
.icon.arrow.compact :global(svg) {
width: 9px;
height: 9px;
}
.icon.arrow.relative { .icon.arrow.relative {
position: relative; position: relative;
margin: 0 -6px 0 -4px; margin: 0 -6px 0 -4px;

View File

@ -74,6 +74,8 @@
{/if} {/if}
</div> </div>
<Drawer <Drawer
on:drawerHide
on:drawerShow
{fillWidth} {fillWidth}
bind:this={bindingDrawer} bind:this={bindingDrawer}
{title} {title}

View File

@ -1,5 +1,5 @@
<script> <script>
import { Icon, Heading } from "@budibase/bbui" import { Icon, Body } from "@budibase/bbui"
export let title export let title
export let icon export let icon
@ -25,7 +25,7 @@
<Icon name={icon} /> <Icon name={icon} />
{/if} {/if}
<div class="title"> <div class="title">
<Heading size="XXS">{title || ""}</Heading> <Body size="S">{title}</Body>
</div> </div>
{#if showAddButton} {#if showAddButton}
<div class="add-button" on:click={onClickAddButton}> <div class="add-button" on:click={onClickAddButton}>
@ -78,15 +78,14 @@
align-items: center; align-items: center;
padding: 0 var(--spacing-l); padding: 0 var(--spacing-l);
border-bottom: var(--border-light); border-bottom: var(--border-light);
gap: var(--spacing-l); gap: var(--spacing-m);
} }
.title { .title {
flex: 1 1 auto; flex: 1 1 auto;
width: 0; width: 0;
} }
.title :global(h1) { .title :global(p) {
overflow: hidden; overflow: hidden;
font-weight: 600;
text-overflow: ellipsis; text-overflow: ellipsis;
white-space: nowrap; white-space: nowrap;
} }

View File

@ -13,9 +13,9 @@
import { generate } from "shortid" import { generate } from "shortid"
import { import {
getEventContextBindings, getEventContextBindings,
getActionBindings,
makeStateBinding, makeStateBinding,
} from "builderStore/dataBinding" } from "builderStore/dataBinding"
import { currentAsset, store } from "builderStore"
import { cloneDeep } from "lodash/fp" import { cloneDeep } from "lodash/fp"
const flipDurationMs = 150 const flipDurationMs = 150
@ -26,6 +26,7 @@
export let actions export let actions
export let bindings = [] export let bindings = []
export let nested export let nested
export let componentInstance
let actionQuery let actionQuery
let selectedAction = actions?.length ? actions[0] : null let selectedAction = actions?.length ? actions[0] : null
@ -68,15 +69,19 @@
acc[action.type].push(action) acc[action.type].push(action)
return acc return acc
}, {}) }, {})
// These are ephemeral bindings which only exist while executing actions // These are ephemeral bindings which only exist while executing actions
$: eventContexBindings = getEventContextBindings( $: eventContextBindings = getEventContextBindings({
$currentAsset, componentInstance,
$store.selectedComponentId, settingKey: key,
key, })
actions, $: actionContextBindings = getActionBindings(actions, selectedAction?.id)
selectedAction?.id
$: allBindings = getAllBindings(
bindings,
[...eventContextBindings, ...actionContextBindings],
actions
) )
$: allBindings = getAllBindings(bindings, eventContexBindings, actions)
$: { $: {
// Ensure each action has a unique ID // Ensure each action has a unique ID
if (actions) { if (actions) {

View File

@ -13,6 +13,7 @@
export let name export let name
export let bindings export let bindings
export let nested export let nested
export let componentInstance
let drawer let drawer
let tmpValue let tmpValue
@ -74,7 +75,7 @@
<ActionButton on:click={openDrawer}>{actionText}</ActionButton> <ActionButton on:click={openDrawer}>{actionText}</ActionButton>
</div> </div>
<Drawer bind:this={drawer} title={"Actions"}> <Drawer bind:this={drawer} title={"Actions"} on:drawerHide on:drawerShow>
<svelte:fragment slot="description"> <svelte:fragment slot="description">
Define what actions to run. Define what actions to run.
</svelte:fragment> </svelte:fragment>
@ -86,6 +87,7 @@
{bindings} {bindings}
{key} {key}
{nested} {nested}
{componentInstance}
/> />
</Drawer> </Drawer>

View File

@ -1,10 +1,12 @@
<script> <script>
import { Select, Label, Stepper } from "@budibase/bbui" import { Select, Label } from "@budibase/bbui"
import { currentAsset, store } from "builderStore" import { currentAsset, store } from "builderStore"
import { getActionProviderComponents } from "builderStore/dataBinding" import { getActionProviderComponents } from "builderStore/dataBinding"
import { onMount } from "svelte" import { onMount } from "svelte"
import DrawerBindableInput from "components/common/bindings/DrawerBindableInput.svelte"
export let parameters export let parameters
export let bindings = []
$: actionProviders = getActionProviderComponents( $: actionProviders = getActionProviderComponents(
$currentAsset, $currentAsset,
@ -51,7 +53,11 @@
<Select bind:value={parameters.type} options={typeOptions} /> <Select bind:value={parameters.type} options={typeOptions} />
{#if parameters.type === "specific"} {#if parameters.type === "specific"}
<Label small>Number</Label> <Label small>Number</Label>
<Stepper bind:value={parameters.number} /> <DrawerBindableInput
{bindings}
value={parameters.number}
on:change={e => (parameters.number = e.detail)}
/>
{/if} {/if}
</div> </div>

View File

@ -42,7 +42,6 @@
<ColorPicker <ColorPicker
value={column.background} value={column.background}
on:change={e => (column.background = e.detail)} on:change={e => (column.background = e.detail)}
alignRight
spectrumTheme={$store.theme} spectrumTheme={$store.theme}
/> />
</Layout> </Layout>
@ -51,7 +50,6 @@
<ColorPicker <ColorPicker
value={column.color} value={column.color}
on:change={e => (column.color = e.detail)} on:change={e => (column.color = e.detail)}
alignRight
spectrumTheme={$store.theme} spectrumTheme={$store.theme}
/> />
</Layout> </Layout>

View File

@ -0,0 +1,154 @@
<script>
import { Icon } from "@budibase/bbui"
import { dndzone } from "svelte-dnd-action"
import { createEventDispatcher } from "svelte"
import { generate } from "shortid"
import { setContext } from "svelte"
import { writable } from "svelte/store"
export let items = []
export let showHandle = true
export let listType
export let listTypeProps = {}
export let listItemKey
export let draggable = true
let store = writable({
selected: null,
actions: {
select: id => {
store.update(state => ({
...state,
selected: id,
}))
},
},
})
setContext("draggable", store)
const dispatch = createEventDispatcher()
const flipDurationMs = 150
let anchors = {}
let draggableItems = []
const buildDragable = items => {
return items.map(item => {
return {
id: listItemKey ? item[listItemKey] : generate(),
item,
}
})
}
$: if (items) {
draggableItems = buildDragable(items)
}
const updateRowOrder = e => {
draggableItems = e.detail.items
}
const serialiseUpdate = () => {
return draggableItems.reduce((acc, ele) => {
acc.push(ele.item)
return acc
}, [])
}
const handleFinalize = e => {
updateRowOrder(e)
dispatch("change", serialiseUpdate())
}
const onItemChanged = e => {
dispatch("itemChange", e.detail)
}
</script>
<ul
class="list-wrap"
use:dndzone={{
items: draggableItems,
flipDurationMs,
dropTargetStyle: { outline: "none" },
dragDisabled: !draggable,
}}
on:finalize={handleFinalize}
on:consider={updateRowOrder}
>
{#each draggableItems as draggable (draggable.id)}
<li
bind:this={anchors[draggable.id]}
class:highlighted={draggable.id === $store.selected}
>
<div class="left-content">
{#if showHandle}
<div class="handle" aria-label="drag-handle">
<Icon name="DragHandle" size="XL" />
</div>
{/if}
</div>
<div class="right-content">
<svelte:component
this={listType}
anchor={anchors[draggable.item._id]}
item={draggable.item}
{...listTypeProps}
on:change={onItemChanged}
/>
</div>
</li>
{/each}
</ul>
<style>
.list-wrap {
list-style-type: none;
margin: 0;
padding: 0;
width: 100%;
border-radius: 4px;
background-color: var(
--spectrum-table-background-color,
var(--spectrum-global-color-gray-50)
);
border: 1px solid
var(--spectrum-table-border-color, var(--spectrum-alias-border-color-mid));
}
.list-wrap > li {
background-color: var(
--spectrum-table-background-color,
var(--spectrum-global-color-gray-50)
);
transition: background-color ease-in-out 130ms;
display: flex;
align-items: center;
border-bottom: 1px solid
var(--spectrum-table-border-color, var(--spectrum-alias-border-color-mid));
}
.list-wrap > li:hover,
li.highlighted {
background-color: var(
--spectrum-table-row-background-color-hover,
var(--spectrum-alias-highlight-hover)
);
}
.list-wrap > li:first-child {
border-top-left-radius: 4px;
border-top-right-radius: 4px;
}
.list-wrap > li:last-child {
border-top-left-radius: var(--spectrum-table-regular-border-radius);
border-top-right-radius: var(--spectrum-table-regular-border-radius);
}
.right-content {
flex: 1;
min-width: 0;
}
.list-wrap li {
padding-left: var(--spacing-s);
padding-right: var(--spacing-s);
}
</style>

View File

@ -0,0 +1,160 @@
<script>
import { Icon, Popover, Layout } from "@budibase/bbui"
import { store } from "builderStore"
import { cloneDeep } from "lodash/fp"
import { createEventDispatcher } from "svelte"
import ComponentSettingsSection from "../../../../../pages/builder/app/[application]/design/[screenId]/[componentId]/_components/Component/ComponentSettingsSection.svelte"
import { getContext } from "svelte"
export let anchor
export let field
export let componentBindings
export let bindings
const draggable = getContext("draggable")
const dispatch = createEventDispatcher()
let popover
let drawers = []
let pseudoComponentInstance
let open = false
$: if (open && $draggable.selected && $draggable.selected != field._id) {
popover.hide()
}
$: if (field) {
pseudoComponentInstance = field
}
$: componentDef = store.actions.components.getDefinition(
pseudoComponentInstance._component
)
$: parsedComponentDef = processComponentDefinitionSettings(componentDef)
const processComponentDefinitionSettings = componentDef => {
if (!componentDef) {
return {}
}
const clone = cloneDeep(componentDef)
const updatedSettings = clone.settings
.filter(setting => setting.key !== "field")
.map(setting => {
return { ...setting, nested: true }
})
clone.settings = updatedSettings
return clone
}
const updateSetting = async (setting, value) => {
const nestedComponentInstance = cloneDeep(pseudoComponentInstance)
const patchFn = store.actions.components.updateComponentSetting(
setting.key,
value
)
patchFn(nestedComponentInstance)
const update = {
...nestedComponentInstance,
active: pseudoComponentInstance.active,
}
dispatch("change", update)
}
</script>
<Icon
name="Settings"
hoverable
size="S"
on:click={() => {
if (!open) {
popover.show()
open = true
}
}}
/>
<Popover
bind:this={popover}
on:open={() => {
drawers = []
$draggable.actions.select(field._id)
}}
on:close={() => {
open = false
if ($draggable.selected == field._id) {
$draggable.actions.select()
}
}}
{anchor}
align="left-outside"
showPopover={drawers.length == 0}
clickOutsideOverride={drawers.length > 0}
maxHeight={600}
handlePostionUpdate={(anchorBounds, eleBounds, cfg) => {
let { left, top } = cfg
let percentageOffset = 30
// left-outside
left = anchorBounds.left - eleBounds.width - 18
// shift up from the anchor, if space allows
let offsetPos = Math.floor(eleBounds.height / 100) * percentageOffset
let defaultTop = anchorBounds.top - offsetPos
if (window.innerHeight - defaultTop < eleBounds.height) {
top = window.innerHeight - eleBounds.height - 5
} else {
top = anchorBounds.top - offsetPos
}
return { ...cfg, left, top }
}}
>
<span class="popover-wrap">
<Layout noPadding noGap>
<div class="type-icon">
<Icon name={parsedComponentDef.icon} />
<span>{field.field}</span>
</div>
<ComponentSettingsSection
componentInstance={pseudoComponentInstance}
componentDefinition={parsedComponentDef}
isScreen={false}
onUpdateSetting={updateSetting}
showSectionTitle={false}
showInstanceName={false}
{bindings}
{componentBindings}
on:drawerShow={e => {
drawers = [...drawers, e.detail]
}}
on:drawerHide={() => {
drawers = drawers.slice(0, -1)
}}
/>
</Layout>
</span>
</Popover>
<style>
.popover-wrap {
background-color: var(--spectrum-alias-background-color-primary);
}
.type-icon {
display: flex;
gap: var(--spacing-m);
margin: var(--spacing-xl);
margin-bottom: 0px;
height: var(--spectrum-alias-item-height-m);
padding: 0px var(--spectrum-alias-item-padding-m);
border-width: var(--spectrum-actionbutton-border-size);
border-radius: var(--spectrum-alias-border-radius-regular);
border: 1px solid
var(
--spectrum-actionbutton-m-border-color,
var(--spectrum-alias-border-color)
);
align-items: center;
}
</style>

View File

@ -1,45 +1,70 @@
<script> <script>
import { Button, ActionButton, Drawer } from "@budibase/bbui" import { cloneDeep, isEqual } from "lodash/fp"
import { createEventDispatcher } from "svelte"
import ColumnDrawer from "./ColumnDrawer.svelte"
import { cloneDeep } from "lodash/fp"
import { import {
getDatasourceForProvider, getDatasourceForProvider,
getSchemaForDatasource, getSchemaForDatasource,
getBindableProperties,
getComponentBindableProperties,
} from "builderStore/dataBinding" } from "builderStore/dataBinding"
import { currentAsset } from "builderStore" import { currentAsset } from "builderStore"
import DraggableList from "../DraggableList.svelte"
import { createEventDispatcher } from "svelte"
import { store, selectedScreen } from "builderStore"
import FieldSetting from "./FieldSetting.svelte"
import { convertOldFieldFormat, getComponentForField } from "./utils"
export let componentInstance export let componentInstance
export let value = [] export let value
const dispatch = createEventDispatcher() const dispatch = createEventDispatcher()
let sanitisedFields
let fieldList
let schema
let cachedValue
let drawer $: bindings = getBindableProperties($selectedScreen, componentInstance._id)
let boundValue $: actionType = componentInstance.actionType
let componentBindings = []
$: if (actionType) {
componentBindings = getComponentBindableProperties(
$selectedScreen,
componentInstance._id
)
}
$: text = getText(value)
$: convertOldColumnFormat(value)
$: datasource = getDatasourceForProvider($currentAsset, componentInstance) $: datasource = getDatasourceForProvider($currentAsset, componentInstance)
$: schema = getSchema($currentAsset, datasource)
$: if (!isEqual(value, cachedValue)) {
cachedValue = value
schema = getSchema($currentAsset, datasource)
}
$: options = Object.keys(schema || {}) $: options = Object.keys(schema || {})
$: sanitisedValue = getValidColumns(value, options) $: sanitisedValue = getValidColumns(convertOldFieldFormat(value), options)
$: updateBoundValue(sanitisedValue) $: updateSanitsedFields(sanitisedValue)
$: unconfigured = buildUnconfiguredOptions(schema, sanitisedFields)
const getText = value => { // Builds unused ones only
if (!value?.length) { const buildUnconfiguredOptions = (schema, selected) => {
return "All fields" if (!schema) {
} return []
let text = `${value.length} field`
if (value.length !== 1) {
text += "s"
}
return text
} }
let schemaClone = cloneDeep(schema)
selected.forEach(val => {
delete schemaClone[val.field]
})
const convertOldColumnFormat = oldColumns => { return Object.keys(schemaClone)
if (typeof oldColumns?.[0] === "string") { .filter(key => !schemaClone[key].autocolumn)
value = oldColumns.map(field => ({ name: field, displayName: field })) .map(key => {
const col = schemaClone[key]
let toggleOn = !value
return {
field: key,
active: typeof col.active != "boolean" ? toggleOn : col.active,
} }
})
} }
const getSchema = (asset, datasource) => { const getSchema = (asset, datasource) => {
@ -54,50 +79,85 @@
return schema return schema
} }
const updateBoundValue = value => { const updateSanitsedFields = value => {
boundValue = cloneDeep(value) sanitisedFields = cloneDeep(value)
} }
const getValidColumns = (columns, options) => { const getValidColumns = (columns, options) => {
if (!Array.isArray(columns) || !columns.length) { if (!Array.isArray(columns) || !columns.length) {
return [] return []
} }
// We need to account for legacy configs which would just be an array
// of strings
if (typeof columns[0] === "string") {
columns = columns.map(col => ({
name: col,
displayName: col,
}))
}
return columns.filter(column => { return columns.filter(column => {
return options.includes(column.name) return options.includes(column.field)
}) })
} }
const open = () => { const buildSudoInstance = instance => {
updateBoundValue(sanitisedValue) if (instance._component) {
drawer.show() return instance
} }
const save = () => { const type = getComponentForField(instance.field, schema)
dispatch("change", getValidColumns(boundValue, options)) instance._component = `@budibase/standard-components/${type}`
drawer.hide()
const pseudoComponentInstance = store.actions.components.createInstance(
instance._component,
{
_instanceName: instance.field,
field: instance.field,
label: instance.field,
placeholder: instance.field,
},
{}
)
return { ...instance, ...pseudoComponentInstance }
}
$: if (sanitisedFields) {
fieldList = [...sanitisedFields, ...unconfigured].map(buildSudoInstance)
}
const processItemUpdate = e => {
const updatedField = e.detail
const parentFieldsUpdated = fieldList ? cloneDeep(fieldList) : []
let parentFieldIdx = parentFieldsUpdated.findIndex(pSetting => {
return pSetting.field === updatedField?.field
})
if (parentFieldIdx == -1) {
parentFieldsUpdated.push(updatedField)
} else {
parentFieldsUpdated[parentFieldIdx] = updatedField
}
dispatch("change", getValidColumns(parentFieldsUpdated, options))
}
const listUpdated = e => {
const parsedColumns = getValidColumns(e.detail, options)
dispatch("change", parsedColumns)
} }
</script> </script>
<div class="field-configuration"> <div class="field-configuration">
<ActionButton on:click={open}>{text}</ActionButton> {#if fieldList?.length}
<DraggableList
on:change={listUpdated}
on:itemChange={processItemUpdate}
items={fieldList}
listItemKey={"_id"}
listType={FieldSetting}
listTypeProps={{
componentBindings,
bindings,
}}
/>
{/if}
</div> </div>
<Drawer bind:this={drawer} title="Form Fields">
<svelte:fragment slot="description">
Configure the fields in your form.
</svelte:fragment>
<Button cta slot="buttons" on:click={save}>Save</Button>
<ColumnDrawer slot="body" bind:columns={boundValue} {options} {schema} />
</Drawer>
<style> <style>
.field-configuration :global(.spectrum-ActionButton) { .field-configuration :global(.spectrum-ActionButton) {
width: 100%; width: 100%;

View File

@ -0,0 +1,56 @@
<script>
import EditFieldPopover from "./EditFieldPopover.svelte"
import { Toggle } from "@budibase/bbui"
import { createEventDispatcher } from "svelte"
import { cloneDeep } from "lodash/fp"
export let item
export let componentBindings
export let bindings
export let anchor
const dispatch = createEventDispatcher()
const onToggle = item => {
return e => {
item.active = e.detail
dispatch("change", { ...cloneDeep(item), active: e.detail })
}
}
</script>
<div class="list-item-body">
<div class="list-item-left">
<EditFieldPopover
{anchor}
field={item}
{componentBindings}
{bindings}
on:change
/>
<div class="field-label">{item.label || item.field}</div>
</div>
<div class="list-item-right">
<Toggle on:change={onToggle(item)} text="" value={item.active} thin />
</div>
</div>
<style>
.field-label {
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
}
.list-item-body,
.list-item-left {
display: flex;
align-items: center;
gap: var(--spacing-m);
min-width: 0;
}
.list-item-right :global(div.spectrum-Switch) {
margin: 0px;
}
.list-item-body {
justify-content: space-between;
}
</style>

View File

@ -0,0 +1,46 @@
export const convertOldFieldFormat = fields => {
if (!fields) {
return []
}
const converted = fields.map(field => {
if (typeof field === "string") {
// existed but was a string
return {
field,
active: true,
}
} else if (typeof field?.active != "boolean") {
// existed but had no state
return {
field: field.name,
active: true,
}
} else {
return field
}
})
return converted
}
export const getComponentForField = (field, schema) => {
if (!field || !schema?.[field]) {
return null
}
const type = schema[field].type
return FieldTypeToComponentMap[type]
}
export const FieldTypeToComponentMap = {
string: "stringfield",
number: "numberfield",
bigint: "bigintfield",
options: "optionsfield",
array: "multifieldselect",
boolean: "booleanfield",
longform: "longformfield",
datetime: "datetimefield",
attachment: "attachmentfield",
link: "relationshipfield",
json: "jsonfield",
barcodeqr: "codescanner",
}

View File

@ -17,7 +17,7 @@
import { generate } from "shortid" import { generate } from "shortid"
import { LuceneUtils, Constants } from "@budibase/frontend-core" import { LuceneUtils, Constants } from "@budibase/frontend-core"
import { getFields } from "helpers/searchFields" import { getFields } from "helpers/searchFields"
import { createEventDispatcher } from "svelte" import { createEventDispatcher, onMount } from "svelte"
export let schemaFields export let schemaFields
export let filters = [] export let filters = []
@ -35,22 +35,28 @@
{ value: "and", label: "Match all filters" }, { value: "and", label: "Match all filters" },
{ value: "or", label: "Match any filter" }, { value: "or", label: "Match any filter" },
] ]
const onEmptyOptions = [
{ value: "all", label: "Return all table rows" },
{ value: "none", label: "Return no rows" },
]
let rawFilters let rawFilters
let matchAny = false let matchAny = false
let onEmptyFilter = "all"
$: parseFilters(filters) $: parseFilters(filters)
$: dispatch("change", enrichFilters(rawFilters, matchAny)) $: dispatch("change", enrichFilters(rawFilters, matchAny, onEmptyFilter))
$: enrichedSchemaFields = getFields(schemaFields || [], { allowLinks: true }) $: enrichedSchemaFields = getFields(schemaFields || [], { allowLinks: true })
$: fieldOptions = enrichedSchemaFields.map(field => field.name) || [] $: fieldOptions = enrichedSchemaFields.map(field => field.name) || []
$: valueTypeOptions = allowBindings ? ["Value", "Binding"] : ["Value"] $: valueTypeOptions = allowBindings ? ["Value", "Binding"] : ["Value"]
// Remove field key prefixes and determine whether to use the "match all" // Remove field key prefixes and determine which behaviours to use
// or "match any" behaviour
const parseFilters = filters => { const parseFilters = filters => {
matchAny = filters?.find(filter => filter.operator === "allOr") != null matchAny = filters?.find(filter => filter.operator === "allOr") != null
onEmptyFilter =
filters?.find(filter => filter.onEmptyFilter)?.onEmptyFilter ?? "all"
rawFilters = (filters || []) rawFilters = (filters || [])
.filter(filter => filter.operator !== "allOr") .filter(filter => filter.operator !== "allOr" && !filter.onEmptyFilter)
.map(filter => { .map(filter => {
const { field } = filter const { field } = filter
let newFilter = { ...filter } let newFilter = { ...filter }
@ -64,9 +70,18 @@
}) })
} }
onMount(() => {
parseFilters(filters)
rawFilters.forEach(filter => {
filter.type =
schemaFields.find(field => field.name === filter.field)?.type ||
filter.type
})
})
// Add field key prefixes and a special metadata filter object to indicate // Add field key prefixes and a special metadata filter object to indicate
// whether to use the "match all" or "match any" behaviour // how to handle filter behaviour
const enrichFilters = (rawFilters, matchAny) => { const enrichFilters = (rawFilters, matchAny, onEmptyFilter) => {
let count = 1 let count = 1
return rawFilters return rawFilters
.filter(filter => filter.field) .filter(filter => filter.field)
@ -75,6 +90,7 @@
field: `${count++}:${filter.field}`, field: `${count++}:${filter.field}`,
})) }))
.concat(matchAny ? [{ operator: "allOr" }] : []) .concat(matchAny ? [{ operator: "allOr" }] : [])
.concat([{ onEmptyFilter }])
} }
const addFilter = () => { const addFilter = () => {
@ -186,6 +202,17 @@
on:change={e => (matchAny = e.detail === "or")} on:change={e => (matchAny = e.detail === "or")}
placeholder={null} placeholder={null}
/> />
{#if datasource?.type === "table"}
<Select
label="When filter empty"
value={onEmptyFilter}
options={onEmptyOptions}
getOptionLabel={opt => opt.label}
getOptionValue={opt => opt.value}
on:change={e => (onEmptyFilter = e.detail)}
placeholder={null}
/>
{/if}
</div> </div>
<div> <div>
<div class="filter-label"> <div class="filter-label">

View File

@ -24,11 +24,22 @@
} }
</script> </script>
<ActionButton on:click={drawer.show}>Define Options</ActionButton> <div class="options-wrap">
<Drawer bind:this={drawer} title="Options"> <div />
<div><ActionButton on:click={drawer.show}>Define Options</ActionButton></div>
</div>
<Drawer bind:this={drawer} title="Options" on:drawerHide on:drawerShow>
<svelte:fragment slot="description"> <svelte:fragment slot="description">
Define the options for this picker. Define the options for this picker.
</svelte:fragment> </svelte:fragment>
<Button cta slot="buttons" on:click={saveOptions}>Save</Button> <Button cta slot="buttons" on:click={saveOptions}>Save</Button>
<OptionsDrawer bind:options={tempValue} slot="body" /> <OptionsDrawer bind:options={tempValue} slot="body" />
</Drawer> </Drawer>
<style>
.options-wrap {
gap: 8px;
display: grid;
grid-template-columns: 90px 1fr;
}
</style>

View File

@ -100,6 +100,8 @@
{key} {key}
{type} {type}
{...props} {...props}
on:drawerHide
on:drawerShow
/> />
</div> </div>
{#if info} {#if info}

View File

@ -5,9 +5,8 @@
export let value = [] export let value = []
export let bindings = [] export let bindings = []
export let componentDefinition export let componentInstance
export let type export let type
const dispatch = createEventDispatcher() const dispatch = createEventDispatcher()
let drawer let drawer
@ -31,7 +30,7 @@
<ActionButton on:click={drawer.show}>{text}</ActionButton> <ActionButton on:click={drawer.show}>{text}</ActionButton>
</div> </div>
<Drawer bind:this={drawer} title="Validation Rules"> <Drawer bind:this={drawer} title="Validation Rules" on:drawerHide on:drawerShow>
<svelte:fragment slot="description"> <svelte:fragment slot="description">
Configure validation rules for this field. Configure validation rules for this field.
</svelte:fragment> </svelte:fragment>
@ -41,7 +40,7 @@
bind:rules={value} bind:rules={value}
{type} {type}
{bindings} {bindings}
{componentDefinition} fieldName={componentInstance?.field}
/> />
</Drawer> </Drawer>

View File

@ -56,6 +56,11 @@ export const syncURLToState = options => {
// Navigate to a certain URL // Navigate to a certain URL
const gotoUrl = (url, params) => { const gotoUrl = (url, params) => {
// Clean URL
if (url?.endsWith("/index")) {
url = url.replace("/index", "")
}
// Allow custom URL handling
if (beforeNavigate) { if (beforeNavigate) {
const res = beforeNavigate(url, params) const res = beforeNavigate(url, params)
if (res?.url) { if (res?.url) {

View File

@ -2,14 +2,15 @@
import { Button, Layout } from "@budibase/bbui" import { Button, Layout } from "@budibase/bbui"
import DatasourceNavigator from "components/backend/DatasourceNavigator/DatasourceNavigator.svelte" import DatasourceNavigator from "components/backend/DatasourceNavigator/DatasourceNavigator.svelte"
import Panel from "components/design/Panel.svelte" import Panel from "components/design/Panel.svelte"
import { isActive, goto, redirect } from "@roxi/routify" import { isActive, redirect, goto, params } from "@roxi/routify"
import BetaButton from "./_components/BetaButton.svelte" import BetaButton from "./_components/BetaButton.svelte"
import { datasources } from "stores/backend" import { datasources } from "stores/backend"
$: { $: {
// If we ever don't have any data other than the users table, prompt the // If we ever don't have any data other than the users table, prompt the
// user to add some // user to add some
if (!$datasources.hasData) { // Don't redirect if setting up google sheets, or we lose the query parameter
if (!$datasources.hasData && !$params["?continue_google_setup"]) {
$redirect("./new") $redirect("./new")
} }
} }

View File

@ -0,0 +1,12 @@
<script>
import { DetailSummary } from "@budibase/bbui"
import InfoDisplay from "./InfoDisplay.svelte"
export let componentDefinition
</script>
<DetailSummary collapsible={false} noPadding={true}>
<InfoDisplay
title={componentDefinition.name}
body={componentDefinition.info}
/>
</DetailSummary>

View File

@ -5,7 +5,7 @@
import DesignSection from "./DesignSection.svelte" import DesignSection from "./DesignSection.svelte"
import CustomStylesSection from "./CustomStylesSection.svelte" import CustomStylesSection from "./CustomStylesSection.svelte"
import ConditionalUISection from "./ConditionalUISection.svelte" import ConditionalUISection from "./ConditionalUISection.svelte"
import ComponentInfoSection from "./ComponentInfoSection.svelte"
import { import {
getBindableProperties, getBindableProperties,
getComponentBindableProperties, getComponentBindableProperties,
@ -55,9 +55,6 @@
</div> </div>
</span> </span>
{#if section == "settings"} {#if section == "settings"}
{#if componentDefinition?.info}
<ComponentInfoSection {componentDefinition} />
{/if}
<ComponentSettingsSection <ComponentSettingsSection
{componentInstance} {componentInstance}
{componentDefinition} {componentDefinition}

View File

@ -6,6 +6,7 @@
import ResetFieldsButton from "components/design/settings/controls/ResetFieldsButton.svelte" import ResetFieldsButton from "components/design/settings/controls/ResetFieldsButton.svelte"
import EjectBlockButton from "components/design/settings/controls/EjectBlockButton.svelte" import EjectBlockButton from "components/design/settings/controls/EjectBlockButton.svelte"
import { getComponentForSetting } from "components/design/settings/componentSettings" import { getComponentForSetting } from "components/design/settings/componentSettings"
import InfoDisplay from "./InfoDisplay.svelte"
import analytics, { Events } from "analytics" import analytics, { Events } from "analytics"
export let componentDefinition export let componentDefinition
@ -13,6 +14,9 @@
export let bindings export let bindings
export let componentBindings export let componentBindings
export let isScreen = false export let isScreen = false
export let onUpdateSetting
export let showSectionTitle = true
export let showInstanceName = true
$: sections = getSections(componentInstance, componentDefinition, isScreen) $: sections = getSections(componentInstance, componentDefinition, isScreen)
@ -47,8 +51,11 @@
const updateSetting = async (setting, value) => { const updateSetting = async (setting, value) => {
try { try {
if (typeof onUpdateSetting === "function") {
await onUpdateSetting(setting, value)
} else {
await store.actions.components.updateSetting(setting.key, value) await store.actions.components.updateSetting(setting.key, value)
}
// Send event if required // Send event if required
if (setting.sendEvents) { if (setting.sendEvents) {
analytics.captureEvent(Events.COMPONENT_UPDATED, { analytics.captureEvent(Events.COMPONENT_UPDATED, {
@ -97,7 +104,7 @@
} }
} }
return true return typeof setting.visible == "boolean" ? setting.visible : true
} }
const canRenderControl = (instance, setting, isScreen) => { const canRenderControl = (instance, setting, isScreen) => {
@ -116,9 +123,22 @@
{#each sections as section, idx (section.name)} {#each sections as section, idx (section.name)}
{#if section.visible} {#if section.visible}
<DetailSummary name={section.name} collapsible={false}> <DetailSummary
name={showSectionTitle ? section.name : ""}
collapsible={false}
>
{#if section.info}
<div class="section-info">
<InfoDisplay body={section.info} />
</div>
{:else if idx === 0 && section.name === "General" && componentDefinition.info}
<InfoDisplay
title={componentDefinition.name}
body={componentDefinition.info}
/>
{/if}
<div class="settings"> <div class="settings">
{#if idx === 0 && !componentInstance._component.endsWith("/layout") && !isScreen} {#if idx === 0 && !componentInstance._component.endsWith("/layout") && !isScreen && showInstanceName}
<PropertyControl <PropertyControl
control={Input} control={Input}
label="Name" label="Name"
@ -157,6 +177,8 @@
{componentBindings} {componentBindings}
{componentInstance} {componentInstance}
{componentDefinition} {componentDefinition}
on:drawerShow
on:drawerHide
/> />
{/if} {/if}
{/each} {/each}

View File

@ -0,0 +1,66 @@
<script>
import { Icon } from "@budibase/bbui"
export let title
export let body
export let icon = "HelpOutline"
</script>
<div class="info" class:noTitle={!title}>
{#if title}
<div class="title">
<Icon name={icon} />
{title || ""}
</div>
<!-- eslint-disable-next-line svelte/no-at-html-tags -->
{@html body}
{:else}
<span class="icon">
<Icon name={icon} />
</span>
<!-- eslint-disable-next-line svelte/no-at-html-tags -->
{@html body}
{/if}
</div>
<style>
.title {
font-size: 12px;
font-weight: 600;
text-transform: uppercase;
margin-bottom: var(--spacing-m);
display: flex;
flex-direction: row;
justify-content: flex-start;
align-items: center;
gap: var(--spacing-s);
}
.title,
.icon {
color: var(--spectrum-global-color-gray-600);
}
.info {
padding: var(--spacing-m) var(--spacing-l) var(--spacing-l) var(--spacing-l);
background-color: var(--background-alt);
border-radius: var(--border-radius-s);
font-size: 13px;
}
.noTitle {
display: flex;
align-items: center;
gap: var(--spacing-m);
}
.info :global(a) {
color: inherit;
transition: color 130ms ease-out;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
.info :global(a:hover) {
color: var(--spectrum-global-color-gray-900);
}
.info :global(a) {
text-decoration: underline;
}
</style>

View File

@ -1,6 +1,6 @@
<script> <script>
import { Button, Drawer } from "@budibase/bbui" import { Button, Drawer } from "@budibase/bbui"
import NavigationLinksDrawer from "./NavigationLinksDrawer.svelte" import NavigationLinksDrawer from "./LinksDrawer.svelte"
import { cloneDeep } from "lodash/fp" import { cloneDeep } from "lodash/fp"
import { store } from "builderStore" import { store } from "builderStore"
@ -20,12 +20,8 @@
} }
</script> </script>
<Button cta on:click={openDrawer}>Configure links</Button> <Button cta on:click={openDrawer}>Configure Links</Button>
<Drawer <Drawer bind:this={drawer} title={"Navigation Links"}>
bind:this={drawer}
title={"Navigation Links"}
width="calc(100% - 334px)"
>
<svelte:fragment slot="description"> <svelte:fragment slot="description">
Configure the links in your navigation bar. Configure the links in your navigation bar.
</svelte:fragment> </svelte:fragment>

View File

@ -0,0 +1,225 @@
<script>
import LinksEditor from "./LinksEditor.svelte"
import { get } from "svelte/store"
import Panel from "components/design/Panel.svelte"
import {
Detail,
Toggle,
Body,
Icon,
ColorPicker,
Input,
Label,
ActionGroup,
ActionButton,
Checkbox,
notifications,
Select,
} from "@budibase/bbui"
import { selectedScreen, store } from "builderStore"
import { DefaultAppTheme } from "constants"
const updateShowNavigation = async e => {
await store.actions.screens.updateSetting(
get(selectedScreen),
"showNavigation",
e.detail
)
}
const update = async (key, value) => {
try {
let navigation = $store.navigation
navigation[key] = value
await store.actions.navigation.save(navigation)
} catch (error) {
notifications.error("Error updating navigation settings")
}
}
</script>
<Panel
title="Navigation"
icon={$selectedScreen.showNavigation ? "Visibility" : "VisibilityOff"}
borderLeft
wide
>
<div class="generalSection">
<div class="subheading">
<Detail>General</Detail>
</div>
<div class="toggle">
<Toggle
on:change={updateShowNavigation}
value={$selectedScreen.showNavigation}
/>
<Body size="S">Show nav on this screen</Body>
</div>
</div>
{#if $selectedScreen.showNavigation}
<div class="divider" />
<div class="customizeSection">
<div class="subheading">
<Detail>Customize</Detail>
</div>
<div class="info">
<Icon name="InfoOutline" size="S" />
<Body size="S">These settings apply to all screens</Body>
</div>
<div class="configureLinks">
<LinksEditor />
</div>
<div class="controls">
<div class="label">
<Label size="M">Position</Label>
</div>
<ActionGroup quiet>
<ActionButton
selected={$store.navigation.navigation === "Top"}
quiet={$store.navigation.navigation !== "Top"}
icon="PaddingTop"
on:click={() => update("navigation", "Top")}
/>
<ActionButton
selected={$store.navigation.navigation === "Left"}
quiet={$store.navigation.navigation !== "Left"}
icon="PaddingLeft"
on:click={() => update("navigation", "Left")}
/>
</ActionGroup>
{#if $store.navigation.navigation === "Top"}
<div class="label">
<Label size="M">Sticky header</Label>
</div>
<Checkbox
value={$store.navigation.sticky}
on:change={e => update("sticky", e.detail)}
/>
<div class="label">
<Label size="M">Width</Label>
</div>
<Select
options={["Max", "Large", "Medium", "Small"]}
plaveholder={null}
value={$store.navigation.navWidth}
on:change={e => update("navWidth", e.detail)}
/>
{/if}
<div class="label">
<Label size="M">Show logo</Label>
</div>
<Checkbox
value={!$store.navigation.hideLogo}
on:change={e => update("hideLogo", !e.detail)}
/>
{#if !$store.navigation.hideLogo}
<div class="label">
<Label size="M">Logo URL</Label>
</div>
<Input
value={$store.navigation.logoUrl}
on:change={e => update("logoUrl", e.detail)}
updateOnChange={false}
/>
{/if}
<div class="label">
<Label size="M">Show title</Label>
</div>
<Checkbox
value={!$store.navigation.hideTitle}
on:change={e => update("hideTitle", !e.detail)}
/>
{#if !$store.navigation.hideTitle}
<div class="label">
<Label size="M">Title</Label>
</div>
<Input
value={$store.navigation.title}
on:change={e => update("title", e.detail)}
updateOnChange={false}
/>
{/if}
<div class="label">
<Label>Background</Label>
</div>
<ColorPicker
spectrumTheme={$store.theme}
value={$store.navigation.navBackground ||
DefaultAppTheme.navBackground}
on:change={e => update("navBackground", e.detail)}
/>
<div class="label">
<Label>Text</Label>
</div>
<ColorPicker
spectrumTheme={$store.theme}
value={$store.navigation.navTextColor || DefaultAppTheme.navTextColor}
on:change={e => update("navTextColor", e.detail)}
/>
</div>
</div>
{/if}
</Panel>
<style>
.generalSection {
padding: 13px 13px 25px;
}
.customizeSection {
padding: 13px 13px 25px;
}
.subheading {
margin-bottom: 10px;
}
.subheading :global(p) {
color: var(--grey-6);
}
.toggle {
display: flex;
align-items: center;
}
.divider {
border-top: 1px solid var(--grey-3);
}
.controls {
position: relative;
display: grid;
grid-template-columns: 90px 1fr;
align-items: start;
transition: background 130ms ease-out, border-color 130ms ease-out;
border-left: 4px solid transparent;
margin: 0 calc(-1 * var(--spacing-xl));
padding: 0 var(--spacing-xl) 0 calc(var(--spacing-xl) - 4px);
gap: 12px;
}
.label {
margin-top: 16px;
transform: translateY(-50%);
}
.info {
background-color: var(--background-alt);
padding: 12px;
display: flex;
border-radius: 4px;
gap: 4px;
margin-bottom: 16px;
}
.info :global(svg) {
margin-right: 5px;
color: var(--spectrum-global-color-gray-600);
}
.configureLinks :global(button) {
margin-bottom: 20px;
width: 100%;
}
</style>

View File

@ -1,12 +1,8 @@
<script> <script>
import Panel from "components/design/Panel.svelte"
import { get } from "svelte/store" import { get } from "svelte/store"
import { Helpers } from "@budibase/bbui" import { Helpers } from "@budibase/bbui"
import { import {
Input, Input,
Layout,
Button,
Toggle,
Checkbox, Checkbox,
Banner, Banner,
Select, Select,
@ -16,7 +12,6 @@
import RoleSelect from "components/design/settings/controls/RoleSelect.svelte" import RoleSelect from "components/design/settings/controls/RoleSelect.svelte"
import { selectedScreen, store } from "builderStore" import { selectedScreen, store } from "builderStore"
import sanitizeUrl from "builderStore/store/screenTemplates/utils/sanitizeUrl" import sanitizeUrl from "builderStore/store/screenTemplates/utils/sanitizeUrl"
import { goto } from "@roxi/routify"
import ButtonActionEditor from "components/design/settings/controls/ButtonActionEditor/ButtonActionEditor.svelte" import ButtonActionEditor from "components/design/settings/controls/ButtonActionEditor/ButtonActionEditor.svelte"
import { getBindableProperties } from "builderStore/dataBinding" import { getBindableProperties } from "builderStore/dataBinding"
@ -119,15 +114,6 @@
label: "On screen load", label: "On screen load",
control: ButtonActionEditor, control: ButtonActionEditor,
}, },
{
key: "showNavigation",
label: "Navigation",
control: Toggle,
props: {
text: "Show nav",
disabled: !!$selectedScreen.layoutId,
},
},
{ {
key: "width", key: "width",
label: "Width", label: "Width",
@ -145,14 +131,7 @@
} }
</script> </script>
<Panel {#if $selectedScreen.layoutId}
title={$selectedScreen.routing.route}
icon={$selectedScreen.routing.route === "/" ? "Home" : "WebPage"}
borderLeft
wide
>
<Layout gap="S" paddingX="L" paddingY="XL">
{#if $selectedScreen.layoutId}
<Banner <Banner
type="warning" type="warning"
extraButtonText="Detach custom layout" extraButtonText="Detach custom layout"
@ -161,8 +140,8 @@
> >
This screen uses a custom layout, which is deprecated This screen uses a custom layout, which is deprecated
</Banner> </Banner>
{/if} {/if}
{#each screenSettings as setting (setting.key)} {#each screenSettings as setting (setting.key)}
<PropertyControl <PropertyControl
control={setting.control} control={setting.control}
label={setting.label} label={setting.label}
@ -172,9 +151,4 @@
props={{ ...setting.props, error: errors[setting.key] }} props={{ ...setting.props, error: errors[setting.key] }}
{bindings} {bindings}
/> />
{/each} {/each}
<Button secondary on:click={() => $goto("../components")}>
View components
</Button>
</Layout>
</Panel>

View File

@ -0,0 +1,78 @@
<script>
import {
Layout,
Label,
ColorPicker,
notifications,
Icon,
Body,
} from "@budibase/bbui"
import { store } from "builderStore"
import { get } from "svelte/store"
import { DefaultAppTheme } from "constants"
import AppThemeSelect from "./AppThemeSelect.svelte"
import ButtonRoundnessSelect from "./ButtonRoundnessSelect.svelte"
import PropertyControl from "components/design/settings/controls/PropertyControl.svelte"
$: customTheme = $store.customTheme || {}
const update = async (property, value) => {
try {
store.actions.customTheme.save({
...get(store).customTheme,
[property]: value,
})
} catch (error) {
notifications.error("Error updating custom theme")
}
}
</script>
<div class="info">
<Icon name="InfoOutline" size="S" />
<Body size="S">These settings apply to all screens</Body>
</div>
<Layout noPadding gap="S">
<Layout noPadding gap="XS">
<AppThemeSelect />
</Layout>
<Layout noPadding gap="XS">
<Label>Button roundness</Label>
<ButtonRoundnessSelect
{customTheme}
on:change={e => update("buttonBorderRadius", e.detail)}
/>
</Layout>
<PropertyControl
label="Accent color"
control={ColorPicker}
value={customTheme.primaryColor || DefaultAppTheme.primaryColor}
onChange={val => update("primaryColor", val)}
props={{
spectrumTheme: $store.theme,
}}
/>
<PropertyControl
label="Hover"
control={ColorPicker}
value={customTheme.primaryColorHover || DefaultAppTheme.primaryColorHover}
onChange={val => update("primaryColorHover", val)}
props={{
spectrumTheme: $store.theme,
}}
/>
</Layout>
<style>
.info {
background-color: var(--background-alt);
padding: 12px;
display: flex;
border-radius: 4px;
gap: 4px;
}
.info :global(svg) {
margin-right: 5px;
color: var(--spectrum-global-color-gray-600);
}
</style>

View File

@ -0,0 +1,51 @@
<script>
import GeneralPanel from "./GeneralPanel.svelte"
import ThemePanel from "./ThemePanel.svelte"
import { selectedScreen } from "builderStore"
import Panel from "components/design/Panel.svelte"
import { capitalise } from "helpers"
import { ActionButton, Layout } from "@budibase/bbui"
let activeTab = "general"
const tabs = ["general", "theme"]
</script>
<Panel
title={$selectedScreen.routing.route}
icon={$selectedScreen.routing.route === "/" ? "Home" : "WebPage"}
borderLeft
wide
>
<div slot="panel-header-content">
<div class="settings-tabs">
{#each tabs as tab}
<ActionButton
size="M"
quiet
selected={activeTab === tab}
on:click={() => {
activeTab = tab
}}
>
{capitalise(tab)}
</ActionButton>
{/each}
</div>
</div>
<Layout gap="S" paddingX="L" paddingY="XL">
{#if activeTab === "theme"}
<ThemePanel />
{:else}
<GeneralPanel />
{/if}
</Layout>
</Panel>
<style>
.settings-tabs {
display: flex;
gap: var(--spacing-s);
padding: 0 var(--spacing-l);
padding-bottom: var(--spacing-l);
}
</style>

View File

@ -0,0 +1,52 @@
<script>
import { syncURLToState } from "helpers/urlStateSync"
import { store, selectedScreen } from "builderStore"
import * as routify from "@roxi/routify"
import { onDestroy } from "svelte"
import { findComponent } from "builderStore/componentUtils"
import ComponentSettingsPanel from "./_components/Component/ComponentSettingsPanel.svelte"
import NavigationPanel from "./_components/Navigation/index.svelte"
import ScreenSettingsPanel from "./_components/Screen/index.svelte"
$: componentId = $store.selectedComponentId
$: store.actions.websocket.selectResource(componentId)
$: params = routify.params
$: routeComponentId = $params.componentId
// Hide new component panel whenever component ID changes
const closeNewComponentPanel = url => {
if (url?.endsWith("/new")) {
url = url.replace("/new", "")
}
return { url }
}
const validate = id => {
if (id === `${$store.selectedScreenId}-screen`) return true
if (id === `${$store.selectedScreenId}-navigation`) return true
return !!findComponent($selectedScreen.props, id)
}
// Keep URL and state in sync for selected component ID
const stopSyncing = syncURLToState({
urlParam: "componentId",
stateKey: "selectedComponentId",
validate,
fallbackUrl: "../",
store,
routify,
beforeNavigate: closeNewComponentPanel,
})
onDestroy(stopSyncing)
</script>
{#if routeComponentId === `${$store.selectedScreenId}-screen`}
<ScreenSettingsPanel />
{:else if routeComponentId === `${$store.selectedScreenId}-navigation`}
<NavigationPanel />
{:else}
<ComponentSettingsPanel />
{/if}
<slot />

View File

@ -0,0 +1 @@
<!-- Required to make Routify happy -->

View File

@ -31,6 +31,10 @@
$: orderMap = createComponentOrderMap(componentList) $: orderMap = createComponentOrderMap(componentList)
const getAllowedComponents = (allComponents, screen, component) => { const getAllowedComponents = (allComponents, screen, component) => {
// Default to using the root screen container if no component specified
if (!component) {
component = screen.props
}
const path = findComponentPath(screen?.props, component?._id) const path = findComponentPath(screen?.props, component?._id)
if (!path?.length) { if (!path?.length) {
return [] return []

Some files were not shown because too many files have changed in this diff Show More