Merge branch 'develop' into account-portal-auth-api-testing-2
This commit is contained in:
commit
178b807573
|
@ -18,6 +18,8 @@ env:
|
||||||
BRANCH: ${{ github.event.pull_request.head.ref }}
|
BRANCH: ${{ github.event.pull_request.head.ref }}
|
||||||
BASE_BRANCH: ${{ github.event.pull_request.base.ref}}
|
BASE_BRANCH: ${{ github.event.pull_request.base.ref}}
|
||||||
PERSONAL_ACCESS_TOKEN: ${{ secrets.PERSONAL_ACCESS_TOKEN }}
|
PERSONAL_ACCESS_TOKEN: ${{ secrets.PERSONAL_ACCESS_TOKEN }}
|
||||||
|
NX_BASE_BRANCH: origin/${{ github.base_ref }}
|
||||||
|
USE_NX_AFFECTED: ${{ github.event_name == 'pull_request' && github.base_ref != 'master'}}
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
lint:
|
lint:
|
||||||
|
@ -25,20 +27,20 @@ jobs:
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repo and submodules
|
- name: Checkout repo and submodules
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v3
|
||||||
if: github.repository == github.event.pull_request.head.repo.full_name
|
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'Budibase/budibase'
|
||||||
with:
|
with:
|
||||||
submodules: true
|
submodules: true
|
||||||
token: ${{ secrets.PERSONAL_ACCESS_TOKEN || github.token }}
|
token: ${{ secrets.PERSONAL_ACCESS_TOKEN || github.token }}
|
||||||
- name: Checkout repo only
|
- name: Checkout repo only
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v3
|
||||||
if: github.repository != github.event.pull_request.head.repo.full_name
|
if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name != 'Budibase/budibase'
|
||||||
|
|
||||||
- name: Use Node.js 14.x
|
- name: Use Node.js 18.x
|
||||||
uses: actions/setup-node@v3
|
uses: actions/setup-node@v3
|
||||||
with:
|
with:
|
||||||
node-version: 14.x
|
node-version: 18.x
|
||||||
cache: "yarn"
|
cache: "yarn"
|
||||||
- run: yarn
|
- run: yarn --frozen-lockfile
|
||||||
- run: yarn lint
|
- run: yarn lint
|
||||||
|
|
||||||
build:
|
build:
|
||||||
|
@ -46,45 +48,66 @@ jobs:
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repo and submodules
|
- name: Checkout repo and submodules
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v3
|
||||||
if: github.repository == github.event.pull_request.head.repo.full_name
|
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'Budibase/budibase'
|
||||||
with:
|
with:
|
||||||
submodules: true
|
submodules: true
|
||||||
token: ${{ secrets.PERSONAL_ACCESS_TOKEN || github.token }}
|
token: ${{ secrets.PERSONAL_ACCESS_TOKEN || github.token }}
|
||||||
|
fetch-depth: 0
|
||||||
- name: Checkout repo only
|
- name: Checkout repo only
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v3
|
||||||
if: github.repository != github.event.pull_request.head.repo.full_name
|
if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name != 'Budibase/budibase'
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
- name: Use Node.js 14.x
|
- name: Use Node.js 18.x
|
||||||
uses: actions/setup-node@v3
|
uses: actions/setup-node@v3
|
||||||
with:
|
with:
|
||||||
node-version: 14.x
|
node-version: 18.x
|
||||||
cache: "yarn"
|
cache: "yarn"
|
||||||
- run: yarn
|
- run: yarn --frozen-lockfile
|
||||||
|
|
||||||
# Run build all the projects
|
# Run build all the projects
|
||||||
- run: yarn build
|
- name: Build
|
||||||
|
run: |
|
||||||
|
yarn build
|
||||||
# Check the types of the projects built via esbuild
|
# Check the types of the projects built via esbuild
|
||||||
- run: yarn check:types
|
- name: Check types
|
||||||
|
run: |
|
||||||
|
if ${{ env.USE_NX_AFFECTED }}; then
|
||||||
|
yarn check:types --since=${{ env.NX_BASE_BRANCH }}
|
||||||
|
else
|
||||||
|
yarn check:types
|
||||||
|
fi
|
||||||
|
|
||||||
test-libraries:
|
test-libraries:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repo and submodules
|
- name: Checkout repo and submodules
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v3
|
||||||
if: github.repository == github.event.pull_request.head.repo.full_name
|
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'Budibase/budibase'
|
||||||
with:
|
with:
|
||||||
submodules: true
|
submodules: true
|
||||||
token: ${{ secrets.PERSONAL_ACCESS_TOKEN || github.token }}
|
token: ${{ secrets.PERSONAL_ACCESS_TOKEN || github.token }}
|
||||||
|
fetch-depth: 0
|
||||||
- name: Checkout repo only
|
- name: Checkout repo only
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v3
|
||||||
if: github.repository != github.event.pull_request.head.repo.full_name
|
if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name != 'Budibase/budibase'
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
- name: Use Node.js 14.x
|
- name: Use Node.js 18.x
|
||||||
uses: actions/setup-node@v3
|
uses: actions/setup-node@v3
|
||||||
with:
|
with:
|
||||||
node-version: 14.x
|
node-version: 18.x
|
||||||
cache: "yarn"
|
cache: "yarn"
|
||||||
- run: yarn
|
- run: yarn --frozen-lockfile
|
||||||
- run: yarn test --ignore=@budibase/worker --ignore=@budibase/server --ignore=@budibase/pro
|
- name: Test
|
||||||
|
run: |
|
||||||
|
if ${{ env.USE_NX_AFFECTED }}; then
|
||||||
|
yarn test --ignore=@budibase/worker --ignore=@budibase/server --ignore=@budibase/pro --since=${{ env.NX_BASE_BRANCH }}
|
||||||
|
else
|
||||||
|
yarn test --ignore=@budibase/worker --ignore=@budibase/server --ignore=@budibase/pro
|
||||||
|
fi
|
||||||
- uses: codecov/codecov-action@v3
|
- uses: codecov/codecov-action@v3
|
||||||
with:
|
with:
|
||||||
token: ${{ secrets.CODECOV_TOKEN }} # not required for public repos
|
token: ${{ secrets.CODECOV_TOKEN }} # not required for public repos
|
||||||
|
@ -96,21 +119,31 @@ jobs:
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repo and submodules
|
- name: Checkout repo and submodules
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v3
|
||||||
if: github.repository == github.event.pull_request.head.repo.full_name
|
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'Budibase/budibase'
|
||||||
with:
|
with:
|
||||||
submodules: true
|
submodules: true
|
||||||
token: ${{ secrets.PERSONAL_ACCESS_TOKEN || github.token }}
|
token: ${{ secrets.PERSONAL_ACCESS_TOKEN || github.token }}
|
||||||
|
fetch-depth: 0
|
||||||
- name: Checkout repo only
|
- name: Checkout repo only
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v3
|
||||||
if: github.repository != github.event.pull_request.head.repo.full_name
|
if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name != 'Budibase/budibase'
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
- name: Use Node.js 14.x
|
- name: Use Node.js 18.x
|
||||||
uses: actions/setup-node@v3
|
uses: actions/setup-node@v3
|
||||||
with:
|
with:
|
||||||
node-version: 14.x
|
node-version: 18.x
|
||||||
cache: "yarn"
|
cache: "yarn"
|
||||||
- run: yarn
|
- run: yarn --frozen-lockfile
|
||||||
- run: yarn test --scope=@budibase/worker --scope=@budibase/server
|
- name: Test worker and server
|
||||||
|
run: |
|
||||||
|
if ${{ env.USE_NX_AFFECTED }}; then
|
||||||
|
yarn test --scope=@budibase/worker --scope=@budibase/server --since=${{ env.NX_BASE_BRANCH }}
|
||||||
|
else
|
||||||
|
yarn test --scope=@budibase/worker --scope=@budibase/server
|
||||||
|
fi
|
||||||
|
|
||||||
- uses: codecov/codecov-action@v3
|
- uses: codecov/codecov-action@v3
|
||||||
with:
|
with:
|
||||||
token: ${{ secrets.CODECOV_TOKEN || github.token }} # not required for public repos
|
token: ${{ secrets.CODECOV_TOKEN || github.token }} # not required for public repos
|
||||||
|
@ -119,42 +152,50 @@ jobs:
|
||||||
|
|
||||||
test-pro:
|
test-pro:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
if: github.repository == github.event.pull_request.head.repo.full_name
|
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'Budibase/budibase'
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repo and submodules
|
- name: Checkout repo and submodules
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v3
|
||||||
with:
|
with:
|
||||||
submodules: true
|
submodules: true
|
||||||
token: ${{ secrets.PERSONAL_ACCESS_TOKEN || github.token }}
|
token: ${{ secrets.PERSONAL_ACCESS_TOKEN || github.token }}
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
- name: Use Node.js 14.x
|
- name: Use Node.js 18.x
|
||||||
uses: actions/setup-node@v3
|
uses: actions/setup-node@v3
|
||||||
with:
|
with:
|
||||||
node-version: 14.x
|
node-version: 18.x
|
||||||
cache: "yarn"
|
cache: "yarn"
|
||||||
- run: yarn
|
- run: yarn --frozen-lockfile
|
||||||
- run: yarn test --scope=@budibase/pro
|
- name: Test
|
||||||
|
run: |
|
||||||
|
if ${{ env.USE_NX_AFFECTED }}; then
|
||||||
|
yarn test --scope=@budibase/pro --since=${{ env.NX_BASE_BRANCH }}
|
||||||
|
else
|
||||||
|
yarn test --scope=@budibase/pro
|
||||||
|
fi
|
||||||
|
|
||||||
integration-test:
|
integration-test:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repo and submodules
|
- name: Checkout repo and submodules
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v3
|
||||||
if: github.repository == github.event.pull_request.head.repo.full_name
|
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'Budibase/budibase'
|
||||||
with:
|
with:
|
||||||
submodules: true
|
submodules: true
|
||||||
token: ${{ secrets.PERSONAL_ACCESS_TOKEN || github.token }}
|
token: ${{ secrets.PERSONAL_ACCESS_TOKEN || github.token }}
|
||||||
- name: Checkout repo only
|
- name: Checkout repo only
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v3
|
||||||
if: github.repository != github.event.pull_request.head.repo.full_name
|
if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name != 'Budibase/budibase'
|
||||||
|
|
||||||
- name: Use Node.js 14.x
|
- name: Use Node.js 18.x
|
||||||
uses: actions/setup-node@v3
|
uses: actions/setup-node@v3
|
||||||
with:
|
with:
|
||||||
node-version: 14.x
|
node-version: 18.x
|
||||||
cache: "yarn"
|
cache: "yarn"
|
||||||
- run: yarn
|
- run: yarn --frozen-lockfile
|
||||||
- run: yarn build --projects=@budibase/server,@budibase/worker,@budibase/client
|
- name: Build packages
|
||||||
|
run: yarn build --scope @budibase/server --scope @budibase/worker --scope @budibase/client --scope @budibase/backend-core
|
||||||
- name: Run tests
|
- name: Run tests
|
||||||
run: |
|
run: |
|
||||||
cd qa-core
|
cd qa-core
|
||||||
|
@ -166,13 +207,12 @@ jobs:
|
||||||
|
|
||||||
check-pro-submodule:
|
check-pro-submodule:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
if: github.repository == github.event.pull_request.head.repo.full_name
|
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'Budibase/budibase'
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repo and submodules
|
- name: Checkout repo and submodules
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v3
|
||||||
with:
|
with:
|
||||||
submodules: true
|
submodules: true
|
||||||
fetch-depth: 0
|
|
||||||
token: ${{ secrets.PERSONAL_ACCESS_TOKEN || github.token }}
|
token: ${{ secrets.PERSONAL_ACCESS_TOKEN || github.token }}
|
||||||
|
|
||||||
- name: Check pro commit
|
- name: Check pro commit
|
||||||
|
@ -190,6 +230,8 @@ jobs:
|
||||||
base_commit=$(git rev-parse origin/develop)
|
base_commit=$(git rev-parse origin/develop)
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
echo "target_branch=$branch"
|
||||||
|
echo "target_branch=$branch" >> "$GITHUB_OUTPUT"
|
||||||
echo "pro_commit=$pro_commit"
|
echo "pro_commit=$pro_commit"
|
||||||
echo "pro_commit=$pro_commit" >> "$GITHUB_OUTPUT"
|
echo "pro_commit=$pro_commit" >> "$GITHUB_OUTPUT"
|
||||||
echo "base_commit=$base_commit"
|
echo "base_commit=$base_commit"
|
||||||
|
@ -204,7 +246,7 @@ jobs:
|
||||||
const baseCommit = '${{ steps.get_pro_commits.outputs.base_commit }}';
|
const baseCommit = '${{ steps.get_pro_commits.outputs.base_commit }}';
|
||||||
|
|
||||||
if (submoduleCommit !== baseCommit) {
|
if (submoduleCommit !== baseCommit) {
|
||||||
console.error('Submodule commit does not match the latest commit on the develop branch.');
|
console.error('Submodule commit does not match the latest commit on the "${{ steps.get_pro_commits.outputs.target_branch }}"" branch.');
|
||||||
console.error('Refer to the pro repo to merge your changes: https://github.com/Budibase/budibase-pro/blob/develop/docs/getting_started.md')
|
console.error('Refer to the pro repo to merge your changes: https://github.com/Budibase/budibase-pro/blob/develop/docs/getting_started.md')
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -0,0 +1,29 @@
|
||||||
|
name: check_unreleased_changes
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- master
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
check_unreleased:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Check for unreleased changes
|
||||||
|
env:
|
||||||
|
REPO: "Budibase/budibase"
|
||||||
|
TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
run: |
|
||||||
|
RELEASE_TIMESTAMP=$(curl -s -H "Authorization: token $TOKEN" \
|
||||||
|
"https://api.github.com/repos/$REPO/releases/latest" | \
|
||||||
|
jq -r .published_at)
|
||||||
|
COMMIT_TIMESTAMP=$(curl -s -H "Authorization: token $TOKEN" \
|
||||||
|
"https://api.github.com/repos/$REPO/commits/master" | \
|
||||||
|
jq -r .commit.committer.date)
|
||||||
|
RELEASE_SECONDS=$(date --date="$RELEASE_TIMESTAMP" "+%s")
|
||||||
|
COMMIT_SECONDS=$(date --date="$COMMIT_TIMESTAMP" "+%s")
|
||||||
|
if (( COMMIT_SECONDS > RELEASE_SECONDS )); then
|
||||||
|
echo "There are unreleased changes. Please release these changes before merging."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "No unreleased changes detected."
|
|
@ -0,0 +1,19 @@
|
||||||
|
name: deploy-featurebranch
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- develop
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
release:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
- uses: passeidireto/trigger-external-workflow-action@main
|
||||||
|
env:
|
||||||
|
BRANCH: ${{ github.head_ref }}
|
||||||
|
with:
|
||||||
|
repository: budibase/budibase-deploys
|
||||||
|
event: featurebranch-qa-deploy
|
||||||
|
github_pat: ${{ secrets.GH_ACCESS_TOKEN }}
|
|
@ -44,7 +44,7 @@ jobs:
|
||||||
|
|
||||||
- uses: actions/setup-node@v1
|
- uses: actions/setup-node@v1
|
||||||
with:
|
with:
|
||||||
node-version: 14.x
|
node-version: 18.x
|
||||||
|
|
||||||
- run: yarn install --frozen-lockfile
|
- run: yarn install --frozen-lockfile
|
||||||
- name: Update versions
|
- name: Update versions
|
||||||
|
|
|
@ -60,9 +60,9 @@ jobs:
|
||||||
- name: "Get Current tag"
|
- name: "Get Current tag"
|
||||||
id: currenttag
|
id: currenttag
|
||||||
run: |
|
run: |
|
||||||
version=v$(./scripts/getCurrentVersion.sh)
|
version=$(./scripts/getCurrentVersion.sh)
|
||||||
echo 'Using tag $version'
|
echo "Using tag $version"
|
||||||
echo "::set-output name=tag::$resversionult"
|
echo "version=$version" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
- name: Build/release Docker images
|
- name: Build/release Docker images
|
||||||
run: |
|
run: |
|
||||||
|
@ -71,7 +71,7 @@ jobs:
|
||||||
env:
|
env:
|
||||||
DOCKER_USER: ${{ secrets.DOCKER_USERNAME }}
|
DOCKER_USER: ${{ secrets.DOCKER_USERNAME }}
|
||||||
DOCKER_PASSWORD: ${{ secrets.DOCKER_API_KEY }}
|
DOCKER_PASSWORD: ${{ secrets.DOCKER_API_KEY }}
|
||||||
BUDIBASE_RELEASE_VERSION: ${{ steps.currenttag.outputs.tag }}
|
BUDIBASE_RELEASE_VERSION: ${{ steps.currenttag.outputs.version }}
|
||||||
|
|
||||||
release-helm-chart:
|
release-helm-chart:
|
||||||
needs: [release-images]
|
needs: [release-images]
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
name: release-singleimage
|
name: Deploy Budibase Single Container Image to DockerHub
|
||||||
|
|
||||||
on:
|
on:
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
@ -8,13 +8,20 @@ env:
|
||||||
PERSONAL_ACCESS_TOKEN: ${{ secrets.PERSONAL_ACCESS_TOKEN }}
|
PERSONAL_ACCESS_TOKEN: ${{ secrets.PERSONAL_ACCESS_TOKEN }}
|
||||||
REGISTRY_URL: registry.hub.docker.com
|
REGISTRY_URL: registry.hub.docker.com
|
||||||
jobs:
|
jobs:
|
||||||
build-amd64:
|
build:
|
||||||
name: "build-amd64"
|
name: "build"
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
node-version: [14.x]
|
node-version: [14.x]
|
||||||
steps:
|
steps:
|
||||||
|
- name: Maximize build space
|
||||||
|
uses: easimon/maximize-build-space@master
|
||||||
|
with:
|
||||||
|
root-reserve-mb: 35000
|
||||||
|
swap-size-mb: 1024
|
||||||
|
remove-android: 'true'
|
||||||
|
remove-dotnet: 'true'
|
||||||
- name: Fail if not a tag
|
- name: Fail if not a tag
|
||||||
run: |
|
run: |
|
||||||
if [[ $GITHUB_REF != refs/tags/* ]]; then
|
if [[ $GITHUB_REF != refs/tags/* ]]; then
|
||||||
|
@ -27,12 +34,14 @@ jobs:
|
||||||
submodules: true
|
submodules: true
|
||||||
token: ${{ secrets.PERSONAL_ACCESS_TOKEN }}
|
token: ${{ secrets.PERSONAL_ACCESS_TOKEN }}
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
- name: Fail if tag is not in master
|
- name: Fail if tag is not in master
|
||||||
run: |
|
run: |
|
||||||
if ! git merge-base --is-ancestor ${{ github.sha }} origin/master; then
|
if ! git merge-base --is-ancestor ${{ github.sha }} origin/master; then
|
||||||
echo "Tag is not in master. This pipeline can only execute tags that are present on the master branch"
|
echo "Tag is not in master. This pipeline can only execute tags that are present on the master branch"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
- name: Use Node.js ${{ matrix.node-version }}
|
- name: Use Node.js ${{ matrix.node-version }}
|
||||||
uses: actions/setup-node@v1
|
uses: actions/setup-node@v1
|
||||||
with:
|
with:
|
||||||
|
@ -68,139 +77,9 @@ jobs:
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
push: true
|
push: true
|
||||||
platforms: linux/amd64
|
platforms: linux/amd64,linux/arm64
|
||||||
tags: budibase/budibase,budibase/budibase:v${{ env.RELEASE_VERSION }}
|
tags: budibase/budibase,budibase/budibase:${{ env.RELEASE_VERSION }}
|
||||||
file: ./hosting/single/Dockerfile
|
file: ./hosting/single/Dockerfile
|
||||||
|
|
||||||
- name: Tag and release Budibase Azure App Service docker image
|
|
||||||
uses: docker/build-push-action@v2
|
|
||||||
with:
|
|
||||||
context: .
|
|
||||||
push: true
|
|
||||||
platforms: linux/amd64
|
|
||||||
build-args: TARGETBUILD=aas
|
|
||||||
tags: budibase/budibase-aas,budibase/budibase-aas:v${{ env.RELEASE_VERSION }}
|
|
||||||
file: ./hosting/single/Dockerfile
|
|
||||||
|
|
||||||
build-arm64:
|
|
||||||
name: "build-arm64"
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
strategy:
|
|
||||||
matrix:
|
|
||||||
node-version: [14.x]
|
|
||||||
steps:
|
|
||||||
- name: Fail if not a tag
|
|
||||||
run: |
|
|
||||||
if [[ $GITHUB_REF != refs/tags/* ]]; then
|
|
||||||
echo "Workflow Dispatch can only be run on tags"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
- name: "Checkout"
|
|
||||||
uses: actions/checkout@v2
|
|
||||||
with:
|
|
||||||
submodules: true
|
|
||||||
token: ${{ secrets.PERSONAL_ACCESS_TOKEN }}
|
|
||||||
fetch-depth: 0
|
|
||||||
- name: Fail if tag is not in master
|
|
||||||
run: |
|
|
||||||
if ! git merge-base --is-ancestor ${{ github.sha }} origin/master; then
|
|
||||||
echo "Tag is not in master. This pipeline can only execute tags that are present on the master branch"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
- name: Use Node.js ${{ matrix.node-version }}
|
|
||||||
uses: actions/setup-node@v1
|
|
||||||
with:
|
|
||||||
node-version: ${{ matrix.node-version }}
|
|
||||||
- name: Setup QEMU
|
|
||||||
uses: docker/setup-qemu-action@v1
|
|
||||||
- name: Setup Docker Buildx
|
|
||||||
id: buildx
|
|
||||||
uses: docker/setup-buildx-action@v1
|
|
||||||
- name: Run Yarn
|
|
||||||
run: yarn
|
|
||||||
- name: Update versions
|
|
||||||
run: ./scripts/updateVersions.sh
|
|
||||||
- name: Runt Yarn Lint
|
|
||||||
run: yarn lint
|
|
||||||
- name: Update versions
|
|
||||||
run: ./scripts/updateVersions.sh
|
|
||||||
- name: Run Yarn Build
|
|
||||||
run: yarn build:docker:pre
|
|
||||||
- name: Login to Docker Hub
|
|
||||||
uses: docker/login-action@v2
|
|
||||||
with:
|
|
||||||
username: ${{ secrets.DOCKER_USERNAME }}
|
|
||||||
password: ${{ secrets.DOCKER_API_KEY }}
|
|
||||||
- name: Get the latest release version
|
|
||||||
id: version
|
|
||||||
run: |
|
|
||||||
release_version=$(cat lerna.json | jq -r '.version')
|
|
||||||
echo $release_version
|
|
||||||
echo "RELEASE_VERSION=$release_version" >> $GITHUB_ENV
|
|
||||||
- name: Tag and release Budibase service docker image
|
|
||||||
uses: docker/build-push-action@v2
|
|
||||||
with:
|
|
||||||
context: .
|
|
||||||
push: true
|
|
||||||
platforms: linux/arm64
|
|
||||||
tags: budibase/budibase,budibase/budibase:v${{ env.RELEASE_VERSION }}
|
|
||||||
file: ./hosting/single/Dockerfile
|
|
||||||
|
|
||||||
build-aas:
|
|
||||||
name: "build-aas"
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
strategy:
|
|
||||||
matrix:
|
|
||||||
node-version: [14.x]
|
|
||||||
steps:
|
|
||||||
- name: Fail if not a tag
|
|
||||||
run: |
|
|
||||||
if [[ $GITHUB_REF != refs/tags/* ]]; then
|
|
||||||
echo "Workflow Dispatch can only be run on tags"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
- name: "Checkout"
|
|
||||||
uses: actions/checkout@v2
|
|
||||||
with:
|
|
||||||
submodules: true
|
|
||||||
token: ${{ secrets.PERSONAL_ACCESS_TOKEN }}
|
|
||||||
fetch-depth: 0
|
|
||||||
- name: Fail if tag is not in master
|
|
||||||
run: |
|
|
||||||
if ! git merge-base --is-ancestor ${{ github.sha }} origin/master; then
|
|
||||||
echo "Tag is not in master. This pipeline can only execute tags that are present on the master branch"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
- name: Use Node.js ${{ matrix.node-version }}
|
|
||||||
uses: actions/setup-node@v1
|
|
||||||
with:
|
|
||||||
node-version: ${{ matrix.node-version }}
|
|
||||||
- name: Setup QEMU
|
|
||||||
uses: docker/setup-qemu-action@v1
|
|
||||||
- name: Setup Docker Buildx
|
|
||||||
id: buildx
|
|
||||||
uses: docker/setup-buildx-action@v1
|
|
||||||
- name: Run Yarn
|
|
||||||
run: yarn
|
|
||||||
- name: Update versions
|
|
||||||
run: ./scripts/updateVersions.sh
|
|
||||||
- name: Runt Yarn Lint
|
|
||||||
run: yarn lint
|
|
||||||
- name: Update versions
|
|
||||||
run: ./scripts/updateVersions.sh
|
|
||||||
- name: Run Yarn Build
|
|
||||||
run: yarn build:docker:pre
|
|
||||||
- name: Login to Docker Hub
|
|
||||||
uses: docker/login-action@v2
|
|
||||||
with:
|
|
||||||
username: ${{ secrets.DOCKER_USERNAME }}
|
|
||||||
password: ${{ secrets.DOCKER_API_KEY }}
|
|
||||||
- name: Get the latest release version
|
|
||||||
id: version
|
|
||||||
run: |
|
|
||||||
release_version=$(cat lerna.json | jq -r '.version')
|
|
||||||
echo $release_version
|
|
||||||
echo "RELEASE_VERSION=$release_version" >> $GITHUB_ENV
|
|
||||||
- name: Tag and release Budibase Azure App Service docker image
|
- name: Tag and release Budibase Azure App Service docker image
|
||||||
uses: docker/build-push-action@v2
|
uses: docker/build-push-action@v2
|
||||||
with:
|
with:
|
||||||
|
|
|
@ -1,3 +1,3 @@
|
||||||
nodejs 14.21.3
|
nodejs 18.17.0
|
||||||
python 3.10.0
|
python 3.10.0
|
||||||
yarn 1.22.19
|
yarn 1.22.19
|
|
@ -1,3 +1,4 @@
|
||||||
|
|
||||||
{
|
{
|
||||||
// Use IntelliSense to learn about possible attributes.
|
// Use IntelliSense to learn about possible attributes.
|
||||||
// Hover to view descriptions of existing attributes.
|
// Hover to view descriptions of existing attributes.
|
||||||
|
@ -8,30 +9,18 @@
|
||||||
"name": "Budibase Server",
|
"name": "Budibase Server",
|
||||||
"type": "node",
|
"type": "node",
|
||||||
"request": "launch",
|
"request": "launch",
|
||||||
"runtimeArgs": [
|
"runtimeArgs": ["--nolazy", "-r", "ts-node/register/transpile-only"],
|
||||||
"--nolazy",
|
"args": ["${workspaceFolder}/packages/server/src/index.ts"],
|
||||||
"-r",
|
|
||||||
"ts-node/register/transpile-only"
|
|
||||||
],
|
|
||||||
"args": [
|
|
||||||
"${workspaceFolder}/packages/server/src/index.ts"
|
|
||||||
],
|
|
||||||
"cwd": "${workspaceFolder}/packages/server"
|
"cwd": "${workspaceFolder}/packages/server"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "Budibase Worker",
|
"name": "Budibase Worker",
|
||||||
"type": "node",
|
"type": "node",
|
||||||
"request": "launch",
|
"request": "launch",
|
||||||
"runtimeArgs": [
|
"runtimeArgs": ["--nolazy", "-r", "ts-node/register/transpile-only"],
|
||||||
"--nolazy",
|
"args": ["${workspaceFolder}/packages/worker/src/index.ts"],
|
||||||
"-r",
|
|
||||||
"ts-node/register/transpile-only"
|
|
||||||
],
|
|
||||||
"args": [
|
|
||||||
"${workspaceFolder}/packages/worker/src/index.ts"
|
|
||||||
],
|
|
||||||
"cwd": "${workspaceFolder}/packages/worker"
|
"cwd": "${workspaceFolder}/packages/worker"
|
||||||
},
|
}
|
||||||
],
|
],
|
||||||
"compounds": [
|
"compounds": [
|
||||||
{
|
{
|
||||||
|
|
|
@ -120,6 +120,8 @@ spec:
|
||||||
{{ end }}
|
{{ end }}
|
||||||
- name: MULTI_TENANCY
|
- name: MULTI_TENANCY
|
||||||
value: {{ .Values.globals.multiTenancy | quote }}
|
value: {{ .Values.globals.multiTenancy | quote }}
|
||||||
|
- name: OFFLINE_MODE
|
||||||
|
value: {{ .Values.globals.offlineMode | quote }}
|
||||||
- name: LOG_LEVEL
|
- name: LOG_LEVEL
|
||||||
value: {{ .Values.services.apps.logLevel | quote }}
|
value: {{ .Values.services.apps.logLevel | quote }}
|
||||||
- name: REDIS_PASSWORD
|
- name: REDIS_PASSWORD
|
||||||
|
|
|
@ -116,6 +116,8 @@ spec:
|
||||||
value: {{ .Values.services.worker.port | quote }}
|
value: {{ .Values.services.worker.port | quote }}
|
||||||
- name: MULTI_TENANCY
|
- name: MULTI_TENANCY
|
||||||
value: {{ .Values.globals.multiTenancy | quote }}
|
value: {{ .Values.globals.multiTenancy | quote }}
|
||||||
|
- name: OFFLINE_MODE
|
||||||
|
value: {{ .Values.globals.offlineMode | quote }}
|
||||||
- name: LOG_LEVEL
|
- name: LOG_LEVEL
|
||||||
value: {{ .Values.services.worker.logLevel | quote }}
|
value: {{ .Values.services.worker.logLevel | quote }}
|
||||||
- name: REDIS_PASSWORD
|
- name: REDIS_PASSWORD
|
||||||
|
|
|
@ -82,6 +82,7 @@ globals:
|
||||||
posthogToken: "phc_bIjZL7oh2GEUd2vqvTBH8WvrX0fWTFQMs6H5KQxiUxU"
|
posthogToken: "phc_bIjZL7oh2GEUd2vqvTBH8WvrX0fWTFQMs6H5KQxiUxU"
|
||||||
selfHosted: "1" # set to 0 for budibase cloud environment, set to 1 for self-hosted setup
|
selfHosted: "1" # set to 0 for budibase cloud environment, set to 1 for self-hosted setup
|
||||||
multiTenancy: "0" # set to 0 to disable multiple orgs, set to 1 to enable multiple orgs
|
multiTenancy: "0" # set to 0 to disable multiple orgs, set to 1 to enable multiple orgs
|
||||||
|
offlineMode: "0" # set to 1 to enable offline mode
|
||||||
accountPortalUrl: ""
|
accountPortalUrl: ""
|
||||||
accountPortalApiKey: ""
|
accountPortalApiKey: ""
|
||||||
cookieDomain: ""
|
cookieDomain: ""
|
||||||
|
@ -136,7 +137,6 @@ services:
|
||||||
path: /health
|
path: /health
|
||||||
port: 10000
|
port: 10000
|
||||||
scheme: HTTP
|
scheme: HTTP
|
||||||
enabled: true
|
|
||||||
periodSeconds: 3
|
periodSeconds: 3
|
||||||
failureThreshold: 1
|
failureThreshold: 1
|
||||||
livenessProbe:
|
livenessProbe:
|
||||||
|
@ -169,7 +169,6 @@ services:
|
||||||
path: /health
|
path: /health
|
||||||
port: 4002
|
port: 4002
|
||||||
scheme: HTTP
|
scheme: HTTP
|
||||||
enabled: true
|
|
||||||
periodSeconds: 3
|
periodSeconds: 3
|
||||||
failureThreshold: 1
|
failureThreshold: 1
|
||||||
livenessProbe:
|
livenessProbe:
|
||||||
|
@ -203,7 +202,6 @@ services:
|
||||||
path: /health
|
path: /health
|
||||||
port: 4003
|
port: 4003
|
||||||
scheme: HTTP
|
scheme: HTTP
|
||||||
enabled: true
|
|
||||||
periodSeconds: 3
|
periodSeconds: 3
|
||||||
failureThreshold: 1
|
failureThreshold: 1
|
||||||
livenessProbe:
|
livenessProbe:
|
||||||
|
@ -410,14 +408,12 @@ couchdb:
|
||||||
## Ref: https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-probes/#configure-probes
|
## Ref: https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-probes/#configure-probes
|
||||||
# FOR COUCHDB
|
# FOR COUCHDB
|
||||||
livenessProbe:
|
livenessProbe:
|
||||||
enabled: true
|
|
||||||
failureThreshold: 3
|
failureThreshold: 3
|
||||||
initialDelaySeconds: 0
|
initialDelaySeconds: 0
|
||||||
periodSeconds: 10
|
periodSeconds: 10
|
||||||
successThreshold: 1
|
successThreshold: 1
|
||||||
timeoutSeconds: 1
|
timeoutSeconds: 1
|
||||||
readinessProbe:
|
readinessProbe:
|
||||||
enabled: true
|
|
||||||
failureThreshold: 3
|
failureThreshold: 3
|
||||||
initialDelaySeconds: 0
|
initialDelaySeconds: 0
|
||||||
periodSeconds: 10
|
periodSeconds: 10
|
||||||
|
|
|
@ -90,7 +90,7 @@ Component libraries are collections of components as well as the definition of t
|
||||||
|
|
||||||
#### 1. Prerequisites
|
#### 1. Prerequisites
|
||||||
|
|
||||||
- NodeJS version `14.x.x`
|
- NodeJS version `18.x.x`
|
||||||
- Python version `3.x`
|
- Python version `3.x`
|
||||||
|
|
||||||
### Using asdf (recommended)
|
### Using asdf (recommended)
|
||||||
|
|
|
@ -5,11 +5,11 @@ ENV COUCHDB_PASSWORD admin
|
||||||
EXPOSE 5984
|
EXPOSE 5984
|
||||||
|
|
||||||
RUN apt-get update && apt-get install -y --no-install-recommends software-properties-common wget unzip curl && \
|
RUN apt-get update && apt-get install -y --no-install-recommends software-properties-common wget unzip curl && \
|
||||||
wget -qO - https://adoptopenjdk.jfrog.io/adoptopenjdk/api/gpg/key/public | apt-key add - && \
|
wget -O - https://packages.adoptium.net/artifactory/api/gpg/key/public | sudo apt-key add - && \
|
||||||
apt-add-repository 'deb http://security.debian.org/debian-security bullseye-security/updates main' && \
|
apt-add-repository 'deb http://security.debian.org/debian-security bullseye-security/updates main' && \
|
||||||
apt-add-repository 'deb http://archive.debian.org/debian stretch-backports main' && \
|
apt-add-repository 'deb http://archive.debian.org/debian stretch-backports main' && \
|
||||||
apt-add-repository --yes https://adoptopenjdk.jfrog.io/adoptopenjdk/deb/ && \
|
apt-add-repository 'deb https://packages.adoptium.net/artifactory/deb bullseye main' && \
|
||||||
apt-get update && apt-get install -y --no-install-recommends adoptopenjdk-8-hotspot && \
|
apt-get update && apt-get install -y --no-install-recommends temurin-8-jdk && \
|
||||||
rm -rf /var/lib/apt/lists/
|
rm -rf /var/lib/apt/lists/
|
||||||
|
|
||||||
# setup clouseau
|
# setup clouseau
|
||||||
|
|
|
@ -1,47 +0,0 @@
|
||||||
version: "3"
|
|
||||||
|
|
||||||
# optional ports are specified throughout for more advanced use cases.
|
|
||||||
|
|
||||||
services:
|
|
||||||
minio-service:
|
|
||||||
restart: on-failure
|
|
||||||
# Last version that supports the "fs" backend
|
|
||||||
image: minio/minio:RELEASE.2022-10-24T18-35-07Z
|
|
||||||
ports:
|
|
||||||
- "9000"
|
|
||||||
- "9001"
|
|
||||||
environment:
|
|
||||||
MINIO_ACCESS_KEY: ${MINIO_ACCESS_KEY}
|
|
||||||
MINIO_SECRET_KEY: ${MINIO_SECRET_KEY}
|
|
||||||
command: server /data --console-address ":9001"
|
|
||||||
healthcheck:
|
|
||||||
test: ["CMD", "curl", "-f", "http://localhost:9000/minio/health/live"]
|
|
||||||
interval: 30s
|
|
||||||
timeout: 20s
|
|
||||||
retries: 3
|
|
||||||
|
|
||||||
couchdb-service:
|
|
||||||
# platform: linux/amd64
|
|
||||||
restart: on-failure
|
|
||||||
image: budibase/couchdb
|
|
||||||
environment:
|
|
||||||
- COUCHDB_PASSWORD=${COUCH_DB_PASSWORD}
|
|
||||||
- COUCHDB_USER=${COUCH_DB_USER}
|
|
||||||
ports:
|
|
||||||
- "5984"
|
|
||||||
- "4369"
|
|
||||||
- "9100"
|
|
||||||
healthcheck:
|
|
||||||
test: ["CMD", "curl", "-f", "http://localhost:5984/_up"]
|
|
||||||
interval: 30s
|
|
||||||
timeout: 20s
|
|
||||||
retries: 3
|
|
||||||
|
|
||||||
redis-service:
|
|
||||||
restart: on-failure
|
|
||||||
image: redis
|
|
||||||
command: redis-server --requirepass ${REDIS_PASSWORD}
|
|
||||||
ports:
|
|
||||||
- "6379"
|
|
||||||
healthcheck:
|
|
||||||
test: ["CMD", "redis-cli", "ping"]
|
|
|
@ -27,6 +27,7 @@ services:
|
||||||
BB_ADMIN_USER_EMAIL: ${BB_ADMIN_USER_EMAIL}
|
BB_ADMIN_USER_EMAIL: ${BB_ADMIN_USER_EMAIL}
|
||||||
BB_ADMIN_USER_PASSWORD: ${BB_ADMIN_USER_PASSWORD}
|
BB_ADMIN_USER_PASSWORD: ${BB_ADMIN_USER_PASSWORD}
|
||||||
PLUGINS_DIR: ${PLUGINS_DIR}
|
PLUGINS_DIR: ${PLUGINS_DIR}
|
||||||
|
OFFLINE_MODE: ${OFFLINE_MODE}
|
||||||
depends_on:
|
depends_on:
|
||||||
- worker-service
|
- worker-service
|
||||||
- redis-service
|
- redis-service
|
||||||
|
@ -54,6 +55,7 @@ services:
|
||||||
INTERNAL_API_KEY: ${INTERNAL_API_KEY}
|
INTERNAL_API_KEY: ${INTERNAL_API_KEY}
|
||||||
REDIS_URL: redis-service:6379
|
REDIS_URL: redis-service:6379
|
||||||
REDIS_PASSWORD: ${REDIS_PASSWORD}
|
REDIS_PASSWORD: ${REDIS_PASSWORD}
|
||||||
|
OFFLINE_MODE: ${OFFLINE_MODE}
|
||||||
depends_on:
|
depends_on:
|
||||||
- redis-service
|
- redis-service
|
||||||
- minio-service
|
- minio-service
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
FROM node:14-slim as build
|
FROM node:18-slim as build
|
||||||
|
|
||||||
# install node-gyp dependencies
|
# install node-gyp dependencies
|
||||||
RUN apt-get update && apt-get upgrade -y && apt-get install -y --no-install-recommends apt-utils cron g++ make python
|
RUN apt-get update && apt-get upgrade -y && apt-get install -y --no-install-recommends apt-utils cron g++ make python3
|
||||||
|
|
||||||
# add pin script
|
# add pin script
|
||||||
WORKDIR /
|
WORKDIR /
|
||||||
|
|
|
@ -1,9 +1,16 @@
|
||||||
module.exports = () => {
|
module.exports = () => {
|
||||||
return {
|
return {
|
||||||
dockerCompose: {
|
couchdb: {
|
||||||
composeFilePath: "../../hosting",
|
image: "budibase/couchdb",
|
||||||
composeFile: "docker-compose.test.yaml",
|
ports: [5984],
|
||||||
startupTimeout: 10000,
|
env: {
|
||||||
|
COUCHDB_PASSWORD: "budibase",
|
||||||
|
COUCHDB_USER: "budibase",
|
||||||
},
|
},
|
||||||
|
wait: {
|
||||||
|
type: "ports",
|
||||||
|
timeout: 20000,
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
{
|
{
|
||||||
"version": "2.8.29-alpha.15",
|
"version": "2.9.33-alpha.5",
|
||||||
"npmClient": "yarn",
|
"npmClient": "yarn",
|
||||||
"packages": [
|
"packages": [
|
||||||
"packages/*"
|
"packages/*"
|
||||||
|
|
|
@ -34,7 +34,7 @@
|
||||||
"preinstall": "node scripts/syncProPackage.js",
|
"preinstall": "node scripts/syncProPackage.js",
|
||||||
"setup": "git config submodule.recurse true && git submodule update && node ./hosting/scripts/setup.js && yarn && yarn build && yarn dev",
|
"setup": "git config submodule.recurse true && git submodule update && node ./hosting/scripts/setup.js && yarn && yarn build && yarn dev",
|
||||||
"bootstrap": "./scripts/link-dependencies.sh && echo '***BOOTSTRAP ONLY REQUIRED FOR USE WITH ACCOUNT PORTAL***'",
|
"bootstrap": "./scripts/link-dependencies.sh && echo '***BOOTSTRAP ONLY REQUIRED FOR USE WITH ACCOUNT PORTAL***'",
|
||||||
"build": "yarn nx run-many -t=build",
|
"build": "lerna run build --stream",
|
||||||
"build:dev": "lerna run --stream prebuild && yarn nx run-many --target=build --output-style=dynamic --watch --preserveWatchOutput",
|
"build:dev": "lerna run --stream prebuild && yarn nx run-many --target=build --output-style=dynamic --watch --preserveWatchOutput",
|
||||||
"check:types": "lerna run check:types",
|
"check:types": "lerna run check:types",
|
||||||
"backend:bootstrap": "./scripts/scopeBackend.sh && yarn run bootstrap",
|
"backend:bootstrap": "./scripts/scopeBackend.sh && yarn run bootstrap",
|
||||||
|
@ -109,7 +109,7 @@
|
||||||
"@budibase/types": "0.0.0"
|
"@budibase/types": "0.0.0"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=14.0.0 <15.0.0"
|
"node": ">=18.0.0 <19.0.0"
|
||||||
},
|
},
|
||||||
"dependencies": {}
|
"dependencies": {}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,4 @@
|
||||||
|
*
|
||||||
|
!dist/**/*
|
||||||
|
dist/tsconfig.build.tsbuildinfo
|
||||||
|
!package.json
|
|
@ -2,11 +2,11 @@
|
||||||
"name": "@budibase/backend-core",
|
"name": "@budibase/backend-core",
|
||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
"description": "Budibase backend core libraries used in server and worker",
|
"description": "Budibase backend core libraries used in server and worker",
|
||||||
"main": "dist/src/index.js",
|
"main": "dist/index.js",
|
||||||
"types": "dist/src/index.d.ts",
|
"types": "dist/src/index.d.ts",
|
||||||
"exports": {
|
"exports": {
|
||||||
".": "./dist/src/index.js",
|
".": "./dist/index.js",
|
||||||
"./tests": "./dist/tests/index.js",
|
"./tests": "./dist/tests.js",
|
||||||
"./*": "./dist/*.js"
|
"./*": "./dist/*.js"
|
||||||
},
|
},
|
||||||
"author": "Budibase",
|
"author": "Budibase",
|
||||||
|
@ -14,7 +14,7 @@
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"prebuild": "rimraf dist/",
|
"prebuild": "rimraf dist/",
|
||||||
"prepack": "cp package.json dist",
|
"prepack": "cp package.json dist",
|
||||||
"build": "tsc -p tsconfig.build.json",
|
"build": "node ./scripts/build.js && tsc -p tsconfig.build.json --emitDeclarationOnly --paths null",
|
||||||
"build:dev": "yarn prebuild && tsc --build --watch --preserveWatchOutput",
|
"build:dev": "yarn prebuild && tsc --build --watch --preserveWatchOutput",
|
||||||
"check:types": "tsc -p tsconfig.json --noEmit --paths null",
|
"check:types": "tsc -p tsconfig.json --noEmit --paths null",
|
||||||
"test": "bash scripts/test.sh",
|
"test": "bash scripts/test.sh",
|
||||||
|
@ -88,5 +88,20 @@
|
||||||
"ts-node": "10.8.1",
|
"ts-node": "10.8.1",
|
||||||
"tsconfig-paths": "4.0.0",
|
"tsconfig-paths": "4.0.0",
|
||||||
"typescript": "4.7.3"
|
"typescript": "4.7.3"
|
||||||
|
},
|
||||||
|
"nx": {
|
||||||
|
"targets": {
|
||||||
|
"build": {
|
||||||
|
"dependsOn": [
|
||||||
|
{
|
||||||
|
"projects": [
|
||||||
|
"@budibase/shared-core",
|
||||||
|
"@budibase/types"
|
||||||
|
],
|
||||||
|
"target": "build"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1 +0,0 @@
|
||||||
export * from "./src/plugin"
|
|
|
@ -0,0 +1,6 @@
|
||||||
|
#!/usr/bin/node
|
||||||
|
const coreBuild = require("../../../scripts/build")
|
||||||
|
|
||||||
|
coreBuild("./src/plugin/index.ts", "./dist/plugins.js")
|
||||||
|
coreBuild("./src/index.ts", "./dist/index.js")
|
||||||
|
coreBuild("./tests/index.ts", "./dist/tests.js")
|
|
@ -4,6 +4,8 @@ import * as context from "../context"
|
||||||
import * as platform from "../platform"
|
import * as platform from "../platform"
|
||||||
import env from "../environment"
|
import env from "../environment"
|
||||||
import * as accounts from "../accounts"
|
import * as accounts from "../accounts"
|
||||||
|
import { UserDB } from "../users"
|
||||||
|
import { sdk } from "@budibase/shared-core"
|
||||||
|
|
||||||
const EXPIRY_SECONDS = 3600
|
const EXPIRY_SECONDS = 3600
|
||||||
|
|
||||||
|
@ -60,6 +62,18 @@ export async function getUser(
|
||||||
// make sure the tenant ID is always correct/set
|
// make sure the tenant ID is always correct/set
|
||||||
user.tenantId = tenantId
|
user.tenantId = tenantId
|
||||||
}
|
}
|
||||||
|
// if has groups, could have builder permissions granted by a group
|
||||||
|
if (user.userGroups && !sdk.users.isGlobalBuilder(user)) {
|
||||||
|
await context.doInTenant(tenantId, async () => {
|
||||||
|
const appIds = await UserDB.getGroupBuilderAppIds(user)
|
||||||
|
if (appIds.length) {
|
||||||
|
const existing = user.builder?.apps || []
|
||||||
|
user.builder = {
|
||||||
|
apps: [...new Set(existing.concat(appIds))],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
return user
|
return user
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -8,6 +8,7 @@ import {
|
||||||
DatabasePutOpts,
|
DatabasePutOpts,
|
||||||
DatabaseCreateIndexOpts,
|
DatabaseCreateIndexOpts,
|
||||||
DatabaseDeleteIndexOpts,
|
DatabaseDeleteIndexOpts,
|
||||||
|
DocExistsResponse,
|
||||||
Document,
|
Document,
|
||||||
isDocument,
|
isDocument,
|
||||||
} from "@budibase/types"
|
} from "@budibase/types"
|
||||||
|
@ -120,6 +121,19 @@ export class DatabaseImpl implements Database {
|
||||||
return this.updateOutput(() => db.get(id))
|
return this.updateOutput(() => db.get(id))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async docExists(docId: string): Promise<DocExistsResponse> {
|
||||||
|
const db = await this.checkSetup()
|
||||||
|
let _rev, exists
|
||||||
|
try {
|
||||||
|
const { etag } = await db.head(docId)
|
||||||
|
_rev = etag
|
||||||
|
exists = true
|
||||||
|
} catch (err) {
|
||||||
|
exists = false
|
||||||
|
}
|
||||||
|
return { _rev, exists }
|
||||||
|
}
|
||||||
|
|
||||||
async remove(idOrDoc: string | Document, rev?: string) {
|
async remove(idOrDoc: string | Document, rev?: string) {
|
||||||
const db = await this.checkSetup()
|
const db = await this.checkSetup()
|
||||||
let _id: string
|
let _id: string
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
import fetch from "node-fetch"
|
import fetch from "node-fetch"
|
||||||
import { getCouchInfo } from "./couch"
|
import { getCouchInfo } from "./couch"
|
||||||
import { SearchFilters, Row } from "@budibase/types"
|
import { SearchFilters, Row, EmptyFilterOption } from "@budibase/types"
|
||||||
import { createUserIndex } from "./searchIndexes/searchIndexes"
|
|
||||||
|
|
||||||
const QUERY_START_REGEX = /\d[0-9]*:/g
|
const QUERY_START_REGEX = /\d[0-9]*:/g
|
||||||
|
|
||||||
|
@ -65,6 +64,7 @@ export class QueryBuilder<T> {
|
||||||
this.#index = index
|
this.#index = index
|
||||||
this.#query = {
|
this.#query = {
|
||||||
allOr: false,
|
allOr: false,
|
||||||
|
onEmptyFilter: EmptyFilterOption.RETURN_ALL,
|
||||||
string: {},
|
string: {},
|
||||||
fuzzy: {},
|
fuzzy: {},
|
||||||
range: {},
|
range: {},
|
||||||
|
@ -218,6 +218,10 @@ export class QueryBuilder<T> {
|
||||||
this.#query.allOr = true
|
this.#query.allOr = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setOnEmptyFilter(value: EmptyFilterOption) {
|
||||||
|
this.#query.onEmptyFilter = value
|
||||||
|
}
|
||||||
|
|
||||||
handleSpaces(input: string) {
|
handleSpaces(input: string) {
|
||||||
if (this.#noEscaping) {
|
if (this.#noEscaping) {
|
||||||
return input
|
return input
|
||||||
|
@ -289,8 +293,9 @@ export class QueryBuilder<T> {
|
||||||
const builder = this
|
const builder = this
|
||||||
let allOr = this.#query && this.#query.allOr
|
let allOr = this.#query && this.#query.allOr
|
||||||
let query = allOr ? "" : "*:*"
|
let query = allOr ? "" : "*:*"
|
||||||
|
let allFiltersEmpty = true
|
||||||
const allPreProcessingOpts = { escape: true, lowercase: true, wrap: true }
|
const allPreProcessingOpts = { escape: true, lowercase: true, wrap: true }
|
||||||
let tableId
|
let tableId: string = ""
|
||||||
if (this.#query.equal!.tableId) {
|
if (this.#query.equal!.tableId) {
|
||||||
tableId = this.#query.equal!.tableId
|
tableId = this.#query.equal!.tableId
|
||||||
delete this.#query.equal!.tableId
|
delete this.#query.equal!.tableId
|
||||||
|
@ -305,7 +310,7 @@ export class QueryBuilder<T> {
|
||||||
}
|
}
|
||||||
|
|
||||||
const contains = (key: string, value: any, mode = "AND") => {
|
const contains = (key: string, value: any, mode = "AND") => {
|
||||||
if (Array.isArray(value) && value.length === 0) {
|
if (!value || (Array.isArray(value) && value.length === 0)) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
if (!Array.isArray(value)) {
|
if (!Array.isArray(value)) {
|
||||||
|
@ -384,6 +389,12 @@ export class QueryBuilder<T> {
|
||||||
built += ` ${mode} `
|
built += ` ${mode} `
|
||||||
}
|
}
|
||||||
built += expression
|
built += expression
|
||||||
|
if (
|
||||||
|
(typeof value !== "string" && value != null) ||
|
||||||
|
(typeof value === "string" && value !== tableId && value !== "")
|
||||||
|
) {
|
||||||
|
allFiltersEmpty = false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (opts?.returnBuilt) {
|
if (opts?.returnBuilt) {
|
||||||
return built
|
return built
|
||||||
|
@ -463,6 +474,13 @@ export class QueryBuilder<T> {
|
||||||
allOr = false
|
allOr = false
|
||||||
build({ tableId }, equal)
|
build({ tableId }, equal)
|
||||||
}
|
}
|
||||||
|
if (allFiltersEmpty) {
|
||||||
|
if (this.#query.onEmptyFilter === EmptyFilterOption.RETURN_NONE) {
|
||||||
|
return ""
|
||||||
|
} else if (this.#query?.allOr) {
|
||||||
|
return query.replace("()", "(*:*)")
|
||||||
|
}
|
||||||
|
}
|
||||||
return query
|
return query
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { newid } from "../../docIds/newid"
|
import { newid } from "../../docIds/newid"
|
||||||
import { getDB } from "../db"
|
import { getDB } from "../db"
|
||||||
import { Database } from "@budibase/types"
|
import { Database, EmptyFilterOption } from "@budibase/types"
|
||||||
import { QueryBuilder, paginatedSearch, fullSearch } from "../lucene"
|
import { QueryBuilder, paginatedSearch, fullSearch } from "../lucene"
|
||||||
|
|
||||||
const INDEX_NAME = "main"
|
const INDEX_NAME = "main"
|
||||||
|
@ -156,6 +156,76 @@ describe("lucene", () => {
|
||||||
expect(resp.rows.length).toBe(2)
|
expect(resp.rows.length).toBe(2)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe("empty filters behaviour", () => {
|
||||||
|
it("should return all rows by default", async () => {
|
||||||
|
const builder = new QueryBuilder(dbName, INDEX_NAME)
|
||||||
|
builder.addEqual("property", "")
|
||||||
|
builder.addEqual("number", null)
|
||||||
|
builder.addString("property", "")
|
||||||
|
builder.addFuzzy("property", "")
|
||||||
|
builder.addNotEqual("number", undefined)
|
||||||
|
builder.addOneOf("number", null)
|
||||||
|
builder.addContains("array", undefined)
|
||||||
|
builder.addNotContains("array", null)
|
||||||
|
builder.addContainsAny("array", null)
|
||||||
|
|
||||||
|
const resp = await builder.run()
|
||||||
|
expect(resp.rows.length).toBe(3)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should return all rows when onEmptyFilter is ALL", async () => {
|
||||||
|
const builder = new QueryBuilder(dbName, INDEX_NAME)
|
||||||
|
builder.setOnEmptyFilter(EmptyFilterOption.RETURN_ALL)
|
||||||
|
builder.setAllOr()
|
||||||
|
builder.addEqual("property", "")
|
||||||
|
builder.addEqual("number", null)
|
||||||
|
builder.addString("property", "")
|
||||||
|
builder.addFuzzy("property", "")
|
||||||
|
builder.addNotEqual("number", undefined)
|
||||||
|
builder.addOneOf("number", null)
|
||||||
|
builder.addContains("array", undefined)
|
||||||
|
builder.addNotContains("array", null)
|
||||||
|
builder.addContainsAny("array", null)
|
||||||
|
|
||||||
|
const resp = await builder.run()
|
||||||
|
expect(resp.rows.length).toBe(3)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should return no rows when onEmptyFilter is NONE", async () => {
|
||||||
|
const builder = new QueryBuilder(dbName, INDEX_NAME)
|
||||||
|
builder.setOnEmptyFilter(EmptyFilterOption.RETURN_NONE)
|
||||||
|
builder.addEqual("property", "")
|
||||||
|
builder.addEqual("number", null)
|
||||||
|
builder.addString("property", "")
|
||||||
|
builder.addFuzzy("property", "")
|
||||||
|
builder.addNotEqual("number", undefined)
|
||||||
|
builder.addOneOf("number", null)
|
||||||
|
builder.addContains("array", undefined)
|
||||||
|
builder.addNotContains("array", null)
|
||||||
|
builder.addContainsAny("array", null)
|
||||||
|
|
||||||
|
const resp = await builder.run()
|
||||||
|
expect(resp.rows.length).toBe(0)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should return all matching rows when onEmptyFilter is NONE, but a filter value is provided", async () => {
|
||||||
|
const builder = new QueryBuilder(dbName, INDEX_NAME)
|
||||||
|
builder.setOnEmptyFilter(EmptyFilterOption.RETURN_NONE)
|
||||||
|
builder.addEqual("property", "")
|
||||||
|
builder.addEqual("number", 1)
|
||||||
|
builder.addString("property", "")
|
||||||
|
builder.addFuzzy("property", "")
|
||||||
|
builder.addNotEqual("number", undefined)
|
||||||
|
builder.addOneOf("number", null)
|
||||||
|
builder.addContains("array", undefined)
|
||||||
|
builder.addNotContains("array", null)
|
||||||
|
builder.addContainsAny("array", null)
|
||||||
|
|
||||||
|
const resp = await builder.run()
|
||||||
|
expect(resp.rows.length).toBe(1)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
describe("skip", () => {
|
describe("skip", () => {
|
||||||
const skipDbName = `db-${newid()}`
|
const skipDbName = `db-${newid()}`
|
||||||
let docs: {
|
let docs: {
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import env from "../environment"
|
import env from "../environment"
|
||||||
import * as context from "../context"
|
import * as context from "../context"
|
||||||
|
export * from "./installation"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Read the TENANT_FEATURE_FLAGS env var and return an array of features flags for each tenant.
|
* Read the TENANT_FEATURE_FLAGS env var and return an array of features flags for each tenant.
|
|
@ -0,0 +1,17 @@
|
||||||
|
export function processFeatureEnvVar<T>(
|
||||||
|
fullList: string[],
|
||||||
|
featureList?: string
|
||||||
|
) {
|
||||||
|
let list
|
||||||
|
if (!featureList) {
|
||||||
|
list = fullList
|
||||||
|
} else {
|
||||||
|
list = featureList.split(",")
|
||||||
|
}
|
||||||
|
for (let feature of list) {
|
||||||
|
if (!fullList.includes(feature)) {
|
||||||
|
throw new Error(`Feature: ${feature} is not an allowed option`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return list as unknown as T[]
|
||||||
|
}
|
|
@ -6,7 +6,8 @@ export * as roles from "./security/roles"
|
||||||
export * as permissions from "./security/permissions"
|
export * as permissions from "./security/permissions"
|
||||||
export * as accounts from "./accounts"
|
export * as accounts from "./accounts"
|
||||||
export * as installation from "./installation"
|
export * as installation from "./installation"
|
||||||
export * as featureFlags from "./featureFlags"
|
export * as featureFlags from "./features"
|
||||||
|
export * as features from "./features/installation"
|
||||||
export * as sessions from "./security/sessions"
|
export * as sessions from "./security/sessions"
|
||||||
export * as platform from "./platform"
|
export * as platform from "./platform"
|
||||||
export * as auth from "./auth"
|
export * as auth from "./auth"
|
||||||
|
|
|
@ -5,7 +5,8 @@ import env from "../environment"
|
||||||
|
|
||||||
export default async (ctx: UserCtx, next: any) => {
|
export default async (ctx: UserCtx, next: any) => {
|
||||||
const appId = getAppId()
|
const appId = getAppId()
|
||||||
const builderFn = env.isWorker()
|
const builderFn =
|
||||||
|
env.isWorker() || !appId
|
||||||
? hasBuilderPermissions
|
? hasBuilderPermissions
|
||||||
: env.isApps()
|
: env.isApps()
|
||||||
? isBuilder
|
? isBuilder
|
||||||
|
|
|
@ -5,7 +5,8 @@ import env from "../environment"
|
||||||
|
|
||||||
export default async (ctx: UserCtx, next: any) => {
|
export default async (ctx: UserCtx, next: any) => {
|
||||||
const appId = getAppId()
|
const appId = getAppId()
|
||||||
const builderFn = env.isWorker()
|
const builderFn =
|
||||||
|
env.isWorker() || !appId
|
||||||
? hasBuilderPermissions
|
? hasBuilderPermissions
|
||||||
: env.isApps()
|
: env.isApps()
|
||||||
? isBuilder
|
? isBuilder
|
||||||
|
|
|
@ -78,7 +78,6 @@ export const BUILTIN_PERMISSIONS = {
|
||||||
permissions: [
|
permissions: [
|
||||||
new Permission(PermissionType.QUERY, PermissionLevel.READ),
|
new Permission(PermissionType.QUERY, PermissionLevel.READ),
|
||||||
new Permission(PermissionType.TABLE, PermissionLevel.READ),
|
new Permission(PermissionType.TABLE, PermissionLevel.READ),
|
||||||
new Permission(PermissionType.VIEW, PermissionLevel.READ),
|
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
WRITE: {
|
WRITE: {
|
||||||
|
@ -87,7 +86,6 @@ export const BUILTIN_PERMISSIONS = {
|
||||||
permissions: [
|
permissions: [
|
||||||
new Permission(PermissionType.QUERY, PermissionLevel.WRITE),
|
new Permission(PermissionType.QUERY, PermissionLevel.WRITE),
|
||||||
new Permission(PermissionType.TABLE, PermissionLevel.WRITE),
|
new Permission(PermissionType.TABLE, PermissionLevel.WRITE),
|
||||||
new Permission(PermissionType.VIEW, PermissionLevel.READ),
|
|
||||||
new Permission(PermissionType.AUTOMATION, PermissionLevel.EXECUTE),
|
new Permission(PermissionType.AUTOMATION, PermissionLevel.EXECUTE),
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
@ -98,7 +96,6 @@ export const BUILTIN_PERMISSIONS = {
|
||||||
new Permission(PermissionType.TABLE, PermissionLevel.WRITE),
|
new Permission(PermissionType.TABLE, PermissionLevel.WRITE),
|
||||||
new Permission(PermissionType.USER, PermissionLevel.READ),
|
new Permission(PermissionType.USER, PermissionLevel.READ),
|
||||||
new Permission(PermissionType.AUTOMATION, PermissionLevel.EXECUTE),
|
new Permission(PermissionType.AUTOMATION, PermissionLevel.EXECUTE),
|
||||||
new Permission(PermissionType.VIEW, PermissionLevel.READ),
|
|
||||||
new Permission(PermissionType.WEBHOOK, PermissionLevel.READ),
|
new Permission(PermissionType.WEBHOOK, PermissionLevel.READ),
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
@ -109,7 +106,6 @@ export const BUILTIN_PERMISSIONS = {
|
||||||
new Permission(PermissionType.TABLE, PermissionLevel.ADMIN),
|
new Permission(PermissionType.TABLE, PermissionLevel.ADMIN),
|
||||||
new Permission(PermissionType.USER, PermissionLevel.ADMIN),
|
new Permission(PermissionType.USER, PermissionLevel.ADMIN),
|
||||||
new Permission(PermissionType.AUTOMATION, PermissionLevel.ADMIN),
|
new Permission(PermissionType.AUTOMATION, PermissionLevel.ADMIN),
|
||||||
new Permission(PermissionType.VIEW, PermissionLevel.ADMIN),
|
|
||||||
new Permission(PermissionType.WEBHOOK, PermissionLevel.READ),
|
new Permission(PermissionType.WEBHOOK, PermissionLevel.READ),
|
||||||
new Permission(PermissionType.QUERY, PermissionLevel.ADMIN),
|
new Permission(PermissionType.QUERY, PermissionLevel.ADMIN),
|
||||||
],
|
],
|
||||||
|
|
|
@ -1,30 +1,32 @@
|
||||||
import env from "../environment"
|
import env from "../environment"
|
||||||
import * as eventHelpers from "./events"
|
import * as eventHelpers from "./events"
|
||||||
import * as accounts from "../accounts"
|
import * as accounts from "../accounts"
|
||||||
|
import * as accountSdk from "../accounts"
|
||||||
import * as cache from "../cache"
|
import * as cache from "../cache"
|
||||||
import { getIdentity, getTenantId, getGlobalDB } from "../context"
|
import { getGlobalDB, getIdentity, getTenantId } from "../context"
|
||||||
import * as dbUtils from "../db"
|
import * as dbUtils from "../db"
|
||||||
import { EmailUnavailableError, HTTPError } from "../errors"
|
import { EmailUnavailableError, HTTPError } from "../errors"
|
||||||
import * as platform from "../platform"
|
import * as platform from "../platform"
|
||||||
import * as sessions from "../security/sessions"
|
import * as sessions from "../security/sessions"
|
||||||
import * as usersCore from "./users"
|
import * as usersCore from "./users"
|
||||||
import {
|
import {
|
||||||
|
Account,
|
||||||
AllDocsResponse,
|
AllDocsResponse,
|
||||||
BulkUserCreated,
|
BulkUserCreated,
|
||||||
BulkUserDeleted,
|
BulkUserDeleted,
|
||||||
|
isSSOAccount,
|
||||||
|
isSSOUser,
|
||||||
RowResponse,
|
RowResponse,
|
||||||
SaveUserOpts,
|
SaveUserOpts,
|
||||||
User,
|
User,
|
||||||
Account,
|
|
||||||
isSSOUser,
|
|
||||||
isSSOAccount,
|
|
||||||
UserStatus,
|
UserStatus,
|
||||||
|
UserGroup,
|
||||||
|
ContextUser,
|
||||||
} from "@budibase/types"
|
} from "@budibase/types"
|
||||||
import * as accountSdk from "../accounts"
|
|
||||||
import {
|
import {
|
||||||
validateUniqueUser,
|
|
||||||
getAccountHolderFromUserIds,
|
getAccountHolderFromUserIds,
|
||||||
isAdmin,
|
isAdmin,
|
||||||
|
validateUniqueUser,
|
||||||
} from "./utils"
|
} from "./utils"
|
||||||
import { searchExistingEmails } from "./lookup"
|
import { searchExistingEmails } from "./lookup"
|
||||||
import { hash } from "../utils"
|
import { hash } from "../utils"
|
||||||
|
@ -32,8 +34,14 @@ import { hash } from "../utils"
|
||||||
type QuotaUpdateFn = (change: number, cb?: () => Promise<any>) => Promise<any>
|
type QuotaUpdateFn = (change: number, cb?: () => Promise<any>) => Promise<any>
|
||||||
type GroupUpdateFn = (groupId: string, userIds: string[]) => Promise<any>
|
type GroupUpdateFn = (groupId: string, userIds: string[]) => Promise<any>
|
||||||
type FeatureFn = () => Promise<Boolean>
|
type FeatureFn = () => Promise<Boolean>
|
||||||
|
type GroupGetFn = (ids: string[]) => Promise<UserGroup[]>
|
||||||
|
type GroupBuildersFn = (user: User) => Promise<string[]>
|
||||||
type QuotaFns = { addUsers: QuotaUpdateFn; removeUsers: QuotaUpdateFn }
|
type QuotaFns = { addUsers: QuotaUpdateFn; removeUsers: QuotaUpdateFn }
|
||||||
type GroupFns = { addUsers: GroupUpdateFn }
|
type GroupFns = {
|
||||||
|
addUsers: GroupUpdateFn
|
||||||
|
getBulk: GroupGetFn
|
||||||
|
getGroupBuilderAppIds: GroupBuildersFn
|
||||||
|
}
|
||||||
type FeatureFns = { isSSOEnforced: FeatureFn; isAppBuildersEnabled: FeatureFn }
|
type FeatureFns = { isSSOEnforced: FeatureFn; isAppBuildersEnabled: FeatureFn }
|
||||||
|
|
||||||
const bulkDeleteProcessing = async (dbUser: User) => {
|
const bulkDeleteProcessing = async (dbUser: User) => {
|
||||||
|
@ -179,6 +187,14 @@ export class UserDB {
|
||||||
return user
|
return user
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static async bulkGet(userIds: string[]) {
|
||||||
|
return await usersCore.bulkGetGlobalUsersById(userIds)
|
||||||
|
}
|
||||||
|
|
||||||
|
static async bulkUpdate(users: User[]) {
|
||||||
|
return await usersCore.bulkUpdateGlobalUsers(users)
|
||||||
|
}
|
||||||
|
|
||||||
static async save(user: User, opts: SaveUserOpts = {}): Promise<User> {
|
static async save(user: User, opts: SaveUserOpts = {}): Promise<User> {
|
||||||
// default booleans to true
|
// default booleans to true
|
||||||
if (opts.hashPassword == null) {
|
if (opts.hashPassword == null) {
|
||||||
|
@ -457,4 +473,12 @@ export class UserDB {
|
||||||
await cache.user.invalidateUser(userId)
|
await cache.user.invalidateUser(userId)
|
||||||
await sessions.invalidateSessions(userId, { reason: "deletion" })
|
await sessions.invalidateSessions(userId, { reason: "deletion" })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static async getGroups(groupIds: string[]) {
|
||||||
|
return await this.groups.getBulk(groupIds)
|
||||||
|
}
|
||||||
|
|
||||||
|
static async getGroupBuilderAppIds(user: User) {
|
||||||
|
return await this.groups.getGroupBuilderAppIds(user)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -86,6 +86,10 @@ export const useAuditLogs = () => {
|
||||||
return useFeature(Feature.AUDIT_LOGS)
|
return useFeature(Feature.AUDIT_LOGS)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const usePublicApiUserRoles = () => {
|
||||||
|
return useFeature(Feature.USER_ROLE_PUBLIC_API)
|
||||||
|
}
|
||||||
|
|
||||||
export const useScimIntegration = () => {
|
export const useScimIntegration = () => {
|
||||||
return useFeature(Feature.SCIM)
|
return useFeature(Feature.SCIM)
|
||||||
}
|
}
|
||||||
|
@ -98,6 +102,10 @@ export const useAppBuilders = () => {
|
||||||
return useFeature(Feature.APP_BUILDERS)
|
return useFeature(Feature.APP_BUILDERS)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const useViewPermissions = () => {
|
||||||
|
return useFeature(Feature.VIEW_PERMISSIONS)
|
||||||
|
}
|
||||||
|
|
||||||
// QUOTAS
|
// QUOTAS
|
||||||
|
|
||||||
export const setAutomationLogsQuota = (value: number) => {
|
export const setAutomationLogsQuota = (value: number) => {
|
||||||
|
|
|
@ -32,8 +32,8 @@ function getTestContainerSettings(
|
||||||
): string | null {
|
): string | null {
|
||||||
const entry = Object.entries(global).find(
|
const entry = Object.entries(global).find(
|
||||||
([k]) =>
|
([k]) =>
|
||||||
k.includes(`_${serverName.toUpperCase()}`) &&
|
k.includes(`${serverName.toUpperCase()}`) &&
|
||||||
k.includes(`_${key.toUpperCase()}__`)
|
k.includes(`${key.toUpperCase()}`)
|
||||||
)
|
)
|
||||||
if (!entry) {
|
if (!entry) {
|
||||||
return null
|
return null
|
||||||
|
@ -67,27 +67,14 @@ function getContainerInfo(containerName: string, port: number) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function getCouchConfig() {
|
function getCouchConfig() {
|
||||||
return getContainerInfo("couchdb-service", 5984)
|
return getContainerInfo("couchdb", 5984)
|
||||||
}
|
|
||||||
|
|
||||||
function getMinioConfig() {
|
|
||||||
return getContainerInfo("minio-service", 9000)
|
|
||||||
}
|
|
||||||
|
|
||||||
function getRedisConfig() {
|
|
||||||
return getContainerInfo("redis-service", 6379)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function setupEnv(...envs: any[]) {
|
export function setupEnv(...envs: any[]) {
|
||||||
const couch = getCouchConfig(),
|
const couch = getCouchConfig()
|
||||||
minio = getCouchConfig(),
|
|
||||||
redis = getRedisConfig()
|
|
||||||
const configs = [
|
const configs = [
|
||||||
{ key: "COUCH_DB_PORT", value: couch.port },
|
{ key: "COUCH_DB_PORT", value: couch.port },
|
||||||
{ key: "COUCH_DB_URL", value: couch.url },
|
{ key: "COUCH_DB_URL", value: couch.url },
|
||||||
{ key: "MINIO_PORT", value: minio.port },
|
|
||||||
{ key: "MINIO_URL", value: minio.url },
|
|
||||||
{ key: "REDIS_URL", value: redis.url },
|
|
||||||
]
|
]
|
||||||
|
|
||||||
for (const config of configs.filter(x => !!x.value)) {
|
for (const config of configs.filter(x => !!x.value)) {
|
||||||
|
|
|
@ -12,7 +12,11 @@
|
||||||
"declaration": true,
|
"declaration": true,
|
||||||
"types": ["node", "jest"],
|
"types": ["node", "jest"],
|
||||||
"outDir": "dist",
|
"outDir": "dist",
|
||||||
"skipLibCheck": true
|
"skipLibCheck": true,
|
||||||
|
"paths": {
|
||||||
|
"@budibase/types": ["../types/src"],
|
||||||
|
"@budibase/shared-core": ["../shared-core/src"]
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"include": ["**/*.js", "**/*.ts"],
|
"include": ["**/*.js", "**/*.ts"],
|
||||||
"exclude": [
|
"exclude": [
|
||||||
|
|
|
@ -1,12 +1,4 @@
|
||||||
{
|
{
|
||||||
"extends": "./tsconfig.build.json",
|
"extends": "./tsconfig.build.json",
|
||||||
"compilerOptions": {
|
|
||||||
"composite": true,
|
|
||||||
"baseUrl": ".",
|
|
||||||
"paths": {
|
|
||||||
"@budibase/types": ["../types/src"],
|
|
||||||
"@budibase/shared-core": ["../shared-core/src"]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"exclude": ["node_modules", "dist"]
|
"exclude": ["node_modules", "dist"]
|
||||||
}
|
}
|
||||||
|
|
|
@ -98,8 +98,7 @@
|
||||||
{
|
{
|
||||||
"projects": [
|
"projects": [
|
||||||
"@budibase/string-templates",
|
"@budibase/string-templates",
|
||||||
"@budibase/shared-core",
|
"@budibase/shared-core"
|
||||||
"@budibase/types"
|
|
||||||
],
|
],
|
||||||
"target": "build"
|
"target": "build"
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,6 +17,7 @@ export default function positionDropdown(element, opts) {
|
||||||
maxWidth,
|
maxWidth,
|
||||||
useAnchorWidth,
|
useAnchorWidth,
|
||||||
offset = 5,
|
offset = 5,
|
||||||
|
customUpdate,
|
||||||
} = opts
|
} = opts
|
||||||
if (!anchor) {
|
if (!anchor) {
|
||||||
return
|
return
|
||||||
|
@ -33,10 +34,16 @@ export default function positionDropdown(element, opts) {
|
||||||
top: null,
|
top: null,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (typeof customUpdate === "function") {
|
||||||
|
styles = customUpdate(anchorBounds, elementBounds, styles)
|
||||||
|
} else {
|
||||||
// Determine vertical styles
|
// Determine vertical styles
|
||||||
if (align === "right-outside") {
|
if (align === "right-outside") {
|
||||||
styles.top = anchorBounds.top
|
styles.top = anchorBounds.top
|
||||||
} else if (window.innerHeight - anchorBounds.bottom < 100) {
|
} else if (
|
||||||
|
window.innerHeight - anchorBounds.bottom <
|
||||||
|
(maxHeight || 100)
|
||||||
|
) {
|
||||||
styles.top = anchorBounds.top - elementBounds.height - offset
|
styles.top = anchorBounds.top - elementBounds.height - offset
|
||||||
styles.maxHeight = maxHeight || 240
|
styles.maxHeight = maxHeight || 240
|
||||||
} else {
|
} else {
|
||||||
|
@ -53,7 +60,8 @@ export default function positionDropdown(element, opts) {
|
||||||
styles.minWidth = anchorBounds.width
|
styles.minWidth = anchorBounds.width
|
||||||
}
|
}
|
||||||
if (align === "right") {
|
if (align === "right") {
|
||||||
styles.left = anchorBounds.left + anchorBounds.width - elementBounds.width
|
styles.left =
|
||||||
|
anchorBounds.left + anchorBounds.width - elementBounds.width
|
||||||
} else if (align === "right-outside") {
|
} else if (align === "right-outside") {
|
||||||
styles.left = anchorBounds.right + offset
|
styles.left = anchorBounds.right + offset
|
||||||
} else if (align === "left-outside") {
|
} else if (align === "left-outside") {
|
||||||
|
@ -61,6 +69,7 @@ export default function positionDropdown(element, opts) {
|
||||||
} else {
|
} else {
|
||||||
styles.left = anchorBounds.left
|
styles.left = anchorBounds.left
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Apply styles
|
// Apply styles
|
||||||
Object.entries(styles).forEach(([style, value]) => {
|
Object.entries(styles).forEach(([style, value]) => {
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
<script>
|
<script>
|
||||||
|
import Popover from "../Popover/Popover.svelte"
|
||||||
|
import Layout from "../Layout/Layout.svelte"
|
||||||
import { createEventDispatcher } from "svelte"
|
import { createEventDispatcher } from "svelte"
|
||||||
import "@spectrum-css/popover/dist/index-vars.css"
|
import "@spectrum-css/popover/dist/index-vars.css"
|
||||||
import clickOutside from "../Actions/click_outside"
|
|
||||||
import { fly } from "svelte/transition"
|
|
||||||
import Icon from "../Icon/Icon.svelte"
|
import Icon from "../Icon/Icon.svelte"
|
||||||
import Input from "../Form/Input.svelte"
|
import Input from "../Form/Input.svelte"
|
||||||
import { capitalise } from "../helpers"
|
import { capitalise } from "../helpers"
|
||||||
|
@ -10,9 +10,11 @@
|
||||||
export let value
|
export let value
|
||||||
export let size = "M"
|
export let size = "M"
|
||||||
export let spectrumTheme
|
export let spectrumTheme
|
||||||
export let alignRight = false
|
export let offset
|
||||||
|
export let align
|
||||||
|
|
||||||
let open = false
|
let dropdown
|
||||||
|
let preview
|
||||||
|
|
||||||
$: customValue = getCustomValue(value)
|
$: customValue = getCustomValue(value)
|
||||||
$: checkColor = getCheckColor(value)
|
$: checkColor = getCheckColor(value)
|
||||||
|
@ -82,7 +84,7 @@
|
||||||
|
|
||||||
const onChange = value => {
|
const onChange = value => {
|
||||||
dispatch("change", value)
|
dispatch("change", value)
|
||||||
open = false
|
dropdown.hide()
|
||||||
}
|
}
|
||||||
|
|
||||||
const getCustomValue = value => {
|
const getCustomValue = value => {
|
||||||
|
@ -119,30 +121,25 @@
|
||||||
|
|
||||||
return "var(--spectrum-global-color-static-gray-900)"
|
return "var(--spectrum-global-color-static-gray-900)"
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleOutsideClick = event => {
|
|
||||||
if (open) {
|
|
||||||
event.stopPropagation()
|
|
||||||
open = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="container">
|
<div
|
||||||
<div class="preview size--{size || 'M'}" on:click={() => (open = true)}>
|
bind:this={preview}
|
||||||
|
class="preview size--{size || 'M'}"
|
||||||
|
on:click={() => {
|
||||||
|
dropdown.toggle()
|
||||||
|
}}
|
||||||
|
>
|
||||||
<div
|
<div
|
||||||
class="fill {spectrumTheme || ''}"
|
class="fill {spectrumTheme || ''}"
|
||||||
style={value ? `background: ${value};` : ""}
|
style={value ? `background: ${value};` : ""}
|
||||||
class:placeholder={!value}
|
class:placeholder={!value}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{#if open}
|
|
||||||
<div
|
<Popover bind:this={dropdown} anchor={preview} maxHeight={320} {offset} {align}>
|
||||||
use:clickOutside={handleOutsideClick}
|
<Layout paddingX="XL" paddingY="L">
|
||||||
transition:fly|local={{ y: -20, duration: 200 }}
|
<div class="container">
|
||||||
class="spectrum-Popover spectrum-Popover--bottom spectrum-Picker-popover is-open"
|
|
||||||
class:spectrum-Popover--align-right={alignRight}
|
|
||||||
>
|
|
||||||
{#each categories as category}
|
{#each categories as category}
|
||||||
<div class="category">
|
<div class="category">
|
||||||
<div class="heading">{category.label}</div>
|
<div class="heading">{category.label}</div>
|
||||||
|
@ -187,8 +184,8 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
</Layout>
|
||||||
</div>
|
</Popover>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.container {
|
.container {
|
||||||
|
@ -248,20 +245,6 @@
|
||||||
width: 48px;
|
width: 48px;
|
||||||
height: 48px;
|
height: 48px;
|
||||||
}
|
}
|
||||||
.spectrum-Popover {
|
|
||||||
width: 210px;
|
|
||||||
z-index: 999;
|
|
||||||
top: 100%;
|
|
||||||
padding: var(--spacing-l) var(--spacing-xl);
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
justify-content: flex-start;
|
|
||||||
align-items: stretch;
|
|
||||||
gap: var(--spacing-xl);
|
|
||||||
}
|
|
||||||
.spectrum-Popover--align-right {
|
|
||||||
right: 0;
|
|
||||||
}
|
|
||||||
.colors {
|
.colors {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr;
|
grid-template-columns: 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr;
|
||||||
|
@ -297,7 +280,11 @@
|
||||||
.category--custom .heading {
|
.category--custom .heading {
|
||||||
margin-bottom: var(--spacing-xs);
|
margin-bottom: var(--spacing-xs);
|
||||||
}
|
}
|
||||||
|
.container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--spacing-xl);
|
||||||
|
}
|
||||||
.spectrum-wrapper {
|
.spectrum-wrapper {
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
}
|
}
|
||||||
|
|
|
@ -44,7 +44,9 @@
|
||||||
align-items: stretch;
|
align-items: stretch;
|
||||||
border-bottom: var(--border-light);
|
border-bottom: var(--border-light);
|
||||||
}
|
}
|
||||||
|
.property-group-container:last-child {
|
||||||
|
border-bottom: 0px;
|
||||||
|
}
|
||||||
.property-group-name {
|
.property-group-name {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
|
@ -4,6 +4,8 @@
|
||||||
import Body from "../Typography/Body.svelte"
|
import Body from "../Typography/Body.svelte"
|
||||||
import Heading from "../Typography/Heading.svelte"
|
import Heading from "../Typography/Heading.svelte"
|
||||||
import { setContext } from "svelte"
|
import { setContext } from "svelte"
|
||||||
|
import { createEventDispatcher } from "svelte"
|
||||||
|
import { generate } from "shortid"
|
||||||
|
|
||||||
export let title
|
export let title
|
||||||
export let fillWidth
|
export let fillWidth
|
||||||
|
@ -11,13 +13,17 @@
|
||||||
export let width = "calc(100% - 626px)"
|
export let width = "calc(100% - 626px)"
|
||||||
export let headless = false
|
export let headless = false
|
||||||
|
|
||||||
|
const dispatch = createEventDispatcher()
|
||||||
|
|
||||||
let visible = false
|
let visible = false
|
||||||
|
let drawerId = generate()
|
||||||
|
|
||||||
export function show() {
|
export function show() {
|
||||||
if (visible) {
|
if (visible) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
visible = true
|
visible = true
|
||||||
|
dispatch("drawerShow", drawerId)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function hide() {
|
export function hide() {
|
||||||
|
@ -25,6 +31,7 @@
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
visible = false
|
visible = false
|
||||||
|
dispatch("drawerHide", drawerId)
|
||||||
}
|
}
|
||||||
|
|
||||||
setContext("drawer-actions", {
|
setContext("drawer-actions", {
|
||||||
|
|
|
@ -2,8 +2,8 @@
|
||||||
import "@spectrum-css/inputgroup/dist/index-vars.css"
|
import "@spectrum-css/inputgroup/dist/index-vars.css"
|
||||||
import "@spectrum-css/popover/dist/index-vars.css"
|
import "@spectrum-css/popover/dist/index-vars.css"
|
||||||
import "@spectrum-css/menu/dist/index-vars.css"
|
import "@spectrum-css/menu/dist/index-vars.css"
|
||||||
import { fly } from "svelte/transition"
|
|
||||||
import { createEventDispatcher } from "svelte"
|
import { createEventDispatcher } from "svelte"
|
||||||
|
import clickOutside from "../../Actions/click_outside"
|
||||||
|
|
||||||
export let value = null
|
export let value = null
|
||||||
export let id = null
|
export let id = null
|
||||||
|
@ -80,10 +80,11 @@
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
{#if open}
|
{#if open}
|
||||||
<div class="overlay" on:mousedown|self={() => (open = false)} />
|
|
||||||
<div
|
<div
|
||||||
transition:fly|local={{ y: -20, duration: 200 }}
|
|
||||||
class="spectrum-Popover spectrum-Popover--bottom is-open"
|
class="spectrum-Popover spectrum-Popover--bottom is-open"
|
||||||
|
use:clickOutside={() => {
|
||||||
|
open = false
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<ul class="spectrum-Menu" role="listbox">
|
<ul class="spectrum-Menu" role="listbox">
|
||||||
{#if options && Array.isArray(options)}
|
{#if options && Array.isArray(options)}
|
||||||
|
@ -125,14 +126,6 @@
|
||||||
.spectrum-Textfield-input {
|
.spectrum-Textfield-input {
|
||||||
width: 0;
|
width: 0;
|
||||||
}
|
}
|
||||||
.overlay {
|
|
||||||
position: fixed;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
width: 100vw;
|
|
||||||
height: 100vh;
|
|
||||||
z-index: 999;
|
|
||||||
}
|
|
||||||
.spectrum-Popover {
|
.spectrum-Popover {
|
||||||
max-height: 240px;
|
max-height: 240px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|
|
@ -23,6 +23,10 @@
|
||||||
export let animate = true
|
export let animate = true
|
||||||
export let customZindex
|
export let customZindex
|
||||||
|
|
||||||
|
export let handlePostionUpdate
|
||||||
|
export let showPopover = true
|
||||||
|
export let clickOutsideOverride = false
|
||||||
|
|
||||||
$: target = portalTarget || getContext(Context.PopoverRoot) || ".spectrum"
|
$: target = portalTarget || getContext(Context.PopoverRoot) || ".spectrum"
|
||||||
|
|
||||||
export const show = () => {
|
export const show = () => {
|
||||||
|
@ -35,7 +39,18 @@
|
||||||
open = false
|
open = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const toggle = () => {
|
||||||
|
if (!open) {
|
||||||
|
show()
|
||||||
|
} else {
|
||||||
|
hide()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const handleOutsideClick = e => {
|
const handleOutsideClick = e => {
|
||||||
|
if (clickOutsideOverride) {
|
||||||
|
return
|
||||||
|
}
|
||||||
if (open) {
|
if (open) {
|
||||||
// Stop propagation if the source is the anchor
|
// Stop propagation if the source is the anchor
|
||||||
let node = e.target
|
let node = e.target
|
||||||
|
@ -54,6 +69,9 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleEscape(e) {
|
function handleEscape(e) {
|
||||||
|
if (!clickOutsideOverride) {
|
||||||
|
return
|
||||||
|
}
|
||||||
if (open && e.key === "Escape") {
|
if (open && e.key === "Escape") {
|
||||||
hide()
|
hide()
|
||||||
}
|
}
|
||||||
|
@ -71,6 +89,7 @@
|
||||||
maxWidth,
|
maxWidth,
|
||||||
useAnchorWidth,
|
useAnchorWidth,
|
||||||
offset,
|
offset,
|
||||||
|
customUpdate: handlePostionUpdate,
|
||||||
}}
|
}}
|
||||||
use:clickOutside={{
|
use:clickOutside={{
|
||||||
callback: dismissible ? handleOutsideClick : () => {},
|
callback: dismissible ? handleOutsideClick : () => {},
|
||||||
|
@ -79,6 +98,7 @@
|
||||||
on:keydown={handleEscape}
|
on:keydown={handleEscape}
|
||||||
class="spectrum-Popover is-open"
|
class="spectrum-Popover is-open"
|
||||||
class:customZindex
|
class:customZindex
|
||||||
|
class:hide-popover={open && !showPopover}
|
||||||
role="presentation"
|
role="presentation"
|
||||||
style="height: {customHeight}; --customZindex: {customZindex};"
|
style="height: {customHeight}; --customZindex: {customZindex};"
|
||||||
transition:fly|local={{ y: -20, duration: animate ? 200 : 0 }}
|
transition:fly|local={{ y: -20, duration: animate ? 200 : 0 }}
|
||||||
|
@ -89,6 +109,10 @@
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
.hide-popover {
|
||||||
|
display: contents;
|
||||||
|
}
|
||||||
|
|
||||||
.spectrum-Popover {
|
.spectrum-Popover {
|
||||||
min-width: var(--spectrum-global-dimension-size-2000);
|
min-width: var(--spectrum-global-dimension-size-2000);
|
||||||
border-color: var(--spectrum-global-color-gray-300);
|
border-color: var(--spectrum-global-color-gray-300);
|
||||||
|
|
|
@ -215,7 +215,7 @@
|
||||||
const nameA = getDisplayName(a)
|
const nameA = getDisplayName(a)
|
||||||
const nameB = getDisplayName(b)
|
const nameB = getDisplayName(b)
|
||||||
if (orderA !== orderB) {
|
if (orderA !== orderB) {
|
||||||
return orderA < orderB ? orderA : orderB
|
return orderA < orderB ? a : b
|
||||||
}
|
}
|
||||||
return nameA < nameB ? a : b
|
return nameA < nameB ? a : b
|
||||||
})
|
})
|
||||||
|
|
|
@ -133,9 +133,7 @@
|
||||||
"dependsOn": [
|
"dependsOn": [
|
||||||
{
|
{
|
||||||
"projects": [
|
"projects": [
|
||||||
"@budibase/shared-core",
|
"@budibase/string-templates"
|
||||||
"@budibase/string-templates",
|
|
||||||
"@budibase/types"
|
|
||||||
],
|
],
|
||||||
"target": "build"
|
"target": "build"
|
||||||
}
|
}
|
||||||
|
@ -145,9 +143,7 @@
|
||||||
"dependsOn": [
|
"dependsOn": [
|
||||||
{
|
{
|
||||||
"projects": [
|
"projects": [
|
||||||
"@budibase/shared-core",
|
"@budibase/string-templates"
|
||||||
"@budibase/string-templates",
|
|
||||||
"@budibase/types"
|
|
||||||
],
|
],
|
||||||
"target": "build"
|
"target": "build"
|
||||||
}
|
}
|
||||||
|
@ -157,9 +153,7 @@
|
||||||
"dependsOn": [
|
"dependsOn": [
|
||||||
{
|
{
|
||||||
"projects": [
|
"projects": [
|
||||||
"@budibase/shared-core",
|
"@budibase/string-templates"
|
||||||
"@budibase/string-templates",
|
|
||||||
"@budibase/types"
|
|
||||||
],
|
],
|
||||||
"target": "build"
|
"target": "build"
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,7 +6,7 @@ import {
|
||||||
findComponentPath,
|
findComponentPath,
|
||||||
getComponentSettings,
|
getComponentSettings,
|
||||||
} from "./componentUtils"
|
} from "./componentUtils"
|
||||||
import { store } from "builderStore"
|
import { store, currentAsset } from "builderStore"
|
||||||
import {
|
import {
|
||||||
queries as queriesStores,
|
queries as queriesStores,
|
||||||
tables as tablesStore,
|
tables as tablesStore,
|
||||||
|
@ -22,6 +22,7 @@ import { TableNames } from "../constants"
|
||||||
import { JSONUtils } from "@budibase/frontend-core"
|
import { JSONUtils } from "@budibase/frontend-core"
|
||||||
import ActionDefinitions from "components/design/settings/controls/ButtonActionEditor/manifest.json"
|
import ActionDefinitions from "components/design/settings/controls/ButtonActionEditor/manifest.json"
|
||||||
import { environment, licensing } from "stores/portal"
|
import { environment, licensing } from "stores/portal"
|
||||||
|
import { convertOldFieldFormat } from "components/design/settings/controls/FieldConfiguration/utils"
|
||||||
|
|
||||||
// Regex to match all instances of template strings
|
// Regex to match all instances of template strings
|
||||||
const CAPTURE_VAR_INSIDE_TEMPLATE = /{{([^}]+)}}/g
|
const CAPTURE_VAR_INSIDE_TEMPLATE = /{{([^}]+)}}/g
|
||||||
|
@ -328,7 +329,7 @@ const getProviderContextBindings = (asset, dataProviders) => {
|
||||||
if (context.type === "form") {
|
if (context.type === "form") {
|
||||||
// Forms do not need table schemas
|
// Forms do not need table schemas
|
||||||
// Their schemas are built from their component field names
|
// Their schemas are built from their component field names
|
||||||
schema = buildFormSchema(component)
|
schema = buildFormSchema(component, asset)
|
||||||
readablePrefix = "Fields"
|
readablePrefix = "Fields"
|
||||||
} else if (context.type === "static") {
|
} else if (context.type === "static") {
|
||||||
// Static contexts are fully defined by the components
|
// Static contexts are fully defined by the components
|
||||||
|
@ -370,6 +371,11 @@ const getProviderContextBindings = (asset, dataProviders) => {
|
||||||
if (runtimeSuffix) {
|
if (runtimeSuffix) {
|
||||||
providerId += `-${runtimeSuffix}`
|
providerId += `-${runtimeSuffix}`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!filterCategoryByContext(component, context)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
const safeComponentId = makePropSafe(providerId)
|
const safeComponentId = makePropSafe(providerId)
|
||||||
|
|
||||||
// Create bindable properties for each schema field
|
// Create bindable properties for each schema field
|
||||||
|
@ -387,6 +393,12 @@ const getProviderContextBindings = (asset, dataProviders) => {
|
||||||
}
|
}
|
||||||
readableBinding += `.${fieldSchema.name || key}`
|
readableBinding += `.${fieldSchema.name || key}`
|
||||||
|
|
||||||
|
const bindingCategory = getComponentBindingCategory(
|
||||||
|
component,
|
||||||
|
context,
|
||||||
|
def
|
||||||
|
)
|
||||||
|
|
||||||
// Create the binding object
|
// Create the binding object
|
||||||
bindings.push({
|
bindings.push({
|
||||||
type: "context",
|
type: "context",
|
||||||
|
@ -399,8 +411,8 @@ const getProviderContextBindings = (asset, dataProviders) => {
|
||||||
// Table ID is used by JSON fields to know what table the field is in
|
// Table ID is used by JSON fields to know what table the field is in
|
||||||
tableId: table?._id,
|
tableId: table?._id,
|
||||||
component: component._component,
|
component: component._component,
|
||||||
category: component._instanceName,
|
category: bindingCategory.category,
|
||||||
icon: def.icon,
|
icon: bindingCategory.icon,
|
||||||
display: {
|
display: {
|
||||||
name: fieldSchema.name || key,
|
name: fieldSchema.name || key,
|
||||||
type: fieldSchema.type,
|
type: fieldSchema.type,
|
||||||
|
@ -413,6 +425,40 @@ const getProviderContextBindings = (asset, dataProviders) => {
|
||||||
return bindings
|
return bindings
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Exclude a data context based on the component settings
|
||||||
|
const filterCategoryByContext = (component, context) => {
|
||||||
|
const { _component } = component
|
||||||
|
if (_component.endsWith("formblock")) {
|
||||||
|
if (
|
||||||
|
(component.actionType == "Create" && context.type === "schema") ||
|
||||||
|
(component.actionType == "View" && context.type === "form")
|
||||||
|
) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
const getComponentBindingCategory = (component, context, def) => {
|
||||||
|
let icon = def.icon
|
||||||
|
let category = component._instanceName
|
||||||
|
|
||||||
|
if (component._component.endsWith("formblock")) {
|
||||||
|
let contextCategorySuffix = {
|
||||||
|
form: "Fields",
|
||||||
|
schema: "Row",
|
||||||
|
}
|
||||||
|
category = `${component._instanceName} - ${
|
||||||
|
contextCategorySuffix[context.type]
|
||||||
|
}`
|
||||||
|
icon = context.type === "form" ? "Form" : "Data"
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
icon,
|
||||||
|
category,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets all bindable properties from the logged in user.
|
* Gets all bindable properties from the logged in user.
|
||||||
*/
|
*/
|
||||||
|
@ -507,6 +553,7 @@ const getSelectedRowsBindings = asset => {
|
||||||
)}.${makePropSafe("selectedRows")}`,
|
)}.${makePropSafe("selectedRows")}`,
|
||||||
readableBinding: `${block._instanceName}.Selected rows`,
|
readableBinding: `${block._instanceName}.Selected rows`,
|
||||||
category: "Selected rows",
|
category: "Selected rows",
|
||||||
|
icon: "ViewRow",
|
||||||
display: { name: block._instanceName },
|
display: { name: block._instanceName },
|
||||||
}))
|
}))
|
||||||
)
|
)
|
||||||
|
@ -582,24 +629,36 @@ const getRoleBindings = () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets all bindable properties exposed in an event action flow up until
|
* Gets all bindable event context properties provided in the component
|
||||||
* the specified action ID, as well as context provided for the action
|
* setting
|
||||||
* setting as a whole by the component.
|
|
||||||
*/
|
*/
|
||||||
export const getEventContextBindings = (
|
export const getEventContextBindings = ({
|
||||||
asset,
|
|
||||||
componentId,
|
|
||||||
settingKey,
|
settingKey,
|
||||||
actions,
|
componentInstance,
|
||||||
actionId
|
componentId,
|
||||||
) => {
|
componentDefinition,
|
||||||
|
asset,
|
||||||
|
}) => {
|
||||||
let bindings = []
|
let bindings = []
|
||||||
|
|
||||||
|
const selectedAsset = asset ?? get(currentAsset)
|
||||||
|
|
||||||
// Check if any context bindings are provided by the component for this
|
// Check if any context bindings are provided by the component for this
|
||||||
// setting
|
// setting
|
||||||
const component = findComponent(asset.props, componentId)
|
const component =
|
||||||
const def = store.actions.components.getDefinition(component?._component)
|
componentInstance ?? findComponent(selectedAsset.props, componentId)
|
||||||
|
|
||||||
|
if (!component) {
|
||||||
|
return bindings
|
||||||
|
}
|
||||||
|
|
||||||
|
const definition =
|
||||||
|
componentDefinition ??
|
||||||
|
store.actions.components.getDefinition(component?._component)
|
||||||
|
|
||||||
const settings = getComponentSettings(component?._component)
|
const settings = getComponentSettings(component?._component)
|
||||||
const eventSetting = settings.find(setting => setting.key === settingKey)
|
const eventSetting = settings.find(setting => setting.key === settingKey)
|
||||||
|
|
||||||
if (eventSetting?.context?.length) {
|
if (eventSetting?.context?.length) {
|
||||||
eventSetting.context.forEach(contextEntry => {
|
eventSetting.context.forEach(contextEntry => {
|
||||||
bindings.push({
|
bindings.push({
|
||||||
|
@ -608,14 +667,23 @@ export const getEventContextBindings = (
|
||||||
contextEntry.key
|
contextEntry.key
|
||||||
)}`,
|
)}`,
|
||||||
category: component._instanceName,
|
category: component._instanceName,
|
||||||
icon: def.icon,
|
icon: definition.icon,
|
||||||
display: {
|
display: {
|
||||||
name: contextEntry.label,
|
name: contextEntry.label,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
return bindings
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets all bindable properties exposed in an event action flow up until
|
||||||
|
* the specified action ID, as well as context provided for the action
|
||||||
|
* setting as a whole by the component.
|
||||||
|
*/
|
||||||
|
export const getActionBindings = (actions, actionId) => {
|
||||||
|
let bindings = []
|
||||||
// Get the steps leading up to this value
|
// Get the steps leading up to this value
|
||||||
const index = actions?.findIndex(action => action.id === actionId)
|
const index = actions?.findIndex(action => action.id === actionId)
|
||||||
if (index == null || index === -1) {
|
if (index == null || index === -1) {
|
||||||
|
@ -642,7 +710,6 @@ export const getEventContextBindings = (
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
return bindings
|
return bindings
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -835,18 +902,36 @@ export const getSchemaForDatasource = (asset, datasource, options) => {
|
||||||
* Builds a form schema given a form component.
|
* Builds a form schema given a form component.
|
||||||
* A form schema is a schema of all the fields nested anywhere within a form.
|
* A form schema is a schema of all the fields nested anywhere within a form.
|
||||||
*/
|
*/
|
||||||
export const buildFormSchema = component => {
|
export const buildFormSchema = (component, asset) => {
|
||||||
let schema = {}
|
let schema = {}
|
||||||
if (!component) {
|
if (!component) {
|
||||||
return schema
|
return schema
|
||||||
}
|
}
|
||||||
|
|
||||||
// If this is a form block, simply use the fields setting
|
|
||||||
if (component._component.endsWith("formblock")) {
|
if (component._component.endsWith("formblock")) {
|
||||||
let schema = {}
|
let schema = {}
|
||||||
component.fields?.forEach(field => {
|
|
||||||
schema[field] = { type: "string" }
|
const datasource = getDatasourceForProvider(asset, component)
|
||||||
|
const info = getSchemaForDatasource(component, datasource)
|
||||||
|
|
||||||
|
if (!component.fields) {
|
||||||
|
Object.values(info?.schema)
|
||||||
|
.filter(
|
||||||
|
({ autocolumn, name }) =>
|
||||||
|
!autocolumn && !["_rev", "_id"].includes(name)
|
||||||
|
)
|
||||||
|
.forEach(({ name }) => {
|
||||||
|
schema[name] = { type: info?.schema[name].type }
|
||||||
})
|
})
|
||||||
|
} else {
|
||||||
|
// Field conversion
|
||||||
|
const patched = convertOldFieldFormat(component.fields || [])
|
||||||
|
patched?.forEach(({ field, active }) => {
|
||||||
|
if (!active) return
|
||||||
|
schema[field] = { type: info?.schema[field].type }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
return schema
|
return schema
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -862,7 +947,7 @@ export const buildFormSchema = component => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
component._children?.forEach(child => {
|
component._children?.forEach(child => {
|
||||||
const childSchema = buildFormSchema(child)
|
const childSchema = buildFormSchema(child, asset)
|
||||||
schema = { ...schema, ...childSchema }
|
schema = { ...schema, ...childSchema }
|
||||||
})
|
})
|
||||||
return schema
|
return schema
|
||||||
|
|
|
@ -4,7 +4,7 @@ import { getTemporalStore } from "./store/temporal"
|
||||||
import { getThemeStore } from "./store/theme"
|
import { getThemeStore } from "./store/theme"
|
||||||
import { getUserStore } from "./store/users"
|
import { getUserStore } from "./store/users"
|
||||||
import { getDeploymentStore } from "./store/deployments"
|
import { getDeploymentStore } from "./store/deployments"
|
||||||
import { derived } from "svelte/store"
|
import { derived, writable } from "svelte/store"
|
||||||
import { findComponent, findComponentPath } from "./componentUtils"
|
import { findComponent, findComponentPath } from "./componentUtils"
|
||||||
import { RoleUtils } from "@budibase/frontend-core"
|
import { RoleUtils } from "@budibase/frontend-core"
|
||||||
import { createHistoryStore } from "builderStore/store/history"
|
import { createHistoryStore } from "builderStore/store/history"
|
||||||
|
@ -61,6 +61,12 @@ export const selectedLayout = derived(store, $store => {
|
||||||
export const selectedComponent = derived(
|
export const selectedComponent = derived(
|
||||||
[store, selectedScreen],
|
[store, selectedScreen],
|
||||||
([$store, $selectedScreen]) => {
|
([$store, $selectedScreen]) => {
|
||||||
|
if (
|
||||||
|
$selectedScreen &&
|
||||||
|
$store.selectedComponentId?.startsWith(`${$selectedScreen._id}-`)
|
||||||
|
) {
|
||||||
|
return $selectedScreen?.props
|
||||||
|
}
|
||||||
if (!$selectedScreen || !$store.selectedComponentId) {
|
if (!$selectedScreen || !$store.selectedComponentId) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
@ -141,3 +147,5 @@ export const userSelectedResourceMap = derived(userStore, $userStore => {
|
||||||
export const isOnlyUser = derived(userStore, $userStore => {
|
export const isOnlyUser = derived(userStore, $userStore => {
|
||||||
return $userStore.length < 2
|
return $userStore.length < 2
|
||||||
})
|
})
|
||||||
|
|
||||||
|
export const screensHeight = writable("210px")
|
||||||
|
|
|
@ -111,6 +111,7 @@ export const getFrontendStore = () => {
|
||||||
}
|
}
|
||||||
let clone = cloneDeep(screen)
|
let clone = cloneDeep(screen)
|
||||||
const result = patchFn(clone)
|
const result = patchFn(clone)
|
||||||
|
|
||||||
if (result === false) {
|
if (result === false) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -225,7 +226,6 @@ export const getFrontendStore = () => {
|
||||||
// Select new screen
|
// Select new screen
|
||||||
store.update(state => {
|
store.update(state => {
|
||||||
state.selectedScreenId = screen._id
|
state.selectedScreenId = screen._id
|
||||||
state.selectedComponentId = screen.props?._id
|
|
||||||
return state
|
return state
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
@ -769,9 +769,13 @@ export const getFrontendStore = () => {
|
||||||
else {
|
else {
|
||||||
await store.actions.screens.patch(screen => {
|
await store.actions.screens.patch(screen => {
|
||||||
// Find the selected component
|
// Find the selected component
|
||||||
|
let selectedComponentId = state.selectedComponentId
|
||||||
|
if (selectedComponentId.startsWith(`${screen._id}-`)) {
|
||||||
|
selectedComponentId = screen?.props._id
|
||||||
|
}
|
||||||
const currentComponent = findComponent(
|
const currentComponent = findComponent(
|
||||||
screen.props,
|
screen.props,
|
||||||
state.selectedComponentId
|
selectedComponentId
|
||||||
)
|
)
|
||||||
if (!currentComponent) {
|
if (!currentComponent) {
|
||||||
return false
|
return false
|
||||||
|
@ -834,6 +838,7 @@ export const getFrontendStore = () => {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const patchScreen = screen => {
|
const patchScreen = screen => {
|
||||||
|
// findComponent looks in the tree not comp.settings[0]
|
||||||
let component = findComponent(screen.props, componentId)
|
let component = findComponent(screen.props, componentId)
|
||||||
if (!component) {
|
if (!component) {
|
||||||
return false
|
return false
|
||||||
|
@ -994,12 +999,20 @@ export const getFrontendStore = () => {
|
||||||
const componentId = state.selectedComponentId
|
const componentId = state.selectedComponentId
|
||||||
const screen = get(selectedScreen)
|
const screen = get(selectedScreen)
|
||||||
const parent = findComponentParent(screen.props, componentId)
|
const parent = findComponentParent(screen.props, componentId)
|
||||||
|
|
||||||
// Check we aren't right at the top of the tree
|
|
||||||
const index = parent?._children.findIndex(x => x._id === componentId)
|
const index = parent?._children.findIndex(x => x._id === componentId)
|
||||||
if (!parent || componentId === screen.props._id) {
|
|
||||||
|
// Check for screen and navigation component edge cases
|
||||||
|
const screenComponentId = `${screen._id}-screen`
|
||||||
|
const navComponentId = `${screen._id}-navigation`
|
||||||
|
if (componentId === screenComponentId) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
if (componentId === navComponentId) {
|
||||||
|
return screenComponentId
|
||||||
|
}
|
||||||
|
if (parent._id === screen.props._id && index === 0) {
|
||||||
|
return navComponentId
|
||||||
|
}
|
||||||
|
|
||||||
// If we have siblings above us, choose the sibling or a descendant
|
// If we have siblings above us, choose the sibling or a descendant
|
||||||
if (index > 0) {
|
if (index > 0) {
|
||||||
|
@ -1021,12 +1034,20 @@ export const getFrontendStore = () => {
|
||||||
return parent._id
|
return parent._id
|
||||||
},
|
},
|
||||||
getNext: () => {
|
getNext: () => {
|
||||||
|
const state = get(store)
|
||||||
const component = get(selectedComponent)
|
const component = get(selectedComponent)
|
||||||
const componentId = component?._id
|
const componentId = component?._id
|
||||||
const screen = get(selectedScreen)
|
const screen = get(selectedScreen)
|
||||||
const parent = findComponentParent(screen.props, componentId)
|
const parent = findComponentParent(screen.props, componentId)
|
||||||
const index = parent?._children.findIndex(x => x._id === componentId)
|
const index = parent?._children.findIndex(x => x._id === componentId)
|
||||||
|
|
||||||
|
// Check for screen and navigation component edge cases
|
||||||
|
const screenComponentId = `${screen._id}-screen`
|
||||||
|
const navComponentId = `${screen._id}-navigation`
|
||||||
|
if (state.selectedComponentId === screenComponentId) {
|
||||||
|
return navComponentId
|
||||||
|
}
|
||||||
|
|
||||||
// If we have children, select first child
|
// If we have children, select first child
|
||||||
if (component._children?.length) {
|
if (component._children?.length) {
|
||||||
return component._children[0]._id
|
return component._children[0]._id
|
||||||
|
@ -1207,7 +1228,12 @@ export const getFrontendStore = () => {
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
updateSetting: async (name, value) => {
|
updateSetting: async (name, value) => {
|
||||||
await store.actions.components.patch(component => {
|
await store.actions.components.patch(
|
||||||
|
store.actions.components.updateComponentSetting(name, value)
|
||||||
|
)
|
||||||
|
},
|
||||||
|
updateComponentSetting: (name, value) => {
|
||||||
|
return component => {
|
||||||
if (!name || !component) {
|
if (!name || !component) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
@ -1235,9 +1261,8 @@ export const getFrontendStore = () => {
|
||||||
component[key] = columnNames
|
component[key] = columnNames
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
component[name] = value
|
component[name] = value
|
||||||
})
|
}
|
||||||
},
|
},
|
||||||
requestEjectBlock: componentId => {
|
requestEjectBlock: componentId => {
|
||||||
store.actions.preview.sendEvent("eject-block", componentId)
|
store.actions.preview.sendEvent("eject-block", componentId)
|
||||||
|
|
|
@ -108,7 +108,10 @@
|
||||||
/****************************************************/
|
/****************************************************/
|
||||||
|
|
||||||
const getInputData = (testData, blockInputs) => {
|
const getInputData = (testData, blockInputs) => {
|
||||||
let newInputData = testData || blockInputs
|
// Test data is not cloned for reactivity
|
||||||
|
let newInputData = testData || cloneDeep(blockInputs)
|
||||||
|
|
||||||
|
// Ensures the app action fields are populated
|
||||||
if (block.event === "app:trigger" && !newInputData?.fields) {
|
if (block.event === "app:trigger" && !newInputData?.fields) {
|
||||||
newInputData = cloneDeep(blockInputs)
|
newInputData = cloneDeep(blockInputs)
|
||||||
}
|
}
|
||||||
|
|
|
@ -50,6 +50,7 @@
|
||||||
type="string"
|
type="string"
|
||||||
{bindings}
|
{bindings}
|
||||||
fillWidth={true}
|
fillWidth={true}
|
||||||
|
updateOnChange={false}
|
||||||
/>
|
/>
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -6,13 +6,15 @@
|
||||||
Select,
|
Select,
|
||||||
Toggle,
|
Toggle,
|
||||||
RadioGroup,
|
RadioGroup,
|
||||||
|
Icon,
|
||||||
DatePicker,
|
DatePicker,
|
||||||
Modal,
|
Modal,
|
||||||
notifications,
|
notifications,
|
||||||
OptionSelectDnD,
|
OptionSelectDnD,
|
||||||
Layout,
|
Layout,
|
||||||
|
AbsTooltip,
|
||||||
} from "@budibase/bbui"
|
} from "@budibase/bbui"
|
||||||
import { createEventDispatcher, getContext } from "svelte"
|
import { createEventDispatcher, getContext, onMount } from "svelte"
|
||||||
import { cloneDeep } from "lodash/fp"
|
import { cloneDeep } from "lodash/fp"
|
||||||
import { tables, datasources } from "stores/backend"
|
import { tables, datasources } from "stores/backend"
|
||||||
import { TableNames, UNEDITABLE_USER_FIELDS } from "constants"
|
import { TableNames, UNEDITABLE_USER_FIELDS } from "constants"
|
||||||
|
@ -47,6 +49,7 @@
|
||||||
|
|
||||||
export let field
|
export let field
|
||||||
|
|
||||||
|
let mounted = false
|
||||||
let fieldDefinitions = cloneDeep(FIELDS)
|
let fieldDefinitions = cloneDeep(FIELDS)
|
||||||
let originalName
|
let originalName
|
||||||
let linkEditDisabled
|
let linkEditDisabled
|
||||||
|
@ -413,16 +416,22 @@
|
||||||
}
|
}
|
||||||
return newError
|
return newError
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
mounted = true
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Layout noPadding gap="S">
|
<Layout noPadding gap="S">
|
||||||
|
{#if mounted}
|
||||||
<Input
|
<Input
|
||||||
|
autofocus
|
||||||
bind:value={editableColumn.name}
|
bind:value={editableColumn.name}
|
||||||
disabled={uneditable ||
|
disabled={uneditable ||
|
||||||
(linkEditDisabled && editableColumn.type === LINK_TYPE)}
|
(linkEditDisabled && editableColumn.type === LINK_TYPE)}
|
||||||
error={errors?.name}
|
error={errors?.name}
|
||||||
/>
|
/>
|
||||||
|
{/if}
|
||||||
<Select
|
<Select
|
||||||
disabled={!typeEnabled}
|
disabled={!typeEnabled}
|
||||||
bind:value={editableColumn.type}
|
bind:value={editableColumn.type}
|
||||||
|
@ -452,12 +461,17 @@
|
||||||
/>
|
/>
|
||||||
{:else if editableColumn.type === "longform"}
|
{:else if editableColumn.type === "longform"}
|
||||||
<div>
|
<div>
|
||||||
<Label
|
<div class="tooltip-alignment">
|
||||||
size="M"
|
<Label size="M">Formatting</Label>
|
||||||
tooltip="Rich text includes support for images, links, tables, lists and more"
|
<AbsTooltip
|
||||||
|
position="top"
|
||||||
|
type="info"
|
||||||
|
text={"Rich text includes support for images, link"}
|
||||||
>
|
>
|
||||||
Formatting
|
<Icon size="XS" name="InfoOutline" />
|
||||||
</Label>
|
</AbsTooltip>
|
||||||
|
</div>
|
||||||
|
|
||||||
<Toggle
|
<Toggle
|
||||||
bind:value={editableColumn.useRichText}
|
bind:value={editableColumn.useRichText}
|
||||||
text="Enable rich text support (markdown)"
|
text="Enable rich text support (markdown)"
|
||||||
|
@ -488,13 +502,18 @@
|
||||||
</div>
|
</div>
|
||||||
{#if datasource?.source !== "ORACLE" && datasource?.source !== "SQL_SERVER"}
|
{#if datasource?.source !== "ORACLE" && datasource?.source !== "SQL_SERVER"}
|
||||||
<div>
|
<div>
|
||||||
<Label
|
<div>
|
||||||
tooltip={isCreating
|
<Label>Time zones</Label>
|
||||||
|
<AbsTooltip
|
||||||
|
position="top"
|
||||||
|
type="info"
|
||||||
|
text={isCreating
|
||||||
? null
|
? null
|
||||||
: "We recommend not changing how timezones are handled for existing columns, as existing data will not be updated"}
|
: "We recommend not changing how timezones are handled for existing columns, as existing data will not be updated"}
|
||||||
>
|
>
|
||||||
Time zones
|
<Icon size="XS" name="InfoOutline" />
|
||||||
</Label>
|
</AbsTooltip>
|
||||||
|
</div>
|
||||||
<Toggle
|
<Toggle
|
||||||
bind:value={editableColumn.ignoreTimezones}
|
bind:value={editableColumn.ignoreTimezones}
|
||||||
text="Ignore time zones"
|
text="Ignore time zones"
|
||||||
|
@ -671,6 +690,12 @@
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.tooltip-alignment {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--spacing-xs);
|
||||||
|
}
|
||||||
|
|
||||||
.label-length {
|
.label-length {
|
||||||
flex-basis: 40%;
|
flex-basis: 40%;
|
||||||
}
|
}
|
||||||
|
|
|
@ -121,7 +121,9 @@
|
||||||
type: "Screen",
|
type: "Screen",
|
||||||
name: screen.routing.route,
|
name: screen.routing.route,
|
||||||
icon: "WebPage",
|
icon: "WebPage",
|
||||||
action: () => $goto(`./design/${screen._id}/components`),
|
action: () => {
|
||||||
|
$goto(`./design/${screen._id}/${screen._id}-screen`)
|
||||||
|
},
|
||||||
})),
|
})),
|
||||||
...($automationStore?.automations?.map(automation => ({
|
...($automationStore?.automations?.map(automation => ({
|
||||||
type: "Automation",
|
type: "Automation",
|
||||||
|
|
|
@ -21,6 +21,7 @@
|
||||||
export let id
|
export let id
|
||||||
export let showTooltip = false
|
export let showTooltip = false
|
||||||
export let selectedBy = null
|
export let selectedBy = null
|
||||||
|
export let compact = false
|
||||||
|
|
||||||
const scrollApi = getContext("scroll")
|
const scrollApi = getContext("scroll")
|
||||||
const dispatch = createEventDispatcher()
|
const dispatch = createEventDispatcher()
|
||||||
|
@ -80,8 +81,9 @@
|
||||||
{#if withArrow}
|
{#if withArrow}
|
||||||
<div
|
<div
|
||||||
class:opened
|
class:opened
|
||||||
class:relative={indentLevel === 0}
|
class:relative={indentLevel === 0 && !compact}
|
||||||
class:absolute={indentLevel > 0}
|
class:absolute={indentLevel > 0 && !compact}
|
||||||
|
class:compact
|
||||||
class="icon arrow"
|
class="icon arrow"
|
||||||
on:click={onIconClick}
|
on:click={onIconClick}
|
||||||
>
|
>
|
||||||
|
@ -194,10 +196,21 @@
|
||||||
padding: 8px;
|
padding: 8px;
|
||||||
margin-left: -8px;
|
margin-left: -8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.compact {
|
||||||
|
position: absolute;
|
||||||
|
left: 6px;
|
||||||
|
padding: 8px;
|
||||||
|
margin-left: -8px;
|
||||||
|
}
|
||||||
.icon.arrow :global(svg) {
|
.icon.arrow :global(svg) {
|
||||||
width: 12px;
|
width: 12px;
|
||||||
height: 12px;
|
height: 12px;
|
||||||
}
|
}
|
||||||
|
.icon.arrow.compact :global(svg) {
|
||||||
|
width: 9px;
|
||||||
|
height: 9px;
|
||||||
|
}
|
||||||
.icon.arrow.relative {
|
.icon.arrow.relative {
|
||||||
position: relative;
|
position: relative;
|
||||||
margin: 0 -6px 0 -4px;
|
margin: 0 -6px 0 -4px;
|
||||||
|
|
|
@ -74,6 +74,8 @@
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
<Drawer
|
<Drawer
|
||||||
|
on:drawerHide
|
||||||
|
on:drawerShow
|
||||||
{fillWidth}
|
{fillWidth}
|
||||||
bind:this={bindingDrawer}
|
bind:this={bindingDrawer}
|
||||||
{title}
|
{title}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
<script>
|
<script>
|
||||||
import { Icon, Heading } from "@budibase/bbui"
|
import { Icon, Body } from "@budibase/bbui"
|
||||||
|
|
||||||
export let title
|
export let title
|
||||||
export let icon
|
export let icon
|
||||||
|
@ -25,7 +25,7 @@
|
||||||
<Icon name={icon} />
|
<Icon name={icon} />
|
||||||
{/if}
|
{/if}
|
||||||
<div class="title">
|
<div class="title">
|
||||||
<Heading size="XXS">{title || ""}</Heading>
|
<Body size="S">{title}</Body>
|
||||||
</div>
|
</div>
|
||||||
{#if showAddButton}
|
{#if showAddButton}
|
||||||
<div class="add-button" on:click={onClickAddButton}>
|
<div class="add-button" on:click={onClickAddButton}>
|
||||||
|
@ -78,15 +78,14 @@
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding: 0 var(--spacing-l);
|
padding: 0 var(--spacing-l);
|
||||||
border-bottom: var(--border-light);
|
border-bottom: var(--border-light);
|
||||||
gap: var(--spacing-l);
|
gap: var(--spacing-m);
|
||||||
}
|
}
|
||||||
.title {
|
.title {
|
||||||
flex: 1 1 auto;
|
flex: 1 1 auto;
|
||||||
width: 0;
|
width: 0;
|
||||||
}
|
}
|
||||||
.title :global(h1) {
|
.title :global(p) {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
font-weight: 600;
|
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,9 +13,9 @@
|
||||||
import { generate } from "shortid"
|
import { generate } from "shortid"
|
||||||
import {
|
import {
|
||||||
getEventContextBindings,
|
getEventContextBindings,
|
||||||
|
getActionBindings,
|
||||||
makeStateBinding,
|
makeStateBinding,
|
||||||
} from "builderStore/dataBinding"
|
} from "builderStore/dataBinding"
|
||||||
import { currentAsset, store } from "builderStore"
|
|
||||||
import { cloneDeep } from "lodash/fp"
|
import { cloneDeep } from "lodash/fp"
|
||||||
|
|
||||||
const flipDurationMs = 150
|
const flipDurationMs = 150
|
||||||
|
@ -26,6 +26,7 @@
|
||||||
export let actions
|
export let actions
|
||||||
export let bindings = []
|
export let bindings = []
|
||||||
export let nested
|
export let nested
|
||||||
|
export let componentInstance
|
||||||
|
|
||||||
let actionQuery
|
let actionQuery
|
||||||
let selectedAction = actions?.length ? actions[0] : null
|
let selectedAction = actions?.length ? actions[0] : null
|
||||||
|
@ -68,15 +69,19 @@
|
||||||
acc[action.type].push(action)
|
acc[action.type].push(action)
|
||||||
return acc
|
return acc
|
||||||
}, {})
|
}, {})
|
||||||
|
|
||||||
// These are ephemeral bindings which only exist while executing actions
|
// These are ephemeral bindings which only exist while executing actions
|
||||||
$: eventContexBindings = getEventContextBindings(
|
$: eventContextBindings = getEventContextBindings({
|
||||||
$currentAsset,
|
componentInstance,
|
||||||
$store.selectedComponentId,
|
settingKey: key,
|
||||||
key,
|
})
|
||||||
actions,
|
$: actionContextBindings = getActionBindings(actions, selectedAction?.id)
|
||||||
selectedAction?.id
|
|
||||||
|
$: allBindings = getAllBindings(
|
||||||
|
bindings,
|
||||||
|
[...eventContextBindings, ...actionContextBindings],
|
||||||
|
actions
|
||||||
)
|
)
|
||||||
$: allBindings = getAllBindings(bindings, eventContexBindings, actions)
|
|
||||||
$: {
|
$: {
|
||||||
// Ensure each action has a unique ID
|
// Ensure each action has a unique ID
|
||||||
if (actions) {
|
if (actions) {
|
||||||
|
|
|
@ -13,6 +13,7 @@
|
||||||
export let name
|
export let name
|
||||||
export let bindings
|
export let bindings
|
||||||
export let nested
|
export let nested
|
||||||
|
export let componentInstance
|
||||||
|
|
||||||
let drawer
|
let drawer
|
||||||
let tmpValue
|
let tmpValue
|
||||||
|
@ -74,7 +75,7 @@
|
||||||
<ActionButton on:click={openDrawer}>{actionText}</ActionButton>
|
<ActionButton on:click={openDrawer}>{actionText}</ActionButton>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Drawer bind:this={drawer} title={"Actions"}>
|
<Drawer bind:this={drawer} title={"Actions"} on:drawerHide on:drawerShow>
|
||||||
<svelte:fragment slot="description">
|
<svelte:fragment slot="description">
|
||||||
Define what actions to run.
|
Define what actions to run.
|
||||||
</svelte:fragment>
|
</svelte:fragment>
|
||||||
|
@ -86,6 +87,7 @@
|
||||||
{bindings}
|
{bindings}
|
||||||
{key}
|
{key}
|
||||||
{nested}
|
{nested}
|
||||||
|
{componentInstance}
|
||||||
/>
|
/>
|
||||||
</Drawer>
|
</Drawer>
|
||||||
|
|
||||||
|
|
|
@ -1,10 +1,12 @@
|
||||||
<script>
|
<script>
|
||||||
import { Select, Label, Stepper } from "@budibase/bbui"
|
import { Select, Label } from "@budibase/bbui"
|
||||||
import { currentAsset, store } from "builderStore"
|
import { currentAsset, store } from "builderStore"
|
||||||
import { getActionProviderComponents } from "builderStore/dataBinding"
|
import { getActionProviderComponents } from "builderStore/dataBinding"
|
||||||
import { onMount } from "svelte"
|
import { onMount } from "svelte"
|
||||||
|
import DrawerBindableInput from "components/common/bindings/DrawerBindableInput.svelte"
|
||||||
|
|
||||||
export let parameters
|
export let parameters
|
||||||
|
export let bindings = []
|
||||||
|
|
||||||
$: actionProviders = getActionProviderComponents(
|
$: actionProviders = getActionProviderComponents(
|
||||||
$currentAsset,
|
$currentAsset,
|
||||||
|
@ -51,7 +53,11 @@
|
||||||
<Select bind:value={parameters.type} options={typeOptions} />
|
<Select bind:value={parameters.type} options={typeOptions} />
|
||||||
{#if parameters.type === "specific"}
|
{#if parameters.type === "specific"}
|
||||||
<Label small>Number</Label>
|
<Label small>Number</Label>
|
||||||
<Stepper bind:value={parameters.number} />
|
<DrawerBindableInput
|
||||||
|
{bindings}
|
||||||
|
value={parameters.number}
|
||||||
|
on:change={e => (parameters.number = e.detail)}
|
||||||
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
@ -42,7 +42,6 @@
|
||||||
<ColorPicker
|
<ColorPicker
|
||||||
value={column.background}
|
value={column.background}
|
||||||
on:change={e => (column.background = e.detail)}
|
on:change={e => (column.background = e.detail)}
|
||||||
alignRight
|
|
||||||
spectrumTheme={$store.theme}
|
spectrumTheme={$store.theme}
|
||||||
/>
|
/>
|
||||||
</Layout>
|
</Layout>
|
||||||
|
@ -51,7 +50,6 @@
|
||||||
<ColorPicker
|
<ColorPicker
|
||||||
value={column.color}
|
value={column.color}
|
||||||
on:change={e => (column.color = e.detail)}
|
on:change={e => (column.color = e.detail)}
|
||||||
alignRight
|
|
||||||
spectrumTheme={$store.theme}
|
spectrumTheme={$store.theme}
|
||||||
/>
|
/>
|
||||||
</Layout>
|
</Layout>
|
||||||
|
|
|
@ -0,0 +1,154 @@
|
||||||
|
<script>
|
||||||
|
import { Icon } from "@budibase/bbui"
|
||||||
|
import { dndzone } from "svelte-dnd-action"
|
||||||
|
import { createEventDispatcher } from "svelte"
|
||||||
|
import { generate } from "shortid"
|
||||||
|
import { setContext } from "svelte"
|
||||||
|
import { writable } from "svelte/store"
|
||||||
|
|
||||||
|
export let items = []
|
||||||
|
export let showHandle = true
|
||||||
|
export let listType
|
||||||
|
export let listTypeProps = {}
|
||||||
|
export let listItemKey
|
||||||
|
export let draggable = true
|
||||||
|
|
||||||
|
let store = writable({
|
||||||
|
selected: null,
|
||||||
|
actions: {
|
||||||
|
select: id => {
|
||||||
|
store.update(state => ({
|
||||||
|
...state,
|
||||||
|
selected: id,
|
||||||
|
}))
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
setContext("draggable", store)
|
||||||
|
|
||||||
|
const dispatch = createEventDispatcher()
|
||||||
|
const flipDurationMs = 150
|
||||||
|
|
||||||
|
let anchors = {}
|
||||||
|
let draggableItems = []
|
||||||
|
|
||||||
|
const buildDragable = items => {
|
||||||
|
return items.map(item => {
|
||||||
|
return {
|
||||||
|
id: listItemKey ? item[listItemKey] : generate(),
|
||||||
|
item,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
$: if (items) {
|
||||||
|
draggableItems = buildDragable(items)
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateRowOrder = e => {
|
||||||
|
draggableItems = e.detail.items
|
||||||
|
}
|
||||||
|
|
||||||
|
const serialiseUpdate = () => {
|
||||||
|
return draggableItems.reduce((acc, ele) => {
|
||||||
|
acc.push(ele.item)
|
||||||
|
return acc
|
||||||
|
}, [])
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleFinalize = e => {
|
||||||
|
updateRowOrder(e)
|
||||||
|
dispatch("change", serialiseUpdate())
|
||||||
|
}
|
||||||
|
|
||||||
|
const onItemChanged = e => {
|
||||||
|
dispatch("itemChange", e.detail)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<ul
|
||||||
|
class="list-wrap"
|
||||||
|
use:dndzone={{
|
||||||
|
items: draggableItems,
|
||||||
|
flipDurationMs,
|
||||||
|
dropTargetStyle: { outline: "none" },
|
||||||
|
dragDisabled: !draggable,
|
||||||
|
}}
|
||||||
|
on:finalize={handleFinalize}
|
||||||
|
on:consider={updateRowOrder}
|
||||||
|
>
|
||||||
|
{#each draggableItems as draggable (draggable.id)}
|
||||||
|
<li
|
||||||
|
bind:this={anchors[draggable.id]}
|
||||||
|
class:highlighted={draggable.id === $store.selected}
|
||||||
|
>
|
||||||
|
<div class="left-content">
|
||||||
|
{#if showHandle}
|
||||||
|
<div class="handle" aria-label="drag-handle">
|
||||||
|
<Icon name="DragHandle" size="XL" />
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
<div class="right-content">
|
||||||
|
<svelte:component
|
||||||
|
this={listType}
|
||||||
|
anchor={anchors[draggable.item._id]}
|
||||||
|
item={draggable.item}
|
||||||
|
{...listTypeProps}
|
||||||
|
on:change={onItemChanged}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
{/each}
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.list-wrap {
|
||||||
|
list-style-type: none;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
width: 100%;
|
||||||
|
border-radius: 4px;
|
||||||
|
background-color: var(
|
||||||
|
--spectrum-table-background-color,
|
||||||
|
var(--spectrum-global-color-gray-50)
|
||||||
|
);
|
||||||
|
border: 1px solid
|
||||||
|
var(--spectrum-table-border-color, var(--spectrum-alias-border-color-mid));
|
||||||
|
}
|
||||||
|
.list-wrap > li {
|
||||||
|
background-color: var(
|
||||||
|
--spectrum-table-background-color,
|
||||||
|
var(--spectrum-global-color-gray-50)
|
||||||
|
);
|
||||||
|
transition: background-color ease-in-out 130ms;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
border-bottom: 1px solid
|
||||||
|
var(--spectrum-table-border-color, var(--spectrum-alias-border-color-mid));
|
||||||
|
}
|
||||||
|
.list-wrap > li:hover,
|
||||||
|
li.highlighted {
|
||||||
|
background-color: var(
|
||||||
|
--spectrum-table-row-background-color-hover,
|
||||||
|
var(--spectrum-alias-highlight-hover)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
.list-wrap > li:first-child {
|
||||||
|
border-top-left-radius: 4px;
|
||||||
|
border-top-right-radius: 4px;
|
||||||
|
}
|
||||||
|
.list-wrap > li:last-child {
|
||||||
|
border-top-left-radius: var(--spectrum-table-regular-border-radius);
|
||||||
|
border-top-right-radius: var(--spectrum-table-regular-border-radius);
|
||||||
|
}
|
||||||
|
.right-content {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
.list-wrap li {
|
||||||
|
padding-left: var(--spacing-s);
|
||||||
|
padding-right: var(--spacing-s);
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,160 @@
|
||||||
|
<script>
|
||||||
|
import { Icon, Popover, Layout } from "@budibase/bbui"
|
||||||
|
import { store } from "builderStore"
|
||||||
|
import { cloneDeep } from "lodash/fp"
|
||||||
|
import { createEventDispatcher } from "svelte"
|
||||||
|
import ComponentSettingsSection from "../../../../../pages/builder/app/[application]/design/[screenId]/[componentId]/_components/Component/ComponentSettingsSection.svelte"
|
||||||
|
import { getContext } from "svelte"
|
||||||
|
|
||||||
|
export let anchor
|
||||||
|
export let field
|
||||||
|
export let componentBindings
|
||||||
|
export let bindings
|
||||||
|
|
||||||
|
const draggable = getContext("draggable")
|
||||||
|
const dispatch = createEventDispatcher()
|
||||||
|
|
||||||
|
let popover
|
||||||
|
let drawers = []
|
||||||
|
let pseudoComponentInstance
|
||||||
|
let open = false
|
||||||
|
|
||||||
|
$: if (open && $draggable.selected && $draggable.selected != field._id) {
|
||||||
|
popover.hide()
|
||||||
|
}
|
||||||
|
|
||||||
|
$: if (field) {
|
||||||
|
pseudoComponentInstance = field
|
||||||
|
}
|
||||||
|
$: componentDef = store.actions.components.getDefinition(
|
||||||
|
pseudoComponentInstance._component
|
||||||
|
)
|
||||||
|
$: parsedComponentDef = processComponentDefinitionSettings(componentDef)
|
||||||
|
|
||||||
|
const processComponentDefinitionSettings = componentDef => {
|
||||||
|
if (!componentDef) {
|
||||||
|
return {}
|
||||||
|
}
|
||||||
|
const clone = cloneDeep(componentDef)
|
||||||
|
const updatedSettings = clone.settings
|
||||||
|
.filter(setting => setting.key !== "field")
|
||||||
|
.map(setting => {
|
||||||
|
return { ...setting, nested: true }
|
||||||
|
})
|
||||||
|
clone.settings = updatedSettings
|
||||||
|
return clone
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateSetting = async (setting, value) => {
|
||||||
|
const nestedComponentInstance = cloneDeep(pseudoComponentInstance)
|
||||||
|
|
||||||
|
const patchFn = store.actions.components.updateComponentSetting(
|
||||||
|
setting.key,
|
||||||
|
value
|
||||||
|
)
|
||||||
|
patchFn(nestedComponentInstance)
|
||||||
|
|
||||||
|
const update = {
|
||||||
|
...nestedComponentInstance,
|
||||||
|
active: pseudoComponentInstance.active,
|
||||||
|
}
|
||||||
|
|
||||||
|
dispatch("change", update)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Icon
|
||||||
|
name="Settings"
|
||||||
|
hoverable
|
||||||
|
size="S"
|
||||||
|
on:click={() => {
|
||||||
|
if (!open) {
|
||||||
|
popover.show()
|
||||||
|
open = true
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Popover
|
||||||
|
bind:this={popover}
|
||||||
|
on:open={() => {
|
||||||
|
drawers = []
|
||||||
|
$draggable.actions.select(field._id)
|
||||||
|
}}
|
||||||
|
on:close={() => {
|
||||||
|
open = false
|
||||||
|
if ($draggable.selected == field._id) {
|
||||||
|
$draggable.actions.select()
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
{anchor}
|
||||||
|
align="left-outside"
|
||||||
|
showPopover={drawers.length == 0}
|
||||||
|
clickOutsideOverride={drawers.length > 0}
|
||||||
|
maxHeight={600}
|
||||||
|
handlePostionUpdate={(anchorBounds, eleBounds, cfg) => {
|
||||||
|
let { left, top } = cfg
|
||||||
|
let percentageOffset = 30
|
||||||
|
// left-outside
|
||||||
|
left = anchorBounds.left - eleBounds.width - 18
|
||||||
|
|
||||||
|
// shift up from the anchor, if space allows
|
||||||
|
let offsetPos = Math.floor(eleBounds.height / 100) * percentageOffset
|
||||||
|
let defaultTop = anchorBounds.top - offsetPos
|
||||||
|
|
||||||
|
if (window.innerHeight - defaultTop < eleBounds.height) {
|
||||||
|
top = window.innerHeight - eleBounds.height - 5
|
||||||
|
} else {
|
||||||
|
top = anchorBounds.top - offsetPos
|
||||||
|
}
|
||||||
|
|
||||||
|
return { ...cfg, left, top }
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span class="popover-wrap">
|
||||||
|
<Layout noPadding noGap>
|
||||||
|
<div class="type-icon">
|
||||||
|
<Icon name={parsedComponentDef.icon} />
|
||||||
|
<span>{field.field}</span>
|
||||||
|
</div>
|
||||||
|
<ComponentSettingsSection
|
||||||
|
componentInstance={pseudoComponentInstance}
|
||||||
|
componentDefinition={parsedComponentDef}
|
||||||
|
isScreen={false}
|
||||||
|
onUpdateSetting={updateSetting}
|
||||||
|
showSectionTitle={false}
|
||||||
|
showInstanceName={false}
|
||||||
|
{bindings}
|
||||||
|
{componentBindings}
|
||||||
|
on:drawerShow={e => {
|
||||||
|
drawers = [...drawers, e.detail]
|
||||||
|
}}
|
||||||
|
on:drawerHide={() => {
|
||||||
|
drawers = drawers.slice(0, -1)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Layout>
|
||||||
|
</span>
|
||||||
|
</Popover>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.popover-wrap {
|
||||||
|
background-color: var(--spectrum-alias-background-color-primary);
|
||||||
|
}
|
||||||
|
.type-icon {
|
||||||
|
display: flex;
|
||||||
|
gap: var(--spacing-m);
|
||||||
|
margin: var(--spacing-xl);
|
||||||
|
margin-bottom: 0px;
|
||||||
|
height: var(--spectrum-alias-item-height-m);
|
||||||
|
padding: 0px var(--spectrum-alias-item-padding-m);
|
||||||
|
border-width: var(--spectrum-actionbutton-border-size);
|
||||||
|
border-radius: var(--spectrum-alias-border-radius-regular);
|
||||||
|
border: 1px solid
|
||||||
|
var(
|
||||||
|
--spectrum-actionbutton-m-border-color,
|
||||||
|
var(--spectrum-alias-border-color)
|
||||||
|
);
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -1,45 +1,70 @@
|
||||||
<script>
|
<script>
|
||||||
import { Button, ActionButton, Drawer } from "@budibase/bbui"
|
import { cloneDeep, isEqual } from "lodash/fp"
|
||||||
import { createEventDispatcher } from "svelte"
|
|
||||||
import ColumnDrawer from "./ColumnDrawer.svelte"
|
|
||||||
import { cloneDeep } from "lodash/fp"
|
|
||||||
import {
|
import {
|
||||||
getDatasourceForProvider,
|
getDatasourceForProvider,
|
||||||
getSchemaForDatasource,
|
getSchemaForDatasource,
|
||||||
|
getBindableProperties,
|
||||||
|
getComponentBindableProperties,
|
||||||
} from "builderStore/dataBinding"
|
} from "builderStore/dataBinding"
|
||||||
import { currentAsset } from "builderStore"
|
import { currentAsset } from "builderStore"
|
||||||
|
import DraggableList from "../DraggableList.svelte"
|
||||||
|
import { createEventDispatcher } from "svelte"
|
||||||
|
import { store, selectedScreen } from "builderStore"
|
||||||
|
import FieldSetting from "./FieldSetting.svelte"
|
||||||
|
import { convertOldFieldFormat, getComponentForField } from "./utils"
|
||||||
|
|
||||||
export let componentInstance
|
export let componentInstance
|
||||||
export let value = []
|
export let value
|
||||||
|
|
||||||
const dispatch = createEventDispatcher()
|
const dispatch = createEventDispatcher()
|
||||||
|
let sanitisedFields
|
||||||
|
let fieldList
|
||||||
|
let schema
|
||||||
|
let cachedValue
|
||||||
|
|
||||||
let drawer
|
$: bindings = getBindableProperties($selectedScreen, componentInstance._id)
|
||||||
let boundValue
|
$: actionType = componentInstance.actionType
|
||||||
|
let componentBindings = []
|
||||||
|
|
||||||
|
$: if (actionType) {
|
||||||
|
componentBindings = getComponentBindableProperties(
|
||||||
|
$selectedScreen,
|
||||||
|
componentInstance._id
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
$: text = getText(value)
|
|
||||||
$: convertOldColumnFormat(value)
|
|
||||||
$: datasource = getDatasourceForProvider($currentAsset, componentInstance)
|
$: datasource = getDatasourceForProvider($currentAsset, componentInstance)
|
||||||
$: schema = getSchema($currentAsset, datasource)
|
|
||||||
|
$: if (!isEqual(value, cachedValue)) {
|
||||||
|
cachedValue = value
|
||||||
|
schema = getSchema($currentAsset, datasource)
|
||||||
|
}
|
||||||
|
|
||||||
$: options = Object.keys(schema || {})
|
$: options = Object.keys(schema || {})
|
||||||
$: sanitisedValue = getValidColumns(value, options)
|
$: sanitisedValue = getValidColumns(convertOldFieldFormat(value), options)
|
||||||
$: updateBoundValue(sanitisedValue)
|
$: updateSanitsedFields(sanitisedValue)
|
||||||
|
$: unconfigured = buildUnconfiguredOptions(schema, sanitisedFields)
|
||||||
|
|
||||||
const getText = value => {
|
// Builds unused ones only
|
||||||
if (!value?.length) {
|
const buildUnconfiguredOptions = (schema, selected) => {
|
||||||
return "All fields"
|
if (!schema) {
|
||||||
}
|
return []
|
||||||
let text = `${value.length} field`
|
|
||||||
if (value.length !== 1) {
|
|
||||||
text += "s"
|
|
||||||
}
|
|
||||||
return text
|
|
||||||
}
|
}
|
||||||
|
let schemaClone = cloneDeep(schema)
|
||||||
|
selected.forEach(val => {
|
||||||
|
delete schemaClone[val.field]
|
||||||
|
})
|
||||||
|
|
||||||
const convertOldColumnFormat = oldColumns => {
|
return Object.keys(schemaClone)
|
||||||
if (typeof oldColumns?.[0] === "string") {
|
.filter(key => !schemaClone[key].autocolumn)
|
||||||
value = oldColumns.map(field => ({ name: field, displayName: field }))
|
.map(key => {
|
||||||
|
const col = schemaClone[key]
|
||||||
|
let toggleOn = !value
|
||||||
|
return {
|
||||||
|
field: key,
|
||||||
|
active: typeof col.active != "boolean" ? toggleOn : col.active,
|
||||||
}
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const getSchema = (asset, datasource) => {
|
const getSchema = (asset, datasource) => {
|
||||||
|
@ -54,50 +79,85 @@
|
||||||
return schema
|
return schema
|
||||||
}
|
}
|
||||||
|
|
||||||
const updateBoundValue = value => {
|
const updateSanitsedFields = value => {
|
||||||
boundValue = cloneDeep(value)
|
sanitisedFields = cloneDeep(value)
|
||||||
}
|
}
|
||||||
|
|
||||||
const getValidColumns = (columns, options) => {
|
const getValidColumns = (columns, options) => {
|
||||||
if (!Array.isArray(columns) || !columns.length) {
|
if (!Array.isArray(columns) || !columns.length) {
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
// We need to account for legacy configs which would just be an array
|
|
||||||
// of strings
|
|
||||||
if (typeof columns[0] === "string") {
|
|
||||||
columns = columns.map(col => ({
|
|
||||||
name: col,
|
|
||||||
displayName: col,
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
return columns.filter(column => {
|
return columns.filter(column => {
|
||||||
return options.includes(column.name)
|
return options.includes(column.field)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const open = () => {
|
const buildSudoInstance = instance => {
|
||||||
updateBoundValue(sanitisedValue)
|
if (instance._component) {
|
||||||
drawer.show()
|
return instance
|
||||||
}
|
}
|
||||||
|
|
||||||
const save = () => {
|
const type = getComponentForField(instance.field, schema)
|
||||||
dispatch("change", getValidColumns(boundValue, options))
|
instance._component = `@budibase/standard-components/${type}`
|
||||||
drawer.hide()
|
|
||||||
|
const pseudoComponentInstance = store.actions.components.createInstance(
|
||||||
|
instance._component,
|
||||||
|
{
|
||||||
|
_instanceName: instance.field,
|
||||||
|
field: instance.field,
|
||||||
|
label: instance.field,
|
||||||
|
placeholder: instance.field,
|
||||||
|
},
|
||||||
|
{}
|
||||||
|
)
|
||||||
|
|
||||||
|
return { ...instance, ...pseudoComponentInstance }
|
||||||
|
}
|
||||||
|
|
||||||
|
$: if (sanitisedFields) {
|
||||||
|
fieldList = [...sanitisedFields, ...unconfigured].map(buildSudoInstance)
|
||||||
|
}
|
||||||
|
|
||||||
|
const processItemUpdate = e => {
|
||||||
|
const updatedField = e.detail
|
||||||
|
const parentFieldsUpdated = fieldList ? cloneDeep(fieldList) : []
|
||||||
|
|
||||||
|
let parentFieldIdx = parentFieldsUpdated.findIndex(pSetting => {
|
||||||
|
return pSetting.field === updatedField?.field
|
||||||
|
})
|
||||||
|
|
||||||
|
if (parentFieldIdx == -1) {
|
||||||
|
parentFieldsUpdated.push(updatedField)
|
||||||
|
} else {
|
||||||
|
parentFieldsUpdated[parentFieldIdx] = updatedField
|
||||||
|
}
|
||||||
|
|
||||||
|
dispatch("change", getValidColumns(parentFieldsUpdated, options))
|
||||||
|
}
|
||||||
|
|
||||||
|
const listUpdated = e => {
|
||||||
|
const parsedColumns = getValidColumns(e.detail, options)
|
||||||
|
dispatch("change", parsedColumns)
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="field-configuration">
|
<div class="field-configuration">
|
||||||
<ActionButton on:click={open}>{text}</ActionButton>
|
{#if fieldList?.length}
|
||||||
|
<DraggableList
|
||||||
|
on:change={listUpdated}
|
||||||
|
on:itemChange={processItemUpdate}
|
||||||
|
items={fieldList}
|
||||||
|
listItemKey={"_id"}
|
||||||
|
listType={FieldSetting}
|
||||||
|
listTypeProps={{
|
||||||
|
componentBindings,
|
||||||
|
bindings,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Drawer bind:this={drawer} title="Form Fields">
|
|
||||||
<svelte:fragment slot="description">
|
|
||||||
Configure the fields in your form.
|
|
||||||
</svelte:fragment>
|
|
||||||
<Button cta slot="buttons" on:click={save}>Save</Button>
|
|
||||||
<ColumnDrawer slot="body" bind:columns={boundValue} {options} {schema} />
|
|
||||||
</Drawer>
|
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.field-configuration :global(.spectrum-ActionButton) {
|
.field-configuration :global(.spectrum-ActionButton) {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|
|
@ -0,0 +1,56 @@
|
||||||
|
<script>
|
||||||
|
import EditFieldPopover from "./EditFieldPopover.svelte"
|
||||||
|
import { Toggle } from "@budibase/bbui"
|
||||||
|
import { createEventDispatcher } from "svelte"
|
||||||
|
import { cloneDeep } from "lodash/fp"
|
||||||
|
|
||||||
|
export let item
|
||||||
|
export let componentBindings
|
||||||
|
export let bindings
|
||||||
|
export let anchor
|
||||||
|
|
||||||
|
const dispatch = createEventDispatcher()
|
||||||
|
const onToggle = item => {
|
||||||
|
return e => {
|
||||||
|
item.active = e.detail
|
||||||
|
dispatch("change", { ...cloneDeep(item), active: e.detail })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="list-item-body">
|
||||||
|
<div class="list-item-left">
|
||||||
|
<EditFieldPopover
|
||||||
|
{anchor}
|
||||||
|
field={item}
|
||||||
|
{componentBindings}
|
||||||
|
{bindings}
|
||||||
|
on:change
|
||||||
|
/>
|
||||||
|
<div class="field-label">{item.label || item.field}</div>
|
||||||
|
</div>
|
||||||
|
<div class="list-item-right">
|
||||||
|
<Toggle on:change={onToggle(item)} text="" value={item.active} thin />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.field-label {
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.list-item-body,
|
||||||
|
.list-item-left {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--spacing-m);
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
.list-item-right :global(div.spectrum-Switch) {
|
||||||
|
margin: 0px;
|
||||||
|
}
|
||||||
|
.list-item-body {
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,46 @@
|
||||||
|
export const convertOldFieldFormat = fields => {
|
||||||
|
if (!fields) {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
const converted = fields.map(field => {
|
||||||
|
if (typeof field === "string") {
|
||||||
|
// existed but was a string
|
||||||
|
return {
|
||||||
|
field,
|
||||||
|
active: true,
|
||||||
|
}
|
||||||
|
} else if (typeof field?.active != "boolean") {
|
||||||
|
// existed but had no state
|
||||||
|
return {
|
||||||
|
field: field.name,
|
||||||
|
active: true,
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return field
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return converted
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getComponentForField = (field, schema) => {
|
||||||
|
if (!field || !schema?.[field]) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
const type = schema[field].type
|
||||||
|
return FieldTypeToComponentMap[type]
|
||||||
|
}
|
||||||
|
|
||||||
|
export const FieldTypeToComponentMap = {
|
||||||
|
string: "stringfield",
|
||||||
|
number: "numberfield",
|
||||||
|
bigint: "bigintfield",
|
||||||
|
options: "optionsfield",
|
||||||
|
array: "multifieldselect",
|
||||||
|
boolean: "booleanfield",
|
||||||
|
longform: "longformfield",
|
||||||
|
datetime: "datetimefield",
|
||||||
|
attachment: "attachmentfield",
|
||||||
|
link: "relationshipfield",
|
||||||
|
json: "jsonfield",
|
||||||
|
barcodeqr: "codescanner",
|
||||||
|
}
|
|
@ -17,7 +17,7 @@
|
||||||
import { generate } from "shortid"
|
import { generate } from "shortid"
|
||||||
import { LuceneUtils, Constants } from "@budibase/frontend-core"
|
import { LuceneUtils, Constants } from "@budibase/frontend-core"
|
||||||
import { getFields } from "helpers/searchFields"
|
import { getFields } from "helpers/searchFields"
|
||||||
import { createEventDispatcher } from "svelte"
|
import { createEventDispatcher, onMount } from "svelte"
|
||||||
|
|
||||||
export let schemaFields
|
export let schemaFields
|
||||||
export let filters = []
|
export let filters = []
|
||||||
|
@ -35,22 +35,28 @@
|
||||||
{ value: "and", label: "Match all filters" },
|
{ value: "and", label: "Match all filters" },
|
||||||
{ value: "or", label: "Match any filter" },
|
{ value: "or", label: "Match any filter" },
|
||||||
]
|
]
|
||||||
|
const onEmptyOptions = [
|
||||||
|
{ value: "all", label: "Return all table rows" },
|
||||||
|
{ value: "none", label: "Return no rows" },
|
||||||
|
]
|
||||||
|
|
||||||
let rawFilters
|
let rawFilters
|
||||||
let matchAny = false
|
let matchAny = false
|
||||||
|
let onEmptyFilter = "all"
|
||||||
|
|
||||||
$: parseFilters(filters)
|
$: parseFilters(filters)
|
||||||
$: dispatch("change", enrichFilters(rawFilters, matchAny))
|
$: dispatch("change", enrichFilters(rawFilters, matchAny, onEmptyFilter))
|
||||||
$: enrichedSchemaFields = getFields(schemaFields || [], { allowLinks: true })
|
$: enrichedSchemaFields = getFields(schemaFields || [], { allowLinks: true })
|
||||||
$: fieldOptions = enrichedSchemaFields.map(field => field.name) || []
|
$: fieldOptions = enrichedSchemaFields.map(field => field.name) || []
|
||||||
$: valueTypeOptions = allowBindings ? ["Value", "Binding"] : ["Value"]
|
$: valueTypeOptions = allowBindings ? ["Value", "Binding"] : ["Value"]
|
||||||
|
|
||||||
// Remove field key prefixes and determine whether to use the "match all"
|
// Remove field key prefixes and determine which behaviours to use
|
||||||
// or "match any" behaviour
|
|
||||||
const parseFilters = filters => {
|
const parseFilters = filters => {
|
||||||
matchAny = filters?.find(filter => filter.operator === "allOr") != null
|
matchAny = filters?.find(filter => filter.operator === "allOr") != null
|
||||||
|
onEmptyFilter =
|
||||||
|
filters?.find(filter => filter.onEmptyFilter)?.onEmptyFilter ?? "all"
|
||||||
rawFilters = (filters || [])
|
rawFilters = (filters || [])
|
||||||
.filter(filter => filter.operator !== "allOr")
|
.filter(filter => filter.operator !== "allOr" && !filter.onEmptyFilter)
|
||||||
.map(filter => {
|
.map(filter => {
|
||||||
const { field } = filter
|
const { field } = filter
|
||||||
let newFilter = { ...filter }
|
let newFilter = { ...filter }
|
||||||
|
@ -64,9 +70,18 @@
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
parseFilters(filters)
|
||||||
|
rawFilters.forEach(filter => {
|
||||||
|
filter.type =
|
||||||
|
schemaFields.find(field => field.name === filter.field)?.type ||
|
||||||
|
filter.type
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
// Add field key prefixes and a special metadata filter object to indicate
|
// Add field key prefixes and a special metadata filter object to indicate
|
||||||
// whether to use the "match all" or "match any" behaviour
|
// how to handle filter behaviour
|
||||||
const enrichFilters = (rawFilters, matchAny) => {
|
const enrichFilters = (rawFilters, matchAny, onEmptyFilter) => {
|
||||||
let count = 1
|
let count = 1
|
||||||
return rawFilters
|
return rawFilters
|
||||||
.filter(filter => filter.field)
|
.filter(filter => filter.field)
|
||||||
|
@ -75,6 +90,7 @@
|
||||||
field: `${count++}:${filter.field}`,
|
field: `${count++}:${filter.field}`,
|
||||||
}))
|
}))
|
||||||
.concat(matchAny ? [{ operator: "allOr" }] : [])
|
.concat(matchAny ? [{ operator: "allOr" }] : [])
|
||||||
|
.concat([{ onEmptyFilter }])
|
||||||
}
|
}
|
||||||
|
|
||||||
const addFilter = () => {
|
const addFilter = () => {
|
||||||
|
@ -186,6 +202,17 @@
|
||||||
on:change={e => (matchAny = e.detail === "or")}
|
on:change={e => (matchAny = e.detail === "or")}
|
||||||
placeholder={null}
|
placeholder={null}
|
||||||
/>
|
/>
|
||||||
|
{#if datasource?.type === "table"}
|
||||||
|
<Select
|
||||||
|
label="When filter empty"
|
||||||
|
value={onEmptyFilter}
|
||||||
|
options={onEmptyOptions}
|
||||||
|
getOptionLabel={opt => opt.label}
|
||||||
|
getOptionValue={opt => opt.value}
|
||||||
|
on:change={e => (onEmptyFilter = e.detail)}
|
||||||
|
placeholder={null}
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div class="filter-label">
|
<div class="filter-label">
|
||||||
|
|
|
@ -24,11 +24,22 @@
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<ActionButton on:click={drawer.show}>Define Options</ActionButton>
|
<div class="options-wrap">
|
||||||
<Drawer bind:this={drawer} title="Options">
|
<div />
|
||||||
|
<div><ActionButton on:click={drawer.show}>Define Options</ActionButton></div>
|
||||||
|
</div>
|
||||||
|
<Drawer bind:this={drawer} title="Options" on:drawerHide on:drawerShow>
|
||||||
<svelte:fragment slot="description">
|
<svelte:fragment slot="description">
|
||||||
Define the options for this picker.
|
Define the options for this picker.
|
||||||
</svelte:fragment>
|
</svelte:fragment>
|
||||||
<Button cta slot="buttons" on:click={saveOptions}>Save</Button>
|
<Button cta slot="buttons" on:click={saveOptions}>Save</Button>
|
||||||
<OptionsDrawer bind:options={tempValue} slot="body" />
|
<OptionsDrawer bind:options={tempValue} slot="body" />
|
||||||
</Drawer>
|
</Drawer>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.options-wrap {
|
||||||
|
gap: 8px;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 90px 1fr;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
|
@ -100,6 +100,8 @@
|
||||||
{key}
|
{key}
|
||||||
{type}
|
{type}
|
||||||
{...props}
|
{...props}
|
||||||
|
on:drawerHide
|
||||||
|
on:drawerShow
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{#if info}
|
{#if info}
|
||||||
|
|
|
@ -5,9 +5,8 @@
|
||||||
|
|
||||||
export let value = []
|
export let value = []
|
||||||
export let bindings = []
|
export let bindings = []
|
||||||
export let componentDefinition
|
export let componentInstance
|
||||||
export let type
|
export let type
|
||||||
|
|
||||||
const dispatch = createEventDispatcher()
|
const dispatch = createEventDispatcher()
|
||||||
let drawer
|
let drawer
|
||||||
|
|
||||||
|
@ -31,7 +30,7 @@
|
||||||
<ActionButton on:click={drawer.show}>{text}</ActionButton>
|
<ActionButton on:click={drawer.show}>{text}</ActionButton>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Drawer bind:this={drawer} title="Validation Rules">
|
<Drawer bind:this={drawer} title="Validation Rules" on:drawerHide on:drawerShow>
|
||||||
<svelte:fragment slot="description">
|
<svelte:fragment slot="description">
|
||||||
Configure validation rules for this field.
|
Configure validation rules for this field.
|
||||||
</svelte:fragment>
|
</svelte:fragment>
|
||||||
|
@ -41,7 +40,7 @@
|
||||||
bind:rules={value}
|
bind:rules={value}
|
||||||
{type}
|
{type}
|
||||||
{bindings}
|
{bindings}
|
||||||
{componentDefinition}
|
fieldName={componentInstance?.field}
|
||||||
/>
|
/>
|
||||||
</Drawer>
|
</Drawer>
|
||||||
|
|
||||||
|
|
|
@ -56,6 +56,11 @@ export const syncURLToState = options => {
|
||||||
|
|
||||||
// Navigate to a certain URL
|
// Navigate to a certain URL
|
||||||
const gotoUrl = (url, params) => {
|
const gotoUrl = (url, params) => {
|
||||||
|
// Clean URL
|
||||||
|
if (url?.endsWith("/index")) {
|
||||||
|
url = url.replace("/index", "")
|
||||||
|
}
|
||||||
|
// Allow custom URL handling
|
||||||
if (beforeNavigate) {
|
if (beforeNavigate) {
|
||||||
const res = beforeNavigate(url, params)
|
const res = beforeNavigate(url, params)
|
||||||
if (res?.url) {
|
if (res?.url) {
|
||||||
|
|
|
@ -2,14 +2,15 @@
|
||||||
import { Button, Layout } from "@budibase/bbui"
|
import { Button, Layout } from "@budibase/bbui"
|
||||||
import DatasourceNavigator from "components/backend/DatasourceNavigator/DatasourceNavigator.svelte"
|
import DatasourceNavigator from "components/backend/DatasourceNavigator/DatasourceNavigator.svelte"
|
||||||
import Panel from "components/design/Panel.svelte"
|
import Panel from "components/design/Panel.svelte"
|
||||||
import { isActive, goto, redirect } from "@roxi/routify"
|
import { isActive, redirect, goto, params } from "@roxi/routify"
|
||||||
import BetaButton from "./_components/BetaButton.svelte"
|
import BetaButton from "./_components/BetaButton.svelte"
|
||||||
import { datasources } from "stores/backend"
|
import { datasources } from "stores/backend"
|
||||||
|
|
||||||
$: {
|
$: {
|
||||||
// If we ever don't have any data other than the users table, prompt the
|
// If we ever don't have any data other than the users table, prompt the
|
||||||
// user to add some
|
// user to add some
|
||||||
if (!$datasources.hasData) {
|
// Don't redirect if setting up google sheets, or we lose the query parameter
|
||||||
|
if (!$datasources.hasData && !$params["?continue_google_setup"]) {
|
||||||
$redirect("./new")
|
$redirect("./new")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,12 @@
|
||||||
|
<script>
|
||||||
|
import { DetailSummary } from "@budibase/bbui"
|
||||||
|
import InfoDisplay from "./InfoDisplay.svelte"
|
||||||
|
export let componentDefinition
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<DetailSummary collapsible={false} noPadding={true}>
|
||||||
|
<InfoDisplay
|
||||||
|
title={componentDefinition.name}
|
||||||
|
body={componentDefinition.info}
|
||||||
|
/>
|
||||||
|
</DetailSummary>
|
|
@ -5,7 +5,7 @@
|
||||||
import DesignSection from "./DesignSection.svelte"
|
import DesignSection from "./DesignSection.svelte"
|
||||||
import CustomStylesSection from "./CustomStylesSection.svelte"
|
import CustomStylesSection from "./CustomStylesSection.svelte"
|
||||||
import ConditionalUISection from "./ConditionalUISection.svelte"
|
import ConditionalUISection from "./ConditionalUISection.svelte"
|
||||||
import ComponentInfoSection from "./ComponentInfoSection.svelte"
|
|
||||||
import {
|
import {
|
||||||
getBindableProperties,
|
getBindableProperties,
|
||||||
getComponentBindableProperties,
|
getComponentBindableProperties,
|
||||||
|
@ -55,9 +55,6 @@
|
||||||
</div>
|
</div>
|
||||||
</span>
|
</span>
|
||||||
{#if section == "settings"}
|
{#if section == "settings"}
|
||||||
{#if componentDefinition?.info}
|
|
||||||
<ComponentInfoSection {componentDefinition} />
|
|
||||||
{/if}
|
|
||||||
<ComponentSettingsSection
|
<ComponentSettingsSection
|
||||||
{componentInstance}
|
{componentInstance}
|
||||||
{componentDefinition}
|
{componentDefinition}
|
|
@ -6,6 +6,7 @@
|
||||||
import ResetFieldsButton from "components/design/settings/controls/ResetFieldsButton.svelte"
|
import ResetFieldsButton from "components/design/settings/controls/ResetFieldsButton.svelte"
|
||||||
import EjectBlockButton from "components/design/settings/controls/EjectBlockButton.svelte"
|
import EjectBlockButton from "components/design/settings/controls/EjectBlockButton.svelte"
|
||||||
import { getComponentForSetting } from "components/design/settings/componentSettings"
|
import { getComponentForSetting } from "components/design/settings/componentSettings"
|
||||||
|
import InfoDisplay from "./InfoDisplay.svelte"
|
||||||
import analytics, { Events } from "analytics"
|
import analytics, { Events } from "analytics"
|
||||||
|
|
||||||
export let componentDefinition
|
export let componentDefinition
|
||||||
|
@ -13,6 +14,9 @@
|
||||||
export let bindings
|
export let bindings
|
||||||
export let componentBindings
|
export let componentBindings
|
||||||
export let isScreen = false
|
export let isScreen = false
|
||||||
|
export let onUpdateSetting
|
||||||
|
export let showSectionTitle = true
|
||||||
|
export let showInstanceName = true
|
||||||
|
|
||||||
$: sections = getSections(componentInstance, componentDefinition, isScreen)
|
$: sections = getSections(componentInstance, componentDefinition, isScreen)
|
||||||
|
|
||||||
|
@ -47,8 +51,11 @@
|
||||||
|
|
||||||
const updateSetting = async (setting, value) => {
|
const updateSetting = async (setting, value) => {
|
||||||
try {
|
try {
|
||||||
|
if (typeof onUpdateSetting === "function") {
|
||||||
|
await onUpdateSetting(setting, value)
|
||||||
|
} else {
|
||||||
await store.actions.components.updateSetting(setting.key, value)
|
await store.actions.components.updateSetting(setting.key, value)
|
||||||
|
}
|
||||||
// Send event if required
|
// Send event if required
|
||||||
if (setting.sendEvents) {
|
if (setting.sendEvents) {
|
||||||
analytics.captureEvent(Events.COMPONENT_UPDATED, {
|
analytics.captureEvent(Events.COMPONENT_UPDATED, {
|
||||||
|
@ -97,7 +104,7 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return true
|
return typeof setting.visible == "boolean" ? setting.visible : true
|
||||||
}
|
}
|
||||||
|
|
||||||
const canRenderControl = (instance, setting, isScreen) => {
|
const canRenderControl = (instance, setting, isScreen) => {
|
||||||
|
@ -116,9 +123,22 @@
|
||||||
|
|
||||||
{#each sections as section, idx (section.name)}
|
{#each sections as section, idx (section.name)}
|
||||||
{#if section.visible}
|
{#if section.visible}
|
||||||
<DetailSummary name={section.name} collapsible={false}>
|
<DetailSummary
|
||||||
|
name={showSectionTitle ? section.name : ""}
|
||||||
|
collapsible={false}
|
||||||
|
>
|
||||||
|
{#if section.info}
|
||||||
|
<div class="section-info">
|
||||||
|
<InfoDisplay body={section.info} />
|
||||||
|
</div>
|
||||||
|
{:else if idx === 0 && section.name === "General" && componentDefinition.info}
|
||||||
|
<InfoDisplay
|
||||||
|
title={componentDefinition.name}
|
||||||
|
body={componentDefinition.info}
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
<div class="settings">
|
<div class="settings">
|
||||||
{#if idx === 0 && !componentInstance._component.endsWith("/layout") && !isScreen}
|
{#if idx === 0 && !componentInstance._component.endsWith("/layout") && !isScreen && showInstanceName}
|
||||||
<PropertyControl
|
<PropertyControl
|
||||||
control={Input}
|
control={Input}
|
||||||
label="Name"
|
label="Name"
|
||||||
|
@ -157,6 +177,8 @@
|
||||||
{componentBindings}
|
{componentBindings}
|
||||||
{componentInstance}
|
{componentInstance}
|
||||||
{componentDefinition}
|
{componentDefinition}
|
||||||
|
on:drawerShow
|
||||||
|
on:drawerHide
|
||||||
/>
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
{/each}
|
{/each}
|
|
@ -0,0 +1,66 @@
|
||||||
|
<script>
|
||||||
|
import { Icon } from "@budibase/bbui"
|
||||||
|
|
||||||
|
export let title
|
||||||
|
export let body
|
||||||
|
export let icon = "HelpOutline"
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="info" class:noTitle={!title}>
|
||||||
|
{#if title}
|
||||||
|
<div class="title">
|
||||||
|
<Icon name={icon} />
|
||||||
|
{title || ""}
|
||||||
|
</div>
|
||||||
|
<!-- eslint-disable-next-line svelte/no-at-html-tags -->
|
||||||
|
{@html body}
|
||||||
|
{:else}
|
||||||
|
<span class="icon">
|
||||||
|
<Icon name={icon} />
|
||||||
|
</span>
|
||||||
|
<!-- eslint-disable-next-line svelte/no-at-html-tags -->
|
||||||
|
{@html body}
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.title {
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
text-transform: uppercase;
|
||||||
|
margin-bottom: var(--spacing-m);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: flex-start;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--spacing-s);
|
||||||
|
}
|
||||||
|
.title,
|
||||||
|
.icon {
|
||||||
|
color: var(--spectrum-global-color-gray-600);
|
||||||
|
}
|
||||||
|
.info {
|
||||||
|
padding: var(--spacing-m) var(--spacing-l) var(--spacing-l) var(--spacing-l);
|
||||||
|
background-color: var(--background-alt);
|
||||||
|
border-radius: var(--border-radius-s);
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
.noTitle {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--spacing-m);
|
||||||
|
}
|
||||||
|
.info :global(a) {
|
||||||
|
color: inherit;
|
||||||
|
transition: color 130ms ease-out;
|
||||||
|
overflow: hidden;
|
||||||
|
white-space: nowrap;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
.info :global(a:hover) {
|
||||||
|
color: var(--spectrum-global-color-gray-900);
|
||||||
|
}
|
||||||
|
.info :global(a) {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -1,6 +1,6 @@
|
||||||
<script>
|
<script>
|
||||||
import { Button, Drawer } from "@budibase/bbui"
|
import { Button, Drawer } from "@budibase/bbui"
|
||||||
import NavigationLinksDrawer from "./NavigationLinksDrawer.svelte"
|
import NavigationLinksDrawer from "./LinksDrawer.svelte"
|
||||||
import { cloneDeep } from "lodash/fp"
|
import { cloneDeep } from "lodash/fp"
|
||||||
import { store } from "builderStore"
|
import { store } from "builderStore"
|
||||||
|
|
||||||
|
@ -20,12 +20,8 @@
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Button cta on:click={openDrawer}>Configure links</Button>
|
<Button cta on:click={openDrawer}>Configure Links</Button>
|
||||||
<Drawer
|
<Drawer bind:this={drawer} title={"Navigation Links"}>
|
||||||
bind:this={drawer}
|
|
||||||
title={"Navigation Links"}
|
|
||||||
width="calc(100% - 334px)"
|
|
||||||
>
|
|
||||||
<svelte:fragment slot="description">
|
<svelte:fragment slot="description">
|
||||||
Configure the links in your navigation bar.
|
Configure the links in your navigation bar.
|
||||||
</svelte:fragment>
|
</svelte:fragment>
|
|
@ -0,0 +1,225 @@
|
||||||
|
<script>
|
||||||
|
import LinksEditor from "./LinksEditor.svelte"
|
||||||
|
import { get } from "svelte/store"
|
||||||
|
import Panel from "components/design/Panel.svelte"
|
||||||
|
import {
|
||||||
|
Detail,
|
||||||
|
Toggle,
|
||||||
|
Body,
|
||||||
|
Icon,
|
||||||
|
ColorPicker,
|
||||||
|
Input,
|
||||||
|
Label,
|
||||||
|
ActionGroup,
|
||||||
|
ActionButton,
|
||||||
|
Checkbox,
|
||||||
|
notifications,
|
||||||
|
Select,
|
||||||
|
} from "@budibase/bbui"
|
||||||
|
import { selectedScreen, store } from "builderStore"
|
||||||
|
import { DefaultAppTheme } from "constants"
|
||||||
|
|
||||||
|
const updateShowNavigation = async e => {
|
||||||
|
await store.actions.screens.updateSetting(
|
||||||
|
get(selectedScreen),
|
||||||
|
"showNavigation",
|
||||||
|
e.detail
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const update = async (key, value) => {
|
||||||
|
try {
|
||||||
|
let navigation = $store.navigation
|
||||||
|
navigation[key] = value
|
||||||
|
await store.actions.navigation.save(navigation)
|
||||||
|
} catch (error) {
|
||||||
|
notifications.error("Error updating navigation settings")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Panel
|
||||||
|
title="Navigation"
|
||||||
|
icon={$selectedScreen.showNavigation ? "Visibility" : "VisibilityOff"}
|
||||||
|
borderLeft
|
||||||
|
wide
|
||||||
|
>
|
||||||
|
<div class="generalSection">
|
||||||
|
<div class="subheading">
|
||||||
|
<Detail>General</Detail>
|
||||||
|
</div>
|
||||||
|
<div class="toggle">
|
||||||
|
<Toggle
|
||||||
|
on:change={updateShowNavigation}
|
||||||
|
value={$selectedScreen.showNavigation}
|
||||||
|
/>
|
||||||
|
<Body size="S">Show nav on this screen</Body>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{#if $selectedScreen.showNavigation}
|
||||||
|
<div class="divider" />
|
||||||
|
<div class="customizeSection">
|
||||||
|
<div class="subheading">
|
||||||
|
<Detail>Customize</Detail>
|
||||||
|
</div>
|
||||||
|
<div class="info">
|
||||||
|
<Icon name="InfoOutline" size="S" />
|
||||||
|
<Body size="S">These settings apply to all screens</Body>
|
||||||
|
</div>
|
||||||
|
<div class="configureLinks">
|
||||||
|
<LinksEditor />
|
||||||
|
</div>
|
||||||
|
<div class="controls">
|
||||||
|
<div class="label">
|
||||||
|
<Label size="M">Position</Label>
|
||||||
|
</div>
|
||||||
|
<ActionGroup quiet>
|
||||||
|
<ActionButton
|
||||||
|
selected={$store.navigation.navigation === "Top"}
|
||||||
|
quiet={$store.navigation.navigation !== "Top"}
|
||||||
|
icon="PaddingTop"
|
||||||
|
on:click={() => update("navigation", "Top")}
|
||||||
|
/>
|
||||||
|
<ActionButton
|
||||||
|
selected={$store.navigation.navigation === "Left"}
|
||||||
|
quiet={$store.navigation.navigation !== "Left"}
|
||||||
|
icon="PaddingLeft"
|
||||||
|
on:click={() => update("navigation", "Left")}
|
||||||
|
/>
|
||||||
|
</ActionGroup>
|
||||||
|
|
||||||
|
{#if $store.navigation.navigation === "Top"}
|
||||||
|
<div class="label">
|
||||||
|
<Label size="M">Sticky header</Label>
|
||||||
|
</div>
|
||||||
|
<Checkbox
|
||||||
|
value={$store.navigation.sticky}
|
||||||
|
on:change={e => update("sticky", e.detail)}
|
||||||
|
/>
|
||||||
|
<div class="label">
|
||||||
|
<Label size="M">Width</Label>
|
||||||
|
</div>
|
||||||
|
<Select
|
||||||
|
options={["Max", "Large", "Medium", "Small"]}
|
||||||
|
plaveholder={null}
|
||||||
|
value={$store.navigation.navWidth}
|
||||||
|
on:change={e => update("navWidth", e.detail)}
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
|
<div class="label">
|
||||||
|
<Label size="M">Show logo</Label>
|
||||||
|
</div>
|
||||||
|
<Checkbox
|
||||||
|
value={!$store.navigation.hideLogo}
|
||||||
|
on:change={e => update("hideLogo", !e.detail)}
|
||||||
|
/>
|
||||||
|
{#if !$store.navigation.hideLogo}
|
||||||
|
<div class="label">
|
||||||
|
<Label size="M">Logo URL</Label>
|
||||||
|
</div>
|
||||||
|
<Input
|
||||||
|
value={$store.navigation.logoUrl}
|
||||||
|
on:change={e => update("logoUrl", e.detail)}
|
||||||
|
updateOnChange={false}
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
|
<div class="label">
|
||||||
|
<Label size="M">Show title</Label>
|
||||||
|
</div>
|
||||||
|
<Checkbox
|
||||||
|
value={!$store.navigation.hideTitle}
|
||||||
|
on:change={e => update("hideTitle", !e.detail)}
|
||||||
|
/>
|
||||||
|
{#if !$store.navigation.hideTitle}
|
||||||
|
<div class="label">
|
||||||
|
<Label size="M">Title</Label>
|
||||||
|
</div>
|
||||||
|
<Input
|
||||||
|
value={$store.navigation.title}
|
||||||
|
on:change={e => update("title", e.detail)}
|
||||||
|
updateOnChange={false}
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
|
<div class="label">
|
||||||
|
<Label>Background</Label>
|
||||||
|
</div>
|
||||||
|
<ColorPicker
|
||||||
|
spectrumTheme={$store.theme}
|
||||||
|
value={$store.navigation.navBackground ||
|
||||||
|
DefaultAppTheme.navBackground}
|
||||||
|
on:change={e => update("navBackground", e.detail)}
|
||||||
|
/>
|
||||||
|
<div class="label">
|
||||||
|
<Label>Text</Label>
|
||||||
|
</div>
|
||||||
|
<ColorPicker
|
||||||
|
spectrumTheme={$store.theme}
|
||||||
|
value={$store.navigation.navTextColor || DefaultAppTheme.navTextColor}
|
||||||
|
on:change={e => update("navTextColor", e.detail)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</Panel>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.generalSection {
|
||||||
|
padding: 13px 13px 25px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.customizeSection {
|
||||||
|
padding: 13px 13px 25px;
|
||||||
|
}
|
||||||
|
.subheading {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subheading :global(p) {
|
||||||
|
color: var(--grey-6);
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.divider {
|
||||||
|
border-top: 1px solid var(--grey-3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.controls {
|
||||||
|
position: relative;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 90px 1fr;
|
||||||
|
align-items: start;
|
||||||
|
transition: background 130ms ease-out, border-color 130ms ease-out;
|
||||||
|
border-left: 4px solid transparent;
|
||||||
|
margin: 0 calc(-1 * var(--spacing-xl));
|
||||||
|
padding: 0 var(--spacing-xl) 0 calc(var(--spacing-xl) - 4px);
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.label {
|
||||||
|
margin-top: 16px;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.info {
|
||||||
|
background-color: var(--background-alt);
|
||||||
|
padding: 12px;
|
||||||
|
display: flex;
|
||||||
|
border-radius: 4px;
|
||||||
|
gap: 4px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
.info :global(svg) {
|
||||||
|
margin-right: 5px;
|
||||||
|
color: var(--spectrum-global-color-gray-600);
|
||||||
|
}
|
||||||
|
|
||||||
|
.configureLinks :global(button) {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -1,12 +1,8 @@
|
||||||
<script>
|
<script>
|
||||||
import Panel from "components/design/Panel.svelte"
|
|
||||||
import { get } from "svelte/store"
|
import { get } from "svelte/store"
|
||||||
import { Helpers } from "@budibase/bbui"
|
import { Helpers } from "@budibase/bbui"
|
||||||
import {
|
import {
|
||||||
Input,
|
Input,
|
||||||
Layout,
|
|
||||||
Button,
|
|
||||||
Toggle,
|
|
||||||
Checkbox,
|
Checkbox,
|
||||||
Banner,
|
Banner,
|
||||||
Select,
|
Select,
|
||||||
|
@ -16,7 +12,6 @@
|
||||||
import RoleSelect from "components/design/settings/controls/RoleSelect.svelte"
|
import RoleSelect from "components/design/settings/controls/RoleSelect.svelte"
|
||||||
import { selectedScreen, store } from "builderStore"
|
import { selectedScreen, store } from "builderStore"
|
||||||
import sanitizeUrl from "builderStore/store/screenTemplates/utils/sanitizeUrl"
|
import sanitizeUrl from "builderStore/store/screenTemplates/utils/sanitizeUrl"
|
||||||
import { goto } from "@roxi/routify"
|
|
||||||
import ButtonActionEditor from "components/design/settings/controls/ButtonActionEditor/ButtonActionEditor.svelte"
|
import ButtonActionEditor from "components/design/settings/controls/ButtonActionEditor/ButtonActionEditor.svelte"
|
||||||
import { getBindableProperties } from "builderStore/dataBinding"
|
import { getBindableProperties } from "builderStore/dataBinding"
|
||||||
|
|
||||||
|
@ -119,15 +114,6 @@
|
||||||
label: "On screen load",
|
label: "On screen load",
|
||||||
control: ButtonActionEditor,
|
control: ButtonActionEditor,
|
||||||
},
|
},
|
||||||
{
|
|
||||||
key: "showNavigation",
|
|
||||||
label: "Navigation",
|
|
||||||
control: Toggle,
|
|
||||||
props: {
|
|
||||||
text: "Show nav",
|
|
||||||
disabled: !!$selectedScreen.layoutId,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
key: "width",
|
key: "width",
|
||||||
label: "Width",
|
label: "Width",
|
||||||
|
@ -145,14 +131,7 @@
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Panel
|
{#if $selectedScreen.layoutId}
|
||||||
title={$selectedScreen.routing.route}
|
|
||||||
icon={$selectedScreen.routing.route === "/" ? "Home" : "WebPage"}
|
|
||||||
borderLeft
|
|
||||||
wide
|
|
||||||
>
|
|
||||||
<Layout gap="S" paddingX="L" paddingY="XL">
|
|
||||||
{#if $selectedScreen.layoutId}
|
|
||||||
<Banner
|
<Banner
|
||||||
type="warning"
|
type="warning"
|
||||||
extraButtonText="Detach custom layout"
|
extraButtonText="Detach custom layout"
|
||||||
|
@ -161,8 +140,8 @@
|
||||||
>
|
>
|
||||||
This screen uses a custom layout, which is deprecated
|
This screen uses a custom layout, which is deprecated
|
||||||
</Banner>
|
</Banner>
|
||||||
{/if}
|
{/if}
|
||||||
{#each screenSettings as setting (setting.key)}
|
{#each screenSettings as setting (setting.key)}
|
||||||
<PropertyControl
|
<PropertyControl
|
||||||
control={setting.control}
|
control={setting.control}
|
||||||
label={setting.label}
|
label={setting.label}
|
||||||
|
@ -172,9 +151,4 @@
|
||||||
props={{ ...setting.props, error: errors[setting.key] }}
|
props={{ ...setting.props, error: errors[setting.key] }}
|
||||||
{bindings}
|
{bindings}
|
||||||
/>
|
/>
|
||||||
{/each}
|
{/each}
|
||||||
<Button secondary on:click={() => $goto("../components")}>
|
|
||||||
View components
|
|
||||||
</Button>
|
|
||||||
</Layout>
|
|
||||||
</Panel>
|
|
|
@ -0,0 +1,78 @@
|
||||||
|
<script>
|
||||||
|
import {
|
||||||
|
Layout,
|
||||||
|
Label,
|
||||||
|
ColorPicker,
|
||||||
|
notifications,
|
||||||
|
Icon,
|
||||||
|
Body,
|
||||||
|
} from "@budibase/bbui"
|
||||||
|
import { store } from "builderStore"
|
||||||
|
import { get } from "svelte/store"
|
||||||
|
import { DefaultAppTheme } from "constants"
|
||||||
|
import AppThemeSelect from "./AppThemeSelect.svelte"
|
||||||
|
import ButtonRoundnessSelect from "./ButtonRoundnessSelect.svelte"
|
||||||
|
import PropertyControl from "components/design/settings/controls/PropertyControl.svelte"
|
||||||
|
|
||||||
|
$: customTheme = $store.customTheme || {}
|
||||||
|
|
||||||
|
const update = async (property, value) => {
|
||||||
|
try {
|
||||||
|
store.actions.customTheme.save({
|
||||||
|
...get(store).customTheme,
|
||||||
|
[property]: value,
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
notifications.error("Error updating custom theme")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="info">
|
||||||
|
<Icon name="InfoOutline" size="S" />
|
||||||
|
<Body size="S">These settings apply to all screens</Body>
|
||||||
|
</div>
|
||||||
|
<Layout noPadding gap="S">
|
||||||
|
<Layout noPadding gap="XS">
|
||||||
|
<AppThemeSelect />
|
||||||
|
</Layout>
|
||||||
|
<Layout noPadding gap="XS">
|
||||||
|
<Label>Button roundness</Label>
|
||||||
|
<ButtonRoundnessSelect
|
||||||
|
{customTheme}
|
||||||
|
on:change={e => update("buttonBorderRadius", e.detail)}
|
||||||
|
/>
|
||||||
|
</Layout>
|
||||||
|
<PropertyControl
|
||||||
|
label="Accent color"
|
||||||
|
control={ColorPicker}
|
||||||
|
value={customTheme.primaryColor || DefaultAppTheme.primaryColor}
|
||||||
|
onChange={val => update("primaryColor", val)}
|
||||||
|
props={{
|
||||||
|
spectrumTheme: $store.theme,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<PropertyControl
|
||||||
|
label="Hover"
|
||||||
|
control={ColorPicker}
|
||||||
|
value={customTheme.primaryColorHover || DefaultAppTheme.primaryColorHover}
|
||||||
|
onChange={val => update("primaryColorHover", val)}
|
||||||
|
props={{
|
||||||
|
spectrumTheme: $store.theme,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Layout>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.info {
|
||||||
|
background-color: var(--background-alt);
|
||||||
|
padding: 12px;
|
||||||
|
display: flex;
|
||||||
|
border-radius: 4px;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
.info :global(svg) {
|
||||||
|
margin-right: 5px;
|
||||||
|
color: var(--spectrum-global-color-gray-600);
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,51 @@
|
||||||
|
<script>
|
||||||
|
import GeneralPanel from "./GeneralPanel.svelte"
|
||||||
|
import ThemePanel from "./ThemePanel.svelte"
|
||||||
|
import { selectedScreen } from "builderStore"
|
||||||
|
import Panel from "components/design/Panel.svelte"
|
||||||
|
import { capitalise } from "helpers"
|
||||||
|
import { ActionButton, Layout } from "@budibase/bbui"
|
||||||
|
|
||||||
|
let activeTab = "general"
|
||||||
|
const tabs = ["general", "theme"]
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Panel
|
||||||
|
title={$selectedScreen.routing.route}
|
||||||
|
icon={$selectedScreen.routing.route === "/" ? "Home" : "WebPage"}
|
||||||
|
borderLeft
|
||||||
|
wide
|
||||||
|
>
|
||||||
|
<div slot="panel-header-content">
|
||||||
|
<div class="settings-tabs">
|
||||||
|
{#each tabs as tab}
|
||||||
|
<ActionButton
|
||||||
|
size="M"
|
||||||
|
quiet
|
||||||
|
selected={activeTab === tab}
|
||||||
|
on:click={() => {
|
||||||
|
activeTab = tab
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{capitalise(tab)}
|
||||||
|
</ActionButton>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Layout gap="S" paddingX="L" paddingY="XL">
|
||||||
|
{#if activeTab === "theme"}
|
||||||
|
<ThemePanel />
|
||||||
|
{:else}
|
||||||
|
<GeneralPanel />
|
||||||
|
{/if}
|
||||||
|
</Layout>
|
||||||
|
</Panel>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.settings-tabs {
|
||||||
|
display: flex;
|
||||||
|
gap: var(--spacing-s);
|
||||||
|
padding: 0 var(--spacing-l);
|
||||||
|
padding-bottom: var(--spacing-l);
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,52 @@
|
||||||
|
<script>
|
||||||
|
import { syncURLToState } from "helpers/urlStateSync"
|
||||||
|
import { store, selectedScreen } from "builderStore"
|
||||||
|
import * as routify from "@roxi/routify"
|
||||||
|
import { onDestroy } from "svelte"
|
||||||
|
import { findComponent } from "builderStore/componentUtils"
|
||||||
|
import ComponentSettingsPanel from "./_components/Component/ComponentSettingsPanel.svelte"
|
||||||
|
import NavigationPanel from "./_components/Navigation/index.svelte"
|
||||||
|
import ScreenSettingsPanel from "./_components/Screen/index.svelte"
|
||||||
|
|
||||||
|
$: componentId = $store.selectedComponentId
|
||||||
|
$: store.actions.websocket.selectResource(componentId)
|
||||||
|
$: params = routify.params
|
||||||
|
$: routeComponentId = $params.componentId
|
||||||
|
|
||||||
|
// Hide new component panel whenever component ID changes
|
||||||
|
const closeNewComponentPanel = url => {
|
||||||
|
if (url?.endsWith("/new")) {
|
||||||
|
url = url.replace("/new", "")
|
||||||
|
}
|
||||||
|
return { url }
|
||||||
|
}
|
||||||
|
|
||||||
|
const validate = id => {
|
||||||
|
if (id === `${$store.selectedScreenId}-screen`) return true
|
||||||
|
if (id === `${$store.selectedScreenId}-navigation`) return true
|
||||||
|
|
||||||
|
return !!findComponent($selectedScreen.props, id)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Keep URL and state in sync for selected component ID
|
||||||
|
const stopSyncing = syncURLToState({
|
||||||
|
urlParam: "componentId",
|
||||||
|
stateKey: "selectedComponentId",
|
||||||
|
validate,
|
||||||
|
fallbackUrl: "../",
|
||||||
|
store,
|
||||||
|
routify,
|
||||||
|
beforeNavigate: closeNewComponentPanel,
|
||||||
|
})
|
||||||
|
|
||||||
|
onDestroy(stopSyncing)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if routeComponentId === `${$store.selectedScreenId}-screen`}
|
||||||
|
<ScreenSettingsPanel />
|
||||||
|
{:else if routeComponentId === `${$store.selectedScreenId}-navigation`}
|
||||||
|
<NavigationPanel />
|
||||||
|
{:else}
|
||||||
|
<ComponentSettingsPanel />
|
||||||
|
{/if}
|
||||||
|
<slot />
|
|
@ -0,0 +1 @@
|
||||||
|
<!-- Required to make Routify happy -->
|
|
@ -31,6 +31,10 @@
|
||||||
$: orderMap = createComponentOrderMap(componentList)
|
$: orderMap = createComponentOrderMap(componentList)
|
||||||
|
|
||||||
const getAllowedComponents = (allComponents, screen, component) => {
|
const getAllowedComponents = (allComponents, screen, component) => {
|
||||||
|
// Default to using the root screen container if no component specified
|
||||||
|
if (!component) {
|
||||||
|
component = screen.props
|
||||||
|
}
|
||||||
const path = findComponentPath(screen?.props, component?._id)
|
const path = findComponentPath(screen?.props, component?._id)
|
||||||
if (!path?.length) {
|
if (!path?.length) {
|
||||||
return []
|
return []
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue