commit
b2575ea352
|
@ -1,98 +1,149 @@
|
|||
name: Budibase CI
|
||||
|
||||
on:
|
||||
# Trigger the workflow on push or pull request,
|
||||
# but only for the master branch
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
- develop
|
||||
pull_request:
|
||||
on:
|
||||
# Trigger the workflow on push or pull request,
|
||||
# but only for the master branch
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
- develop
|
||||
workflow_dispatch:
|
||||
pull_request:
|
||||
branches:
|
||||
- master
|
||||
- develop
|
||||
workflow_dispatch:
|
||||
|
||||
env:
|
||||
BRANCH: ${{ github.event.pull_request.head.ref }}
|
||||
BASE_BRANCH: ${{ github.event.pull_request.base.ref}}
|
||||
PERSONAL_ACCESS_TOKEN : ${{ secrets.PERSONAL_ACCESS_TOKEN }}
|
||||
PERSONAL_ACCESS_TOKEN: ${{ secrets.PERSONAL_ACCESS_TOKEN }}
|
||||
|
||||
jobs:
|
||||
lint:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Use Node.js 14.x
|
||||
uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: 14.x
|
||||
- run: yarn
|
||||
- run: yarn lint
|
||||
- uses: actions/checkout@v3
|
||||
- name: Use Node.js 14.x
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 14.x
|
||||
cache: "yarn"
|
||||
- run: yarn
|
||||
- run: yarn lint
|
||||
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
submodules: true
|
||||
token: ${{ secrets.PERSONAL_ACCESS_TOKEN || github.token }}
|
||||
- name: Use Node.js 14.x
|
||||
uses: actions/setup-node@v1
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 14.x
|
||||
- name: Install Pro
|
||||
run: yarn install:pro $BRANCH $BASE_BRANCH
|
||||
cache: "yarn"
|
||||
- run: yarn
|
||||
- run: yarn bootstrap
|
||||
# Run build all the projects
|
||||
- run: yarn build
|
||||
# Check the types of the projects built via esbuild
|
||||
- run: yarn check:types
|
||||
|
||||
test:
|
||||
test-libraries:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
submodules: true
|
||||
token: ${{ secrets.PERSONAL_ACCESS_TOKEN || github.token }}
|
||||
- name: Use Node.js 14.x
|
||||
uses: actions/setup-node@v1
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 14.x
|
||||
- name: Install Pro
|
||||
run: yarn install:pro $BRANCH $BASE_BRANCH
|
||||
cache: "yarn"
|
||||
- run: yarn
|
||||
- run: yarn bootstrap
|
||||
- run: yarn build
|
||||
- run: yarn test
|
||||
- run: yarn test --ignore=@budibase/worker --ignore=@budibase/server --ignore=@budibase/pro
|
||||
- uses: codecov/codecov-action@v3
|
||||
with:
|
||||
token: ${{ secrets.CODECOV_TOKEN }} # not required for public repos
|
||||
name: codecov-umbrella
|
||||
verbose: true
|
||||
|
||||
test-services:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
submodules: true
|
||||
token: ${{ secrets.PERSONAL_ACCESS_TOKEN || github.token }}
|
||||
- name: Use Node.js 14.x
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 14.x
|
||||
cache: "yarn"
|
||||
- run: yarn
|
||||
- run: yarn test --scope=@budibase/worker --scope=@budibase/server
|
||||
- uses: codecov/codecov-action@v3
|
||||
with:
|
||||
token: ${{ secrets.CODECOV_TOKEN || github.token }} # not required for public repos
|
||||
name: codecov-umbrella
|
||||
verbose: true
|
||||
|
||||
test-pro:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
submodules: true
|
||||
token: ${{ secrets.PERSONAL_ACCESS_TOKEN || github.token }}
|
||||
- name: Use Node.js 14.x
|
||||
uses: actions/setup-node@v1
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 14.x
|
||||
- name: Install Pro
|
||||
run: yarn install:pro $BRANCH $BASE_BRANCH
|
||||
cache: "yarn"
|
||||
- run: yarn
|
||||
- run: yarn bootstrap
|
||||
- run: yarn test:pro
|
||||
- run: yarn test --scope=@budibase/pro
|
||||
|
||||
integration-test:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
submodules: true
|
||||
token: ${{ secrets.PERSONAL_ACCESS_TOKEN || github.token }}
|
||||
- name: Use Node.js 14.x
|
||||
uses: actions/setup-node@v1
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 14.x
|
||||
- name: Install Pro
|
||||
run: yarn install:pro $BRANCH $BASE_BRANCH
|
||||
- run: yarn && yarn bootstrap && yarn build
|
||||
- run: |
|
||||
cache: "yarn"
|
||||
- run: yarn
|
||||
- run: yarn build
|
||||
- name: Run tests
|
||||
run: |
|
||||
cd qa-core
|
||||
yarn setup
|
||||
yarn test:ci
|
||||
env:
|
||||
BB_ADMIN_USER_EMAIL: admin
|
||||
BB_ADMIN_USER_PASSWORD: admin
|
||||
|
||||
check-pro-submodule:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
submodules: true
|
||||
token: ${{ secrets.PERSONAL_ACCESS_TOKEN || github.token }}
|
||||
fetch-depth: 0
|
||||
- name: Check submodule
|
||||
run: |
|
||||
cd packages/pro
|
||||
git fetch
|
||||
if ! git merge-base --is-ancestor $(git log -n 1 --pretty=format:%H) origin/develop; then
|
||||
echo "Current commit has not been merged to develop"
|
||||
echo "Refer to the pro repo to merge your changes: https://github.com/Budibase/budibase-pro/blob/develop/docs/getting_started.md"
|
||||
exit 1
|
||||
else
|
||||
echo "All good, the submodule had been merged!"
|
||||
fi
|
||||
|
|
|
@ -1,21 +1,13 @@
|
|||
name: Budibase Prerelease
|
||||
concurrency: release-prerelease
|
||||
concurrency:
|
||||
group: release-prerelease
|
||||
cancel-in-progress: false
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- develop
|
||||
paths:
|
||||
- '.aws/**'
|
||||
- '.github/**'
|
||||
- 'charts/**'
|
||||
- 'packages/**'
|
||||
- 'scripts/**'
|
||||
- 'package.json'
|
||||
- 'yarn.lock'
|
||||
- 'package.json'
|
||||
- 'yarn.lock'
|
||||
workflow_dispatch:
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- v*-alpha.*
|
||||
workflow_dispatch:
|
||||
|
||||
env:
|
||||
# Posthog token used by ui at build time
|
||||
|
@ -24,43 +16,60 @@ env:
|
|||
INTERCOM_TOKEN: ${{ secrets.INTERCOM_TOKEN }}
|
||||
PERSONAL_ACCESS_TOKEN: ${{ secrets.PERSONAL_ACCESS_TOKEN }}
|
||||
FEATURE_PREVIEW_URL: https://budirelease.live
|
||||
|
||||
|
||||
jobs:
|
||||
release-images:
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Fail if branch is not develop
|
||||
if: github.ref != 'refs/heads/develop'
|
||||
run: |
|
||||
echo "Ref is not develop, you must run this job from develop."
|
||||
exit 1
|
||||
- uses: actions/checkout@v2
|
||||
with:
|
||||
submodules: true
|
||||
token: ${{ secrets.PERSONAL_ACCESS_TOKEN }}
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Fail if tag is not develop
|
||||
run: |
|
||||
if ! git merge-base --is-ancestor ${{ github.sha }} origin/develop; then
|
||||
echo "Tag is not in develop"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: 14.x
|
||||
|
||||
- name: Install Pro
|
||||
run: yarn install:pro develop
|
||||
|
||||
- run: yarn
|
||||
- run: yarn bootstrap
|
||||
- run: yarn build
|
||||
- run: yarn install --frozen-lockfile
|
||||
- name: Update versions
|
||||
run: |
|
||||
version=$(cat lerna.json \
|
||||
| grep version \
|
||||
| head -1 \
|
||||
| awk -F: '{gsub(/"/,"",$2);gsub(/[[:space:]]*/,"",$2); print $2}' \
|
||||
| sed 's/[",]//g')
|
||||
echo "Setting version $version"
|
||||
yarn lerna exec "yarn version --no-git-tag-version --new-version=$version"
|
||||
echo "Updating dependencies"
|
||||
node scripts/syncLocalDependencies.js $version
|
||||
echo "Syncing yarn workspace"
|
||||
yarn
|
||||
- run: yarn build --configuration=production
|
||||
- run: yarn build:sdk
|
||||
# - run: yarn test
|
||||
|
||||
- name: Publish budibase packages to NPM
|
||||
env:
|
||||
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
run: |
|
||||
run: |
|
||||
# setup the username and email.
|
||||
git config --global user.name "Budibase Staging Release Bot"
|
||||
git config --global user.email "<>"
|
||||
git submodule foreach git commit -a -m 'Release process'
|
||||
git commit -a -m 'Release process'
|
||||
echo //registry.npmjs.org/:_authToken=${NPM_TOKEN} >> .npmrc
|
||||
yarn release:develop
|
||||
|
||||
- name: Build/release Docker images
|
||||
run: |
|
||||
run: |
|
||||
docker login -u $DOCKER_USER -p $DOCKER_PASSWORD
|
||||
yarn build:docker:develop
|
||||
env:
|
||||
|
@ -84,7 +93,7 @@ jobs:
|
|||
git config user.name "Budibase Helm Bot"
|
||||
git config user.email "<>"
|
||||
git reset --hard
|
||||
git pull
|
||||
git fetch
|
||||
mkdir sync
|
||||
echo "Packaging chart to sync dir"
|
||||
helm package charts/budibase --version 0.0.0-develop --app-version develop --destination sync
|
||||
|
|
|
@ -1,60 +1,65 @@
|
|||
name: Budibase Release
|
||||
concurrency: release
|
||||
concurrency:
|
||||
group: release
|
||||
cancel-in-progress: false
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
paths:
|
||||
- '.aws/**'
|
||||
- '.github/**'
|
||||
- 'charts/**'
|
||||
- 'packages/**'
|
||||
- 'scripts/**'
|
||||
- 'package.json'
|
||||
- 'yarn.lock'
|
||||
- 'package.json'
|
||||
- 'yarn.lock'
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
versioning:
|
||||
type: choice
|
||||
description: "Versioning type: patch, minor, major"
|
||||
default: patch
|
||||
options:
|
||||
- patch
|
||||
- minor
|
||||
- major
|
||||
required: true
|
||||
push:
|
||||
tags:
|
||||
- "v[0-9]+.[0-9]+.[0-9]+"
|
||||
# Exclude all pre-releases
|
||||
- "!v*[0-9]+.[0-9]+.[0-9]+-*"
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
tags:
|
||||
description: "Release tag"
|
||||
required: true
|
||||
type: boolean
|
||||
|
||||
env:
|
||||
# Posthog token used by ui at build time
|
||||
# Posthog token used by ui at build time
|
||||
POSTHOG_TOKEN: phc_bIjZL7oh2GEUd2vqvTBH8WvrX0fWTFQMs6H5KQxiUxU
|
||||
INTERCOM_TOKEN: ${{ secrets.INTERCOM_TOKEN }}
|
||||
SENTRY_DSN: ${{ secrets.SENTRY_DSN }}
|
||||
PERSONAL_ACCESS_TOKEN : ${{ secrets.PERSONAL_ACCESS_TOKEN }}
|
||||
PERSONAL_ACCESS_TOKEN: ${{ secrets.PERSONAL_ACCESS_TOKEN }}
|
||||
|
||||
jobs:
|
||||
release-images:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Fail if branch is not master
|
||||
if: github.ref != 'refs/heads/master'
|
||||
run: |
|
||||
echo "Ref is not master, you must run this job from master."
|
||||
exit 1
|
||||
- uses: actions/checkout@v2
|
||||
with:
|
||||
submodules: true
|
||||
token: ${{ secrets.PERSONAL_ACCESS_TOKEN }}
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Fail if branch is not master
|
||||
if: github.ref != 'refs/heads/master'
|
||||
run: |
|
||||
echo "Ref is not master, you must run this job from master."
|
||||
// Change to "exit 1" when merged. Left to 0 to not fail all the pipelines and not to cause noise
|
||||
exit 0
|
||||
|
||||
- uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: 14.x
|
||||
|
||||
- name: Install Pro
|
||||
run: yarn install:pro master
|
||||
|
||||
- run: yarn
|
||||
- run: yarn bootstrap
|
||||
- run: yarn install --frozen-lockfile
|
||||
- name: Update versions
|
||||
run: |
|
||||
version=$(cat lerna.json \
|
||||
| grep version \
|
||||
| head -1 \
|
||||
| awk -F: '{gsub(/"/,"",$2);gsub(/[[:space:]]*/,"",$2); print $2}' \
|
||||
| sed 's/[",]//g')
|
||||
echo "Setting version $version"
|
||||
yarn lerna exec "yarn version --no-git-tag-version --new-version=$version"
|
||||
echo "Updating dependencies"
|
||||
node scripts/syncLocalDependencies.js $version
|
||||
echo "Syncing yarn workspace"
|
||||
yarn
|
||||
- run: yarn lint
|
||||
- run: yarn build
|
||||
- run: yarn build --configuration=production
|
||||
- run: yarn build:sdk
|
||||
|
||||
- name: Publish budibase packages to NPM
|
||||
|
@ -65,15 +70,17 @@ jobs:
|
|||
# setup the username and email. I tend to use 'GitHub Actions Bot' with no email by default
|
||||
git config --global user.name "Budibase Release Bot"
|
||||
git config --global user.email "<>"
|
||||
git submodule foreach git commit -a -m 'Release process'
|
||||
git commit -a -m 'Release process'
|
||||
echo //registry.npmjs.org/:_authToken=${NPM_TOKEN} >> .npmrc
|
||||
yarn release
|
||||
|
||||
- name: 'Get Previous tag'
|
||||
- name: "Get Previous tag"
|
||||
id: previoustag
|
||||
uses: "WyriHaximus/github-action-get-previous-tag@v1"
|
||||
|
||||
- name: Build/release Docker images
|
||||
run: |
|
||||
run: |
|
||||
docker login -u $DOCKER_USER -p $DOCKER_PASSWORD
|
||||
yarn build:docker
|
||||
env:
|
||||
|
@ -103,7 +110,7 @@ jobs:
|
|||
git config user.name "Budibase Helm Bot"
|
||||
git config user.email "<>"
|
||||
git reset --hard
|
||||
git pull
|
||||
git fetch
|
||||
mkdir sync
|
||||
echo "Packaging chart to sync dir"
|
||||
helm package charts/budibase --version 0.0.0-master --app-version v"$RELEASE_VERSION" --destination sync
|
||||
|
|
|
@ -0,0 +1,41 @@
|
|||
name: Tag prerelease
|
||||
concurrency:
|
||||
group: tag-prerelease
|
||||
cancel-in-progress: false
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- develop
|
||||
paths:
|
||||
- ".aws/**"
|
||||
- ".github/**"
|
||||
- "charts/**"
|
||||
- "packages/**"
|
||||
- "scripts/**"
|
||||
- "package.json"
|
||||
- "yarn.lock"
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
tag-prerelease:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Fail if branch is not develop
|
||||
if: github.ref != 'refs/heads/develop'
|
||||
run: |
|
||||
echo "Ref is not develop, you must run this job from develop."
|
||||
exit 1
|
||||
- uses: actions/checkout@v2
|
||||
with:
|
||||
submodules: true
|
||||
token: ${{ secrets.PERSONAL_ACCESS_TOKEN }}
|
||||
|
||||
- run: yarn
|
||||
- name: Tag prerelease
|
||||
run: |
|
||||
# setup the username and email.
|
||||
git config --global user.name "Budibase Staging Release Bot"
|
||||
git config --global user.email "<>"
|
||||
./scripts/versionCommit.sh prerelease
|
|
@ -0,0 +1,51 @@
|
|||
name: Tag release
|
||||
concurrency:
|
||||
group: tag-release
|
||||
cancel-in-progress: false
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
paths:
|
||||
- ".aws/**"
|
||||
- ".github/**"
|
||||
- "charts/**"
|
||||
- "packages/**"
|
||||
- "scripts/**"
|
||||
- "package.json"
|
||||
- "yarn.lock"
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
versioning:
|
||||
type: choice
|
||||
description: "Versioning type: patch, minor, major"
|
||||
default: patch
|
||||
options:
|
||||
- patch
|
||||
- minor
|
||||
- major
|
||||
required: true
|
||||
|
||||
jobs:
|
||||
tag-prerelease:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Fail if branch is not master
|
||||
if: github.ref != 'refs/heads/master'
|
||||
run: |
|
||||
echo "Ref is not master, you must run this job from master."
|
||||
exit 1
|
||||
- uses: actions/checkout@v2
|
||||
with:
|
||||
submodules: true
|
||||
token: ${{ secrets.PERSONAL_ACCESS_TOKEN }}
|
||||
|
||||
- run: yarn
|
||||
- name: Tag prerelease
|
||||
run: |
|
||||
# setup the username and email.
|
||||
git config --global user.name "Budibase Staging Release Bot"
|
||||
git config --global user.email "<>"
|
||||
./scripts/versionCommit.sh ${{ github.event.inputs.versioning }}
|
|
@ -0,0 +1,3 @@
|
|||
[submodule "packages/pro"]
|
||||
path = packages/pro
|
||||
url = git@github.com:Budibase/budibase-pro.git
|
|
@ -0,0 +1,4 @@
|
|||
# .husky/post-checkout
|
||||
# ...
|
||||
|
||||
git config submodule.recurse true
|
|
@ -1 +1 @@
|
|||
3.10.0
|
||||
3.10.0
|
||||
|
|
|
@ -144,8 +144,6 @@ The following commands can be executed to manually get Budibase up and running (
|
|||
|
||||
`yarn` to install project dependencies
|
||||
|
||||
`yarn bootstrap` will install all budibase modules and symlink them together using lerna.
|
||||
|
||||
`yarn build` will build all budibase packages.
|
||||
|
||||
#### 4. Running
|
||||
|
@ -243,7 +241,7 @@ An overview of the CI pipelines can be found [here](../.github/workflows/README.
|
|||
|
||||
Note that only budibase maintainers will be able to access the pro repo.
|
||||
|
||||
The `yarn bootstrap` command can be used to replace the NPM supplied dependency with the local source aware version. This is achieved using the `yarn link` command. To see specifically how dependencies are linked see [scripts/link-dependencies.sh](../scripts/link-dependencies.sh). The same link script is used to link dependencies to account-portal in local dev.
|
||||
By default, NX will make sure that dependencies are replaced with local source aware version. This is achieved using the `yarn link` command. To see specifically how dependencies are linked see [scripts/link-dependencies.sh](../scripts/link-dependencies.sh). The same link script is used to link dependencies to account-portal in local dev.
|
||||
|
||||
### Troubleshooting
|
||||
|
||||
|
|
|
@ -1,13 +1,17 @@
|
|||
## Dev Environment on Debian 11
|
||||
|
||||
### Install NVM & Node 14
|
||||
|
||||
NVM documentation: https://github.com/nvm-sh/nvm#installing-and-updating
|
||||
|
||||
Install NVM
|
||||
|
||||
```
|
||||
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.0/install.sh | bash
|
||||
```
|
||||
|
||||
Install Node 14
|
||||
|
||||
```
|
||||
nvm install 14
|
||||
```
|
||||
|
@ -17,13 +21,16 @@ nvm install 14
|
|||
```
|
||||
npm install -g yarn jest lerna
|
||||
```
|
||||
|
||||
### Install Docker and Docker Compose
|
||||
|
||||
```
|
||||
apt install docker.io
|
||||
pip3 install docker-compose
|
||||
```
|
||||
|
||||
### Clone the repo
|
||||
|
||||
```
|
||||
git clone https://github.com/Budibase/budibase.git
|
||||
```
|
||||
|
@ -44,10 +51,13 @@ This setup process was tested on Debian 11 (bullseye) with version numbers show
|
|||
cd budibase
|
||||
yarn setup
|
||||
```
|
||||
|
||||
The yarn setup command runs several build steps i.e.
|
||||
|
||||
```
|
||||
node ./hosting/scripts/setup.js && yarn && yarn bootstrap && yarn build && yarn dev
|
||||
```
|
||||
|
||||
So this command will actually run the application in dev mode. It creates .env files under `./packages/server` and `./packages/worker` and runs docker containers for each service via docker-compose.
|
||||
|
||||
The dev version will be available on port 10000 i.e.
|
||||
|
@ -55,6 +65,7 @@ The dev version will be available on port 10000 i.e.
|
|||
http://127.0.0.1:10000/builder/admin
|
||||
|
||||
### File descriptor issues with Vite and Chrome in Linux
|
||||
|
||||
If your dev environment stalls forever, with some network requests stuck in flight, it's likely that Chrome is trying to open more file descriptors than your system allows.
|
||||
To fix this, apply the following tweaks.
|
||||
|
||||
|
@ -62,4 +73,4 @@ Debian based distros:
|
|||
Add `* - nofile 65536` to `/etc/security/limits.conf`.
|
||||
|
||||
Arch:
|
||||
Add `DefaultLimitNOFILE=65536` to `/etc/systemd/system.conf`.
|
||||
Add `DefaultLimitNOFILE=65536` to `/etc/systemd/system.conf`.
|
||||
|
|
|
@ -4,14 +4,14 @@
|
|||
|
||||
Install instructions [here](https://brew.sh/)
|
||||
|
||||
| **NOTE**: If you are working on a M1 Apple Silicon which is running Z shell, you could need to add
|
||||
`eval $(/opt/homebrew/bin/brew shellenv)` line to your `.zshrc`. This will make your zsh to find the apps you install
|
||||
| **NOTE**: If you are working on a M1 Apple Silicon which is running Z shell, you could need to add
|
||||
`eval $(/opt/homebrew/bin/brew shellenv)` line to your `.zshrc`. This will make your zsh to find the apps you install
|
||||
through brew.
|
||||
|
||||
|
||||
### Install Node
|
||||
|
||||
Budibase requires a recent version of node 14:
|
||||
|
||||
```
|
||||
brew install node npm
|
||||
node -v
|
||||
|
@ -22,12 +22,15 @@ node -v
|
|||
```
|
||||
npm install -g yarn jest lerna
|
||||
```
|
||||
|
||||
### Install Docker and Docker Compose
|
||||
|
||||
```
|
||||
brew install docker docker-compose
|
||||
```
|
||||
|
||||
### Clone the repo
|
||||
|
||||
```
|
||||
git clone https://github.com/Budibase/budibase.git
|
||||
```
|
||||
|
@ -48,10 +51,13 @@ This setup process was tested on Mac OSX 12 (Monterey) with version numbers show
|
|||
cd budibase
|
||||
yarn setup
|
||||
```
|
||||
|
||||
The yarn setup command runs several build steps i.e.
|
||||
|
||||
```
|
||||
node ./hosting/scripts/setup.js && yarn && yarn bootstrap && yarn build && yarn dev
|
||||
```
|
||||
|
||||
So this command will actually run the application in dev mode. It creates .env files under `./packages/server` and `./packages/worker` and runs docker containers for each service via docker-compose.
|
||||
|
||||
The dev version will be available on port 10000 i.e.
|
||||
|
|
|
@ -1,13 +1,15 @@
|
|||
## Dev Environment on Windows 10/11 (WSL2)
|
||||
|
||||
|
||||
### Install WSL with Ubuntu LTS
|
||||
|
||||
Enable WSL 2 on Windows 10/11 for docker support.
|
||||
|
||||
```
|
||||
wsl --set-default-version 2
|
||||
```
|
||||
|
||||
Install Ubuntu LTS.
|
||||
|
||||
```
|
||||
wsl --install Ubuntu
|
||||
```
|
||||
|
@ -16,6 +18,7 @@ Or follow the instruction here:
|
|||
https://learn.microsoft.com/en-us/windows/wsl/install
|
||||
|
||||
### Install Docker in windows
|
||||
|
||||
Download the installer from docker and install it.
|
||||
|
||||
Check this url for more detailed instructions:
|
||||
|
@ -24,18 +27,21 @@ https://docs.docker.com/desktop/install/windows-install/
|
|||
You should follow the next steps from within the Ubuntu terminal.
|
||||
|
||||
### Install NVM & Node 14
|
||||
|
||||
NVM documentation: https://github.com/nvm-sh/nvm#installing-and-updating
|
||||
|
||||
Install NVM
|
||||
|
||||
```
|
||||
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.0/install.sh | bash
|
||||
```
|
||||
|
||||
Install Node 14
|
||||
|
||||
```
|
||||
nvm install 14
|
||||
```
|
||||
|
||||
|
||||
### Install npm requirements
|
||||
|
||||
```
|
||||
|
@ -43,6 +49,7 @@ npm install -g yarn jest lerna
|
|||
```
|
||||
|
||||
### Clone the repo
|
||||
|
||||
```
|
||||
git clone https://github.com/Budibase/budibase.git
|
||||
```
|
||||
|
@ -63,10 +70,13 @@ This setup process was tested on Windows 11 with version numbers show below. You
|
|||
cd budibase
|
||||
yarn setup
|
||||
```
|
||||
|
||||
The yarn setup command runs several build steps i.e.
|
||||
|
||||
```
|
||||
node ./hosting/scripts/setup.js && yarn && yarn bootstrap && yarn build && yarn dev
|
||||
```
|
||||
|
||||
So this command will actually run the application in dev mode. It creates .env files under `./packages/server` and `./packages/worker` and runs docker containers for each service via docker-compose.
|
||||
|
||||
The dev version will be available on port 10000 i.e.
|
||||
|
@ -74,8 +84,9 @@ The dev version will be available on port 10000 i.e.
|
|||
http://127.0.0.1:10000/builder/admin
|
||||
|
||||
### Working with the code
|
||||
|
||||
Here are the instructions to work on the application from within Visual Studio Code (in Windows) through the WSL. All the commands and files are within the Ubuntu system and it should run as if you were working on a Linux machine.
|
||||
|
||||
https://code.visualstudio.com/docs/remote/wsl
|
||||
|
||||
Note you will be able to run the application from within the WSL terminal and you will be able to access the application from the a browser in Windows.
|
||||
Note you will be able to run the application from within the WSL terminal and you will be able to access the application from the a browser in Windows.
|
||||
|
|
|
@ -0,0 +1,77 @@
|
|||
version: "3"
|
||||
|
||||
# optional ports are specified throughout for more advanced use cases.
|
||||
|
||||
services:
|
||||
app-service:
|
||||
build: ../packages/server
|
||||
container_name: build-bbapps
|
||||
environment:
|
||||
SELF_HOSTED: 1
|
||||
COUCH_DB_URL: http://${COUCH_DB_USER}:${COUCH_DB_PASSWORD}@couchdb-service:5984
|
||||
WORKER_URL: http://worker-service:4003
|
||||
MINIO_URL: http://minio-service:9000
|
||||
MINIO_ACCESS_KEY: ${MINIO_ACCESS_KEY}
|
||||
MINIO_SECRET_KEY: ${MINIO_SECRET_KEY}
|
||||
INTERNAL_API_KEY: ${INTERNAL_API_KEY}
|
||||
BUDIBASE_ENVIRONMENT: ${BUDIBASE_ENVIRONMENT}
|
||||
PORT: 4002
|
||||
API_ENCRYPTION_KEY: ${API_ENCRYPTION_KEY}
|
||||
JWT_SECRET: ${JWT_SECRET}
|
||||
LOG_LEVEL: info
|
||||
SENTRY_DSN: https://a34ae347621946bf8acded18e5b7d4b8@o420233.ingest.sentry.io/5338131
|
||||
ENABLE_ANALYTICS: "true"
|
||||
REDIS_URL: redis-service:6379
|
||||
REDIS_PASSWORD: ${REDIS_PASSWORD}
|
||||
BB_ADMIN_USER_EMAIL: ${BB_ADMIN_USER_EMAIL}
|
||||
BB_ADMIN_USER_PASSWORD: ${BB_ADMIN_USER_PASSWORD}
|
||||
PLUGINS_DIR: ${PLUGINS_DIR}
|
||||
depends_on:
|
||||
- worker-service
|
||||
- redis-service
|
||||
# volumes:
|
||||
# - /some/path/to/plugins:/plugins
|
||||
|
||||
worker-service:
|
||||
build: ../packages/worker
|
||||
container_name: build-bbworker
|
||||
environment:
|
||||
SELF_HOSTED: 1
|
||||
PORT: 4003
|
||||
CLUSTER_PORT: ${MAIN_PORT}
|
||||
API_ENCRYPTION_KEY: ${API_ENCRYPTION_KEY}
|
||||
JWT_SECRET: ${JWT_SECRET}
|
||||
MINIO_ACCESS_KEY: ${MINIO_ACCESS_KEY}
|
||||
MINIO_SECRET_KEY: ${MINIO_SECRET_KEY}
|
||||
MINIO_URL: http://minio-service:9000
|
||||
APPS_URL: http://app-service:4002
|
||||
COUCH_DB_USERNAME: ${COUCH_DB_USER}
|
||||
COUCH_DB_PASSWORD: ${COUCH_DB_PASSWORD}
|
||||
COUCH_DB_URL: http://${COUCH_DB_USER}:${COUCH_DB_PASSWORD}@couchdb-service:5984
|
||||
SENTRY_DSN: https://a34ae347621946bf8acded18e5b7d4b8@o420233.ingest.sentry.io/5338131
|
||||
INTERNAL_API_KEY: ${INTERNAL_API_KEY}
|
||||
REDIS_URL: redis-service:6379
|
||||
REDIS_PASSWORD: ${REDIS_PASSWORD}
|
||||
depends_on:
|
||||
- redis-service
|
||||
- minio-service
|
||||
|
||||
proxy-service-docker:
|
||||
ports:
|
||||
- "${MAIN_PORT}:10000"
|
||||
container_name: build-bbproxy
|
||||
image: budibase/proxy
|
||||
environment:
|
||||
- PROXY_RATE_LIMIT_WEBHOOKS_PER_SECOND=10
|
||||
- PROXY_RATE_LIMIT_API_PER_SECOND=20
|
||||
- APPS_UPSTREAM_URL=http://app-service:4002
|
||||
- WORKER_UPSTREAM_URL=http://worker-service:4003
|
||||
- MINIO_UPSTREAM_URL=http://minio-service:9000
|
||||
- COUCHDB_UPSTREAM_URL=http://couchdb-service:5984
|
||||
- WATCHTOWER_UPSTREAM_URL=http://watchtower-service:8080
|
||||
- RESOLVER=127.0.0.11
|
||||
depends_on:
|
||||
- minio-service
|
||||
- worker-service
|
||||
- app-service
|
||||
- couchdb-service
|
|
@ -1,22 +1,22 @@
|
|||
FROM node:14-slim as build
|
||||
FROM node:16-slim as build
|
||||
|
||||
# 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
|
||||
|
||||
# add pin script
|
||||
WORKDIR /
|
||||
ADD scripts/pinVersions.js scripts/cleanup.sh ./
|
||||
ADD scripts/cleanup.sh ./
|
||||
RUN chmod +x /cleanup.sh
|
||||
|
||||
# build server
|
||||
WORKDIR /app
|
||||
ADD packages/server .
|
||||
RUN node /pinVersions.js && yarn && yarn build && /cleanup.sh
|
||||
RUN yarn install --frozen-lockfile --production=true && /cleanup.sh
|
||||
|
||||
# build worker
|
||||
WORKDIR /worker
|
||||
ADD packages/worker .
|
||||
RUN node /pinVersions.js && yarn && yarn build && /cleanup.sh
|
||||
RUN yarn install --frozen-lockfile --production=true && /cleanup.sh
|
||||
|
||||
FROM budibase/couchdb
|
||||
ARG TARGETARCH
|
||||
|
@ -31,9 +31,7 @@ COPY --from=build /worker /worker
|
|||
|
||||
# install base dependencies
|
||||
RUN apt-get update && \
|
||||
apt-get install -y --no-install-recommends software-properties-common nginx uuid-runtime redis-server && \
|
||||
apt-add-repository 'deb http://security.debian.org/debian-security bullseye-security/updates main' && \
|
||||
apt-get update
|
||||
apt-get install -y --no-install-recommends software-properties-common nginx uuid-runtime redis-server
|
||||
|
||||
# install other dependencies, nodejs, oracle requirements, jdk8, redis, nginx
|
||||
WORKDIR /nodejs
|
||||
|
|
20
lerna.json
20
lerna.json
|
@ -1,8 +1,22 @@
|
|||
{
|
||||
"version": "2.6.23",
|
||||
"version": "2.6.24-alpha.0",
|
||||
"npmClient": "yarn",
|
||||
"packages": [
|
||||
"packages/backend-core",
|
||||
"packages/bbui",
|
||||
"packages/builder",
|
||||
"packages/cli",
|
||||
"packages/client",
|
||||
"packages/frontend-core",
|
||||
"packages/sdk",
|
||||
"packages/server",
|
||||
"packages/shared-core",
|
||||
"packages/string-templates",
|
||||
"packages/types",
|
||||
"packages/worker",
|
||||
"packages/pro/packages/pro"
|
||||
],
|
||||
"useWorkspaces": true,
|
||||
"packages": ["packages/*"],
|
||||
"command": {
|
||||
"publish": {
|
||||
"ignoreChanges": [
|
||||
|
@ -17,4 +31,4 @@
|
|||
"loadEnvFiles": false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
10
nx.json
10
nx.json
|
@ -6,5 +6,15 @@
|
|||
"cacheableOperations": ["build", "test"]
|
||||
}
|
||||
}
|
||||
},
|
||||
"targetDefaults": {
|
||||
"dev:builder": {
|
||||
"dependsOn": [
|
||||
{
|
||||
"projects": ["@budibase/string-templates"],
|
||||
"target": "build"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
69
package.json
69
package.json
|
@ -2,37 +2,44 @@
|
|||
"name": "root",
|
||||
"private": true,
|
||||
"devDependencies": {
|
||||
"@esbuild-plugins/node-resolve": "^0.2.2",
|
||||
"@esbuild-plugins/tsconfig-paths": "^0.1.2",
|
||||
"@nx/js": "16.2.1",
|
||||
"@rollup/plugin-json": "^4.0.2",
|
||||
"@typescript-eslint/parser": "5.45.0",
|
||||
"babel-eslint": "^10.0.3",
|
||||
"esbuild": "^0.17.18",
|
||||
"eslint": "^7.28.0",
|
||||
"eslint-plugin-cypress": "^2.11.3",
|
||||
"eslint-plugin-svelte3": "^3.2.0",
|
||||
"husky": "^7.0.1",
|
||||
"husky": "^8.0.3",
|
||||
"js-yaml": "^4.1.0",
|
||||
"kill-port": "^1.6.1",
|
||||
"lerna": "^6.6.1",
|
||||
"lerna": "7.0.0-alpha.0",
|
||||
"madge": "^6.0.0",
|
||||
"minimist": "^1.2.8",
|
||||
"nx": "^16.2.1",
|
||||
"prettier": "^2.3.1",
|
||||
"prettier-plugin-svelte": "^2.3.0",
|
||||
"rimraf": "^3.0.2",
|
||||
"rollup-plugin-replace": "^2.2.0",
|
||||
"semver": "^7.5.0",
|
||||
"svelte": "^3.38.2",
|
||||
"typescript": "4.7.3"
|
||||
},
|
||||
"scripts": {
|
||||
"setup": "node ./hosting/scripts/setup.js && yarn && yarn bootstrap && yarn build && yarn dev",
|
||||
"bootstrap": "lerna link && ./scripts/link-dependencies.sh",
|
||||
"build": "lerna run --stream build",
|
||||
"build:dev": "lerna run --stream prebuild && tsc --build --watch --preserveWatchOutput",
|
||||
"preinstall": "node scripts/syncProPackage.js",
|
||||
"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***'",
|
||||
"build": "yarn nx run-many -t=build",
|
||||
"build:dev": "lerna run --stream prebuild && yarn nx run-many --target=build --output-style=dynamic --watch --preserveWatchOutput",
|
||||
"check:types": "lerna run check:types --skip-nx-cache",
|
||||
"backend:bootstrap": "./scripts/scopeBackend.sh && yarn run bootstrap",
|
||||
"backend:build": "./scripts/scopeBackend.sh 'lerna run --stream build'",
|
||||
"build:sdk": "lerna run --stream build:sdk",
|
||||
"deps:circular": "madge packages/server/dist/index.js packages/worker/src/index.ts packages/backend-core/dist/src/index.js packages/cli/src/index.js --circular",
|
||||
"release": "lerna publish ${RELEASE_VERSION_TYPE:-patch} --yes --force-publish && yarn release:pro",
|
||||
"release:develop": "lerna publish prerelease --yes --force-publish --dist-tag develop --exact && yarn release:pro:develop",
|
||||
"release:pro": "bash scripts/pro/release.sh",
|
||||
"release:pro:develop": "bash scripts/pro/release.sh develop",
|
||||
"release": "lerna publish ${RELEASE_VERSION_TYPE:-patch} --yes --force-publish --no-git-tag-version --no-push --no-git-reset",
|
||||
"release:develop": "lerna publish from-package --yes --force-publish --dist-tag develop --exact --no-git-tag-version --no-push --no-git-reset",
|
||||
"restore": "yarn run clean && yarn run bootstrap && yarn run build",
|
||||
"nuke": "yarn run nuke:packages && yarn run nuke:docker",
|
||||
"nuke:packages": "yarn run restore",
|
||||
|
@ -41,12 +48,12 @@
|
|||
"kill-builder": "kill-port 3000",
|
||||
"kill-server": "kill-port 4001 4002",
|
||||
"kill-all": "yarn run kill-builder && yarn run kill-server",
|
||||
"dev": "yarn run kill-all && lerna link && lerna run --stream --parallel dev:builder --concurrency 1 --stream",
|
||||
"dev:noserver": "yarn run kill-builder && lerna link && lerna run --stream dev:stack:up && lerna run --stream --parallel dev:builder --concurrency 1 --ignore @budibase/backend-core --ignore @budibase/server --ignore @budibase/worker",
|
||||
"dev:server": "yarn run kill-server && lerna run --stream --parallel dev:builder --concurrency 1 --scope @budibase/backend-core --scope @budibase/worker --scope @budibase/server",
|
||||
"dev": "yarn run kill-all && lerna run --stream --parallel dev:builder --stream",
|
||||
"dev:noserver": "yarn run kill-builder && lerna run --stream dev:stack:up && lerna run --stream --parallel dev:builder --ignore @budibase/backend-core --ignore @budibase/server --ignore @budibase/worker",
|
||||
"dev:server": "yarn run kill-server && lerna run --stream --parallel dev:builder --scope @budibase/worker --scope @budibase/server",
|
||||
"dev:built": "yarn run kill-all && cd packages/server && yarn dev:stack:up && cd ../../ && lerna run --stream --parallel dev:built",
|
||||
"dev:docker": "yarn build && docker-compose -f hosting/docker-compose.build.yaml -f hosting/docker-compose.dev.yaml --env-file hosting/.env up --build --scale proxy-service=0",
|
||||
"test": "lerna run --stream test --stream",
|
||||
"test:pro": "bash scripts/pro/test.sh",
|
||||
"lint:eslint": "eslint packages && eslint qa-core",
|
||||
"lint:prettier": "prettier --check \"packages/**/*.{js,ts,svelte}\" && prettier --write \"examples/**/*.{js,ts,svelte}\" && prettier --check \"qa-core/**/*.{js,ts,svelte}\"",
|
||||
"lint": "yarn run lint:eslint && yarn run lint:prettier",
|
||||
|
@ -54,16 +61,16 @@
|
|||
"lint:fix:prettier": "prettier --write \"packages/**/*.{js,ts,svelte}\" && prettier --write \"examples/**/*.{js,ts,svelte}\" && prettier --write \"qa-core/**/*.{js,ts,svelte}\"",
|
||||
"lint:fix": "yarn run lint:fix:prettier && yarn run lint:fix:eslint",
|
||||
"build:specs": "lerna run --stream specs",
|
||||
"build:docker": "lerna run --stream build:docker && npm run build:docker:proxy && cd hosting/scripts/linux/ && ./release-to-docker-hub.sh $BUDIBASE_RELEASE_VERSION && cd -",
|
||||
"build:docker": "lerna run --stream build:docker && yarn build:docker:proxy && cd hosting/scripts/linux/ && ./release-to-docker-hub.sh $BUDIBASE_RELEASE_VERSION && cd -",
|
||||
"build:docker:pre": "lerna run --stream build && lerna run --stream predocker",
|
||||
"build:docker:proxy": "docker build hosting/proxy -t proxy-service",
|
||||
"build:docker:selfhost": "lerna run --stream build:docker && cd hosting/scripts/linux/ && ./release-to-docker-hub.sh latest && cd -",
|
||||
"build:docker:develop": "node scripts/pinVersions && lerna run --stream build:docker && npm run build:docker:proxy && cd hosting/scripts/linux/ && ./release-to-docker-hub.sh develop && cd -",
|
||||
"build:docker:develop": "node scripts/pinVersions && lerna run --stream build:docker && yarn build:docker:proxy && cd hosting/scripts/linux/ && ./release-to-docker-hub.sh develop && cd -",
|
||||
"build:docker:airgap": "node hosting/scripts/airgapped/airgappedDockerBuild",
|
||||
"build:digitalocean": "cd hosting/digitalocean && ./build.sh && cd -",
|
||||
"build:docker:single:multiarch": "docker buildx build --platform linux/arm64,linux/amd64 -f hosting/single/Dockerfile -t budibase:latest .",
|
||||
"build:docker:single:image": "docker build -f hosting/single/Dockerfile -t budibase:latest .",
|
||||
"build:docker:single": "npm run build:docker:pre && npm run build:docker:single:image",
|
||||
"build:docker:single": "yarn build && lerna run --concurrency 1 predocker && yarn build:docker:single:image",
|
||||
"build:docker:dependencies": "docker build -f hosting/dependencies/Dockerfile -t budibase/dependencies:latest ./hosting",
|
||||
"publish:docker:couch": "docker buildx build --platform linux/arm64,linux/amd64 -f hosting/couchdb/Dockerfile -t budibase/couchdb:latest -t budibase/couchdb:v3.2.1 --push ./hosting/couchdb",
|
||||
"publish:docker:dependencies": "docker buildx build --platform linux/arm64,linux/amd64 -f hosting/dependencies/Dockerfile -t budibase/dependencies:latest -t budibase/dependencies:v3.2.1 --push ./hosting",
|
||||
|
@ -82,12 +89,32 @@
|
|||
"mode:account": "yarn mode:cloud && yarn env:account:enable",
|
||||
"security:audit": "node scripts/audit.js",
|
||||
"postinstall": "husky install",
|
||||
"install:pro": "bash scripts/pro/install.sh",
|
||||
"dep:clean": "yarn clean && yarn bootstrap"
|
||||
"dep:clean": "yarn clean -y && yarn bootstrap",
|
||||
"submodules:load": "git submodule init && git submodule update && yarn && yarn bootstrap",
|
||||
"submodules:unload": "git submodule deinit --all && yarn && yarn bootstrap"
|
||||
},
|
||||
"workspaces": {
|
||||
"packages": [
|
||||
"packages/*"
|
||||
"packages/backend-core",
|
||||
"packages/bbui",
|
||||
"packages/builder",
|
||||
"packages/cli",
|
||||
"packages/client",
|
||||
"packages/frontend-core",
|
||||
"packages/sdk",
|
||||
"packages/server",
|
||||
"packages/shared-core",
|
||||
"packages/string-templates",
|
||||
"packages/types",
|
||||
"packages/worker",
|
||||
"packages/pro/packages/pro"
|
||||
]
|
||||
}
|
||||
},
|
||||
"resolutions": {
|
||||
"@budibase/backend-core": "0.0.0",
|
||||
"@budibase/shared-core": "0.0.0",
|
||||
"@budibase/string-templates": "0.0.0",
|
||||
"@budibase/types": "0.0.0"
|
||||
},
|
||||
"dependencies": {}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@budibase/backend-core",
|
||||
"version": "2.6.23",
|
||||
"version": "0.0.0",
|
||||
"description": "Budibase backend core libraries used in server and worker",
|
||||
"main": "dist/src/index.js",
|
||||
"types": "dist/src/index.d.ts",
|
||||
|
@ -15,8 +15,6 @@
|
|||
"prebuild": "rimraf dist/",
|
||||
"prepack": "cp package.json dist",
|
||||
"build": "tsc -p tsconfig.build.json",
|
||||
"build:pro": "../../scripts/pro/build.sh",
|
||||
"postbuild": "yarn run build:pro",
|
||||
"build:dev": "yarn prebuild && tsc --build --watch --preserveWatchOutput",
|
||||
"test": "bash scripts/test.sh",
|
||||
"test:watch": "jest --watchAll"
|
||||
|
@ -24,7 +22,7 @@
|
|||
"dependencies": {
|
||||
"@budibase/nano": "10.1.2",
|
||||
"@budibase/pouchdb-replication-stream": "1.2.10",
|
||||
"@budibase/types": "^2.6.23",
|
||||
"@budibase/types": "0.0.0",
|
||||
"@shopify/jest-koa-mocks": "5.0.1",
|
||||
"@techpass/passport-openidconnect": "0.3.2",
|
||||
"aws-cloudfront-sign": "2.2.0",
|
||||
|
@ -35,7 +33,7 @@
|
|||
"correlation-id": "4.0.0",
|
||||
"dotenv": "16.0.1",
|
||||
"emitter-listener": "1.1.2",
|
||||
"ioredis": "4.28.0",
|
||||
"ioredis": "5.3.2",
|
||||
"joi": "17.6.0",
|
||||
"jsonwebtoken": "9.0.0",
|
||||
"koa-passport": "4.1.4",
|
||||
|
@ -64,7 +62,6 @@
|
|||
"@swc/jest": "^0.2.24",
|
||||
"@trendyol/jest-testcontainers": "^2.1.1",
|
||||
"@types/chance": "1.1.3",
|
||||
"@types/ioredis": "4.28.0",
|
||||
"@types/jest": "29.5.0",
|
||||
"@types/koa": "2.13.4",
|
||||
"@types/lodash": "4.14.180",
|
||||
|
@ -76,7 +73,7 @@
|
|||
"@types/tar-fs": "2.0.1",
|
||||
"@types/uuid": "8.3.4",
|
||||
"chance": "1.1.8",
|
||||
"ioredis-mock": "5.8.0",
|
||||
"ioredis-mock": "8.7.0",
|
||||
"jest": "29.5.0",
|
||||
"jest-environment-node": "29.5.0",
|
||||
"jest-serial-runner": "^1.2.1",
|
||||
|
@ -90,5 +87,19 @@
|
|||
"tsconfig-paths": "4.0.0",
|
||||
"typescript": "4.7.3"
|
||||
},
|
||||
"nx": {
|
||||
"targets": {
|
||||
"build": {
|
||||
"dependsOn": [
|
||||
{
|
||||
"projects": [
|
||||
"@budibase/types"
|
||||
],
|
||||
"target": "build"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"gitHead": "d1836a898cab3f8ab80ee6d8f42be1a9eed7dcdc"
|
||||
}
|
||||
|
|
|
@ -72,16 +72,12 @@ describe("writethrough", () => {
|
|||
writethrough.put({ ...current, value: 4 }),
|
||||
])
|
||||
|
||||
// with a lock, this will work
|
||||
const newRev = responses.map(x => x.rev).find(x => x !== current._rev)
|
||||
expect(newRev).toBeDefined()
|
||||
expect(responses.map(x => x.rev)).toEqual(
|
||||
expect.arrayContaining([current._rev, current._rev, newRev])
|
||||
)
|
||||
expectFunctionWasCalledTimesWith(
|
||||
mocks.alerts.logWarn,
|
||||
2,
|
||||
"Ignoring redlock conflict in write-through cache"
|
||||
)
|
||||
|
||||
const output = await db.get(current._id)
|
||||
expect(output.value).toBe(4)
|
||||
|
|
|
@ -21,7 +21,7 @@ export enum ViewName {
|
|||
AUTOMATION_LOGS = "automation_logs",
|
||||
ACCOUNT_BY_EMAIL = "account_by_email",
|
||||
PLATFORM_USERS_LOWERCASE = "platform_users_lowercase",
|
||||
USER_BY_GROUP = "by_group_user",
|
||||
USER_BY_GROUP = "user_by_group",
|
||||
APP_BACKUP_BY_TRIGGER = "by_trigger",
|
||||
}
|
||||
|
||||
|
|
|
@ -16,6 +16,7 @@ export enum Header {
|
|||
LICENSE_KEY = "x-budibase-license-key",
|
||||
API_VER = "x-budibase-api-version",
|
||||
APP_ID = "x-budibase-app-id",
|
||||
SESSION_ID = "x-budibase-session-id",
|
||||
TYPE = "x-budibase-type",
|
||||
PREVIEW_ROLE = "x-budibase-role",
|
||||
TENANT_ID = "x-budibase-tenant-id",
|
||||
|
|
|
@ -12,7 +12,7 @@ import {
|
|||
isDocument,
|
||||
} from "@budibase/types"
|
||||
import { getCouchInfo } from "./connections"
|
||||
import { directCouchCall } from "./utils"
|
||||
import { directCouchUrlCall } from "./utils"
|
||||
import { getPouchDB } from "./pouchDB"
|
||||
import { WriteStream, ReadStream } from "fs"
|
||||
import { newid } from "../../docIds/newid"
|
||||
|
@ -46,6 +46,8 @@ export class DatabaseImpl implements Database {
|
|||
private readonly instanceNano?: Nano.ServerScope
|
||||
private readonly pouchOpts: DatabaseOpts
|
||||
|
||||
private readonly couchInfo = getCouchInfo()
|
||||
|
||||
constructor(dbName?: string, opts?: DatabaseOpts, connection?: string) {
|
||||
if (dbName == null) {
|
||||
throw new Error("Database name cannot be undefined.")
|
||||
|
@ -53,8 +55,8 @@ export class DatabaseImpl implements Database {
|
|||
this.name = dbName
|
||||
this.pouchOpts = opts || {}
|
||||
if (connection) {
|
||||
const couchInfo = getCouchInfo(connection)
|
||||
this.instanceNano = buildNano(couchInfo)
|
||||
this.couchInfo = getCouchInfo(connection)
|
||||
this.instanceNano = buildNano(this.couchInfo)
|
||||
}
|
||||
if (!DatabaseImpl.nano) {
|
||||
DatabaseImpl.init()
|
||||
|
@ -67,7 +69,11 @@ export class DatabaseImpl implements Database {
|
|||
}
|
||||
|
||||
async exists() {
|
||||
let response = await directCouchCall(`/${this.name}`, "HEAD")
|
||||
const response = await directCouchUrlCall({
|
||||
url: `${this.couchInfo.url}/${this.name}`,
|
||||
method: "HEAD",
|
||||
cookie: this.couchInfo.cookie,
|
||||
})
|
||||
return response.status === 200
|
||||
}
|
||||
|
||||
|
|
|
@ -4,21 +4,21 @@ export const getCouchInfo = (connection?: string) => {
|
|||
const urlInfo = getUrlInfo(connection)
|
||||
let username
|
||||
let password
|
||||
if (env.COUCH_DB_USERNAME) {
|
||||
// set from env
|
||||
username = env.COUCH_DB_USERNAME
|
||||
} else if (urlInfo.auth.username) {
|
||||
if (urlInfo.auth?.username) {
|
||||
// set from url
|
||||
username = urlInfo.auth.username
|
||||
} else if (env.COUCH_DB_USERNAME) {
|
||||
// set from env
|
||||
username = env.COUCH_DB_USERNAME
|
||||
} else if (!env.isTest()) {
|
||||
throw new Error("CouchDB username not set")
|
||||
}
|
||||
if (env.COUCH_DB_PASSWORD) {
|
||||
// set from env
|
||||
password = env.COUCH_DB_PASSWORD
|
||||
} else if (urlInfo.auth.password) {
|
||||
if (urlInfo.auth?.password) {
|
||||
// set from url
|
||||
password = urlInfo.auth.password
|
||||
} else if (env.COUCH_DB_PASSWORD) {
|
||||
// set from env
|
||||
password = env.COUCH_DB_PASSWORD
|
||||
} else if (!env.isTest()) {
|
||||
throw new Error("CouchDB password not set")
|
||||
}
|
||||
|
|
|
@ -9,6 +9,20 @@ export async function directCouchCall(
|
|||
) {
|
||||
let { url, cookie } = getCouchInfo()
|
||||
const couchUrl = `${url}/${path}`
|
||||
return await directCouchUrlCall({ url: couchUrl, cookie, method, body })
|
||||
}
|
||||
|
||||
export async function directCouchUrlCall({
|
||||
url,
|
||||
cookie,
|
||||
method,
|
||||
body,
|
||||
}: {
|
||||
url: string
|
||||
cookie: string
|
||||
method: string
|
||||
body?: any
|
||||
}) {
|
||||
const params: any = {
|
||||
method: method,
|
||||
headers: {
|
||||
|
@ -19,7 +33,7 @@ export async function directCouchCall(
|
|||
params.body = JSON.stringify(body)
|
||||
params.headers["Content-Type"] = "application/json"
|
||||
}
|
||||
return await fetch(checkSlashesInUrl(encodeURI(couchUrl)), params)
|
||||
return await fetch(checkSlashesInUrl(encodeURI(url)), params)
|
||||
}
|
||||
|
||||
export async function directCouchQuery(
|
||||
|
|
|
@ -97,7 +97,6 @@ const environment = {
|
|||
REDIS_URL: process.env.REDIS_URL || "localhost:6379",
|
||||
REDIS_PASSWORD: process.env.REDIS_PASSWORD,
|
||||
REDIS_CLUSTERED: process.env.REDIS_CLUSTERED,
|
||||
MOCK_REDIS: process.env.MOCK_REDIS,
|
||||
MINIO_ACCESS_KEY: process.env.MINIO_ACCESS_KEY,
|
||||
MINIO_SECRET_KEY: process.env.MINIO_SECRET_KEY,
|
||||
AWS_REGION: process.env.AWS_REGION,
|
||||
|
@ -129,6 +128,7 @@ const environment = {
|
|||
PLUGIN_BUCKET_NAME:
|
||||
process.env.PLUGIN_BUCKET_NAME || DefaultBucketName.PLUGINS,
|
||||
USE_COUCH: process.env.USE_COUCH || true,
|
||||
MOCK_REDIS: process.env.MOCK_REDIS,
|
||||
DEFAULT_LICENSE: process.env.DEFAULT_LICENSE,
|
||||
SERVICE: process.env.SERVICE || "budibase",
|
||||
LOG_LEVEL: process.env.LOG_LEVEL || "info",
|
||||
|
|
|
@ -21,6 +21,7 @@ export * as context from "./context"
|
|||
export * as cache from "./cache"
|
||||
export * as objectStore from "./objectStore"
|
||||
export * as redis from "./redis"
|
||||
export { Client as RedisClient } from "./redis"
|
||||
export * as locks from "./redis/redlockImpl"
|
||||
export * as utils from "./utils"
|
||||
export * as errors from "./errors"
|
||||
|
|
|
@ -96,6 +96,7 @@ if (!env.DISABLE_PINO_LOGGER) {
|
|||
|
||||
const mergingObject: any = {
|
||||
err: error,
|
||||
pid: process.pid,
|
||||
...contextObject,
|
||||
}
|
||||
|
||||
|
|
|
@ -6,7 +6,8 @@ let userClient: Client,
|
|||
appClient: Client,
|
||||
cacheClient: Client,
|
||||
writethroughClient: Client,
|
||||
lockClient: Client
|
||||
lockClient: Client,
|
||||
socketClient: Client
|
||||
|
||||
async function init() {
|
||||
userClient = await new Client(utils.Databases.USER_CACHE).init()
|
||||
|
@ -14,9 +15,10 @@ async function init() {
|
|||
appClient = await new Client(utils.Databases.APP_METADATA).init()
|
||||
cacheClient = await new Client(utils.Databases.GENERIC_CACHE).init()
|
||||
lockClient = await new Client(utils.Databases.LOCKS).init()
|
||||
writethroughClient = await new Client(
|
||||
utils.Databases.WRITE_THROUGH,
|
||||
utils.SelectableDatabase.WRITE_THROUGH
|
||||
writethroughClient = await new Client(utils.Databases.WRITE_THROUGH).init()
|
||||
socketClient = await new Client(
|
||||
utils.Databases.SOCKET_IO,
|
||||
utils.SelectableDatabase.SOCKET_IO
|
||||
).init()
|
||||
}
|
||||
|
||||
|
@ -27,6 +29,7 @@ export async function shutdown() {
|
|||
if (cacheClient) await cacheClient.finish()
|
||||
if (writethroughClient) await writethroughClient.finish()
|
||||
if (lockClient) await lockClient.finish()
|
||||
if (socketClient) await socketClient.finish()
|
||||
}
|
||||
|
||||
process.on("exit", async () => {
|
||||
|
@ -74,3 +77,10 @@ export async function getLockClient() {
|
|||
}
|
||||
return lockClient
|
||||
}
|
||||
|
||||
export async function getSocketClient() {
|
||||
if (!socketClient) {
|
||||
await init()
|
||||
}
|
||||
return socketClient
|
||||
}
|
||||
|
|
|
@ -1,6 +1,15 @@
|
|||
import env from "../environment"
|
||||
// ioredis mock is all in memory
|
||||
const Redis = env.MOCK_REDIS ? require("ioredis-mock") : require("ioredis")
|
||||
import Redis from "ioredis"
|
||||
// mock-redis doesn't have any typing
|
||||
let MockRedis: any | undefined
|
||||
if (env.MOCK_REDIS) {
|
||||
try {
|
||||
// ioredis mock is all in memory
|
||||
MockRedis = require("ioredis-mock")
|
||||
} catch (err) {
|
||||
console.log("Mock redis unavailable")
|
||||
}
|
||||
}
|
||||
import {
|
||||
addDbPrefix,
|
||||
removeDbPrefix,
|
||||
|
@ -18,7 +27,7 @@ const DEFAULT_SELECT_DB = SelectableDatabase.DEFAULT
|
|||
// for testing just generate the client once
|
||||
let CLOSED = false
|
||||
let CLIENTS: { [key: number]: any } = {}
|
||||
|
||||
0
|
||||
let CONNECTED = false
|
||||
|
||||
// mock redis always connected
|
||||
|
@ -55,6 +64,7 @@ function connectionError(
|
|||
* will return the ioredis client which will be ready to use.
|
||||
*/
|
||||
function init(selectDb = DEFAULT_SELECT_DB) {
|
||||
const RedisCore = env.MOCK_REDIS && MockRedis ? MockRedis : Redis
|
||||
let timeout: NodeJS.Timeout
|
||||
CLOSED = false
|
||||
let client = pickClient(selectDb)
|
||||
|
@ -64,7 +74,7 @@ function init(selectDb = DEFAULT_SELECT_DB) {
|
|||
}
|
||||
// testing uses a single in memory client
|
||||
if (env.MOCK_REDIS) {
|
||||
CLIENTS[selectDb] = new Redis(getRedisOptions())
|
||||
CLIENTS[selectDb] = new RedisCore(getRedisOptions())
|
||||
}
|
||||
// start the timer - only allowed 5 seconds to connect
|
||||
timeout = setTimeout(() => {
|
||||
|
@ -84,11 +94,11 @@ function init(selectDb = DEFAULT_SELECT_DB) {
|
|||
const { redisProtocolUrl, opts, host, port } = getRedisOptions()
|
||||
|
||||
if (CLUSTERED) {
|
||||
client = new Redis.Cluster([{ host, port }], opts)
|
||||
client = new RedisCore.Cluster([{ host, port }], opts)
|
||||
} else if (redisProtocolUrl) {
|
||||
client = new Redis(redisProtocolUrl)
|
||||
client = new RedisCore(redisProtocolUrl)
|
||||
} else {
|
||||
client = new Redis(opts)
|
||||
client = new RedisCore(opts)
|
||||
}
|
||||
// attach handlers
|
||||
client.on("end", (err: Error) => {
|
||||
|
@ -183,6 +193,9 @@ class RedisWrapper {
|
|||
CLOSED = false
|
||||
init(this._select)
|
||||
await waitForConnection(this._select)
|
||||
if (this._select && !env.isTest()) {
|
||||
this.getClient().select(this._select)
|
||||
}
|
||||
return this
|
||||
}
|
||||
|
||||
|
@ -209,6 +222,11 @@ class RedisWrapper {
|
|||
return this.getClient().keys(addDbPrefix(db, pattern))
|
||||
}
|
||||
|
||||
async exists(key: string) {
|
||||
const db = this._db
|
||||
return await this.getClient().exists(addDbPrefix(db, key))
|
||||
}
|
||||
|
||||
async get(key: string) {
|
||||
const db = this._db
|
||||
let response = await this.getClient().get(addDbPrefix(db, key))
|
||||
|
|
|
@ -4,10 +4,10 @@ import { LockOptions, LockType } from "@budibase/types"
|
|||
import * as context from "../context"
|
||||
import env from "../environment"
|
||||
|
||||
const getClient = async (
|
||||
async function getClient(
|
||||
type: LockType,
|
||||
opts?: Redlock.Options
|
||||
): Promise<Redlock> => {
|
||||
): Promise<Redlock> {
|
||||
if (type === LockType.CUSTOM) {
|
||||
return newRedlock(opts)
|
||||
}
|
||||
|
@ -18,6 +18,9 @@ const getClient = async (
|
|||
case LockType.TRY_ONCE: {
|
||||
return newRedlock(OPTIONS.TRY_ONCE)
|
||||
}
|
||||
case LockType.TRY_TWICE: {
|
||||
return newRedlock(OPTIONS.TRY_TWICE)
|
||||
}
|
||||
case LockType.DEFAULT: {
|
||||
return newRedlock(OPTIONS.DEFAULT)
|
||||
}
|
||||
|
@ -35,6 +38,9 @@ const OPTIONS = {
|
|||
// immediately throws an error if the lock is already held
|
||||
retryCount: 0,
|
||||
},
|
||||
TRY_TWICE: {
|
||||
retryCount: 1,
|
||||
},
|
||||
TEST: {
|
||||
// higher retry count in unit tests
|
||||
// due to high contention.
|
||||
|
@ -62,7 +68,7 @@ const OPTIONS = {
|
|||
},
|
||||
}
|
||||
|
||||
const newRedlock = async (opts: Redlock.Options = {}) => {
|
||||
export async function newRedlock(opts: Redlock.Options = {}) {
|
||||
let options = { ...OPTIONS.DEFAULT, ...opts }
|
||||
const redisWrapper = await getLockClient()
|
||||
const client = redisWrapper.getClient()
|
||||
|
@ -81,22 +87,26 @@ type RedlockExecution<T> =
|
|||
| SuccessfulRedlockExecution<T>
|
||||
| UnsuccessfulRedlockExecution
|
||||
|
||||
export const doWithLock = async <T>(
|
||||
function getLockName(opts: LockOptions) {
|
||||
// determine lock name
|
||||
// by default use the tenantId for uniqueness, unless using a system lock
|
||||
const prefix = opts.systemLock ? "system" : context.getTenantId()
|
||||
let name: string = `lock:${prefix}_${opts.name}`
|
||||
// add additional unique name if required
|
||||
if (opts.resource) {
|
||||
name = name + `_${opts.resource}`
|
||||
}
|
||||
return name
|
||||
}
|
||||
|
||||
export async function doWithLock<T>(
|
||||
opts: LockOptions,
|
||||
task: () => Promise<T>
|
||||
): Promise<RedlockExecution<T>> => {
|
||||
): Promise<RedlockExecution<T>> {
|
||||
const redlock = await getClient(opts.type, opts.customOptions)
|
||||
let lock
|
||||
try {
|
||||
// determine lock name
|
||||
// by default use the tenantId for uniqueness, unless using a system lock
|
||||
const prefix = opts.systemLock ? "system" : context.getTenantId()
|
||||
let name: string = `lock:${prefix}_${opts.name}`
|
||||
|
||||
// add additional unique name if required
|
||||
if (opts.resource) {
|
||||
name = name + `_${opts.resource}`
|
||||
}
|
||||
const name = getLockName(opts)
|
||||
|
||||
// create the lock
|
||||
lock = await redlock.lock(name, opts.ttl)
|
||||
|
@ -112,7 +122,6 @@ export const doWithLock = async <T>(
|
|||
if (opts.type === LockType.TRY_ONCE) {
|
||||
// don't throw for try-once locks, they will always error
|
||||
// due to retry count (0) exceeded
|
||||
console.warn(e)
|
||||
return { executed: false }
|
||||
} else {
|
||||
console.error(e)
|
||||
|
|
|
@ -27,6 +27,7 @@ export enum Databases {
|
|||
GENERIC_CACHE = "data_cache",
|
||||
WRITE_THROUGH = "writeThrough",
|
||||
LOCKS = "locks",
|
||||
SOCKET_IO = "socket_io",
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -40,7 +41,7 @@ export enum Databases {
|
|||
*/
|
||||
export enum SelectableDatabase {
|
||||
DEFAULT = 0,
|
||||
WRITE_THROUGH = 1,
|
||||
SOCKET_IO = 1,
|
||||
UNUSED_1 = 2,
|
||||
UNUSED_2 = 3,
|
||||
UNUSED_3 = 4,
|
||||
|
@ -94,7 +95,7 @@ export function getRedisOptions() {
|
|||
opts.port = port
|
||||
opts.password = password
|
||||
}
|
||||
return { opts, host, port, redisProtocolUrl }
|
||||
return { opts, host, port: parseInt(port), redisProtocolUrl }
|
||||
}
|
||||
|
||||
export function addDbPrefix(db: string, key: string) {
|
||||
|
|
|
@ -5,6 +5,7 @@ import * as db from "../../db"
|
|||
import { Header } from "../../constants"
|
||||
import { newid } from "../../utils"
|
||||
import env from "../../environment"
|
||||
import { BBContext } from "@budibase/types"
|
||||
|
||||
describe("utils", () => {
|
||||
const config = new DBTestConfiguration()
|
||||
|
@ -106,4 +107,85 @@ describe("utils", () => {
|
|||
expect(actual).toBe(undefined)
|
||||
})
|
||||
})
|
||||
|
||||
describe("isServingBuilder", () => {
|
||||
let ctx: BBContext
|
||||
|
||||
const expectResult = (result: boolean) =>
|
||||
expect(utils.isServingBuilder(ctx)).toBe(result)
|
||||
|
||||
beforeEach(() => {
|
||||
ctx = structures.koa.newContext()
|
||||
})
|
||||
|
||||
it("returns true if current path is in builder", async () => {
|
||||
ctx.path = "/builder/app/app_"
|
||||
expectResult(true)
|
||||
})
|
||||
|
||||
it("returns false if current path doesn't have '/' suffix", async () => {
|
||||
ctx.path = "/builder/app"
|
||||
expectResult(false)
|
||||
|
||||
ctx.path = "/xx"
|
||||
expectResult(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe("isServingBuilderPreview", () => {
|
||||
let ctx: BBContext
|
||||
|
||||
const expectResult = (result: boolean) =>
|
||||
expect(utils.isServingBuilderPreview(ctx)).toBe(result)
|
||||
|
||||
beforeEach(() => {
|
||||
ctx = structures.koa.newContext()
|
||||
})
|
||||
|
||||
it("returns true if current path is in builder preview", async () => {
|
||||
ctx.path = "/app/preview/xx"
|
||||
expectResult(true)
|
||||
})
|
||||
|
||||
it("returns false if current path is not in builder preview", async () => {
|
||||
ctx.path = "/builder"
|
||||
expectResult(false)
|
||||
|
||||
ctx.path = "/xx"
|
||||
expectResult(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe("isPublicAPIRequest", () => {
|
||||
let ctx: BBContext
|
||||
|
||||
const expectResult = (result: boolean) =>
|
||||
expect(utils.isPublicApiRequest(ctx)).toBe(result)
|
||||
|
||||
beforeEach(() => {
|
||||
ctx = structures.koa.newContext()
|
||||
})
|
||||
|
||||
it("returns true if current path remains to public API", async () => {
|
||||
ctx.path = "/api/public/v1/invoices"
|
||||
expectResult(true)
|
||||
|
||||
ctx.path = "/api/public/v1"
|
||||
expectResult(true)
|
||||
|
||||
ctx.path = "/api/public/v2"
|
||||
expectResult(true)
|
||||
|
||||
ctx.path = "/api/public/v21"
|
||||
expectResult(true)
|
||||
})
|
||||
|
||||
it("returns false if current path doesn't remain to public API", async () => {
|
||||
ctx.path = "/api/public"
|
||||
expectResult(false)
|
||||
|
||||
ctx.path = "/xx"
|
||||
expectResult(false)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
@ -1,11 +1,5 @@
|
|||
import { getAllApps, queryGlobalView } from "../db"
|
||||
import {
|
||||
Header,
|
||||
MAX_VALID_DATE,
|
||||
DocumentType,
|
||||
SEPARATOR,
|
||||
ViewName,
|
||||
} from "../constants"
|
||||
import { getAllApps } from "../db"
|
||||
import { Header, MAX_VALID_DATE, DocumentType, SEPARATOR } from "../constants"
|
||||
import env from "../environment"
|
||||
import * as tenancy from "../tenancy"
|
||||
import * as context from "../context"
|
||||
|
@ -23,7 +17,9 @@ const APP_PREFIX = DocumentType.APP + SEPARATOR
|
|||
const PROD_APP_PREFIX = "/app/"
|
||||
|
||||
const BUILDER_PREVIEW_PATH = "/app/preview"
|
||||
const BUILDER_REFERER_PREFIX = "/builder/app/"
|
||||
const BUILDER_PREFIX = "/builder"
|
||||
const BUILDER_APP_PREFIX = `${BUILDER_PREFIX}/app/`
|
||||
const PUBLIC_API_PREFIX = "/api/public/v"
|
||||
|
||||
function confirmAppId(possibleAppId: string | undefined) {
|
||||
return possibleAppId && possibleAppId.startsWith(APP_PREFIX)
|
||||
|
@ -69,6 +65,18 @@ export function isServingApp(ctx: Ctx) {
|
|||
return false
|
||||
}
|
||||
|
||||
export function isServingBuilder(ctx: Ctx): boolean {
|
||||
return ctx.path.startsWith(BUILDER_APP_PREFIX)
|
||||
}
|
||||
|
||||
export function isServingBuilderPreview(ctx: Ctx): boolean {
|
||||
return ctx.path.startsWith(BUILDER_PREVIEW_PATH)
|
||||
}
|
||||
|
||||
export function isPublicApiRequest(ctx: Ctx): boolean {
|
||||
return ctx.path.startsWith(PUBLIC_API_PREFIX)
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a request tries to find the appId, which can be located in various places
|
||||
* @param {object} ctx The main request body to look through.
|
||||
|
@ -110,7 +118,7 @@ export async function getAppIdFromCtx(ctx: Ctx) {
|
|||
// make sure this is performed after prod app url resolution, in case the
|
||||
// referer header is present from a builder redirect
|
||||
const referer = ctx.request.headers.referer
|
||||
if (!appId && referer?.includes(BUILDER_REFERER_PREFIX)) {
|
||||
if (!appId && referer?.includes(BUILDER_APP_PREFIX)) {
|
||||
const refererId = parseAppIdFromUrl(ctx.request.headers.referer)
|
||||
appId = confirmAppId(refererId)
|
||||
}
|
||||
|
|
|
@ -90,6 +90,10 @@ export const useScimIntegration = () => {
|
|||
return useFeature(Feature.SCIM)
|
||||
}
|
||||
|
||||
export const useSyncAutomations = () => {
|
||||
return useFeature(Feature.SYNC_AUTOMATIONS)
|
||||
}
|
||||
|
||||
// QUOTAS
|
||||
|
||||
export const setAutomationLogsQuota = (value: number) => {
|
||||
|
|
|
@ -7,11 +7,6 @@
|
|||
"@budibase/types": ["../types/src"]
|
||||
}
|
||||
},
|
||||
"references": [
|
||||
{ "path": "../types" }
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules",
|
||||
"dist",
|
||||
]
|
||||
}
|
||||
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "@budibase/bbui",
|
||||
"description": "A UI solution used in the different Budibase projects.",
|
||||
"version": "2.6.23",
|
||||
"version": "0.0.0",
|
||||
"license": "MPL-2.0",
|
||||
"svelte": "src/index.js",
|
||||
"module": "dist/bbui.es.js",
|
||||
|
@ -38,8 +38,8 @@
|
|||
],
|
||||
"dependencies": {
|
||||
"@adobe/spectrum-css-workflow-icons": "1.2.1",
|
||||
"@budibase/shared-core": "^2.6.23",
|
||||
"@budibase/string-templates": "^2.6.23",
|
||||
"@budibase/shared-core": "0.0.0",
|
||||
"@budibase/string-templates": "0.0.0",
|
||||
"@spectrum-css/accordion": "3.0.24",
|
||||
"@spectrum-css/actionbutton": "1.0.1",
|
||||
"@spectrum-css/actiongroup": "1.0.1",
|
||||
|
@ -90,5 +90,19 @@
|
|||
"resolutions": {
|
||||
"loader-utils": "1.4.1"
|
||||
},
|
||||
"nx": {
|
||||
"targets": {
|
||||
"build": {
|
||||
"dependsOn": [
|
||||
{
|
||||
"projects": [
|
||||
"@budibase/string-templates"
|
||||
],
|
||||
"target": "build"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"gitHead": "d1836a898cab3f8ab80ee6d8f42be1a9eed7dcdc"
|
||||
}
|
||||
|
|
|
@ -102,7 +102,9 @@
|
|||
margin-left: 0;
|
||||
transition: color ease-out 130ms;
|
||||
}
|
||||
.is-selected:not(.spectrum-ActionButton--emphasized):not(.spectrum-ActionButton--quiet) {
|
||||
.is-selected:not(.spectrum-ActionButton--emphasized):not(
|
||||
.spectrum-ActionButton--quiet
|
||||
) {
|
||||
background: var(--spectrum-global-color-gray-300);
|
||||
border-color: var(--spectrum-global-color-gray-500);
|
||||
}
|
||||
|
|
|
@ -56,6 +56,8 @@ export default function positionDropdown(element, opts) {
|
|||
styles.left = anchorBounds.left + anchorBounds.width - elementBounds.width
|
||||
} else if (align === "right-outside") {
|
||||
styles.left = anchorBounds.right + offset
|
||||
} else if (align === "left-outside") {
|
||||
styles.left = anchorBounds.left - elementBounds.width - offset
|
||||
} else {
|
||||
styles.left = anchorBounds.left
|
||||
}
|
||||
|
|
|
@ -13,10 +13,12 @@
|
|||
export let url = ""
|
||||
export let disabled = false
|
||||
export let initials = "JD"
|
||||
export let color = null
|
||||
|
||||
const DefaultColor = "#3aab87"
|
||||
|
||||
$: color = getColor(initials)
|
||||
$: avatarColor = color || getColor(initials)
|
||||
$: style = getStyle(size, avatarColor)
|
||||
|
||||
const getColor = initials => {
|
||||
if (!initials?.length) {
|
||||
|
@ -26,6 +28,12 @@
|
|||
const hue = ((code % 26) / 26) * 360
|
||||
return `hsl(${hue}, 50%, 50%)`
|
||||
}
|
||||
|
||||
const getStyle = (sizeKey, color) => {
|
||||
const size = `var(${sizes.get(sizeKey)})`
|
||||
const fontSize = `calc(${size} / 2)`
|
||||
return `width:${size}; height:${size}; font-size:${fontSize}; background:${color};`
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if url}
|
||||
|
@ -37,13 +45,7 @@
|
|||
style="width: var({sizes.get(size)}); height: var({sizes.get(size)});"
|
||||
/>
|
||||
{:else}
|
||||
<div
|
||||
class="spectrum-Avatar"
|
||||
class:is-disabled={disabled}
|
||||
style="width: var({sizes.get(size)}); height: var({sizes.get(
|
||||
size
|
||||
)}); font-size: calc(var({sizes.get(size)}) / 2); background: {color};"
|
||||
>
|
||||
<div class="spectrum-Avatar" class:is-disabled={disabled} {style}>
|
||||
{initials || ""}
|
||||
</div>
|
||||
{/if}
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
import "@spectrum-css/button/dist/index-vars.css"
|
||||
import Tooltip from "../Tooltip/Tooltip.svelte"
|
||||
|
||||
export let type
|
||||
export let disabled = false
|
||||
export let size = "M"
|
||||
export let cta = false
|
||||
|
@ -21,6 +22,7 @@
|
|||
|
||||
<button
|
||||
{id}
|
||||
{type}
|
||||
class:spectrum-Button--cta={cta}
|
||||
class:spectrum-Button--primary={primary}
|
||||
class:spectrum-Button--secondary={secondary}
|
||||
|
@ -73,6 +75,7 @@
|
|||
button {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.spectrum-Button-label {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
|
|
|
@ -3,11 +3,13 @@
|
|||
import Button from "../Button/Button.svelte"
|
||||
import Body from "../Typography/Body.svelte"
|
||||
import Heading from "../Typography/Heading.svelte"
|
||||
import { setContext } from "svelte"
|
||||
|
||||
export let title
|
||||
export let fillWidth
|
||||
export let left = "314px"
|
||||
export let width = "calc(100% - 626px)"
|
||||
export let headless = false
|
||||
|
||||
let visible = false
|
||||
|
||||
|
@ -25,6 +27,11 @@
|
|||
visible = false
|
||||
}
|
||||
|
||||
setContext("drawer-actions", {
|
||||
hide,
|
||||
show,
|
||||
})
|
||||
|
||||
const easeInOutQuad = x => {
|
||||
return x < 0.5 ? 2 * x * x : 1 - Math.pow(-2 * x + 2, 2) / 2
|
||||
}
|
||||
|
@ -47,27 +54,34 @@
|
|||
<section
|
||||
class:fillWidth
|
||||
class="drawer"
|
||||
class:headless
|
||||
transition:slide|local
|
||||
style={`width: ${width}; left: ${left};`}
|
||||
>
|
||||
<header>
|
||||
<div class="text">
|
||||
<Heading size="XS">{title}</Heading>
|
||||
<Body size="S">
|
||||
<slot name="description" />
|
||||
</Body>
|
||||
</div>
|
||||
<div class="buttons">
|
||||
<Button secondary quiet on:click={hide}>Cancel</Button>
|
||||
<slot name="buttons" />
|
||||
</div>
|
||||
</header>
|
||||
{#if !headless}
|
||||
<header>
|
||||
<div class="text">
|
||||
<Heading size="XS">{title}</Heading>
|
||||
<Body size="S">
|
||||
<slot name="description" />
|
||||
</Body>
|
||||
</div>
|
||||
<div class="buttons">
|
||||
<Button secondary quiet on:click={hide}>Cancel</Button>
|
||||
<slot name="buttons" />
|
||||
</div>
|
||||
</header>
|
||||
{/if}
|
||||
<slot name="body" />
|
||||
</section>
|
||||
</Portal>
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
.drawer.headless :global(.drawer-contents) {
|
||||
height: calc(40vh + 75px);
|
||||
}
|
||||
|
||||
.buttons {
|
||||
display: flex;
|
||||
gap: var(--spacing-m);
|
||||
|
|
|
@ -0,0 +1,19 @@
|
|||
<script>
|
||||
import { slide } from "svelte/transition"
|
||||
|
||||
export let error = null
|
||||
</script>
|
||||
|
||||
<div transition:slide|local={{ duration: 130 }} class="error-message">
|
||||
{error}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.error-message {
|
||||
background: var(--spectrum-global-color-red-400);
|
||||
color: white;
|
||||
font-size: 14px;
|
||||
padding: 6px 16px;
|
||||
font-weight: 500;
|
||||
}
|
||||
</style>
|
|
@ -1,7 +1,7 @@
|
|||
<script>
|
||||
import Icon from "../Icon/Icon.svelte"
|
||||
import { getContext, onMount } from "svelte"
|
||||
import { slide } from "svelte/transition"
|
||||
import ErrorMessage from "./ErrorMessage.svelte"
|
||||
|
||||
export let disabled = false
|
||||
export let error = null
|
||||
|
@ -55,9 +55,7 @@
|
|||
{/if}
|
||||
</div>
|
||||
{#if error}
|
||||
<div transition:slide|local={{ duration: 130 }} class="error-message">
|
||||
{error}
|
||||
</div>
|
||||
<ErrorMessage {error} />
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
|
@ -110,13 +108,6 @@
|
|||
.field {
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
.error-message {
|
||||
background: var(--spectrum-global-color-red-400);
|
||||
color: white;
|
||||
font-size: 14px;
|
||||
padding: 6px 16px;
|
||||
font-weight: 500;
|
||||
}
|
||||
.error-icon {
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
|
|
@ -4,3 +4,4 @@ export { default as FancySelect } from "./FancySelect.svelte"
|
|||
export { default as FancyButton } from "./FancyButton.svelte"
|
||||
export { default as FancyForm } from "./FancyForm.svelte"
|
||||
export { default as FancyButtonRadio } from "./FancyButtonRadio.svelte"
|
||||
export { default as ErrorMessage } from "./ErrorMessage.svelte"
|
||||
|
|
|
@ -18,10 +18,14 @@
|
|||
export let ignoreTimezones = false
|
||||
export let time24hr = false
|
||||
export let range = false
|
||||
export let flatpickr
|
||||
export let useKeyboardShortcuts = true
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
const flatpickrId = `${uuid()}-wrapper`
|
||||
|
||||
let open = false
|
||||
let flatpickr, flatpickrOptions
|
||||
let flatpickrOptions
|
||||
|
||||
// Another classic flatpickr issue. Errors were randomly being thrown due to
|
||||
// flatpickr internal code. Making sure that "destroy" is a valid function
|
||||
|
@ -59,6 +63,8 @@
|
|||
dispatch("change", timestamp.toISOString())
|
||||
}
|
||||
},
|
||||
onOpen: () => dispatch("open"),
|
||||
onClose: () => dispatch("close"),
|
||||
}
|
||||
|
||||
$: redrawOptions = {
|
||||
|
@ -113,12 +119,16 @@
|
|||
|
||||
const onOpen = () => {
|
||||
open = true
|
||||
document.addEventListener("keyup", clearDateOnBackspace)
|
||||
if (useKeyboardShortcuts) {
|
||||
document.addEventListener("keyup", clearDateOnBackspace)
|
||||
}
|
||||
}
|
||||
|
||||
const onClose = () => {
|
||||
open = false
|
||||
document.removeEventListener("keyup", clearDateOnBackspace)
|
||||
if (useKeyboardShortcuts) {
|
||||
document.removeEventListener("keyup", clearDateOnBackspace)
|
||||
}
|
||||
|
||||
// Manually blur all input fields since flatpickr creates a second
|
||||
// duplicate input field.
|
||||
|
|
|
@ -165,7 +165,7 @@
|
|||
{/if}
|
||||
{#if !disabled}
|
||||
<div class="delete-button" on:click={removeFile}>
|
||||
<Icon name="Close" />
|
||||
<Icon name="Delete" />
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
@ -209,7 +209,7 @@
|
|||
{/if}
|
||||
{#if !disabled}
|
||||
<div class="delete-button" on:click={removeFile}>
|
||||
<Icon name="Close" />
|
||||
<Icon name="Delete" />
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
|
|
@ -12,6 +12,7 @@
|
|||
export let emphasized = false
|
||||
export let onTop = false
|
||||
export let size = "M"
|
||||
export let beforeSwitch = null
|
||||
|
||||
let thisSelected = undefined
|
||||
|
||||
|
@ -28,9 +29,18 @@
|
|||
thisSelected = selected
|
||||
dispatch("select", thisSelected)
|
||||
} else if ($tab.title !== thisSelected) {
|
||||
thisSelected = $tab.title
|
||||
selected = $tab.title
|
||||
dispatch("select", thisSelected)
|
||||
if (typeof beforeSwitch == "function") {
|
||||
const proceed = beforeSwitch($tab.title)
|
||||
if (proceed) {
|
||||
thisSelected = $tab.title
|
||||
selected = $tab.title
|
||||
dispatch("select", thisSelected)
|
||||
}
|
||||
} else {
|
||||
thisSelected = $tab.title
|
||||
selected = $tab.title
|
||||
dispatch("select", thisSelected)
|
||||
}
|
||||
}
|
||||
if ($tab.title !== thisSelected) {
|
||||
tab.update(state => {
|
||||
|
|
|
@ -31,4 +31,12 @@
|
|||
.spectrum-Tooltip-tip {
|
||||
border-top-color: var(--spectrum-global-color-gray-500);
|
||||
}
|
||||
.spectrum-Tooltip {
|
||||
max-width: 280px;
|
||||
}
|
||||
.spectrum-Tooltip-label {
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@budibase/builder",
|
||||
"version": "2.6.23",
|
||||
"version": "0.0.0",
|
||||
"license": "GPL-3.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
|
@ -9,7 +9,7 @@
|
|||
"dev:builder": "routify -c dev:vite",
|
||||
"dev:vite": "vite --host 0.0.0.0",
|
||||
"rollup": "rollup -c -w",
|
||||
"test": "vitest"
|
||||
"test": "vitest run"
|
||||
},
|
||||
"jest": {
|
||||
"globals": {
|
||||
|
@ -58,10 +58,18 @@
|
|||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"@budibase/bbui": "^2.6.23",
|
||||
"@budibase/frontend-core": "^2.6.23",
|
||||
"@budibase/shared-core": "^2.6.23",
|
||||
"@budibase/string-templates": "^2.6.23",
|
||||
"@budibase/bbui": "0.0.0",
|
||||
"@budibase/frontend-core": "0.0.0",
|
||||
"@budibase/shared-core": "0.0.0",
|
||||
"@budibase/string-templates": "0.0.0",
|
||||
"@budibase/types": "0.0.0",
|
||||
"@codemirror/autocomplete": "^6.7.1",
|
||||
"@codemirror/commands": "^6.2.4",
|
||||
"@codemirror/lang-javascript": "^6.1.8",
|
||||
"@codemirror/language": "^6.6.0",
|
||||
"@codemirror/state": "^6.2.0",
|
||||
"@codemirror/theme-one-dark": "^6.1.2",
|
||||
"@codemirror/view": "^6.11.2",
|
||||
"@fortawesome/fontawesome-svg-core": "^6.2.1",
|
||||
"@fortawesome/free-brands-svg-icons": "^6.2.1",
|
||||
"@fortawesome/free-solid-svg-icons": "^6.2.1",
|
||||
|
@ -116,5 +124,31 @@
|
|||
"vite": "^3.0.8",
|
||||
"vitest": "^0.29.2"
|
||||
},
|
||||
"nx": {
|
||||
"targets": {
|
||||
"build": {
|
||||
"dependsOn": [
|
||||
{
|
||||
"projects": [
|
||||
"@budibase/string-templates",
|
||||
"@budibase/shared-core"
|
||||
],
|
||||
"target": "build"
|
||||
}
|
||||
]
|
||||
},
|
||||
"test": {
|
||||
"dependsOn": [
|
||||
{
|
||||
"projects": [
|
||||
"@budibase/shared-core",
|
||||
"@budibase/string-templates"
|
||||
],
|
||||
"target": "build"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"gitHead": "115189f72a850bfb52b65ec61d932531bf327072"
|
||||
}
|
||||
|
|
|
@ -77,7 +77,7 @@ export const getAuthBindings = () => {
|
|||
runtime: `${safeUser}.${safeOAuth2}.${safeAccessToken}`,
|
||||
readable: `Current User.OAuthToken`,
|
||||
key: "accessToken",
|
||||
display: { name: "OAuthToken" },
|
||||
display: { name: "OAuthToken", type: "text" },
|
||||
},
|
||||
]
|
||||
|
||||
|
@ -434,6 +434,9 @@ export const getUserBindings = () => {
|
|||
providerId: "user",
|
||||
category: "Current User",
|
||||
icon: "User",
|
||||
display: {
|
||||
name: key,
|
||||
},
|
||||
})
|
||||
return acc
|
||||
}, [])
|
||||
|
@ -550,7 +553,7 @@ const getUrlBindings = asset => {
|
|||
readableBinding: `URL.${param}`,
|
||||
category: "URL",
|
||||
icon: "RailTop",
|
||||
display: { type: "string" },
|
||||
display: { type: "string", name: param },
|
||||
}))
|
||||
const queryParamsBinding = {
|
||||
type: "context",
|
||||
|
@ -558,7 +561,7 @@ const getUrlBindings = asset => {
|
|||
readableBinding: "Query params",
|
||||
category: "URL",
|
||||
icon: "RailTop",
|
||||
display: { type: "object" },
|
||||
display: { type: "object", name: "Query params" },
|
||||
}
|
||||
return urlParamBindings.concat([queryParamsBinding])
|
||||
}
|
||||
|
@ -589,7 +592,6 @@ export const getEventContextBindings = (
|
|||
actionId
|
||||
) => {
|
||||
let bindings = []
|
||||
|
||||
// Check if any context bindings are provided by the component for this
|
||||
// setting
|
||||
const component = findComponent(asset.props, componentId)
|
||||
|
@ -605,6 +607,9 @@ export const getEventContextBindings = (
|
|||
)}`,
|
||||
category: component._instanceName,
|
||||
icon: def.icon,
|
||||
display: {
|
||||
name: contextEntry.label,
|
||||
},
|
||||
})
|
||||
})
|
||||
}
|
||||
|
@ -628,6 +633,9 @@ export const getEventContextBindings = (
|
|||
runtimeBinding: `actions.${idx}.${contextValue.value}`,
|
||||
category: "Actions",
|
||||
icon: "JourneyAction",
|
||||
display: {
|
||||
name: contextValue.label,
|
||||
},
|
||||
})
|
||||
})
|
||||
}
|
||||
|
|
|
@ -2,6 +2,7 @@ import { datasources, tables } from "../stores/backend"
|
|||
import { IntegrationNames } from "../constants/backend"
|
||||
import { get } from "svelte/store"
|
||||
import cloneDeep from "lodash/cloneDeepWith"
|
||||
import { API } from "api"
|
||||
|
||||
function prepareData(config) {
|
||||
let datasource = {}
|
||||
|
@ -37,3 +38,9 @@ export async function createRestDatasource(integration) {
|
|||
const config = cloneDeep(integration)
|
||||
return saveDatasource(config)
|
||||
}
|
||||
|
||||
export async function validateDatasourceConfig(config) {
|
||||
const datasource = prepareData(config)
|
||||
const resp = await API.validateDatasource(datasource)
|
||||
return resp
|
||||
}
|
||||
|
|
|
@ -2,6 +2,7 @@ import { getFrontendStore } from "./store/frontend"
|
|||
import { getAutomationStore } from "./store/automation"
|
||||
import { getTemporalStore } from "./store/temporal"
|
||||
import { getThemeStore } from "./store/theme"
|
||||
import { getUserStore } from "./store/users"
|
||||
import { derived } from "svelte/store"
|
||||
import { findComponent, findComponentPath } from "./componentUtils"
|
||||
import { RoleUtils } from "@budibase/frontend-core"
|
||||
|
@ -12,6 +13,7 @@ export const store = getFrontendStore()
|
|||
export const automationStore = getAutomationStore()
|
||||
export const themeStore = getThemeStore()
|
||||
export const temporalStore = getTemporalStore()
|
||||
export const userStore = getUserStore()
|
||||
|
||||
// Setup history for screens
|
||||
export const screenHistoryStore = createHistoryStore({
|
||||
|
|
|
@ -147,6 +147,9 @@ const automationActions = store => ({
|
|||
testData,
|
||||
})
|
||||
if (!result?.trigger && !result?.steps?.length) {
|
||||
if (result?.err?.code === "usage_limit_exceeded") {
|
||||
throw "You have exceeded your automation quota"
|
||||
}
|
||||
throw "Something went wrong testing your automation"
|
||||
}
|
||||
store.update(state => {
|
||||
|
|
|
@ -37,8 +37,10 @@ import {
|
|||
} from "builderStore/dataBinding"
|
||||
import { makePropSafe as safe } from "@budibase/string-templates"
|
||||
import { getComponentFieldOptions } from "helpers/formFields"
|
||||
import { createBuilderWebsocket } from "builderStore/websocket"
|
||||
|
||||
const INITIAL_FRONTEND_STATE = {
|
||||
initialised: false,
|
||||
apps: [],
|
||||
name: "",
|
||||
url: "",
|
||||
|
@ -69,7 +71,9 @@ const INITIAL_FRONTEND_STATE = {
|
|||
customTheme: {},
|
||||
previewDevice: "desktop",
|
||||
highlightedSettingKey: null,
|
||||
propertyFocus: null,
|
||||
builderSidePanel: false,
|
||||
hasLock: true,
|
||||
|
||||
// URL params
|
||||
selectedScreenId: null,
|
||||
|
@ -86,6 +90,7 @@ const INITIAL_FRONTEND_STATE = {
|
|||
|
||||
export const getFrontendStore = () => {
|
||||
const store = writable({ ...INITIAL_FRONTEND_STATE })
|
||||
let websocket
|
||||
|
||||
// This is a fake implementation of a "patch" API endpoint to try and prevent
|
||||
// 409s. All screen doc mutations (aside from creation) use this function,
|
||||
|
@ -110,10 +115,11 @@ export const getFrontendStore = () => {
|
|||
store.actions = {
|
||||
reset: () => {
|
||||
store.set({ ...INITIAL_FRONTEND_STATE })
|
||||
websocket?.disconnect()
|
||||
},
|
||||
initialise: async pkg => {
|
||||
const { layouts, screens, application, clientLibPath } = pkg
|
||||
|
||||
const { layouts, screens, application, clientLibPath, hasLock } = pkg
|
||||
websocket = createBuilderWebsocket(application.appId)
|
||||
await store.actions.components.refreshDefinitions(application.appId)
|
||||
|
||||
// Reset store state
|
||||
|
@ -137,6 +143,8 @@ export const getFrontendStore = () => {
|
|||
upgradableVersion: application.upgradableVersion,
|
||||
navigation: application.navigation || {},
|
||||
usedPlugins: application.usedPlugins || [],
|
||||
hasLock,
|
||||
initialised: true,
|
||||
}))
|
||||
screenHistoryStore.reset()
|
||||
automationHistoryStore.reset()
|
||||
|
@ -1319,6 +1327,12 @@ export const getFrontendStore = () => {
|
|||
highlightedSettingKey: key,
|
||||
}))
|
||||
},
|
||||
propertyFocus: key => {
|
||||
store.update(state => ({
|
||||
...state,
|
||||
propertyFocus: key,
|
||||
}))
|
||||
},
|
||||
},
|
||||
dnd: {
|
||||
start: component => {
|
||||
|
|
|
@ -0,0 +1,42 @@
|
|||
import { writable, get } from "svelte/store"
|
||||
|
||||
export const getUserStore = () => {
|
||||
const store = writable([])
|
||||
|
||||
const init = users => {
|
||||
store.set(users)
|
||||
}
|
||||
|
||||
const updateUser = user => {
|
||||
const $users = get(store)
|
||||
if (!$users.some(x => x.sessionId === user.sessionId)) {
|
||||
store.set([...$users, user])
|
||||
} else {
|
||||
store.update(state => {
|
||||
const index = state.findIndex(x => x.sessionId === user.sessionId)
|
||||
state[index] = user
|
||||
return state.slice()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const removeUser = sessionId => {
|
||||
store.update(state => {
|
||||
return state.filter(x => x.sessionId !== sessionId)
|
||||
})
|
||||
}
|
||||
|
||||
const reset = () => {
|
||||
store.set([])
|
||||
}
|
||||
|
||||
return {
|
||||
...store,
|
||||
actions: {
|
||||
init,
|
||||
updateUser,
|
||||
removeUser,
|
||||
reset,
|
||||
},
|
||||
}
|
||||
}
|
|
@ -1,3 +1,4 @@
|
|||
import { ActionStepID } from "constants/backend/automations"
|
||||
import { TableNames } from "../constants"
|
||||
import {
|
||||
AUTO_COLUMN_DISPLAY_NAMES,
|
||||
|
@ -53,3 +54,9 @@ export function buildAutoColumn(tableName, name, subtype) {
|
|||
}
|
||||
return base
|
||||
}
|
||||
|
||||
export function checkForCollectStep(automation) {
|
||||
return automation.definition.steps.some(
|
||||
step => step.stepId === ActionStepID.COLLECT
|
||||
)
|
||||
}
|
||||
|
|
|
@ -0,0 +1,53 @@
|
|||
import { createWebsocket } from "@budibase/frontend-core"
|
||||
import { userStore, store } from "builderStore"
|
||||
import { datasources, tables } from "stores/backend"
|
||||
import { get } from "svelte/store"
|
||||
import { auth } from "stores/portal"
|
||||
import { SocketEvent, BuilderSocketEvent } from "@budibase/shared-core"
|
||||
import { notifications } from "@budibase/bbui"
|
||||
|
||||
export const createBuilderWebsocket = appId => {
|
||||
const socket = createWebsocket("/socket/builder")
|
||||
|
||||
// Built-in events
|
||||
socket.on("connect", () => {
|
||||
socket.emit(BuilderSocketEvent.SelectApp, { appId }, ({ users }) => {
|
||||
userStore.actions.init(users)
|
||||
})
|
||||
})
|
||||
socket.on("connect_error", err => {
|
||||
console.log("Failed to connect to builder websocket:", err.message)
|
||||
})
|
||||
socket.on("disconnect", () => {
|
||||
userStore.actions.reset()
|
||||
})
|
||||
|
||||
// User events
|
||||
socket.onOther(SocketEvent.UserUpdate, ({ user }) => {
|
||||
userStore.actions.updateUser(user)
|
||||
})
|
||||
socket.onOther(SocketEvent.UserDisconnect, ({ sessionId }) => {
|
||||
userStore.actions.removeUser(sessionId)
|
||||
})
|
||||
socket.onOther(BuilderSocketEvent.LockTransfer, ({ userId }) => {
|
||||
if (userId === get(auth)?.user?._id) {
|
||||
notifications.success("You can now edit screens and automations")
|
||||
store.update(state => ({
|
||||
...state,
|
||||
hasLock: true,
|
||||
}))
|
||||
}
|
||||
})
|
||||
|
||||
// Table events
|
||||
socket.onOther(BuilderSocketEvent.TableChange, ({ id, table }) => {
|
||||
tables.replaceTable(id, table)
|
||||
})
|
||||
|
||||
// Datasource events
|
||||
socket.onOther(BuilderSocketEvent.DatasourceChange, ({ id, datasource }) => {
|
||||
datasources.replaceDatasource(id, datasource)
|
||||
})
|
||||
|
||||
return socket
|
||||
}
|
|
@ -6,24 +6,48 @@
|
|||
Body,
|
||||
Icon,
|
||||
notifications,
|
||||
Tags,
|
||||
Tag,
|
||||
} from "@budibase/bbui"
|
||||
import { automationStore } from "builderStore"
|
||||
import { admin } from "stores/portal"
|
||||
import { automationStore, selectedAutomation } from "builderStore"
|
||||
import { admin, licensing } from "stores/portal"
|
||||
import { externalActions } from "./ExternalActions"
|
||||
import { TriggerStepID } from "constants/backend/automations"
|
||||
import { checkForCollectStep } from "builderStore/utils"
|
||||
|
||||
export let blockIdx
|
||||
export let lastStep
|
||||
|
||||
const disabled = {
|
||||
SEND_EMAIL_SMTP: {
|
||||
disabled: !$admin.checklist.smtp.checked,
|
||||
message: "Please configure SMTP",
|
||||
},
|
||||
}
|
||||
|
||||
let syncAutomationsEnabled = $licensing.syncAutomationsEnabled
|
||||
let collectBlockAllowedSteps = [TriggerStepID.APP, TriggerStepID.WEBHOOK]
|
||||
let selectedAction
|
||||
let actionVal
|
||||
let actions = Object.entries($automationStore.blockDefinitions.ACTION)
|
||||
|
||||
$: collectBlockExists = checkForCollectStep($selectedAutomation)
|
||||
|
||||
const disabled = () => {
|
||||
return {
|
||||
SEND_EMAIL_SMTP: {
|
||||
disabled: !$admin.checklist.smtp.checked,
|
||||
message: "Please configure SMTP",
|
||||
},
|
||||
COLLECT: {
|
||||
disabled: !lastStep || !syncAutomationsEnabled || collectBlockExists,
|
||||
message: collectDisabledMessage(),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
const collectDisabledMessage = () => {
|
||||
if (collectBlockExists) {
|
||||
return "Only one Collect step allowed"
|
||||
}
|
||||
if (!lastStep) {
|
||||
return "Only available as the last step"
|
||||
}
|
||||
}
|
||||
|
||||
const external = actions.reduce((acc, elm) => {
|
||||
const [k, v] = elm
|
||||
if (!v.internal && !v.custom) {
|
||||
|
@ -38,6 +62,15 @@
|
|||
acc[k] = v
|
||||
}
|
||||
delete acc.LOOP
|
||||
|
||||
// Filter out Collect block if not App Action or Webhook
|
||||
if (
|
||||
!collectBlockAllowedSteps.includes(
|
||||
$selectedAutomation.definition.trigger.stepId
|
||||
)
|
||||
) {
|
||||
delete acc.COLLECT
|
||||
}
|
||||
return acc
|
||||
}, {})
|
||||
|
||||
|
@ -48,7 +81,6 @@
|
|||
}
|
||||
return acc
|
||||
}, {})
|
||||
console.log(plugins)
|
||||
|
||||
const selectAction = action => {
|
||||
actionVal = action
|
||||
|
@ -72,7 +104,7 @@
|
|||
<ModalContent
|
||||
title="Add automation step"
|
||||
confirmText="Save"
|
||||
size="M"
|
||||
size="L"
|
||||
disabled={!selectedAction}
|
||||
onConfirm={addBlockToAutomation}
|
||||
>
|
||||
|
@ -107,7 +139,7 @@
|
|||
<Detail size="S">Actions</Detail>
|
||||
<div class="item-list">
|
||||
{#each Object.entries(internal) as [idx, action]}
|
||||
{@const isDisabled = disabled[idx] && disabled[idx].disabled}
|
||||
{@const isDisabled = disabled()[idx] && disabled()[idx].disabled}
|
||||
<div
|
||||
class="item"
|
||||
class:disabled={isDisabled}
|
||||
|
@ -117,8 +149,14 @@
|
|||
<div class="item-body">
|
||||
<Icon name={action.icon} />
|
||||
<Body size="XS">{action.name}</Body>
|
||||
{#if isDisabled}
|
||||
<Icon name="Help" tooltip={disabled[idx].message} />
|
||||
{#if isDisabled && !syncAutomationsEnabled}
|
||||
<div class="tag-color">
|
||||
<Tags>
|
||||
<Tag icon="LockClosed">Business</Tag>
|
||||
</Tags>
|
||||
</div>
|
||||
{:else if isDisabled}
|
||||
<Icon name="Help" tooltip={disabled()[idx].message} />
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
@ -152,6 +190,7 @@
|
|||
display: flex;
|
||||
margin-left: var(--spacing-m);
|
||||
gap: var(--spacing-m);
|
||||
align-items: center;
|
||||
}
|
||||
.item-list {
|
||||
display: grid;
|
||||
|
@ -181,4 +220,8 @@
|
|||
.disabled :global(.spectrum-Body) {
|
||||
color: var(--spectrum-global-color-gray-600);
|
||||
}
|
||||
|
||||
.tag-color :global(.spectrum-Tags-item) {
|
||||
background: var(--spectrum-global-color-gray-200);
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -17,7 +17,11 @@
|
|||
import ActionModal from "./ActionModal.svelte"
|
||||
import FlowItemHeader from "./FlowItemHeader.svelte"
|
||||
import RoleSelect from "components/design/settings/controls/RoleSelect.svelte"
|
||||
import { ActionStepID, TriggerStepID } from "constants/backend/automations"
|
||||
import {
|
||||
ActionStepID,
|
||||
TriggerStepID,
|
||||
Features,
|
||||
} from "constants/backend/automations"
|
||||
import { permissions } from "stores/backend"
|
||||
|
||||
export let block
|
||||
|
@ -31,6 +35,9 @@
|
|||
let showLooping = false
|
||||
let role
|
||||
|
||||
$: collectBlockExists = $selectedAutomation.definition.steps.some(
|
||||
step => step.stepId === ActionStepID.COLLECT
|
||||
)
|
||||
$: automationId = $selectedAutomation?._id
|
||||
$: showBindingPicker =
|
||||
block.stepId === ActionStepID.CREATE_ROW ||
|
||||
|
@ -184,7 +191,7 @@
|
|||
{#if !isTrigger}
|
||||
<div>
|
||||
<div class="block-options">
|
||||
{#if !loopBlock}
|
||||
{#if block?.features?.[Features.LOOPING] || !block.features}
|
||||
<ActionButton on:click={() => addLooping()} icon="Reuse">
|
||||
Add Looping
|
||||
</ActionButton>
|
||||
|
@ -224,21 +231,28 @@
|
|||
</Layout>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<Modal bind:this={actionModal} width="30%">
|
||||
<ActionModal {blockIdx} />
|
||||
</Modal>
|
||||
|
||||
<Modal bind:this={webhookModal} width="30%">
|
||||
<CreateWebhookModal />
|
||||
</Modal>
|
||||
</div>
|
||||
<div class="separator" />
|
||||
<Icon on:click={() => actionModal.show()} hoverable name="AddCircle" size="S" />
|
||||
{#if isTrigger ? totalBlocks > 1 : blockIdx !== totalBlocks - 2}
|
||||
{#if !collectBlockExists || !lastStep}
|
||||
<div class="separator" />
|
||||
<Icon
|
||||
on:click={() => actionModal.show()}
|
||||
hoverable
|
||||
name="AddCircle"
|
||||
size="S"
|
||||
/>
|
||||
{#if isTrigger ? totalBlocks > 1 : blockIdx !== totalBlocks - 2}
|
||||
<div class="separator" />
|
||||
{/if}
|
||||
{/if}
|
||||
|
||||
<Modal bind:this={actionModal} width="30%">
|
||||
<ActionModal {lastStep} {blockIdx} />
|
||||
</Modal>
|
||||
|
||||
<Modal bind:this={webhookModal} width="30%">
|
||||
<CreateWebhookModal />
|
||||
</Modal>
|
||||
|
||||
<style>
|
||||
.delete-padding {
|
||||
padding-left: 30px;
|
||||
|
|
|
@ -52,7 +52,7 @@
|
|||
await automationStore.actions.test($selectedAutomation, testData)
|
||||
$automationStore.showTestPanel = true
|
||||
} catch (error) {
|
||||
notifications.error("Error testing automation")
|
||||
notifications.error(error)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
|
|
@ -11,8 +11,8 @@
|
|||
ActionButton,
|
||||
Drawer,
|
||||
Modal,
|
||||
Detail,
|
||||
notifications,
|
||||
Icon,
|
||||
} from "@budibase/bbui"
|
||||
import CreateWebhookModal from "components/automation/Shared/CreateWebhookModal.svelte"
|
||||
import { automationStore, selectedAutomation } from "builderStore"
|
||||
|
@ -27,9 +27,18 @@
|
|||
import CronBuilder from "./CronBuilder.svelte"
|
||||
import Editor from "components/integration/QueryEditor.svelte"
|
||||
import ModalBindableInput from "components/common/bindings/ModalBindableInput.svelte"
|
||||
import CodeEditor from "components/common/CodeEditor/CodeEditor.svelte"
|
||||
import {
|
||||
bindingsToCompletions,
|
||||
jsAutocomplete,
|
||||
EditorModes,
|
||||
} from "components/common/CodeEditor"
|
||||
import FilterDrawer from "components/design/settings/controls/FilterEditor/FilterDrawer.svelte"
|
||||
import { LuceneUtils } from "@budibase/frontend-core"
|
||||
import { getSchemaForTable } from "builderStore/dataBinding"
|
||||
import {
|
||||
getSchemaForTable,
|
||||
getEnvironmentBindings,
|
||||
} from "builderStore/dataBinding"
|
||||
import { Utils } from "@budibase/frontend-core"
|
||||
import { TriggerStepID, ActionStepID } from "constants/backend/automations"
|
||||
import { onMount } from "svelte"
|
||||
|
@ -43,7 +52,6 @@
|
|||
let webhookModal
|
||||
let drawer
|
||||
let fillWidth = true
|
||||
let codeBindingOpen = false
|
||||
let inputData
|
||||
|
||||
$: filters = lookForFilters(schemaProperties) || []
|
||||
|
@ -61,11 +69,63 @@
|
|||
$: isTrigger = block?.type === "TRIGGER"
|
||||
$: isUpdateRow = stepId === ActionStepID.UPDATE_ROW
|
||||
|
||||
/**
|
||||
* TODO - Remove after November 2023
|
||||
* *******************************
|
||||
* Code added to provide backwards compatibility between Values 1,2,3,4,5
|
||||
* and the new JSON body.
|
||||
*/
|
||||
let deprecatedSchemaProperties
|
||||
$: {
|
||||
if (block?.stepId === "integromat" || block?.stepId === "zapier") {
|
||||
deprecatedSchemaProperties = schemaProperties.filter(
|
||||
prop => !prop[0].startsWith("value")
|
||||
)
|
||||
if (!deprecatedSchemaProperties.map(entry => entry[0]).includes("body")) {
|
||||
deprecatedSchemaProperties.push([
|
||||
"body",
|
||||
{
|
||||
title: "Payload",
|
||||
type: "json",
|
||||
},
|
||||
])
|
||||
}
|
||||
} else {
|
||||
deprecatedSchemaProperties = schemaProperties
|
||||
}
|
||||
}
|
||||
/****************************************************/
|
||||
|
||||
const getInputData = (testData, blockInputs) => {
|
||||
let newInputData = testData || blockInputs
|
||||
if (block.event === "app:trigger" && !newInputData?.fields) {
|
||||
newInputData = cloneDeep(blockInputs)
|
||||
}
|
||||
|
||||
/**
|
||||
* TODO - Remove after November 2023
|
||||
* *******************************
|
||||
* Code added to provide backwards compatibility between Values 1,2,3,4,5
|
||||
* and the new JSON body.
|
||||
*/
|
||||
if (
|
||||
(block?.stepId === "integromat" || block?.stepId === "zapier") &&
|
||||
!newInputData?.body?.value
|
||||
) {
|
||||
let deprecatedValues = {
|
||||
...newInputData,
|
||||
}
|
||||
delete deprecatedValues.url
|
||||
delete deprecatedValues.body
|
||||
newInputData = {
|
||||
url: newInputData.url,
|
||||
body: {
|
||||
value: JSON.stringify(deprecatedValues),
|
||||
},
|
||||
}
|
||||
}
|
||||
/**********************************/
|
||||
|
||||
inputData = newInputData
|
||||
setDefaultEnumValues()
|
||||
}
|
||||
|
@ -158,6 +218,19 @@
|
|||
}
|
||||
const outputs = Object.entries(schema)
|
||||
|
||||
let bindingIcon = ""
|
||||
let bindindingRank = 0
|
||||
|
||||
if (idx === 0) {
|
||||
bindingIcon = automation.trigger.icon
|
||||
} else if (isLoopBlock) {
|
||||
bindingIcon = "Reuse"
|
||||
bindindingRank = idx + 1
|
||||
} else {
|
||||
bindingIcon = allSteps[idx].icon
|
||||
bindindingRank = idx - loopBlockCount
|
||||
}
|
||||
|
||||
bindings = bindings.concat(
|
||||
outputs.map(([name, value]) => {
|
||||
let runtimeName = isLoopBlock
|
||||
|
@ -166,17 +239,24 @@
|
|||
? `steps[${idx - loopBlockCount}].${name}`
|
||||
: `steps.${idx - loopBlockCount}.${name}`
|
||||
const runtime = idx === 0 ? `trigger.${name}` : runtimeName
|
||||
const categoryName =
|
||||
idx === 0
|
||||
? "Trigger outputs"
|
||||
: isLoopBlock
|
||||
? "Loop Outputs"
|
||||
: `Step ${idx - loopBlockCount} outputs`
|
||||
return {
|
||||
label: runtime,
|
||||
readableBinding: runtime,
|
||||
runtimeBinding: runtime,
|
||||
type: value.type,
|
||||
description: value.description,
|
||||
category:
|
||||
idx === 0
|
||||
? "Trigger outputs"
|
||||
: isLoopBlock
|
||||
? "Loop Outputs"
|
||||
: `Step ${idx - loopBlockCount} outputs`,
|
||||
path: runtime,
|
||||
icon: bindingIcon,
|
||||
category: categoryName,
|
||||
display: {
|
||||
type: value.type,
|
||||
name: name,
|
||||
rank: bindindingRank,
|
||||
},
|
||||
}
|
||||
})
|
||||
)
|
||||
|
@ -185,15 +265,12 @@
|
|||
// Environment bindings
|
||||
if ($licensing.environmentVariablesEnabled) {
|
||||
bindings = bindings.concat(
|
||||
$environment.variables.map(variable => {
|
||||
getEnvironmentBindings().map(binding => {
|
||||
return {
|
||||
label: `env.${variable.name}`,
|
||||
path: `env.${variable.name}`,
|
||||
icon: "Key",
|
||||
category: "Environment",
|
||||
...binding,
|
||||
display: {
|
||||
type: "string",
|
||||
name: variable.name,
|
||||
...binding.display,
|
||||
rank: 98,
|
||||
},
|
||||
}
|
||||
})
|
||||
|
@ -239,7 +316,7 @@
|
|||
</script>
|
||||
|
||||
<div class="fields">
|
||||
{#each schemaProperties as [key, value]}
|
||||
{#each deprecatedSchemaProperties as [key, value]}
|
||||
<div class="block-field">
|
||||
{#if key !== "fields"}
|
||||
<Label
|
||||
|
@ -256,6 +333,28 @@
|
|||
options={value.enum}
|
||||
getOptionLabel={(x, idx) => (value.pretty ? value.pretty[idx] : x)}
|
||||
/>
|
||||
{:else if value.type === "json"}
|
||||
<Editor
|
||||
editorHeight="250"
|
||||
editorWidth="448"
|
||||
mode="json"
|
||||
value={inputData[key]?.value}
|
||||
on:change={e => {
|
||||
/**
|
||||
* TODO - Remove after November 2023
|
||||
* *******************************
|
||||
* Code added to provide backwards compatibility between Values 1,2,3,4,5
|
||||
* and the new JSON body.
|
||||
*/
|
||||
delete inputData.value1
|
||||
delete inputData.value2
|
||||
delete inputData.value3
|
||||
delete inputData.value4
|
||||
delete inputData.value5
|
||||
/***********************/
|
||||
onChange(e, key)
|
||||
}}
|
||||
/>
|
||||
{:else if value.customType === "column"}
|
||||
<Select
|
||||
on:change={e => onChange(e, key)}
|
||||
|
@ -363,25 +462,27 @@
|
|||
<SchemaSetup on:change={e => onChange(e, key)} value={inputData[key]} />
|
||||
{:else if value.customType === "code"}
|
||||
<CodeEditorModal>
|
||||
<ActionButton
|
||||
on:click={() => (codeBindingOpen = !codeBindingOpen)}
|
||||
quiet
|
||||
icon={codeBindingOpen ? "ChevronDown" : "ChevronRight"}
|
||||
>
|
||||
<Detail size="S">Bindings</Detail>
|
||||
</ActionButton>
|
||||
{#if codeBindingOpen}
|
||||
<pre>{JSON.stringify(bindings, null, 2)}</pre>
|
||||
{/if}
|
||||
<Editor
|
||||
mode="javascript"
|
||||
<CodeEditor
|
||||
value={inputData[key]}
|
||||
on:change={e => {
|
||||
// need to pass without the value inside
|
||||
onChange({ detail: e.detail.value }, key)
|
||||
inputData[key] = e.detail.value
|
||||
onChange({ detail: e.detail }, key)
|
||||
inputData[key] = e.detail
|
||||
}}
|
||||
value={inputData[key]}
|
||||
completions={[
|
||||
jsAutocomplete([
|
||||
...bindingsToCompletions(bindings, EditorModes.JS),
|
||||
]),
|
||||
]}
|
||||
mode={EditorModes.JS}
|
||||
height={500}
|
||||
/>
|
||||
<div class="messaging">
|
||||
<Icon name="FlashOn" />
|
||||
<div class="messaging-wrap">
|
||||
<div>Add available bindings by typing <strong>$</strong></div>
|
||||
</div>
|
||||
</div>
|
||||
</CodeEditorModal>
|
||||
{:else if value.customType === "loopOption"}
|
||||
<Select
|
||||
|
@ -431,6 +532,11 @@
|
|||
{/if}
|
||||
|
||||
<style>
|
||||
.messaging {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-top: var(--spacing-xl);
|
||||
}
|
||||
.fields {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
|
|
@ -32,10 +32,12 @@
|
|||
<Grid
|
||||
{API}
|
||||
tableId={id}
|
||||
tableType={$tables.selected?.type}
|
||||
allowAddRows={!isUsersTable}
|
||||
allowDeleteRows={!isUsersTable}
|
||||
schemaOverrides={isUsersTable ? userSchemaOverrides : null}
|
||||
on:updatetable={e => tables.updateTable(e.detail)}
|
||||
showAvatars={false}
|
||||
on:updatetable={e => tables.replaceTable(id, e.detail)}
|
||||
>
|
||||
<svelte:fragment slot="controls">
|
||||
{#if isInternal}
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
|
||||
export let query = {}
|
||||
export let data = []
|
||||
export let editRows = false
|
||||
|
||||
let loading = false
|
||||
let error = false
|
||||
|
@ -12,7 +13,14 @@
|
|||
{#if error}
|
||||
<div class="errors">{error}</div>
|
||||
{/if}
|
||||
<Table schema={query.schema} {data} {loading} {type} rowCount={5} />
|
||||
<Table
|
||||
schema={query.schema}
|
||||
{data}
|
||||
{loading}
|
||||
{type}
|
||||
rowCount={5}
|
||||
allowEditing={editRows}
|
||||
/>
|
||||
|
||||
<style>
|
||||
.errors {
|
||||
|
|
|
@ -81,6 +81,7 @@
|
|||
<Label>{label}</Label>
|
||||
<Editor
|
||||
editorHeight="250"
|
||||
editorWidth="320"
|
||||
mode="json"
|
||||
on:change={({ detail }) => (value = detail.value)}
|
||||
value={stringVal}
|
||||
|
|
|
@ -23,6 +23,7 @@
|
|||
export let disableSorting = false
|
||||
export let customPlaceholder = false
|
||||
export let allowEditing = true
|
||||
export let allowClickRows
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
|
@ -112,6 +113,7 @@
|
|||
{customPlaceholder}
|
||||
allowEditRows={allowEditing}
|
||||
showAutoColumns={!hideAutocolumns}
|
||||
{allowClickRows}
|
||||
on:clickrelationship={e => selectRelationship(e.detail)}
|
||||
on:sort
|
||||
>
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
import ImportModal from "../modals/ImportModal.svelte"
|
||||
|
||||
export let tableId
|
||||
export let tableType
|
||||
export let disabled
|
||||
|
||||
let modal
|
||||
|
@ -12,5 +13,5 @@
|
|||
Import
|
||||
</ActionButton>
|
||||
<Modal bind:this={modal}>
|
||||
<ImportModal {tableId} on:importrows />
|
||||
<ImportModal {tableId} {tableType} on:importrows />
|
||||
</Modal>
|
||||
|
|
|
@ -4,11 +4,12 @@
|
|||
|
||||
export let disabled = false
|
||||
|
||||
const { rows, tableId } = getContext("grid")
|
||||
const { rows, tableId, tableType } = getContext("grid")
|
||||
</script>
|
||||
|
||||
<ImportButton
|
||||
{disabled}
|
||||
tableId={$tableId}
|
||||
{tableType}
|
||||
on:importrows={rows.actions.refreshData}
|
||||
/>
|
||||
|
|
|
@ -113,17 +113,26 @@
|
|||
})
|
||||
download(data, `export.${exportFormat}`)
|
||||
} else if (filters || sorting) {
|
||||
const data = await API.exportRows({
|
||||
tableId: view,
|
||||
format: exportFormat,
|
||||
search: {
|
||||
query: luceneFilter,
|
||||
sort: sorting?.sortColumn,
|
||||
sortOrder: sorting?.sortOrder,
|
||||
paginate: false,
|
||||
},
|
||||
})
|
||||
download(data, `export.${exportFormat}`)
|
||||
let response
|
||||
try {
|
||||
response = await API.exportRows({
|
||||
tableId: view,
|
||||
format: exportFormat,
|
||||
search: {
|
||||
query: luceneFilter,
|
||||
sort: sorting?.sortColumn,
|
||||
sortOrder: sorting?.sortOrder,
|
||||
paginate: false,
|
||||
},
|
||||
})
|
||||
} catch (e) {
|
||||
console.error("Failed to export", e)
|
||||
notifications.error("Export Failed")
|
||||
}
|
||||
if (response) {
|
||||
download(response, `export.${exportFormat}`)
|
||||
notifications.success("Export Successful")
|
||||
}
|
||||
} else {
|
||||
await exportView()
|
||||
}
|
||||
|
|
|
@ -13,15 +13,18 @@
|
|||
const dispatch = createEventDispatcher()
|
||||
|
||||
export let tableId
|
||||
export let tableType
|
||||
let rows = []
|
||||
let allValid = false
|
||||
let displayColumn = null
|
||||
let identifierFields = []
|
||||
|
||||
async function importData() {
|
||||
try {
|
||||
await API.importTableData({
|
||||
tableId,
|
||||
rows,
|
||||
identifierFields,
|
||||
})
|
||||
notifications.success("Rows successfully imported")
|
||||
} catch (error) {
|
||||
|
@ -45,6 +48,13 @@
|
|||
</Body>
|
||||
<Layout gap="XS" noPadding>
|
||||
<Label grey extraSmall>CSV or JSON file to import</Label>
|
||||
<TableDataImport {tableId} bind:rows bind:allValid bind:displayColumn />
|
||||
<TableDataImport
|
||||
{tableId}
|
||||
{tableType}
|
||||
bind:rows
|
||||
bind:allValid
|
||||
bind:displayColumn
|
||||
bind:identifierFields
|
||||
/>
|
||||
</Layout>
|
||||
</ModalContent>
|
||||
|
|
|
@ -1,254 +0,0 @@
|
|||
<script>
|
||||
import {
|
||||
ModalContent,
|
||||
Modal,
|
||||
Body,
|
||||
Layout,
|
||||
Detail,
|
||||
Heading,
|
||||
notifications,
|
||||
} from "@budibase/bbui"
|
||||
import { onMount } from "svelte"
|
||||
import ICONS from "../icons"
|
||||
import { API } from "api"
|
||||
import { IntegrationTypes, DatasourceTypes } from "constants/backend"
|
||||
import CreateTableModal from "components/backend/TableNavigator/modals/CreateTableModal.svelte"
|
||||
import DatasourceConfigModal from "components/backend/DatasourceNavigator/modals/DatasourceConfigModal.svelte"
|
||||
import GoogleDatasourceConfigModal from "components/backend/DatasourceNavigator/modals/GoogleDatasourceConfigModal.svelte"
|
||||
import { createRestDatasource } from "builderStore/datasource"
|
||||
import { goto } from "@roxi/routify"
|
||||
import ImportRestQueriesModal from "./ImportRestQueriesModal.svelte"
|
||||
import DatasourceCard from "../_components/DatasourceCard.svelte"
|
||||
|
||||
export let modal
|
||||
let integrations = {}
|
||||
let integration = {}
|
||||
let internalTableModal
|
||||
let externalDatasourceModal
|
||||
let importModal
|
||||
|
||||
$: showImportButton = false
|
||||
$: customIntegrations = Object.entries(integrations).filter(
|
||||
entry => entry[1].custom
|
||||
)
|
||||
$: sortedIntegrations = sortIntegrations(integrations)
|
||||
|
||||
checkShowImport()
|
||||
|
||||
onMount(() => {
|
||||
fetchIntegrations()
|
||||
})
|
||||
|
||||
function selectIntegration(integrationType) {
|
||||
const selected = integrations[integrationType]
|
||||
|
||||
// build the schema
|
||||
const config = {}
|
||||
for (let key of Object.keys(selected.datasource)) {
|
||||
config[key] = selected.datasource[key].default
|
||||
}
|
||||
integration = {
|
||||
type: integrationType,
|
||||
plus: selected.plus,
|
||||
config,
|
||||
schema: selected.datasource,
|
||||
auth: selected.auth,
|
||||
}
|
||||
if (selected.friendlyName) {
|
||||
integration.name = selected.friendlyName
|
||||
}
|
||||
checkShowImport()
|
||||
}
|
||||
|
||||
function checkShowImport() {
|
||||
showImportButton = integration.type === "REST"
|
||||
}
|
||||
|
||||
function showImportModal() {
|
||||
importModal.show()
|
||||
}
|
||||
|
||||
async function chooseNextModal() {
|
||||
if (integration.type === IntegrationTypes.INTERNAL) {
|
||||
externalDatasourceModal.hide()
|
||||
internalTableModal.show()
|
||||
} else if (integration.type === IntegrationTypes.REST) {
|
||||
try {
|
||||
// Skip modal for rest, create straight away
|
||||
const resp = await createRestDatasource(integration)
|
||||
$goto(`./datasource/${resp._id}`)
|
||||
} catch (error) {
|
||||
notifications.error("Error creating datasource")
|
||||
}
|
||||
} else {
|
||||
externalDatasourceModal.show()
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchIntegrations() {
|
||||
let newIntegrations = {
|
||||
[IntegrationTypes.INTERNAL]: { datasource: {}, name: "INTERNAL/CSV" },
|
||||
}
|
||||
try {
|
||||
const integrationList = await API.getIntegrations()
|
||||
newIntegrations = {
|
||||
...newIntegrations,
|
||||
...integrationList,
|
||||
}
|
||||
} catch (error) {
|
||||
notifications.error("Error fetching integrations")
|
||||
}
|
||||
integrations = newIntegrations
|
||||
}
|
||||
|
||||
function sortIntegrations(integrations) {
|
||||
let integrationsArray = Object.entries(integrations)
|
||||
function getTypeOrder(schema) {
|
||||
if (schema.type === DatasourceTypes.API) {
|
||||
return 1
|
||||
}
|
||||
if (schema.type === DatasourceTypes.RELATIONAL) {
|
||||
return 2
|
||||
}
|
||||
return schema.type?.charCodeAt(0)
|
||||
}
|
||||
|
||||
integrationsArray.sort((a, b) => {
|
||||
let typeOrderA = getTypeOrder(a[1])
|
||||
let typeOrderB = getTypeOrder(b[1])
|
||||
if (typeOrderA === typeOrderB) {
|
||||
return a[1].friendlyName?.localeCompare(b[1].friendlyName)
|
||||
}
|
||||
return typeOrderA < typeOrderB ? -1 : 1
|
||||
})
|
||||
return integrationsArray
|
||||
}
|
||||
</script>
|
||||
|
||||
<Modal bind:this={internalTableModal}>
|
||||
<CreateTableModal />
|
||||
</Modal>
|
||||
|
||||
<Modal bind:this={externalDatasourceModal}>
|
||||
{#if integration?.auth?.type === "google"}
|
||||
<GoogleDatasourceConfigModal {integration} {modal} />
|
||||
{:else}
|
||||
<DatasourceConfigModal {integration} {modal} />
|
||||
{/if}
|
||||
</Modal>
|
||||
|
||||
<Modal bind:this={importModal}>
|
||||
{#if integration.type === "REST"}
|
||||
<ImportRestQueriesModal
|
||||
navigateDatasource={true}
|
||||
createDatasource={true}
|
||||
onCancel={() => modal.show()}
|
||||
/>
|
||||
{/if}
|
||||
</Modal>
|
||||
|
||||
<Modal bind:this={modal}>
|
||||
<ModalContent
|
||||
disabled={!Object.keys(integration).length}
|
||||
title="Add datasource"
|
||||
confirmText="Continue"
|
||||
showSecondaryButton={showImportButton}
|
||||
secondaryButtonText="Import"
|
||||
secondaryAction={() => showImportModal()}
|
||||
showCancelButton={false}
|
||||
size="M"
|
||||
onConfirm={() => {
|
||||
chooseNextModal()
|
||||
}}
|
||||
>
|
||||
<Layout noPadding gap="XS">
|
||||
<Body size="S">Get started with Budibase DB</Body>
|
||||
<div
|
||||
class:selected={integration.type === IntegrationTypes.INTERNAL}
|
||||
on:click={() => selectIntegration(IntegrationTypes.INTERNAL)}
|
||||
class="item hoverable"
|
||||
>
|
||||
<div class="item-body with-type">
|
||||
<svelte:component this={ICONS.BUDIBASE} height="20" width="20" />
|
||||
<div class="text">
|
||||
<Heading size="XXS">Budibase DB</Heading>
|
||||
<Detail size="S" class="type">Non-relational</Detail>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Layout>
|
||||
|
||||
<Layout noPadding gap="XS">
|
||||
<Body size="S">Connect to an external datasource</Body>
|
||||
<div class="item-list">
|
||||
{#each sortedIntegrations.filter(([key, val]) => key !== IntegrationTypes.INTERNAL && !val.custom) as [integrationType, schema]}
|
||||
<DatasourceCard
|
||||
on:selected={evt => selectIntegration(evt.detail)}
|
||||
{schema}
|
||||
bind:integrationType
|
||||
{integration}
|
||||
/>
|
||||
{/each}
|
||||
</div>
|
||||
</Layout>
|
||||
|
||||
{#if customIntegrations.length > 0}
|
||||
<Layout noPadding gap="XS">
|
||||
<Body size="S">Custom datasource</Body>
|
||||
<div class="item-list">
|
||||
{#each customIntegrations as [integrationType, schema]}
|
||||
<DatasourceCard
|
||||
on:selected={evt => selectIntegration(evt.detail)}
|
||||
{schema}
|
||||
bind:integrationType
|
||||
{integration}
|
||||
/>
|
||||
{/each}
|
||||
</div>
|
||||
</Layout>
|
||||
{/if}
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
|
||||
<style>
|
||||
.item-list {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(150px, 1fr));
|
||||
grid-gap: var(--spectrum-alias-grid-baseline);
|
||||
}
|
||||
|
||||
.item {
|
||||
cursor: pointer;
|
||||
display: grid;
|
||||
grid-gap: var(--spectrum-alias-grid-margin-xsmall);
|
||||
padding: var(--spectrum-alias-item-padding-s)
|
||||
var(--spectrum-alias-item-padding-m);
|
||||
background: var(--spectrum-alias-background-color-secondary);
|
||||
transition: background 0.13s ease-out;
|
||||
border-radius: 5px;
|
||||
box-sizing: border-box;
|
||||
border-width: 2px;
|
||||
}
|
||||
.item:hover,
|
||||
.item.selected {
|
||||
background: var(--spectrum-alias-background-color-tertiary);
|
||||
}
|
||||
|
||||
.item-body {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
gap: var(--spacing-m);
|
||||
}
|
||||
.item-body.with-type {
|
||||
align-items: flex-start;
|
||||
}
|
||||
.item-body.with-type :global(svg) {
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.text :global(.spectrum-Detail) {
|
||||
color: var(--spectrum-global-color-gray-700);
|
||||
}
|
||||
</style>
|
|
@ -4,55 +4,66 @@
|
|||
import IntegrationConfigForm from "components/backend/DatasourceNavigator/TableIntegrationMenu/IntegrationConfigForm.svelte"
|
||||
import { IntegrationNames } from "constants/backend"
|
||||
import cloneDeep from "lodash/cloneDeepWith"
|
||||
import { saveDatasource as save } from "builderStore/datasource"
|
||||
import { onMount } from "svelte"
|
||||
import {
|
||||
saveDatasource as save,
|
||||
validateDatasourceConfig,
|
||||
} from "builderStore/datasource"
|
||||
import { DatasourceFeature } from "@budibase/types"
|
||||
|
||||
export let integration
|
||||
export let modal
|
||||
|
||||
// kill the reference so the input isn't saved
|
||||
let datasource = cloneDeep(integration)
|
||||
let skipFetch = false
|
||||
let isValid = false
|
||||
|
||||
$: name =
|
||||
IntegrationNames[datasource.type] || datasource.name || datasource.type
|
||||
|
||||
async function validateConfig() {
|
||||
const displayError = message =>
|
||||
notifications.error(message ?? "Error validating datasource")
|
||||
|
||||
let connected = false
|
||||
try {
|
||||
const resp = await validateDatasourceConfig(datasource)
|
||||
if (!resp.connected) {
|
||||
displayError(`Unable to connect - ${resp.error}`)
|
||||
}
|
||||
connected = resp.connected
|
||||
} catch (err) {
|
||||
displayError(err?.message)
|
||||
}
|
||||
return connected
|
||||
}
|
||||
|
||||
async function saveDatasource() {
|
||||
if (integration.features[DatasourceFeature.CONNECTION_CHECKING]) {
|
||||
const valid = await validateConfig()
|
||||
if (!valid) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
try {
|
||||
if (!datasource.name) {
|
||||
datasource.name = name
|
||||
}
|
||||
const resp = await save(datasource, skipFetch)
|
||||
const resp = await save(datasource)
|
||||
$goto(`./datasource/${resp._id}`)
|
||||
notifications.success(`Datasource updated successfully.`)
|
||||
notifications.success(`Datasource created successfully.`)
|
||||
} catch (err) {
|
||||
notifications.error(err?.message ?? "Error saving datasource")
|
||||
// prevent the modal from closing
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
skipFetch = false
|
||||
})
|
||||
</script>
|
||||
|
||||
<ModalContent
|
||||
title={`Connect to ${name}`}
|
||||
onConfirm={() => saveDatasource()}
|
||||
onCancel={() => modal.show()}
|
||||
confirmText={datasource.plus
|
||||
? "Save and fetch tables"
|
||||
: "Save and continue to query"}
|
||||
confirmText={datasource.plus ? "Connect" : "Save and continue to query"}
|
||||
cancelText="Back"
|
||||
showSecondaryButton={datasource.plus}
|
||||
secondaryButtonText={datasource.plus ? "Skip table fetch" : undefined}
|
||||
secondaryAction={() => {
|
||||
skipFetch = true
|
||||
saveDatasource()
|
||||
return true
|
||||
}}
|
||||
size="L"
|
||||
disabled={!isValid}
|
||||
>
|
||||
|
|
|
@ -8,7 +8,6 @@
|
|||
import { onMount } from "svelte"
|
||||
|
||||
export let integration
|
||||
export let modal
|
||||
|
||||
// kill the reference so the input isn't saved
|
||||
let datasource = cloneDeep(integration)
|
||||
|
@ -21,7 +20,6 @@
|
|||
|
||||
<ModalContent
|
||||
title={`Connect to ${IntegrationNames[datasource.type]}`}
|
||||
onCancel={() => modal.show()}
|
||||
cancelText="Back"
|
||||
size="L"
|
||||
>
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<script>
|
||||
import { Select } from "@budibase/bbui"
|
||||
import { Select, Toggle, Multiselect } from "@budibase/bbui"
|
||||
import { FIELDS } from "constants/backend"
|
||||
import { API } from "api"
|
||||
import { parseFile } from "./utils"
|
||||
|
@ -9,14 +9,17 @@
|
|||
let fileType = null
|
||||
|
||||
let loading = false
|
||||
let updateExistingRows = false
|
||||
let validation = {}
|
||||
let validateHash = ""
|
||||
let schema = null
|
||||
let invalidColumns = []
|
||||
|
||||
export let tableId = null
|
||||
export let tableType
|
||||
export let rows = []
|
||||
export let allValid = false
|
||||
export let identifierFields = []
|
||||
|
||||
const typeOptions = [
|
||||
{
|
||||
|
@ -159,6 +162,22 @@
|
|||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{#if tableType === "internal"}
|
||||
<br />
|
||||
<Toggle
|
||||
bind:value={updateExistingRows}
|
||||
on:change={() => (identifierFields = [])}
|
||||
thin
|
||||
text="Update existing rows"
|
||||
/>
|
||||
{#if updateExistingRows}
|
||||
<Multiselect
|
||||
label="Identifier field(s)"
|
||||
options={Object.keys(validation)}
|
||||
bind:value={identifierFields}
|
||||
/>
|
||||
{/if}
|
||||
{/if}
|
||||
{#if invalidColumns.length > 0}
|
||||
<p class="spectrum-FieldLabel spectrum-FieldLabel--sizeM">
|
||||
The following columns are present in the data you wish to import, but do
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
import { API } from "api"
|
||||
import { parseFile } from "./utils"
|
||||
|
||||
let fileInput
|
||||
let error = null
|
||||
let fileName = null
|
||||
let fileType = null
|
||||
|
@ -16,6 +17,7 @@
|
|||
export let schema = {}
|
||||
export let allValid = true
|
||||
export let displayColumn = null
|
||||
export let promptUpload = false
|
||||
|
||||
const typeOptions = [
|
||||
{
|
||||
|
@ -99,10 +101,19 @@
|
|||
schema[name].type = e.detail
|
||||
schema[name].constraints = FIELDS[e.detail.toUpperCase()].constraints
|
||||
}
|
||||
|
||||
const openFileUpload = (promptUpload, fileInput) => {
|
||||
if (promptUpload && fileInput) {
|
||||
fileInput.click()
|
||||
}
|
||||
}
|
||||
|
||||
$: openFileUpload(promptUpload, fileInput)
|
||||
</script>
|
||||
|
||||
<div class="dropzone">
|
||||
<input
|
||||
bind:this={fileInput}
|
||||
disabled={loading}
|
||||
id="file-upload"
|
||||
accept="text/csv,application/json"
|
||||
|
|
|
@ -6,7 +6,8 @@
|
|||
import NavItem from "components/common/NavItem.svelte"
|
||||
import { goto, isActive } from "@roxi/routify"
|
||||
|
||||
const alphabetical = (a, b) => a.name?.toLowerCase() > b.name?.toLowerCase()
|
||||
const alphabetical = (a, b) =>
|
||||
a.name?.toLowerCase() > b.name?.toLowerCase() ? 1 : -1
|
||||
|
||||
export let sourceId
|
||||
|
||||
|
|
|
@ -28,6 +28,7 @@
|
|||
? selectedSource._id
|
||||
: BUDIBASE_INTERNAL_DB_ID
|
||||
|
||||
export let promptUpload = false
|
||||
export let name
|
||||
export let beforeSave = async () => {}
|
||||
export let afterSave = async table => {
|
||||
|
@ -136,7 +137,13 @@
|
|||
<Label grey extraSmall
|
||||
>Create a Table from a CSV or JSON file (Optional)</Label
|
||||
>
|
||||
<TableDataImport bind:rows bind:schema bind:allValid bind:displayColumn />
|
||||
<TableDataImport
|
||||
{promptUpload}
|
||||
bind:rows
|
||||
bind:schema
|
||||
bind:allValid
|
||||
bind:displayColumn
|
||||
/>
|
||||
</Layout>
|
||||
</div>
|
||||
</ModalContent>
|
||||
|
|
|
@ -1,143 +0,0 @@
|
|||
<script>
|
||||
import {
|
||||
Button,
|
||||
ButtonGroup,
|
||||
ModalContent,
|
||||
Modal,
|
||||
notifications,
|
||||
ProgressCircle,
|
||||
Layout,
|
||||
Body,
|
||||
Icon,
|
||||
} from "@budibase/bbui"
|
||||
import { auth, apps } from "stores/portal"
|
||||
import { processStringSync } from "@budibase/string-templates"
|
||||
import { API } from "api"
|
||||
|
||||
export let app
|
||||
export let buttonSize = "M"
|
||||
|
||||
let APP_DEV_LOCK_SECONDS = 600 //common area for this?
|
||||
let appLockModal
|
||||
let processing = false
|
||||
|
||||
$: lockedBy = app?.lockedBy
|
||||
$: lockedByYou = $auth.user.email === lockedBy?.email
|
||||
|
||||
$: lockIdentifer = `${
|
||||
lockedBy && lockedBy.firstName ? lockedBy?.firstName : lockedBy?.email
|
||||
}`
|
||||
|
||||
$: lockedByHeading =
|
||||
lockedBy && lockedByYou ? "Locked by you" : `Locked by ${lockIdentifer}`
|
||||
|
||||
const getExpiryDuration = app => {
|
||||
if (!app?.lockedBy?.lockedAt) {
|
||||
return -1
|
||||
}
|
||||
let expiry =
|
||||
new Date(app.lockedBy.lockedAt).getTime() + APP_DEV_LOCK_SECONDS * 1000
|
||||
return expiry - new Date().getTime()
|
||||
}
|
||||
|
||||
const releaseLock = async () => {
|
||||
processing = true
|
||||
if (app) {
|
||||
try {
|
||||
await API.releaseAppLock(app.devId)
|
||||
await apps.load()
|
||||
notifications.success("Lock released successfully")
|
||||
} catch (err) {
|
||||
notifications.error("Error releasing lock")
|
||||
}
|
||||
} else {
|
||||
notifications.error("No application is selected")
|
||||
}
|
||||
processing = false
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if lockedBy}
|
||||
<div class="lock-status">
|
||||
<Icon
|
||||
name="LockClosed"
|
||||
hoverable
|
||||
size={buttonSize}
|
||||
on:click={e => {
|
||||
e.stopPropagation()
|
||||
appLockModal.show()
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<Modal bind:this={appLockModal}>
|
||||
<ModalContent
|
||||
title={lockedByHeading}
|
||||
showConfirmButton={false}
|
||||
showCancelButton={false}
|
||||
>
|
||||
<Layout noPadding>
|
||||
<Body size="S">
|
||||
Apps are locked to prevent work being lost from overlapping changes
|
||||
between your team.
|
||||
</Body>
|
||||
{#if lockedByYou && getExpiryDuration(app) > 0}
|
||||
<span class="lock-expiry-body">
|
||||
{processStringSync(
|
||||
"This lock will expire in {{ duration time 'millisecond' }} from now.",
|
||||
{
|
||||
time: getExpiryDuration(app),
|
||||
}
|
||||
)}
|
||||
</span>
|
||||
{/if}
|
||||
<div class="lock-modal-actions">
|
||||
<ButtonGroup>
|
||||
<Button
|
||||
secondary
|
||||
quiet={lockedBy && lockedByYou}
|
||||
disabled={processing}
|
||||
on:click={() => {
|
||||
appLockModal.hide()
|
||||
}}
|
||||
>
|
||||
<span class="cancel"
|
||||
>{lockedBy && !lockedByYou ? "Done" : "Cancel"}</span
|
||||
>
|
||||
</Button>
|
||||
{#if lockedByYou}
|
||||
<Button
|
||||
cta
|
||||
disabled={processing}
|
||||
on:click={() => {
|
||||
releaseLock()
|
||||
appLockModal.hide()
|
||||
}}
|
||||
>
|
||||
{#if processing}
|
||||
<ProgressCircle overBackground={true} size="S" />
|
||||
{:else}
|
||||
<span class="unlock">Release Lock</span>
|
||||
{/if}
|
||||
</Button>
|
||||
{/if}
|
||||
</ButtonGroup>
|
||||
</div>
|
||||
</Layout>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
|
||||
<style>
|
||||
.lock-modal-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin-top: var(--spacing-l);
|
||||
gap: var(--spacing-xl);
|
||||
}
|
||||
.lock-status {
|
||||
display: flex;
|
||||
gap: var(--spacing-s);
|
||||
max-width: 175px;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,289 @@
|
|||
<script>
|
||||
import { Label } from "@budibase/bbui"
|
||||
import { onMount, createEventDispatcher } from "svelte"
|
||||
|
||||
import {
|
||||
autocompletion,
|
||||
closeBrackets,
|
||||
completionKeymap,
|
||||
closeBracketsKeymap,
|
||||
} from "@codemirror/autocomplete"
|
||||
import {
|
||||
EditorView,
|
||||
lineNumbers,
|
||||
keymap,
|
||||
highlightSpecialChars,
|
||||
drawSelection,
|
||||
dropCursor,
|
||||
highlightActiveLine,
|
||||
highlightActiveLineGutter,
|
||||
highlightWhitespace,
|
||||
placeholder as placeholderFn,
|
||||
MatchDecorator,
|
||||
ViewPlugin,
|
||||
Decoration,
|
||||
} from "@codemirror/view"
|
||||
import {
|
||||
bracketMatching,
|
||||
foldKeymap,
|
||||
foldGutter,
|
||||
syntaxHighlighting,
|
||||
} from "@codemirror/language"
|
||||
import { oneDark, oneDarkHighlightStyle } from "@codemirror/theme-one-dark"
|
||||
import {
|
||||
defaultKeymap,
|
||||
historyKeymap,
|
||||
history,
|
||||
indentWithTab,
|
||||
} from "@codemirror/commands"
|
||||
import { Compartment } from "@codemirror/state"
|
||||
import { javascript } from "@codemirror/lang-javascript"
|
||||
import { EditorModes, getDefaultTheme } from "./"
|
||||
import { themeStore } from "builderStore"
|
||||
|
||||
export let label
|
||||
export let completions = []
|
||||
export let height = 200
|
||||
export let resize = "none"
|
||||
export let mode = EditorModes.Handlebars
|
||||
export let value = ""
|
||||
export let placeholder = null
|
||||
|
||||
// Export a function to expose caret position
|
||||
export const getCaretPosition = () => {
|
||||
const selection_range = editor.state.selection.ranges[0]
|
||||
return {
|
||||
start: selection_range.from,
|
||||
end: selection_range.to,
|
||||
}
|
||||
}
|
||||
|
||||
export const insertAtPos = opts => {
|
||||
// Updating the value inside.
|
||||
// Retain focus
|
||||
editor.dispatch({
|
||||
changes: {
|
||||
from: opts.start || editor.state.doc.length,
|
||||
to: opts.end || editor.state.doc.length,
|
||||
insert: opts.value,
|
||||
},
|
||||
selection: opts.cursor
|
||||
? {
|
||||
anchor: opts.start + opts.value.length,
|
||||
}
|
||||
: undefined,
|
||||
})
|
||||
}
|
||||
|
||||
// For handlebars only.
|
||||
const bindStyle = new MatchDecorator({
|
||||
regexp: /{{[."#\-\w\s\][]*}}/g,
|
||||
decoration: () => {
|
||||
return Decoration.mark({
|
||||
tag: "span",
|
||||
attributes: {
|
||||
class: "binding-wrap",
|
||||
},
|
||||
})
|
||||
},
|
||||
})
|
||||
|
||||
let plugin = ViewPlugin.define(
|
||||
view => ({
|
||||
decorations: bindStyle.createDeco(view),
|
||||
update(u) {
|
||||
this.decorations = bindStyle.updateDeco(u, this.decorations)
|
||||
},
|
||||
}),
|
||||
{
|
||||
decorations: v => v.decorations,
|
||||
}
|
||||
)
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
// Theming!
|
||||
let currentTheme = $themeStore?.theme
|
||||
let isDark = !currentTheme.includes("light")
|
||||
let themeConfig = new Compartment()
|
||||
|
||||
const buildKeymap = () => {
|
||||
const baseMap = [
|
||||
...closeBracketsKeymap,
|
||||
...defaultKeymap,
|
||||
...historyKeymap,
|
||||
...foldKeymap,
|
||||
...completionKeymap,
|
||||
indentWithTab,
|
||||
]
|
||||
return baseMap
|
||||
}
|
||||
|
||||
const buildBaseExtensions = () => {
|
||||
return [
|
||||
...(mode.name === "handlebars" ? [plugin] : []),
|
||||
history(),
|
||||
drawSelection(),
|
||||
dropCursor(),
|
||||
bracketMatching(),
|
||||
closeBrackets(),
|
||||
highlightActiveLine(),
|
||||
syntaxHighlighting(oneDarkHighlightStyle, { fallback: true }),
|
||||
highlightActiveLineGutter(),
|
||||
highlightSpecialChars(),
|
||||
autocompletion({
|
||||
override: [...completions],
|
||||
closeOnBlur: true,
|
||||
icons: false,
|
||||
optionClass: () => "autocomplete-option",
|
||||
}),
|
||||
EditorView.lineWrapping,
|
||||
EditorView.updateListener.of(v => {
|
||||
const docStr = v.state.doc?.toString()
|
||||
if (docStr === value) {
|
||||
return
|
||||
}
|
||||
dispatch("change", docStr)
|
||||
}),
|
||||
keymap.of(buildKeymap()),
|
||||
themeConfig.of([
|
||||
getDefaultTheme({
|
||||
height: editorHeight,
|
||||
resize,
|
||||
dark: isDark,
|
||||
}),
|
||||
...(isDark ? [oneDark] : []),
|
||||
]),
|
||||
]
|
||||
}
|
||||
|
||||
const buildExtensions = base => {
|
||||
const complete = [...base]
|
||||
if (mode.name == "javascript") {
|
||||
complete.push(javascript())
|
||||
complete.push(highlightWhitespace())
|
||||
complete.push(lineNumbers())
|
||||
complete.push(foldGutter())
|
||||
complete.push(
|
||||
EditorView.inputHandler.of((view, from, to, insert) => {
|
||||
if (insert === "$") {
|
||||
let { text } = view.state.doc.lineAt(from)
|
||||
|
||||
const left = from ? text.substring(0, from) : ""
|
||||
const right = to ? text.substring(to) : ""
|
||||
const wrap = !left.includes('$("') || !right.includes('")')
|
||||
const tr = view.state.update(
|
||||
{
|
||||
changes: [{ from, insert: wrap ? '$("")' : "$" }],
|
||||
selection: {
|
||||
anchor: from + (wrap ? 3 : 1),
|
||||
},
|
||||
},
|
||||
{
|
||||
scrollIntoView: true,
|
||||
userEvent: "input.type",
|
||||
}
|
||||
)
|
||||
view.dispatch(tr)
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
if (placeholder) {
|
||||
complete.push(placeholderFn(placeholder))
|
||||
}
|
||||
return complete
|
||||
}
|
||||
|
||||
let textarea
|
||||
let editor
|
||||
let mounted = false
|
||||
let isEditorInitialised = false
|
||||
|
||||
const initEditor = () => {
|
||||
const baseExtensions = buildBaseExtensions()
|
||||
|
||||
editor = new EditorView({
|
||||
doc: value,
|
||||
extensions: buildExtensions(baseExtensions),
|
||||
parent: textarea,
|
||||
})
|
||||
}
|
||||
|
||||
$: editorHeight = typeof height === "number" ? `${height}px` : height
|
||||
|
||||
// Init when all elements are ready
|
||||
$: if (mounted && !isEditorInitialised) {
|
||||
isEditorInitialised = true
|
||||
initEditor()
|
||||
}
|
||||
|
||||
// Theme change
|
||||
$: if (mounted && isEditorInitialised && $themeStore?.theme) {
|
||||
if (currentTheme != $themeStore?.theme) {
|
||||
currentTheme = $themeStore?.theme
|
||||
isDark = !currentTheme.includes("light")
|
||||
|
||||
// Issue theme compartment update
|
||||
editor.dispatch({
|
||||
effects: themeConfig.reconfigure([
|
||||
getDefaultTheme({
|
||||
height: editorHeight,
|
||||
resize,
|
||||
dark: isDark,
|
||||
}),
|
||||
...(isDark ? [oneDark] : []),
|
||||
]),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
onMount(async () => {
|
||||
mounted = true
|
||||
return () => {
|
||||
if (editor) {
|
||||
editor.destroy()
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
{#if label}
|
||||
<div>
|
||||
<Label small>{label}</Label>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class={`code-editor ${mode?.name || ""}`}>
|
||||
<div tabindex="-1" bind:this={textarea} />
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.code-editor.handlebars :global(.cm-content) {
|
||||
font-family: var(--font-sans);
|
||||
}
|
||||
.code-editor :global(.cm-tooltip.cm-completionInfo) {
|
||||
padding: var(--spacing-m);
|
||||
}
|
||||
.code-editor :global(.cm-tooltip-autocomplete > ul > li[aria-selected]) {
|
||||
border-radius: var(
|
||||
--spectrum-popover-border-radius,
|
||||
var(--spectrum-alias-border-radius-regular)
|
||||
),
|
||||
var(
|
||||
--spectrum-popover-border-radius,
|
||||
var(--spectrum-alias-border-radius-regular)
|
||||
),
|
||||
0, 0;
|
||||
}
|
||||
|
||||
.code-editor :global(.autocomplete-option .cm-completionDetail) {
|
||||
background-color: var(--spectrum-global-color-gray-200);
|
||||
border-radius: var(--border-radius-s);
|
||||
padding: 4px 6px;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,387 @@
|
|||
import { EditorView } from "@codemirror/view"
|
||||
import { getManifest } from "@budibase/string-templates"
|
||||
import sanitizeHtml from "sanitize-html"
|
||||
import { groupBy } from "lodash"
|
||||
|
||||
export const EditorModes = {
|
||||
JS: {
|
||||
name: "javascript",
|
||||
json: false,
|
||||
match: /\$$/,
|
||||
},
|
||||
Handlebars: {
|
||||
name: "handlebars",
|
||||
base: "text/html",
|
||||
match: /{{[\s]*[\w\s]*/,
|
||||
},
|
||||
Text: {
|
||||
name: "text/html",
|
||||
},
|
||||
}
|
||||
|
||||
export const SECTIONS = {
|
||||
HB_HELPER: {
|
||||
name: "Helper",
|
||||
type: "helper",
|
||||
icon: "Code",
|
||||
},
|
||||
}
|
||||
|
||||
export const getDefaultTheme = opts => {
|
||||
const { height, resize, dark } = opts
|
||||
return EditorView.theme(
|
||||
{
|
||||
"&.cm-focused .cm-cursor": {
|
||||
borderLeftColor: "var(--spectrum-alias-text-color)",
|
||||
},
|
||||
"&": {
|
||||
height: height ? `${height}` : "",
|
||||
lineHeight: "1.3",
|
||||
border:
|
||||
"var(--spectrum-alias-border-size-thin) solid var(--spectrum-alias-border-color)",
|
||||
borderRadius: "var(--border-radius-s)",
|
||||
backgroundColor:
|
||||
"var( --spectrum-textfield-m-background-color, var(--spectrum-global-color-gray-50) )",
|
||||
resize: resize ? `${resize}` : "",
|
||||
overflow: "hidden",
|
||||
color: "var(--spectrum-alias-text-color)",
|
||||
},
|
||||
"& .cm-tooltip.cm-tooltip-autocomplete > ul": {
|
||||
fontFamily:
|
||||
"var(--spectrum-alias-body-text-font-family, var(--spectrum-global-font-family-base))",
|
||||
maxHeight: "16em",
|
||||
},
|
||||
"& .cm-placeholder": {
|
||||
color: "var(--spectrum-alias-text-color)",
|
||||
fontStyle: "italic",
|
||||
},
|
||||
"&.cm-focused": {
|
||||
outline: "none",
|
||||
borderColor: "var(--spectrum-alias-border-color-mouse-focus)",
|
||||
},
|
||||
// AUTO COMPLETE
|
||||
"& .cm-completionDetail": {
|
||||
fontStyle: "unset",
|
||||
textTransform: "uppercase",
|
||||
fontSize: "10px",
|
||||
backgroundColor: "var(--spectrum-global-color-gray-100)",
|
||||
color: "var(--spectrum-global-color-gray-600)",
|
||||
},
|
||||
"& .cm-completionLabel": {
|
||||
marginLeft:
|
||||
"calc(var(--spectrum-alias-workflow-icon-size-m) + var(--spacing-m))",
|
||||
},
|
||||
"& .info-bubble": {
|
||||
fontSize: "var(--font-size-s)",
|
||||
display: "grid",
|
||||
gridGap: "var(--spacing-s)",
|
||||
gridTemplateColumns: "1fr",
|
||||
color: "var(--spectrum-global-color-gray-800)",
|
||||
},
|
||||
"& .cm-tooltip": {
|
||||
marginLeft: "var(--spacing-s)",
|
||||
border: "1px solid var(--spectrum-global-color-gray-300)",
|
||||
borderRadius:
|
||||
"var( --spectrum-popover-border-radius, var(--spectrum-alias-border-radius-regular) )",
|
||||
backgroundColor: "var(--spectrum-global-color-gray-50)",
|
||||
},
|
||||
// Section header
|
||||
"& .info-section": {
|
||||
display: "flex",
|
||||
padding: "var(--spacing-s)",
|
||||
gap: "var(--spacing-m)",
|
||||
borderBottom: "1px solid var(--spectrum-global-color-gray-200)",
|
||||
color: "var(--spectrum-global-color-gray-800)",
|
||||
fontWeight: "bold",
|
||||
},
|
||||
"& .info-section .spectrum-Icon": {
|
||||
color: "var(--spectrum-global-color-gray-600)",
|
||||
},
|
||||
// Autocomplete Option
|
||||
"& .cm-tooltip.cm-tooltip-autocomplete .autocomplete-option": {
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
fontSize: "var(--spectrum-alias-font-size-default)",
|
||||
padding: "var(--spacing-s)",
|
||||
color: "var(--spectrum-global-color-gray-800)",
|
||||
},
|
||||
"& .cm-tooltip-autocomplete ul li[aria-selected].autocomplete-option": {
|
||||
backgroundColor: "var(--spectrum-global-color-gray-200)",
|
||||
},
|
||||
"& .binding-wrap": {
|
||||
color: "var(--spectrum-global-color-blue-700)",
|
||||
fontFamily: "monospace",
|
||||
},
|
||||
},
|
||||
{ dark }
|
||||
)
|
||||
}
|
||||
|
||||
export const buildHelperInfoNode = (completion, helper) => {
|
||||
const ele = document.createElement("div")
|
||||
ele.classList.add("info-bubble")
|
||||
|
||||
const exampleNodeHtml = helper.example
|
||||
? `<div class="binding__example">${helper.example}</div>`
|
||||
: ""
|
||||
const descriptionMarkup = sanitizeHtml(helper.description, {
|
||||
allowedTags: [],
|
||||
allowedAttributes: {},
|
||||
})
|
||||
const descriptionNodeHtml = `<div class="binding__description">${descriptionMarkup}</div>`
|
||||
|
||||
ele.innerHTML = `
|
||||
${exampleNodeHtml}
|
||||
${descriptionNodeHtml}
|
||||
`
|
||||
return ele
|
||||
}
|
||||
|
||||
const toSpectrumIcon = name => {
|
||||
return `<svg
|
||||
class="spectrum-Icon spectrum-Icon--sizeM"
|
||||
focusable="false"
|
||||
aria-hidden="false"
|
||||
aria-label="${name}-section-icon"
|
||||
>
|
||||
<use style="pointer-events: none;" xlink:href="#spectrum-icon-18-${name}" />
|
||||
</svg>`
|
||||
}
|
||||
|
||||
export const buildSectionHeader = (type, sectionName, icon, rank) => {
|
||||
const ele = document.createElement("div")
|
||||
ele.classList.add("info-section")
|
||||
ele.classList.add(type)
|
||||
ele.innerHTML = `${toSpectrumIcon(icon)}<span>${sectionName}</span>`
|
||||
return {
|
||||
name: sectionName,
|
||||
header: () => ele,
|
||||
rank,
|
||||
}
|
||||
}
|
||||
|
||||
export const helpersToCompletion = (helpers, mode) => {
|
||||
const { type, name: sectionName, icon } = SECTIONS.HB_HELPER
|
||||
const helperSection = buildSectionHeader(type, sectionName, icon, 99)
|
||||
|
||||
return Object.keys(helpers).reduce((acc, key) => {
|
||||
let helper = helpers[key]
|
||||
acc.push({
|
||||
label: key,
|
||||
info: completion => {
|
||||
return buildHelperInfoNode(completion, helper)
|
||||
},
|
||||
type: "helper",
|
||||
section: helperSection,
|
||||
detail: "FUNCTION",
|
||||
apply: (view, completion, from, to) => {
|
||||
insertBinding(view, from, to, key, mode)
|
||||
},
|
||||
})
|
||||
return acc
|
||||
}, [])
|
||||
}
|
||||
|
||||
export const getHelperCompletions = mode => {
|
||||
const manifest = getManifest()
|
||||
return Object.keys(manifest).reduce((acc, key) => {
|
||||
acc = acc || []
|
||||
return [...acc, ...helpersToCompletion(manifest[key], mode)]
|
||||
}, [])
|
||||
}
|
||||
|
||||
const bindingFilter = (options, query) => {
|
||||
return options.filter(completion => {
|
||||
const section_parsed = completion.section.name.toLowerCase()
|
||||
const label_parsed = completion.label.toLowerCase()
|
||||
const query_parsed = query.toLowerCase()
|
||||
|
||||
return (
|
||||
section_parsed.includes(query_parsed) ||
|
||||
label_parsed.includes(query_parsed)
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
export const hbAutocomplete = baseCompletions => {
|
||||
async function coreCompletion(context) {
|
||||
let bindingStart = context.matchBefore(EditorModes.Handlebars.match)
|
||||
|
||||
let options = baseCompletions || []
|
||||
|
||||
if (!bindingStart) {
|
||||
return null
|
||||
}
|
||||
// Accommodate spaces
|
||||
const match = bindingStart.text.match(/{{[\s]*/)
|
||||
const query = bindingStart.text.replace(match[0], "")
|
||||
let filtered = bindingFilter(options, query)
|
||||
|
||||
return {
|
||||
from: bindingStart.from + match[0].length,
|
||||
filter: false,
|
||||
options: filtered,
|
||||
}
|
||||
}
|
||||
|
||||
return coreCompletion
|
||||
}
|
||||
|
||||
export const jsAutocomplete = baseCompletions => {
|
||||
async function coreCompletion(context) {
|
||||
let jsBinding = context.matchBefore(/\$\("[\s\w]*/)
|
||||
let options = baseCompletions || []
|
||||
|
||||
if (jsBinding) {
|
||||
// Accommodate spaces
|
||||
const match = jsBinding.text.match(/\$\("[\s]*/)
|
||||
const query = jsBinding.text.replace(match[0], "")
|
||||
let filtered = bindingFilter(options, query)
|
||||
return {
|
||||
from: jsBinding.from + match[0].length,
|
||||
filter: false,
|
||||
options: filtered,
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
return coreCompletion
|
||||
}
|
||||
|
||||
export const buildBindingInfoNode = (completion, binding) => {
|
||||
const ele = document.createElement("div")
|
||||
ele.classList.add("info-bubble")
|
||||
|
||||
const exampleNodeHtml = binding.readableBinding
|
||||
? `<div class="binding__example">{{ ${binding.readableBinding} }}</div>`
|
||||
: ""
|
||||
|
||||
const descriptionNodeHtml = binding.description
|
||||
? `<div class="binding__description">${binding.description}</div>`
|
||||
: ""
|
||||
|
||||
ele.innerHTML = `
|
||||
${exampleNodeHtml}
|
||||
${descriptionNodeHtml}
|
||||
`
|
||||
return ele
|
||||
}
|
||||
|
||||
// Readdress these methods. They shouldn't be used
|
||||
export const hbInsert = (value, from, to, text) => {
|
||||
let parsedInsert = ""
|
||||
|
||||
const left = from ? value.substring(0, from) : ""
|
||||
const right = to ? value.substring(to) : ""
|
||||
|
||||
if (!left.includes("{{") || !right.includes("}}")) {
|
||||
parsedInsert = `{{ ${text} }}`
|
||||
} else {
|
||||
parsedInsert = ` ${text} `
|
||||
}
|
||||
|
||||
return parsedInsert
|
||||
}
|
||||
|
||||
export function jsInsert(value, from, to, text, { helper } = {}) {
|
||||
let parsedInsert = ""
|
||||
|
||||
const left = from ? value.substring(0, from) : ""
|
||||
const right = to ? value.substring(to) : ""
|
||||
|
||||
if (helper) {
|
||||
parsedInsert = `helpers.${text}()`
|
||||
} else if (!left.includes('$("') || !right.includes('")')) {
|
||||
parsedInsert = `$("${text}")`
|
||||
} else {
|
||||
parsedInsert = text
|
||||
}
|
||||
|
||||
return parsedInsert
|
||||
}
|
||||
|
||||
// Autocomplete apply behaviour
|
||||
export const insertBinding = (view, from, to, text, mode) => {
|
||||
let parsedInsert
|
||||
|
||||
if (mode.name == "javascript") {
|
||||
parsedInsert = jsInsert(view.state.doc?.toString(), from, to, text)
|
||||
} else if (mode.name == "handlebars") {
|
||||
parsedInsert = hbInsert(view.state.doc?.toString(), from, to, text)
|
||||
} else {
|
||||
console.log("Unsupported")
|
||||
return
|
||||
}
|
||||
|
||||
let bindingClosePattern = mode.name == "javascript" ? /[\s]*"\)/ : /[\s]*}}/
|
||||
let sliced = view.state.doc?.toString().slice(to)
|
||||
|
||||
const rightBrace = sliced.match(bindingClosePattern)
|
||||
let cursorPos = from + parsedInsert.length
|
||||
|
||||
if (rightBrace) {
|
||||
cursorPos = from + parsedInsert.length + rightBrace[0].length
|
||||
}
|
||||
|
||||
view.dispatch({
|
||||
changes: {
|
||||
from,
|
||||
to,
|
||||
insert: parsedInsert,
|
||||
},
|
||||
selection: {
|
||||
anchor: cursorPos,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export const bindingsToCompletions = (bindings, mode) => {
|
||||
const bindingByCategory = groupBy(bindings, "category")
|
||||
const categoryMeta = bindings?.reduce((acc, ele) => {
|
||||
acc[ele.category] = acc[ele.category] || {}
|
||||
|
||||
if (ele.icon) {
|
||||
acc[ele.category]["icon"] = acc[ele.category]["icon"] || ele.icon
|
||||
}
|
||||
if (typeof ele.display?.rank == "number") {
|
||||
acc[ele.category]["rank"] = acc[ele.category]["rank"] || ele.display.rank
|
||||
}
|
||||
return acc
|
||||
}, {})
|
||||
|
||||
const completions = Object.keys(bindingByCategory).reduce((comps, catKey) => {
|
||||
const { icon, rank } = categoryMeta[catKey] || {}
|
||||
|
||||
const bindindSectionHeader = buildSectionHeader(
|
||||
bindingByCategory.type,
|
||||
catKey,
|
||||
icon || "",
|
||||
typeof rank == "number" ? rank : 1
|
||||
)
|
||||
|
||||
return [
|
||||
...comps,
|
||||
...bindingByCategory[catKey].reduce((acc, binding) => {
|
||||
let displayType = binding.fieldSchema?.type || binding.display?.type
|
||||
acc.push({
|
||||
label: binding.display?.name || "NO NAME",
|
||||
info: completion => {
|
||||
return buildBindingInfoNode(completion, binding)
|
||||
},
|
||||
type: "binding",
|
||||
detail: displayType,
|
||||
section: bindindSectionHeader,
|
||||
apply: (view, completion, from, to) => {
|
||||
insertBinding(view, from, to, binding.readableBinding, mode)
|
||||
},
|
||||
})
|
||||
return acc
|
||||
}, []),
|
||||
]
|
||||
}, [])
|
||||
|
||||
return completions
|
||||
}
|
|
@ -146,15 +146,18 @@
|
|||
|
||||
/* Override default active line highlight colour in dark theme */
|
||||
div
|
||||
:global(.CodeMirror-focused.cm-s-tomorrow-night-eighties
|
||||
.CodeMirror-activeline-background) {
|
||||
:global(
|
||||
.CodeMirror-focused.cm-s-tomorrow-night-eighties
|
||||
.CodeMirror-activeline-background
|
||||
) {
|
||||
background: rgba(255, 255, 255, 0.075);
|
||||
}
|
||||
|
||||
/* Remove active line styling when not focused */
|
||||
div
|
||||
:global(.CodeMirror:not(.CodeMirror-focused)
|
||||
.CodeMirror-activeline-background) {
|
||||
:global(
|
||||
.CodeMirror:not(.CodeMirror-focused) .CodeMirror-activeline-background
|
||||
) {
|
||||
background: unset;
|
||||
}
|
||||
|
||||
|
|
|
@ -28,7 +28,6 @@
|
|||
.dash-card {
|
||||
background: var(--spectrum-alias-background-color-primary);
|
||||
border-radius: var(--border-radius-s);
|
||||
overflow: hidden;
|
||||
min-height: 170px;
|
||||
}
|
||||
.dash-card-header {
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
faLock,
|
||||
faFileArrowUp,
|
||||
faChevronLeft,
|
||||
faCircleInfo,
|
||||
} from "@fortawesome/free-solid-svg-icons"
|
||||
import { faGithub, faDiscord } from "@fortawesome/free-brands-svg-icons"
|
||||
|
||||
|
@ -20,7 +21,8 @@
|
|||
faDiscord,
|
||||
faEnvelope,
|
||||
faFileArrowUp,
|
||||
faChevronLeft
|
||||
faChevronLeft,
|
||||
faCircleInfo
|
||||
)
|
||||
dom.watch()
|
||||
</script>
|
||||
|
|
|
@ -83,7 +83,7 @@
|
|||
.help {
|
||||
z-index: 2;
|
||||
position: absolute;
|
||||
bottom: var(--spacing-xl);
|
||||
bottom: 24px;
|
||||
right: 24px;
|
||||
}
|
||||
|
||||
|
|
|
@ -17,6 +17,7 @@
|
|||
export let highlighted = false
|
||||
export let rightAlignIcon = false
|
||||
export let id
|
||||
export let showTooltip = false
|
||||
|
||||
const scrollApi = getContext("scroll")
|
||||
const dispatch = createEventDispatcher()
|
||||
|
@ -84,7 +85,7 @@
|
|||
<Icon color={iconColor} size="S" name={icon} />
|
||||
</div>
|
||||
{/if}
|
||||
<div class="text">{text}</div>
|
||||
<div class="text" title={showTooltip ? text : null}>{text}</div>
|
||||
{#if withActions}
|
||||
<div class="actions">
|
||||
<slot />
|
||||
|
|
|
@ -1,17 +1,13 @@
|
|||
<script>
|
||||
import groupBy from "lodash/fp/groupBy"
|
||||
import {
|
||||
Search,
|
||||
TextArea,
|
||||
DrawerContent,
|
||||
Tabs,
|
||||
Tab,
|
||||
Body,
|
||||
Layout,
|
||||
Button,
|
||||
ActionButton,
|
||||
Heading,
|
||||
Icon,
|
||||
Popover,
|
||||
} from "@budibase/bbui"
|
||||
import { createEventDispatcher, onMount } from "svelte"
|
||||
import {
|
||||
|
@ -23,11 +19,21 @@
|
|||
readableToRuntimeBinding,
|
||||
runtimeToReadableBinding,
|
||||
} from "builderStore/dataBinding"
|
||||
import { handlebarsCompletions } from "constants/completions"
|
||||
import { addHBSBinding, addJSBinding } from "./utils"
|
||||
import CodeMirrorEditor from "components/common/CodeMirrorEditor.svelte"
|
||||
import { store } from "builderStore"
|
||||
import { convertToJS } from "@budibase/string-templates"
|
||||
import { admin } from "stores/portal"
|
||||
import CodeEditor from "../CodeEditor/CodeEditor.svelte"
|
||||
import {
|
||||
getHelperCompletions,
|
||||
jsAutocomplete,
|
||||
hbAutocomplete,
|
||||
EditorModes,
|
||||
bindingsToCompletions,
|
||||
hbInsert,
|
||||
jsInsert,
|
||||
} from "../CodeEditor"
|
||||
import { getContext } from "svelte"
|
||||
import BindingPicker from "./BindingPicker.svelte"
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
|
@ -41,54 +47,21 @@
|
|||
export let allowJS = false
|
||||
export let allowHelpers = true
|
||||
|
||||
let helpers = handlebarsCompletions()
|
||||
const drawerActions = getContext("drawer-actions")
|
||||
const bindingDrawerActions = getContext("binding-drawer-actions")
|
||||
|
||||
let getCaretPosition
|
||||
let search = ""
|
||||
let insertAtPos
|
||||
let initialValueJS = typeof value === "string" && value?.startsWith("{{ js ")
|
||||
let mode = initialValueJS ? "JavaScript" : "Handlebars"
|
||||
let mode = initialValueJS ? "JavaScript" : "Text"
|
||||
let jsValue = initialValueJS ? value : null
|
||||
let hbsValue = initialValueJS ? null : value
|
||||
|
||||
let selectedCategory = null
|
||||
|
||||
let popover
|
||||
let popoverAnchor
|
||||
let hoverTarget
|
||||
let sidebar = true
|
||||
let targetMode = null
|
||||
|
||||
$: usingJS = mode === "JavaScript"
|
||||
$: searchRgx = new RegExp(search, "ig")
|
||||
$: categories = Object.entries(groupBy("category", bindings))
|
||||
|
||||
$: bindingIcons = bindings?.reduce((acc, ele) => {
|
||||
if (ele.icon) {
|
||||
acc[ele.category] = acc[ele.category] || ele.icon
|
||||
}
|
||||
return acc
|
||||
}, {})
|
||||
|
||||
$: categoryIcons = { ...bindingIcons, Helpers: "MagicWand" }
|
||||
|
||||
$: filteredCategories = categories
|
||||
.map(([name, categoryBindings]) => ({
|
||||
name,
|
||||
bindings: categoryBindings?.filter(binding => {
|
||||
return binding.readableBinding.match(searchRgx)
|
||||
}),
|
||||
}))
|
||||
.filter(category => {
|
||||
return (
|
||||
category.bindings?.length > 0 &&
|
||||
(!selectedCategory ? true : selectedCategory === category.name)
|
||||
)
|
||||
})
|
||||
|
||||
$: filteredHelpers = helpers?.filter(helper => {
|
||||
return helper.label.match(searchRgx) || helper.description.match(searchRgx)
|
||||
})
|
||||
|
||||
$: categoryNames = getCategoryNames(categories)
|
||||
|
||||
$: codeMirrorHints = bindings?.map(x => `$("${x.readableBinding}")`)
|
||||
$: editorMode = mode == "JavaScript" ? EditorModes.JS : EditorModes.Handlebars
|
||||
$: bindingCompletions = bindingsToCompletions(bindings, editorMode)
|
||||
|
||||
const updateValue = val => {
|
||||
valid = isValid(readableToRuntimeBinding(bindings, val))
|
||||
|
@ -97,43 +70,30 @@
|
|||
}
|
||||
}
|
||||
|
||||
const getCategoryNames = categories => {
|
||||
let names = [...categories.map(cat => cat[0])]
|
||||
if (allowHelpers) {
|
||||
names.push("Helpers")
|
||||
}
|
||||
return names
|
||||
}
|
||||
|
||||
// Adds a JS/HBS helper to the expression
|
||||
const addHelper = (helper, js) => {
|
||||
let tempVal
|
||||
const onSelectHelper = (helper, js) => {
|
||||
const pos = getCaretPosition()
|
||||
const { start, end } = pos
|
||||
if (js) {
|
||||
const decoded = decodeJSBinding(jsValue)
|
||||
tempVal = jsValue = encodeJSBinding(
|
||||
addJSBinding(decoded, pos, helper.text, { helper: true })
|
||||
)
|
||||
let js = decodeJSBinding(jsValue)
|
||||
const insertVal = jsInsert(js, start, end, helper.text, { helper: true })
|
||||
insertAtPos({ start, end, value: insertVal })
|
||||
} else {
|
||||
tempVal = hbsValue = addHBSBinding(hbsValue, pos, helper.text)
|
||||
const insertVal = hbInsert(hbsValue, start, end, helper.text)
|
||||
insertAtPos({ start, end, value: insertVal })
|
||||
}
|
||||
updateValue(tempVal)
|
||||
}
|
||||
|
||||
// Adds a data binding to the expression
|
||||
const addBinding = (binding, { forceJS } = {}) => {
|
||||
const onSelectBinding = (binding, { forceJS } = {}) => {
|
||||
const { start, end } = getCaretPosition()
|
||||
if (usingJS || forceJS) {
|
||||
let js = decodeJSBinding(jsValue)
|
||||
js = addJSBinding(js, getCaretPosition(), binding.readableBinding)
|
||||
jsValue = encodeJSBinding(js)
|
||||
updateValue(jsValue)
|
||||
const insertVal = jsInsert(js, start, end, binding.readableBinding)
|
||||
insertAtPos({ start, end, value: insertVal })
|
||||
} else {
|
||||
hbsValue = addHBSBinding(
|
||||
hbsValue,
|
||||
getCaretPosition(),
|
||||
binding.readableBinding
|
||||
)
|
||||
updateValue(hbsValue)
|
||||
const insertVal = hbInsert(hbsValue, start, end, binding.readableBinding)
|
||||
insertAtPos({ start, end, value: insertVal })
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -152,24 +112,25 @@
|
|||
updateValue(jsValue)
|
||||
}
|
||||
|
||||
const switchMode = () => {
|
||||
if (targetMode == "Text") {
|
||||
jsValue = null
|
||||
updateValue(jsValue)
|
||||
} else {
|
||||
hbsValue = null
|
||||
updateValue(hbsValue)
|
||||
}
|
||||
mode = targetMode + ""
|
||||
targetMode = null
|
||||
}
|
||||
|
||||
const convert = () => {
|
||||
const runtime = readableToRuntimeBinding(bindings, hbsValue)
|
||||
const runtimeJs = encodeJSBinding(convertToJS(runtime))
|
||||
jsValue = runtimeToReadableBinding(bindings, runtimeJs)
|
||||
hbsValue = null
|
||||
mode = "JavaScript"
|
||||
addBinding("", { forceJS: true })
|
||||
}
|
||||
|
||||
const getHelperExample = (helper, js) => {
|
||||
let example = helper.example || ""
|
||||
if (js) {
|
||||
example = convertToJS(example).split("\n")[0].split("= ")[1]
|
||||
if (example === "null;") {
|
||||
example = ""
|
||||
}
|
||||
}
|
||||
return example || ""
|
||||
onSelectBinding("", { forceJS: true })
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
|
@ -177,332 +138,301 @@
|
|||
})
|
||||
</script>
|
||||
|
||||
<span class="detailPopover">
|
||||
<Popover
|
||||
align="right-outside"
|
||||
bind:this={popover}
|
||||
anchor={popoverAnchor}
|
||||
maxWidth={300}
|
||||
dismissible={false}
|
||||
>
|
||||
<Layout gap="S">
|
||||
<div class="helper">
|
||||
{#if hoverTarget.title}
|
||||
<div class="helper__name">{hoverTarget.title}</div>
|
||||
{/if}
|
||||
{#if hoverTarget.description}
|
||||
<div class="helper__description">
|
||||
{@html hoverTarget.description}
|
||||
</div>
|
||||
{/if}
|
||||
{#if hoverTarget.example}
|
||||
<pre class="helper__example">{hoverTarget.example}</pre>
|
||||
{/if}
|
||||
</div>
|
||||
</Layout>
|
||||
</Popover>
|
||||
</span>
|
||||
<span class="binding-drawer">
|
||||
<DrawerContent>
|
||||
<div class="main">
|
||||
<Tabs
|
||||
selected={mode}
|
||||
on:select={onChangeMode}
|
||||
beforeSwitch={selectedMode => {
|
||||
if (selectedMode == mode) {
|
||||
return true
|
||||
}
|
||||
|
||||
<DrawerContent>
|
||||
<svelte:fragment slot="sidebar">
|
||||
<Layout noPadding gap="S">
|
||||
{#if selectedCategory}
|
||||
<div>
|
||||
<ActionButton
|
||||
secondary
|
||||
icon={"ArrowLeft"}
|
||||
on:click={() => {
|
||||
selectedCategory = null
|
||||
}}
|
||||
>
|
||||
Back
|
||||
</ActionButton>
|
||||
</div>
|
||||
{/if}
|
||||
//Get the current mode value
|
||||
const editorValue = usingJS ? decodeJSBinding(jsValue) : hbsValue
|
||||
|
||||
{#if !selectedCategory}
|
||||
<div class="heading">Search</div>
|
||||
<Search placeholder="Search" bind:value={search} />
|
||||
{/if}
|
||||
|
||||
{#if !selectedCategory && !search}
|
||||
<ul class="category-list">
|
||||
{#each categoryNames as categoryName}
|
||||
<li
|
||||
on:click={() => {
|
||||
selectedCategory = categoryName
|
||||
}}
|
||||
>
|
||||
<Icon name={categoryIcons[categoryName]} />
|
||||
<span class="category-name">{categoryName} </span>
|
||||
<span class="category-chevron"><Icon name="ChevronRight" /></span>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
{/if}
|
||||
|
||||
{#if selectedCategory || search}
|
||||
{#each filteredCategories as category}
|
||||
{#if category.bindings?.length}
|
||||
<div class="cat-heading">
|
||||
<Icon name={categoryIcons[category.name]} />{category.name}
|
||||
</div>
|
||||
<ul>
|
||||
{#each category.bindings as binding}
|
||||
<li
|
||||
class="binding"
|
||||
on:mouseenter={e => {
|
||||
popoverAnchor = e.target
|
||||
if (!binding.description) {
|
||||
return
|
||||
}
|
||||
hoverTarget = {
|
||||
title: binding.display?.name || binding.fieldSchema?.name,
|
||||
description: binding.description,
|
||||
}
|
||||
popover.show()
|
||||
e.stopPropagation()
|
||||
}}
|
||||
on:mouseleave={() => {
|
||||
popover.hide()
|
||||
popoverAnchor = null
|
||||
hoverTarget = null
|
||||
}}
|
||||
on:focus={() => {}}
|
||||
on:blur={() => {}}
|
||||
on:click={() => addBinding(binding)}
|
||||
>
|
||||
<span class="binding__label">
|
||||
{#if binding.display?.name}
|
||||
{binding.display.name}
|
||||
{:else if binding.fieldSchema?.name}
|
||||
{binding.fieldSchema?.name}
|
||||
{:else}
|
||||
{binding.readableBinding}
|
||||
{/if}
|
||||
</span>
|
||||
|
||||
{#if binding.display?.type || binding.fieldSchema?.type}
|
||||
<span class="binding__typeWrap">
|
||||
<span class="binding__type">
|
||||
{binding.display?.type || binding.fieldSchema?.type}
|
||||
</span>
|
||||
</span>
|
||||
if (editorValue) {
|
||||
targetMode = selectedMode
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}}
|
||||
>
|
||||
<Tab title="Text">
|
||||
<div class="main-content" class:binding-panel={sidebar}>
|
||||
<div class="editor">
|
||||
<div class="overlay-wrap">
|
||||
{#if targetMode}
|
||||
<div class="mode-overlay">
|
||||
<div class="prompt-body">
|
||||
<Heading size="S">
|
||||
{`Switch to ${targetMode}?`}
|
||||
</Heading>
|
||||
<Body>This will discard anything in your binding</Body>
|
||||
<div class="switch-actions">
|
||||
<Button
|
||||
secondary
|
||||
size="S"
|
||||
on:click={() => {
|
||||
targetMode = null
|
||||
}}
|
||||
>
|
||||
No - keep text
|
||||
</Button>
|
||||
<Button cta size="S" on:click={switchMode}>
|
||||
Yes - discard text
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
<CodeEditor
|
||||
value={hbsValue}
|
||||
on:change={onChangeHBSValue}
|
||||
bind:getCaretPosition
|
||||
bind:insertAtPos
|
||||
completions={[
|
||||
hbAutocomplete([
|
||||
...bindingCompletions,
|
||||
...getHelperCompletions(editorMode),
|
||||
]),
|
||||
]}
|
||||
placeholder=""
|
||||
height="100%"
|
||||
/>
|
||||
</div>
|
||||
<div class="binding-footer">
|
||||
<div class="messaging">
|
||||
{#if !valid}
|
||||
<div class="syntax-error">
|
||||
Current Handlebars syntax is invalid, please check the
|
||||
guide
|
||||
<a href="https://handlebarsjs.com/guide/">here</a>
|
||||
for more details.
|
||||
</div>
|
||||
{:else}
|
||||
<Icon name="FlashOn" />
|
||||
<div class="messaging-wrap">
|
||||
<div>
|
||||
Add available bindings by typing {{ or use the
|
||||
menu on the right
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
{/if}
|
||||
{/each}
|
||||
|
||||
{#if selectedCategory === "Helpers" || search}
|
||||
{#if filteredHelpers?.length}
|
||||
<div class="heading">Helpers</div>
|
||||
<ul class="helpers">
|
||||
{#each filteredHelpers as helper}
|
||||
<li
|
||||
class="binding"
|
||||
on:click={() => addHelper(helper, usingJS)}
|
||||
on:mouseenter={e => {
|
||||
popoverAnchor = e.target
|
||||
if (!helper.displayText && helper.description) {
|
||||
return
|
||||
}
|
||||
hoverTarget = {
|
||||
title: helper.displayText,
|
||||
description: helper.description,
|
||||
example: getHelperExample(helper, usingJS),
|
||||
}
|
||||
popover.show()
|
||||
e.stopPropagation()
|
||||
}}
|
||||
on:mouseleave={() => {
|
||||
popover.hide()
|
||||
popoverAnchor = null
|
||||
hoverTarget = null
|
||||
}}
|
||||
on:focus={() => {}}
|
||||
on:blur={() => {}}
|
||||
>
|
||||
<span class="binding__label">{helper.displayText}</span>
|
||||
<span class="binding__typeWrap">
|
||||
<span class="binding__type">function</span>
|
||||
</span>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
{/if}
|
||||
{/if}
|
||||
{/if}
|
||||
</Layout>
|
||||
</svelte:fragment>
|
||||
<div class="main">
|
||||
<Tabs selected={mode} on:select={onChangeMode}>
|
||||
<Tab title="Handlebars">
|
||||
<div class="main-content">
|
||||
<TextArea
|
||||
bind:getCaretPosition
|
||||
value={hbsValue}
|
||||
on:change={onChangeHBSValue}
|
||||
placeholder="Add text, or click the objects on the left to add them to the textbox."
|
||||
/>
|
||||
{#if !valid}
|
||||
<p class="syntax-error">
|
||||
Current Handlebars syntax is invalid, please check the guide
|
||||
<a href="https://handlebarsjs.com/guide/">here</a>
|
||||
for more details.
|
||||
</p>
|
||||
{/if}
|
||||
{#if $admin.isDev && allowJS}
|
||||
<div class="convert">
|
||||
<Button secondary on:click={convert}>Convert to JS</Button>
|
||||
</div>
|
||||
<div class="actions">
|
||||
{#if $admin.isDev && allowJS}
|
||||
<ActionButton
|
||||
secondary
|
||||
on:click={() => {
|
||||
convert()
|
||||
targetMode = null
|
||||
}}
|
||||
>
|
||||
Convert To JS
|
||||
</ActionButton>
|
||||
{/if}
|
||||
<ActionButton
|
||||
secondary
|
||||
icon={sidebar ? "RailRightClose" : "RailRightOpen"}
|
||||
on:click={() => {
|
||||
sidebar = !sidebar
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</Tab>
|
||||
{#if allowJS}
|
||||
<Tab title="JavaScript">
|
||||
<div class="main-content">
|
||||
<Layout noPadding gap="XS">
|
||||
<CodeMirrorEditor
|
||||
bind:getCaretPosition
|
||||
height={200}
|
||||
value={decodeJSBinding(jsValue)}
|
||||
on:change={onChangeJSValue}
|
||||
hints={codeMirrorHints}
|
||||
/>
|
||||
<Body size="S">
|
||||
JavaScript expressions are executed as functions, so ensure that
|
||||
your expression returns a value.
|
||||
</Body>
|
||||
</Layout>
|
||||
|
||||
{#if sidebar}
|
||||
<div class="binding-picker">
|
||||
<BindingPicker
|
||||
{bindings}
|
||||
{allowHelpers}
|
||||
addHelper={onSelectHelper}
|
||||
addBinding={onSelectBinding}
|
||||
mode={editorMode}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</Tab>
|
||||
{/if}
|
||||
</Tabs>
|
||||
</div>
|
||||
</DrawerContent>
|
||||
{#if allowJS}
|
||||
<Tab title="JavaScript">
|
||||
<div class="main-content" class:binding-panel={sidebar}>
|
||||
<div class="editor">
|
||||
<div class="overlay-wrap">
|
||||
{#if targetMode}
|
||||
<div class="mode-overlay">
|
||||
<div class="prompt-body">
|
||||
<Heading size="S">
|
||||
{`Switch to ${targetMode}?`}
|
||||
</Heading>
|
||||
<Body>This will discard anything in your binding</Body>
|
||||
<div class="switch-actions">
|
||||
<Button
|
||||
secondary
|
||||
size="S"
|
||||
on:click={() => {
|
||||
targetMode = null
|
||||
}}
|
||||
>
|
||||
No - keep javascript
|
||||
</Button>
|
||||
<Button cta size="S" on:click={switchMode}>
|
||||
Yes - discard javascript
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<CodeEditor
|
||||
value={decodeJSBinding(jsValue)}
|
||||
on:change={onChangeJSValue}
|
||||
completions={[
|
||||
jsAutocomplete([
|
||||
...bindingCompletions,
|
||||
...getHelperCompletions(editorMode),
|
||||
]),
|
||||
]}
|
||||
mode={EditorModes.JS}
|
||||
bind:getCaretPosition
|
||||
bind:insertAtPos
|
||||
height="100%"
|
||||
/>
|
||||
</div>
|
||||
<div class="binding-footer">
|
||||
<div class="messaging">
|
||||
<Icon name="FlashOn" />
|
||||
<div class="messaging-wrap">
|
||||
<div>
|
||||
Add available bindings by typing $ or use the menu on
|
||||
the right
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="actions">
|
||||
<ActionButton
|
||||
secondary
|
||||
icon={sidebar ? "RailRightClose" : "RailRightOpen"}
|
||||
on:click={() => {
|
||||
sidebar = !sidebar
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if sidebar}
|
||||
<div class="binding-picker">
|
||||
<BindingPicker
|
||||
{bindings}
|
||||
{allowHelpers}
|
||||
addHelper={onSelectHelper}
|
||||
addBinding={onSelectBinding}
|
||||
mode={editorMode}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</Tab>
|
||||
{/if}
|
||||
<div class="drawer-actions">
|
||||
<Button
|
||||
secondary
|
||||
quiet
|
||||
on:click={() => {
|
||||
store.actions.settings.propertyFocus(null)
|
||||
drawerActions.hide()
|
||||
}}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
cta
|
||||
disabled={!valid}
|
||||
on:click={() => {
|
||||
bindingDrawerActions.save()
|
||||
}}
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
</div>
|
||||
</Tabs>
|
||||
</div>
|
||||
</DrawerContent>
|
||||
</span>
|
||||
|
||||
<style>
|
||||
ul.helpers li * {
|
||||
pointer-events: none;
|
||||
.binding-drawer :global(.container > .main) {
|
||||
overflow: hidden;
|
||||
height: 100%;
|
||||
padding: 0px;
|
||||
}
|
||||
ul.category-list li {
|
||||
|
||||
.binding-drawer :global(.container > .main > .main) {
|
||||
overflow: hidden;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.binding-drawer :global(.spectrum-Tabs-content) {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.binding-drawer :global(.spectrum-Tabs-content > div),
|
||||
.binding-drawer :global(.spectrum-Tabs-content > div > div),
|
||||
.binding-drawer :global(.spectrum-Tabs-content .main-content) {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.binding-drawer .main-content {
|
||||
grid-template-rows: unset;
|
||||
}
|
||||
|
||||
.messaging {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-m);
|
||||
align-items: center;
|
||||
}
|
||||
ul.category-list .category-name {
|
||||
font-weight: 600;
|
||||
text-transform: capitalize;
|
||||
}
|
||||
ul.category-list .category-chevron {
|
||||
min-width: 0;
|
||||
flex: 1;
|
||||
text-align: right;
|
||||
}
|
||||
ul.category-list .category-chevron :global(div.icon),
|
||||
.cat-heading :global(div.icon) {
|
||||
display: inline-block;
|
||||
.messaging-wrap {
|
||||
overflow: hidden;
|
||||
}
|
||||
li.binding {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
li.binding .binding__typeWrap {
|
||||
flex: 1;
|
||||
text-align: right;
|
||||
text-transform: capitalize;
|
||||
.messaging-wrap > div {
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
}
|
||||
.main :global(textarea) {
|
||||
min-height: 202px !important;
|
||||
}
|
||||
.main {
|
||||
margin: calc(-1 * var(--spacing-xl));
|
||||
}
|
||||
|
||||
.main-content {
|
||||
padding: var(--spacing-s) var(--spacing-xl);
|
||||
}
|
||||
|
||||
.heading,
|
||||
.cat-heading {
|
||||
font-size: var(--font-size-s);
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
color: var(--spectrum-global-color-gray-600);
|
||||
}
|
||||
|
||||
.cat-heading {
|
||||
.main :global(.spectrum-Tabs div.drawer-actions) {
|
||||
display: flex;
|
||||
gap: var(--spacing-m);
|
||||
align-items: center;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
ul {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
.main :global(.spectrum-Tabs-content),
|
||||
.main :global(.spectrum-Tabs-content .main-content) {
|
||||
margin-top: 0px;
|
||||
padding: 0px;
|
||||
}
|
||||
|
||||
li {
|
||||
font-size: var(--font-size-s);
|
||||
padding: var(--spacing-m);
|
||||
border-radius: 4px;
|
||||
background-color: var(--spectrum-global-color-gray-200);
|
||||
transition: background-color 130ms ease-in-out, color 130ms ease-in-out,
|
||||
border-color 130ms ease-in-out;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
li:not(:last-of-type) {
|
||||
margin-bottom: var(--spacing-s);
|
||||
}
|
||||
li :global(*) {
|
||||
transition: color 130ms ease-in-out;
|
||||
}
|
||||
li:hover {
|
||||
color: var(--spectrum-global-color-gray-900);
|
||||
background-color: var(--spectrum-global-color-gray-50);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.binding__label {
|
||||
font-weight: 600;
|
||||
text-transform: capitalize;
|
||||
}
|
||||
|
||||
.binding__type {
|
||||
font-family: var(--font-mono);
|
||||
background-color: var(--spectrum-global-color-gray-200);
|
||||
border-radius: var(--border-radius-s);
|
||||
padding: 2px 4px;
|
||||
margin-left: 2px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.helper {
|
||||
.main :global(.spectrum-Tabs) {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-start;
|
||||
align-items: flex-start;
|
||||
gap: var(--spacing-xs);
|
||||
}
|
||||
.helper__name {
|
||||
font-weight: bold;
|
||||
}
|
||||
.helper__description,
|
||||
.helper__description :global(*) {
|
||||
color: var(--spectrum-global-color-gray-700);
|
||||
}
|
||||
.helper__example {
|
||||
white-space: normal;
|
||||
margin: 0.5rem 0 0 0;
|
||||
font-weight: 700;
|
||||
}
|
||||
.helper__description :global(p) {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.syntax-error {
|
||||
padding-top: var(--spacing-m);
|
||||
color: var(--red);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
@ -511,7 +441,66 @@
|
|||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.convert {
|
||||
padding-top: var(--spacing-m);
|
||||
.binding-footer {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
.main-content {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
grid-template-rows: 380px;
|
||||
}
|
||||
.main-content.binding-panel {
|
||||
grid-template-columns: 1fr 320px;
|
||||
}
|
||||
.binding-picker {
|
||||
border-left: 2px solid var(--border-light);
|
||||
border-left: var(--border-light);
|
||||
overflow: scroll;
|
||||
height: 100%;
|
||||
}
|
||||
.editor {
|
||||
padding: var(--spacing-xl);
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-xl);
|
||||
}
|
||||
.overlay-wrap {
|
||||
position: relative;
|
||||
flex: 1;
|
||||
}
|
||||
.mode-overlay {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: 2;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background-color: var(
|
||||
--spectrum-textfield-m-background-color,
|
||||
var(--spectrum-global-color-gray-50)
|
||||
);
|
||||
border-radius: var(--border-radius-s);
|
||||
}
|
||||
.prompt-body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: var(--spacing-l);
|
||||
}
|
||||
.prompt-body .switch-actions {
|
||||
display: flex;
|
||||
gap: var(--spacing-l);
|
||||
}
|
||||
|
||||
.binding-drawer :global(.code-editor),
|
||||
.binding-drawer :global(.code-editor > div) {
|
||||
height: 100%;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -0,0 +1,393 @@
|
|||
<script>
|
||||
import groupBy from "lodash/fp/groupBy"
|
||||
import { convertToJS } from "@budibase/string-templates"
|
||||
import { Input, Layout, ActionButton, Icon, Popover } from "@budibase/bbui"
|
||||
import { handlebarsCompletions } from "constants/completions"
|
||||
|
||||
export let addHelper
|
||||
export let addBinding
|
||||
export let bindings
|
||||
export let mode
|
||||
export let allowHelpers
|
||||
|
||||
let search = ""
|
||||
let popover
|
||||
let popoverAnchor
|
||||
let hoverTarget
|
||||
let helpers = handlebarsCompletions()
|
||||
|
||||
let selectedCategory
|
||||
|
||||
$: searchRgx = new RegExp(search, "ig")
|
||||
|
||||
// Icons
|
||||
$: bindingIcons = bindings?.reduce((acc, ele) => {
|
||||
if (ele.icon) {
|
||||
acc[ele.category] = acc[ele.category] || ele.icon
|
||||
}
|
||||
return acc
|
||||
}, {})
|
||||
$: categoryIcons = { ...bindingIcons, Helpers: "MagicWand" }
|
||||
|
||||
$: categories = Object.entries(groupBy("category", bindings))
|
||||
$: categoryNames = getCategoryNames(categories)
|
||||
|
||||
$: filteredCategories = categories
|
||||
.map(([name, categoryBindings]) => ({
|
||||
name,
|
||||
bindings: categoryBindings?.filter(binding => {
|
||||
return binding.readableBinding.match(searchRgx)
|
||||
}),
|
||||
}))
|
||||
.filter(category => {
|
||||
return (
|
||||
category.bindings?.length > 0 &&
|
||||
(!selectedCategory ? true : selectedCategory === category.name)
|
||||
)
|
||||
})
|
||||
$: filteredHelpers = helpers?.filter(helper => {
|
||||
return helper.label.match(searchRgx) || helper.description.match(searchRgx)
|
||||
})
|
||||
|
||||
const getHelperExample = (helper, js) => {
|
||||
let example = helper.example || ""
|
||||
if (js) {
|
||||
example = convertToJS(example).split("\n")[0].split("= ")[1]
|
||||
if (example === "null;") {
|
||||
example = ""
|
||||
}
|
||||
}
|
||||
return example || ""
|
||||
}
|
||||
|
||||
const getCategoryNames = categories => {
|
||||
let names = [...categories.map(cat => cat[0])]
|
||||
if (allowHelpers) {
|
||||
names.push("Helpers")
|
||||
}
|
||||
return names
|
||||
}
|
||||
</script>
|
||||
|
||||
<span class="detailPopover">
|
||||
<Popover
|
||||
align="left-outside"
|
||||
bind:this={popover}
|
||||
anchor={popoverAnchor}
|
||||
maxWidth={300}
|
||||
dismissible={false}
|
||||
>
|
||||
<Layout gap="S">
|
||||
<div class="helper">
|
||||
{#if hoverTarget.title}
|
||||
<div class="helper__name">{hoverTarget.title}</div>
|
||||
{/if}
|
||||
{#if hoverTarget.description}
|
||||
<div class="helper__description">
|
||||
{@html hoverTarget.description}
|
||||
</div>
|
||||
{/if}
|
||||
{#if hoverTarget.example}
|
||||
<pre class="helper__example">{hoverTarget.example}</pre>
|
||||
{/if}
|
||||
</div>
|
||||
</Layout>
|
||||
</Popover>
|
||||
</span>
|
||||
|
||||
<Layout noPadding gap="S">
|
||||
{#if selectedCategory}
|
||||
<div class="sub-section-back">
|
||||
<ActionButton
|
||||
secondary
|
||||
icon={"ArrowLeft"}
|
||||
on:click={() => {
|
||||
selectedCategory = null
|
||||
}}
|
||||
>
|
||||
Back
|
||||
</ActionButton>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if !selectedCategory}
|
||||
<div class="search">
|
||||
<span class="search-input">
|
||||
<Input
|
||||
placeholder={"Search for bindings"}
|
||||
autocomplete="off"
|
||||
bind:value={search}
|
||||
/>
|
||||
</span>
|
||||
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<span
|
||||
class="search-input-icon"
|
||||
on:click={() => {
|
||||
if (!search) {
|
||||
return
|
||||
}
|
||||
search = null
|
||||
}}
|
||||
class:searching={search}
|
||||
>
|
||||
<Icon name={search ? "Close" : "Search"} />
|
||||
</span>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if !selectedCategory && !search}
|
||||
<ul class="category-list">
|
||||
{#each categoryNames as categoryName}
|
||||
<li
|
||||
on:click={() => {
|
||||
selectedCategory = categoryName
|
||||
}}
|
||||
>
|
||||
<Icon name={categoryIcons[categoryName]} />
|
||||
<span class="category-name">{categoryName} </span>
|
||||
<span class="category-chevron"><Icon name="ChevronRight" /></span>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
{/if}
|
||||
|
||||
{#if selectedCategory || search}
|
||||
{#each filteredCategories as category}
|
||||
{#if category.bindings?.length}
|
||||
<div class="sub-section">
|
||||
<div class="cat-heading">
|
||||
<Icon name={categoryIcons[category.name]} />{category.name}
|
||||
</div>
|
||||
<ul>
|
||||
{#each category.bindings as binding}
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<li
|
||||
class="binding"
|
||||
on:mouseenter={e => {
|
||||
popoverAnchor = e.target
|
||||
if (!binding.description) {
|
||||
return
|
||||
}
|
||||
hoverTarget = {
|
||||
title: binding.display?.name || binding.fieldSchema?.name,
|
||||
description: binding.description,
|
||||
}
|
||||
popover.show()
|
||||
e.stopPropagation()
|
||||
}}
|
||||
on:mouseleave={() => {
|
||||
popover.hide()
|
||||
popoverAnchor = null
|
||||
hoverTarget = null
|
||||
}}
|
||||
on:focus={() => {}}
|
||||
on:blur={() => {}}
|
||||
on:click={() => addBinding(binding)}
|
||||
>
|
||||
<span class="binding__label">
|
||||
{#if binding.display?.name}
|
||||
{binding.display.name}
|
||||
{:else if binding.fieldSchema?.name}
|
||||
{binding.fieldSchema?.name}
|
||||
{:else}
|
||||
{binding.readableBinding}
|
||||
{/if}
|
||||
</span>
|
||||
|
||||
{#if binding.display?.type || binding.fieldSchema?.type}
|
||||
<span class="binding__typeWrap">
|
||||
<span class="binding__type">
|
||||
{binding.display?.type || binding.fieldSchema?.type}
|
||||
</span>
|
||||
</span>
|
||||
{/if}
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
</div>
|
||||
{/if}
|
||||
{/each}
|
||||
|
||||
{#if selectedCategory === "Helpers" || search}
|
||||
{#if filteredHelpers?.length}
|
||||
<div class="sub-section">
|
||||
<div class="cat-heading">Helpers</div>
|
||||
<ul class="helpers">
|
||||
{#each filteredHelpers as helper}
|
||||
<li
|
||||
class="binding"
|
||||
on:click={() => addHelper(helper, mode.name == "javascript")}
|
||||
on:mouseenter={e => {
|
||||
popoverAnchor = e.target
|
||||
if (!helper.displayText && helper.description) {
|
||||
return
|
||||
}
|
||||
hoverTarget = {
|
||||
title: helper.displayText,
|
||||
description: helper.description,
|
||||
example: getHelperExample(
|
||||
helper,
|
||||
mode.name == "javascript"
|
||||
),
|
||||
}
|
||||
popover.show()
|
||||
e.stopPropagation()
|
||||
}}
|
||||
on:mouseleave={() => {
|
||||
popover.hide()
|
||||
popoverAnchor = null
|
||||
hoverTarget = null
|
||||
}}
|
||||
on:focus={() => {}}
|
||||
on:blur={() => {}}
|
||||
>
|
||||
<span class="binding__label">{helper.displayText}</span>
|
||||
<span class="binding__typeWrap">
|
||||
<span class="binding__type">function</span>
|
||||
</span>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
{/if}
|
||||
</Layout>
|
||||
|
||||
<style>
|
||||
.search :global(input) {
|
||||
border: none;
|
||||
border-radius: 0px;
|
||||
background: none;
|
||||
padding: 0px;
|
||||
}
|
||||
|
||||
.search {
|
||||
padding: var(--spacing-m) var(--spacing-l);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
border-top: 0px;
|
||||
border-bottom: var(--border-light);
|
||||
border-left: 2px solid transparent;
|
||||
border-right: 2px solid transparent;
|
||||
margin-right: 1px;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
background-color: var(--background);
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.search-input {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.search-input-icon.searching {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
ul.category-list {
|
||||
padding: 0px var(--spacing-l);
|
||||
padding-bottom: var(--spacing-l);
|
||||
}
|
||||
.sub-section {
|
||||
padding: var(--spacing-l);
|
||||
padding-top: 0px;
|
||||
}
|
||||
.sub-section-back {
|
||||
padding: var(--spacing-l);
|
||||
padding-top: var(--spacing-xl);
|
||||
padding-bottom: 0px;
|
||||
}
|
||||
.cat-heading {
|
||||
margin-bottom: var(--spacing-l);
|
||||
}
|
||||
ul.helpers li * {
|
||||
pointer-events: none;
|
||||
}
|
||||
ul.category-list li {
|
||||
display: flex;
|
||||
gap: var(--spacing-m);
|
||||
align-items: center;
|
||||
}
|
||||
ul.category-list .category-name {
|
||||
font-weight: 600;
|
||||
text-transform: capitalize;
|
||||
}
|
||||
ul.category-list .category-chevron {
|
||||
flex: 1;
|
||||
text-align: right;
|
||||
}
|
||||
ul.category-list .category-chevron :global(div.icon),
|
||||
.cat-heading :global(div.icon) {
|
||||
display: inline-block;
|
||||
}
|
||||
li.binding {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
li.binding .binding__typeWrap {
|
||||
flex: 1;
|
||||
text-align: right;
|
||||
text-transform: capitalize;
|
||||
}
|
||||
|
||||
:global(.drawer-actions) {
|
||||
display: flex;
|
||||
gap: var(--spacing-m);
|
||||
}
|
||||
|
||||
.cat-heading {
|
||||
font-size: var(--font-size-s);
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
color: var(--spectrum-global-color-gray-600);
|
||||
}
|
||||
|
||||
.cat-heading {
|
||||
display: flex;
|
||||
gap: var(--spacing-m);
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
ul {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
li {
|
||||
font-size: var(--font-size-s);
|
||||
padding: var(--spacing-m);
|
||||
border-radius: 4px;
|
||||
background-color: var(--spectrum-global-color-gray-200);
|
||||
transition: background-color 130ms ease-in-out, color 130ms ease-in-out,
|
||||
border-color 130ms ease-in-out;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
li:not(:last-of-type) {
|
||||
margin-bottom: var(--spacing-s);
|
||||
}
|
||||
li :global(*) {
|
||||
transition: color 130ms ease-in-out;
|
||||
}
|
||||
li:hover {
|
||||
color: var(--spectrum-global-color-gray-900);
|
||||
background-color: var(--spectrum-global-color-gray-50);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.binding__label {
|
||||
font-weight: 600;
|
||||
text-transform: capitalize;
|
||||
}
|
||||
|
||||
.binding__type {
|
||||
font-family: var(--font-mono);
|
||||
background-color: var(--spectrum-global-color-gray-200);
|
||||
border-radius: var(--border-radius-s);
|
||||
padding: 2px 4px;
|
||||
margin-left: 2px;
|
||||
font-weight: 600;
|
||||
}
|
||||
</style>
|
|
@ -5,7 +5,7 @@
|
|||
runtimeToReadableBinding,
|
||||
} from "builderStore/dataBinding"
|
||||
import ClientBindingPanel from "components/common/bindings/ClientBindingPanel.svelte"
|
||||
import { createEventDispatcher } from "svelte"
|
||||
import { createEventDispatcher, setContext } from "svelte"
|
||||
import { isJSBinding } from "@budibase/string-templates"
|
||||
|
||||
export let panel = ClientBindingPanel
|
||||
|
@ -34,6 +34,10 @@
|
|||
bindingDrawer.hide()
|
||||
}
|
||||
|
||||
setContext("binding-drawer-actions", {
|
||||
save: handleClose,
|
||||
})
|
||||
|
||||
const onChange = (value, optionPicked) => {
|
||||
// Add HBS braces if picking binding
|
||||
if (optionPicked && !options?.includes(value)) {
|
||||
|
@ -63,7 +67,6 @@
|
|||
on:pick={e => onChange(e.detail, true)}
|
||||
on:blur={() => dispatch("blur")}
|
||||
{placeholder}
|
||||
options={allOptions}
|
||||
{error}
|
||||
/>
|
||||
{#if !disabled}
|
||||
|
@ -77,6 +80,7 @@
|
|||
<svelte:fragment slot="description">
|
||||
Add the objects on the left to enrich your text.
|
||||
</svelte:fragment>
|
||||
|
||||
<Button cta slot="buttons" on:click={handleClose} disabled={!valid}>
|
||||
Save
|
||||
</Button>
|
||||
|
|
|
@ -4,8 +4,11 @@
|
|||
readableToRuntimeBinding,
|
||||
runtimeToReadableBinding,
|
||||
} from "builderStore/dataBinding"
|
||||
|
||||
import { store } from "builderStore"
|
||||
|
||||
import ClientBindingPanel from "components/common/bindings/ClientBindingPanel.svelte"
|
||||
import { createEventDispatcher } from "svelte"
|
||||
import { createEventDispatcher, setContext } from "svelte"
|
||||
import { isJSBinding } from "@budibase/string-templates"
|
||||
|
||||
export let panel = ClientBindingPanel
|
||||
|
@ -20,6 +23,7 @@
|
|||
export let allowHelpers = true
|
||||
export let updateOnChange = true
|
||||
export let drawerLeft
|
||||
export let key
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
let bindingDrawer
|
||||
|
@ -32,10 +36,15 @@
|
|||
|
||||
const saveBinding = () => {
|
||||
onChange(tempValue)
|
||||
store.actions.settings.propertyFocus(null)
|
||||
onBlur()
|
||||
bindingDrawer.hide()
|
||||
}
|
||||
|
||||
setContext("binding-drawer-actions", {
|
||||
save: saveBinding,
|
||||
})
|
||||
|
||||
const onChange = value => {
|
||||
currentVal = readableToRuntimeBinding(bindings, value)
|
||||
dispatch("change", currentVal)
|
||||
|
@ -58,12 +67,24 @@
|
|||
{updateOnChange}
|
||||
/>
|
||||
{#if !disabled}
|
||||
<div class="icon" on:click={bindingDrawer.show}>
|
||||
<div
|
||||
class="icon"
|
||||
on:click={() => {
|
||||
store.actions.settings.propertyFocus(key)
|
||||
bindingDrawer.show()
|
||||
}}
|
||||
>
|
||||
<Icon size="S" name="FlashOn" />
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
<Drawer {fillWidth} bind:this={bindingDrawer} {title} left={drawerLeft}>
|
||||
<Drawer
|
||||
{fillWidth}
|
||||
bind:this={bindingDrawer}
|
||||
{title}
|
||||
left={drawerLeft}
|
||||
headless
|
||||
>
|
||||
<svelte:fragment slot="description">
|
||||
Add the objects on the left to enrich your text.
|
||||
</svelte:fragment>
|
||||
|
|
|
@ -113,109 +113,113 @@
|
|||
})
|
||||
</script>
|
||||
|
||||
<div class="action-top-nav">
|
||||
<div class="action-buttons">
|
||||
<div class="version">
|
||||
<VersionModal />
|
||||
</div>
|
||||
<RevertModal />
|
||||
|
||||
{#if isPublished}
|
||||
<div class="publish-popover">
|
||||
<div bind:this={publishPopoverAnchor}>
|
||||
<ActionButton
|
||||
quiet
|
||||
icon="Globe"
|
||||
size="M"
|
||||
tooltip="Your published app"
|
||||
on:click={publishPopover.show()}
|
||||
/>
|
||||
</div>
|
||||
<Popover
|
||||
bind:this={publishPopover}
|
||||
align="right"
|
||||
disabled={!isPublished}
|
||||
anchor={publishPopoverAnchor}
|
||||
offset={10}
|
||||
>
|
||||
<div class="popover-content">
|
||||
<Layout noPadding gap="M">
|
||||
<Heading size="XS">Your published app</Heading>
|
||||
<Body size="S">
|
||||
<span class="publish-popover-message">
|
||||
{processStringSync(
|
||||
"Last published {{ duration time 'millisecond' }} ago",
|
||||
{
|
||||
time:
|
||||
new Date().getTime() -
|
||||
new Date(latestDeployments[0].updatedAt).getTime(),
|
||||
}
|
||||
)}
|
||||
</span>
|
||||
</Body>
|
||||
<div class="buttons">
|
||||
<Button
|
||||
warning={true}
|
||||
icon="GlobeStrike"
|
||||
disabled={!isPublished}
|
||||
on:click={unpublishApp}
|
||||
>
|
||||
Unpublish
|
||||
</Button>
|
||||
<Button cta on:click={viewApp}>View app</Button>
|
||||
</div>
|
||||
</Layout>
|
||||
</div>
|
||||
</Popover>
|
||||
{#if $store.hasLock}
|
||||
<div class="action-top-nav">
|
||||
<div class="action-buttons">
|
||||
<div class="version">
|
||||
<VersionModal />
|
||||
</div>
|
||||
{/if}
|
||||
<RevertModal />
|
||||
|
||||
{#if !isPublished}
|
||||
<ActionButton
|
||||
quiet
|
||||
icon="GlobeStrike"
|
||||
size="M"
|
||||
tooltip="Your app has not been published yet"
|
||||
disabled
|
||||
/>
|
||||
{/if}
|
||||
{#if isPublished}
|
||||
<div class="publish-popover">
|
||||
<div bind:this={publishPopoverAnchor}>
|
||||
<ActionButton
|
||||
quiet
|
||||
icon="Globe"
|
||||
size="M"
|
||||
tooltip="Your published app"
|
||||
on:click={publishPopover.show()}
|
||||
/>
|
||||
</div>
|
||||
<Popover
|
||||
bind:this={publishPopover}
|
||||
align="right"
|
||||
disabled={!isPublished}
|
||||
anchor={publishPopoverAnchor}
|
||||
offset={10}
|
||||
>
|
||||
<div class="popover-content">
|
||||
<Layout noPadding gap="M">
|
||||
<Heading size="XS">Your published app</Heading>
|
||||
<Body size="S">
|
||||
<span class="publish-popover-message">
|
||||
{processStringSync(
|
||||
"Last published {{ duration time 'millisecond' }} ago",
|
||||
{
|
||||
time:
|
||||
new Date().getTime() -
|
||||
new Date(latestDeployments[0].updatedAt).getTime(),
|
||||
}
|
||||
)}
|
||||
</span>
|
||||
</Body>
|
||||
<div class="buttons">
|
||||
<Button
|
||||
warning={true}
|
||||
icon="GlobeStrike"
|
||||
disabled={!isPublished}
|
||||
on:click={unpublishApp}
|
||||
>
|
||||
Unpublish
|
||||
</Button>
|
||||
<Button cta on:click={viewApp}>View app</Button>
|
||||
</div>
|
||||
</Layout>
|
||||
</div>
|
||||
</Popover>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<TourWrap
|
||||
tourStepKey={$store.onboarding
|
||||
? TOUR_STEP_KEYS.BUILDER_USER_MANAGEMENT
|
||||
: TOUR_STEP_KEYS.FEATURE_USER_MANAGEMENT}
|
||||
>
|
||||
<span id="builder-app-users-button">
|
||||
{#if !isPublished}
|
||||
<ActionButton
|
||||
quiet
|
||||
icon="UserGroup"
|
||||
icon="GlobeStrike"
|
||||
size="M"
|
||||
on:click={() => {
|
||||
store.update(state => {
|
||||
state.builderSidePanel = true
|
||||
return state
|
||||
})
|
||||
}}
|
||||
>
|
||||
Users
|
||||
</ActionButton>
|
||||
</span>
|
||||
</TourWrap>
|
||||
</div>
|
||||
</div>
|
||||
tooltip="Your app has not been published yet"
|
||||
disabled
|
||||
/>
|
||||
{/if}
|
||||
|
||||
<ConfirmDialog
|
||||
bind:this={unpublishModal}
|
||||
title="Confirm unpublish"
|
||||
okText="Unpublish app"
|
||||
onOk={confirmUnpublishApp}
|
||||
>
|
||||
Are you sure you want to unpublish the app <b>{selectedApp?.name}</b>?
|
||||
</ConfirmDialog>
|
||||
<TourWrap
|
||||
tourStepKey={$store.onboarding
|
||||
? TOUR_STEP_KEYS.BUILDER_USER_MANAGEMENT
|
||||
: TOUR_STEP_KEYS.FEATURE_USER_MANAGEMENT}
|
||||
>
|
||||
<span id="builder-app-users-button">
|
||||
<ActionButton
|
||||
quiet
|
||||
icon="UserGroup"
|
||||
size="M"
|
||||
on:click={() => {
|
||||
store.update(state => {
|
||||
state.builderSidePanel = true
|
||||
return state
|
||||
})
|
||||
}}
|
||||
>
|
||||
Users
|
||||
</ActionButton>
|
||||
</span>
|
||||
</TourWrap>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ConfirmDialog
|
||||
bind:this={unpublishModal}
|
||||
title="Confirm unpublish"
|
||||
okText="Unpublish app"
|
||||
onOk={confirmUnpublishApp}
|
||||
>
|
||||
Are you sure you want to unpublish the app <b>{selectedApp?.name}</b>?
|
||||
</ConfirmDialog>
|
||||
{/if}
|
||||
|
||||
<div class="buttons">
|
||||
<Button on:click={previewApp} secondary>Preview</Button>
|
||||
<DeployModal onOk={completePublish} />
|
||||
{#if $store.hasLock}
|
||||
<DeployModal onOk={completePublish} />
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
|
|
|
@ -9,6 +9,8 @@
|
|||
import { store } from "builderStore"
|
||||
import { API } from "api"
|
||||
|
||||
export let disabled = false
|
||||
|
||||
let revertModal
|
||||
let appName
|
||||
|
||||
|
@ -34,6 +36,7 @@
|
|||
size="M"
|
||||
tooltip="Revert changes"
|
||||
on:click={revertModal.show}
|
||||
{disabled}
|
||||
/>
|
||||
|
||||
<Modal bind:this={revertModal}>
|
||||
|
|
|
@ -58,6 +58,7 @@
|
|||
justify-content: flex-start;
|
||||
align-items: stretch;
|
||||
transition: width 130ms ease-out;
|
||||
overflow: hidden;
|
||||
}
|
||||
.panel.borderLeft {
|
||||
border-left: var(--border-light);
|
||||
|
|
|
@ -17,14 +17,14 @@ import URLSelect from "./controls/URLSelect.svelte"
|
|||
import OptionsEditor from "./controls/OptionsEditor/OptionsEditor.svelte"
|
||||
import FormFieldSelect from "./controls/FormFieldSelect.svelte"
|
||||
import ValidationEditor from "./controls/ValidationEditor/ValidationEditor.svelte"
|
||||
import DrawerBindableCombobox from "components/common/bindings/DrawerBindableCombobox.svelte"
|
||||
import DrawerBindableInput from "components/common/bindings/DrawerBindableInput.svelte"
|
||||
import ColumnEditor from "./controls/ColumnEditor/ColumnEditor.svelte"
|
||||
import BasicColumnEditor from "./controls/ColumnEditor/BasicColumnEditor.svelte"
|
||||
import BarButtonList from "./controls/BarButtonList.svelte"
|
||||
import FieldConfiguration from "./controls/FieldConfiguration/FieldConfiguration.svelte"
|
||||
|
||||
const componentMap = {
|
||||
text: DrawerBindableCombobox,
|
||||
text: DrawerBindableInput,
|
||||
select: Select,
|
||||
radio: RadioGroup,
|
||||
dataSource: DataSourceSelect,
|
||||
|
|
|
@ -126,8 +126,7 @@
|
|||
}
|
||||
|
||||
const getAllBindings = (bindings, eventContextBindings, actions) => {
|
||||
let allBindings = eventContextBindings.concat(bindings)
|
||||
|
||||
let allBindings = []
|
||||
if (!actions) {
|
||||
return []
|
||||
}
|
||||
|
@ -145,14 +144,35 @@
|
|||
.forEach(action => {
|
||||
// Check we have a binding for this action, and generate one if not
|
||||
const stateBinding = makeStateBinding(action.parameters.key)
|
||||
const hasKey = allBindings.some(binding => {
|
||||
const hasKey = bindings.some(binding => {
|
||||
return binding.runtimeBinding === stateBinding.runtimeBinding
|
||||
})
|
||||
if (!hasKey) {
|
||||
allBindings.push(stateBinding)
|
||||
bindings.push(stateBinding)
|
||||
}
|
||||
})
|
||||
// Get which indexes are asynchronous automations as we want to filter them out from the bindings
|
||||
const asynchronousAutomationIndexes = actions
|
||||
.map((action, index) => {
|
||||
if (
|
||||
action[EVENT_TYPE_KEY] === "Trigger Automation" &&
|
||||
!action.parameters?.synchronous
|
||||
) {
|
||||
return index
|
||||
}
|
||||
})
|
||||
.filter(index => index !== undefined)
|
||||
|
||||
// Based on the above, filter out the asynchronous automations from the bindings
|
||||
if (asynchronousAutomationIndexes) {
|
||||
allBindings = eventContextBindings
|
||||
.filter((binding, index) => {
|
||||
return !asynchronousAutomationIndexes.includes(index)
|
||||
})
|
||||
.concat(bindings)
|
||||
} else {
|
||||
allBindings = eventContextBindings.concat(bindings)
|
||||
}
|
||||
return allBindings
|
||||
}
|
||||
</script>
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
<script>
|
||||
import { Select, Label, Input, Checkbox } from "@budibase/bbui"
|
||||
import { Select, Label, Input, Checkbox, Icon } from "@budibase/bbui"
|
||||
import { automationStore } from "builderStore"
|
||||
import SaveFields from "./SaveFields.svelte"
|
||||
import { TriggerStepID } from "constants/backend/automations"
|
||||
import { TriggerStepID, ActionStepID } from "constants/backend/automations"
|
||||
|
||||
export let parameters = {}
|
||||
export let bindings = []
|
||||
|
@ -16,6 +16,14 @@
|
|||
? AUTOMATION_STATUS.EXISTING
|
||||
: AUTOMATION_STATUS.NEW
|
||||
|
||||
$: {
|
||||
if (automationStatus === AUTOMATION_STATUS.NEW) {
|
||||
parameters.synchronous = false
|
||||
}
|
||||
parameters.synchronous = automations.find(
|
||||
automation => automation._id === parameters.automationId
|
||||
)?.synchronous
|
||||
}
|
||||
$: automations = $automationStore.automations
|
||||
.filter(a => a.definition.trigger?.stepId === TriggerStepID.APP)
|
||||
.map(automation => {
|
||||
|
@ -23,10 +31,15 @@
|
|||
automation.definition.trigger.inputs.fields || {}
|
||||
).map(([name, type]) => ({ name, type }))
|
||||
|
||||
let hasCollectBlock = automation.definition.steps.some(
|
||||
step => step.stepId === ActionStepID.COLLECT
|
||||
)
|
||||
|
||||
return {
|
||||
name: automation.name,
|
||||
_id: automation._id,
|
||||
schema,
|
||||
synchronous: hasCollectBlock,
|
||||
}
|
||||
})
|
||||
$: hasAutomations = automations && automations.length > 0
|
||||
|
@ -35,6 +48,8 @@
|
|||
)
|
||||
$: selectedSchema = selectedAutomation?.schema
|
||||
|
||||
$: error = parameters.timeout > 120 ? "Timeout must be less than 120s" : null
|
||||
|
||||
const onFieldsChanged = e => {
|
||||
parameters.fields = Object.entries(e.detail || {}).reduce(
|
||||
(acc, [key, value]) => {
|
||||
|
@ -57,6 +72,14 @@
|
|||
parameters.fields = {}
|
||||
parameters.automationId = automations[0]?._id
|
||||
}
|
||||
|
||||
const onChange = value => {
|
||||
let automationId = value.detail
|
||||
parameters.synchronous = automations.find(
|
||||
automation => automation._id === automationId
|
||||
)?.synchronous
|
||||
parameters.automationId = automationId
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="root">
|
||||
|
@ -85,6 +108,7 @@
|
|||
|
||||
{#if automationStatus === AUTOMATION_STATUS.EXISTING}
|
||||
<Select
|
||||
on:change={onChange}
|
||||
bind:value={parameters.automationId}
|
||||
placeholder="Choose automation"
|
||||
options={automations}
|
||||
|
@ -98,6 +122,29 @@
|
|||
/>
|
||||
{/if}
|
||||
|
||||
{#if parameters.synchronous}
|
||||
<Label small />
|
||||
|
||||
<div class="synchronous-info">
|
||||
<Icon name="Info" />
|
||||
<div>
|
||||
<i
|
||||
>This automation will run synchronously as it contains a Collect
|
||||
step</i
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<Label small />
|
||||
|
||||
<div class="timeout-width">
|
||||
<Input
|
||||
label="Timeout in seconds (120 max)"
|
||||
type="number"
|
||||
{error}
|
||||
bind:value={parameters.timeout}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
<Label small />
|
||||
<Checkbox
|
||||
text="Do not display default notification"
|
||||
|
@ -133,6 +180,9 @@
|
|||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
.timeout-width {
|
||||
width: 30%;
|
||||
}
|
||||
|
||||
.params {
|
||||
display: grid;
|
||||
|
@ -142,6 +192,11 @@
|
|||
align-items: center;
|
||||
}
|
||||
|
||||
.synchronous-info {
|
||||
display: flex;
|
||||
gap: var(--spacing-s);
|
||||
}
|
||||
|
||||
.fields {
|
||||
margin-top: var(--spacing-l);
|
||||
display: grid;
|
||||
|
|
|
@ -57,7 +57,13 @@
|
|||
{
|
||||
"name": "Trigger Automation",
|
||||
"type": "application",
|
||||
"component": "TriggerAutomation"
|
||||
"component": "TriggerAutomation",
|
||||
"context": [
|
||||
{
|
||||
"label": "Automation Result",
|
||||
"value": "result"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Update Field Value",
|
||||
|
|
|
@ -21,6 +21,7 @@
|
|||
export let componentBindings = []
|
||||
export let nested = false
|
||||
export let highlighted = false
|
||||
export let propertyFocus = false
|
||||
export let info = null
|
||||
|
||||
$: nullishValue = value == null || value === ""
|
||||
|
@ -72,6 +73,10 @@
|
|||
if (highlighted) {
|
||||
store.actions.settings.highlight(null)
|
||||
}
|
||||
// To fix focus 'affect' when property is target of a drawer other actions in the builder.
|
||||
if (propertyFocus) {
|
||||
store.actions.settings.propertyFocus(null)
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
|
@ -79,6 +84,7 @@
|
|||
class="property-control"
|
||||
class:wide={!label || labelHidden}
|
||||
class:highlighted={highlighted && nullishValue}
|
||||
class:property-focus={propertyFocus}
|
||||
>
|
||||
{#if label && !labelHidden}
|
||||
<div class="label">
|
||||
|
@ -125,6 +131,14 @@
|
|||
background: var(--spectrum-global-color-gray-300);
|
||||
border-color: var(--spectrum-global-color-static-red-600);
|
||||
}
|
||||
|
||||
.property-control.property-focus :global(input) {
|
||||
border-color: var(
|
||||
--spectrum-textfield-m-border-color-down,
|
||||
var(--spectrum-alias-border-color-mouse-focus)
|
||||
);
|
||||
}
|
||||
|
||||
.label {
|
||||
margin-top: 16px;
|
||||
transform: translateY(-50%);
|
||||
|
|
|
@ -18,6 +18,7 @@
|
|||
export let tab = true
|
||||
export let mode
|
||||
export let editorHeight = 500
|
||||
export let editorWidth = 640
|
||||
// export let parameters = []
|
||||
|
||||
let width
|
||||
|
@ -169,7 +170,9 @@
|
|||
{#if label}
|
||||
<Label small>{label}</Label>
|
||||
{/if}
|
||||
<div style={`--code-mirror-height: ${editorHeight}px`}>
|
||||
<div
|
||||
style={`--code-mirror-height: ${editorHeight}px; --code-mirror-width: ${editorWidth}px;`}
|
||||
>
|
||||
<textarea tabindex="0" bind:this={refs.editor} readonly {value} />
|
||||
</div>
|
||||
|
||||
|
@ -183,6 +186,7 @@
|
|||
}
|
||||
|
||||
div :global(.CodeMirror) {
|
||||
width: var(--code-mirror-width) !important;
|
||||
height: var(--code-mirror-height) !important;
|
||||
border-radius: var(--border-radius-s);
|
||||
font-family: var(--font-mono);
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<script>
|
||||
import { goto } from "@roxi/routify"
|
||||
import { goto, beforeUrlChange } from "@roxi/routify"
|
||||
import {
|
||||
Icon,
|
||||
Select,
|
||||
|
@ -12,6 +12,8 @@
|
|||
Heading,
|
||||
Tabs,
|
||||
Tab,
|
||||
Modal,
|
||||
ModalContent,
|
||||
} from "@budibase/bbui"
|
||||
import { notifications, Divider } from "@budibase/bbui"
|
||||
import ExtraQueryConfig from "./ExtraQueryConfig.svelte"
|
||||
|
@ -29,6 +31,12 @@
|
|||
|
||||
export let query
|
||||
|
||||
const resumeNavigation = () => {
|
||||
if (typeof navigateTo == "string") {
|
||||
$goto(typeof navigateTo == "string" ? `${navigateTo}` : navigateTo)
|
||||
}
|
||||
}
|
||||
|
||||
const transformerDocs = "https://docs.budibase.com/docs/transformers"
|
||||
|
||||
let fields = query?.schema ? schemaToFields(query.schema) : []
|
||||
|
@ -36,6 +44,31 @@
|
|||
let data = []
|
||||
let saveId
|
||||
let currentTab = "JSON"
|
||||
let saveModal
|
||||
let override = false
|
||||
let navigateTo = null
|
||||
|
||||
// seed the transformer
|
||||
if (query && !query.transformer) {
|
||||
query.transformer = "return data"
|
||||
}
|
||||
|
||||
// initialise a new empty schema
|
||||
if (query && !query.schema) {
|
||||
query.schema = {}
|
||||
}
|
||||
|
||||
let queryStr = JSON.stringify(query)
|
||||
|
||||
$beforeUrlChange(event => {
|
||||
const updated = JSON.stringify(query)
|
||||
|
||||
if (updated !== queryStr && !override) {
|
||||
navigateTo = event.type == "pushstate" ? event.url : null
|
||||
saveModal.show()
|
||||
return false
|
||||
} else return true
|
||||
})
|
||||
|
||||
$: datasource = $datasources.list.find(ds => ds._id === query.datasourceId)
|
||||
$: query.schema = fieldsToSchema(fields)
|
||||
|
@ -60,11 +93,6 @@
|
|||
}
|
||||
}
|
||||
|
||||
// seed the transformer
|
||||
if (query && !query.transformer) {
|
||||
query.transformer = "return data"
|
||||
}
|
||||
|
||||
function resetDependentFields() {
|
||||
if (query.fields.extra) {
|
||||
query.fields.extra = {}
|
||||
|
@ -101,22 +129,48 @@
|
|||
}
|
||||
}
|
||||
|
||||
// return the query.
|
||||
async function saveQuery() {
|
||||
try {
|
||||
const { _id } = await queries.save(query.datasourceId, query)
|
||||
saveId = _id
|
||||
notifications.success(`Query saved successfully`)
|
||||
const response = await queries.save(query.datasourceId, query)
|
||||
saveId = response._id
|
||||
|
||||
// Go to the correct URL if we just created a new query
|
||||
if (!query._rev) {
|
||||
$goto(`../../${_id}`)
|
||||
if (response?._rev) {
|
||||
queryStr = JSON.stringify(query)
|
||||
}
|
||||
|
||||
return response
|
||||
} catch (error) {
|
||||
notifications.error("Error saving query")
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<Modal
|
||||
bind:this={saveModal}
|
||||
on:hide={() => {
|
||||
navigateTo = null
|
||||
}}
|
||||
>
|
||||
<ModalContent
|
||||
title="You have unsaved changes"
|
||||
confirmText="Save and Continue"
|
||||
cancelText="Discard Changes"
|
||||
size="L"
|
||||
onConfirm={async () => {
|
||||
await saveQuery()
|
||||
override = true
|
||||
resumeNavigation()
|
||||
}}
|
||||
onCancel={async () => {
|
||||
override = true
|
||||
resumeNavigation()
|
||||
}}
|
||||
>
|
||||
<Body>Leaving this section will mean losing and changes to your query</Body>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
|
||||
<div class="wrapper">
|
||||
<Layout gap="S" noPadding>
|
||||
<Heading size="M">Query {integrationInfo?.friendlyName}</Heading>
|
||||
|
@ -125,7 +179,13 @@
|
|||
<div class="config">
|
||||
<div class="config-field">
|
||||
<Label>Query Name</Label>
|
||||
<Input bind:value={query.name} />
|
||||
<Input
|
||||
value={query.name}
|
||||
on:input={e => {
|
||||
let newValue = e.target.value || ""
|
||||
query.name = newValue.trim()
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{#if queryConfig}
|
||||
<div class="config-field">
|
||||
|
@ -149,18 +209,20 @@
|
|||
/>
|
||||
{/if}
|
||||
{#key query.parameters}
|
||||
<BindingBuilder
|
||||
queryBindings={query.parameters}
|
||||
bindable={false}
|
||||
on:change={e => {
|
||||
query.parameters = e.detail.map(binding => {
|
||||
return {
|
||||
name: binding.name,
|
||||
default: binding.value,
|
||||
}
|
||||
})
|
||||
}}
|
||||
/>
|
||||
<div class="binding-wrap">
|
||||
<BindingBuilder
|
||||
queryBindings={query.parameters}
|
||||
bindable={false}
|
||||
on:change={e => {
|
||||
query.parameters = e.detail.map(binding => {
|
||||
return {
|
||||
name: binding.name,
|
||||
default: binding.value,
|
||||
}
|
||||
})
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{/key}
|
||||
{/if}
|
||||
</div>
|
||||
|
@ -203,7 +265,18 @@
|
|||
<div class="viewer-controls">
|
||||
<Heading size="S">Results</Heading>
|
||||
<ButtonGroup gap="XS">
|
||||
<Button cta disabled={queryInvalid} on:click={saveQuery}>
|
||||
<Button
|
||||
cta
|
||||
disabled={queryInvalid}
|
||||
on:click={async () => {
|
||||
await saveQuery()
|
||||
notifications.success(`Query saved successfully`)
|
||||
// Go to the correct URL if we just created a new query
|
||||
if (!query._rev) {
|
||||
$goto(`../../${query._id}`)
|
||||
}
|
||||
}}
|
||||
>
|
||||
Save Query
|
||||
</Button>
|
||||
<Button secondary on:click={previewQuery}>Run Query</Button>
|
||||
|
@ -274,4 +347,9 @@
|
|||
min-width: 150px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.binding-wrap :global(div.container) {
|
||||
padding-left: 0px;
|
||||
padding-right: 0px;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -0,0 +1,31 @@
|
|||
<script>
|
||||
import { Modal, ModalContent, Body } from "@budibase/bbui"
|
||||
|
||||
let modal
|
||||
|
||||
export let onConfirm
|
||||
|
||||
export function show() {
|
||||
modal.show()
|
||||
}
|
||||
|
||||
export function hide() {
|
||||
modal.hide()
|
||||
}
|
||||
</script>
|
||||
|
||||
<Modal bind:this={modal} on:hide={modal}>
|
||||
<ModalContent
|
||||
title="Your account is currently de-activated"
|
||||
size="S"
|
||||
showCancelButton={true}
|
||||
showCloseIcon={false}
|
||||
confirmText={"View plans"}
|
||||
{onConfirm}
|
||||
>
|
||||
<Body size="S"
|
||||
>Due to the free plan user limit being exceeded, your account has been
|
||||
de-activated. Upgrade your plan to re-activate your account.</Body
|
||||
>
|
||||
</ModalContent>
|
||||
</Modal>
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue