Merge branch 'master' into global-bindings

This commit is contained in:
Michael Drury 2023-10-17 14:13:04 +01:00 committed by GitHub
commit ee0da189d2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
311 changed files with 8961 additions and 7977 deletions

View File

@ -1,9 +1,14 @@
packages/server/node_modules *
packages/builder !/packages/
packages/frontend-core !/scripts/
packages/backend-core /packages/*/node_modules
packages/worker/node_modules packages/server/scripts/
packages/cli !packages/server/scripts/integrations/oracle
packages/client !nx.json
packages/bbui !/hosting/single/
packages/string-templates !/hosting/letsencrypt /
!package.json
!yarn.lock
!lerna.json
!.yarnrc

View File

@ -10,7 +10,6 @@ on:
push: push:
branches: branches:
- master - master
- develop
pull_request: pull_request:
workflow_dispatch: workflow_dispatch:
@ -20,18 +19,12 @@ env:
PERSONAL_ACCESS_TOKEN: ${{ secrets.PERSONAL_ACCESS_TOKEN }} PERSONAL_ACCESS_TOKEN: ${{ secrets.PERSONAL_ACCESS_TOKEN }}
NX_BASE_BRANCH: origin/${{ github.base_ref }} NX_BASE_BRANCH: origin/${{ github.base_ref }}
USE_NX_AFFECTED: ${{ github.event_name == 'pull_request' && github.base_ref != 'master'}} USE_NX_AFFECTED: ${{ github.event_name == 'pull_request' && github.base_ref != 'master'}}
NX_CLOUD_ACCESS_TOKEN: ${{ secrets.NX_CLOUD_ACCESS_TOKEN }}
jobs: jobs:
lint: lint:
runs-on: ubuntu-latest runs-on: ubuntu-latest
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: Checkout repo and submodules - name: Checkout repo and submodules
uses: actions/checkout@v3 uses: actions/checkout@v3
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'Budibase/budibase' if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'Budibase/budibase'
@ -121,7 +114,7 @@ jobs:
name: codecov-umbrella name: codecov-umbrella
verbose: true verbose: true
test-services: test-worker:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout repo and submodules - name: Checkout repo and submodules
@ -143,12 +136,48 @@ jobs:
node-version: 18.x node-version: 18.x
cache: "yarn" cache: "yarn"
- run: yarn --frozen-lockfile - run: yarn --frozen-lockfile
- name: Test worker and server - name: Test worker
run: | run: |
if ${{ env.USE_NX_AFFECTED }}; then if ${{ env.USE_NX_AFFECTED }}; then
yarn test --scope=@budibase/worker --scope=@budibase/server --since=${{ env.NX_BASE_BRANCH }} yarn test --scope=@budibase/worker --since=${{ env.NX_BASE_BRANCH }}
else else
yarn test --scope=@budibase/worker --scope=@budibase/server yarn test --scope=@budibase/worker
fi
- uses: codecov/codecov-action@v3
with:
token: ${{ secrets.CODECOV_TOKEN || github.token }} # not required for public repos
name: codecov-umbrella
verbose: true
test-server:
runs-on: ubuntu-latest
steps:
- name: Checkout repo and submodules
uses: actions/checkout@v3
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'Budibase/budibase'
with:
submodules: true
token: ${{ secrets.PERSONAL_ACCESS_TOKEN || github.token }}
fetch-depth: 0
- name: Checkout repo only
uses: actions/checkout@v3
if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name != 'Budibase/budibase'
with:
fetch-depth: 0
- name: Use Node.js 18.x
uses: actions/setup-node@v3
with:
node-version: 18.x
cache: "yarn"
- run: yarn --frozen-lockfile
- name: Test server
run: |
if ${{ env.USE_NX_AFFECTED }}; then
yarn test --scope=@budibase/server --since=${{ env.NX_BASE_BRANCH }}
else
yarn test --scope=@budibase/server
fi fi
- uses: codecov/codecov-action@v3 - uses: codecov/codecov-action@v3
@ -232,20 +261,21 @@ jobs:
branch="${{ github.base_ref || github.ref_name }}" branch="${{ github.base_ref || github.ref_name }}"
echo "Running on branch '$branch' (base_ref=${{ github.base_ref }}, ref_name=${{ github.head_ref }})" echo "Running on branch '$branch' (base_ref=${{ github.base_ref }}, ref_name=${{ github.head_ref }})"
if [[ $branch == "master" ]]; then base_commit=$(git rev-parse origin/master)
base_commit=$(git rev-parse origin/master)
if [[ ! -z $base_commit ]]; then
echo "target_branch=$branch"
echo "target_branch=$branch" >> "$GITHUB_OUTPUT"
echo "pro_commit=$pro_commit"
echo "pro_commit=$pro_commit" >> "$GITHUB_OUTPUT"
echo "base_commit=$base_commit"
echo "base_commit=$base_commit" >> "$GITHUB_OUTPUT"
else else
base_commit=$(git rev-parse origin/develop) echo "Nothing to do - branch to branch merge."
fi fi
echo "target_branch=$branch" - name: Check submodule merged to base branch
echo "target_branch=$branch" >> "$GITHUB_OUTPUT" if: ${{ steps.get_pro_commits.outputs.base_commit != '' }}
echo "pro_commit=$pro_commit"
echo "pro_commit=$pro_commit" >> "$GITHUB_OUTPUT"
echo "base_commit=$base_commit"
echo "base_commit=$base_commit" >> "$GITHUB_OUTPUT"
- name: Check submodule merged to develop
uses: actions/github-script@v4 uses: actions/github-script@v4
with: with:
github-token: ${{ secrets.GITHUB_TOKEN }} github-token: ${{ secrets.GITHUB_TOKEN }}
@ -254,9 +284,9 @@ 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 "${{ steps.get_pro_commits.outputs.target_branch }}"" 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 {
console.log('All good, the submodule had been merged and setup correctly!') console.log('All good, the submodule had been merged and setup correctly!')
} }

View File

@ -1,29 +0,0 @@
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,27 @@
name: close-featurebranch
on:
pull_request:
types: [closed]
branches:
- master
workflow_dispatch:
inputs:
BRANCH:
type: string
description: Which featurebranch branch to destroy?
required: true
jobs:
release:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: passeidireto/trigger-external-workflow-action@main
env:
PAYLOAD_BRANCH: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.BRANCH || github.head_ref }}
PAYLOAD_PR_NUMBER: ${{ github.event.pull_request.number }}
with:
repository: budibase/budibase-deploys
event: featurebranch-qa-close
github_pat: ${{ secrets.GH_ACCESS_TOKEN }}

View File

@ -3,7 +3,7 @@ name: deploy-featurebranch
on: on:
pull_request: pull_request:
branches: branches:
- develop - master
jobs: jobs:
release: release:
@ -12,7 +12,8 @@ jobs:
- uses: actions/checkout@v3 - uses: actions/checkout@v3
- uses: passeidireto/trigger-external-workflow-action@main - uses: passeidireto/trigger-external-workflow-action@main
env: env:
BRANCH: ${{ github.head_ref }} PAYLOAD_BRANCH: ${{ github.head_ref }}
PAYLOAD_PR_NUMBER: ${{ github.event.pull_request.number }}
with: with:
repository: budibase/budibase-deploys repository: budibase/budibase-deploys
event: featurebranch-qa-deploy event: featurebranch-qa-deploy

View File

@ -1,41 +0,0 @@
name: "deploy-preprod"
on:
workflow_dispatch:
workflow_call:
jobs:
deploy-to-legacy-preprod-env:
runs-on: ubuntu-latest
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
- uses: actions/checkout@v2
with:
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: Get the latest budibase release version
id: version
run: |
release_version=$(cat lerna.json | jq -r '.version')
echo "RELEASE_VERSION=$release_version" >> $GITHUB_ENV
- uses: passeidireto/trigger-external-workflow-action@main
env:
PAYLOAD_VERSION: ${{ env.RELEASE_VERSION }}
with:
repository: budibase/budibase-deploys
event: budicloud-preprod-deploy
github_pat: ${{ secrets.GH_ACCESS_TOKEN }}

View File

@ -1,124 +0,0 @@
name: Budibase Prerelease
concurrency:
group: release-prerelease
cancel-in-progress: false
on:
push:
tags:
- "*-alpha.*"
workflow_dispatch:
env:
# Posthog token used by ui at build time
# disable unless needed for testing
# POSTHOG_TOKEN: phc_uDYOfnFt6wAbBAXkC6STjcrTpAFiWIhqgFcsC1UVO5F
INTERCOM_TOKEN: ${{ secrets.INTERCOM_TOKEN }}
PERSONAL_ACCESS_TOKEN: ${{ secrets.PERSONAL_ACCESS_TOKEN }}
FEATURE_PREVIEW_URL: https://budirelease.live
jobs:
release-images:
runs-on: ubuntu-latest
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
- uses: actions/checkout@v2
with:
submodules: true
token: ${{ secrets.PERSONAL_ACCESS_TOKEN }}
fetch-depth: 0
- name: Fail if tag is not develop
run: |
if ! git merge-base --is-ancestor ${{ github.sha }} origin/develop; then
echo "Tag is not in develop"
exit 1
fi
- uses: actions/setup-node@v1
with:
node-version: 18.x
- run: yarn install --frozen-lockfile
- name: Update versions
run: ./scripts/updateVersions.sh
- run: yarn build
- run: yarn build:sdk
- name: Publish budibase packages to NPM
env:
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
run: |
# setup the username and email.
git config --global user.name "Budibase Staging Release Bot"
git config --global user.email "<>"
git submodule foreach git commit -a -m 'Release process'
git commit -a -m 'Release process'
echo //registry.npmjs.org/:_authToken=${NPM_TOKEN} >> .npmrc
yarn release:develop
- name: Build/release Docker images
run: |
docker login -u $DOCKER_USER -p $DOCKER_PASSWORD
yarn build:docker:develop
env:
DOCKER_USER: ${{ secrets.DOCKER_USERNAME }}
DOCKER_PASSWORD: ${{ secrets.DOCKER_API_KEY }}
release-helm-chart:
needs: [release-images]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Setup Helm
uses: azure/setup-helm@v1
id: helm-install
# due to helm repo index issue: https://github.com/helm/helm/issues/7363
# we need to create new package in a different dir, merge the index and move the package back
- name: Build and release helm chart
run: |
git config user.name "Budibase Helm Bot"
git config user.email "<>"
git reset --hard
git fetch
mkdir sync
echo "Packaging chart to sync dir"
helm package charts/budibase --version 0.0.0-develop --app-version develop --destination sync
echo "Packaging successful"
git checkout gh-pages
echo "Indexing helm repo"
helm repo index --merge docs/index.yaml sync
mv -f sync/* docs
rm -rf sync
echo "Pushing new helm release"
git add -A
git commit -m "Helm Release: develop"
git push
trigger-deploy-to-qa-env:
needs: [release-helm-chart]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Get the current budibase release version
id: version
run: |
release_version=$(cat lerna.json | jq -r '.version')
echo "RELEASE_VERSION=$release_version" >> $GITHUB_ENV
- uses: passeidireto/trigger-external-workflow-action@main
env:
PAYLOAD_VERSION: ${{ env.RELEASE_VERSION }}
with:
repository: budibase/budibase-deploys
event: budicloud-qa-deploy
github_pat: ${{ secrets.GH_ACCESS_TOKEN }}

View File

@ -110,19 +110,13 @@ jobs:
git commit -m "Helm Release: ${{ env.RELEASE_VERSION }}" git commit -m "Helm Release: ${{ env.RELEASE_VERSION }}"
git push git push
deploy-to-legacy-preprod-env:
needs: [release-images]
uses: ./.github/workflows/deploy-preprod.yml
secrets: inherit
# Trigger deploy to new EKS preprod environment trigger-deploy-to-qa-env:
trigger-deploy-to-preprod-env:
needs: [release-helm-chart] needs: [release-helm-chart]
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
- name: Get the current budibase release version
- name: Get the latest budibase release version
id: version id: version
run: | run: |
release_version=$(cat lerna.json | jq -r '.version') release_version=$(cat lerna.json | jq -r '.version')
@ -133,5 +127,5 @@ jobs:
PAYLOAD_VERSION: ${{ env.RELEASE_VERSION }} PAYLOAD_VERSION: ${{ env.RELEASE_VERSION }}
with: with:
repository: budibase/budibase-deploys repository: budibase/budibase-deploys
event: budicloud-preprod-deploy event: budicloud-qa-deploy
github_pat: ${{ secrets.GH_ACCESS_TOKEN }} github_pat: ${{ secrets.GH_ACCESS_TOKEN }}

View File

@ -0,0 +1,69 @@
name: Test
on:
workflow_dispatch:
env:
CI: true
PERSONAL_ACCESS_TOKEN: ${{ secrets.PERSONAL_ACCESS_TOKEN }}
REGISTRY_URL: registry.hub.docker.com
NX_CLOUD_ACCESS_TOKEN: ${{ secrets.NX_CLOUD_ACCESS_TOKEN }}
jobs:
build:
name: "build"
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [18.x]
steps:
- name: "Checkout"
uses: actions/checkout@v4
with:
submodules: true
token: ${{ secrets.PERSONAL_ACCESS_TOKEN }}
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v3
with:
node-version: ${{ matrix.node-version }}
cache: "yarn"
- name: Setup QEMU
uses: docker/setup-qemu-action@v3
- name: Setup Docker Buildx
id: buildx
uses: docker/setup-buildx-action@v3
- name: Run Yarn
run: yarn
- name: Run Yarn Build
run: yarn build --scope @budibase/server --scope @budibase/worker
- 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@v5
with:
context: .
push: true
pull: true
platforms: linux/amd64,linux/arm64
tags: budibase/budibase-test:test
file: ./hosting/single/Dockerfile.v2
cache-from: type=registry,ref=budibase/budibase-test:test
cache-to: type=inline
- 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-test:aas
file: ./hosting/single/Dockerfile.v2

View File

@ -18,7 +18,7 @@ jobs:
- name: Maximize build space - name: Maximize build space
uses: easimon/maximize-build-space@master uses: easimon/maximize-build-space@master
with: with:
root-reserve-mb: 35000 root-reserve-mb: 30000
swap-size-mb: 1024 swap-size-mb: 1024
remove-android: 'true' remove-android: 'true'
remove-dotnet: 'true' remove-dotnet: 'true'
@ -33,14 +33,6 @@ jobs:
with: with:
submodules: true submodules: true
token: ${{ secrets.PERSONAL_ACCESS_TOKEN }} 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 }} - name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v1 uses: actions/setup-node@v1
@ -55,10 +47,6 @@ jobs:
run: yarn run: yarn
- name: Update versions - name: Update versions
run: ./scripts/updateVersions.sh run: ./scripts/updateVersions.sh
- name: Runt Yarn Lint
run: yarn lint
- name: Update versions
run: ./scripts/updateVersions.sh
- name: Run Yarn Build - name: Run Yarn Build
run: yarn build:docker:pre run: yarn build:docker:pre
- name: Login to Docker Hub - name: Login to Docker Hub

View File

@ -2,7 +2,7 @@ name: Close stale issues and PRs # https://github.com/actions/stale
on: on:
workflow_dispatch: workflow_dispatch:
schedule: schedule:
- cron: '*/30 * * * *' # Every 30 mins - cron: "*/30 * * * *" # Every 30 mins
jobs: jobs:
stale: stale:
@ -10,20 +10,37 @@ jobs:
steps: steps:
- uses: actions/stale@v8 - uses: actions/stale@v8
with: with:
# stale rules operations-per-run: 1
days-before-stale: 60 # stale rules for PRs
days-before-pr-stale: 7 days-before-pr-stale: 7
stale-issue-label: stale stale-issue-label: stale
stale-issue-message: "This issue has been automatically marked as stale because it has not had any activity for 60 days."
# close rules
# days after being marked as stale to close
days-before-close: 30
close-issue-label: closed-stale
close-issue-message: This issue has been automatically closed it has not had any activity in 90 days."
days-before-pr-close: 7
# exemptions
exempt-pr-labels: pinned,security,roadmap exempt-pr-labels: pinned,security,roadmap
days-before-pr-close: 7
- uses: actions/stale@v8
with:
operations-per-run: 3
# stale rules for high priority bugs
days-before-stale: 30
only-issue-labels: bug,High priority
stale-issue-label: warn
- uses: actions/stale@v8
with:
operations-per-run: 3
# stale rules for medium priority bugs
days-before-stale: 90
only-issue-labels: bug,Medium priority
stale-issue-label: warn
- uses: actions/stale@v8
with:
operations-per-run: 3
# stale rules for all bugs
days-before-stale: 180
stale-issue-label: stale
only-issue-labels: bug
stale-issue-message: "This issue has been automatically marked as stale because it has not had any activity for six months."
days-before-close: 30

View File

@ -1,42 +0,0 @@
name: Tag prerelease
concurrency:
group: tag-prerelease
cancel-in-progress: false
on:
push:
branches:
- develop
paths:
- ".aws/**"
- ".github/**"
- "charts/**"
- "packages/**"
- "scripts/**"
- "package.json"
- "yarn.lock"
workflow_dispatch:
jobs:
tag-prerelease:
runs-on: ubuntu-latest
steps:
- name: Fail if branch is not develop
if: github.ref != 'refs/heads/develop'
run: |
echo "Ref is not develop, you must run this job from develop."
exit 1
- uses: actions/checkout@v2
with:
submodules: true
token: ${{ secrets.PERSONAL_ACCESS_TOKEN }}
- run: cd scripts && yarn
- name: Tag prerelease
run: |
cd scripts
# setup the username and email.
git config --global user.name "Budibase Staging Release Bot"
git config --global user.email "<>"
./versionCommit.sh prerelease

View File

@ -4,17 +4,6 @@ concurrency:
cancel-in-progress: false cancel-in-progress: false
on: on:
push:
branches:
- master
paths:
- ".aws/**"
- ".github/**"
- "charts/**"
- "packages/**"
- "scripts/**"
- "package.json"
- "yarn.lock"
workflow_dispatch: workflow_dispatch:
inputs: inputs:
versioning: versioning:

View File

@ -1 +1 @@
network-timeout 100000 network-timeout 1000000

View File

@ -138,6 +138,8 @@ To develop the Budibase platform you'll need [Docker](https://www.docker.com/) a
`yarn setup` will check that all necessary components are installed and setup the repo for usage. `yarn setup` will check that all necessary components are installed and setup the repo for usage.
If you have access to the `@budibase/pro` submodule then please follow the Pro section of this guide before running the above command.
##### Manual method ##### Manual method
The following commands can be executed to manually get Budibase up and running (assuming Docker/Docker Compose has been installed). The following commands can be executed to manually get Budibase up and running (assuming Docker/Docker Compose has been installed).
@ -146,6 +148,8 @@ The following commands can be executed to manually get Budibase up and running (
`yarn build` will build all budibase packages. `yarn build` will build all budibase packages.
If you have access to the `@budibase/pro` submodule then please follow the Pro section of this guide before running the above commands.
#### 4. Running #### 4. Running
To run the budibase server and builder in dev mode (i.e. with live reloading): To run the budibase server and builder in dev mode (i.e. with live reloading):

View File

@ -55,7 +55,7 @@ http {
set $csp_style "style-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net https://fonts.googleapis.com https://rsms.me https://maxcdn.bootstrapcdn.com"; set $csp_style "style-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net https://fonts.googleapis.com https://rsms.me https://maxcdn.bootstrapcdn.com";
set $csp_object "object-src 'none'"; set $csp_object "object-src 'none'";
set $csp_base_uri "base-uri 'self'"; set $csp_base_uri "base-uri 'self'";
set $csp_connect "connect-src 'self' https://*.budibase.net https://api-iam.intercom.io https://api-iam.intercom.io https://api-ping.intercom.io https://app.posthog.com wss://nexus-websocket-a.intercom.io wss://nexus-websocket-b.intercom.io https://nexus-websocket-a.intercom.io https://nexus-websocket-b.intercom.io https://uploads.intercomcdn.com https://uploads.intercomusercontent.com https://*.amazonaws.com https://*.s3.amazonaws.com https://*.s3.us-east-2.amazonaws.com https://*.s3.us-east-1.amazonaws.com https://*.s3.us-west-1.amazonaws.com https://*.s3.us-west-2.amazonaws.com https://*.s3.af-south-1.amazonaws.com https://*.s3.ap-east-1.amazonaws.com https://*.s3.ap-southeast-3.amazonaws.com https://*.s3.ap-south-1.amazonaws.com https://*.s3.ap-northeast-3.amazonaws.com https://*.s3.ap-northeast-2.amazonaws.com https://*.s3.ap-southeast-1.amazonaws.com https://*.s3.ap-southeast-2.amazonaws.com https://*.s3.ap-northeast-1.amazonaws.com https://*.s3.ca-central-1.amazonaws.com https://*.s3.cn-north-1.amazonaws.com https://*.s3.cn-northwest-1.amazonaws.com https://*.s3.eu-central-1.amazonaws.com https://*.s3.eu-west-1.amazonaws.com https://*.s3.eu-west-2.amazonaws.com https://*.s3.eu-south-1.amazonaws.com https://*.s3.eu-west-3.amazonaws.com https://*.s3.eu-north-1.amazonaws.com https://*.s3.sa-east-1.amazonaws.com https://*.s3.me-south-1.amazonaws.com https://*.s3.us-gov-east-1.amazonaws.com https://*.s3.us-gov-west-1.amazonaws.com https://api.github.com"; set $csp_connect "connect-src 'self' https://*.budibase.app https://*.budibaseqa.app https://*.budibase.net https://api-iam.intercom.io https://api-iam.intercom.io https://api-ping.intercom.io https://app.posthog.com wss://nexus-websocket-a.intercom.io wss://nexus-websocket-b.intercom.io https://nexus-websocket-a.intercom.io https://nexus-websocket-b.intercom.io https://uploads.intercomcdn.com https://uploads.intercomusercontent.com https://*.amazonaws.com https://*.s3.amazonaws.com https://*.s3.us-east-2.amazonaws.com https://*.s3.us-east-1.amazonaws.com https://*.s3.us-west-1.amazonaws.com https://*.s3.us-west-2.amazonaws.com https://*.s3.af-south-1.amazonaws.com https://*.s3.ap-east-1.amazonaws.com https://*.s3.ap-southeast-3.amazonaws.com https://*.s3.ap-south-1.amazonaws.com https://*.s3.ap-northeast-3.amazonaws.com https://*.s3.ap-northeast-2.amazonaws.com https://*.s3.ap-southeast-1.amazonaws.com https://*.s3.ap-southeast-2.amazonaws.com https://*.s3.ap-northeast-1.amazonaws.com https://*.s3.ca-central-1.amazonaws.com https://*.s3.cn-north-1.amazonaws.com https://*.s3.cn-northwest-1.amazonaws.com https://*.s3.eu-central-1.amazonaws.com https://*.s3.eu-west-1.amazonaws.com https://*.s3.eu-west-2.amazonaws.com https://*.s3.eu-south-1.amazonaws.com https://*.s3.eu-west-3.amazonaws.com https://*.s3.eu-north-1.amazonaws.com https://*.s3.sa-east-1.amazonaws.com https://*.s3.me-south-1.amazonaws.com https://*.s3.us-gov-east-1.amazonaws.com https://*.s3.us-gov-west-1.amazonaws.com https://api.github.com";
set $csp_font "font-src 'self' data: https://cdn.jsdelivr.net https://fonts.gstatic.com https://rsms.me https://maxcdn.bootstrapcdn.com https://js.intercomcdn.com https://fonts.intercomcdn.com"; set $csp_font "font-src 'self' data: https://cdn.jsdelivr.net https://fonts.gstatic.com https://rsms.me https://maxcdn.bootstrapcdn.com https://js.intercomcdn.com https://fonts.intercomcdn.com";
set $csp_frame "frame-src 'self' https:"; set $csp_frame "frame-src 'self' https:";
set $csp_img "img-src http: https: data: blob:"; set $csp_img "img-src http: https: data: blob:";

View File

@ -12,14 +12,14 @@ RUN chmod +x /cleanup.sh
WORKDIR /app WORKDIR /app
ADD packages/server . ADD packages/server .
COPY yarn.lock . COPY yarn.lock .
RUN yarn install --production=true RUN yarn install --production=true --network-timeout 100000
RUN /cleanup.sh RUN /cleanup.sh
# build worker # build worker
WORKDIR /worker WORKDIR /worker
ADD packages/worker . ADD packages/worker .
COPY yarn.lock . COPY yarn.lock .
RUN yarn install --production=true RUN yarn install --production=true --network-timeout 100000
RUN /cleanup.sh RUN /cleanup.sh
FROM budibase/couchdb FROM budibase/couchdb

View File

@ -0,0 +1,126 @@
FROM node:18-slim as build
# install node-gyp dependencies
RUN apt-get update && apt-get install -y --no-install-recommends g++ make python3 jq
# copy and install dependencies
WORKDIR /app
COPY package.json .
COPY yarn.lock .
COPY lerna.json .
COPY .yarnrc .
COPY packages/server/package.json packages/server/package.json
COPY packages/worker/package.json packages/worker/package.json
# string-templates does not get bundled during the esbuild process, so we want to use the local version
COPY packages/string-templates/package.json packages/string-templates/package.json
COPY scripts/removeWorkspaceDependencies.sh scripts/removeWorkspaceDependencies.sh
RUN chmod +x ./scripts/removeWorkspaceDependencies.sh
RUN ./scripts/removeWorkspaceDependencies.sh
# We will never want to sync pro, but the script is still required
RUN echo '' > scripts/syncProPackage.js
RUN jq 'del(.scripts.postinstall)' package.json > temp.json && mv temp.json package.json
RUN --mount=type=cache,target=/root/.yarn YARN_CACHE_FOLDER=/root/.yarn yarn install --production
# copy the actual code
COPY packages/server/dist packages/server/dist
COPY packages/server/pm2.config.js packages/server/pm2.config.js
COPY packages/server/client packages/server/client
COPY packages/server/builder packages/server/builder
COPY packages/worker/dist packages/worker/dist
COPY packages/worker/pm2.config.js packages/worker/pm2.config.js
COPY packages/string-templates packages/string-templates
FROM budibase/couchdb as runner
ARG TARGETARCH
ENV TARGETARCH $TARGETARCH
#TARGETBUILD can be set to single (for single docker image) or aas (for azure app service)
# e.g. docker build --build-arg TARGETBUILD=aas ....
ARG TARGETBUILD=single
ENV TARGETBUILD $TARGETBUILD
# install base dependencies
RUN apt-get update && \
apt-get install -y --no-install-recommends software-properties-common nginx uuid-runtime redis-server
# Install postgres client for pg_dump utils
RUN apt install software-properties-common apt-transport-https gpg -y \
&& curl -fsSl https://www.postgresql.org/media/keys/ACCC4CF8.asc | gpg --dearmor | tee /usr/share/keyrings/postgresql.gpg > /dev/null \
&& echo deb [arch=amd64,arm64,ppc64el signed-by=/usr/share/keyrings/postgresql.gpg] http://apt.postgresql.org/pub/repos/apt/ $(lsb_release -cs)-pgdg main | tee /etc/apt/sources.list.d/postgresql.list \
&& apt update -y \
&& apt install postgresql-client-15 -y \
&& apt remove software-properties-common apt-transport-https gpg -y
# install other dependencies, nodejs, oracle requirements, jdk8, redis, nginx
WORKDIR /nodejs
RUN curl -sL https://deb.nodesource.com/setup_18.x -o /tmp/nodesource_setup.sh && \
bash /tmp/nodesource_setup.sh && \
apt-get install -y --no-install-recommends libaio1 nodejs && \
npm install --global yarn pm2
# setup nginx
COPY hosting/single/nginx/nginx.conf /etc/nginx
COPY hosting/single/nginx/nginx-default-site.conf /etc/nginx/sites-enabled/default
RUN mkdir -p /var/log/nginx && \
touch /var/log/nginx/error.log && \
touch /var/run/nginx.pid && \
usermod -a -G tty www-data
WORKDIR /
RUN mkdir -p scripts/integrations/oracle
COPY packages/server/scripts/integrations/oracle scripts/integrations/oracle
RUN /bin/bash -e ./scripts/integrations/oracle/instantclient/linux/install.sh
# setup minio
WORKDIR /minio
COPY scripts/install-minio.sh ./install.sh
RUN chmod +x install.sh && ./install.sh
# setup runner file
WORKDIR /
COPY hosting/single/runner.sh .
RUN chmod +x ./runner.sh
COPY hosting/single/healthcheck.sh .
RUN chmod +x ./healthcheck.sh
# Script below sets the path for storing data based on $DATA_DIR
# For Azure App Service install SSH & point data locations to /home
COPY hosting/single/ssh/sshd_config /etc/
COPY hosting/single/ssh/ssh_setup.sh /tmp
RUN /build-target-paths.sh
# setup letsencrypt certificate
RUN apt-get install -y certbot python3-certbot-nginx
COPY hosting/letsencrypt /app/letsencrypt
RUN chmod +x /app/letsencrypt/certificate-request.sh /app/letsencrypt/certificate-renew.sh
COPY --from=build /app/node_modules /node_modules
COPY --from=build /app/package.json /package.json
COPY --from=build /app/packages/server /app
COPY --from=build /app/packages/worker /worker
COPY --from=build /app/packages/string-templates /string-templates
RUN cd /string-templates && yarn link && cd ../app && yarn link @budibase/string-templates && cd ../worker && yarn link @budibase/string-templates
EXPOSE 80
EXPOSE 443
# Expose port 2222 for SSH on Azure App Service build
EXPOSE 2222
VOLUME /data
HEALTHCHECK --interval=15s --timeout=15s --start-period=45s CMD "/healthcheck.sh"
# must set this just before running
ENV NODE_ENV=production
WORKDIR /
CMD ["./runner.sh"]

View File

@ -7,16 +7,16 @@ declare -a DOCKER_VARS=("APP_PORT" "APPS_URL" "ARCHITECTURE" "BUDIBASE_ENVIRONME
[[ -z "${BUDIBASE_ENVIRONMENT}" ]] && export BUDIBASE_ENVIRONMENT=PRODUCTION [[ -z "${BUDIBASE_ENVIRONMENT}" ]] && export BUDIBASE_ENVIRONMENT=PRODUCTION
[[ -z "${CLUSTER_PORT}" ]] && export CLUSTER_PORT=80 [[ -z "${CLUSTER_PORT}" ]] && export CLUSTER_PORT=80
[[ -z "${DEPLOYMENT_ENVIRONMENT}" ]] && export DEPLOYMENT_ENVIRONMENT=docker [[ -z "${DEPLOYMENT_ENVIRONMENT}" ]] && export DEPLOYMENT_ENVIRONMENT=docker
[[ -z "${MINIO_URL}" ]] && export MINIO_URL=http://localhost:9000 [[ -z "${MINIO_URL}" ]] && export MINIO_URL=http://127.0.0.1:9000
[[ -z "${NODE_ENV}" ]] && export NODE_ENV=production [[ -z "${NODE_ENV}" ]] && export NODE_ENV=production
[[ -z "${POSTHOG_TOKEN}" ]] && export POSTHOG_TOKEN=phc_bIjZL7oh2GEUd2vqvTBH8WvrX0fWTFQMs6H5KQxiUxU [[ -z "${POSTHOG_TOKEN}" ]] && export POSTHOG_TOKEN=phc_bIjZL7oh2GEUd2vqvTBH8WvrX0fWTFQMs6H5KQxiUxU
[[ -z "${TENANT_FEATURE_FLAGS}" ]] && export TENANT_FEATURE_FLAGS="*:LICENSING,*:USER_GROUPS,*:ONBOARDING_TOUR" [[ -z "${TENANT_FEATURE_FLAGS}" ]] && export TENANT_FEATURE_FLAGS="*:LICENSING,*:USER_GROUPS,*:ONBOARDING_TOUR"
[[ -z "${ACCOUNT_PORTAL_URL}" ]] && export ACCOUNT_PORTAL_URL=https://account.budibase.app [[ -z "${ACCOUNT_PORTAL_URL}" ]] && export ACCOUNT_PORTAL_URL=https://account.budibase.app
[[ -z "${REDIS_URL}" ]] && export REDIS_URL=localhost:6379 [[ -z "${REDIS_URL}" ]] && export REDIS_URL=127.0.0.1:6379
[[ -z "${SELF_HOSTED}" ]] && export SELF_HOSTED=1 [[ -z "${SELF_HOSTED}" ]] && export SELF_HOSTED=1
[[ -z "${WORKER_PORT}" ]] && export WORKER_PORT=4002 [[ -z "${WORKER_PORT}" ]] && export WORKER_PORT=4002
[[ -z "${WORKER_URL}" ]] && export WORKER_URL=http://localhost:4002 [[ -z "${WORKER_URL}" ]] && export WORKER_URL=http://127.0.0.1:4002
[[ -z "${APPS_URL}" ]] && export APPS_URL=http://localhost:4001 [[ -z "${APPS_URL}" ]] && export APPS_URL=http://127.0.0.1:4001
[[ -z "${SERVER_TOP_LEVEL_PATH}" ]] && export SERVER_TOP_LEVEL_PATH=/app [[ -z "${SERVER_TOP_LEVEL_PATH}" ]] && export SERVER_TOP_LEVEL_PATH=/app
# export CUSTOM_DOMAIN=budi001.custom.com # export CUSTOM_DOMAIN=budi001.custom.com
@ -51,7 +51,7 @@ do
fi fi
done done
if [[ -z "${COUCH_DB_URL}" ]]; then if [[ -z "${COUCH_DB_URL}" ]]; then
export COUCH_DB_URL=http://$COUCHDB_USER:$COUCHDB_PASSWORD@localhost:5984 export COUCH_DB_URL=http://$COUCHDB_USER:$COUCHDB_PASSWORD@127.0.0.1:5984
fi fi
if [ ! -f "${DATA_DIR}/.env" ]; then if [ ! -f "${DATA_DIR}/.env" ]; then
touch ${DATA_DIR}/.env touch ${DATA_DIR}/.env

View File

@ -1,5 +1,5 @@
{ {
"version": "2.10.12-alpha.2", "version": "2.11.35",
"npmClient": "yarn", "npmClient": "yarn",
"packages": [ "packages": [
"packages/*" "packages/*"

View File

@ -8,5 +8,9 @@
} }
} }
}, },
"targetDefaults": {} "targetDefaults": {
"build": {
"inputs": ["{workspaceRoot}/scripts/build.js"]
}
}
} }

View File

@ -3,14 +3,11 @@
"private": true, "private": true,
"devDependencies": { "devDependencies": {
"@esbuild-plugins/tsconfig-paths": "^0.1.2", "@esbuild-plugins/tsconfig-paths": "^0.1.2",
"@nx/js": "16.4.3", "@typescript-eslint/parser": "6.7.2",
"@rollup/plugin-json": "^4.0.2",
"@typescript-eslint/parser": "5.45.0",
"esbuild": "^0.18.17", "esbuild": "^0.18.17",
"esbuild-node-externals": "^1.8.0", "esbuild-node-externals": "^1.8.0",
"eslint": "^8.44.0", "eslint": "^8.44.0",
"husky": "^8.0.3", "husky": "^8.0.3",
"js-yaml": "^4.1.0",
"kill-port": "^1.6.1", "kill-port": "^1.6.1",
"lerna": "7.1.1", "lerna": "7.1.1",
"madge": "^6.0.0", "madge": "^6.0.0",
@ -19,10 +16,8 @@
"nx-cloud": "16.0.5", "nx-cloud": "16.0.5",
"prettier": "2.8.8", "prettier": "2.8.8",
"prettier-plugin-svelte": "^2.3.0", "prettier-plugin-svelte": "^2.3.0",
"rimraf": "^3.0.2", "svelte": "3.49.0",
"rollup-plugin-replace": "^2.2.0", "typescript": "5.2.2",
"svelte": "^3.38.2",
"typescript": "4.7.3",
"@babel/core": "^7.22.5", "@babel/core": "^7.22.5",
"@babel/eslint-parser": "^7.22.5", "@babel/eslint-parser": "^7.22.5",
"@babel/preset-env": "^7.22.5", "@babel/preset-env": "^7.22.5",
@ -51,7 +46,7 @@
"dev:noserver": "yarn run kill-builder && lerna run --stream dev:stack:up && lerna run --stream dev:builder --ignore @budibase/backend-core --ignore @budibase/server --ignore @budibase/worker", "dev:noserver": "yarn run kill-builder && lerna run --stream dev:stack:up && lerna run --stream dev:builder --ignore @budibase/backend-core --ignore @budibase/server --ignore @budibase/worker",
"dev:server": "yarn run kill-server && lerna run --stream dev:builder --scope @budibase/worker --scope @budibase/server", "dev:server": "yarn run kill-server && lerna run --stream dev:builder --scope @budibase/worker --scope @budibase/server",
"dev:built": "yarn run kill-all && cd packages/server && yarn dev:stack:up && cd ../../ && lerna run --stream dev:built", "dev:built": "yarn run kill-all && cd packages/server && yarn dev:stack:up && cd ../../ && lerna run --stream dev:built",
"dev:docker": "yarn build:docker:pre && docker-compose -f hosting/docker-compose.build.yaml -f hosting/docker-compose.dev.yaml --env-file hosting/.env up --build --scale proxy-service=0", "dev:docker": "yarn build && docker-compose -f hosting/docker-compose.build.yaml -f hosting/docker-compose.dev.yaml --env-file hosting/.env up --build --scale proxy-service=0",
"test": "lerna run --stream test --stream", "test": "lerna run --stream test --stream",
"lint:eslint": "eslint packages qa-core --max-warnings=0", "lint:eslint": "eslint packages qa-core --max-warnings=0",
"lint:prettier": "prettier --check \"packages/**/*.{js,ts,svelte}\" && prettier --write \"examples/**/*.{js,ts,svelte}\" && prettier --check \"qa-core/**/*.{js,ts,svelte}\"", "lint:prettier": "prettier --check \"packages/**/*.{js,ts,svelte}\" && prettier --write \"examples/**/*.{js,ts,svelte}\" && prettier --check \"qa-core/**/*.{js,ts,svelte}\"",
@ -61,7 +56,6 @@
"lint:fix": "yarn run lint:fix:prettier && yarn run lint:fix:eslint", "lint:fix": "yarn run lint:fix:prettier && yarn run lint:fix:eslint",
"build:specs": "lerna run --stream specs", "build:specs": "lerna run --stream specs",
"build:docker": "lerna run --stream build:docker && yarn build:docker:proxy && cd hosting/scripts/linux/ && ./release-to-docker-hub.sh $BUDIBASE_RELEASE_VERSION && cd -", "build:docker": "lerna run --stream build:docker && yarn build:docker:proxy && cd hosting/scripts/linux/ && ./release-to-docker-hub.sh $BUDIBASE_RELEASE_VERSION && cd -",
"build:docker:pre": "yarn build && lerna run --stream predocker",
"build:docker:proxy": "docker build hosting/proxy -t proxy-service", "build:docker:proxy": "docker build hosting/proxy -t proxy-service",
"build:docker:selfhost": "lerna run --stream build:docker && cd hosting/scripts/linux/ && ./release-to-docker-hub.sh latest && cd -", "build:docker:selfhost": "lerna run --stream build:docker && cd hosting/scripts/linux/ && ./release-to-docker-hub.sh latest && cd -",
"build:docker:develop": "node scripts/pinVersions && lerna run --stream build:docker && yarn build:docker:proxy && cd hosting/scripts/linux/ && ./release-to-docker-hub.sh develop && cd -", "build:docker:develop": "node scripts/pinVersions && lerna run --stream build:docker && yarn build:docker:proxy && cd hosting/scripts/linux/ && ./release-to-docker-hub.sh develop && cd -",
@ -69,12 +63,10 @@
"build:docker:airgap:single": "SINGLE_IMAGE=1 node hosting/scripts/airgapped/airgappedDockerBuild", "build:docker:airgap:single": "SINGLE_IMAGE=1 node hosting/scripts/airgapped/airgappedDockerBuild",
"build:digitalocean": "cd hosting/digitalocean && ./build.sh && cd -", "build:digitalocean": "cd hosting/digitalocean && ./build.sh && cd -",
"build:docker:single:multiarch": "docker buildx build --platform linux/arm64,linux/amd64 -f hosting/single/Dockerfile -t budibase:latest .", "build:docker:single:multiarch": "docker buildx build --platform linux/arm64,linux/amd64 -f hosting/single/Dockerfile -t budibase:latest .",
"build:docker:single:image": "docker build -f hosting/single/Dockerfile -t budibase:latest .", "build:docker:single": "./scripts/build-single-image.sh",
"build:docker:single": "yarn build && lerna run --concurrency 1 predocker && yarn build:docker:single:image",
"build:docker:dependencies": "docker build -f hosting/dependencies/Dockerfile -t budibase/dependencies:latest ./hosting", "build:docker:dependencies": "docker build -f hosting/dependencies/Dockerfile -t budibase/dependencies:latest ./hosting",
"publish:docker:couch": "docker buildx build --platform linux/arm64,linux/amd64 -f hosting/couchdb/Dockerfile -t budibase/couchdb:latest -t budibase/couchdb:v3.2.1 --push ./hosting/couchdb", "publish:docker:couch": "docker buildx build --platform linux/arm64,linux/amd64 -f hosting/couchdb/Dockerfile -t budibase/couchdb:latest -t budibase/couchdb:v3.2.1 --push ./hosting/couchdb",
"publish:docker:dependencies": "docker buildx build --platform linux/arm64,linux/amd64 -f hosting/dependencies/Dockerfile -t budibase/dependencies:latest -t budibase/dependencies:v3.2.1 --push ./hosting", "publish:docker:dependencies": "docker buildx build --platform linux/arm64,linux/amd64 -f hosting/dependencies/Dockerfile -t budibase/dependencies:latest -t budibase/dependencies:v3.2.1 --push ./hosting",
"build:docs": "lerna run --stream build:docs",
"release:helm": "node scripts/releaseHelmChart", "release:helm": "node scripts/releaseHelmChart",
"env:multi:enable": "lerna run --stream env:multi:enable", "env:multi:enable": "lerna run --stream env:multi:enable",
"env:multi:disable": "lerna run --stream env:multi:disable", "env:multi:disable": "lerna run --stream env:multi:disable",

View File

@ -26,24 +26,21 @@
"@budibase/shared-core": "0.0.0", "@budibase/shared-core": "0.0.0",
"@budibase/types": "0.0.0", "@budibase/types": "0.0.0",
"@techpass/passport-openidconnect": "0.3.2", "@techpass/passport-openidconnect": "0.3.2",
"aws-cloudfront-sign": "2.2.0", "aws-cloudfront-sign": "3.0.2",
"aws-sdk": "2.1030.0", "aws-sdk": "2.1030.0",
"bcrypt": "5.1.0", "bcrypt": "5.1.0",
"bcryptjs": "2.4.3", "bcryptjs": "2.4.3",
"bull": "4.10.1", "bull": "4.10.1",
"correlation-id": "4.0.0", "correlation-id": "4.0.0",
"dotenv": "16.0.1", "dotenv": "16.0.1",
"emitter-listener": "1.1.2",
"ioredis": "5.3.2", "ioredis": "5.3.2",
"joi": "17.6.0", "joi": "17.6.0",
"jsonwebtoken": "9.0.0", "jsonwebtoken": "9.0.0",
"koa-passport": "4.1.4", "koa-passport": "4.1.4",
"koa-pino-logger": "4.0.0", "koa-pino-logger": "4.0.0",
"lodash": "4.17.21", "lodash": "4.17.21",
"lodash.isarguments": "3.1.0",
"node-fetch": "2.6.7", "node-fetch": "2.6.7",
"passport-google-oauth": "2.0.0", "passport-google-oauth": "2.0.0",
"passport-jwt": "4.0.0",
"passport-local": "1.0.0", "passport-local": "1.0.0",
"passport-oauth2-refresh": "^2.1.0", "passport-oauth2-refresh": "^2.1.0",
"pino": "8.11.0", "pino": "8.11.0",
@ -59,14 +56,13 @@
"uuid": "8.3.2" "uuid": "8.3.2"
}, },
"devDependencies": { "devDependencies": {
"@jest/test-sequencer": "29.6.2",
"@shopify/jest-koa-mocks": "5.1.1", "@shopify/jest-koa-mocks": "5.1.1",
"@swc/core": "1.3.71", "@swc/core": "1.3.71",
"@swc/jest": "0.2.27", "@swc/jest": "0.2.27",
"@trendyol/jest-testcontainers": "^2.1.1", "@trendyol/jest-testcontainers": "^2.1.1",
"@types/chance": "1.1.3", "@types/chance": "1.1.3",
"@types/jest": "29.5.3", "@types/cookies": "0.7.8",
"@types/koa": "2.13.4", "@types/jest": "29.5.5",
"@types/lodash": "4.14.180", "@types/lodash": "4.14.180",
"@types/node": "18.17.0", "@types/node": "18.17.0",
"@types/node-fetch": "2.6.4", "@types/node-fetch": "2.6.4",
@ -80,14 +76,10 @@
"jest": "29.6.2", "jest": "29.6.2",
"jest-environment-node": "29.6.2", "jest-environment-node": "29.6.2",
"jest-serial-runner": "1.2.1", "jest-serial-runner": "1.2.1",
"koa": "2.13.4",
"nodemon": "2.0.16",
"pino-pretty": "10.0.0", "pino-pretty": "10.0.0",
"pouchdb-adapter-memory": "7.2.2", "pouchdb-adapter-memory": "7.2.2",
"timekeeper": "2.2.0", "timekeeper": "2.2.0",
"ts-node": "10.8.1", "typescript": "5.2.2"
"tsconfig-paths": "4.0.0",
"typescript": "4.7.3"
}, },
"nx": { "nx": {
"targets": { "targets": {

View File

@ -120,7 +120,7 @@ export async function getUsers(
): Promise<{ users: User[]; notFoundIds?: string[] }> { ): Promise<{ users: User[]; notFoundIds?: string[] }> {
const client = await redis.getUserClient() const client = await redis.getUserClient()
// try cache // try cache
let usersFromCache = await client.bulkGet(userIds) let usersFromCache = await client.bulkGet<User>(userIds)
const missingUsersFromCache = userIds.filter(uid => !usersFromCache[uid]) const missingUsersFromCache = userIds.filter(uid => !usersFromCache[uid])
const users = Object.values(usersFromCache) const users = Object.values(usersFromCache)
let notFoundIds let notFoundIds

View File

@ -1,5 +1,10 @@
import { prefixed, DocumentType } from "@budibase/types" import { prefixed, DocumentType } from "@budibase/types"
export { SEPARATOR, UNICODE_MAX, DocumentType } from "@budibase/types" export {
SEPARATOR,
UNICODE_MAX,
DocumentType,
InternalTable,
} from "@budibase/types"
/** /**
* Can be used to create a few different forms of querying a view. * Can be used to create a few different forms of querying a view.
@ -18,7 +23,7 @@ export enum ViewName {
ROUTING = "screen_routes", ROUTING = "screen_routes",
AUTOMATION_LOGS = "automation_logs", AUTOMATION_LOGS = "automation_logs",
ACCOUNT_BY_EMAIL = "account_by_email", ACCOUNT_BY_EMAIL = "account_by_email",
PLATFORM_USERS_LOWERCASE = "platform_users_lowercase", PLATFORM_USERS_LOWERCASE = "platform_users_lowercase_2",
USER_BY_GROUP = "user_by_group", USER_BY_GROUP = "user_by_group",
APP_BACKUP_BY_TRIGGER = "by_trigger", APP_BACKUP_BY_TRIGGER = "by_trigger",
} }
@ -30,10 +35,6 @@ export const DeprecatedViews = {
], ],
} }
export enum InternalTable {
USER_METADATA = "ta_users",
}
export const StaticDatabases = { export const StaticDatabases = {
GLOBAL: { GLOBAL: {
name: "global-db", name: "global-db",

View File

@ -190,6 +190,10 @@ export const createPlatformUserView = async () => {
if (doc.tenantId) { if (doc.tenantId) {
emit(doc._id.toLowerCase(), doc._id) emit(doc._id.toLowerCase(), doc._id)
} }
if (doc.ssoId) {
emit(doc.ssoId, doc._id)
}
}` }`
await createPlatformView(viewJs, ViewName.PLATFORM_USERS_LOWERCASE) await createPlatformView(viewJs, ViewName.PLATFORM_USERS_LOWERCASE)
} }

View File

@ -45,6 +45,11 @@ export function generateGlobalUserID(id?: any) {
return `${DocumentType.USER}${SEPARATOR}${id || newid()}` return `${DocumentType.USER}${SEPARATOR}${id || newid()}`
} }
const isGlobalUserIDRegex = new RegExp(`^${DocumentType.USER}${SEPARATOR}.+`)
export function isGlobalUserID(id: string) {
return isGlobalUserIDRegex.test(id)
}
/** /**
* Generates a new user ID based on the passed in global ID. * Generates a new user ID based on the passed in global ID.
* @param {string} globalId The ID of the global user. * @param {string} globalId The ID of the global user.

View File

@ -1,5 +1,5 @@
import env from "../environment" import env from "../environment"
const cfsign = require("aws-cloudfront-sign") import * as cfsign from "aws-cloudfront-sign"
let PRIVATE_KEY: string | undefined let PRIVATE_KEY: string | undefined
@ -21,7 +21,7 @@ function getPrivateKey() {
const getCloudfrontSignParams = () => { const getCloudfrontSignParams = () => {
return { return {
keypairId: env.CLOUDFRONT_PUBLIC_KEY_ID, keypairId: env.CLOUDFRONT_PUBLIC_KEY_ID!,
privateKeyString: getPrivateKey(), privateKeyString: getPrivateKey(),
expireTime: new Date().getTime() + 1000 * 60 * 60, // 1 hour expireTime: new Date().getTime() + 1000 * 60 * 60, // 1 hour
} }

View File

@ -5,6 +5,7 @@ import {
PlatformUser, PlatformUser,
PlatformUserByEmail, PlatformUserByEmail,
PlatformUserById, PlatformUserById,
PlatformUserBySsoId,
User, User,
} from "@budibase/types" } from "@budibase/types"
@ -45,6 +46,20 @@ function newUserEmailDoc(
} }
} }
function newUserSsoIdDoc(
ssoId: string,
email: string,
userId: string,
tenantId: string
): PlatformUserBySsoId {
return {
_id: ssoId,
userId,
email,
tenantId,
}
}
/** /**
* Add a new user id or email doc if it doesn't exist. * Add a new user id or email doc if it doesn't exist.
*/ */
@ -64,11 +79,24 @@ async function addUserDoc(emailOrId: string, newDocFn: () => PlatformUser) {
} }
} }
export async function addUser(tenantId: string, userId: string, email: string) { export async function addUser(
await Promise.all([ tenantId: string,
userId: string,
email: string,
ssoId?: string
) {
const promises = [
addUserDoc(userId, () => newUserIdDoc(userId, tenantId)), addUserDoc(userId, () => newUserIdDoc(userId, tenantId)),
addUserDoc(email, () => newUserEmailDoc(userId, email, tenantId)), addUserDoc(email, () => newUserEmailDoc(userId, email, tenantId)),
]) ]
if (ssoId) {
promises.push(
addUserDoc(ssoId, () => newUserSsoIdDoc(ssoId, email, userId, tenantId))
)
}
await Promise.all(promises)
} }
// DELETE // DELETE

View File

@ -6,6 +6,7 @@ import {
AutomationStepIdArray, AutomationStepIdArray,
AutomationIOType, AutomationIOType,
AutomationCustomIOType, AutomationCustomIOType,
DatasourceFeature,
} from "@budibase/types" } from "@budibase/types"
import joi from "joi" import joi from "joi"
@ -67,9 +68,27 @@ function validateDatasource(schema: any) {
version: joi.string().optional(), version: joi.string().optional(),
schema: joi.object({ schema: joi.object({
docs: joi.string(), docs: joi.string(),
plus: joi.boolean().optional(),
isSQL: joi.boolean().optional(),
auth: joi
.object({
type: joi.string().required(),
})
.optional(),
features: joi
.object(
Object.fromEntries(
Object.values(DatasourceFeature).map(key => [
key,
joi.boolean().optional(),
])
)
)
.optional(),
relationships: joi.boolean().optional(),
description: joi.string().required(),
friendlyName: joi.string().required(), friendlyName: joi.string().required(),
type: joi.string().allow(...DATASOURCE_TYPES), type: joi.string().allow(...DATASOURCE_TYPES),
description: joi.string().required(),
datasource: joi.object().pattern(joi.string(), fieldValidator).required(), datasource: joi.object().pattern(joi.string(), fieldValidator).required(),
query: joi query: joi
.object() .object()

View File

@ -242,7 +242,7 @@ class RedisWrapper {
} }
} }
async bulkGet(keys: string[]) { async bulkGet<T>(keys: string[]) {
const db = this._db const db = this._db
if (keys.length === 0) { if (keys.length === 0) {
return {} return {}
@ -250,7 +250,7 @@ class RedisWrapper {
const prefixedKeys = keys.map(key => addDbPrefix(db, key)) const prefixedKeys = keys.map(key => addDbPrefix(db, key))
let response = await this.getClient().mget(prefixedKeys) let response = await this.getClient().mget(prefixedKeys)
if (Array.isArray(response)) { if (Array.isArray(response)) {
let final: Record<string, any> = {} let final: Record<string, T> = {}
let count = 0 let count = 0
for (let result of response) { for (let result of response) {
if (result) { if (result) {

View File

@ -1,8 +1,9 @@
import { PermissionType, PermissionLevel } from "@budibase/types" import { PermissionLevel, PermissionType } from "@budibase/types"
export { PermissionType, PermissionLevel } from "@budibase/types"
import flatten from "lodash/flatten" import flatten from "lodash/flatten"
import cloneDeep from "lodash/fp/cloneDeep" import cloneDeep from "lodash/fp/cloneDeep"
export { PermissionType, PermissionLevel } from "@budibase/types"
export type RoleHierarchy = { export type RoleHierarchy = {
permissionId: string permissionId: string
}[] }[]
@ -78,6 +79,7 @@ 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.APP, PermissionLevel.READ),
], ],
}, },
WRITE: { WRITE: {
@ -88,6 +90,7 @@ export const BUILTIN_PERMISSIONS = {
new Permission(PermissionType.TABLE, PermissionLevel.WRITE), new Permission(PermissionType.TABLE, PermissionLevel.WRITE),
new Permission(PermissionType.AUTOMATION, PermissionLevel.EXECUTE), new Permission(PermissionType.AUTOMATION, PermissionLevel.EXECUTE),
new Permission(PermissionType.LEGACY_VIEW, PermissionLevel.READ), new Permission(PermissionType.LEGACY_VIEW, PermissionLevel.READ),
new Permission(PermissionType.APP, PermissionLevel.READ),
], ],
}, },
POWER: { POWER: {
@ -99,6 +102,7 @@ export const BUILTIN_PERMISSIONS = {
new Permission(PermissionType.AUTOMATION, PermissionLevel.EXECUTE), new Permission(PermissionType.AUTOMATION, PermissionLevel.EXECUTE),
new Permission(PermissionType.WEBHOOK, PermissionLevel.READ), new Permission(PermissionType.WEBHOOK, PermissionLevel.READ),
new Permission(PermissionType.LEGACY_VIEW, PermissionLevel.READ), new Permission(PermissionType.LEGACY_VIEW, PermissionLevel.READ),
new Permission(PermissionType.APP, PermissionLevel.READ),
], ],
}, },
ADMIN: { ADMIN: {
@ -111,6 +115,7 @@ export const BUILTIN_PERMISSIONS = {
new Permission(PermissionType.WEBHOOK, PermissionLevel.READ), new Permission(PermissionType.WEBHOOK, PermissionLevel.READ),
new Permission(PermissionType.QUERY, PermissionLevel.ADMIN), new Permission(PermissionType.QUERY, PermissionLevel.ADMIN),
new Permission(PermissionType.LEGACY_VIEW, PermissionLevel.READ), new Permission(PermissionType.LEGACY_VIEW, PermissionLevel.READ),
new Permission(PermissionType.APP, PermissionLevel.READ),
], ],
}, },
} }

View File

@ -215,21 +215,23 @@ async function getAllUserRoles(userRoleId?: string): Promise<RoleDoc[]> {
return roles return roles
} }
export async function getUserRoleIdHierarchy(
userRoleId?: string
): Promise<string[]> {
const roles = await getUserRoleHierarchy(userRoleId)
return roles.map(role => role._id!)
}
/** /**
* Returns an ordered array of the user's inherited role IDs, this can be used * Returns an ordered array of the user's inherited role IDs, this can be used
* to determine if a user can access something that requires a specific role. * to determine if a user can access something that requires a specific role.
* @param {string} userRoleId The user's role ID, this can be found in their access token. * @param {string} userRoleId The user's role ID, this can be found in their access token.
* @param {object} opts Various options, such as whether to only retrieve the IDs (default true). * @returns {Promise<object[]>} returns an ordered array of the roles, with the first being their
* @returns {Promise<string[]|object[]>} returns an ordered array of the roles, with the first being their
* highest level of access and the last being the lowest level. * highest level of access and the last being the lowest level.
*/ */
export async function getUserRoleHierarchy( export async function getUserRoleHierarchy(userRoleId?: string) {
userRoleId?: string,
opts = { idOnly: true }
) {
// special case, if they don't have a role then they are a public user // special case, if they don't have a role then they are a public user
const roles = await getAllUserRoles(userRoleId) return getAllUserRoles(userRoleId)
return opts.idOnly ? roles.map(role => role._id) : roles
} }
// this function checks that the provided permissions are in an array format // this function checks that the provided permissions are in an array format
@ -249,6 +251,11 @@ export function checkForRoleResourceArray(
return rolePerms return rolePerms
} }
export async function getAllRoleIds(appId?: string) {
const roles = await getAllRoles(appId)
return roles.map(role => role._id)
}
/** /**
* Given an app ID this will retrieve all of the roles that are currently within that app. * Given an app ID this will retrieve all of the roles that are currently within that app.
* @return {Promise<object[]>} An array of the role objects that were found. * @return {Promise<object[]>} An array of the role objects that were found.
@ -332,9 +339,7 @@ export class AccessController {
} }
let roleIds = userRoleId ? this.userHierarchies[userRoleId] : null let roleIds = userRoleId ? this.userHierarchies[userRoleId] : null
if (!roleIds && userRoleId) { if (!roleIds && userRoleId) {
roleIds = (await getUserRoleHierarchy(userRoleId, { roleIds = await getUserRoleIdHierarchy(userRoleId)
idOnly: true,
})) as string[]
this.userHierarchies[userRoleId] = roleIds this.userHierarchies[userRoleId] = roleIds
} }

View File

@ -278,7 +278,12 @@ export class UserDB {
builtUser._rev = response.rev builtUser._rev = response.rev
await eventHelpers.handleSaveEvents(builtUser, dbUser) await eventHelpers.handleSaveEvents(builtUser, dbUser)
await platform.users.addUser(tenantId, builtUser._id!, builtUser.email) await platform.users.addUser(
tenantId,
builtUser._id!,
builtUser.email,
builtUser.ssoId
)
await cache.user.invalidateUser(response.id) await cache.user.invalidateUser(response.id)
await Promise.all(groupPromises) await Promise.all(groupPromises)

View File

@ -14,13 +14,14 @@ import {
} from "../db" } from "../db"
import { import {
BulkDocsResponse, BulkDocsResponse,
ContextUser,
SearchQuery,
SearchQueryOperators,
SearchUsersRequest, SearchUsersRequest,
User, User,
ContextUser,
} from "@budibase/types" } from "@budibase/types"
import { getGlobalDB } from "../context"
import * as context from "../context" import * as context from "../context"
import { user as userCache } from "../cache" import { getGlobalDB } from "../context"
type GetOpts = { cleanup?: boolean } type GetOpts = { cleanup?: boolean }
@ -39,6 +40,31 @@ function removeUserPassword(users: User | User[]) {
return users return users
} }
export const isSupportedUserSearch = (query: SearchQuery) => {
const allowed = [
{ op: SearchQueryOperators.STRING, key: "email" },
{ op: SearchQueryOperators.EQUAL, key: "_id" },
]
for (let [key, operation] of Object.entries(query)) {
if (typeof operation !== "object") {
return false
}
const fields = Object.keys(operation || {})
// this filter doesn't contain options - ignore
if (fields.length === 0) {
continue
}
const allowedOperation = allowed.find(
allow =>
allow.op === key && fields.length === 1 && fields[0] === allow.key
)
if (!allowedOperation) {
return false
}
}
return true
}
export const bulkGetGlobalUsersById = async ( export const bulkGetGlobalUsersById = async (
userIds: string[], userIds: string[],
opts?: GetOpts opts?: GetOpts
@ -211,8 +237,8 @@ export const searchGlobalUsersByEmail = async (
const PAGE_LIMIT = 8 const PAGE_LIMIT = 8
export const paginatedUsers = async ({ export const paginatedUsers = async ({
page, bookmark,
email, query,
appId, appId,
}: SearchUsersRequest = {}) => { }: SearchUsersRequest = {}) => {
const db = getGlobalDB() const db = getGlobalDB()
@ -222,18 +248,20 @@ export const paginatedUsers = async ({
limit: PAGE_LIMIT + 1, limit: PAGE_LIMIT + 1,
} }
// add a startkey if the page was specified (anchor) // add a startkey if the page was specified (anchor)
if (page) { if (bookmark) {
opts.startkey = page opts.startkey = bookmark
} }
// property specifies what to use for the page/anchor // property specifies what to use for the page/anchor
let userList: User[], let userList: User[],
property = "_id", property = "_id",
getKey getKey
if (appId) { if (query?.equal?._id) {
userList = [await getById(query.equal._id)]
} else if (appId) {
userList = await searchGlobalUsersByApp(appId, opts) userList = await searchGlobalUsersByApp(appId, opts)
getKey = (doc: any) => getGlobalUserByAppPage(appId, doc) getKey = (doc: any) => getGlobalUserByAppPage(appId, doc)
} else if (email) { } else if (query?.string?.email) {
userList = await searchGlobalUsersByEmail(email, opts) userList = await searchGlobalUsersByEmail(query?.string?.email, opts)
property = "email" property = "email"
} else { } else {
// no search, query allDocs // no search, query allDocs

View File

@ -10,7 +10,7 @@ import {
Event, Event,
TenantResolutionStrategy, TenantResolutionStrategy,
} from "@budibase/types" } from "@budibase/types"
import { SetOption } from "cookies" import type { SetOption } from "cookies"
const jwt = require("jsonwebtoken") const jwt = require("jsonwebtoken")
const APP_PREFIX = DocumentType.APP + SEPARATOR const APP_PREFIX = DocumentType.APP + SEPARATOR

View File

@ -86,8 +86,8 @@ export const useAuditLogs = () => {
return useFeature(Feature.AUDIT_LOGS) return useFeature(Feature.AUDIT_LOGS)
} }
export const usePublicApiUserRoles = () => { export const useExpandedPublicApi = () => {
return useFeature(Feature.USER_ROLE_PUBLIC_API) return useFeature(Feature.EXPANDED_PUBLIC_API)
} }
export const useScimIntegration = () => { export const useScimIntegration = () => {

View File

@ -1,4 +1,4 @@
import { generator, uuid, quotas } from "." import { generator, quotas, uuid } from "."
import { generateGlobalUserID } from "../../../../src/docIds" import { generateGlobalUserID } from "../../../../src/docIds"
import { import {
Account, Account,
@ -6,10 +6,11 @@ import {
AccountSSOProviderType, AccountSSOProviderType,
AuthType, AuthType,
CloudAccount, CloudAccount,
Hosting,
SSOAccount,
CreateAccount, CreateAccount,
CreatePassswordAccount, CreatePassswordAccount,
CreateVerifiableSSOAccount,
Hosting,
SSOAccount,
} from "@budibase/types" } from "@budibase/types"
import sample from "lodash/sample" import sample from "lodash/sample"
@ -68,6 +69,23 @@ export function ssoAccount(account: Account = cloudAccount()): SSOAccount {
} }
} }
export function verifiableSsoAccount(
account: Account = cloudAccount()
): SSOAccount {
return {
...account,
authType: AuthType.SSO,
oauth2: {
accessToken: generator.string(),
refreshToken: generator.string(),
},
pictureUrl: generator.url(),
provider: AccountSSOProvider.MICROSOFT,
providerType: AccountSSOProviderType.MICROSOFT,
thirdPartyProfile: { id: "abc123" },
}
}
export const cloudCreateAccount: CreatePassswordAccount = { export const cloudCreateAccount: CreatePassswordAccount = {
email: "cloud@budibase.com", email: "cloud@budibase.com",
tenantId: "cloud", tenantId: "cloud",
@ -91,6 +109,19 @@ export const cloudSSOCreateAccount: CreateAccount = {
profession: "Software Engineer", profession: "Software Engineer",
} }
export const cloudVerifiableSSOCreateAccount: CreateVerifiableSSOAccount = {
email: "cloud-sso@budibase.com",
tenantId: "cloud-sso",
hosting: Hosting.CLOUD,
authType: AuthType.SSO,
tenantName: "cloudsso",
name: "Budi Armstrong",
size: "10+",
profession: "Software Engineer",
provider: AccountSSOProvider.MICROSOFT,
thirdPartyProfile: { id: "abc123" },
}
export const selfCreateAccount: CreatePassswordAccount = { export const selfCreateAccount: CreatePassswordAccount = {
email: "self@budibase.com", email: "self@budibase.com",
tenantId: "self", tenantId: "self",

View File

@ -10,14 +10,13 @@ import { authDetails } from "./sso"
import { uuid } from "./common" import { uuid } from "./common"
import { generator } from "./generator" import { generator } from "./generator"
import { tenant } from "." import { tenant } from "."
import { generateGlobalUserID } from "../../../../src/docIds"
export const newEmail = () => { export const newEmail = () => {
return `${uuid()}@test.com` return `${uuid()}@test.com`
} }
export const user = (userProps?: Partial<Omit<User, "userId">>): User => { export const user = (userProps?: Partial<Omit<User, "userId">>): User => {
const userId = userProps?._id || generateGlobalUserID() const userId = userProps?._id
return { return {
_id: userId, _id: userId,
userId, userId,
@ -53,7 +52,7 @@ export const adminOnlyUser = (userProps?: any): AdminOnlyUser => {
} }
} }
export const builderUser = (userProps?: any): BuilderUser => { export const builderUser = (userProps?: Partial<User>): BuilderUser => {
return { return {
...user(userProps), ...user(userProps),
builder: { builder: {

View File

@ -18,7 +18,7 @@ class DBTestConfiguration {
// TENANCY // TENANCY
doInTenant(task: any) { doInTenant<T>(task: () => Promise<T>) {
return context.doInTenant(this.tenantId, () => { return context.doInTenant(this.tenantId, () => {
return task() return task()
}) })

View File

@ -1 +1,2 @@
export * from "./core/utilities" export * from "./core/utilities"
export * from "./extra"

View File

@ -20,14 +20,12 @@
"@rollup/plugin-commonjs": "^16.0.0", "@rollup/plugin-commonjs": "^16.0.0",
"@rollup/plugin-json": "^4.1.0", "@rollup/plugin-json": "^4.1.0",
"@rollup/plugin-node-resolve": "^11.2.1", "@rollup/plugin-node-resolve": "^11.2.1",
"cross-env": "^7.0.2",
"nollup": "^0.14.1",
"postcss": "^8.2.9", "postcss": "^8.2.9",
"rollup": "^2.45.2", "rollup": "^2.45.2",
"rollup-plugin-postcss": "^4.0.0", "rollup-plugin-postcss": "^4.0.0",
"rollup-plugin-svelte": "^7.1.0", "rollup-plugin-svelte": "^7.1.0",
"rollup-plugin-terser": "^7.0.2", "rollup-plugin-terser": "^7.0.2",
"svelte": "^3.38.2" "svelte": "3.49.0"
}, },
"keywords": [ "keywords": [
"svelte" "svelte"
@ -84,9 +82,9 @@
"@spectrum-css/vars": "3.0.1", "@spectrum-css/vars": "3.0.1",
"dayjs": "^1.10.8", "dayjs": "^1.10.8",
"easymde": "^2.16.1", "easymde": "^2.16.1",
"svelte-dnd-action": "^0.9.8",
"svelte-flatpickr": "3.2.3", "svelte-flatpickr": "3.2.3",
"svelte-portal": "^1.0.0", "svelte-portal": "^1.0.0"
"svelte-dnd-action": "^0.9.8"
}, },
"resolutions": { "resolutions": {
"loader-utils": "1.4.1" "loader-utils": "1.4.1"

View File

@ -14,12 +14,12 @@
export let autocomplete = false export let autocomplete = false
export let sort = false export let sort = false
export let autoWidth = false export let autoWidth = false
export let fetchTerm = null export let searchTerm = null
export let useFetch = false
export let customPopoverHeight export let customPopoverHeight
export let customPopoverOffsetBelow export let customPopoverOffsetBelow
export let customPopoverMaxHeight export let customPopoverMaxHeight
export let open = false export let open = false
export let loading
const dispatch = createEventDispatcher() const dispatch = createEventDispatcher()
@ -82,6 +82,7 @@
</script> </script>
<Picker <Picker
on:loadMore
{id} {id}
{error} {error}
{disabled} {disabled}
@ -90,9 +91,8 @@
{options} {options}
isPlaceholder={!arrayValue.length} isPlaceholder={!arrayValue.length}
{autocomplete} {autocomplete}
bind:fetchTerm bind:searchTerm
bind:open bind:open
{useFetch}
{isOptionSelected} {isOptionSelected}
{getOptionLabel} {getOptionLabel}
{getOptionValue} {getOptionValue}
@ -102,4 +102,5 @@
{customPopoverHeight} {customPopoverHeight}
{customPopoverOffsetBelow} {customPopoverOffsetBelow}
{customPopoverMaxHeight} {customPopoverMaxHeight}
{loading}
/> />

View File

@ -2,7 +2,7 @@
import "@spectrum-css/picker/dist/index-vars.css" import "@spectrum-css/picker/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 { createEventDispatcher } from "svelte" import { createEventDispatcher, onDestroy } from "svelte"
import clickOutside from "../../Actions/click_outside" import clickOutside from "../../Actions/click_outside"
import Search from "./Search.svelte" import Search from "./Search.svelte"
import Icon from "../../Icon/Icon.svelte" import Icon from "../../Icon/Icon.svelte"
@ -10,6 +10,7 @@
import Popover from "../../Popover/Popover.svelte" import Popover from "../../Popover/Popover.svelte"
import Tags from "../../Tags/Tags.svelte" import Tags from "../../Tags/Tags.svelte"
import Tag from "../../Tags/Tag.svelte" import Tag from "../../Tags/Tag.svelte"
import ProgressCircle from "../../ProgressCircle/ProgressCircle.svelte"
export let id = null export let id = null
export let disabled = false export let disabled = false
@ -35,19 +36,20 @@
export let autoWidth = false export let autoWidth = false
export let autocomplete = false export let autocomplete = false
export let sort = false export let sort = false
export let fetchTerm = null export let searchTerm = null
export let useFetch = false
export let customPopoverHeight export let customPopoverHeight
export let customPopoverOffsetBelow export let customPopoverOffsetBelow
export let customPopoverMaxHeight export let customPopoverMaxHeight
export let align = "left" export let align = "left"
export let footer = null export let footer = null
export let customAnchor = null export let customAnchor = null
export let loading
const dispatch = createEventDispatcher() const dispatch = createEventDispatcher()
let searchTerm = null
let button let button
let popover let popover
let component
$: sortedOptions = getSortedOptions(options, getOptionLabel, sort) $: sortedOptions = getSortedOptions(options, getOptionLabel, sort)
$: filteredOptions = getFilteredOptions( $: filteredOptions = getFilteredOptions(
@ -82,7 +84,7 @@
} }
const getFilteredOptions = (options, term, getLabel) => { const getFilteredOptions = (options, term, getLabel) => {
if (autocomplete && term && !fetchTerm) { if (autocomplete && term) {
const lowerCaseTerm = term.toLowerCase() const lowerCaseTerm = term.toLowerCase()
return options.filter(option => { return options.filter(option => {
return `${getLabel(option)}`.toLowerCase().includes(lowerCaseTerm) return `${getLabel(option)}`.toLowerCase().includes(lowerCaseTerm)
@ -90,6 +92,20 @@
} }
return options return options
} }
const onScroll = e => {
const scrollPxThreshold = 100
const scrollPositionFromBottom =
e.target.scrollHeight - e.target.clientHeight - e.target.scrollTop
if (scrollPositionFromBottom < scrollPxThreshold) {
dispatch("loadMore")
}
}
$: component?.addEventListener("scroll", onScroll)
onDestroy(() => {
component?.removeEventListener("scroll", null)
})
</script> </script>
<button <button
@ -163,14 +179,13 @@
> >
{#if autocomplete} {#if autocomplete}
<Search <Search
value={useFetch ? fetchTerm : searchTerm} value={searchTerm}
on:change={event => on:change={event => (searchTerm = event.detail)}
useFetch ? (fetchTerm = event.detail) : (searchTerm = event.detail)}
{disabled} {disabled}
placeholder="Search" placeholder="Search"
/> />
{/if} {/if}
<ul class="spectrum-Menu" role="listbox"> <ul class="spectrum-Menu" role="listbox" bind:this={component}>
{#if placeholderOption} {#if placeholderOption}
<li <li
class="spectrum-Menu-item placeholder" class="spectrum-Menu-item placeholder"
@ -248,6 +263,12 @@
{/if} {/if}
</ul> </ul>
{#if loading}
<div class="loading" class:loading--withAutocomplete={autocomplete}>
<ProgressCircle size="S" />
</div>
{/if}
{#if footer} {#if footer}
<div class="footer"> <div class="footer">
{footer} {footer}
@ -323,18 +344,19 @@
/* Search styles inside popover */ /* Search styles inside popover */
.popover-content :global(.spectrum-Search) { .popover-content :global(.spectrum-Search) {
margin-top: -1px; margin-top: -1px;
margin-left: -1px; width: 100%;
width: calc(100% + 2px);
} }
.popover-content :global(.spectrum-Search input) { .popover-content :global(.spectrum-Search input) {
height: auto; height: auto;
border-bottom-left-radius: 0; border-bottom-left-radius: 0;
border-bottom-right-radius: 0; border-bottom-right-radius: 0;
border-left: 0;
border-right: 0;
padding-top: var(--spectrum-global-dimension-size-100); padding-top: var(--spectrum-global-dimension-size-100);
padding-bottom: var(--spectrum-global-dimension-size-100); padding-bottom: var(--spectrum-global-dimension-size-100);
} }
.popover-content :global(.spectrum-Search .spectrum-ClearButton) { .popover-content :global(.spectrum-Search .spectrum-ClearButton) {
right: 1px; right: 2px;
top: 2px; top: 2px;
} }
.popover-content :global(.spectrum-Search .spectrum-Textfield-icon) { .popover-content :global(.spectrum-Search .spectrum-Textfield-icon) {
@ -359,4 +381,14 @@
.option-tag :global(.spectrum-Tags-item > .spectrum-Icon) { .option-tag :global(.spectrum-Tags-item > .spectrum-Icon) {
margin-top: 2px; margin-top: 2px;
} }
.loading {
position: fixed;
justify-content: center;
right: var(--spacing-s);
top: var(--spacing-s);
}
.loading--withAutocomplete {
top: calc(34px + var(--spacing-m));
}
</style> </style>

View File

@ -25,6 +25,8 @@
export let tag = null export let tag = null
export let customPopoverOffsetBelow export let customPopoverOffsetBelow
export let customPopoverMaxHeight export let customPopoverMaxHeight
export let searchTerm = null
export let loading
const dispatch = createEventDispatcher() const dispatch = createEventDispatcher()
@ -65,6 +67,8 @@
<Picker <Picker
on:click on:click
bind:open bind:open
bind:searchTerm
on:loadMore
{quiet} {quiet}
{id} {id}
{error} {error}
@ -92,4 +96,5 @@
placeholderOption={placeholder === false ? null : placeholder} placeholderOption={placeholder === false ? null : placeholder}
isOptionSelected={option => option === value} isOptionSelected={option => option === value}
onSelectOption={selectOption} onSelectOption={selectOption}
{loading}
/> />

View File

@ -96,8 +96,8 @@
{disabled} {disabled}
{readonly} {readonly}
{id} {id}
value={value || ""} value={value ?? ""}
placeholder={placeholder || ""} placeholder={placeholder ?? ""}
on:click on:click
on:blur on:blur
on:focus on:focus

View File

@ -16,8 +16,7 @@
export let sort = false export let sort = false
export let autoWidth = false export let autoWidth = false
export let autocomplete = false export let autocomplete = false
export let fetchTerm = null export let searchTerm = null
export let useFetch = false
export let customPopoverHeight export let customPopoverHeight
const dispatch = createEventDispatcher() const dispatch = createEventDispatcher()
@ -41,8 +40,7 @@
{autoWidth} {autoWidth}
{autocomplete} {autocomplete}
{customPopoverHeight} {customPopoverHeight}
bind:fetchTerm bind:searchTerm
{useFetch}
on:change={onChange} on:change={onChange}
on:click on:click
/> />

View File

@ -21,14 +21,6 @@
"hsla(240, 90%, 75%, 0.3)", "hsla(240, 90%, 75%, 0.3)",
"hsla(320, 90%, 75%, 0.3)", "hsla(320, 90%, 75%, 0.3)",
] ]
$: {
if (constraints.inclusion.length) {
options = constraints.inclusion.map(value => ({
name: value,
id: Math.random(),
}))
}
}
const removeInput = idx => { const removeInput = idx => {
delete optionColors[options[idx].name] delete optionColors[options[idx].name]
constraints.inclusion = constraints.inclusion.filter((e, i) => i !== idx) constraints.inclusion = constraints.inclusion.filter((e, i) => i !== idx)
@ -80,6 +72,11 @@
// Initialize anchor arrays on mount, assuming 'options' is already populated // Initialize anchor arrays on mount, assuming 'options' is already populated
colorPopovers = constraints.inclusion.map(() => undefined) colorPopovers = constraints.inclusion.map(() => undefined)
anchors = constraints.inclusion.map(() => undefined) anchors = constraints.inclusion.map(() => undefined)
options = constraints.inclusion.map(value => ({
name: value,
id: Math.random(),
}))
}) })
</script> </script>

View File

@ -25,6 +25,7 @@
longform: StringRenderer, longform: StringRenderer,
array: ArrayRenderer, array: ArrayRenderer,
internal: InternalRenderer, internal: InternalRenderer,
bb_reference: RelationshipRenderer,
} }
$: type = getType(schema) $: type = getType(schema)
$: customRenderer = customRenderers?.find(x => x.column === schema?.name) $: customRenderer = customRenderers?.find(x => x.column === schema?.name)

View File

@ -1,8 +0,0 @@
const ncp = require("ncp").ncp
ncp("./dist", "../server/builder", function (err) {
if (err) {
return console.error(err)
}
console.log("Copied dist folder to ../server/builder")
})

View File

@ -65,7 +65,6 @@
"@fortawesome/free-brands-svg-icons": "^6.2.1", "@fortawesome/free-brands-svg-icons": "^6.2.1",
"@fortawesome/free-solid-svg-icons": "^6.2.1", "@fortawesome/free-solid-svg-icons": "^6.2.1",
"@sentry/browser": "5.19.1", "@sentry/browser": "5.19.1",
"@spectrum-css/accordion": "^3.0.24",
"@spectrum-css/page": "^3.0.1", "@spectrum-css/page": "^3.0.1",
"@spectrum-css/vars": "^3.0.1", "@spectrum-css/vars": "^3.0.1",
"codemirror": "^5.59.0", "codemirror": "^5.59.0",
@ -75,20 +74,19 @@
"lodash": "4.17.21", "lodash": "4.17.21",
"posthog-js": "^1.36.0", "posthog-js": "^1.36.0",
"remixicon": "2.5.0", "remixicon": "2.5.0",
"sanitize-html": "^2.7.0",
"shortid": "2.2.15", "shortid": "2.2.15",
"svelte-dnd-action": "^0.9.8", "svelte-dnd-action": "^0.9.8",
"svelte-loading-spinners": "^0.1.1", "svelte-loading-spinners": "^0.1.1",
"svelte-portal": "1.0.0", "svelte-portal": "1.0.0",
"uuid": "8.3.1",
"yup": "0.29.2" "yup": "0.29.2"
}, },
"devDependencies": { "devDependencies": {
"@babel/core": "^7.12.14", "@babel/core": "^7.12.14",
"@babel/plugin-transform-runtime": "^7.13.10", "@babel/plugin-transform-runtime": "^7.13.10",
"@babel/preset-env": "^7.13.12", "@babel/preset-env": "^7.13.12",
"@babel/runtime": "^7.13.10", "@rollup/plugin-replace": "^5.0.3",
"@rollup/plugin-replace": "^2.4.2", "@roxi/routify": "2.18.12",
"@roxi/routify": "2.18.5",
"@sveltejs/vite-plugin-svelte": "1.0.1", "@sveltejs/vite-plugin-svelte": "1.0.1",
"@testing-library/jest-dom": "5.17.0", "@testing-library/jest-dom": "5.17.0",
"@testing-library/svelte": "^3.2.2", "@testing-library/svelte": "^3.2.2",
@ -96,26 +94,19 @@
"identity-obj-proxy": "^3.0.0", "identity-obj-proxy": "^3.0.0",
"jest": "29.6.2", "jest": "29.6.2",
"jsdom": "^21.1.1", "jsdom": "^21.1.1",
"mochawesome": "^7.1.3",
"mochawesome-merge": "^4.2.1",
"mochawesome-report-generator": "^6.2.0",
"ncp": "^2.0.0", "ncp": "^2.0.0",
"rimraf": "^3.0.2",
"rollup": "^2.44.0",
"rollup-plugin-copy": "^3.4.0",
"start-server-and-test": "^1.12.1",
"svelte": "^3.48.0", "svelte": "^3.48.0",
"svelte-jester": "^1.3.2", "svelte-jester": "^1.3.2",
"ts-node": "10.8.1", "vite": "^4.4.11",
"tsconfig-paths": "4.0.0", "vite-plugin-static-copy": "^0.17.0",
"typescript": "4.7.3",
"vite": "^3.0.8",
"vite-plugin-static-copy": "^0.16.0",
"vitest": "^0.29.2" "vitest": "^0.29.2"
}, },
"nx": { "nx": {
"targets": { "targets": {
"build": { "build": {
"outputs": [
"{workspaceRoot}/packages/server/builder"
],
"dependsOn": [ "dependsOn": [
{ {
"projects": [ "projects": [

View File

@ -2,6 +2,7 @@ export const Events = {
COMPONENT_CREATED: "component:created", COMPONENT_CREATED: "component:created",
COMPONENT_UPDATED: "component:updated", COMPONENT_UPDATED: "component:updated",
APP_VIEW_PUBLISHED: "app:view_published", APP_VIEW_PUBLISHED: "app:view_published",
BLOCK_EJECTED: "block:ejected",
} }
export const EventSource = { export const EventSource = {

View File

@ -996,12 +996,15 @@ export const buildFormSchema = (component, asset) => {
if (component._component.endsWith("formblock")) { if (component._component.endsWith("formblock")) {
let schema = {} let schema = {}
const datasource = getDatasourceForProvider(asset, component) const datasource = getDatasourceForProvider(asset, component)
const info = getSchemaForDatasource(component, datasource) const info = getSchemaForDatasource(component, datasource)
if (!info?.schema) {
return schema
}
if (!component.fields) { if (!component.fields) {
Object.values(info?.schema) Object.values(info.schema)
.filter( .filter(
({ autocolumn, name }) => ({ autocolumn, name }) =>
!autocolumn && !["_rev", "_id"].includes(name) !autocolumn && !["_rev", "_id"].includes(name)

View File

@ -221,18 +221,6 @@ const automationActions = store => ({
newAutomation.definition.steps.splice(blockIdx, 0, block) newAutomation.definition.steps.splice(blockIdx, 0, block)
await store.actions.save(newAutomation) await store.actions.save(newAutomation)
}, },
/**
* "rowControl" appears to be the name of the flag used to determine whether
* a certain automation block uses values or bindings as inputs
*/
toggleRowControl: async (block, rowControl) => {
const newBlock = { ...block, rowControl }
const newAutomation = store.actions.getUpdatedDefinition(
get(selectedAutomation),
newBlock
)
await store.actions.save(newAutomation)
},
deleteAutomationBlock: async block => { deleteAutomationBlock: async block => {
const automation = get(selectedAutomation) const automation = get(selectedAutomation)
let newAutomation = cloneDeep(automation) let newAutomation = cloneDeep(automation)

View File

@ -64,6 +64,7 @@ const INITIAL_FRONTEND_STATE = {
}, },
features: { features: {
componentValidation: false, componentValidation: false,
disableUserMetadata: false,
}, },
errors: [], errors: [],
hasAppPackage: false, hasAppPackage: false,
@ -1286,6 +1287,11 @@ export const getFrontendStore = () => {
return false return false
} }
// Log event
analytics.captureEvent(Events.BLOCK_EJECTED, {
block: block._component,
})
// Attach block children back into ejected definition, using the // Attach block children back into ejected definition, using the
// _containsSlot flag to know where to insert them // _containsSlot flag to know where to insert them
const slotContainer = findAllMatchingComponents( const slotContainer = findAllMatchingComponents(

View File

@ -33,6 +33,8 @@ const generateTableBlock = datasource => {
showTitleButton: true, showTitleButton: true,
titleButtonText: "Create row", titleButtonText: "Create row",
titleButtonClickBehaviour: "new", titleButtonClickBehaviour: "new",
sidePanelSaveLabel: "Save",
sidePanelDeleteLabel: "Delete",
}) })
.instanceName(`${datasource.label} - Table block`) .instanceName(`${datasource.label} - Table block`)
return tableBlock return tableBlock

View File

@ -7,7 +7,6 @@
Detail, Detail,
Modal, Modal,
Button, Button,
Select,
ActionButton, ActionButton,
notifications, notifications,
Label, Label,
@ -39,9 +38,6 @@
step => step.stepId === ActionStepID.COLLECT step => step.stepId === ActionStepID.COLLECT
) )
$: automationId = $selectedAutomation?._id $: automationId = $selectedAutomation?._id
$: showBindingPicker =
block.stepId === ActionStepID.CREATE_ROW ||
block.stepId === ActionStepID.UPDATE_ROW
$: isTrigger = block.type === "TRIGGER" $: isTrigger = block.type === "TRIGGER"
$: steps = $selectedAutomation?.definition?.steps ?? [] $: steps = $selectedAutomation?.definition?.steps ?? []
$: blockIdx = steps.findIndex(step => step.id === block.id) $: blockIdx = steps.findIndex(step => step.id === block.id)
@ -96,15 +92,6 @@
} }
} }
/**
* "rowControl" appears to be the name of the flag used to determine whether
* a certain automation block uses values or bindings as inputs
*/
function toggleRowControl(evt) {
const rowControl = evt.detail !== "Use values"
automationStore.actions.toggleRowControl(block, rowControl)
}
async function addLooping() { async function addLooping() {
const loopDefinition = $automationStore.blockDefinitions.ACTION.LOOP const loopDefinition = $automationStore.blockDefinitions.ACTION.LOOP
const loopBlock = automationStore.actions.constructBlock( const loopBlock = automationStore.actions.constructBlock(
@ -189,16 +176,6 @@
Add Looping Add Looping
</ActionButton> </ActionButton>
{/if} {/if}
{#if showBindingPicker}
<Select
on:change={toggleRowControl}
defaultValue="Use values"
autoWidth
value={block.rowControl ? "Use bindings" : "Use values"}
options={["Use values", "Use bindings"]}
placeholder={null}
/>
{/if}
<ActionButton <ActionButton
on:click={() => deleteStep()} on:click={() => deleteStep()}
icon="DeleteOutline" icon="DeleteOutline"

View File

@ -23,6 +23,7 @@
import { environment, licensing } from "stores/portal" import { environment, licensing } from "stores/portal"
import WebhookDisplay from "../Shared/WebhookDisplay.svelte" import WebhookDisplay from "../Shared/WebhookDisplay.svelte"
import DrawerBindableInput from "../../common/bindings/DrawerBindableInput.svelte" import DrawerBindableInput from "../../common/bindings/DrawerBindableInput.svelte"
import DrawerBindableSlot from "../../common/bindings/DrawerBindableSlot.svelte"
import AutomationBindingPanel from "../../common/bindings/ServerBindingPanel.svelte" import AutomationBindingPanel from "../../common/bindings/ServerBindingPanel.svelte"
import CodeEditorModal from "./CodeEditorModal.svelte" import CodeEditorModal from "./CodeEditorModal.svelte"
import QuerySelector from "./QuerySelector.svelte" import QuerySelector from "./QuerySelector.svelte"
@ -82,33 +83,6 @@
? [hbAutocomplete([...bindingsToCompletions(bindings, codeMode)])] ? [hbAutocomplete([...bindingsToCompletions(bindings, codeMode)])]
: [] : []
/**
* TODO - Remove after November 2023
* *******************************
* Code added to provide backwards compatibility between Values 1,2,3,4,5
* and the new JSON body.
*/
let deprecatedSchemaProperties
$: {
if (block?.stepId === "integromat" || block?.stepId === "zapier") {
deprecatedSchemaProperties = schemaProperties.filter(
prop => !prop[0].startsWith("value")
)
if (!deprecatedSchemaProperties.map(entry => entry[0]).includes("body")) {
deprecatedSchemaProperties.push([
"body",
{
title: "Payload",
type: "json",
},
])
}
} else {
deprecatedSchemaProperties = schemaProperties
}
}
/****************************************************/
const getInputData = (testData, blockInputs) => { const getInputData = (testData, blockInputs) => {
// Test data is not cloned for reactivity // Test data is not cloned for reactivity
let newInputData = testData || cloneDeep(blockInputs) let newInputData = testData || cloneDeep(blockInputs)
@ -118,30 +92,6 @@
newInputData = cloneDeep(blockInputs) newInputData = cloneDeep(blockInputs)
} }
/**
* TODO - Remove after November 2023
* *******************************
* Code added to provide backwards compatibility between Values 1,2,3,4,5
* and the new JSON body.
*/
if (
(block?.stepId === "integromat" || block?.stepId === "zapier") &&
!newInputData?.body?.value
) {
let deprecatedValues = {
...newInputData,
}
delete deprecatedValues.url
delete deprecatedValues.body
newInputData = {
url: newInputData.url,
body: {
value: JSON.stringify(deprecatedValues),
},
}
}
/**********************************/
inputData = newInputData inputData = newInputData
setDefaultEnumValues() setDefaultEnumValues()
} }
@ -337,7 +287,7 @@
</script> </script>
<div class="fields"> <div class="fields">
{#each deprecatedSchemaProperties as [key, value]} {#each schemaProperties as [key, value]}
{#if canShowField(key, value)} {#if canShowField(key, value)}
<div class="block-field"> <div class="block-field">
{#if key !== "fields" && value.type !== "boolean"} {#if key !== "fields" && value.type !== "boolean"}
@ -362,18 +312,6 @@
mode="json" mode="json"
value={inputData[key]?.value} value={inputData[key]?.value}
on:change={e => { on:change={e => {
/**
* TODO - Remove after November 2023
* *******************************
* Code added to provide backwards compatibility between Values 1,2,3,4,5
* and the new JSON body.
*/
delete inputData.value1
delete inputData.value2
delete inputData.value3
delete inputData.value4
delete inputData.value5
/***********************/
onChange(e, key) onChange(e, key)
}} }}
/> />
@ -386,10 +324,23 @@
/> />
</div> </div>
{:else if value.type === "date"} {:else if value.type === "date"}
<DatePicker <DrawerBindableSlot
fillWidth
title={value.title}
panel={AutomationBindingPanel}
type={"date"}
value={inputData[key]} value={inputData[key]}
on:change={e => onChange(e, key)} on:change={e => onChange(e, key)}
/> {bindings}
allowJS={true}
updateOnChange={false}
drawerLeft="260px"
>
<DatePicker
value={inputData[key]}
on:change={e => onChange(e, key)}
/>
</DrawerBindableSlot>
{:else if value.customType === "column"} {:else if value.customType === "column"}
<Select <Select
on:change={e => onChange(e, key)} on:change={e => onChange(e, key)}
@ -469,7 +420,6 @@
/> />
{:else if value.customType === "row"} {:else if value.customType === "row"}
<RowSelector <RowSelector
{block}
value={inputData[key]} value={inputData[key]}
meta={inputData["meta"] || {}} meta={inputData["meta"] || {}}
on:change={e => { on:change={e => {

View File

@ -1,18 +1,16 @@
<script> <script>
import { tables } from "stores/backend" import { tables } from "stores/backend"
import { Select, Checkbox } from "@budibase/bbui" import { Select, Checkbox } from "@budibase/bbui"
import DrawerBindableInput from "../../common/bindings/DrawerBindableInput.svelte"
import AutomationBindingPanel from "../../common/bindings/ServerBindingPanel.svelte"
import { createEventDispatcher } from "svelte" import { createEventDispatcher } from "svelte"
import RowSelectorTypes from "./RowSelectorTypes.svelte" import RowSelectorTypes from "./RowSelectorTypes.svelte"
import ModalBindableInput from "../../common/bindings/ModalBindableInput.svelte" import DrawerBindableSlot from "../../common/bindings/DrawerBindableSlot.svelte"
import AutomationBindingPanel from "../../common/bindings/ServerBindingPanel.svelte"
const dispatch = createEventDispatcher() const dispatch = createEventDispatcher()
export let value export let value
export let meta export let meta
export let bindings export let bindings
export let block
export let isTestModal export let isTestModal
export let isUpdateRow export let isUpdateRow
@ -25,16 +23,6 @@
let table let table
let schemaFields let schemaFields
let placeholders = {
number: 10,
boolean: "true",
datetime: "2022-02-16T12:00:00.000Z ",
options: "1",
array: "1 2 3 4",
link: "ro_ta_123_456",
longform: "long form text",
}
$: rowControl = block.rowControl
$: { $: {
table = $tables.list.find(table => table._id === value?.tableId) table = $tables.list.find(table => table._id === value?.tableId)
schemaFields = Object.entries(table?.schema ?? {}) schemaFields = Object.entries(table?.schema ?? {})
@ -57,19 +45,13 @@
return value return value
} }
if (type === "boolean") {
if (typeof value === "boolean") {
return value
}
return value === "true"
}
if (type === "number") { if (type === "number") {
if (typeof value === "number") { if (typeof value === "number") {
return value return value
} }
return Number(value) return Number(value)
} }
if (type === "options") { if (type === "options" || type === "boolean") {
return value return value
} }
if (type === "array") { if (type === "array") {
@ -83,8 +65,7 @@
if (Array.isArray(value)) { if (Array.isArray(value)) {
return value return value
} }
return value.split(",").map(x => x.trim())
return [value]
} }
if (type === "json") { if (type === "json") {
@ -128,9 +109,31 @@
{#if schemaFields.length} {#if schemaFields.length}
<div class="schema-fields"> <div class="schema-fields">
{#each schemaFields as [field, schema]} {#each schemaFields as [field, schema]}
{#if !schema.autocolumn} {#if !schema.autocolumn && schema.type !== "attachment"}
{#if schema.type !== "attachment"} {#if isTestModal}
{#if !rowControl} <RowSelectorTypes
{isTestModal}
{field}
{schema}
bindings={parsedBindings}
{value}
{onChange}
/>
{:else}
<DrawerBindableSlot
fillWidth
title={value.title}
label={field}
panel={AutomationBindingPanel}
type={schema.type}
{schema}
value={value[field]}
on:change={e => onChange(e, field)}
{bindings}
allowJS={true}
updateOnChange={false}
drawerLeft="260px"
>
<RowSelectorTypes <RowSelectorTypes
{isTestModal} {isTestModal}
{field} {field}
@ -139,37 +142,19 @@
{value} {value}
{onChange} {onChange}
/> />
{:else} </DrawerBindableSlot>
<div>
<svelte:component
this={isTestModal ? ModalBindableInput : DrawerBindableInput}
placeholder={placeholders[schema.type]}
panel={AutomationBindingPanel}
value={Array.isArray(value[field])
? value[field].join(" ")
: value[field]}
on:change={e => onChange(e, field, schema.type)}
label={field}
type="string"
bindings={parsedBindings}
fillWidth={true}
allowJS={true}
updateOnChange={false}
/>
{#if isUpdateRow && schema.type === "link"}
<div class="checkbox-field">
<Checkbox
value={meta.fields?.[field]?.clearRelationships}
text={"Clear relationships if empty?"}
size={"S"}
on:change={e => onChangeSetting(e, field)}
/>
</div>
{/if}
</div>
{/if}
{/if} {/if}
{/if} {/if}
{#if isUpdateRow && schema.type === "link"}
<div class="checkbox-field">
<Checkbox
value={meta.fields?.[field]?.clearRelationships}
text={"Clear relationships if empty?"}
size={"S"}
on:change={e => onChangeSetting(e, field)}
/>
</div>
{/if}
{/each} {/each}
</div> </div>
{/if} {/if}

View File

@ -1,7 +1,6 @@
<script> <script>
import { import {
Select, Select,
Toggle,
DatePicker, DatePicker,
Multiselect, Multiselect,
TextArea, TextArea,
@ -45,19 +44,28 @@
on:change={e => onChange(e, field)} on:change={e => onChange(e, field)}
/> />
{:else if schema.type === "boolean"} {:else if schema.type === "boolean"}
<Toggle <Select
text={field}
value={value[field]}
on:change={e => onChange(e, field)} on:change={e => onChange(e, field)}
label={field}
value={value[field]}
options={[
{ label: "True", value: "true" },
{ label: "False", value: "false" },
]}
/> />
{:else if schema.type === "array"} {:else if schema.type === "array"}
<Multiselect <Multiselect
bind:value={value[field]} bind:value={value[field]}
label={field} label={field}
options={schema.constraints.inclusion} options={schema.constraints.inclusion}
on:change={e => onChange(e, field)}
/> />
{:else if schema.type === "longform"} {:else if schema.type === "longform"}
<TextArea label={field} bind:value={value[field]} /> <TextArea
label={field}
bind:value={value[field]}
on:change={e => onChange(e, field)}
/>
{:else if schema.type === "json"} {:else if schema.type === "json"}
<span> <span>
<Label>{field}</Label> <Label>{field}</Label>
@ -73,7 +81,11 @@
/> />
</span> </span>
{:else if schema.type === "link"} {:else if schema.type === "link"}
<LinkedRowSelector bind:linkedRows={value[field]} {schema} /> <LinkedRowSelector
bind:linkedRows={value[field]}
{schema}
on:change={e => onChange(e, field)}
/>
{:else if schema.type === "string" || schema.type === "number"} {:else if schema.type === "string" || schema.type === "number"}
<svelte:component <svelte:component
this={isTestModal ? ModalBindableInput : DrawerBindableInput} this={isTestModal ? ModalBindableInput : DrawerBindableInput}

View File

@ -1,33 +0,0 @@
<script>
import Table from "./Table.svelte"
export let query = {}
export let data = []
export let editRows = false
let loading = false
let error = false
let type = "external"
</script>
{#if error}
<div class="errors">{error}</div>
{/if}
<Table
schema={query.schema}
{data}
{loading}
{type}
rowCount={5}
allowEditing={editRows}
/>
<style>
.errors {
color: var(--red);
background: var(--red-light);
padding: var(--spacing-m);
border-radius: var(--border-radius-m);
margin-bottom: var(--spacing-m);
}
</style>

View File

@ -4,6 +4,7 @@
import { TableNames } from "constants" import { TableNames } from "constants"
import { Grid } from "@budibase/frontend-core" import { Grid } from "@budibase/frontend-core"
import { API } from "api" import { API } from "api"
import { store } from "builderStore"
import GridAddColumnModal from "components/backend/DataTable/modals/grid/GridCreateColumnModal.svelte" import GridAddColumnModal from "components/backend/DataTable/modals/grid/GridCreateColumnModal.svelte"
import GridCreateEditRowModal from "components/backend/DataTable/modals/grid/GridCreateEditRowModal.svelte" import GridCreateEditRowModal from "components/backend/DataTable/modals/grid/GridCreateEditRowModal.svelte"
import GridEditUserModal from "components/backend/DataTable/modals/grid/GridEditUserModal.svelte" import GridEditUserModal from "components/backend/DataTable/modals/grid/GridEditUserModal.svelte"
@ -14,6 +15,7 @@
import GridManageAccessButton from "components/backend/DataTable/buttons/grid/GridManageAccessButton.svelte" import GridManageAccessButton from "components/backend/DataTable/buttons/grid/GridManageAccessButton.svelte"
import GridRelationshipButton from "components/backend/DataTable/buttons/grid/GridRelationshipButton.svelte" import GridRelationshipButton from "components/backend/DataTable/buttons/grid/GridRelationshipButton.svelte"
import GridEditColumnModal from "components/backend/DataTable/modals/grid/GridEditColumnModal.svelte" import GridEditColumnModal from "components/backend/DataTable/modals/grid/GridEditColumnModal.svelte"
import GridUsersTableButton from "components/backend/DataTable/modals/grid/GridUsersTableButton.svelte"
const userSchemaOverrides = { const userSchemaOverrides = {
firstName: { displayName: "First name", disabled: true }, firstName: { displayName: "First name", disabled: true },
@ -59,22 +61,22 @@
datasource={gridDatasource} datasource={gridDatasource}
canAddRows={!isUsersTable} canAddRows={!isUsersTable}
canDeleteRows={!isUsersTable} canDeleteRows={!isUsersTable}
canEditRows={!isUsersTable || !$store.features.disableUserMetadata}
canEditColumns={!isUsersTable || !$store.features.disableUserMetadata}
schemaOverrides={isUsersTable ? userSchemaOverrides : null} schemaOverrides={isUsersTable ? userSchemaOverrides : null}
showAvatars={false} showAvatars={false}
on:updatedatasource={handleGridTableUpdate} on:updatedatasource={handleGridTableUpdate}
> >
<svelte:fragment slot="filter"> <svelte:fragment slot="filter">
{#if isUsersTable && $store.features.disableUserMetadata}
<GridUsersTableButton />
{/if}
<GridFilterButton /> <GridFilterButton />
</svelte:fragment> </svelte:fragment>
<svelte:fragment slot="edit-column">
<GridEditColumnModal />
</svelte:fragment>
<svelte:fragment slot="add-column">
<GridAddColumnModal />
</svelte:fragment>
<svelte:fragment slot="controls"> <svelte:fragment slot="controls">
<GridCreateViewButton /> {#if !isUsersTable}
<GridCreateViewButton />
{/if}
<GridManageAccessButton /> <GridManageAccessButton />
{#if relationshipsEnabled} {#if relationshipsEnabled}
<GridRelationshipButton /> <GridRelationshipButton />
@ -84,7 +86,6 @@
{:else} {:else}
<GridImportButton /> <GridImportButton />
{/if} {/if}
<GridExportButton /> <GridExportButton />
{#if isUsersTable} {#if isUsersTable}
<GridEditUserModal /> <GridEditUserModal />
@ -92,6 +93,12 @@
<GridCreateEditRowModal /> <GridCreateEditRowModal />
{/if} {/if}
</svelte:fragment> </svelte:fragment>
<svelte:fragment slot="edit-column">
<GridEditColumnModal />
</svelte:fragment>
<svelte:fragment slot="add-column">
<GridAddColumnModal />
</svelte:fragment>
</Grid> </Grid>
</div> </div>

View File

@ -13,7 +13,13 @@
let modal let modal
$: tempValue = filters || [] $: tempValue = filters || []
$: schemaFields = Object.values(schema || {}) $: schemaFields = Object.entries(schema || {}).map(
([fieldName, fieldSchema]) => ({
name: fieldName, // Using the key as name if not defined in the schema, for example in some autogenerated columns
...fieldSchema,
})
)
$: text = getText(filters) $: text = getText(filters)
$: selected = tempValue.filter(x => !x.onEmptyFilter)?.length > 0 $: selected = tempValue.filter(x => !x.onEmptyFilter)?.length > 0

View File

@ -5,7 +5,6 @@
Label, Label,
Select, Select,
Toggle, Toggle,
RadioGroup,
Icon, Icon,
DatePicker, DatePicker,
Modal, Modal,
@ -26,16 +25,18 @@
ALLOWABLE_STRING_TYPES, ALLOWABLE_STRING_TYPES,
ALLOWABLE_NUMBER_TYPES, ALLOWABLE_NUMBER_TYPES,
SWITCHABLE_TYPES, SWITCHABLE_TYPES,
PrettyRelationshipDefinitions,
} from "constants/backend" } from "constants/backend"
import { getAutoColumnInformation, buildAutoColumn } from "builderStore/utils" import { getAutoColumnInformation, buildAutoColumn } from "builderStore/utils"
import ConfirmDialog from "components/common/ConfirmDialog.svelte" import ConfirmDialog from "components/common/ConfirmDialog.svelte"
import { truncate } from "lodash"
import ModalBindableInput from "components/common/bindings/ModalBindableInput.svelte" import ModalBindableInput from "components/common/bindings/ModalBindableInput.svelte"
import { getBindings } from "components/backend/DataTable/formula" import { getBindings } from "components/backend/DataTable/formula"
import JSONSchemaModal from "./JSONSchemaModal.svelte" import JSONSchemaModal from "./JSONSchemaModal.svelte"
import { ValidColumnNameRegex } from "@budibase/shared-core" import { ValidColumnNameRegex } from "@budibase/shared-core"
import { FieldType, FieldSubtype, SourceName } from "@budibase/types"
import RelationshipSelector from "components/common/RelationshipSelector.svelte"
const AUTO_TYPE = "auto" const AUTO_TYPE = FIELDS.AUTO.type
const FORMULA_TYPE = FIELDS.FORMULA.type const FORMULA_TYPE = FIELDS.FORMULA.type
const LINK_TYPE = FIELDS.LINK.type const LINK_TYPE = FIELDS.LINK.type
const STRING_TYPE = FIELDS.STRING.type const STRING_TYPE = FIELDS.STRING.type
@ -50,13 +51,36 @@
export let field export let field
let mounted = false let mounted = false
let fieldDefinitions = cloneDeep(FIELDS) const fieldDefinitions = Object.values(FIELDS).reduce(
// Storing the fields by complex field id
(acc, field) => ({
...acc,
[makeFieldId(field.type, field.subtype)]: field,
}),
{}
)
function makeFieldId(type, subtype, autocolumn) {
// don't make field IDs for auto types
if (type === AUTO_TYPE || autocolumn) {
return type.toUpperCase()
} else {
return `${type}${subtype || ""}`.toUpperCase()
}
}
let originalName let originalName
let linkEditDisabled let linkEditDisabled
let primaryDisplay let primaryDisplay
let indexes = [...($tables.selected.indexes || [])] let indexes = [...($tables.selected.indexes || [])]
let isCreating = undefined let isCreating = undefined
let relationshipPart1 = PrettyRelationshipDefinitions.Many
let relationshipPart2 = PrettyRelationshipDefinitions.One
let relationshipTableIdPrimary = null
let relationshipTableIdSecondary = null
let table = $tables.selected let table = $tables.selected
let confirmDeleteDialog let confirmDeleteDialog
let savingColumn let savingColumn
@ -64,18 +88,65 @@
let jsonSchemaModal let jsonSchemaModal
let allowedTypes = [] let allowedTypes = []
let editableColumn = { let editableColumn = {
type: "string", type: FIELDS.STRING.type,
constraints: fieldDefinitions.STRING.constraints, constraints: FIELDS.STRING.constraints,
// Initial value for column name in other table for linked records // Initial value for column name in other table for linked records
fieldName: $tables.selected.name, fieldName: $tables.selected.name,
} }
let relationshipOpts1 = Object.values(PrettyRelationshipDefinitions)
let relationshipOpts2 = Object.values(PrettyRelationshipDefinitions)
$: if (primaryDisplay) { $: if (primaryDisplay) {
editableColumn.constraints.presence = { allowEmpty: false } editableColumn.constraints.presence = { allowEmpty: false }
} }
let relationshipMap = {
[RelationshipType.ONE_TO_MANY]: {
part1: PrettyRelationshipDefinitions.MANY,
part2: PrettyRelationshipDefinitions.ONE,
},
[RelationshipType.MANY_TO_MANY]: {
part1: PrettyRelationshipDefinitions.MANY,
part2: PrettyRelationshipDefinitions.MANY,
},
[RelationshipType.MANY_TO_ONE]: {
part1: PrettyRelationshipDefinitions.ONE,
part2: PrettyRelationshipDefinitions.MANY,
},
}
$: {
// this parses any changes the user has made when creating a new internal relationship
// into what we expect the schema to look like
if (editableColumn.type === LINK_TYPE) {
relationshipTableIdPrimary = table._id
if (relationshipPart1 === PrettyRelationshipDefinitions.ONE) {
relationshipOpts2 = relationshipOpts2.filter(
opt => opt !== PrettyRelationshipDefinitions.ONE
)
} else {
relationshipOpts2 = Object.values(PrettyRelationshipDefinitions)
}
if (relationshipPart2 === PrettyRelationshipDefinitions.ONE) {
relationshipOpts1 = relationshipOpts1.filter(
opt => opt !== PrettyRelationshipDefinitions.ONE
)
} else {
relationshipOpts1 = Object.values(PrettyRelationshipDefinitions)
}
// Determine the relationship type based on the selected values of both parts
editableColumn.relationshipType = Object.entries(relationshipMap).find(
([_, parts]) =>
parts.part1 === relationshipPart1 && parts.part2 === relationshipPart2
)?.[0]
// Set the tableId based on the selected table
editableColumn.tableId = relationshipTableIdSecondary
}
}
const initialiseField = (field, savingColumn) => { const initialiseField = (field, savingColumn) => {
isCreating = !field isCreating = !field
if (field && !savingColumn) { if (field && !savingColumn) {
editableColumn = cloneDeep(field) editableColumn = cloneDeep(field)
originalName = editableColumn.name ? editableColumn.name + "" : null originalName = editableColumn.name ? editableColumn.name + "" : null
@ -83,6 +154,20 @@
primaryDisplay = primaryDisplay =
$tables.selected.primaryDisplay == null || $tables.selected.primaryDisplay == null ||
$tables.selected.primaryDisplay === editableColumn.name $tables.selected.primaryDisplay === editableColumn.name
// Here we are setting the relationship values based on the editableColumn
// This part of the code is used when viewing an existing field hence the check
// for the tableId
if (editableColumn.type === LINK_TYPE && editableColumn.tableId) {
relationshipTableIdPrimary = table._id
relationshipTableIdSecondary = editableColumn.tableId
if (editableColumn.relationshipType in relationshipMap) {
const { part1, part2 } =
relationshipMap[editableColumn.relationshipType]
relationshipPart1 = part1
relationshipPart2 = part2
}
}
} else if (!savingColumn) { } else if (!savingColumn) {
let highestNumber = 0 let highestNumber = 0
Object.keys(table.schema).forEach(columnName => { Object.keys(table.schema).forEach(columnName => {
@ -99,7 +184,19 @@
editableColumn.name = "Column 01" editableColumn.name = "Column 01"
} }
} }
allowedTypes = getAllowedTypes()
if (!savingColumn) {
editableColumn.fieldId = makeFieldId(
editableColumn.type,
editableColumn.subtype,
editableColumn.autocolumn
)
allowedTypes = getAllowedTypes().map(t => ({
fieldId: makeFieldId(t.type, t.subtype),
...t,
}))
}
} }
$: initialiseField(field, savingColumn) $: initialiseField(field, savingColumn)
@ -157,14 +254,10 @@
!uneditable && !uneditable &&
editableColumn?.type !== AUTO_TYPE && editableColumn?.type !== AUTO_TYPE &&
!editableColumn.autocolumn !editableColumn.autocolumn
$: relationshipOptions = getRelationshipOptions(editableColumn)
$: external = table.type === "external" $: external = table.type === "external"
// in the case of internal tables the sourceId will just be undefined // in the case of internal tables the sourceId will just be undefined
$: tableOptions = $tables.list.filter( $: tableOptions = $tables.list.filter(
opt => opt => opt.type === table.type && table.sourceId === opt.sourceId
opt._id !== $tables.selected._id &&
opt.type === table.type &&
table.sourceId === opt.sourceId
) )
$: typeEnabled = $: typeEnabled =
!originalName || !originalName ||
@ -180,6 +273,8 @@
let saveColumn = cloneDeep(editableColumn) let saveColumn = cloneDeep(editableColumn)
delete saveColumn.fieldId
if (saveColumn.type === AUTO_TYPE) { if (saveColumn.type === AUTO_TYPE) {
saveColumn = buildAutoColumn( saveColumn = buildAutoColumn(
$tables.selected.name, $tables.selected.name,
@ -200,10 +295,7 @@
dispatch("updatecolumns") dispatch("updatecolumns")
gridDispatch("close-edit-column") gridDispatch("close-edit-column")
if ( if (saveColumn.type === LINK_TYPE) {
saveColumn.type === LINK_TYPE &&
saveColumn.relationshipType === RelationshipType.MANY_TO_MANY
) {
// Fetching the new tables // Fetching the new tables
tables.fetch() tables.fetch()
// Fetching the new relationships // Fetching the new relationships
@ -235,31 +327,43 @@
confirmDeleteDialog.hide() confirmDeleteDialog.hide()
dispatch("updatecolumns") dispatch("updatecolumns")
gridDispatch("close-edit-column") gridDispatch("close-edit-column")
if (editableColumn.type === LINK_TYPE) {
// Updating the relationships
datasources.fetch()
}
} }
} catch (error) { } catch (error) {
notifications.error(`Error deleting column: ${error.message}`) notifications.error(`Error deleting column: ${error.message}`)
} }
} }
function handleTypeChange(event) { function onHandleTypeChange(event) {
handleTypeChange(event.detail)
}
function handleTypeChange(type) {
// remove any extra fields that may not be related to this type // remove any extra fields that may not be related to this type
delete editableColumn.autocolumn delete editableColumn.autocolumn
delete editableColumn.subtype delete editableColumn.subtype
delete editableColumn.tableId delete editableColumn.tableId
delete editableColumn.relationshipType delete editableColumn.relationshipType
delete editableColumn.formulaType delete editableColumn.formulaType
delete editableColumn.constraints
// Add in defaults and initial definition // Add in defaults and initial definition
const definition = fieldDefinitions[event.detail?.toUpperCase()] const definition = fieldDefinitions[type?.toUpperCase()]
if (definition?.constraints) { if (definition?.constraints) {
editableColumn.constraints = definition.constraints editableColumn.constraints = definition.constraints
} }
editableColumn.type = definition.type
editableColumn.subtype = definition.subtype
// Default relationships many to many // Default relationships many to many
if (editableColumn.type === LINK_TYPE) { if (editableColumn.type === LINK_TYPE) {
editableColumn.relationshipType = RelationshipType.MANY_TO_MANY editableColumn.relationshipType = RelationshipType.MANY_TO_MANY
} } else if (editableColumn.type === FORMULA_TYPE) {
if (editableColumn.type === FORMULA_TYPE) {
editableColumn.formulaType = "dynamic" editableColumn.formulaType = "dynamic"
} }
} }
@ -288,35 +392,6 @@
return match ? parseInt(match[1]) : 0 return match ? parseInt(match[1]) : 0
} }
function getRelationshipOptions(field) {
if (!field || !field.tableId) {
return null
}
const linkTable = tableOptions?.find(table => table._id === field.tableId)
if (!linkTable) {
return null
}
const thisName = truncate(table.name, { length: 14 }),
linkName = truncate(linkTable.name, { length: 14 })
return [
{
name: `Many ${thisName} rows → many ${linkName} rows`,
alt: `Many ${table.name} rows → many ${linkTable.name} rows`,
value: RelationshipType.MANY_TO_MANY,
},
{
name: `One ${linkName} row → many ${thisName} rows`,
alt: `One ${linkTable.name} rows → many ${table.name} rows`,
value: RelationshipType.ONE_TO_MANY,
},
{
name: `One ${thisName} row → many ${linkName} rows`,
alt: `One ${table.name} rows → many ${linkTable.name} rows`,
value: RelationshipType.MANY_TO_ONE,
},
]
}
function getAllowedTypes() { function getAllowedTypes() {
if ( if (
originalName && originalName &&
@ -328,10 +403,29 @@
ALLOWABLE_NUMBER_TYPES.indexOf(editableColumn.type) !== -1 ALLOWABLE_NUMBER_TYPES.indexOf(editableColumn.type) !== -1
) { ) {
return ALLOWABLE_NUMBER_OPTIONS return ALLOWABLE_NUMBER_OPTIONS
} else if (!external) { }
const isUsers =
editableColumn.type === FieldType.BB_REFERENCE &&
editableColumn.subtype === FieldSubtype.USERS
if (!external) {
return [ return [
...Object.values(fieldDefinitions), FIELDS.STRING,
{ name: "Auto Column", type: AUTO_TYPE }, FIELDS.BARCODEQR,
FIELDS.LONGFORM,
FIELDS.OPTIONS,
FIELDS.ARRAY,
FIELDS.NUMBER,
FIELDS.BIGINT,
FIELDS.BOOLEAN,
FIELDS.DATETIME,
FIELDS.ATTACHMENT,
FIELDS.LINK,
FIELDS.FORMULA,
FIELDS.JSON,
isUsers ? FIELDS.USERS : FIELDS.USER,
FIELDS.AUTO,
] ]
} else { } else {
let fields = [ let fields = [
@ -344,6 +438,7 @@
FIELDS.BOOLEAN, FIELDS.BOOLEAN,
FIELDS.FORMULA, FIELDS.FORMULA,
FIELDS.BIGINT, FIELDS.BIGINT,
isUsers ? FIELDS.USERS : FIELDS.USER,
] ]
// no-sql or a spreadsheet // no-sql or a spreadsheet
if (!external || table.sql) { if (!external || table.sql) {
@ -357,8 +452,9 @@
if (!fieldToCheck) { if (!fieldToCheck) {
return return
} }
// most types need this, just make sure its always present // most types need this, just make sure its always present
if (fieldToCheck && !fieldToCheck.constraints) { if (!fieldToCheck.constraints) {
fieldToCheck.constraints = {} fieldToCheck.constraints = {}
} }
// some string types may have been built by server, may not always have constraints // some string types may have been built by server, may not always have constraints
@ -417,6 +513,13 @@
return newError return newError
} }
function isUsersColumn(column) {
return (
column.type === FieldType.BB_REFERENCE &&
[FieldSubtype.USER, FieldSubtype.USERS].includes(column.subtype)
)
}
onMount(() => { onMount(() => {
mounted = true mounted = true
}) })
@ -434,14 +537,14 @@
{/if} {/if}
<Select <Select
disabled={!typeEnabled} disabled={!typeEnabled}
bind:value={editableColumn.type} bind:value={editableColumn.fieldId}
on:change={handleTypeChange} on:change={onHandleTypeChange}
options={allowedTypes} options={allowedTypes}
getOptionLabel={field => field.name} getOptionLabel={field => field.name}
getOptionValue={field => field.type} getOptionValue={field => field.fieldId}
getOptionIcon={field => field.icon} getOptionIcon={field => field.icon}
isOptionEnabled={option => { isOptionEnabled={option => {
if (option.type == AUTO_TYPE) { if (option.type === AUTO_TYPE) {
return availableAutoColumnKeys?.length > 0 return availableAutoColumnKeys?.length > 0
} }
return true return true
@ -500,7 +603,7 @@
<DatePicker bind:value={editableColumn.constraints.datetime.latest} /> <DatePicker bind:value={editableColumn.constraints.datetime.latest} />
</div> </div>
</div> </div>
{#if datasource?.source !== "ORACLE" && datasource?.source !== "SQL_SERVER"} {#if datasource?.source !== SourceName.ORACLE && datasource?.source !== SourceName.SQL_SERVER && !editableColumn.dateOnly}
<div> <div>
<div class="row"> <div class="row">
<Label>Time zones</Label> <Label>Time zones</Label>
@ -520,6 +623,7 @@
/> />
</div> </div>
{/if} {/if}
<Toggle bind:value={editableColumn.dateOnly} text="Date only" />
{:else if editableColumn.type === "number" && !editableColumn.autocolumn} {:else if editableColumn.type === "number" && !editableColumn.autocolumn}
<div class="split-label"> <div class="split-label">
<div class="label-length"> <div class="label-length">
@ -546,30 +650,17 @@
</div> </div>
</div> </div>
{:else if editableColumn.type === "link"} {:else if editableColumn.type === "link"}
<Select <RelationshipSelector
label="Table" bind:relationshipPart1
disabled={linkEditDisabled} bind:relationshipPart2
bind:value={editableColumn.tableId} bind:relationshipTableIdPrimary
options={tableOptions} bind:relationshipTableIdSecondary
getOptionLabel={table => table.name} bind:editableColumn
getOptionValue={table => table._id} {relationshipOpts1}
/> {relationshipOpts2}
{#if relationshipOptions && relationshipOptions.length > 0} {linkEditDisabled}
<RadioGroup {tableOptions}
disabled={linkEditDisabled} {errors}
label="Define the relationship"
bind:value={editableColumn.relationshipType}
options={relationshipOptions}
getOptionLabel={option => option.name}
getOptionValue={option => option.value}
getOptionTitle={option => option.alt}
/>
{/if}
<Input
disabled={linkEditDisabled}
label={`Column name in other table`}
bind:value={editableColumn.fieldName}
error={errors.relatedName}
/> />
{:else if editableColumn.type === FORMULA_TYPE} {:else if editableColumn.type === FORMULA_TYPE}
{#if !table.sql} {#if !table.sql}
@ -616,6 +707,20 @@
<Button primary text on:click={openJsonSchemaEditor} <Button primary text on:click={openJsonSchemaEditor}
>Open schema editor</Button >Open schema editor</Button
> >
{:else if isUsersColumn(editableColumn) && datasource?.source !== SourceName.GOOGLE_SHEETS}
<Toggle
value={editableColumn.subtype === FieldSubtype.USERS}
on:change={e =>
handleTypeChange(
makeFieldId(
FieldType.BB_REFERENCE,
e.detail ? FieldSubtype.USERS : FieldSubtype.USER
)
)}
disabled={!isCreating}
thin
text="Allow multiple users"
/>
{/if} {/if}
{#if editableColumn.type === AUTO_TYPE || editableColumn.autocolumn} {#if editableColumn.type === AUTO_TYPE || editableColumn.autocolumn}
<Select <Select

View File

@ -0,0 +1,54 @@
<script>
import { ActionButton, Popover, Heading, Body, Button } from "@budibase/bbui"
import { store } from "builderStore"
let anchor
let open = false
const openSidePanel = () => {
store.update(state => ({
...state,
builderSidePanel: true,
}))
open = false
}
</script>
<div bind:this={anchor}>
<ActionButton on:click={() => (open = true)} icon="Help" quiet>
Why can't I edit this table?
</ActionButton>
</div>
<Popover bind:open {anchor} align="left">
<div class="content">
<Heading size="XS">The app users table is read only</Heading>
<Body size="S">
You can continue to view the users that have access to your application.
</Body>
<Body size="S">
Manage and invite more application users using the user side panel in the
top right of your screen.
</Body>
<div class="button">
<Button cta on:click={openSidePanel}>Open users panel</Button>
</div>
</div>
</Popover>
<style>
.content {
width: 300px;
padding: var(--spacing-l);
display: flex;
flex-direction: column;
gap: var(--spacing-l);
}
.content :global(.spectrum-Heading) {
font-weight: 400;
}
.button {
display: flex;
justify-content: flex-end;
}
</style>

View File

@ -180,7 +180,7 @@
<div class="hierarchy-items-container"> <div class="hierarchy-items-container">
<NavItem <NavItem
icon="UserGroup" icon="UserGroup"
text="Users" text="App users"
selected={$isActive("./table/:tableId") && selected={$isActive("./table/:tableId") &&
$tables.selected?._id === TableNames.USERS} $tables.selected?._id === TableNames.USERS}
on:click={() => selectTable(TableNames.USERS)} on:click={() => selectTable(TableNames.USERS)}

View File

@ -13,6 +13,8 @@
import { Helpers } from "@budibase/bbui" import { Helpers } from "@budibase/bbui"
import { RelationshipErrorChecker } from "./relationshipErrors" import { RelationshipErrorChecker } from "./relationshipErrors"
import { onMount } from "svelte" import { onMount } from "svelte"
import RelationshipSelector from "components/common/RelationshipSelector.svelte"
import { PrettyRelationshipDefinitions } from "constants/backend"
export let save export let save
export let datasource export let datasource
@ -22,16 +24,21 @@
export let selectedFromTable export let selectedFromTable
export let close export let close
const relationshipTypes = [ let relationshipMap = {
{ [RelationshipType.MANY_TO_MANY]: {
label: "One to Many", part1: PrettyRelationshipDefinitions.MANY,
value: RelationshipType.MANY_TO_ONE, part2: PrettyRelationshipDefinitions.MANY,
}, },
{ [RelationshipType.MANY_TO_ONE]: {
label: "Many to Many", part1: PrettyRelationshipDefinitions.ONE,
value: RelationshipType.MANY_TO_MANY, part2: PrettyRelationshipDefinitions.MANY,
}, },
] }
let relationshipOpts1 = Object.values(PrettyRelationshipDefinitions)
let relationshipOpts2 = Object.values(PrettyRelationshipDefinitions)
let relationshipPart1 = PrettyRelationshipDefinitions.MANY
let relationshipPart2 = PrettyRelationshipDefinitions.ONE
let originalFromColumnName = toRelationship.name, let originalFromColumnName = toRelationship.name,
originalToColumnName = fromRelationship.name originalToColumnName = fromRelationship.name
@ -49,15 +56,34 @@
) )
let errors = {} let errors = {}
let fromPrimary, fromForeign, fromColumn, toColumn let fromPrimary, fromForeign, fromColumn, toColumn
let fromId, toId, throughId, throughToKey, throughFromKey
let throughId, throughToKey, throughFromKey
let isManyToMany, isManyToOne, relationshipType let isManyToMany, isManyToOne, relationshipType
let hasValidated = false let hasValidated = false
$: fromId = null
$: toId = null
$: tableOptions = plusTables.map(table => ({ $: tableOptions = plusTables.map(table => ({
label: table.name, label: table.name,
value: table._id, value: table._id,
name: table.name,
_id: table._id,
})) }))
$: valid = getErrorCount(errors) === 0 && allRequiredAttributesSet()
$: {
// Determine the relationship type based on the selected values of both parts
relationshipType = Object.entries(relationshipMap).find(
([_, parts]) =>
parts.part1 === relationshipPart1 && parts.part2 === relationshipPart2
)?.[0]
changed(() => {
hasValidated = false
})
}
$: valid =
getErrorCount(errors) === 0 && allRequiredAttributesSet(relationshipType)
$: isManyToMany = relationshipType === RelationshipType.MANY_TO_MANY $: isManyToMany = relationshipType === RelationshipType.MANY_TO_MANY
$: isManyToOne = relationshipType === RelationshipType.MANY_TO_ONE $: isManyToOne = relationshipType === RelationshipType.MANY_TO_ONE
@ -114,7 +140,7 @@
return Object.entries(errors).filter(entry => !!entry[1]).length return Object.entries(errors).filter(entry => !!entry[1]).length
} }
function allRequiredAttributesSet() { function allRequiredAttributesSet(relationshipType) {
const base = getTable(fromId) && getTable(toId) && fromColumn && toColumn const base = getTable(fromId) && getTable(toId) && fromColumn && toColumn
if (relationshipType === RelationshipType.MANY_TO_ONE) { if (relationshipType === RelationshipType.MANY_TO_ONE) {
return base && fromPrimary && fromForeign return base && fromPrimary && fromForeign
@ -124,9 +150,10 @@
} }
function validate() { function validate() {
if (!allRequiredAttributesSet() && !hasValidated) { if (!allRequiredAttributesSet(relationshipType) && !hasValidated) {
return return
} }
hasValidated = true hasValidated = true
errorChecker.setType(relationshipType) errorChecker.setType(relationshipType)
const fromTable = getTable(fromId), const fromTable = getTable(fromId),
@ -336,33 +363,34 @@
onConfirm={saveRelationship} onConfirm={saveRelationship}
disabled={!valid} disabled={!valid}
> >
<Select
label="Relationship type"
options={relationshipTypes}
bind:value={relationshipType}
bind:error={errors.relationshipType}
on:change={() =>
changed(() => {
hasValidated = false
})}
/>
<div class="headings"> <div class="headings">
<Detail>Tables</Detail> <Detail>Tables</Detail>
</div> </div>
{#if !selectedFromTable}
<Select <RelationshipSelector
label="Select from table" bind:relationshipPart1
options={tableOptions} bind:relationshipPart2
bind:value={fromId} bind:relationshipTableIdPrimary={fromId}
bind:error={errors.fromTable} bind:relationshipTableIdSecondary={toId}
on:change={e => {relationshipOpts1}
changed(() => { {relationshipOpts2}
const table = plusTables.find(tbl => tbl._id === e.detail) {tableOptions}
fromColumn = table?.name || "" {errors}
fromPrimary = table?.primary?.[0] primaryDisabled={selectedFromTable}
})} primaryTableChanged={e =>
/> changed(() => {
{/if} const table = plusTables.find(tbl => tbl._id === e.detail)
fromColumn = table?.name || ""
fromPrimary = table?.primary?.[0]
})}
secondaryTableChanged={e =>
changed(() => {
const table = plusTables.find(tbl => tbl._id === e.detail)
toColumn = table.name || ""
fromForeign = null
})}
/>
{#if isManyToOne && fromId} {#if isManyToOne && fromId}
<Select <Select
label={`Primary Key (${getTable(fromId).name})`} label={`Primary Key (${getTable(fromId).name})`}
@ -372,18 +400,6 @@
on:change={changed} on:change={changed}
/> />
{/if} {/if}
<Select
label={"Select to table"}
options={tableOptions}
bind:value={toId}
bind:error={errors.toTable}
on:change={e =>
changed(() => {
const table = plusTables.find(tbl => tbl._id === e.detail)
toColumn = table.name || ""
fromForeign = null
})}
/>
{#if isManyToMany} {#if isManyToMany}
<Select <Select
label={"Through"} label={"Through"}

View File

@ -57,7 +57,7 @@
{#if $store.error} {#if $store.error}
<InlineAlert <InlineAlert
type="error" type="error"
header={$store.error.title} header="Error fetching {tableType}"
message={$store.error.description} message={$store.error.description}
/> />
{/if} {/if}

View File

@ -1,6 +1,6 @@
import { derived, writable, get } from "svelte/store" import { derived, writable, get } from "svelte/store"
import { keepOpen, notifications } from "@budibase/bbui" import { keepOpen, notifications } from "@budibase/bbui"
import { datasources, ImportTableError, tables } from "stores/backend" import { datasources, tables } from "stores/backend"
export const createTableSelectionStore = (integration, datasource) => { export const createTableSelectionStore = (integration, datasource) => {
const tableNamesStore = writable([]) const tableNamesStore = writable([])
@ -30,12 +30,7 @@ export const createTableSelectionStore = (integration, datasource) => {
notifications.success(`Tables fetched successfully.`) notifications.success(`Tables fetched successfully.`)
await onComplete() await onComplete()
} catch (err) { } catch (err) {
if (err instanceof ImportTableError) { errorStore.set(err)
errorStore.set(err)
} else {
notifications.error("Error fetching tables.")
}
return keepOpen return keepOpen
} }
} }

View File

@ -49,6 +49,15 @@
label: "Long Form Text", label: "Long Form Text",
value: FIELDS.LONGFORM.type, value: FIELDS.LONGFORM.type,
}, },
{
label: "User",
value: `${FIELDS.USER.type}${FIELDS.USER.subtype}`,
},
{
label: "Users",
value: `${FIELDS.USERS.type}${FIELDS.USERS.subtype}`,
},
] ]
$: { $: {
@ -143,7 +152,7 @@
<div class="field"> <div class="field">
<span>{name}</span> <span>{name}</span>
<Select <Select
value={schema[name]?.type} value={`${schema[name]?.type}${schema[name]?.subtype || ""}`}
options={typeOptions} options={typeOptions}
placeholder={null} placeholder={null}
getOptionLabel={option => option.label} getOptionLabel={option => option.label}

View File

@ -3,6 +3,7 @@
import { FIELDS } from "constants/backend" import { FIELDS } from "constants/backend"
import { API } from "api" import { API } from "api"
import { parseFile } from "./utils" import { parseFile } from "./utils"
import { canBeDisplayColumn } from "@budibase/shared-core"
export let rows = [] export let rows = []
export let schema = {} export let schema = {}
@ -10,36 +11,82 @@
export let displayColumn = null export let displayColumn = null
export let promptUpload = false export let promptUpload = false
const typeOptions = [ const typeOptions = {
{ [FIELDS.STRING.type]: {
label: "Text", label: "Text",
value: FIELDS.STRING.type, value: FIELDS.STRING.type,
config: {
type: FIELDS.STRING.type,
constraints: FIELDS.STRING.constraints,
},
}, },
{ [FIELDS.NUMBER.type]: {
label: "Number", label: "Number",
value: FIELDS.NUMBER.type, value: FIELDS.NUMBER.type,
config: {
type: FIELDS.NUMBER.type,
constraints: FIELDS.NUMBER.constraints,
},
}, },
{ [FIELDS.DATETIME.type]: {
label: "Date", label: "Date",
value: FIELDS.DATETIME.type, value: FIELDS.DATETIME.type,
config: {
type: FIELDS.DATETIME.type,
constraints: FIELDS.DATETIME.constraints,
},
}, },
{ [FIELDS.OPTIONS.type]: {
label: "Options", label: "Options",
value: FIELDS.OPTIONS.type, value: FIELDS.OPTIONS.type,
config: {
type: FIELDS.OPTIONS.type,
constraints: FIELDS.OPTIONS.constraints,
},
}, },
{ [FIELDS.ARRAY.type]: {
label: "Multi-select", label: "Multi-select",
value: FIELDS.ARRAY.type, value: FIELDS.ARRAY.type,
config: {
type: FIELDS.ARRAY.type,
constraints: FIELDS.ARRAY.constraints,
},
}, },
{ [FIELDS.BARCODEQR.type]: {
label: "Barcode/QR", label: "Barcode/QR",
value: FIELDS.BARCODEQR.type, value: FIELDS.BARCODEQR.type,
config: {
type: FIELDS.BARCODEQR.type,
constraints: FIELDS.BARCODEQR.constraints,
},
}, },
{ [FIELDS.LONGFORM.type]: {
label: "Long Form Text", label: "Long Form Text",
value: FIELDS.LONGFORM.type, value: FIELDS.LONGFORM.type,
config: {
type: FIELDS.LONGFORM.type,
constraints: FIELDS.LONGFORM.constraints,
},
}, },
] user: {
label: "User",
value: "user",
config: {
type: FIELDS.USER.type,
subtype: FIELDS.USER.subtype,
constraints: FIELDS.USER.constraints,
},
},
users: {
label: "Users",
value: "users",
config: {
type: FIELDS.USERS.type,
subtype: FIELDS.USERS.subtype,
constraints: FIELDS.USERS.constraints,
},
},
}
let fileInput let fileInput
let error = null let error = null
@ -48,10 +95,16 @@
let validation = {} let validation = {}
let validateHash = "" let validateHash = ""
let errors = {} let errors = {}
let selectedColumnTypes = {}
$: displayColumnOptions = Object.keys(schema || {}).filter(column => { $: displayColumnOptions = Object.keys(schema || {}).filter(column => {
return validation[column] return validation[column] && canBeDisplayColumn(schema[column].type)
}) })
$: if (displayColumn && !canBeDisplayColumn(schema[displayColumn].type)) {
displayColumn = null
}
$: { $: {
// binding in consumer is causing double renders here // binding in consumer is causing double renders here
const newValidateHash = JSON.stringify(rows) + JSON.stringify(schema) const newValidateHash = JSON.stringify(rows) + JSON.stringify(schema)
@ -72,6 +125,13 @@
rows = response.rows rows = response.rows
schema = response.schema schema = response.schema
fileName = response.fileName fileName = response.fileName
selectedColumnTypes = Object.entries(response.schema).reduce(
(acc, [colName, fieldConfig]) => ({
...acc,
[colName]: fieldConfig.type,
}),
{}
)
} catch (e) { } catch (e) {
loading = false loading = false
error = e error = e
@ -98,8 +158,10 @@
} }
const handleChange = (name, e) => { const handleChange = (name, e) => {
schema[name].type = e.detail const { config } = typeOptions[e.detail]
schema[name].constraints = FIELDS[e.detail.toUpperCase()].constraints schema[name].type = config.type
schema[name].subtype = config.subtype
schema[name].constraints = config.constraints
} }
const openFileUpload = (promptUpload, fileInput) => { const openFileUpload = (promptUpload, fileInput) => {
@ -142,9 +204,9 @@
<div class="field"> <div class="field">
<span>{column.name}</span> <span>{column.name}</span>
<Select <Select
bind:value={column.type} bind:value={selectedColumnTypes[column.name]}
on:change={e => handleChange(name, e)} on:change={e => handleChange(name, e)}
options={typeOptions} options={Object.values(typeOptions)}
placeholder={null} placeholder={null}
getOptionLabel={option => option.label} getOptionLabel={option => option.label}
getOptionValue={option => option.value} getOptionValue={option => option.value}

View File

@ -3,16 +3,19 @@
import { API } from "api" import { API } from "api"
import { Select, Label, Multiselect } from "@budibase/bbui" import { Select, Label, Multiselect } from "@budibase/bbui"
import { capitalise } from "../../helpers" import { capitalise } from "../../helpers"
import { createEventDispatcher } from "svelte"
export let schema export let schema
export let linkedRows = [] export let linkedRows = []
const dispatch = createEventDispatcher()
let rows = [] let rows = []
let linkedIds = (Array.isArray(linkedRows) ? linkedRows : [])?.map( let linkedIds = []
$: linkedIds = (Array.isArray(linkedRows) ? linkedRows : [])?.map(
row => row?._id || row row => row?._id || row
) )
$: linkedRows = linkedIds
$: label = capitalise(schema.name) $: label = capitalise(schema.name)
$: linkedTableId = schema.tableId $: linkedTableId = schema.tableId
$: linkedTable = $tables.list.find(table => table._id === linkedTableId) $: linkedTable = $tables.list.find(table => table._id === linkedTableId)
@ -44,7 +47,10 @@
options={rows} options={rows}
getOptionLabel={getPrettyName} getOptionLabel={getPrettyName}
getOptionValue={row => row._id} getOptionValue={row => row._id}
on:change={e => (linkedIds = e.detail ? [e.detail] : [])} on:change={e => {
linkedIds = e.detail ? [e.detail] : []
dispatch("change", linkedIds)
}}
{label} {label}
sort sort
/> />
@ -56,5 +62,6 @@
getOptionLabel={getPrettyName} getOptionLabel={getPrettyName}
getOptionValue={row => row._id} getOptionValue={row => row._id}
sort sort
on:change={() => dispatch("change", linkedIds)}
/> />
{/if} {/if}

View File

@ -102,7 +102,7 @@
</div> </div>
{/if} {/if}
<div class="text" title={showTooltip ? text : null}> <div class="text" title={showTooltip ? text : null}>
{text} <span title={text}>{text}</span>
{#if selectedBy} {#if selectedBy}
<UserAvatars size="XS" users={selectedBy} /> <UserAvatars size="XS" users={selectedBy} />
{/if} {/if}
@ -227,9 +227,6 @@
.text { .text {
font-weight: 600; font-weight: 600;
font-size: 12px; font-size: 12px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
flex: 1 1 auto; flex: 1 1 auto;
color: var(--spectrum-global-color-gray-900); color: var(--spectrum-global-color-gray-900);
order: 2; order: 2;
@ -238,6 +235,11 @@
align-items: center; align-items: center;
gap: 8px; gap: 8px;
} }
.text span {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.scrollable .text { .scrollable .text {
flex: 0 0 auto; flex: 0 0 auto;
max-width: 160px; max-width: 160px;

View File

@ -0,0 +1,84 @@
<script>
import { Select, Input } from "@budibase/bbui"
export let relationshipPart1
export let relationshipPart2
export let relationshipTableIdPrimary
export let relationshipTableIdSecondary
export let editableColumn
export let linkEditDisabled = false
export let tableOptions
export let errors
export let relationshipOpts1
export let relationshipOpts2
export let primaryTableChanged
export let secondaryTableChanged
export let primaryDisabled = true
</script>
<div class="relationship-container">
<div class="relationship-part">
<Select
disabled={linkEditDisabled}
bind:value={relationshipPart1}
options={relationshipOpts1}
bind:error={errors.relationshipType}
/>
</div>
<div class="relationship-label">in</div>
<div class="relationship-part">
<Select
disabled={primaryDisabled}
options={tableOptions}
getOptionLabel={table => table.name}
getOptionValue={table => table._id}
bind:value={relationshipTableIdPrimary}
on:change={primaryTableChanged}
bind:error={errors.fromTable}
/>
</div>
</div>
<div class="relationship-container">
<div class="relationship-part">
<Select
disabled={linkEditDisabled}
bind:value={relationshipPart2}
options={relationshipOpts2}
getOptionLabel={option => "To " + option.toLowerCase()}
/>
</div>
<div class="relationship-label">in</div>
<div class="relationship-part">
<Select
disabled={linkEditDisabled}
bind:value={relationshipTableIdSecondary}
bind:error={errors.toTable}
options={tableOptions.filter(
table => table._id !== relationshipTableIdPrimary
)}
getOptionLabel={table => table.name}
getOptionValue={table => table._id}
on:change={secondaryTableChanged}
/>
</div>
</div>
{#if editableColumn}
<Input
disabled={linkEditDisabled}
label={`Column name in other table`}
bind:value={editableColumn.fieldName}
error={errors.relatedName}
/>
{/if}
<style>
.relationship-container {
display: flex;
align-items: center;
gap: 20px;
}
.relationship-part {
flex-basis: 60%;
}
</style>

View File

@ -38,14 +38,12 @@
hoverable hoverable
on:click={store.undo} on:click={store.undo}
disabled={!$store.canUndo} disabled={!$store.canUndo}
tooltip="Undo latest change"
/> />
<Icon <Icon
name="Redo" name="Redo"
hoverable hoverable
on:click={store.redo} on:click={store.redo}
disabled={!$store.canRedo} disabled={!$store.canRedo}
tooltip="Redo latest undo"
/> />
</div> </div>

View File

@ -0,0 +1,250 @@
<script>
import { Icon, Input, Drawer, Button } from "@budibase/bbui"
import {
readableToRuntimeBinding,
runtimeToReadableBinding,
} from "builderStore/dataBinding"
import ClientBindingPanel from "components/common/bindings/ClientBindingPanel.svelte"
import { createEventDispatcher, setContext } from "svelte"
import { isJSBinding } from "@budibase/string-templates"
export let panel = ClientBindingPanel
export let value = ""
export let bindings = []
export let title = "Bindings"
export let placeholder
export let label
export let disabled = false
export let fillWidth
export let allowJS = true
export let allowHelpers = true
export let updateOnChange = true
export let drawerLeft
export let type
export let schema
const dispatch = createEventDispatcher()
let bindingDrawer
let valid = true
let currentVal = value
$: readableValue = runtimeToReadableBinding(bindings, value)
$: tempValue = readableValue
$: isJS = isJSBinding(value)
const saveBinding = () => {
onChange(tempValue)
onBlur()
bindingDrawer.hide()
}
setContext("binding-drawer-actions", {
save: saveBinding,
})
const onChange = value => {
if (type === "link" && value && hasValidLinks(value)) {
currentVal = value.split(",")
} else if (type === "array" && value && hasValidOptions(value)) {
currentVal = value.split(",")
} else {
currentVal = readableToRuntimeBinding(bindings, value)
}
dispatch("change", currentVal)
}
const onBlur = () => {
dispatch("blur", currentVal)
}
const isValidDate = value => {
return !value || !isNaN(new Date(value).valueOf())
}
const hasValidLinks = value => {
let links = []
if (Array.isArray(value)) {
links = value
} else if (value && typeof value === "string") {
links = value.split(",")
} else {
return !value
}
return links.every(link => link.startsWith("ro_"))
}
const hasValidOptions = value => {
let links = []
if (Array.isArray(value)) {
links = value
} else if (value && typeof value === "string") {
links = value.split(",")
} else {
return !value
}
return links.every(link => schema?.constraints?.inclusion?.includes(link))
}
const isValidBoolean = value => {
return value === "false" || value === "true" || value == ""
}
const validationMap = {
date: isValidDate,
datetime: isValidDate,
link: hasValidLinks,
array: hasValidOptions,
longform: value => !isJSBinding(value),
json: value => !isJSBinding(value),
boolean: isValidBoolean,
}
const isValid = value => {
const validate = validationMap[type]
return validate ? validate(value) : true
}
const getIconClass = (value, type) => {
if (type === "longform" && !isJSBinding(value)) {
return "text-area-slot-icon"
}
if (type === "json" && !isJSBinding(value)) {
return "json-slot-icon"
}
if (type !== "string" && type !== "number") {
return "slot-icon"
}
return ""
}
</script>
<div class="control" class:disabled>
{#if !isValid(value)}
<Input
{label}
{disabled}
readonly={isJS}
value={isJS ? "(JavaScript function)" : readableValue}
on:change={event => onChange(event.detail)}
on:blur={onBlur}
{placeholder}
{updateOnChange}
/>
<div
class="icon"
on:click={() => {
if (!isJS) {
dispatch("change", "")
}
}}
>
<Icon disabled={isJS} size="S" name="Close" />
</div>
{:else}
<slot
{label}
{disabled}
readonly={isJS}
value={isJS ? "(JavaScript function)" : readableValue}
{placeholder}
{updateOnChange}
/>
{/if}
{#if !disabled && type !== "formula"}
<div
class={`icon ${getIconClass(value, type)}`}
on:click={() => {
bindingDrawer.show()
}}
>
<Icon size="S" name="FlashOn" />
</div>
{/if}
</div>
<Drawer
on:drawerHide
on:drawerShow
{fillWidth}
bind:this={bindingDrawer}
{title}
left={drawerLeft}
headless
>
<svelte:fragment slot="description">
Add the objects on the left to enrich your text.
</svelte:fragment>
<Button cta slot="buttons" disabled={!valid} on:click={saveBinding}>
Save
</Button>
<svelte:component
this={panel}
slot="body"
bind:valid
value={readableValue}
on:change={event => (tempValue = event.detail)}
{bindings}
{allowJS}
{allowHelpers}
/>
</Drawer>
<style>
.control {
flex: 1;
position: relative;
}
.slot-icon {
right: 31px !important;
border-right: 1px solid var(--spectrum-alias-border-color);
border-top-right-radius: 0px !important;
border-bottom-right-radius: 0px !important;
}
.text-area-slot-icon {
border-bottom: 1px solid var(--spectrum-alias-border-color);
border-bottom-right-radius: 0px !important;
top: 26px !important;
}
.json-slot-icon {
border-bottom: 1px solid var(--spectrum-alias-border-color);
border-bottom-right-radius: 0px !important;
top: 23px !important;
right: 0px !important;
}
.icon {
right: 1px;
bottom: 1px;
position: absolute;
justify-content: center;
align-items: center;
display: flex;
flex-direction: row;
box-sizing: border-box;
border-left: 1px solid var(--spectrum-alias-border-color);
border-top-right-radius: var(--spectrum-alias-border-radius-regular);
border-bottom-right-radius: var(--spectrum-alias-border-radius-regular);
width: 31px;
color: var(--spectrum-alias-text-color);
background-color: var(--spectrum-global-color-gray-75);
transition: background-color
var(--spectrum-global-animation-duration-100, 130ms),
box-shadow var(--spectrum-global-animation-duration-100, 130ms),
border-color var(--spectrum-global-animation-duration-100, 130ms);
height: calc(var(--spectrum-alias-item-height-m) - 2px);
}
.icon:hover {
cursor: pointer;
color: var(--spectrum-alias-text-color-hover);
background-color: var(--spectrum-global-color-gray-50);
border-color: var(--spectrum-alias-border-color-hover);
}
.control:not(.disabled) :global(.spectrum-Textfield-input) {
padding-right: 40px;
}
</style>

View File

@ -224,10 +224,10 @@
<span <span
class="app-link" class="app-link"
on:click={() => { on:click={() => {
appActionPopover.hide()
if (isPublished) { if (isPublished) {
viewApp() viewApp()
} else { } else {
appActionPopover.hide()
updateAppModal.show() updateAppModal.show()
} }
}} }}

View File

@ -12,11 +12,19 @@
export let borderLeft = false export let borderLeft = false
export let borderRight = false export let borderRight = false
export let wide = false export let wide = false
export let extraWide = false
export let closeButtonIcon = "Close"
$: customHeaderContent = $$slots["panel-header-content"] $: customHeaderContent = $$slots["panel-header-content"]
</script> </script>
<div class="panel" class:wide class:borderLeft class:borderRight> <div
class="panel"
class:wide
class:extraWide
class:borderLeft
class:borderRight
>
<div class="header" class:custom={customHeaderContent}> <div class="header" class:custom={customHeaderContent}>
{#if showBackButton} {#if showBackButton}
<Icon name="ArrowLeft" hoverable on:click={onClickBackButton} /> <Icon name="ArrowLeft" hoverable on:click={onClickBackButton} />
@ -33,7 +41,7 @@
</div> </div>
{/if} {/if}
{#if showCloseButton} {#if showCloseButton}
<Icon name="Close" hoverable on:click={onClickCloseButton} /> <Icon name={closeButtonIcon} hoverable on:click={onClickCloseButton} />
{/if} {/if}
</div> </div>
@ -70,6 +78,10 @@
width: 310px; width: 310px;
flex: 0 0 310px; flex: 0 0 310px;
} }
.panel.extraWide {
width: 450px;
flex: 0 0 450px;
}
.header { .header {
flex: 0 0 48px; flex: 0 0 48px;
display: flex; display: flex;

View File

@ -65,6 +65,7 @@ const componentMap = {
"field/array": FormFieldSelect, "field/array": FormFieldSelect,
"field/json": FormFieldSelect, "field/json": FormFieldSelect,
"field/barcodeqr": FormFieldSelect, "field/barcodeqr": FormFieldSelect,
"field/bb_reference": FormFieldSelect,
// Some validation types are the same as others, so not all types are // Some validation types are the same as others, so not all types are
// explicitly listed here. e.g. options uses string validation // explicitly listed here. e.g. options uses string validation
"validation/string": ValidationEditor, "validation/string": ValidationEditor,
@ -74,6 +75,7 @@ const componentMap = {
"validation/datetime": ValidationEditor, "validation/datetime": ValidationEditor,
"validation/attachment": ValidationEditor, "validation/attachment": ValidationEditor,
"validation/link": ValidationEditor, "validation/link": ValidationEditor,
"validation/bb_reference": ValidationEditor,
} }
export const getComponentForSetting = setting => { export const getComponentForSetting = setting => {

View File

@ -3,7 +3,6 @@
import { createEventDispatcher } from "svelte" import { createEventDispatcher } from "svelte"
import { notifications } from "@budibase/bbui" import { notifications } from "@budibase/bbui"
import ButtonActionDrawer from "./ButtonActionDrawer.svelte" import ButtonActionDrawer from "./ButtonActionDrawer.svelte"
import { automationStore } from "builderStore"
import { cloneDeep } from "lodash/fp" import { cloneDeep } from "lodash/fp"
const dispatch = createEventDispatcher() const dispatch = createEventDispatcher()
@ -24,47 +23,11 @@
} }
const saveEventData = async () => { const saveEventData = async () => {
// any automations that need created from event triggers
const automationsToCreate = tmpValue.filter(
action => action["##eventHandlerType"] === "Trigger Automation"
)
for (let action of automationsToCreate) {
await createAutomation(action.parameters)
}
dispatch("change", tmpValue) dispatch("change", tmpValue)
notifications.success("Component actions saved.") notifications.success("Component actions saved.")
drawer.hide() drawer.hide()
} }
// called by the parent modal when actions are saved
const createAutomation = async parameters => {
if (parameters.automationId || !parameters.newAutomationName) {
return
}
try {
let trigger = automationStore.actions.constructBlock(
"TRIGGER",
"APP",
$automationStore.blockDefinitions.TRIGGER.APP
)
trigger.inputs = {
fields: Object.keys(parameters.fields ?? {}).reduce((fields, key) => {
fields[key] = "string"
return fields
}, {}),
}
const automation = await automationStore.actions.create(
parameters.newAutomationName,
trigger
)
parameters.automationId = automation._id
delete parameters.newAutomationName
} catch (error) {
notifications.error("Error creating automation")
}
}
$: actionCount = value?.length $: actionCount = value?.length
$: actionText = `${actionCount || "No"} action${ $: actionText = `${actionCount || "No"} action${
actionCount !== 1 ? "s" : "" actionCount !== 1 ? "s" : ""

View File

@ -1,6 +1,5 @@
<script> <script>
import { currentAsset, store } from "builderStore" import { currentAsset, store } from "builderStore"
import { onMount } from "svelte"
import { Label, Combobox, Select } from "@budibase/bbui" import { Label, Combobox, Select } from "@budibase/bbui"
import { import {
getActionProviderComponents, getActionProviderComponents,
@ -10,12 +9,6 @@
export let parameters export let parameters
onMount(() => {
if (!parameters.type) {
parameters.type = "top"
}
})
$: formComponent = findComponent($currentAsset.props, parameters.componentId) $: formComponent = findComponent($currentAsset.props, parameters.componentId)
$: formSchema = buildFormSchema(formComponent) $: formSchema = buildFormSchema(formComponent)
$: fieldOptions = Object.keys(formSchema || {}) $: fieldOptions = Object.keys(formSchema || {})

View File

@ -1,91 +0,0 @@
<script>
import { Button, ActionButton, Drawer } from "@budibase/bbui"
import { createEventDispatcher } from "svelte"
import ColumnDrawer from "./ColumnEditor/ColumnDrawer.svelte"
import { cloneDeep } from "lodash/fp"
import {
getDatasourceForProvider,
getSchemaForDatasource,
} from "builderStore/dataBinding"
import { currentAsset } from "builderStore"
import { getFields } from "helpers/searchFields"
export let componentInstance
export let value = []
export let allowCellEditing = true
export let subject = "Table"
const dispatch = createEventDispatcher()
let drawer
let boundValue
$: datasource = getDatasourceForProvider($currentAsset, componentInstance)
$: schema = getSchema($currentAsset, datasource)
$: options = allowCellEditing
? Object.keys(schema || {})
: enrichedSchemaFields?.map(field => field.name)
$: sanitisedValue = getValidColumns(value, options)
$: updateBoundValue(sanitisedValue)
$: enrichedSchemaFields = getFields(Object.values(schema || {}), {
allowLinks: true,
})
const getSchema = (asset, datasource) => {
const schema = getSchemaForDatasource(asset, datasource).schema
// Don't show ID and rev in tables
if (schema) {
delete schema._id
delete schema._rev
}
return schema
}
const updateBoundValue = value => {
boundValue = cloneDeep(value)
}
const getValidColumns = (columns, options) => {
if (!Array.isArray(columns) || !columns.length) {
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 options.includes(column.name)
})
}
const open = () => {
updateBoundValue(sanitisedValue)
drawer.show()
}
const save = () => {
dispatch("change", getValidColumns(boundValue, options))
drawer.hide()
}
</script>
<ActionButton on:click={open}>Configure columns</ActionButton>
<Drawer bind:this={drawer} title="{subject} Columns">
<svelte:fragment slot="description">
Configure the columns in your {subject.toLowerCase()}.
</svelte:fragment>
<Button cta slot="buttons" on:click={save}>Save</Button>
<ColumnDrawer
slot="body"
bind:columns={boundValue}
{options}
{schema}
{allowCellEditing}
/>
</Drawer>

View File

@ -37,7 +37,7 @@
} }
$: datasource = getDatasourceForProvider($currentAsset, componentInstance) $: datasource = getDatasourceForProvider($currentAsset, componentInstance)
$: resourceId = datasource.resourceId || datasource.tableId $: resourceId = datasource?.resourceId || datasource?.tableId
$: if (!isEqual(value, cachedValue)) { $: if (!isEqual(value, cachedValue)) {
cachedValue = cloneDeep(value) cachedValue = cloneDeep(value)

View File

@ -43,4 +43,5 @@ export const FieldTypeToComponentMap = {
link: "relationshipfield", link: "relationshipfield",
json: "jsonfield", json: "jsonfield",
barcodeqr: "codescanner", barcodeqr: "codescanner",
bb_reference: "bbreferencefield",
} }

View File

@ -3,21 +3,23 @@
Body, Body,
Button, Button,
Combobox, Combobox,
Multiselect,
DatePicker, DatePicker,
DrawerContent, DrawerContent,
Icon, Icon,
Input, Input,
Layout,
Select,
Label, Label,
Layout,
Multiselect,
Select,
} from "@budibase/bbui" } from "@budibase/bbui"
import DrawerBindableInput from "components/common/bindings/DrawerBindableInput.svelte" import DrawerBindableInput from "components/common/bindings/DrawerBindableInput.svelte"
import ClientBindingPanel from "components/common/bindings/ClientBindingPanel.svelte" import ClientBindingPanel from "components/common/bindings/ClientBindingPanel.svelte"
import { generate } from "shortid" import { generate } from "shortid"
import { LuceneUtils, Constants } from "@budibase/frontend-core" import { Constants, LuceneUtils } from "@budibase/frontend-core"
import { getFields } from "helpers/searchFields" import { getFields } from "helpers/searchFields"
import { FieldType } from "@budibase/types"
import { createEventDispatcher, onMount } from "svelte" import { createEventDispatcher, onMount } from "svelte"
import FilterUsers from "./FilterUsers.svelte"
export let schemaFields export let schemaFields
export let filters = [] export let filters = []
@ -29,7 +31,6 @@
const dispatch = createEventDispatcher() const dispatch = createEventDispatcher()
const { OperatorOptions } = Constants const { OperatorOptions } = Constants
const { getValidOperatorsForType } = LuceneUtils
const KeyedFieldRegex = /\d[0-9]*:/g const KeyedFieldRegex = /\d[0-9]*:/g
const behaviourOptions = [ const behaviourOptions = [
{ value: "and", label: "Match all filters" }, { value: "and", label: "Match all filters" },
@ -120,22 +121,19 @@
return enrichedSchemaFields.find(field => field.name === filter.field) return enrichedSchemaFields.find(field => field.name === filter.field)
} }
const santizeTypes = filter => { const sanitizeTypes = filter => {
// Update type based on field // Update type based on field
const fieldSchema = enrichedSchemaFields.find(x => x.name === filter.field) const fieldSchema = enrichedSchemaFields.find(x => x.name === filter.field)
filter.type = fieldSchema?.type filter.type = fieldSchema?.type
filter.subtype = fieldSchema?.subtype
// Update external type based on field // Update external type based on field
filter.externalType = getSchema(filter)?.externalType filter.externalType = getSchema(filter)?.externalType
} }
const santizeOperator = filter => { const sanitizeOperator = filter => {
// Ensure a valid operator is selected // Ensure a valid operator is selected
const operators = getValidOperatorsForType( const operators = getValidOperatorsForType(filter).map(x => x.value)
filter.type,
filter.field,
datasource
).map(x => x.value)
if (!operators.includes(filter.operator)) { if (!operators.includes(filter.operator)) {
filter.operator = operators[0] ?? OperatorOptions.Equals.value filter.operator = operators[0] ?? OperatorOptions.Equals.value
} }
@ -148,7 +146,7 @@
filter.noValue = noValueOptions.includes(filter.operator) filter.noValue = noValueOptions.includes(filter.operator)
} }
const santizeValue = filter => { const sanitizeValue = (filter, previousType) => {
// Check if the operator allows a value at all // Check if the operator allows a value at all
if (filter.noValue) { if (filter.noValue) {
filter.value = null filter.value = null
@ -162,28 +160,47 @@
} }
} else if (filter.type === "array" && filter.valueType === "Value") { } else if (filter.type === "array" && filter.valueType === "Value") {
filter.value = [] filter.value = []
} else if (
previousType !== filter.type &&
(previousType === FieldType.BB_REFERENCE ||
filter.type === FieldType.BB_REFERENCE)
) {
filter.value = filter.type === "array" ? [] : null
} }
} }
const onFieldChange = filter => { const onFieldChange = filter => {
santizeTypes(filter) const previousType = filter.type
santizeOperator(filter) sanitizeTypes(filter)
santizeValue(filter) sanitizeOperator(filter)
sanitizeValue(filter, previousType)
} }
const onOperatorChange = filter => { const onOperatorChange = filter => {
santizeOperator(filter) sanitizeOperator(filter)
santizeValue(filter) sanitizeValue(filter, filter.type)
} }
const onValueTypeChange = filter => { const onValueTypeChange = filter => {
santizeValue(filter) sanitizeValue(filter)
} }
const getFieldOptions = field => { const getFieldOptions = field => {
const schema = enrichedSchemaFields.find(x => x.name === field) const schema = enrichedSchemaFields.find(x => x.name === field)
return schema?.constraints?.inclusion || [] return schema?.constraints?.inclusion || []
} }
const getValidOperatorsForType = filter => {
if (!filter?.field) {
return []
}
return LuceneUtils.getValidOperatorsForType(
{ type: filter.type, subtype: filter.subtype },
filter.field,
datasource
)
}
</script> </script>
<DrawerContent> <DrawerContent>
@ -228,11 +245,7 @@
/> />
<Select <Select
disabled={!filter.field} disabled={!filter.field}
options={getValidOperatorsForType( options={getValidOperatorsForType(filter)}
filter.type,
filter.field,
datasource
)}
bind:value={filter.operator} bind:value={filter.operator}
on:change={() => onOperatorChange(filter)} on:change={() => onOperatorChange(filter)}
placeholder={null} placeholder={null}
@ -285,6 +298,15 @@
timeOnly={getSchema(filter)?.timeOnly} timeOnly={getSchema(filter)?.timeOnly}
bind:value={filter.value} bind:value={filter.value}
/> />
{:else if filter.type === FieldType.BB_REFERENCE}
<FilterUsers
bind:value={filter.value}
multiselect={[
OperatorOptions.In.value,
OperatorOptions.ContainsAny.value,
].includes(filter.operator)}
disabled={filter.noValue}
/>
{:else} {:else}
<DrawerBindableInput disabled /> <DrawerBindableInput disabled />
{/if} {/if}

View File

@ -0,0 +1,34 @@
<script>
import { Select, Multiselect } from "@budibase/bbui"
import { fetchData } from "@budibase/frontend-core"
import { API } from "api"
export let value = null
export let disabled
export let multiselect = false
$: fetch = fetchData({
API,
datasource: {
type: "user",
},
options: {
limit: 100,
},
})
$: options = $fetch.rows
$: component = multiselect ? Multiselect : Select
</script>
<svelte:component
this={component}
bind:value
autocomplete
{options}
getOptionLabel={option => option.email}
getOptionValue={option => option._id}
{disabled}
/>

View File

@ -18,31 +18,20 @@
</script> </script>
{#each extraFields as { key, displayName, type }} {#each extraFields as { key, displayName, type }}
<div class="config-field"> <Label>{displayName}</Label>
<Label>{displayName}</Label> {#if type === "string"}
{#if type === "string"} <Input
<Input on:change={() => populateExtraQuery(extraQueryFields)}
on:change={() => populateExtraQuery(extraQueryFields)} bind:value={extraQueryFields[key]}
bind:value={extraQueryFields[key]} />
/> {/if}
{/if}
{#if type === "list"} {#if type === "list"}
<Select <Select
on:change={() => populateExtraQuery(extraQueryFields)} on:change={() => populateExtraQuery(extraQueryFields)}
bind:value={extraQueryFields[key]} bind:value={extraQueryFields[key]}
options={config[key].data[query.queryVerb]} options={config[key].data[query.queryVerb]}
getOptionLabel={current => current} getOptionLabel={current => current}
/> />
{/if} {/if}
</div>
{/each} {/each}
<style>
.config-field {
display: grid;
grid-template-columns: 20% 1fr;
grid-gap: var(--spacing-l);
align-items: center;
}
</style>

View File

@ -34,6 +34,7 @@
export let bindings = [] export let bindings = []
export let bindingDrawerLeft export let bindingDrawerLeft
export let allowHelpers = true export let allowHelpers = true
export let customButtonText = null
let fields = Object.entries(object || {}).map(([name, value]) => ({ let fields = Object.entries(object || {}).map(([name, value]) => ({
name, name,
@ -158,9 +159,13 @@
{/if} {/if}
{#if !readOnly && !noAddButton} {#if !readOnly && !noAddButton}
<div> <div>
<ActionButton icon="Add" secondary thin outline on:click={addEntry} <ActionButton icon="Add" secondary thin outline on:click={addEntry}>
>Add{name ? ` ${lowercase(name)}` : ""}</ActionButton {#if customButtonText}
> {customButtonText}
{:else}
{`Add${name ? ` ${lowercase(name)}` : ""}`}
{/if}
</ActionButton>
</div> </div>
{/if} {/if}

View File

@ -1,364 +1,444 @@
<script> <script>
import { goto, beforeUrlChange } from "@roxi/routify" import { goto } from "@roxi/routify"
import { datasources, integrations, queries } from "stores/backend"
import { import {
Icon, Icon,
Select, Select,
Button,
ButtonGroup,
Body,
Label,
Layout,
Input, Input,
Heading, Label,
Tabs,
Tab,
Modal,
ModalContent,
notifications, notifications,
Heading,
Body,
Divider, Divider,
Button,
} from "@budibase/bbui" } from "@budibase/bbui"
import ExtraQueryConfig from "./ExtraQueryConfig.svelte" import { capitalise } from "helpers"
import IntegrationQueryEditor from "components/integration/index.svelte"
import ExternalDataSourceTable from "components/backend/DataTable/ExternalDataSourceTable.svelte"
import BindingBuilder from "components/integration/QueryViewerBindingBuilder.svelte"
import { datasources, integrations, queries } from "stores/backend"
import { capitalise } from "../../helpers"
import CodeMirrorEditor from "components/common/CodeMirrorEditor.svelte"
import JSONPreview from "./JSONPreview.svelte"
import { SchemaTypeOptions } from "constants/backend"
import KeyValueBuilder from "./KeyValueBuilder.svelte"
import { fieldsToSchema, schemaToFields } from "helpers/data/utils"
import AccessLevelSelect from "./AccessLevelSelect.svelte" import AccessLevelSelect from "./AccessLevelSelect.svelte"
import IntegrationQueryEditor from "components/integration/index.svelte"
import QueryViewerSidePanel from "./QueryViewerSidePanel/index.svelte"
import { cloneDeep } from "lodash/fp"
import BindingBuilder from "components/integration/QueryViewerBindingBuilder.svelte"
import CodeMirrorEditor from "components/common/CodeMirrorEditor.svelte"
import { ValidQueryNameRegex } from "@budibase/shared-core" import { ValidQueryNameRegex } from "@budibase/shared-core"
import ExtraQueryConfig from "./ExtraQueryConfig.svelte"
import QueryViewerSavePromptModal from "./QueryViewerSavePromptModal.svelte"
import { Utils } from "@budibase/frontend-core"
export let query export let query
let queryHash
const resumeNavigation = () => { let loading = false
if (typeof navigateTo == "string") { let modified = false
$goto(typeof navigateTo == "string" ? `${navigateTo}` : navigateTo) let scrolling = false
} let showSidePanel = false
let nameError
let newQuery
let datasource
let integration
let schemaType
let autoSchema = {}
let rows = []
const parseQuery = query => {
modified = false
datasource = $datasources.list.find(ds => ds._id === query.datasourceId)
integration = $integrations[datasource.source]
schemaType = integration.query[query.queryVerb].type
newQuery = cloneDeep(query)
// Set the location where the query code will be written to an empty string so that it doesn't
// get changed from undefined -> "" by the input, breaking our unsaved changes checks
newQuery.fields[schemaType] ??= ""
queryHash = JSON.stringify(newQuery)
} }
const transformerDocs = "https://docs.budibase.com/docs/transformers" $: parseQuery(query)
let fields = query?.schema ? schemaToFields(query.schema) : [] const checkIsModified = newQuery => {
let parameters const newQueryHash = JSON.stringify(newQuery)
let data = [] modified = newQueryHash !== queryHash
let saveId
let currentTab = "JSON"
let saveModal
let override = false
let navigateTo = null
let nameError = null
// seed the transformer return modified
if (query && !query.transformer) {
query.transformer = "return data"
} }
// initialise a new empty schema const debouncedCheckIsModified = Utils.debounce(checkIsModified, 1000)
if (query && !query.schema) {
query.schema = {}
}
let queryStr = JSON.stringify(query) $: debouncedCheckIsModified(newQuery)
$beforeUrlChange(event => { async function runQuery({ suppressErrors = true }) {
const updated = JSON.stringify(query)
if (updated !== queryStr && !override) {
navigateTo = event.type == "pushstate" ? event.url : null
saveModal.show()
return false
} else return true
})
$: datasource = $datasources.list.find(ds => ds._id === query.datasourceId)
$: query.schema = fieldsToSchema(fields)
$: datasourceType = datasource?.source
$: integrationInfo = datasourceType ? $integrations[datasourceType] : null
$: queryConfig = integrationInfo?.query
$: shouldShowQueryConfig = queryConfig && query.queryVerb
$: readQuery = query.queryVerb === "read" || query.readable
$: queryInvalid = !query.name || nameError || (readQuery && data.length === 0)
//Cast field in query preview response to number if specified by schema
$: {
for (let i = 0; i < data.length; i++) {
let row = data[i]
for (let fieldName of Object.keys(fields)) {
if (fields[fieldName] === "number" && !isNaN(Number(row[fieldName]))) {
row[fieldName] = Number(row[fieldName])
} else {
row[fieldName] = row[fieldName]?.toString()
}
}
}
}
function resetDependentFields() {
if (query.fields.extra) {
query.fields.extra = {}
}
}
function populateExtraQuery(extraQueryFields) {
query.fields.extra = extraQueryFields
}
async function previewQuery() {
try { try {
const response = await queries.preview(query) showSidePanel = true
loading = true
const response = await queries.preview(newQuery)
if (response.rows.length === 0) { if (response.rows.length === 0) {
notifications.info( notifications.info(
"Query results empty. Please execute a query with results to create your schema." "Query results empty. Please execute a query with results to create your schema."
) )
return return
} }
data = response.rows
// need to merge fields that already exist/might have changed if (Object.keys(newQuery.schema).length === 0) {
if (fields) { // Assign this to a variable instead of directly to the newQuery.schema so that a user
for (let key of Object.keys(response.schema)) { // can change the table they're querying and have the schema update until they first
if (fields[key]) { // edit it
response.schema[key] = fields[key] autoSchema = response.schema
}
}
} }
fields = response.schema
currentTab = "JSON" rows = response.rows
notifications.success("Query executed successfully") notifications.success("Query executed successfully")
} catch (error) { } catch (error) {
notifications.error(`Query Error: ${error.message}`) notifications.error(`Query Error: ${error.message}`)
if (!suppressErrors) {
throw error
}
} finally {
loading = false
} }
} }
// return the query.
async function saveQuery() { async function saveQuery() {
try { try {
const response = await queries.save(query.datasourceId, query) showSidePanel = true
saveId = response._id loading = true
const response = await queries.save(newQuery.datasourceId, {
if (response?._rev) { ...newQuery,
queryStr = JSON.stringify(query) schema:
} Object.keys(newQuery.schema).length === 0
? autoSchema
: newQuery.schema,
})
notifications.success("Query saved successfully") notifications.success("Query saved successfully")
return response return response
} catch (error) { } catch (error) {
notifications.error(error.message || "Error saving query") notifications.error(error.message || "Error saving query")
} finally {
loading = false
} }
} }
function resetDependentFields() {
if (newQuery.fields.extra) {
newQuery.fields.extra = {}
}
}
function populateExtraQuery(extraQueryFields) {
newQuery.fields.extra = extraQueryFields
}
const handleScroll = e => {
scrolling = e.target.scrollTop !== 0
}
</script> </script>
<Modal <QueryViewerSavePromptModal
bind:this={saveModal} checkIsModified={() => checkIsModified(newQuery)}
on:hide={() => { attemptSave={() => runQuery({ suppressErrors: false }).then(saveQuery)}
navigateTo = null />
}} <div class="queryViewer">
> <div class="main">
<ModalContent <div class="header" class:scrolling>
title="You have unsaved changes" <div class="title">
confirmText="Save and Continue"
cancelText="Discard Changes"
size="L"
onConfirm={async () => {
await saveQuery()
override = true
resumeNavigation()
}}
onCancel={async () => {
override = true
resumeNavigation()
}}
>
<Body>Leaving this section will mean losing and changes to your query</Body>
</ModalContent>
</Modal>
<div class="wrapper">
<Layout gap="S" noPadding>
<Heading size="M">Query {integrationInfo?.friendlyName}</Heading>
<Divider />
<Heading size="S">Config</Heading>
<div class="config">
<div class="config-field">
<Label>Query Name</Label>
<Input
value={query.name}
on:input={e => {
let newValue = e.target.value || ""
if (newValue.match(ValidQueryNameRegex)) {
query.name = newValue.trim()
nameError = null
} else {
nameError = "Invalid query name"
}
}}
error={nameError}
/>
</div>
{#if queryConfig}
<div class="config-field">
<Label>Function</Label>
<Select
bind:value={query.queryVerb}
on:change={resetDependentFields}
options={Object.keys(queryConfig)}
getOptionLabel={verb =>
queryConfig[verb]?.displayName || capitalise(verb)}
/>
</div>
<div class="config-field">
<AccessLevelSelect {saveId} {query} label="Access Level" />
</div>
{#if integrationInfo?.extra && query.queryVerb}
<ExtraQueryConfig
{query}
{populateExtraQuery}
config={integrationInfo.extra}
/>
{/if}
{#key query.parameters}
<div class="binding-wrap">
<BindingBuilder
queryBindings={query.parameters}
bindable={false}
on:change={e => {
query.parameters = e.detail.map(binding => {
return {
name: binding.name,
default: binding.value,
}
})
}}
/>
</div>
{/key}
{/if}
</div>
{#if shouldShowQueryConfig}
<Divider />
<div class="config">
<Heading size="S">Fields</Heading>
<Body size="S">Fill in the fields specific to this query.</Body>
<IntegrationQueryEditor
{datasource}
{query}
height={200}
schema={queryConfig[query.queryVerb]}
bind:parameters
/>
<Divider />
</div>
<div class="config">
<div class="help-heading">
<Heading size="S">Transformer</Heading>
<Icon
on:click={() => window.open(transformerDocs)}
hoverable
name="Help"
size="L"
/>
</div>
<Body size="S"> <Body size="S">
Add a JavaScript function to transform the query result. {newQuery.name || "Untitled query"}<span class="unsaved"
>{modified ? "*" : ""}</span
>
</Body> </Body>
<CodeMirrorEditor
height={200}
label="Transformer"
value={query.transformer}
resize="vertical"
on:change={e => (query.transformer = e.detail)}
/>
<Divider />
</div> </div>
<div class="viewer-controls"> <div class="controls">
<Heading size="S">Results</Heading> <Button disabled={loading} on:click={runQuery} overBackground>
<ButtonGroup gap="S"> <Icon size="S" name="Play" />
Run query</Button
>
<div class="tooltip" title="Run your query to enable saving">
<Button <Button
cta
disabled={queryInvalid}
on:click={async () => { on:click={async () => {
await saveQuery() const response = await saveQuery()
// Go to the correct URL if we just created a new query
if (!query._rev) { // When creating a new query the initally passed in query object will have no id.
$goto(`../../${query._id}`) if (response._id && !newQuery._id) {
// Set the comparison query hash to match the new query so that the user doesn't
// get nagged when navigating to the edit view
queryHash = JSON.stringify(newQuery)
$goto(`../../${response._id}`)
} }
}} }}
disabled={loading ||
!newQuery.name ||
nameError ||
rows.length === 0}
overBackground
> >
Save Query <Icon size="S" name="SaveFloppy" />
Save
</Button> </Button>
<Button secondary on:click={previewQuery}>Run Query</Button> </div>
</ButtonGroup>
</div> </div>
<Body size="S"> </div>
Below, you can preview the results from your query and change the
schema. <div class="body" on:scroll={handleScroll}>
</Body> <div class="bodyInner">
<section class="viewer"> <div class="configField">
{#if data} <Label>Name</Label>
<Tabs bind:selected={currentTab}> <Input
<Tab title="JSON"> value={newQuery.name}
<JSONPreview data={data[0]} minHeight="120" /> on:input={e => {
</Tab> let newValue = e.target.value || ""
<Tab title="Schema"> if (newValue.match(ValidQueryNameRegex)) {
<KeyValueBuilder newQuery.name = newValue.trim()
bind:object={fields} nameError = null
name="field" } else {
headings nameError = "Invalid query name"
options={SchemaTypeOptions} }
}}
error={nameError}
/>
{#if integration.query}
<Label>Function</Label>
<Select
bind:value={newQuery.queryVerb}
on:change={resetDependentFields}
options={Object.keys(integration.query)}
getOptionLabel={verb =>
integration.query[verb]?.displayName || capitalise(verb)}
/>
<Label>Access</Label>
<AccessLevelSelect query={newQuery} />
{#if integration?.extra && newQuery.queryVerb}
<ExtraQueryConfig
query={newQuery}
{populateExtraQuery}
config={integration.extra}
/> />
</Tab> {/if}
<Tab title="Preview"> {/if}
<ExternalDataSourceTable {query} {data} /> </div>
</Tab>
</Tabs> <Divider />
{/if}
</section> <div class="heading">
{/if} <Heading weight="L" size="XS">Query</Heading>
</Layout> </div>
<div class="copy">
<Body size="S">
{#if schemaType === "sql"}
Add some SQL to query your data
{:else if schemaType === "json"}
Add some JSON to query your data
{:else if schemaType === "fields"}
Add some fields to query your data
{:else}
Enter your query below
{/if}
</Body>
</div>
<IntegrationQueryEditor
noLabel
{datasource}
bind:query={newQuery}
height={200}
schema={integration.query[newQuery.queryVerb]}
/>
<Divider />
<div class="heading">
<Heading weight="L" size="XS">Bindings</Heading>
</div>
<div class="copy">
<Body size="S">
Bindings come in two parts: the binding name, and a default/fallback
value. These bindings can be used as Handlebars expressions
throughout the query.
</Body>
</div>
{#key newQuery.parameters}
<BindingBuilder
hideHeading
queryBindings={newQuery.parameters}
on:change={e => {
newQuery.parameters = e.detail.map(binding => {
return {
name: binding.name,
default: binding.value,
}
})
}}
/>
{/key}
<Divider />
<div class="heading">
<Heading weight="L" size="XS">Transformer</Heading>
</div>
<div class="copy">
<Body size="S">
Add a JavaScript function to transform the query result.
</Body>
</div>
<CodeMirrorEditor
height={200}
value={newQuery.transformer}
resize="vertical"
on:change={e => (newQuery.transformer = e.detail)}
/>
</div>
</div>
</div>
<div class:showSidePanel class="sidePanel">
<QueryViewerSidePanel
onClose={() => (showSidePanel = false)}
onSchemaChange={newSchema => {
newQuery.schema = newSchema
}}
{rows}
schema={Object.keys(newQuery.schema).length === 0
? autoSchema
: newQuery.schema}
/>
</div>
</div> </div>
<style> <style>
.wrapper { .unsaved {
width: 640px; color: var(--grey-5);
font-style: italic;
}
.queryViewer {
height: 100%;
margin: -28px -40px -40px -40px;
display: flex;
flex: 1;
}
.queryViewer :global(.spectrum-Divider) {
margin: 35px 0;
}
.main {
flex-grow: 1;
height: 100%;
display: flex;
flex-direction: column;
}
.header {
align-items: center;
padding: 8px 10px 8px 16px;
display: flex;
border-bottom: 2px solid transparent;
transition: border-bottom 130ms ease-out, background 130ms ease-out;
}
.header.scrolling {
border-bottom: var(--border-light);
background: var(--background);
}
.body {
flex-grow: 1;
overflow-y: scroll;
padding: 23px 23px 80px;
box-sizing: border-box;
}
.bodyInner {
max-width: 520px;
margin: auto; margin: auto;
} }
.config { .title {
display: grid; /* width 0 paired with flex-grow necessary here for the truncation to work properly*/
grid-gap: var(--spacing-s); width: 0;
z-index: 1; flex-grow: 1;
} }
.config-field { .title :global(p) {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.controls {
flex-shrink: 0;
}
.tooltip {
display: inline-block;
}
.controls :global(button) {
border: none;
color: var(--grey-7);
font-weight: 300;
}
.controls :global(button):hover {
background-color: transparent;
color: var(--ink);
}
.controls :global(.is-disabled) {
pointer-events: none;
background-color: transparent;
color: var(--grey-3);
}
.controls :global(span) {
display: flex;
align-items: center;
}
.controls :global(.icon) {
margin-right: 8px;
}
.configField {
display: grid; display: grid;
grid-template-columns: 20% 1fr; grid-template-columns: 20% 1fr;
grid-gap: var(--spacing-l); grid-gap: var(--spacing-l);
align-items: center; align-items: center;
} }
.help-heading { .configField :global(label) {
display: flex; color: var(--grey-6);
justify-content: space-between;
} }
.viewer { .heading {
min-height: 200px; margin-bottom: 8px;
width: 640px;
} }
.viewer-controls { .copy {
display: flex; margin-bottom: 14px;
flex-direction: row;
justify-content: space-between;
gap: var(--spacing-m);
min-width: 150px;
align-items: center;
} }
.binding-wrap :global(div.container) { .copy :global(p) {
padding-left: 0px; color: var(--grey-7);
padding-right: 0px; }
.sidePanel {
flex-shrink: 0;
height: 100%;
width: 0;
overflow: hidden;
transition: width 150ms;
}
.sidePanel :global(.panel) {
height: 100%;
}
.showSidePanel {
width: 450px;
} }
</style> </style>

View File

@ -1,9 +1,9 @@
<script> <script>
import { Body, Heading, Layout } from "@budibase/bbui"
import KeyValueBuilder from "components/integration/KeyValueBuilder.svelte" import KeyValueBuilder from "components/integration/KeyValueBuilder.svelte"
import { getUserBindings } from "builderStore/dataBinding" import { getUserBindings } from "builderStore/dataBinding"
export let bindable = true export let bindable = true
export let queryBindings = [] export let queryBindings = []
export let hideHeading = false
const userBindings = getUserBindings() const userBindings = getUserBindings()
@ -13,44 +13,16 @@
}, {}) }, {})
</script> </script>
<Layout noPadding={bindable} gap="S"> <KeyValueBuilder
<div class="controls" class:height={!bindable}> bind:object={internalBindings}
<Heading size="XS">Bindings</Heading> tooltip="Set the name of the binding which can be used in Handlebars statements throughout your query"
</div> name="binding"
<Body size="S"> customButtonText="Bindings"
{#if !bindable} headings
Bindings come in two parts: the binding name, and a default/fallback keyPlaceholder="Binding name"
value. These bindings can be used as Handlebars expressions throughout the valuePlaceholder="Default"
query. bindings={[...userBindings]}
{:else} bindingDrawerLeft="260px"
Enter a value for each binding. The default values will be used for any allowHelpers={false}
values left blank. on:change
{/if} />
</Body>
<div class="bindings" class:bindable>
<KeyValueBuilder
bind:object={internalBindings}
tooltip="Set the name of the binding which can be used in Handlebars statements throughout your query"
name="binding"
headings
keyPlaceholder="Binding name"
valuePlaceholder="Default"
bindings={[...userBindings]}
bindingDrawerLeft="260px"
allowHelpers={false}
on:change
/>
</div>
</Layout>
<style>
.controls {
display: flex;
align-items: center;
justify-content: space-between;
}
.height {
height: 40px;
}
</style>

View File

@ -0,0 +1,53 @@
<script>
import { goto, beforeUrlChange } from "@roxi/routify"
import { Body, Modal, ModalContent } from "@budibase/bbui"
export let checkIsModified = () => {}
export let attemptSave = () => {}
let modal
let navigateTo
let override = false
$beforeUrlChange(event => {
if (checkIsModified() && !override) {
navigateTo = event.type == "pushstate" ? event.url : null
modal.show()
return false
} else return true
})
const resumeNavigation = () => {
if (typeof navigateTo == "string") {
$goto(typeof navigateTo == "string" ? `${navigateTo}` : navigateTo)
}
}
</script>
<Modal
bind:this={modal}
on:hide={() => {
navigateTo = null
}}
>
<ModalContent
title="You have unsaved changes"
confirmText="Save and Continue"
cancelText="Discard Changes"
size="L"
onConfirm={async () => {
try {
await attemptSave()
override = true
resumeNavigation()
} catch (e) {
navigateTo = false
}
}}
onCancel={async () => {
override = true
resumeNavigation()
}}
>
<Body>Leaving this section will mean losing any changes to your query</Body>
</ModalContent>
</Modal>

View File

@ -0,0 +1,22 @@
<script>
export let data
$: string = JSON.stringify(data || {}, null, 2)
</script>
<textarea class="json" disabled value={string} />
<style>
.json {
width: 100%;
height: 100%;
color: var(--ink);
padding: 12px;
box-sizing: border-box;
resize: none;
background-color: var(
--spectrum-textfield-m-background-color,
var(--spectrum-global-color-gray-50)
);
}
</style>

View File

@ -0,0 +1,33 @@
<script>
import Table from "components/backend/DataTable/Table.svelte"
import { cloneDeep } from "lodash/fp"
export let schema = {}
export let rows = []
$: rowsCopy = cloneDeep(rows)
// Cast field in query preview response to number if specified by schema
$: {
for (let i = 0; i < rowsCopy.length; i++) {
let row = rowsCopy[i]
for (let fieldName of Object.keys(schema)) {
if (schema[fieldName] === "number" && !isNaN(Number(row[fieldName]))) {
row[fieldName] = Number(row[fieldName])
} else {
row[fieldName] = row[fieldName]?.toString()
}
}
}
}
</script>
<div class="table">
<Table {schema} data={rowsCopy} type="external" allowEditing={false} />
</div>
<style>
.table :global(.spectrum-Table-cell) {
min-width: 100px;
}
</style>

View File

@ -0,0 +1,29 @@
<script>
import KeyValueBuilder from "../KeyValueBuilder.svelte"
import { SchemaTypeOptions } from "constants/backend"
export let schema
export let onSchemaChange = () => {}
const handleChange = e => {
let newSchema = {}
// KeyValueBuilder on change event returns an array of objects with each object
// containing a field
e.detail.forEach(({ name, value }) => {
newSchema[name] = value
})
onSchemaChange(newSchema)
}
</script>
{#key schema}
<KeyValueBuilder
on:change={handleChange}
object={schema}
name="field"
headings
options={SchemaTypeOptions}
/>
{/key}

View File

@ -0,0 +1,66 @@
<script>
import Panel from "components/design/Panel.svelte"
import { ActionButton } from "@budibase/bbui"
import JSONPanel from "./JSONPanel.svelte"
import SchemaPanel from "./SchemaPanel.svelte"
import PreviewPanel from "./PreviewPanel.svelte"
export let rows
export let schema
export let onSchemaChange = () => {}
export let onClose = () => {}
const tabs = ["JSON", "Schema", "Preview"]
let activeTab = "JSON"
</script>
<Panel
showCloseButton
closeButtonIcon="RailRightClose"
onClickCloseButton={onClose}
title="Query results"
icon={"SQLQuery"}
borderLeft
extraWide
>
<div slot="panel-header-content">
<div class="settings-tabs">
{#each tabs as tab}
<ActionButton
size="M"
quiet
selected={activeTab === tab}
on:click={() => {
activeTab = tab
}}
>
{tab}
</ActionButton>
{/each}
</div>
</div>
<div class="content">
{#if activeTab === "JSON"}
<JSONPanel data={rows[0] || {}} />
{:else if activeTab === "Schema"}
<SchemaPanel {onSchemaChange} {schema} />
{:else}
<PreviewPanel {schema} {rows} />
{/if}
</div>
</Panel>
<style>
.settings-tabs {
display: flex;
gap: var(--spacing-s);
padding: 0 var(--spacing-l);
padding-bottom: var(--spacing-l);
}
.content {
padding: 14px;
height: 100%;
overflow: scroll;
}
</style>

View File

@ -23,6 +23,7 @@
export let schema export let schema
export let editable = true export let editable = true
export let height = 500 export let height = 500
export let noLabel = false
let stepEditors = [] let stepEditors = []
@ -75,7 +76,7 @@
{#if schema.type === QueryTypes.SQL} {#if schema.type === QueryTypes.SQL}
<Editor <Editor
editorHeight={height} editorHeight={height}
label="Query" label={noLabel ? null : "Query"}
mode="sql" mode="sql"
on:change={updateQuery} on:change={updateQuery}
readOnly={!editable} readOnly={!editable}
@ -85,7 +86,7 @@
{:else if shouldDisplayJsonBox} {:else if shouldDisplayJsonBox}
<Editor <Editor
editorHeight={height} editorHeight={height}
label="Query" label={noLabel ? null : "Query"}
mode="json" mode="json"
on:change={updateQuery} on:change={updateQuery}
readOnly={!editable} readOnly={!editable}

View File

@ -0,0 +1,70 @@
<script>
import {
ModalContent,
Toggle,
Input,
Layout,
Dropzone,
notifications,
Body,
} from "@budibase/bbui"
import { API } from "api"
import { automationStore, store } from "../../builderStore"
export let app
$: disabled = (encrypted && !password) || !file
let encrypted = false,
password
let file
async function updateApp() {
try {
let data = new FormData()
data.append("appExport", file)
if (encrypted) {
data.append("encryptionPassword", password.trim())
}
const appId = app.devId
await API.updateAppFromExport(appId, data)
const pkg = await API.fetchAppPackage(appId)
await store.actions.initialise(pkg)
await automationStore.actions.fetch()
notifications.success("App updated successfully")
} catch (err) {
notifications.error(`Failed to update app - ${err.message || err}`)
}
}
</script>
<ModalContent
title={`Update ${app.name}`}
confirmText="Update"
onConfirm={updateApp}
bind:disabled
>
<Body size="S"
>Updating an app using an app export will replace all tables, datasources,
queries, screens and automations. It is recommended to perform a backup
before running this operation.</Body
>
<Layout noPadding gap="XS">
<Dropzone
gallery={false}
label="App export"
on:change={e => {
file = e.detail?.[0]
encrypted = file?.name?.endsWith(".enc.tar.gz")
}}
/>
<Toggle text="Encrypted" bind:value={encrypted} />
{#if encrypted}
<Input
type="password"
label="Password"
placeholder="Type here..."
bind:value={password}
/>
{/if}
</Layout>
</ModalContent>

View File

@ -1,126 +1,4 @@
export const FIELDS = { import { FieldType, FieldSubtype } from "@budibase/types"
STRING: {
name: "Text",
type: "string",
icon: "Text",
constraints: {
type: "string",
length: {},
presence: false,
},
},
BARCODEQR: {
name: "Barcode/QR",
type: "barcodeqr",
icon: "Camera",
constraints: {
type: "string",
length: {},
presence: false,
},
},
LONGFORM: {
name: "Long Form Text",
type: "longform",
icon: "TextAlignLeft",
constraints: {
type: "string",
length: {},
presence: false,
},
},
OPTIONS: {
name: "Options",
type: "options",
icon: "Dropdown",
constraints: {
type: "string",
presence: false,
inclusion: [],
},
},
ARRAY: {
name: "Multi-select",
type: "array",
icon: "Duplicate",
constraints: {
type: "array",
presence: false,
inclusion: [],
},
},
NUMBER: {
name: "Number",
type: "number",
icon: "123",
constraints: {
type: "number",
presence: false,
numericality: { greaterThanOrEqualTo: "", lessThanOrEqualTo: "" },
},
},
BIGINT: {
name: "BigInt",
type: "bigint",
icon: "TagBold",
},
BOOLEAN: {
name: "Boolean",
type: "boolean",
icon: "Boolean",
constraints: {
type: "boolean",
presence: false,
},
},
DATETIME: {
name: "Date/Time",
type: "datetime",
icon: "Calendar",
constraints: {
type: "string",
length: {},
presence: false,
datetime: {
latest: "",
earliest: "",
},
},
},
ATTACHMENT: {
name: "Attachment",
type: "attachment",
icon: "Folder",
constraints: {
type: "array",
presence: false,
},
},
LINK: {
name: "Relationship",
type: "link",
icon: "Link",
constraints: {
type: "array",
presence: false,
},
},
FORMULA: {
name: "Formula",
type: "formula",
icon: "Calculator",
constraints: {},
},
JSON: {
name: "JSON",
type: "json",
icon: "Brackets",
constraints: {
type: "object",
presence: false,
},
},
}
export const AUTO_COLUMN_SUB_TYPES = { export const AUTO_COLUMN_SUB_TYPES = {
AUTO_ID: "autoID", AUTO_ID: "autoID",
@ -138,6 +16,151 @@ export const AUTO_COLUMN_DISPLAY_NAMES = {
UPDATED_AT: "Updated At", UPDATED_AT: "Updated At",
} }
export const FIELDS = {
STRING: {
name: "Text",
type: FieldType.STRING,
icon: "Text",
constraints: {
type: "string",
length: {},
presence: false,
},
},
BARCODEQR: {
name: "Barcode/QR",
type: FieldType.BARCODEQR,
icon: "Camera",
constraints: {
type: "string",
length: {},
presence: false,
},
},
LONGFORM: {
name: "Long Form Text",
type: FieldType.LONGFORM,
icon: "TextAlignLeft",
constraints: {
type: "string",
length: {},
presence: false,
},
},
OPTIONS: {
name: "Options",
type: FieldType.OPTIONS,
icon: "Dropdown",
constraints: {
type: "string",
presence: false,
inclusion: [],
},
},
ARRAY: {
name: "Multi-select",
type: FieldType.ARRAY,
icon: "Duplicate",
constraints: {
type: "array",
presence: false,
inclusion: [],
},
},
NUMBER: {
name: "Number",
type: FieldType.NUMBER,
icon: "123",
constraints: {
type: "number",
presence: false,
numericality: { greaterThanOrEqualTo: "", lessThanOrEqualTo: "" },
},
},
BIGINT: {
name: "BigInt",
type: FieldType.BIGINT,
icon: "TagBold",
},
BOOLEAN: {
name: "Boolean",
type: FieldType.BOOLEAN,
icon: "Boolean",
constraints: {
type: "boolean",
presence: false,
},
},
DATETIME: {
name: "Date/Time",
type: FieldType.DATETIME,
icon: "Calendar",
constraints: {
type: "string",
length: {},
presence: false,
datetime: {
latest: "",
earliest: "",
},
},
},
ATTACHMENT: {
name: "Attachment",
type: FieldType.ATTACHMENT,
icon: "Folder",
constraints: {
type: "array",
presence: false,
},
},
LINK: {
name: "Relationship",
type: FieldType.LINK,
icon: "Link",
constraints: {
type: "array",
presence: false,
},
},
AUTO: {
name: "Auto Column",
type: FieldType.AUTO,
icon: "MagicWand",
constraints: {},
},
FORMULA: {
name: "Formula",
type: FieldType.FORMULA,
icon: "Calculator",
constraints: {},
},
JSON: {
name: "JSON",
type: FieldType.JSON,
icon: "Brackets",
constraints: {
type: "object",
presence: false,
},
},
USER: {
name: "User",
type: FieldType.BB_REFERENCE,
subtype: FieldSubtype.USER,
icon: "User",
},
USERS: {
name: "Users",
type: FieldType.BB_REFERENCE,
subtype: FieldSubtype.USERS,
icon: "User",
constraints: {
type: "array",
},
},
}
export const FILE_TYPES = { export const FILE_TYPES = {
IMAGE: ["png", "tiff", "gif", "raw", "jpg", "jpeg"], IMAGE: ["png", "tiff", "gif", "raw", "jpg", "jpeg"],
CODE: ["js", "rs", "py", "java", "rb", "hs", "yml"], CODE: ["js", "rs", "py", "java", "rb", "hs", "yml"],
@ -170,6 +193,11 @@ export const RelationshipType = {
MANY_TO_ONE: "many-to-one", MANY_TO_ONE: "many-to-one",
} }
export const PrettyRelationshipDefinitions = {
MANY: "Many rows",
ONE: "One row",
}
export const ALLOWABLE_STRING_OPTIONS = [ export const ALLOWABLE_STRING_OPTIONS = [
FIELDS.STRING, FIELDS.STRING,
FIELDS.OPTIONS, FIELDS.OPTIONS,

View File

@ -3,16 +3,17 @@
* e.g. * e.g.
* name all names result * name all names result
* ------ ----------- -------- * ------ ----------- --------
* ("foo") ["foo"] "foo (1)" * ("foo") ["foo"] "foo 1"
* ("foo") ["foo", "foo (1)"] "foo (2)" * ("foo") ["foo", "foo 1"] "foo 2"
* ("foo (1)") ["foo", "foo (1)"] "foo (2)" * ("foo 1") ["foo", "foo 1"] "foo 2"
* ("foo") ["foo", "foo (2)"] "foo (1)" * ("foo") ["foo", "foo 2"] "foo 1"
* *
* Repl * Repl
*/ */
export const duplicateName = (name, allNames) => { export const duplicateName = (name, allNames) => {
const baseName = name.split(" (")[0] const duplicatePattern = new RegExp(`\\s(\\d+)$`)
const isDuplicate = new RegExp(`${baseName}\\s\\((\\d+)\\)$`) const baseName = name.split(duplicatePattern)[0]
const isDuplicate = new RegExp(`${baseName}\\s(\\d+)$`)
// get the sequence from matched names // get the sequence from matched names
const sequence = [] const sequence = []
@ -28,7 +29,6 @@ export const duplicateName = (name, allNames) => {
return false return false
}) })
sequence.sort((a, b) => a - b) sequence.sort((a, b) => a - b)
// get the next number in the sequence // get the next number in the sequence
let number let number
if (sequence.length === 0) { if (sequence.length === 0) {
@ -46,5 +46,5 @@ export const duplicateName = (name, allNames) => {
} }
} }
return `${baseName} (${number})` return `${baseName} ${number}`
} }

View File

@ -9,34 +9,34 @@ describe("duplicate", () => {
const duplicate = duplicateName(name, names) const duplicate = duplicateName(name, names)
expect(duplicate).toBe("foo (1)") expect(duplicate).toBe("foo 1")
}) })
it("with multiple existing", async () => { it("with multiple existing", async () => {
const names = ["foo", "foo (1)", "foo (2)"] const names = ["foo", "foo 1", "foo 2"]
const name = "foo" const name = "foo"
const duplicate = duplicateName(name, names) const duplicate = duplicateName(name, names)
expect(duplicate).toBe("foo (3)") expect(duplicate).toBe("foo 3")
}) })
it("with mixed multiple existing", async () => { it("with mixed multiple existing", async () => {
const names = ["foo", "foo (1)", "foo (2)", "bar", "bar (1)", "bar (2)"] const names = ["foo", "foo 1", "foo 2", "bar", "bar 1", "bar 2"]
const name = "foo" const name = "foo"
const duplicate = duplicateName(name, names) const duplicate = duplicateName(name, names)
expect(duplicate).toBe("foo (3)") expect(duplicate).toBe("foo 3")
}) })
it("with incomplete sequence", async () => { it("with incomplete sequence", async () => {
const names = ["foo", "foo (2)", "foo (3)"] const names = ["foo", "foo 2", "foo 3"]
const name = "foo" const name = "foo"
const duplicate = duplicateName(name, names) const duplicate = duplicateName(name, names)
expect(duplicate).toBe("foo (1)") expect(duplicate).toBe("foo 1")
}) })
}) })
}) })

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