Merge branch 'master' into BUDI-7656/add-migration
This commit is contained in:
commit
ac638badd4
|
@ -10,4 +10,5 @@ packages/builder/.routify
|
|||
packages/sdk/sdk
|
||||
packages/account-portal/packages/server/build
|
||||
packages/account-portal/packages/ui/.routify
|
||||
packages/account-portal/packages/ui/build
|
||||
packages/account-portal/packages/ui/build
|
||||
**/*.ivm.bundle.js
|
|
@ -43,7 +43,18 @@
|
|||
"no-useless-escape": "off",
|
||||
"no-undef": "off",
|
||||
"no-prototype-builtins": "off",
|
||||
"local-rules/no-budibase-imports": "error"
|
||||
"local-rules/no-budibase-imports": "error",
|
||||
"local-rules/no-test-com": "error"
|
||||
}
|
||||
},
|
||||
{
|
||||
"files": [
|
||||
"packages/builder/**/*",
|
||||
"packages/client/**/*",
|
||||
"packages/frontend-core/**/*"
|
||||
],
|
||||
"rules": {
|
||||
"no-console": ["error", { "allow": ["warn", "error", "debug"] }]
|
||||
}
|
||||
}
|
||||
],
|
||||
|
|
|
@ -33,13 +33,13 @@ jobs:
|
|||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: ${{ env.IS_OSS_CONTRIBUTOR == 'false' }}
|
||||
token: ${{ secrets.PERSONAL_ACCESS_TOKEN || github.token }}
|
||||
|
||||
- name: Use Node.js 20.x
|
||||
uses: actions/setup-node@v3
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20.x
|
||||
cache: yarn
|
||||
|
@ -50,14 +50,14 @@ jobs:
|
|||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: ${{ env.IS_OSS_CONTRIBUTOR == 'false' }}
|
||||
token: ${{ secrets.PERSONAL_ACCESS_TOKEN || github.token }}
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Use Node.js 20.x
|
||||
uses: actions/setup-node@v3
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20.x
|
||||
cache: yarn
|
||||
|
@ -80,7 +80,7 @@ jobs:
|
|||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
|
@ -92,14 +92,14 @@ jobs:
|
|||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: ${{ env.IS_OSS_CONTRIBUTOR == 'false' }}
|
||||
token: ${{ secrets.PERSONAL_ACCESS_TOKEN || github.token }}
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Use Node.js 20.x
|
||||
uses: actions/setup-node@v3
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20.x
|
||||
cache: yarn
|
||||
|
@ -116,14 +116,14 @@ jobs:
|
|||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: ${{ env.IS_OSS_CONTRIBUTOR == 'false' }}
|
||||
token: ${{ secrets.PERSONAL_ACCESS_TOKEN || github.token }}
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Use Node.js 20.x
|
||||
uses: actions/setup-node@v3
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20.x
|
||||
cache: yarn
|
||||
|
@ -140,14 +140,14 @@ jobs:
|
|||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: ${{ env.IS_OSS_CONTRIBUTOR == 'false' }}
|
||||
token: ${{ secrets.PERSONAL_ACCESS_TOKEN || github.token }}
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Use Node.js 20.x
|
||||
uses: actions/setup-node@v3
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20.x
|
||||
cache: yarn
|
||||
|
@ -165,14 +165,14 @@ jobs:
|
|||
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'Budibase/budibase'
|
||||
steps:
|
||||
- name: Checkout repo and submodules
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: true
|
||||
token: ${{ secrets.PERSONAL_ACCESS_TOKEN || github.token }}
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Use Node.js 20.x
|
||||
uses: actions/setup-node@v3
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20.x
|
||||
cache: yarn
|
||||
|
@ -189,13 +189,13 @@ jobs:
|
|||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: ${{ env.IS_OSS_CONTRIBUTOR == 'false' }}
|
||||
token: ${{ secrets.PERSONAL_ACCESS_TOKEN || github.token }}
|
||||
|
||||
- name: Use Node.js 20.x
|
||||
uses: actions/setup-node@v3
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20.x
|
||||
cache: yarn
|
||||
|
@ -219,7 +219,7 @@ jobs:
|
|||
if: inputs.run_as_oss != true && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'Budibase/budibase')
|
||||
steps:
|
||||
- name: Checkout repo and submodules
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: true
|
||||
token: ${{ secrets.PERSONAL_ACCESS_TOKEN || github.token }}
|
||||
|
@ -249,7 +249,7 @@ jobs:
|
|||
|
||||
- name: Check submodule merged to base branch
|
||||
if: ${{ steps.get_pro_commits.outputs.base_commit != '' }}
|
||||
uses: actions/github-script@v4
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
script: |
|
||||
|
@ -269,7 +269,7 @@ jobs:
|
|||
if: inputs.run_as_oss != true && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'Budibase/budibase')
|
||||
steps:
|
||||
- name: Checkout repo and submodules
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: true
|
||||
token: ${{ secrets.PERSONAL_ACCESS_TOKEN || github.token }}
|
||||
|
@ -299,7 +299,7 @@ jobs:
|
|||
|
||||
- name: Check submodule merged to base branch
|
||||
if: ${{ steps.get_accountportal_commits.outputs.base_commit != '' }}
|
||||
uses: actions/github-script@v4
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
script: |
|
||||
|
|
|
@ -17,7 +17,7 @@ jobs:
|
|||
github.event.label.name == 'feature-branch'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
- uses: passeidireto/trigger-external-workflow-action@main
|
||||
env:
|
||||
PAYLOAD_BRANCH: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.BRANCH || github.head_ref }}
|
||||
|
|
|
@ -17,7 +17,7 @@ jobs:
|
|||
contains(github.event.pull_request.labels.*.name, 'feature-branch')
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
- uses: passeidireto/trigger-external-workflow-action@main
|
||||
env:
|
||||
PAYLOAD_BRANCH: ${{ github.head_ref }}
|
||||
|
|
|
@ -28,7 +28,7 @@ jobs:
|
|||
run: |
|
||||
echo "Ref is not master, you must run this job from master."
|
||||
exit 1
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: true
|
||||
token: ${{ secrets.PERSONAL_ACCESS_TOKEN }}
|
||||
|
@ -53,7 +53,7 @@ jobs:
|
|||
needs: [tag-release]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- uses: peter-evans/repository-dispatch@v2
|
||||
with:
|
||||
|
|
|
@ -11,4 +11,5 @@ packages/sdk/sdk
|
|||
packages/pro/coverage
|
||||
packages/account-portal/packages/ui/build
|
||||
packages/account-portal/packages/ui/.routify
|
||||
packages/account-portal/packages/server/build
|
||||
packages/account-portal/packages/server/build
|
||||
**/*.ivm.bundle.js
|
49
README.md
49
README.md
|
@ -11,7 +11,7 @@
|
|||
The low code platform you'll enjoy using
|
||||
</h3>
|
||||
<p align="center">
|
||||
Budibase is an open source low-code platform, and the easiest way to build internal apps that improve productivity.
|
||||
Budibase is an open-source low-code platform that saves engineers 100s of hours building forms, portals, and approval apps, securely.
|
||||
</p>
|
||||
|
||||
<h3 align="center">
|
||||
|
@ -20,7 +20,7 @@
|
|||
<br>
|
||||
|
||||
<p align="center">
|
||||
<img alt="Budibase design ui" src="https://res.cloudinary.com/daog6scxm/image/upload/v1633524049/ui/design-ui-wide-mobile_gdaveq.jpg">
|
||||
<img alt="Budibase design ui" src="https://res.cloudinary.com/daog6scxm/image/upload/v1680181644/ui/homepage-design-ui_sizp7b.png">
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
|
@ -57,7 +57,7 @@
|
|||
## ✨ Features
|
||||
|
||||
### Build and ship real software
|
||||
Unlike other platforms, with Budibase you build and ship single page applications. Budibase applications have performance baked in and can be designed responsively, providing your users with a great experience.
|
||||
Unlike other platforms, with Budibase you build and ship single page applications. Budibase applications have performance baked in and can be designed responsively, providing users with a great experience.
|
||||
<br /><br />
|
||||
|
||||
### Open source and extensible
|
||||
|
@ -65,40 +65,36 @@ Budibase is open-source - licensed as GPL v3. This should fill you with confiden
|
|||
<br /><br />
|
||||
|
||||
### Load data or start from scratch
|
||||
Budibase pulls in data from multiple sources, including MongoDB, CouchDB, PostgreSQL, MySQL, Airtable, S3, DynamoDB, or a REST API. And unlike other platforms, with Budibase you can start from scratch and create business apps with no datasources. [Request new datasources](https://github.com/Budibase/budibase/discussions?discussions_q=category%3AIdeas).
|
||||
Budibase pulls data from multiple sources, including MongoDB, CouchDB, PostgreSQL, MySQL, Airtable, S3, DynamoDB, or a REST API. And unlike other platforms, with Budibase you can start from scratch and create business apps with no data sources. [Request new datasources](https://github.com/Budibase/budibase/discussions?discussions_q=category%3AIdeas).
|
||||
|
||||
<p align="center">
|
||||
<img alt="Budibase data" src="https://res.cloudinary.com/daog6scxm/image/upload/v1636970242/Out%20of%20beta%20launch/data_n1tlhf.png">
|
||||
<img alt="Budibase data" src="https://res.cloudinary.com/daog6scxm/image/upload/v1680281798/ui/data_klbuna.png">
|
||||
</p>
|
||||
<br /><br />
|
||||
|
||||
### Design and build apps with powerful pre-made components
|
||||
|
||||
Budibase comes out of the box with beautifully designed, powerful components which you can use like building blocks to build your UI. We also expose a lot of your favourite CSS styling options so you can go that extra creative mile. [Request new component](https://github.com/Budibase/budibase/discussions?discussions_q=category%3AIdeas).
|
||||
Budibase comes out of the box with beautifully designed, powerful components which you can use like building blocks to build your UI. We also expose many of your favourite CSS styling options so you can go that extra creative mile. [Request new component](https://github.com/Budibase/budibase/discussions?discussions_q=category%3AIdeas).
|
||||
|
||||
<p align="center">
|
||||
<img alt="Budibase design" src="https://res.cloudinary.com/daog6scxm/image/upload/v1636970243/Out%20of%20beta%20launch/design-like-a-pro_qhlfeu.gif">
|
||||
<img alt="Budibase design" src="https://res.cloudinary.com/daog6scxm/image/upload/v1675437167/ui/form_2x_mbli8y.png">
|
||||
</p>
|
||||
<br /><br />
|
||||
|
||||
### Automate processes, integrate with other tools, and connect to webhooks
|
||||
Save time by automating manual processes and workflows. From connecting to webhooks, to automating emails, simply tell Budibase what to do and let it work for you. You can easily [create new automations for Budibase here](https://github.com/Budibase/automations) or [Request new automation](https://github.com/Budibase/budibase/discussions?discussions_q=category%3AIdeas).
|
||||
|
||||
<p align="center">
|
||||
<img alt="Budibase automations" src="https://res.cloudinary.com/daog6scxm/image/upload/v1636970486/Out%20of%20beta%20launch/automation_riro7u.png">
|
||||
</p>
|
||||
### Automate processes, integrate with other tools and connect to webhooks
|
||||
Save time by automating manual processes and workflows. From connecting to webhooks to automating emails, simply tell Budibase what to do and let it work for you. You can easily [create new automations for Budibase here](https://github.com/Budibase/automations) or [Request new automation](https://github.com/Budibase/budibase/discussions?discussions_q=category%3AIdeas).
|
||||
<br /><br />
|
||||
|
||||
### Integrate with your favorite tools
|
||||
Budibase integrates with a number of popular tools allowing you to build apps that perfectly fit your stack.
|
||||
|
||||
<p align="center">
|
||||
<img alt="Budibase integrations" src="https://res.cloudinary.com/daog6scxm/image/upload/v1636970242/Out%20of%20beta%20launch/integrations_kc7dqt.png">
|
||||
<img alt="Budibase integrations" src="https://res.cloudinary.com/daog6scxm/image/upload/v1680195228/ui/automate_fg9z07.png">
|
||||
</p>
|
||||
<br /><br />
|
||||
|
||||
### Admin paradise
|
||||
Budibase is made to scale. With Budibase, you can self-host on your own infrastructure and globally manage users, onboarding, SMTP, apps, groups, theming and more. You can also provide users/groups with an app portal and disseminate user-management to the group manager.
|
||||
### Deploy with confidence and security
|
||||
Budibase is made to scale. With Budibase, you can self-host on your own infrastructure and globally manage users, onboarding, SMTP, apps, groups, theming and more. You can also provide users/groups with an app portal and disseminate user management to the group manager.
|
||||
|
||||
- Checkout the promo video: https://youtu.be/xoljVpty_Kw
|
||||
|
||||
|
@ -119,17 +115,14 @@ As with anything that we build in Budibase, our new public API is simple to use,
|
|||
#### Docs
|
||||
You can learn more about the Budibase API at the following places:
|
||||
|
||||
- [General documentation](https://docs.budibase.com/docs/public-api) : Learn how to get your API key, how to use spec, and how to use with Postman
|
||||
- [General documentation](https://docs.budibase.com/docs/public-api): Learn how to get your API key, how to use spec, and how to use Postman
|
||||
- [Interactive API documentation](https://docs.budibase.com/reference/post_applications) : Learn how to interact with the API
|
||||
|
||||
#### Guides
|
||||
|
||||
- [Build an app with Budibase and Next.js](https://budibase.com/blog/building-a-crud-app-with-budibase-and-next.js/)
|
||||
<br /><br />
|
||||
|
||||
## 🏁 Get started
|
||||
|
||||
Deploy Budibase self-hosted in your existing infrastructure, using Docker, Kubernetes, and Digital Ocean.
|
||||
Or use Budibase Cloud if you don't need to self-host, and would like to get started quickly.
|
||||
Deploy Budibase using Docker, Kubernetes, and Digital Ocean on your existing infrastructure. Or use Budibase Cloud if you don't need to self-host and would like to get started quickly.
|
||||
|
||||
### [Get started with self-hosting Budibase](https://docs.budibase.com/docs/hosting-methods)
|
||||
|
||||
|
@ -162,7 +155,7 @@ If you have a question or would like to talk with other Budibase users and join
|
|||
|
||||
## ❗ Code of conduct
|
||||
|
||||
Budibase is dedicated to providing a welcoming, diverse, and harrassment-free experience for everyone. We expect everyone in the Budibase community to abide by our [**Code of Conduct**](https://github.com/Budibase/budibase/blob/HEAD/docs/CODE_OF_CONDUCT.md). Please read it.
|
||||
Budibase is dedicated to providing everyone a welcoming, diverse, and harassment-free experience. We expect everyone in the Budibase community to abide by our [**Code of Conduct**](https://github.com/Budibase/budibase/blob/HEAD/docs/CODE_OF_CONDUCT.md). Please read it.
|
||||
<br />
|
||||
|
||||
|
||||
|
@ -171,16 +164,16 @@ Budibase is dedicated to providing a welcoming, diverse, and harrassment-free ex
|
|||
|
||||
## 🙌 Contributing to Budibase
|
||||
|
||||
From opening a bug report to creating a pull request: every contribution is appreciated and welcomed. If you're planning to implement a new feature or change the API please create an issue first. This way we can ensure your work is not in vain.
|
||||
Environment setup instructions are available for [Debian](https://github.com/Budibase/budibase/tree/HEAD/docs/DEV-SETUP-DEBIAN.md) and [MacOSX](https://github.com/Budibase/budibase/tree/HEAD/docs/DEV-SETUP-MACOSX.md)
|
||||
From opening a bug report to creating a pull request: every contribution is appreciated and welcomed. If you're planning to implement a new feature or change the API, please create an issue first. This way, we can ensure your work is not in vain.
|
||||
Environment setup instructions are available [here](https://github.com/Budibase/budibase/tree/HEAD/docs/CONTRIBUTING.md).
|
||||
|
||||
### Not Sure Where to Start?
|
||||
A good place to start contributing, is the [First time issues project](https://github.com/Budibase/budibase/projects/22).
|
||||
A good place to start contributing is the [First time issues project](https://github.com/Budibase/budibase/projects/22).
|
||||
|
||||
### How the repository is organized
|
||||
Budibase is a monorepo managed by lerna. Lerna manages the building and publishing of the budibase packages. At a high level, here are the packages that make up Budibase.
|
||||
|
||||
- [packages/builder](https://github.com/Budibase/budibase/tree/HEAD/packages/builder) - contains code for the budibase builder client side svelte application.
|
||||
- [packages/builder](https://github.com/Budibase/budibase/tree/HEAD/packages/builder) - contains code for the budibase builder client-side svelte application.
|
||||
|
||||
- [packages/client](https://github.com/Budibase/budibase/tree/HEAD/packages/client) - A module that runs in the browser responsible for reading JSON definition and creating living, breathing web apps from it.
|
||||
|
||||
|
@ -193,7 +186,7 @@ For more information, see [CONTRIBUTING.md](https://github.com/Budibase/budibase
|
|||
|
||||
## 📝 License
|
||||
|
||||
Budibase is open-source, licensed as [GPL v3](https://www.gnu.org/licenses/gpl-3.0.en.html). The client and component libraries are licensed as [MPL](https://directory.fsf.org/wiki/License:MPL-2.0) - so the apps that you build can be licensed however you like.
|
||||
Budibase is open-source, licensed as [GPL v3](https://www.gnu.org/licenses/gpl-3.0.en.html). The client and component libraries are licensed as [MPL](https://directory.fsf.org/wiki/License:MPL-2.0) - so the apps you build can be licensed however you like.
|
||||
|
||||
<br /><br />
|
||||
|
||||
|
|
|
@ -1,76 +0,0 @@
|
|||
## 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
|
||||
```
|
||||
|
||||
### Install npm requirements
|
||||
|
||||
```
|
||||
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
|
||||
```
|
||||
|
||||
### Check Versions
|
||||
|
||||
This setup process was tested on Debian 11 (bullseye) with version numbers show below. Your mileage may vary using anything else.
|
||||
|
||||
- Docker: 20.10.5
|
||||
- Docker-Compose: 1.29.2
|
||||
- Node: v14.20.1
|
||||
- Yarn: 1.22.19
|
||||
- Lerna: 5.1.4
|
||||
|
||||
### Build
|
||||
|
||||
```
|
||||
cd budibase
|
||||
yarn setup
|
||||
```
|
||||
|
||||
The yarn setup command runs several build steps i.e.
|
||||
|
||||
```
|
||||
node ./hosting/scripts/setup.js && yarn && 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.
|
||||
|
||||
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.
|
||||
|
||||
Debian based distros:
|
||||
Add `* - nofile 65536` to `/etc/security/limits.conf`.
|
||||
|
||||
Arch:
|
||||
Add `DefaultLimitNOFILE=65536` to `/etc/systemd/system.conf`.
|
|
@ -1,84 +0,0 @@
|
|||
## Dev Environment on MAC OSX 12 (Monterey)
|
||||
|
||||
### Install Homebrew
|
||||
|
||||
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
|
||||
through brew.
|
||||
|
||||
### Install Node
|
||||
|
||||
Budibase requires a recent version of node 14:
|
||||
|
||||
```
|
||||
brew install node npm
|
||||
node -v
|
||||
```
|
||||
|
||||
### Install npm requirements
|
||||
|
||||
```
|
||||
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
|
||||
```
|
||||
|
||||
### Check Versions
|
||||
|
||||
This setup process was tested on Mac OSX 12 (Monterey) with version numbers shown below. Your mileage may vary using anything else.
|
||||
|
||||
- Docker: 20.10.14
|
||||
- Docker-Compose: 2.6.0
|
||||
- Node: 14.20.1
|
||||
- Yarn: 1.22.19
|
||||
- Lerna: 5.1.4
|
||||
|
||||
### Build
|
||||
|
||||
```
|
||||
cd budibase
|
||||
yarn setup
|
||||
```
|
||||
|
||||
The yarn setup command runs several build steps i.e.
|
||||
|
||||
```
|
||||
node ./hosting/scripts/setup.js && yarn && 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.
|
||||
|
||||
http://127.0.0.1:10000/builder/admin
|
||||
|
||||
| **NOTE**: If you are working on a M1 Apple Silicon, you will need to uncomment `# platform: linux/amd64` line in
|
||||
[hosting/docker-compose-dev.yaml](../hosting/docker-compose.dev.yaml)
|
||||
|
||||
### Troubleshootings
|
||||
|
||||
#### Yarn setup errors
|
||||
|
||||
If there are errors with the `yarn setup` command, you can try installing nvm and node 14. This is the same as the instructions for Debian 11.
|
||||
|
||||
#### Node 14.20.1 not supported for arm64
|
||||
|
||||
If you are working with M1 or M2 Mac and trying the Node installation via `nvm`, probably you will find the error `curl: (22) The requested URL returned error: 404`.
|
||||
|
||||
Version `v14.20.1` is not supported for arm64; in order to use it, you can switch the CPU architecture for this by the following command:
|
||||
|
||||
```shell
|
||||
arch -x86_64 zsh #Run this before nvm install
|
||||
```
|
|
@ -1,92 +0,0 @@
|
|||
## 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
|
||||
```
|
||||
|
||||
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:
|
||||
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
|
||||
|
||||
```
|
||||
npm install -g yarn jest lerna
|
||||
```
|
||||
|
||||
### Clone the repo
|
||||
|
||||
```
|
||||
git clone https://github.com/Budibase/budibase.git
|
||||
```
|
||||
|
||||
### Check Versions
|
||||
|
||||
This setup process was tested on Windows 11 with version numbers show below. Your mileage may vary using anything else.
|
||||
|
||||
- Docker: 20.10.7
|
||||
- Docker-Compose: 2.10.2
|
||||
- Node: v14.20.1
|
||||
- Yarn: 1.22.19
|
||||
- Lerna: 5.5.4
|
||||
|
||||
### Build
|
||||
|
||||
```
|
||||
cd budibase
|
||||
yarn setup
|
||||
```
|
||||
|
||||
The yarn setup command runs several build steps i.e.
|
||||
|
||||
```
|
||||
node ./hosting/scripts/setup.js && yarn && 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.
|
||||
|
||||
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.
|
|
@ -18,4 +18,37 @@ module.exports = {
|
|||
}
|
||||
},
|
||||
},
|
||||
"no-test-com": {
|
||||
meta: {
|
||||
type: "problem",
|
||||
docs: {
|
||||
description:
|
||||
"disallow the use of 'test.com' in strings and replace it with 'example.com'",
|
||||
category: "Possible Errors",
|
||||
recommended: false,
|
||||
},
|
||||
schema: [], // no options
|
||||
fixable: "code", // Indicates that this rule supports automatic fixing
|
||||
},
|
||||
create: function (context) {
|
||||
return {
|
||||
Literal(node) {
|
||||
if (
|
||||
typeof node.value === "string" &&
|
||||
node.value.includes("test.com")
|
||||
) {
|
||||
context.report({
|
||||
node,
|
||||
message:
|
||||
"test.com is a privately owned domain and could point anywhere, use example.com instead.",
|
||||
fix: function (fixer) {
|
||||
const newText = node.raw.replace(/test\.com/g, "example.com")
|
||||
return fixer.replaceText(node, newText)
|
||||
},
|
||||
})
|
||||
}
|
||||
},
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
|
|
|
@ -98,7 +98,6 @@ services:
|
|||
couchdb-service:
|
||||
restart: unless-stopped
|
||||
image: budibase/couchdb
|
||||
pull_policy: always
|
||||
environment:
|
||||
- COUCHDB_PASSWORD=${COUCH_DB_PASSWORD}
|
||||
- COUCHDB_USER=${COUCH_DB_USER}
|
||||
|
|
|
@ -124,6 +124,8 @@ HEALTHCHECK --interval=15s --timeout=15s --start-period=45s CMD "/healthcheck.sh
|
|||
|
||||
# must set this just before running
|
||||
ENV NODE_ENV=production
|
||||
# this is required for isolated-vm to work on Node 20+
|
||||
ENV NODE_OPTIONS="--no-node-snapshot"
|
||||
WORKDIR /
|
||||
|
||||
CMD ["./runner.sh"]
|
||||
|
|
|
@ -207,8 +207,7 @@ Desde comunicar un bug a solventar un error en el codigo, toda contribucion es a
|
|||
implementar una nueva funcionalidad o un realizar un cambio en la API, por favor crea un [nuevo mensaje aqui](https://github.com/Budibase/budibase/issues),
|
||||
de esta manera nos encargaremos que tu trabajo no sea en vano.
|
||||
|
||||
Aqui tienes instrucciones de como configurar tu entorno Budibase para [Debian](https://github.com/Budibase/budibase/tree/HEAD/docs/DEV-SETUP-DEBIAN.md)
|
||||
y [MacOSX](https://github.com/Budibase/budibase/tree/HEAD/docs/DEV-SETUP-MACOSX.md)
|
||||
Aqui tienes instrucciones de como configurar tu entorno Budibase para [aquí](https://github.com/Budibase/budibase/tree/HEAD/docs/CONTRIBUTING.md).
|
||||
|
||||
### No estas seguro por donde empezar?
|
||||
Un buen lugar para empezar a contribuir con nosotros es [aqui](https://github.com/Budibase/budibase/projects/22).
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"version": "2.14.3",
|
||||
"version": "2.19.6",
|
||||
"npmClient": "yarn",
|
||||
"packages": [
|
||||
"packages/*",
|
||||
|
|
15
package.json
15
package.json
|
@ -46,7 +46,7 @@
|
|||
"kill-accountportal": "kill-port 3001 4003",
|
||||
"kill-all": "yarn run kill-builder && yarn run kill-server && yarn kill-accountportal",
|
||||
"dev": "yarn run kill-all && lerna run --parallel prebuild && lerna run --stream dev --ignore=@budibase/account-portal-ui --ignore @budibase/account-portal-server",
|
||||
"dev:noserver": "yarn run kill-builder && lerna run --stream dev:stack:up && lerna run --stream dev --ignore @budibase/backend-core --ignore @budibase/server --ignore @budibase/worker --ignore=@budibase/account-portal-ui --ignore @budibase/account-portal-server",
|
||||
"dev:noserver": "yarn run kill-builder && lerna run --stream dev:stack:up --ignore @budibase/account-portal-server && lerna run --stream dev --ignore @budibase/backend-core --ignore @budibase/server --ignore @budibase/worker --ignore=@budibase/account-portal-ui --ignore @budibase/account-portal-server",
|
||||
"dev:server": "yarn run kill-server && lerna run --stream dev --scope @budibase/worker --scope @budibase/server",
|
||||
"dev:accountportal": "yarn kill-accountportal && lerna run dev --stream --scope @budibase/account-portal-ui --scope @budibase/account-portal-server",
|
||||
"dev:all": "yarn run kill-all && lerna run --stream dev",
|
||||
|
@ -58,7 +58,7 @@
|
|||
"lint": "yarn run lint:eslint && yarn run lint:prettier",
|
||||
"lint:fix:eslint": "eslint --fix --max-warnings=0 packages qa-core",
|
||||
"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",
|
||||
"lint:fix": "yarn run lint:fix:eslint && yarn run lint:fix:prettier",
|
||||
"build:specs": "lerna run --stream specs",
|
||||
"build:docker:airgap": "node hosting/scripts/airgapped/airgappedDockerBuild",
|
||||
"build:docker:airgap:single": "SINGLE_IMAGE=1 node hosting/scripts/airgapped/airgappedDockerBuild",
|
||||
|
@ -97,7 +97,16 @@
|
|||
"@budibase/backend-core": "0.0.0",
|
||||
"@budibase/shared-core": "0.0.0",
|
||||
"@budibase/string-templates": "0.0.0",
|
||||
"@budibase/types": "0.0.0"
|
||||
"@budibase/types": "0.0.0",
|
||||
"tough-cookie": "4.1.3",
|
||||
"node-fetch": "2.6.7",
|
||||
"semver": "7.5.3",
|
||||
"http-cache-semantics": "4.1.1",
|
||||
"msgpackr": "1.10.1",
|
||||
"axios": "1.6.3",
|
||||
"xml2js": "0.6.2",
|
||||
"unset-value": "2.0.1",
|
||||
"passport": "0.6.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.0.0 <21.0.0"
|
||||
|
|
|
@ -1 +1 @@
|
|||
Subproject commit 8ee2734e77709438cbcaaabc024f677c7b24c883
|
||||
Subproject commit 8c446c4ba385592127fa31755d3b64467b291882
|
|
@ -21,23 +21,23 @@
|
|||
"test:watch": "jest --watchAll"
|
||||
},
|
||||
"dependencies": {
|
||||
"@budibase/nano": "10.1.4",
|
||||
"@budibase/nano": "10.1.5",
|
||||
"@budibase/pouchdb-replication-stream": "1.2.10",
|
||||
"@budibase/shared-core": "0.0.0",
|
||||
"@budibase/types": "0.0.0",
|
||||
"@techpass/passport-openidconnect": "0.3.2",
|
||||
"@govtechsg/passport-openidconnect": "^1.0.2",
|
||||
"aws-cloudfront-sign": "3.0.2",
|
||||
"aws-sdk": "2.1030.0",
|
||||
"bcrypt": "5.1.0",
|
||||
"bcryptjs": "2.4.3",
|
||||
"bull": "4.10.1",
|
||||
"correlation-id": "4.0.0",
|
||||
"dd-trace": "3.13.2",
|
||||
"dd-trace": "5.2.0",
|
||||
"dotenv": "16.0.1",
|
||||
"ioredis": "5.3.2",
|
||||
"joi": "17.6.0",
|
||||
"jsonwebtoken": "9.0.2",
|
||||
"koa-passport": "4.1.4",
|
||||
"koa-passport": "^6.0.0",
|
||||
"koa-pino-logger": "4.0.0",
|
||||
"lodash": "4.17.21",
|
||||
"node-fetch": "2.6.7",
|
||||
|
@ -52,9 +52,9 @@
|
|||
"redlock": "4.2.0",
|
||||
"rotating-file-stream": "3.1.0",
|
||||
"sanitize-s3-objectkey": "0.0.1",
|
||||
"semver": "7.3.7",
|
||||
"semver": "^7.5.4",
|
||||
"tar-fs": "2.1.1",
|
||||
"uuid": "8.3.2"
|
||||
"uuid": "^8.3.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@shopify/jest-koa-mocks": "5.1.1",
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { IdentityContext } from "@budibase/types"
|
||||
import { IdentityContext, VM } from "@budibase/types"
|
||||
import { ExecutionTimeTracker } from "../timers"
|
||||
|
||||
// keep this out of Budibase types, don't want to expose context info
|
||||
|
@ -11,4 +11,5 @@ export type ContextMap = {
|
|||
automationId?: string
|
||||
isMigrating?: boolean
|
||||
jsExecutionTracker?: ExecutionTimeTracker
|
||||
vm?: VM
|
||||
}
|
||||
|
|
|
@ -19,6 +19,8 @@ import { WriteStream, ReadStream } from "fs"
|
|||
import { newid } from "../../docIds/newid"
|
||||
import { DDInstrumentedDatabase } from "../instrumentation"
|
||||
|
||||
const DATABASE_NOT_FOUND = "Database does not exist."
|
||||
|
||||
function buildNano(couchInfo: { url: string; cookie: string }) {
|
||||
return Nano({
|
||||
url: couchInfo.url,
|
||||
|
@ -31,6 +33,8 @@ function buildNano(couchInfo: { url: string; cookie: string }) {
|
|||
})
|
||||
}
|
||||
|
||||
type DBCall<T> = () => Promise<T>
|
||||
|
||||
export function DatabaseWithConnection(
|
||||
dbName: string,
|
||||
connection: string,
|
||||
|
@ -78,7 +82,11 @@ export class DatabaseImpl implements Database {
|
|||
return this.instanceNano || DatabaseImpl.nano
|
||||
}
|
||||
|
||||
async checkSetup() {
|
||||
private getDb() {
|
||||
return this.nano().db.use(this.name)
|
||||
}
|
||||
|
||||
private async checkAndCreateDb() {
|
||||
let shouldCreate = !this.pouchOpts?.skip_setup
|
||||
// check exists in a lightweight fashion
|
||||
let exists = await this.exists()
|
||||
|
@ -95,14 +103,22 @@ export class DatabaseImpl implements Database {
|
|||
}
|
||||
}
|
||||
}
|
||||
return this.nano().db.use(this.name)
|
||||
return this.getDb()
|
||||
}
|
||||
|
||||
private async updateOutput(fnc: any) {
|
||||
// this function fetches the DB and handles if DB creation is needed
|
||||
private async performCall<T>(
|
||||
call: (db: Nano.DocumentScope<any>) => Promise<DBCall<T>> | DBCall<T>
|
||||
): Promise<any> {
|
||||
const db = this.getDb()
|
||||
const fnc = await call(db)
|
||||
try {
|
||||
return await fnc()
|
||||
} catch (err: any) {
|
||||
if (err.statusCode) {
|
||||
if (err.statusCode === 404 && err.reason === DATABASE_NOT_FOUND) {
|
||||
await this.checkAndCreateDb()
|
||||
return await this.performCall(call)
|
||||
} else if (err.statusCode) {
|
||||
err.status = err.statusCode
|
||||
}
|
||||
throw err
|
||||
|
@ -110,11 +126,12 @@ export class DatabaseImpl implements Database {
|
|||
}
|
||||
|
||||
async get<T extends Document>(id?: string): Promise<T> {
|
||||
const db = await this.checkSetup()
|
||||
if (!id) {
|
||||
throw new Error("Unable to get doc without a valid _id.")
|
||||
}
|
||||
return this.updateOutput(() => db.get(id))
|
||||
return this.performCall(db => {
|
||||
if (!id) {
|
||||
throw new Error("Unable to get doc without a valid _id.")
|
||||
}
|
||||
return () => db.get(id)
|
||||
})
|
||||
}
|
||||
|
||||
async getMultiple<T extends Document>(
|
||||
|
@ -147,22 +164,23 @@ export class DatabaseImpl implements Database {
|
|||
}
|
||||
|
||||
async remove(idOrDoc: string | Document, rev?: string) {
|
||||
const db = await this.checkSetup()
|
||||
let _id: string
|
||||
let _rev: string
|
||||
return this.performCall(db => {
|
||||
let _id: string
|
||||
let _rev: string
|
||||
|
||||
if (isDocument(idOrDoc)) {
|
||||
_id = idOrDoc._id!
|
||||
_rev = idOrDoc._rev!
|
||||
} else {
|
||||
_id = idOrDoc
|
||||
_rev = rev!
|
||||
}
|
||||
if (isDocument(idOrDoc)) {
|
||||
_id = idOrDoc._id!
|
||||
_rev = idOrDoc._rev!
|
||||
} else {
|
||||
_id = idOrDoc
|
||||
_rev = rev!
|
||||
}
|
||||
|
||||
if (!_id || !_rev) {
|
||||
throw new Error("Unable to remove doc without a valid _id and _rev.")
|
||||
}
|
||||
return this.updateOutput(() => db.destroy(_id, _rev))
|
||||
if (!_id || !_rev) {
|
||||
throw new Error("Unable to remove doc without a valid _id and _rev.")
|
||||
}
|
||||
return () => db.destroy(_id, _rev)
|
||||
})
|
||||
}
|
||||
|
||||
async post(document: AnyDocument, opts?: DatabasePutOpts) {
|
||||
|
@ -176,45 +194,49 @@ export class DatabaseImpl implements Database {
|
|||
if (!document._id) {
|
||||
throw new Error("Cannot store document without _id field.")
|
||||
}
|
||||
const db = await this.checkSetup()
|
||||
if (!document.createdAt) {
|
||||
document.createdAt = new Date().toISOString()
|
||||
}
|
||||
document.updatedAt = new Date().toISOString()
|
||||
if (opts?.force && document._id) {
|
||||
try {
|
||||
const existing = await this.get(document._id)
|
||||
if (existing) {
|
||||
document._rev = existing._rev
|
||||
}
|
||||
} catch (err: any) {
|
||||
if (err.status !== 404) {
|
||||
throw err
|
||||
return this.performCall(async db => {
|
||||
if (!document.createdAt) {
|
||||
document.createdAt = new Date().toISOString()
|
||||
}
|
||||
document.updatedAt = new Date().toISOString()
|
||||
if (opts?.force && document._id) {
|
||||
try {
|
||||
const existing = await this.get(document._id)
|
||||
if (existing) {
|
||||
document._rev = existing._rev
|
||||
}
|
||||
} catch (err: any) {
|
||||
if (err.status !== 404) {
|
||||
throw err
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return this.updateOutput(() => db.insert(document))
|
||||
return () => db.insert(document)
|
||||
})
|
||||
}
|
||||
|
||||
async bulkDocs(documents: AnyDocument[]) {
|
||||
const db = await this.checkSetup()
|
||||
return this.updateOutput(() => db.bulk({ docs: documents }))
|
||||
return this.performCall(db => {
|
||||
return () => db.bulk({ docs: documents })
|
||||
})
|
||||
}
|
||||
|
||||
async allDocs<T extends Document>(
|
||||
params: DatabaseQueryOpts
|
||||
): Promise<AllDocsResponse<T>> {
|
||||
const db = await this.checkSetup()
|
||||
return this.updateOutput(() => db.list(params))
|
||||
return this.performCall(db => {
|
||||
return () => db.list(params)
|
||||
})
|
||||
}
|
||||
|
||||
async query<T extends Document>(
|
||||
viewName: string,
|
||||
params: DatabaseQueryOpts
|
||||
): Promise<AllDocsResponse<T>> {
|
||||
const db = await this.checkSetup()
|
||||
const [database, view] = viewName.split("/")
|
||||
return this.updateOutput(() => db.view(database, view, params))
|
||||
return this.performCall(db => {
|
||||
const [database, view] = viewName.split("/")
|
||||
return () => db.view(database, view, params)
|
||||
})
|
||||
}
|
||||
|
||||
async destroy() {
|
||||
|
@ -231,8 +253,9 @@ export class DatabaseImpl implements Database {
|
|||
}
|
||||
|
||||
async compact() {
|
||||
const db = await this.checkSetup()
|
||||
return this.updateOutput(() => db.compact())
|
||||
return this.performCall(db => {
|
||||
return () => db.compact()
|
||||
})
|
||||
}
|
||||
|
||||
// All below functions are in-frequently called, just utilise PouchDB
|
||||
|
|
|
@ -31,13 +31,6 @@ export class DDInstrumentedDatabase implements Database {
|
|||
})
|
||||
}
|
||||
|
||||
checkSetup(): Promise<DocumentScope<any>> {
|
||||
return tracer.trace("db.checkSetup", span => {
|
||||
span?.addTags({ db_name: this.name })
|
||||
return this.db.checkSetup()
|
||||
})
|
||||
}
|
||||
|
||||
get<T extends Document>(id?: string | undefined): Promise<T> {
|
||||
return tracer.trace("db.get", span => {
|
||||
span?.addTags({ db_name: this.name, doc_id: id })
|
||||
|
|
|
@ -179,6 +179,7 @@ const environment = {
|
|||
...getPackageJsonFields(),
|
||||
DISABLE_PINO_LOGGER: process.env.DISABLE_PINO_LOGGER,
|
||||
OFFLINE_MODE: process.env.OFFLINE_MODE,
|
||||
SESSION_EXPIRY_SECONDS: process.env.SESSION_EXPIRY_SECONDS,
|
||||
_set(key: any, value: any) {
|
||||
process.env[key] = value
|
||||
// @ts-ignore
|
||||
|
|
|
@ -3,6 +3,7 @@ import {
|
|||
Event,
|
||||
Datasource,
|
||||
Query,
|
||||
QueryPreview,
|
||||
QueryCreatedEvent,
|
||||
QueryUpdatedEvent,
|
||||
QueryDeletedEvent,
|
||||
|
@ -68,9 +69,9 @@ const run = async (count: number, timestamp?: string | number) => {
|
|||
await publishEvent(Event.QUERIES_RUN, properties, timestamp)
|
||||
}
|
||||
|
||||
const previewed = async (datasource: Datasource, query: Query) => {
|
||||
const previewed = async (datasource: Datasource, query: QueryPreview) => {
|
||||
const properties: QueryPreviewedEvent = {
|
||||
queryId: query._id,
|
||||
queryId: query.queryId,
|
||||
datasourceId: datasource._id as string,
|
||||
source: datasource.source,
|
||||
queryVerb: query.queryVerb,
|
||||
|
|
|
@ -2,6 +2,7 @@ export * as configs from "./configs"
|
|||
export * as events from "./events"
|
||||
export * as migrations from "./migrations"
|
||||
export * as users from "./users"
|
||||
export * as userUtils from "./users/utils"
|
||||
export * as roles from "./security/roles"
|
||||
export * as permissions from "./security/permissions"
|
||||
export * as accounts from "./accounts"
|
||||
|
|
|
@ -6,6 +6,7 @@ import * as context from "./context"
|
|||
import semver from "semver"
|
||||
import { bustCache, withCache, TTL, CacheKey } from "./cache/generic"
|
||||
import environment from "./environment"
|
||||
import { logAlert } from "./logging"
|
||||
|
||||
export const getInstall = async (): Promise<Installation> => {
|
||||
return withCache(CacheKey.INSTALLATION, TTL.ONE_DAY, getInstallFromDB, {
|
||||
|
@ -80,27 +81,35 @@ export const checkInstallVersion = async (): Promise<void> => {
|
|||
const currentVersion = install.version
|
||||
const newVersion = environment.VERSION
|
||||
|
||||
if (currentVersion !== newVersion) {
|
||||
const isUpgrade = semver.gt(newVersion, currentVersion)
|
||||
const isDowngrade = semver.lt(newVersion, currentVersion)
|
||||
try {
|
||||
if (currentVersion !== newVersion) {
|
||||
const isUpgrade = semver.gt(newVersion, currentVersion)
|
||||
const isDowngrade = semver.lt(newVersion, currentVersion)
|
||||
|
||||
const success = await updateVersion(newVersion)
|
||||
const success = await updateVersion(newVersion)
|
||||
|
||||
if (success) {
|
||||
await context.doInIdentityContext(
|
||||
{
|
||||
_id: install.installId,
|
||||
type: IdentityType.INSTALLATION,
|
||||
},
|
||||
async () => {
|
||||
if (isUpgrade) {
|
||||
await events.installation.upgraded(currentVersion, newVersion)
|
||||
} else if (isDowngrade) {
|
||||
await events.installation.downgraded(currentVersion, newVersion)
|
||||
if (success) {
|
||||
await context.doInIdentityContext(
|
||||
{
|
||||
_id: install.installId,
|
||||
type: IdentityType.INSTALLATION,
|
||||
},
|
||||
async () => {
|
||||
if (isUpgrade) {
|
||||
await events.installation.upgraded(currentVersion, newVersion)
|
||||
} else if (isDowngrade) {
|
||||
await events.installation.downgraded(currentVersion, newVersion)
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
await events.identification.identifyInstallationGroup(install.installId)
|
||||
)
|
||||
await events.identification.identifyInstallationGroup(install.installId)
|
||||
}
|
||||
}
|
||||
} catch (err: any) {
|
||||
if (err?.message?.includes("Invalid Version")) {
|
||||
logAlert(`Invalid version "${newVersion}" - is it semver?`)
|
||||
} else {
|
||||
logAlert("Failed to retrieve version", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,11 +2,12 @@ import { Header } from "../../constants"
|
|||
|
||||
const correlator = require("correlation-id")
|
||||
|
||||
export const setHeader = (headers: any) => {
|
||||
export const setHeader = (headers: Record<string, string>) => {
|
||||
const correlationId = correlator.getId()
|
||||
if (correlationId) {
|
||||
headers[Header.CORRELATION_ID] = correlationId
|
||||
if (!correlationId) {
|
||||
return
|
||||
}
|
||||
headers[Header.CORRELATION_ID] = correlationId
|
||||
}
|
||||
|
||||
export function getId() {
|
||||
|
|
|
@ -23,7 +23,7 @@ const getCloudfrontSignParams = () => {
|
|||
return {
|
||||
keypairId: env.CLOUDFRONT_PUBLIC_KEY_ID!,
|
||||
privateKeyString: getPrivateKey(),
|
||||
expireTime: new Date().getTime() + 1000 * 60 * 60, // 1 hour
|
||||
expireTime: new Date().getTime() + 1000 * 60 * 60 * 24, // 1 day
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -7,7 +7,7 @@ import tar from "tar-fs"
|
|||
import zlib from "zlib"
|
||||
import { promisify } from "util"
|
||||
import { join } from "path"
|
||||
import fs from "fs"
|
||||
import fs, { ReadStream } from "fs"
|
||||
import env from "../environment"
|
||||
import { budibaseTempDir } from "./utils"
|
||||
import { v4 } from "uuid"
|
||||
|
@ -184,7 +184,7 @@ export async function upload({
|
|||
export async function streamUpload(
|
||||
bucketName: string,
|
||||
filename: string,
|
||||
stream: any,
|
||||
stream: ReadStream | ReadableStream,
|
||||
extra = {}
|
||||
) {
|
||||
const objectStore = ObjectStore(bucketName)
|
||||
|
@ -255,7 +255,8 @@ export async function listAllObjects(bucketName: string, path: string) {
|
|||
objects = objects.concat(response.Contents)
|
||||
}
|
||||
isTruncated = !!response.IsTruncated
|
||||
} while (isTruncated)
|
||||
token = response.NextContinuationToken
|
||||
} while (isTruncated && token)
|
||||
return objects
|
||||
}
|
||||
|
||||
|
|
|
@ -2,7 +2,7 @@ import env from "../environment"
|
|||
import { getRedisOptions } from "../redis/utils"
|
||||
import { JobQueue } from "./constants"
|
||||
import InMemoryQueue from "./inMemoryQueue"
|
||||
import BullQueue, { QueueOptions } from "bull"
|
||||
import BullQueue, { QueueOptions, JobOptions } from "bull"
|
||||
import { addListeners, StalledFn } from "./listeners"
|
||||
import { Duration } from "../utils"
|
||||
import * as timers from "../timers"
|
||||
|
@ -24,17 +24,24 @@ async function cleanup() {
|
|||
|
||||
export function createQueue<T>(
|
||||
jobQueue: JobQueue,
|
||||
opts: { removeStalledCb?: StalledFn } = {}
|
||||
opts: {
|
||||
removeStalledCb?: StalledFn
|
||||
maxStalledCount?: number
|
||||
jobOptions?: JobOptions
|
||||
} = {}
|
||||
): BullQueue.Queue<T> {
|
||||
const redisOpts = getRedisOptions()
|
||||
const queueConfig: QueueOptions = {
|
||||
redis: redisOpts,
|
||||
settings: {
|
||||
maxStalledCount: 0,
|
||||
maxStalledCount: opts.maxStalledCount ? opts.maxStalledCount : 0,
|
||||
lockDuration: QUEUE_LOCK_MS,
|
||||
lockRenewTime: QUEUE_LOCK_RENEW_INTERNAL_MS,
|
||||
},
|
||||
}
|
||||
if (opts.jobOptions) {
|
||||
queueConfig.defaultJobOptions = opts.jobOptions
|
||||
}
|
||||
let queue: any
|
||||
if (!env.isTest()) {
|
||||
queue = new BullQueue(jobQueue, queueConfig)
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
const redis = require("../redis/init")
|
||||
const { v4: uuidv4 } = require("uuid")
|
||||
const { logWarn } = require("../logging")
|
||||
|
||||
import * as redis from "../redis/init"
|
||||
import { v4 as uuidv4 } from "uuid"
|
||||
import { logWarn } from "../logging"
|
||||
import env from "../environment"
|
||||
import { Duration } from "../utils"
|
||||
import {
|
||||
Session,
|
||||
ScannedSession,
|
||||
|
@ -10,8 +10,10 @@ import {
|
|||
CreateSession,
|
||||
} from "@budibase/types"
|
||||
|
||||
// a week in seconds
|
||||
const EXPIRY_SECONDS = 86400 * 7
|
||||
// a week expiry is the default
|
||||
const EXPIRY_SECONDS = env.SESSION_EXPIRY_SECONDS
|
||||
? parseInt(env.SESSION_EXPIRY_SECONDS)
|
||||
: Duration.fromDays(7).toSeconds()
|
||||
|
||||
function makeSessionID(userId: string, sessionId: string) {
|
||||
return `${userId}/${sessionId}`
|
||||
|
|
|
@ -251,7 +251,8 @@ export class UserDB {
|
|||
}
|
||||
|
||||
const change = dbUser ? 0 : 1 // no change if there is existing user
|
||||
const creatorsChange = isCreator(dbUser) !== isCreator(user) ? 1 : 0
|
||||
const creatorsChange =
|
||||
(await isCreator(dbUser)) !== (await isCreator(user)) ? 1 : 0
|
||||
return UserDB.quotas.addUsers(change, creatorsChange, async () => {
|
||||
await validateUniqueUser(email, tenantId)
|
||||
|
||||
|
@ -335,7 +336,7 @@ export class UserDB {
|
|||
}
|
||||
newUser.userGroups = groups || []
|
||||
newUsers.push(newUser)
|
||||
if (isCreator(newUser)) {
|
||||
if (await isCreator(newUser)) {
|
||||
newCreators.push(newUser)
|
||||
}
|
||||
}
|
||||
|
@ -432,12 +433,16 @@ export class UserDB {
|
|||
_deleted: true,
|
||||
}))
|
||||
const dbResponse = await usersCore.bulkUpdateGlobalUsers(toDelete)
|
||||
const creatorsToDelete = usersToDelete.filter(isCreator)
|
||||
|
||||
const creatorsEval = await Promise.all(usersToDelete.map(isCreator))
|
||||
const creatorsToDeleteCount = creatorsEval.filter(
|
||||
creator => !!creator
|
||||
).length
|
||||
|
||||
for (let user of usersToDelete) {
|
||||
await bulkDeleteProcessing(user)
|
||||
}
|
||||
await UserDB.quotas.removeUsers(toDelete.length, creatorsToDelete.length)
|
||||
await UserDB.quotas.removeUsers(toDelete.length, creatorsToDeleteCount)
|
||||
|
||||
// Build Response
|
||||
// index users by id
|
||||
|
@ -486,7 +491,7 @@ export class UserDB {
|
|||
|
||||
await db.remove(userId, dbUser._rev)
|
||||
|
||||
const creatorsToDelete = isCreator(dbUser) ? 1 : 0
|
||||
const creatorsToDelete = (await isCreator(dbUser)) ? 1 : 0
|
||||
await UserDB.quotas.removeUsers(1, creatorsToDelete)
|
||||
await eventHelpers.handleDeleteEvents(dbUser)
|
||||
await cache.user.invalidateUser(userId)
|
||||
|
|
|
@ -0,0 +1,67 @@
|
|||
import { User, UserGroup } from "@budibase/types"
|
||||
import { generator, structures } from "../../../tests"
|
||||
import { DBTestConfiguration } from "../../../tests/extra"
|
||||
import { getGlobalDB } from "../../context"
|
||||
import { isCreator } from "../utils"
|
||||
|
||||
const config = new DBTestConfiguration()
|
||||
|
||||
describe("Users", () => {
|
||||
it("User is a creator if it is configured as a global builder", async () => {
|
||||
const user: User = structures.users.user({ builder: { global: true } })
|
||||
expect(await isCreator(user)).toBe(true)
|
||||
})
|
||||
|
||||
it("User is a creator if it is configured as a global admin", async () => {
|
||||
const user: User = structures.users.user({ admin: { global: true } })
|
||||
expect(await isCreator(user)).toBe(true)
|
||||
})
|
||||
|
||||
it("User is a creator if it is configured with creator permission", async () => {
|
||||
const user: User = structures.users.user({ builder: { creator: true } })
|
||||
expect(await isCreator(user)).toBe(true)
|
||||
})
|
||||
|
||||
it("User is a creator if it is a builder in some application", async () => {
|
||||
const user: User = structures.users.user({ builder: { apps: ["app1"] } })
|
||||
expect(await isCreator(user)).toBe(true)
|
||||
})
|
||||
|
||||
it("User is a creator if it has CREATOR permission in some application", async () => {
|
||||
const user: User = structures.users.user({ roles: { app1: "CREATOR" } })
|
||||
expect(await isCreator(user)).toBe(true)
|
||||
})
|
||||
|
||||
it("User is a creator if it has ADMIN permission in some application", async () => {
|
||||
const user: User = structures.users.user({ roles: { app1: "ADMIN" } })
|
||||
expect(await isCreator(user)).toBe(true)
|
||||
})
|
||||
|
||||
it("User is a creator if it remains to a group with ADMIN permissions", async () => {
|
||||
const usersInGroup = 10
|
||||
const groupId = "gr_17abffe89e0b40268e755b952f101a59"
|
||||
const group: UserGroup = {
|
||||
...structures.userGroups.userGroup(),
|
||||
...{ _id: groupId, roles: { app1: "ADMIN" } },
|
||||
}
|
||||
const users: User[] = []
|
||||
for (const _ of Array.from({ length: usersInGroup })) {
|
||||
const userId = `us_${generator.guid()}`
|
||||
const user: User = structures.users.user({
|
||||
_id: userId,
|
||||
userGroups: [groupId],
|
||||
})
|
||||
users.push(user)
|
||||
}
|
||||
|
||||
await config.doInTenant(async () => {
|
||||
const db = getGlobalDB()
|
||||
await db.put(group)
|
||||
for (let user of users) {
|
||||
await db.put(user)
|
||||
const creator = await isCreator(user)
|
||||
expect(creator).toBe(true)
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
|
@ -309,7 +309,8 @@ export async function getCreatorCount() {
|
|||
let creators = 0
|
||||
async function iterate(startPage?: string) {
|
||||
const page = await paginatedUsers({ bookmark: startPage })
|
||||
creators += page.data.filter(isCreator).length
|
||||
const creatorsEval = await Promise.all(page.data.map(isCreator))
|
||||
creators += creatorsEval.filter(creator => !!creator).length
|
||||
if (page.hasNextPage) {
|
||||
await iterate(page.nextPage)
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { CloudAccount } from "@budibase/types"
|
||||
import { CloudAccount, ContextUser, User, UserGroup } from "@budibase/types"
|
||||
import * as accountSdk from "../accounts"
|
||||
import env from "../environment"
|
||||
import { getPlatformUser } from "./lookup"
|
||||
|
@ -6,17 +6,48 @@ import { EmailUnavailableError } from "../errors"
|
|||
import { getTenantId } from "../context"
|
||||
import { sdk } from "@budibase/shared-core"
|
||||
import { getAccountByTenantId } from "../accounts"
|
||||
import { BUILTIN_ROLE_IDS } from "../security/roles"
|
||||
import * as context from "../context"
|
||||
|
||||
// extract from shared-core to make easily accessible from backend-core
|
||||
export const isBuilder = sdk.users.isBuilder
|
||||
export const isAdmin = sdk.users.isAdmin
|
||||
export const isCreator = sdk.users.isCreator
|
||||
export const isGlobalBuilder = sdk.users.isGlobalBuilder
|
||||
export const isAdminOrBuilder = sdk.users.isAdminOrBuilder
|
||||
export const hasAdminPermissions = sdk.users.hasAdminPermissions
|
||||
export const hasBuilderPermissions = sdk.users.hasBuilderPermissions
|
||||
export const hasAppBuilderPermissions = sdk.users.hasAppBuilderPermissions
|
||||
|
||||
export async function isCreator(user?: User | ContextUser) {
|
||||
const isCreatorByUserDefinition = sdk.users.isCreator(user)
|
||||
if (!isCreatorByUserDefinition && user) {
|
||||
return await isCreatorByGroupMembership(user)
|
||||
}
|
||||
return isCreatorByUserDefinition
|
||||
}
|
||||
|
||||
async function isCreatorByGroupMembership(user?: User | ContextUser) {
|
||||
const userGroups = user?.userGroups || []
|
||||
if (userGroups.length > 0) {
|
||||
const db = context.getGlobalDB()
|
||||
const groups: UserGroup[] = []
|
||||
for (let groupId of userGroups) {
|
||||
try {
|
||||
const group = await db.get<UserGroup>(groupId)
|
||||
groups.push(group)
|
||||
} catch (e: any) {
|
||||
if (e.error !== "not_found") {
|
||||
throw e
|
||||
}
|
||||
}
|
||||
}
|
||||
return groups.some(group =>
|
||||
Object.values(group.roles || {}).includes(BUILTIN_ROLE_IDS.ADMIN)
|
||||
)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
export async function validateUniqueUser(email: string, tenantId: string) {
|
||||
// check budibase users in other tenants
|
||||
if (env.MULTI_TENANCY) {
|
||||
|
|
|
@ -44,11 +44,11 @@ describe("utils", () => {
|
|||
|
||||
it("gets appId from url", async () => {
|
||||
await config.doInTenant(async () => {
|
||||
const url = "http://test.com"
|
||||
const url = "http://example.com"
|
||||
env._set("PLATFORM_URL", url)
|
||||
|
||||
const ctx = structures.koa.newContext()
|
||||
ctx.host = `${config.tenantId}.test.com`
|
||||
ctx.host = `${config.tenantId}.example.com`
|
||||
|
||||
const expected = db.generateAppID(config.tenantId)
|
||||
const app = structures.apps.app(expected)
|
||||
|
@ -89,7 +89,7 @@ describe("utils", () => {
|
|||
const ctx = structures.koa.newContext()
|
||||
const expected = db.generateAppID()
|
||||
ctx.request.headers = {
|
||||
referer: `http://test.com/builder/app/${expected}/design/screen_123/screens`,
|
||||
referer: `http://example.com/builder/app/${expected}/design/screen_123/screens`,
|
||||
}
|
||||
|
||||
const actual = await utils.getAppIdFromCtx(ctx)
|
||||
|
@ -100,7 +100,7 @@ describe("utils", () => {
|
|||
const ctx = structures.koa.newContext()
|
||||
const appId = db.generateAppID()
|
||||
ctx.request.headers = {
|
||||
referer: `http://test.com/foo/app/${appId}/bar`,
|
||||
referer: `http://example.com/foo/app/${appId}/bar`,
|
||||
}
|
||||
|
||||
const actual = await utils.getAppIdFromCtx(ctx)
|
||||
|
|
|
@ -3,5 +3,5 @@ import { v4 as uuid } from "uuid"
|
|||
export { v4 as uuid } from "uuid"
|
||||
|
||||
export const email = () => {
|
||||
return `${uuid()}@test.com`
|
||||
return `${uuid()}@example.com`
|
||||
}
|
||||
|
|
|
@ -61,7 +61,7 @@ export function ssoProfile(user?: User): SSOProfile {
|
|||
},
|
||||
_json: {
|
||||
email: user.email,
|
||||
picture: "http://test.com",
|
||||
picture: "http://example.com",
|
||||
},
|
||||
provider: generator.string(),
|
||||
}
|
||||
|
|
|
@ -25,7 +25,7 @@ export const user = (userProps?: Partial<Omit<User, "userId">>): User => {
|
|||
roles: { app_test: "admin" },
|
||||
firstName: generator.first(),
|
||||
lastName: generator.last(),
|
||||
pictureUrl: "http://test.com",
|
||||
pictureUrl: "http://example.com",
|
||||
tenantId: tenant.id(),
|
||||
...userProps,
|
||||
}
|
||||
|
|
|
@ -18,7 +18,6 @@ export default function positionDropdown(element, opts) {
|
|||
useAnchorWidth,
|
||||
offset = 5,
|
||||
customUpdate,
|
||||
offsetBelow,
|
||||
} = opts
|
||||
if (!anchor) {
|
||||
return
|
||||
|
@ -48,7 +47,7 @@ export default function positionDropdown(element, opts) {
|
|||
styles.top = anchorBounds.top - elementBounds.height - offset
|
||||
styles.maxHeight = maxHeight || 240
|
||||
} else {
|
||||
styles.top = anchorBounds.bottom + (offsetBelow || offset)
|
||||
styles.top = anchorBounds.bottom + offset
|
||||
styles.maxHeight =
|
||||
maxHeight || window.innerHeight - anchorBounds.bottom - 20
|
||||
}
|
||||
|
|
|
@ -184,7 +184,7 @@
|
|||
{#if environmentVariablesEnabled}
|
||||
<div on:click={() => showModal()} class="add-variable">
|
||||
<svg
|
||||
class="spectrum-Icon spectrum-Icon--sizeS "
|
||||
class="spectrum-Icon spectrum-Icon--sizeS"
|
||||
focusable="false"
|
||||
aria-hidden="true"
|
||||
>
|
||||
|
@ -195,7 +195,7 @@
|
|||
{:else}
|
||||
<div on:click={() => handleUpgradePanel()} class="add-variable">
|
||||
<svg
|
||||
class="spectrum-Icon spectrum-Icon--sizeS "
|
||||
class="spectrum-Icon spectrum-Icon--sizeS"
|
||||
focusable="false"
|
||||
aria-hidden="true"
|
||||
>
|
||||
|
|
|
@ -15,8 +15,6 @@
|
|||
export let autoWidth = false
|
||||
export let searchTerm = null
|
||||
export let customPopoverHeight
|
||||
export let customPopoverOffsetBelow
|
||||
export let customPopoverMaxHeight
|
||||
export let open = false
|
||||
export let loading
|
||||
|
||||
|
@ -98,7 +96,5 @@
|
|||
{sort}
|
||||
{autoWidth}
|
||||
{customPopoverHeight}
|
||||
{customPopoverOffsetBelow}
|
||||
{customPopoverMaxHeight}
|
||||
{loading}
|
||||
/>
|
||||
|
|
|
@ -37,8 +37,6 @@
|
|||
export let sort = false
|
||||
export let searchTerm = null
|
||||
export let customPopoverHeight
|
||||
export let customPopoverOffsetBelow
|
||||
export let customPopoverMaxHeight
|
||||
export let align = "left"
|
||||
export let footer = null
|
||||
export let customAnchor = null
|
||||
|
@ -156,9 +154,7 @@
|
|||
on:close={() => (open = false)}
|
||||
useAnchorWidth={!autoWidth}
|
||||
maxWidth={autoWidth ? 400 : null}
|
||||
maxHeight={customPopoverMaxHeight}
|
||||
customHeight={customPopoverHeight}
|
||||
offsetBelow={customPopoverOffsetBelow}
|
||||
>
|
||||
<div
|
||||
class="popover-content"
|
||||
|
|
|
@ -12,6 +12,7 @@
|
|||
export let getOptionIcon = () => null
|
||||
export let getOptionColour = () => null
|
||||
export let getOptionSubtitle = () => null
|
||||
export let compare = null
|
||||
export let useOptionIconImage = false
|
||||
export let isOptionEnabled
|
||||
export let readonly = false
|
||||
|
@ -23,8 +24,6 @@
|
|||
export let footer = null
|
||||
export let open = false
|
||||
export let tag = null
|
||||
export let customPopoverOffsetBelow
|
||||
export let customPopoverMaxHeight
|
||||
export let searchTerm = null
|
||||
export let loading
|
||||
|
||||
|
@ -34,13 +33,19 @@
|
|||
$: fieldIcon = getFieldAttribute(getOptionIcon, value, options)
|
||||
$: fieldColour = getFieldAttribute(getOptionColour, value, options)
|
||||
|
||||
function compareOptionAndValue(option, value) {
|
||||
return typeof compare === "function"
|
||||
? compare(option, value)
|
||||
: option === value
|
||||
}
|
||||
|
||||
const getFieldAttribute = (getAttribute, value, options) => {
|
||||
// Wait for options to load if there is a value but no options
|
||||
if (!options?.length) {
|
||||
return ""
|
||||
}
|
||||
const index = options.findIndex(
|
||||
(option, idx) => getOptionValue(option, idx) === value
|
||||
const index = options.findIndex((option, idx) =>
|
||||
compareOptionAndValue(getOptionValue(option, idx), value)
|
||||
)
|
||||
return index !== -1 ? getAttribute(options[index], index) : null
|
||||
}
|
||||
|
@ -90,11 +95,9 @@
|
|||
{autocomplete}
|
||||
{sort}
|
||||
{tag}
|
||||
{customPopoverOffsetBelow}
|
||||
{customPopoverMaxHeight}
|
||||
isPlaceholder={value == null || value === ""}
|
||||
placeholderOption={placeholder === false ? null : placeholder}
|
||||
isOptionSelected={option => option === value}
|
||||
isOptionSelected={option => compareOptionAndValue(option, value)}
|
||||
onSelectOption={selectOption}
|
||||
{loading}
|
||||
/>
|
||||
|
|
|
@ -28,6 +28,7 @@
|
|||
export let footer = null
|
||||
export let tag = null
|
||||
export let helpText = null
|
||||
export let compare
|
||||
const dispatch = createEventDispatcher()
|
||||
const onChange = e => {
|
||||
value = e.detail
|
||||
|
@ -65,6 +66,7 @@
|
|||
{autocomplete}
|
||||
{customPopoverHeight}
|
||||
{tag}
|
||||
{compare}
|
||||
on:change={onChange}
|
||||
on:click
|
||||
/>
|
||||
|
|
|
@ -40,7 +40,7 @@
|
|||
loading = false
|
||||
}
|
||||
|
||||
async function confirm() {
|
||||
export async function confirm() {
|
||||
loading = true
|
||||
if (!onConfirm || (await onConfirm()) !== keepOpen) {
|
||||
hide()
|
||||
|
|
|
@ -18,7 +18,6 @@
|
|||
export let useAnchorWidth = false
|
||||
export let dismissible = true
|
||||
export let offset = 5
|
||||
export let offsetBelow
|
||||
export let customHeight
|
||||
export let animate = true
|
||||
export let customZindex
|
||||
|
@ -89,7 +88,6 @@
|
|||
maxWidth,
|
||||
useAnchorWidth,
|
||||
offset,
|
||||
offsetBelow,
|
||||
customUpdate: handlePostionUpdate,
|
||||
}}
|
||||
use:clickOutside={{
|
||||
|
|
Binary file not shown.
After Width: | Height: | Size: 12 KiB |
|
@ -23,7 +23,6 @@
|
|||
"\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$": "<rootDir>/internals/mocks/fileMock.js",
|
||||
"\\.(css|less|sass|scss)$": "identity-obj-proxy",
|
||||
"components(.*)$": "<rootDir>/src/components$1",
|
||||
"builderStore(.*)$": "<rootDir>/src/builderStore$1",
|
||||
"stores(.*)$": "<rootDir>/src/stores$1",
|
||||
"analytics(.*)$": "<rootDir>/src/analytics$1",
|
||||
"constants/backend": "<rootDir>/src/constants/backend/index.js"
|
||||
|
@ -66,6 +65,7 @@
|
|||
"@fortawesome/free-solid-svg-icons": "^6.4.2",
|
||||
"@spectrum-css/page": "^3.0.1",
|
||||
"@spectrum-css/vars": "^3.0.1",
|
||||
"@zerodevx/svelte-json-view": "^1.0.7",
|
||||
"codemirror": "^5.59.0",
|
||||
"dayjs": "^1.10.8",
|
||||
"downloadjs": "1.4.7",
|
||||
|
|
|
@ -3,14 +3,14 @@ import {
|
|||
CookieUtils,
|
||||
Constants,
|
||||
} from "@budibase/frontend-core"
|
||||
import { store } from "./builderStore"
|
||||
import { appStore } from "stores/builder"
|
||||
import { get } from "svelte/store"
|
||||
import { auth } from "./stores/portal"
|
||||
import { auth, navigation } from "./stores/portal"
|
||||
|
||||
export const API = createAPIClient({
|
||||
attachHeaders: headers => {
|
||||
// Attach app ID header from store
|
||||
let appId = get(store).appId
|
||||
let appId = get(appStore).appId
|
||||
if (appId) {
|
||||
headers["x-budibase-app-id"] = appId
|
||||
}
|
||||
|
@ -45,4 +45,15 @@ export const API = createAPIClient({
|
|||
}
|
||||
}
|
||||
},
|
||||
onMigrationDetected: appId => {
|
||||
const updatingUrl = `/builder/app/updating/${appId}`
|
||||
|
||||
if (window.location.pathname === updatingUrl) {
|
||||
return
|
||||
}
|
||||
|
||||
get(navigation).goto(
|
||||
`${updatingUrl}?returnUrl=${encodeURIComponent(window.location.pathname)}`
|
||||
)
|
||||
},
|
||||
})
|
||||
|
|
|
@ -1,156 +0,0 @@
|
|||
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 { getDeploymentStore } from "./store/deployments"
|
||||
import { derived, get } from "svelte/store"
|
||||
import { findComponent, findComponentPath } from "./componentUtils"
|
||||
import { RoleUtils } from "@budibase/frontend-core"
|
||||
import { createHistoryStore } from "builderStore/store/history"
|
||||
import { cloneDeep } from "lodash/fp"
|
||||
|
||||
export const store = getFrontendStore()
|
||||
export const automationStore = getAutomationStore()
|
||||
export const themeStore = getThemeStore()
|
||||
export const temporalStore = getTemporalStore()
|
||||
export const userStore = getUserStore()
|
||||
export const deploymentStore = getDeploymentStore()
|
||||
|
||||
// Setup history for screens
|
||||
export const screenHistoryStore = createHistoryStore({
|
||||
getDoc: id => get(store).screens?.find(screen => screen._id === id),
|
||||
selectDoc: store.actions.screens.select,
|
||||
afterAction: () => {
|
||||
// Ensure a valid component is selected
|
||||
if (!get(selectedComponent)) {
|
||||
store.update(state => ({
|
||||
...state,
|
||||
selectedComponentId: get(selectedScreen)?.props._id,
|
||||
}))
|
||||
}
|
||||
},
|
||||
})
|
||||
store.actions.screens.save = screenHistoryStore.wrapSaveDoc(
|
||||
store.actions.screens.save
|
||||
)
|
||||
store.actions.screens.delete = screenHistoryStore.wrapDeleteDoc(
|
||||
store.actions.screens.delete
|
||||
)
|
||||
|
||||
// Setup history for automations
|
||||
export const automationHistoryStore = createHistoryStore({
|
||||
getDoc: automationStore.actions.getDefinition,
|
||||
selectDoc: automationStore.actions.select,
|
||||
})
|
||||
automationStore.actions.save = automationHistoryStore.wrapSaveDoc(
|
||||
automationStore.actions.save
|
||||
)
|
||||
automationStore.actions.delete = automationHistoryStore.wrapDeleteDoc(
|
||||
automationStore.actions.delete
|
||||
)
|
||||
|
||||
export const selectedScreen = derived(store, $store => {
|
||||
return $store.screens.find(screen => screen._id === $store.selectedScreenId)
|
||||
})
|
||||
|
||||
export const selectedLayout = derived(store, $store => {
|
||||
return $store.layouts?.find(layout => layout._id === $store.selectedLayoutId)
|
||||
})
|
||||
|
||||
export const selectedComponent = derived(
|
||||
[store, selectedScreen],
|
||||
([$store, $selectedScreen]) => {
|
||||
if (
|
||||
$selectedScreen &&
|
||||
$store.selectedComponentId?.startsWith(`${$selectedScreen._id}-`)
|
||||
) {
|
||||
return $selectedScreen?.props
|
||||
}
|
||||
if (!$selectedScreen || !$store.selectedComponentId) {
|
||||
return null
|
||||
}
|
||||
const selected = findComponent(
|
||||
$selectedScreen?.props,
|
||||
$store.selectedComponentId
|
||||
)
|
||||
|
||||
const clone = selected ? cloneDeep(selected) : selected
|
||||
store.actions.components.migrateSettings(clone)
|
||||
return clone
|
||||
}
|
||||
)
|
||||
|
||||
// For legacy compatibility only, but with the new design UI this is just
|
||||
// the selected screen
|
||||
export const currentAsset = selectedScreen
|
||||
|
||||
export const sortedScreens = derived(store, $store => {
|
||||
return $store.screens.slice().sort((a, b) => {
|
||||
// Sort by role first
|
||||
const roleA = RoleUtils.getRolePriority(a.routing.roleId)
|
||||
const roleB = RoleUtils.getRolePriority(b.routing.roleId)
|
||||
if (roleA !== roleB) {
|
||||
return roleA > roleB ? -1 : 1
|
||||
}
|
||||
// Then put home screens first
|
||||
const homeA = !!a.routing.homeScreen
|
||||
const homeB = !!b.routing.homeScreen
|
||||
if (homeA !== homeB) {
|
||||
return homeA ? -1 : 1
|
||||
}
|
||||
// Then sort alphabetically by each URL param
|
||||
const aParams = a.routing.route.split("/")
|
||||
const bParams = b.routing.route.split("/")
|
||||
let minParams = Math.min(aParams.length, bParams.length)
|
||||
for (let i = 0; i < minParams; i++) {
|
||||
if (aParams[i] === bParams[i]) {
|
||||
continue
|
||||
}
|
||||
return aParams[i] < bParams[i] ? -1 : 1
|
||||
}
|
||||
// Then sort by the fewest amount of URL params
|
||||
return aParams.length < bParams.length ? -1 : 1
|
||||
})
|
||||
})
|
||||
|
||||
export const selectedComponentPath = derived(
|
||||
[store, selectedScreen],
|
||||
([$store, $selectedScreen]) => {
|
||||
return findComponentPath(
|
||||
$selectedScreen?.props,
|
||||
$store.selectedComponentId
|
||||
).map(component => component._id)
|
||||
}
|
||||
)
|
||||
|
||||
// Derived automation state
|
||||
export const selectedAutomation = derived(automationStore, $automationStore => {
|
||||
if (!$automationStore.selectedAutomationId) {
|
||||
return null
|
||||
}
|
||||
return $automationStore.automations?.find(
|
||||
x => x._id === $automationStore.selectedAutomationId
|
||||
)
|
||||
})
|
||||
|
||||
// Derive map of resource IDs to other users.
|
||||
// We only ever care about a single user in each resource, so if multiple users
|
||||
// share the same datasource we can just overwrite them.
|
||||
export const userSelectedResourceMap = derived(userStore, $userStore => {
|
||||
let map = {}
|
||||
$userStore.forEach(user => {
|
||||
const resource = user.builderMetadata?.selectedResourceId
|
||||
if (resource) {
|
||||
if (!map[resource]) {
|
||||
map[resource] = []
|
||||
}
|
||||
map[resource].push(user)
|
||||
}
|
||||
})
|
||||
return map
|
||||
})
|
||||
|
||||
export const isOnlyUser = derived(userStore, $userStore => {
|
||||
return $userStore.length < 2
|
||||
})
|
File diff suppressed because it is too large
Load Diff
|
@ -1,545 +0,0 @@
|
|||
import { expect, describe, it, vi } from "vitest"
|
||||
import {
|
||||
runtimeToReadableBinding,
|
||||
readableToRuntimeBinding,
|
||||
updateReferencesInObject,
|
||||
} from "../dataBinding"
|
||||
|
||||
vi.mock("@budibase/frontend-core")
|
||||
vi.mock("builderStore/componentUtils")
|
||||
vi.mock("builderStore/store")
|
||||
vi.mock("builderStore/store/theme")
|
||||
vi.mock("builderStore/store/temporal")
|
||||
|
||||
describe("runtimeToReadableBinding", () => {
|
||||
const bindableProperties = [
|
||||
{
|
||||
category: "Current User",
|
||||
icon: "User",
|
||||
providerId: "user",
|
||||
readableBinding: "Current User.firstName",
|
||||
runtimeBinding: "[user].[firstName]",
|
||||
type: "context",
|
||||
},
|
||||
{
|
||||
category: "Bindings",
|
||||
icon: "Brackets",
|
||||
readableBinding: "Binding.count",
|
||||
runtimeBinding: "count",
|
||||
type: "context",
|
||||
},
|
||||
]
|
||||
it("should convert a runtime binding to a readable one", () => {
|
||||
const textWithBindings = `Hello {{ [user].[firstName] }}! The count is {{ count }}.`
|
||||
expect(
|
||||
runtimeToReadableBinding(
|
||||
bindableProperties,
|
||||
textWithBindings,
|
||||
"readableBinding"
|
||||
)
|
||||
).toEqual(
|
||||
`Hello {{ Current User.firstName }}! The count is {{ Binding.count }}.`
|
||||
)
|
||||
})
|
||||
|
||||
it("should not convert to readable binding if it is already readable", () => {
|
||||
const textWithBindings = `Hello {{ [user].[firstName] }}! The count is {{ Binding.count }}.`
|
||||
expect(
|
||||
runtimeToReadableBinding(
|
||||
bindableProperties,
|
||||
textWithBindings,
|
||||
"readableBinding"
|
||||
)
|
||||
).toEqual(
|
||||
`Hello {{ Current User.firstName }}! The count is {{ Binding.count }}.`
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe("readableToRuntimeBinding", () => {
|
||||
const bindableProperties = [
|
||||
{
|
||||
category: "Current User",
|
||||
icon: "User",
|
||||
providerId: "user",
|
||||
readableBinding: "Current User.firstName",
|
||||
runtimeBinding: "[user].[firstName]",
|
||||
type: "context",
|
||||
},
|
||||
{
|
||||
category: "Bindings",
|
||||
icon: "Brackets",
|
||||
readableBinding: "Binding.count",
|
||||
runtimeBinding: "count",
|
||||
type: "context",
|
||||
},
|
||||
]
|
||||
it("should convert a readable binding to a runtime one", () => {
|
||||
const textWithBindings = `Hello {{ Current User.firstName }}! The count is {{ Binding.count }}.`
|
||||
expect(
|
||||
readableToRuntimeBinding(
|
||||
bindableProperties,
|
||||
textWithBindings,
|
||||
"runtimeBinding"
|
||||
)
|
||||
).toEqual(`Hello {{ [user].[firstName] }}! The count is {{ count }}.`)
|
||||
})
|
||||
})
|
||||
|
||||
describe("updateReferencesInObject", () => {
|
||||
it("should increment steps in sequence on 'add'", () => {
|
||||
let obj = [
|
||||
{
|
||||
id: "a0",
|
||||
parameters: {
|
||||
text: "Alpha",
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "a1",
|
||||
parameters: {
|
||||
text: "Apple",
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "b2",
|
||||
parameters: {
|
||||
text: "Banana {{ actions.1.row }}",
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "c3",
|
||||
parameters: {
|
||||
text: "Carrot {{ actions.1.row }}",
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "d4",
|
||||
parameters: {
|
||||
text: "Dog {{ actions.3.row }}",
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "e5",
|
||||
parameters: {
|
||||
text: "Eagle {{ actions.4.row }}",
|
||||
},
|
||||
},
|
||||
]
|
||||
updateReferencesInObject({
|
||||
obj,
|
||||
modifiedIndex: 0,
|
||||
action: "add",
|
||||
label: "actions",
|
||||
})
|
||||
|
||||
expect(obj).toEqual([
|
||||
{
|
||||
id: "a0",
|
||||
parameters: {
|
||||
text: "Alpha",
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "a1",
|
||||
parameters: {
|
||||
text: "Apple",
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "b2",
|
||||
parameters: {
|
||||
text: "Banana {{ actions.2.row }}",
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "c3",
|
||||
parameters: {
|
||||
text: "Carrot {{ actions.2.row }}",
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "d4",
|
||||
parameters: {
|
||||
text: "Dog {{ actions.4.row }}",
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "e5",
|
||||
parameters: {
|
||||
text: "Eagle {{ actions.5.row }}",
|
||||
},
|
||||
},
|
||||
])
|
||||
})
|
||||
|
||||
it("should decrement steps in sequence on 'delete'", () => {
|
||||
let obj = [
|
||||
{
|
||||
id: "a1",
|
||||
parameters: {
|
||||
text: "Apple",
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "b2",
|
||||
parameters: {
|
||||
text: "Banana {{ actions.1.row }}",
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "d4",
|
||||
parameters: {
|
||||
text: "Dog {{ actions.3.row }}",
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "e5",
|
||||
parameters: {
|
||||
text: "Eagle {{ actions.4.row }}",
|
||||
},
|
||||
},
|
||||
]
|
||||
updateReferencesInObject({
|
||||
obj,
|
||||
modifiedIndex: 2,
|
||||
action: "delete",
|
||||
label: "actions",
|
||||
})
|
||||
|
||||
expect(obj).toEqual([
|
||||
{
|
||||
id: "a1",
|
||||
parameters: {
|
||||
text: "Apple",
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "b2",
|
||||
parameters: {
|
||||
text: "Banana {{ actions.1.row }}",
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "d4",
|
||||
parameters: {
|
||||
text: "Dog {{ actions.2.row }}",
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "e5",
|
||||
parameters: {
|
||||
text: "Eagle {{ actions.3.row }}",
|
||||
},
|
||||
},
|
||||
])
|
||||
})
|
||||
|
||||
it("should handle on 'move' to a lower index", () => {
|
||||
let obj = [
|
||||
{
|
||||
id: "a1",
|
||||
parameters: {
|
||||
text: "Apple",
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "b2",
|
||||
parameters: {
|
||||
text: "Banana {{ actions.0.row }}",
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "e5",
|
||||
parameters: {
|
||||
text: "Eagle {{ actions.3.row }}",
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "c3",
|
||||
parameters: {
|
||||
text: "Carrot {{ actions.0.row }}",
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "d4",
|
||||
parameters: {
|
||||
text: "Dog {{ actions.2.row }}",
|
||||
},
|
||||
},
|
||||
]
|
||||
updateReferencesInObject({
|
||||
obj,
|
||||
modifiedIndex: 2,
|
||||
action: "move",
|
||||
label: "actions",
|
||||
originalIndex: 4,
|
||||
})
|
||||
|
||||
expect(obj).toEqual([
|
||||
{
|
||||
id: "a1",
|
||||
parameters: {
|
||||
text: "Apple",
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "b2",
|
||||
parameters: {
|
||||
text: "Banana {{ actions.0.row }}",
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "e5",
|
||||
parameters: {
|
||||
text: "Eagle {{ actions.4.row }}",
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "c3",
|
||||
parameters: {
|
||||
text: "Carrot {{ actions.0.row }}",
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "d4",
|
||||
parameters: {
|
||||
text: "Dog {{ actions.3.row }}",
|
||||
},
|
||||
},
|
||||
])
|
||||
})
|
||||
|
||||
it("should handle on 'move' to a higher index", () => {
|
||||
let obj = [
|
||||
{
|
||||
id: "b2",
|
||||
parameters: {
|
||||
text: "Banana {{ actions.0.row }}",
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "c3",
|
||||
parameters: {
|
||||
text: "Carrot {{ actions.0.row }}",
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "a1",
|
||||
parameters: {
|
||||
text: "Apple",
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "d4",
|
||||
parameters: {
|
||||
text: "Dog {{ actions.2.row }}",
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "e5",
|
||||
parameters: {
|
||||
text: "Eagle {{ actions.3.row }}",
|
||||
},
|
||||
},
|
||||
]
|
||||
updateReferencesInObject({
|
||||
obj,
|
||||
modifiedIndex: 2,
|
||||
action: "move",
|
||||
label: "actions",
|
||||
originalIndex: 0,
|
||||
})
|
||||
|
||||
expect(obj).toEqual([
|
||||
{
|
||||
id: "b2",
|
||||
parameters: {
|
||||
text: "Banana {{ actions.2.row }}",
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "c3",
|
||||
parameters: {
|
||||
text: "Carrot {{ actions.2.row }}",
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "a1",
|
||||
parameters: {
|
||||
text: "Apple",
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "d4",
|
||||
parameters: {
|
||||
text: "Dog {{ actions.1.row }}",
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "e5",
|
||||
parameters: {
|
||||
text: "Eagle {{ actions.3.row }}",
|
||||
},
|
||||
},
|
||||
])
|
||||
})
|
||||
|
||||
it("should handle on 'move' of action being referenced, dragged to a higher index", () => {
|
||||
let obj = [
|
||||
{
|
||||
"##eventHandlerType": "Validate Form",
|
||||
id: "cCD0Dwcnq",
|
||||
},
|
||||
{
|
||||
"##eventHandlerType": "Close Screen Modal",
|
||||
id: "3fbbIOfN0H",
|
||||
},
|
||||
{
|
||||
"##eventHandlerType": "Save Row",
|
||||
parameters: {
|
||||
tableId: "ta_bb_employee",
|
||||
},
|
||||
id: "aehg5cTmhR",
|
||||
},
|
||||
{
|
||||
"##eventHandlerType": "Close Side Panel",
|
||||
id: "mzkpf86cxo",
|
||||
},
|
||||
{
|
||||
"##eventHandlerType": "Navigate To",
|
||||
id: "h0uDFeJa8A",
|
||||
},
|
||||
{
|
||||
parameters: {
|
||||
autoDismiss: true,
|
||||
type: "success",
|
||||
message: "{{ actions.1.row }}",
|
||||
},
|
||||
"##eventHandlerType": "Show Notification",
|
||||
id: "JEI5lAyJZ",
|
||||
},
|
||||
]
|
||||
updateReferencesInObject({
|
||||
obj,
|
||||
modifiedIndex: 2,
|
||||
action: "move",
|
||||
label: "actions",
|
||||
originalIndex: 1,
|
||||
})
|
||||
|
||||
expect(obj).toEqual([
|
||||
{
|
||||
"##eventHandlerType": "Validate Form",
|
||||
id: "cCD0Dwcnq",
|
||||
},
|
||||
{
|
||||
"##eventHandlerType": "Close Screen Modal",
|
||||
id: "3fbbIOfN0H",
|
||||
},
|
||||
{
|
||||
"##eventHandlerType": "Save Row",
|
||||
parameters: {
|
||||
tableId: "ta_bb_employee",
|
||||
},
|
||||
id: "aehg5cTmhR",
|
||||
},
|
||||
{
|
||||
"##eventHandlerType": "Close Side Panel",
|
||||
id: "mzkpf86cxo",
|
||||
},
|
||||
{
|
||||
"##eventHandlerType": "Navigate To",
|
||||
id: "h0uDFeJa8A",
|
||||
},
|
||||
{
|
||||
parameters: {
|
||||
autoDismiss: true,
|
||||
type: "success",
|
||||
message: "{{ actions.2.row }}",
|
||||
},
|
||||
"##eventHandlerType": "Show Notification",
|
||||
id: "JEI5lAyJZ",
|
||||
},
|
||||
])
|
||||
})
|
||||
|
||||
it("should handle on 'move' of action being referenced, dragged to a lower index", () => {
|
||||
let obj = [
|
||||
{
|
||||
"##eventHandlerType": "Save Row",
|
||||
parameters: {
|
||||
tableId: "ta_bb_employee",
|
||||
},
|
||||
id: "aehg5cTmhR",
|
||||
},
|
||||
{
|
||||
"##eventHandlerType": "Validate Form",
|
||||
id: "cCD0Dwcnq",
|
||||
},
|
||||
{
|
||||
"##eventHandlerType": "Close Screen Modal",
|
||||
id: "3fbbIOfN0H",
|
||||
},
|
||||
{
|
||||
"##eventHandlerType": "Close Side Panel",
|
||||
id: "mzkpf86cxo",
|
||||
},
|
||||
{
|
||||
"##eventHandlerType": "Navigate To",
|
||||
id: "h0uDFeJa8A",
|
||||
},
|
||||
{
|
||||
parameters: {
|
||||
autoDismiss: true,
|
||||
type: "success",
|
||||
message: "{{ actions.4.row }}",
|
||||
},
|
||||
"##eventHandlerType": "Show Notification",
|
||||
id: "JEI5lAyJZ",
|
||||
},
|
||||
]
|
||||
updateReferencesInObject({
|
||||
obj,
|
||||
modifiedIndex: 0,
|
||||
action: "move",
|
||||
label: "actions",
|
||||
originalIndex: 4,
|
||||
})
|
||||
|
||||
expect(obj).toEqual([
|
||||
{
|
||||
"##eventHandlerType": "Save Row",
|
||||
parameters: {
|
||||
tableId: "ta_bb_employee",
|
||||
},
|
||||
id: "aehg5cTmhR",
|
||||
},
|
||||
{
|
||||
"##eventHandlerType": "Validate Form",
|
||||
id: "cCD0Dwcnq",
|
||||
},
|
||||
{
|
||||
"##eventHandlerType": "Close Screen Modal",
|
||||
id: "3fbbIOfN0H",
|
||||
},
|
||||
{
|
||||
"##eventHandlerType": "Close Side Panel",
|
||||
id: "mzkpf86cxo",
|
||||
},
|
||||
{
|
||||
"##eventHandlerType": "Navigate To",
|
||||
id: "h0uDFeJa8A",
|
||||
},
|
||||
{
|
||||
parameters: {
|
||||
autoDismiss: true,
|
||||
type: "success",
|
||||
message: "{{ actions.0.row }}",
|
||||
},
|
||||
"##eventHandlerType": "Show Notification",
|
||||
id: "JEI5lAyJZ",
|
||||
},
|
||||
])
|
||||
})
|
||||
})
|
|
@ -1,5 +1,5 @@
|
|||
<script>
|
||||
import { selectedAutomation } from "builderStore"
|
||||
import { selectedAutomation } from "stores/builder"
|
||||
import Flowchart from "./FlowChart/FlowChart.svelte"
|
||||
</script>
|
||||
|
||||
|
|
|
@ -9,20 +9,25 @@
|
|||
Tags,
|
||||
Tag,
|
||||
} from "@budibase/bbui"
|
||||
import { automationStore, selectedAutomation } from "builderStore"
|
||||
import { automationStore, selectedAutomation } from "stores/builder"
|
||||
import { admin, licensing } from "stores/portal"
|
||||
import { externalActions } from "./ExternalActions"
|
||||
import { TriggerStepID, ActionStepID } from "constants/backend/automations"
|
||||
import { checkForCollectStep } from "builderStore/utils"
|
||||
import { checkForCollectStep } from "helpers/utils"
|
||||
|
||||
export let blockIdx
|
||||
export let lastStep
|
||||
|
||||
let syncAutomationsEnabled = $licensing.syncAutomationsEnabled
|
||||
let triggerAutomationRunEnabled = $licensing.triggerAutomationRunEnabled
|
||||
let collectBlockAllowedSteps = [TriggerStepID.APP, TriggerStepID.WEBHOOK]
|
||||
let selectedAction
|
||||
let actionVal
|
||||
let actions = Object.entries($automationStore.blockDefinitions.ACTION)
|
||||
let lockedFeatures = [
|
||||
ActionStepID.COLLECT,
|
||||
ActionStepID.TRIGGER_AUTOMATION_RUN,
|
||||
]
|
||||
|
||||
$: collectBlockExists = checkForCollectStep($selectedAutomation)
|
||||
|
||||
|
@ -36,6 +41,10 @@
|
|||
disabled: !lastStep || !syncAutomationsEnabled || collectBlockExists,
|
||||
message: collectDisabledMessage(),
|
||||
},
|
||||
TRIGGER_AUTOMATION_RUN: {
|
||||
disabled: !triggerAutomationRunEnabled,
|
||||
message: "Please upgrade to a paid plan",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -119,10 +128,10 @@
|
|||
>
|
||||
<div class="item-body">
|
||||
<img
|
||||
width="20"
|
||||
height="20"
|
||||
width={20}
|
||||
height={20}
|
||||
src={externalActions[action.stepId].icon}
|
||||
alt="zapier"
|
||||
alt={externalActions[action.stepId].name}
|
||||
/>
|
||||
<span class="icon-spacing">
|
||||
<Body size="XS">
|
||||
|
@ -149,10 +158,10 @@
|
|||
<div class="item-body">
|
||||
<Icon name={action.icon} />
|
||||
<Body size="XS">{action.name}</Body>
|
||||
{#if isDisabled && !syncAutomationsEnabled && action.stepId === ActionStepID.COLLECT}
|
||||
{#if isDisabled && !syncAutomationsEnabled && !triggerAutomationRunEnabled && lockedFeatures.includes(action.stepId)}
|
||||
<div class="tag-color">
|
||||
<Tags>
|
||||
<Tag icon="LockClosed">Business</Tag>
|
||||
<Tag icon="LockClosed">Premium</Tag>
|
||||
</Tags>
|
||||
</div>
|
||||
{:else if isDisabled}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<script>
|
||||
import { processStringSync } from "@budibase/string-templates"
|
||||
import { get } from "lodash/fp"
|
||||
import { tables } from "stores/backend"
|
||||
import { tables } from "stores/builder"
|
||||
|
||||
export let block
|
||||
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import DiscordLogo from "assets/discord.svg"
|
||||
import ZapierLogo from "assets/zapier.png"
|
||||
import n8nLogo from "assets/n8n_square.png"
|
||||
import MakeLogo from "assets/make.svg"
|
||||
import SlackLogo from "assets/slack.svg"
|
||||
|
||||
|
@ -8,4 +9,5 @@ export const externalActions = {
|
|||
discord: { name: "discord", icon: DiscordLogo },
|
||||
slack: { name: "slack", icon: SlackLogo },
|
||||
integromat: { name: "integromat", icon: MakeLogo },
|
||||
n8n: { name: "n8n", icon: n8nLogo },
|
||||
}
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
automationStore,
|
||||
selectedAutomation,
|
||||
automationHistoryStore,
|
||||
} from "builderStore"
|
||||
} from "stores/builder"
|
||||
import ConfirmDialog from "components/common/ConfirmDialog.svelte"
|
||||
import FlowItem from "./FlowItem.svelte"
|
||||
import TestDataModal from "./TestDataModal.svelte"
|
||||
|
|
|
@ -1,5 +1,9 @@
|
|||
<script>
|
||||
import { automationStore, selectedAutomation } from "builderStore"
|
||||
import {
|
||||
automationStore,
|
||||
selectedAutomation,
|
||||
permissions,
|
||||
} from "stores/builder"
|
||||
import {
|
||||
Icon,
|
||||
Divider,
|
||||
|
@ -17,7 +21,6 @@
|
|||
import FlowItemHeader from "./FlowItemHeader.svelte"
|
||||
import RoleSelect from "components/design/settings/controls/RoleSelect.svelte"
|
||||
import { ActionStepID, TriggerStepID } from "constants/backend/automations"
|
||||
import { permissions } from "stores/backend"
|
||||
|
||||
export let block
|
||||
export let testDataModal
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<script>
|
||||
import { automationStore, selectedAutomation } from "builderStore"
|
||||
import { Icon, Body, StatusLight, AbsTooltip } from "@budibase/bbui"
|
||||
import { automationStore, selectedAutomation } from "stores/builder"
|
||||
import { Icon, Body, AbsTooltip, StatusLight } from "@budibase/bbui"
|
||||
import { externalActions } from "./ExternalActions"
|
||||
import { createEventDispatcher } from "svelte"
|
||||
import { Features } from "constants/backend/automations"
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
notifications,
|
||||
ActionButton,
|
||||
} from "@budibase/bbui"
|
||||
import { automationStore, selectedAutomation } from "builderStore"
|
||||
import { automationStore, selectedAutomation } from "stores/builder"
|
||||
import AutomationBlockSetup from "../../SetupPanel/AutomationBlockSetup.svelte"
|
||||
import { cloneDeep } from "lodash/fp"
|
||||
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
<script>
|
||||
import { Icon, Divider, Tabs, Tab, TextArea, Label } from "@budibase/bbui"
|
||||
import { Icon, Divider, Tabs, Tab, Label } from "@budibase/bbui"
|
||||
import FlowItemHeader from "./FlowChart/FlowItemHeader.svelte"
|
||||
import { ActionStepID } from "constants/backend/automations"
|
||||
import { JsonView } from "@zerodevx/svelte-json-view"
|
||||
|
||||
export let automation
|
||||
export let testResults
|
||||
|
@ -14,13 +15,6 @@
|
|||
return results?.steps.filter(x => x.stepId !== ActionStepID.LOOP || [])
|
||||
}
|
||||
|
||||
function textArea(results, message) {
|
||||
if (!results) {
|
||||
return message
|
||||
}
|
||||
return JSON.stringify(results, null, 2)
|
||||
}
|
||||
|
||||
$: filteredResults = prepTestResults(testResults)
|
||||
|
||||
$: {
|
||||
|
@ -71,26 +65,34 @@
|
|||
{/if}
|
||||
|
||||
<div class="tabs">
|
||||
<Tabs noHorizPadding selected="Input">
|
||||
<Tabs quiet noHorizPadding selected="Input">
|
||||
<Tab title="Input">
|
||||
<TextArea
|
||||
minHeight="160px"
|
||||
disabled
|
||||
value={textArea(filteredResults?.[idx]?.inputs, "No input")}
|
||||
/>
|
||||
<div class="wrap">
|
||||
{#if filteredResults?.[idx]?.inputs}
|
||||
<JsonView depth={2} json={filteredResults?.[idx]?.inputs} />
|
||||
{:else}
|
||||
No input
|
||||
{/if}
|
||||
</div>
|
||||
</Tab>
|
||||
<Tab title="Output">
|
||||
<TextArea
|
||||
minHeight="160px"
|
||||
disabled
|
||||
value={textArea(filteredResults?.[idx]?.outputs, "No output")}
|
||||
/>
|
||||
<div class="wrap">
|
||||
{#if filteredResults?.[idx]?.outputs}
|
||||
<JsonView
|
||||
depth={2}
|
||||
json={filteredResults?.[idx]?.outputs}
|
||||
/>
|
||||
{:else}
|
||||
No input
|
||||
{/if}
|
||||
</div>
|
||||
</Tab>
|
||||
</Tabs>
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{#if blocks.length - 1 !== idx}
|
||||
<div class="separator" />
|
||||
{/if}
|
||||
|
@ -104,6 +106,33 @@
|
|||
overflow: auto;
|
||||
}
|
||||
|
||||
.wrap {
|
||||
font-family: monospace;
|
||||
background-color: var(
|
||||
--spectrum-textfield-m-background-color,
|
||||
var(--spectrum-global-color-gray-50)
|
||||
);
|
||||
border: 1px solid var(--spectrum-global-color-gray-300);
|
||||
font-size: 12px;
|
||||
max-height: 160px; /* Adjusted max-height */
|
||||
height: 160px;
|
||||
--jsonPaddingLeft: 20px;
|
||||
--jsonborderleft: 20px;
|
||||
overflow: auto;
|
||||
overflow: overlay;
|
||||
overflow-x: hidden;
|
||||
padding-left: var(--spacing-s);
|
||||
padding-top: var(--spacing-xl);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.wrap::-webkit-scrollbar {
|
||||
width: 7px; /* width of the scrollbar */
|
||||
}
|
||||
|
||||
.wrap::-webkit-scrollbar-track {
|
||||
background: transparent; /* transparent track */
|
||||
}
|
||||
.tabs {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<script>
|
||||
import { Icon, Divider } from "@budibase/bbui"
|
||||
import TestDisplay from "./TestDisplay.svelte"
|
||||
import { automationStore } from "builderStore"
|
||||
import { automationStore } from "stores/builder"
|
||||
|
||||
export let automation
|
||||
</script>
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
automationStore,
|
||||
selectedAutomation,
|
||||
userSelectedResourceMap,
|
||||
} from "builderStore"
|
||||
} from "stores/builder"
|
||||
import NavItem from "components/common/NavItem.svelte"
|
||||
import EditAutomationPopover from "./EditAutomationPopover.svelte"
|
||||
import { notifications } from "@budibase/bbui"
|
||||
|
|
|
@ -1,44 +1,117 @@
|
|||
<script>
|
||||
import AutomationList from "./AutomationList.svelte"
|
||||
import CreateAutomationModal from "./CreateAutomationModal.svelte"
|
||||
import { Modal, Icon } from "@budibase/bbui"
|
||||
import Panel from "components/design/Panel.svelte"
|
||||
import { Modal, notifications, Layout } from "@budibase/bbui"
|
||||
import NavHeader from "components/common/NavHeader.svelte"
|
||||
import { onMount } from "svelte"
|
||||
import {
|
||||
automationStore,
|
||||
selectedAutomation,
|
||||
userSelectedResourceMap,
|
||||
} from "stores/builder"
|
||||
import NavItem from "components/common/NavItem.svelte"
|
||||
import EditAutomationPopover from "./EditAutomationPopover.svelte"
|
||||
|
||||
export let modal
|
||||
export let webhookModal
|
||||
let searchString
|
||||
|
||||
$: selectedAutomationId = $selectedAutomation?._id
|
||||
|
||||
$: filteredAutomations = $automationStore.automations
|
||||
.filter(automation => {
|
||||
return (
|
||||
!searchString ||
|
||||
automation.name.toLowerCase().includes(searchString.toLowerCase())
|
||||
)
|
||||
})
|
||||
.sort((a, b) => {
|
||||
const lowerA = a.name.toLowerCase()
|
||||
const lowerB = b.name.toLowerCase()
|
||||
return lowerA > lowerB ? 1 : -1
|
||||
})
|
||||
|
||||
$: showNoResults = searchString && !filteredAutomations.length
|
||||
|
||||
onMount(async () => {
|
||||
try {
|
||||
await automationStore.actions.fetch()
|
||||
} catch (error) {
|
||||
notifications.error("Error getting automations list")
|
||||
}
|
||||
})
|
||||
|
||||
function selectAutomation(id) {
|
||||
automationStore.actions.select(id)
|
||||
}
|
||||
</script>
|
||||
|
||||
<Panel title="Automations" borderRight noHeaderBorder titleCSS={false}>
|
||||
<span class="panel-title-content" slot="panel-title-content">
|
||||
<div class="header">
|
||||
<div>Automations</div>
|
||||
<div on:click={modal.show} class="add-automation-button">
|
||||
<Icon name="Add" />
|
||||
</div>
|
||||
</div>
|
||||
</span>
|
||||
<AutomationList />
|
||||
</Panel>
|
||||
<div class="side-bar">
|
||||
<div class="side-bar-controls">
|
||||
<NavHeader
|
||||
title="Automations"
|
||||
placeholder="Search for automation"
|
||||
bind:value={searchString}
|
||||
onAdd={() => modal.show()}
|
||||
/>
|
||||
</div>
|
||||
<div class="side-bar-nav">
|
||||
{#each filteredAutomations as automation}
|
||||
<NavItem
|
||||
text={automation.name}
|
||||
selected={automation._id === selectedAutomationId}
|
||||
on:click={() => selectAutomation(automation._id)}
|
||||
selectedBy={$userSelectedResourceMap[automation._id]}
|
||||
>
|
||||
<EditAutomationPopover {automation} />
|
||||
</NavItem>
|
||||
{/each}
|
||||
|
||||
{#if showNoResults}
|
||||
<Layout paddingY="none" paddingX="L">
|
||||
<div class="no-results">
|
||||
There aren't any automations matching that name
|
||||
</div>
|
||||
</Layout>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Modal bind:this={modal}>
|
||||
<CreateAutomationModal {webhookModal} />
|
||||
</Modal>
|
||||
|
||||
<style>
|
||||
.header {
|
||||
.side-bar {
|
||||
flex: 0 0 260px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
border-right: var(--border-light);
|
||||
background: var(--spectrum-global-color-gray-100);
|
||||
overflow: hidden;
|
||||
transition: margin-left 300ms ease-out;
|
||||
}
|
||||
@media (max-width: 640px) {
|
||||
.side-bar {
|
||||
margin-left: -262px;
|
||||
}
|
||||
}
|
||||
|
||||
.side-bar-controls {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: var(--spacing-m);
|
||||
gap: var(--spacing-l);
|
||||
padding: 0 var(--spacing-l);
|
||||
}
|
||||
.side-bar-nav {
|
||||
flex: 1 1 auto;
|
||||
overflow: auto;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
.add-automation-button {
|
||||
margin-left: 130px;
|
||||
color: var(--grey-7);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.add-automation-button:hover {
|
||||
color: var(--ink);
|
||||
.no-results {
|
||||
color: var(--spectrum-global-color-gray-600);
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<script>
|
||||
import { automationStore } from "builderStore"
|
||||
import { automationStore } from "stores/builder"
|
||||
import {
|
||||
notifications,
|
||||
Input,
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<script>
|
||||
import { automationStore } from "builderStore"
|
||||
import { automationStore } from "stores/builder"
|
||||
import { ActionMenu, MenuItem, notifications, Icon } from "@budibase/bbui"
|
||||
import ConfirmDialog from "components/common/ConfirmDialog.svelte"
|
||||
import UpdateAutomationModal from "components/automation/AutomationPanel/UpdateAutomationModal.svelte"
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<script>
|
||||
import { automationStore } from "builderStore"
|
||||
import { automationStore } from "stores/builder"
|
||||
import {
|
||||
notifications,
|
||||
Icon,
|
||||
|
|
|
@ -15,11 +15,9 @@
|
|||
Icon,
|
||||
Checkbox,
|
||||
DatePicker,
|
||||
Detail,
|
||||
} from "@budibase/bbui"
|
||||
import CreateWebhookModal from "components/automation/Shared/CreateWebhookModal.svelte"
|
||||
import { automationStore, selectedAutomation } from "builderStore"
|
||||
import { tables } from "stores/backend"
|
||||
import { automationStore, selectedAutomation, tables } from "stores/builder"
|
||||
import { environment, licensing } from "stores/portal"
|
||||
import WebhookDisplay from "../Shared/WebhookDisplay.svelte"
|
||||
import DrawerBindableInput from "../../common/bindings/DrawerBindableInput.svelte"
|
||||
|
@ -28,10 +26,13 @@
|
|||
import CodeEditorModal from "./CodeEditorModal.svelte"
|
||||
import QuerySelector from "./QuerySelector.svelte"
|
||||
import QueryParamSelector from "./QueryParamSelector.svelte"
|
||||
import AutomationSelector from "./AutomationSelector.svelte"
|
||||
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 BindingPicker from "components/common/bindings/BindingPicker.svelte"
|
||||
import { BindingHelpers } from "components/common/bindings/utils"
|
||||
import {
|
||||
bindingsToCompletions,
|
||||
hbAutocomplete,
|
||||
|
@ -42,7 +43,7 @@
|
|||
import {
|
||||
getSchemaForDatasourcePlus,
|
||||
getEnvironmentBindings,
|
||||
} from "builderStore/dataBinding"
|
||||
} from "dataBinding"
|
||||
import { TriggerStepID, ActionStepID } from "constants/backend/automations"
|
||||
import { onMount } from "svelte"
|
||||
import { cloneDeep } from "lodash/fp"
|
||||
|
@ -51,12 +52,11 @@
|
|||
export let testData
|
||||
export let schemaProperties
|
||||
export let isTestModal = false
|
||||
|
||||
let webhookModal
|
||||
let drawer
|
||||
let fillWidth = true
|
||||
let inputData
|
||||
let codeBindingOpen = false
|
||||
let insertAtPos, getCaretPosition
|
||||
$: filters = lookForFilters(schemaProperties) || []
|
||||
$: tempFilters = filters
|
||||
$: stepId = block.stepId
|
||||
|
@ -75,6 +75,11 @@
|
|||
$: isUpdateRow = stepId === ActionStepID.UPDATE_ROW
|
||||
$: codeMode =
|
||||
stepId === "EXECUTE_BASH" ? EditorModes.Handlebars : EditorModes.JS
|
||||
$: bindingsHelpers = new BindingHelpers(getCaretPosition, insertAtPos, {
|
||||
disableWrapping: true,
|
||||
})
|
||||
$: editingJs = codeMode === EditorModes.JS
|
||||
$: requiredProperties = block.schema.inputs.required || []
|
||||
|
||||
$: stepCompletions =
|
||||
codeMode === EditorModes.Handlebars
|
||||
|
@ -101,7 +106,6 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
const onChange = Utils.sequential(async (e, key) => {
|
||||
// We need to cache the schema as part of the definition because it is
|
||||
// used in the server to detect relationships. It would be far better to
|
||||
|
@ -145,6 +149,7 @@
|
|||
if (!block || !automation) {
|
||||
return []
|
||||
}
|
||||
|
||||
// Find previous steps to the selected one
|
||||
let allSteps = [...automation.steps]
|
||||
|
||||
|
@ -156,22 +161,98 @@
|
|||
// Extract all outputs from all previous steps as available bindingsx§x
|
||||
let bindings = []
|
||||
let loopBlockCount = 0
|
||||
const addBinding = (name, value, icon, idx, isLoopBlock, bindingName) => {
|
||||
if (!name) return
|
||||
const runtimeBinding = determineRuntimeBinding(name, idx, isLoopBlock)
|
||||
const categoryName = determineCategoryName(idx, isLoopBlock, bindingName)
|
||||
|
||||
bindings.push(
|
||||
createBindingObject(
|
||||
name,
|
||||
value,
|
||||
icon,
|
||||
idx,
|
||||
loopBlockCount,
|
||||
isLoopBlock,
|
||||
runtimeBinding,
|
||||
categoryName,
|
||||
bindingName
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
const determineRuntimeBinding = (name, idx, isLoopBlock) => {
|
||||
let runtimeName
|
||||
|
||||
/* Begin special cases for generating custom schemas based on triggers */
|
||||
if (idx === 0 && automation.trigger?.event === "app:trigger") {
|
||||
return `trigger.fields.${name}`
|
||||
}
|
||||
|
||||
if (
|
||||
idx === 0 &&
|
||||
(automation.trigger?.event === "row:update" ||
|
||||
automation.trigger?.event === "row:save")
|
||||
) {
|
||||
if (name !== "id" && name !== "revision") return `trigger.row.${name}`
|
||||
}
|
||||
/* End special cases for generating custom schemas based on triggers */
|
||||
|
||||
if (isLoopBlock) {
|
||||
runtimeName = `loop.${name}`
|
||||
} else if (block.name.startsWith("JS")) {
|
||||
runtimeName = `steps[${idx - loopBlockCount}].${name}`
|
||||
} else {
|
||||
runtimeName = `steps.${idx - loopBlockCount}.${name}`
|
||||
}
|
||||
return idx === 0 ? `trigger.${name}` : runtimeName
|
||||
}
|
||||
|
||||
const determineCategoryName = (idx, isLoopBlock, bindingName) => {
|
||||
if (idx === 0) return "Trigger outputs"
|
||||
if (isLoopBlock) return "Loop Outputs"
|
||||
return bindingName
|
||||
? `${bindingName} outputs`
|
||||
: `Step ${idx - loopBlockCount} outputs`
|
||||
}
|
||||
|
||||
const createBindingObject = (
|
||||
name,
|
||||
value,
|
||||
icon,
|
||||
idx,
|
||||
loopBlockCount,
|
||||
isLoopBlock,
|
||||
runtimeBinding,
|
||||
categoryName,
|
||||
bindingName
|
||||
) => {
|
||||
return {
|
||||
readableBinding: bindingName
|
||||
? `${bindingName}.${name}`
|
||||
: runtimeBinding,
|
||||
runtimeBinding,
|
||||
type: value.type,
|
||||
description: value.description,
|
||||
icon,
|
||||
category: categoryName,
|
||||
display: {
|
||||
type: value.type,
|
||||
name,
|
||||
rank: isLoopBlock ? idx + 1 : idx - loopBlockCount,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
for (let idx = 0; idx < blockIdx; idx++) {
|
||||
let wasLoopBlock = allSteps[idx - 1]?.stepId === ActionStepID.LOOP
|
||||
let isLoopBlock =
|
||||
allSteps[idx]?.stepId === ActionStepID.LOOP &&
|
||||
allSteps.find(x => x.blockToLoop === block.id)
|
||||
allSteps.some(x => x.blockToLoop === block.id)
|
||||
let schema = cloneDeep(allSteps[idx]?.schema?.outputs?.properties) ?? {}
|
||||
let bindingName =
|
||||
automation.stepNames?.[allSteps[idx - loopBlockCount].id]
|
||||
|
||||
// If the previous block was a loop block, decrement the index so the following
|
||||
// steps are in the correct order
|
||||
if (wasLoopBlock) {
|
||||
loopBlockCount++
|
||||
continue
|
||||
}
|
||||
|
||||
let schema = allSteps[idx]?.schema?.outputs?.properties ?? {}
|
||||
|
||||
// If its a Loop Block, we need to add this custom schema
|
||||
if (isLoopBlock) {
|
||||
schema = {
|
||||
currentItem: {
|
||||
|
@ -180,54 +261,44 @@
|
|||
},
|
||||
}
|
||||
}
|
||||
const outputs = Object.entries(schema)
|
||||
let bindingIcon = ""
|
||||
let bindingRank = 0
|
||||
if (idx === 0) {
|
||||
bindingIcon = automation.trigger.icon
|
||||
} else if (isLoopBlock) {
|
||||
bindingIcon = "Reuse"
|
||||
bindingRank = idx + 1
|
||||
} else {
|
||||
bindingIcon = allSteps[idx].icon
|
||||
bindingRank = idx - loopBlockCount
|
||||
|
||||
if (idx === 0 && automation.trigger?.event === "app:trigger") {
|
||||
schema = Object.fromEntries(
|
||||
Object.keys(automation.trigger.inputs.fields || []).map(key => [
|
||||
key,
|
||||
{ type: automation.trigger.inputs.fields[key] },
|
||||
])
|
||||
)
|
||||
}
|
||||
let bindingName =
|
||||
automation.stepNames?.[allSteps[idx - loopBlockCount].id]
|
||||
bindings = bindings.concat(
|
||||
outputs.map(([name, value]) => {
|
||||
let runtimeName = isLoopBlock
|
||||
? `loop.${name}`
|
||||
: block.name.startsWith("JS")
|
||||
? `steps[${idx - loopBlockCount}].${name}`
|
||||
: `steps.${idx - loopBlockCount}.${name}`
|
||||
const runtime = idx === 0 ? `trigger.${name}` : runtimeName
|
||||
|
||||
let categoryName
|
||||
if (idx === 0) {
|
||||
categoryName = "Trigger outputs"
|
||||
} else if (isLoopBlock) {
|
||||
categoryName = "Loop Outputs"
|
||||
} else if (bindingName) {
|
||||
categoryName = `${bindingName} outputs`
|
||||
} else {
|
||||
categoryName = `Step ${idx - loopBlockCount} outputs`
|
||||
if (
|
||||
(idx === 0 && automation.trigger.event === "row:update") ||
|
||||
(idx === 0 && automation.trigger.event === "row:save")
|
||||
) {
|
||||
let table = $tables.list.find(
|
||||
table => table._id === automation.trigger.inputs.tableId
|
||||
)
|
||||
// We want to generate our own schema for the bindings from the table schema itself
|
||||
for (const key in table?.schema) {
|
||||
schema[key] = {
|
||||
type: table.schema[key].type,
|
||||
}
|
||||
}
|
||||
// remove the original binding
|
||||
delete schema.row
|
||||
}
|
||||
let icon =
|
||||
idx === 0
|
||||
? automation.trigger.icon
|
||||
: isLoopBlock
|
||||
? "Reuse"
|
||||
: allSteps[idx].icon
|
||||
|
||||
return {
|
||||
readableBinding: bindingName ? `${bindingName}.${name}` : runtime,
|
||||
runtimeBinding: runtime,
|
||||
type: value.type,
|
||||
description: value.description,
|
||||
icon: bindingIcon,
|
||||
category: categoryName,
|
||||
display: {
|
||||
type: value.type,
|
||||
name: name,
|
||||
rank: bindingRank,
|
||||
},
|
||||
}
|
||||
})
|
||||
if (wasLoopBlock) {
|
||||
loopBlockCount++
|
||||
continue
|
||||
}
|
||||
Object.entries(schema).forEach(([name, value]) =>
|
||||
addBinding(name, value, icon, idx, isLoopBlock, bindingName)
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -245,10 +316,8 @@
|
|||
})
|
||||
)
|
||||
}
|
||||
|
||||
return bindings
|
||||
}
|
||||
|
||||
function lookForFilters(properties) {
|
||||
if (!properties) {
|
||||
return []
|
||||
|
@ -286,10 +355,16 @@
|
|||
value.customType !== "code" &&
|
||||
value.customType !== "queryParams" &&
|
||||
value.customType !== "cron" &&
|
||||
value.customType !== "triggerSchema"
|
||||
value.customType !== "triggerSchema" &&
|
||||
value.customType !== "automationFields"
|
||||
)
|
||||
}
|
||||
|
||||
function getFieldLabel(key, value) {
|
||||
const requiredSuffix = requiredProperties.includes(key) ? "*" : ""
|
||||
return `${value.title || (key === "row" ? "Table" : key)} ${requiredSuffix}`
|
||||
}
|
||||
|
||||
onMount(async () => {
|
||||
try {
|
||||
await environment.loadVariables()
|
||||
|
@ -307,7 +382,7 @@
|
|||
<Label
|
||||
tooltip={value.title === "Binding / Value"
|
||||
? "If using the String input type, please use a comma or newline separated string"
|
||||
: null}>{value.title || (key === "row" ? "Table" : key)}</Label
|
||||
: null}>{getFieldLabel(key, value)}</Label
|
||||
>
|
||||
{/if}
|
||||
<div class:field-width={shouldRenderField(value)}>
|
||||
|
@ -421,6 +496,12 @@
|
|||
on:change={e => onChange(e, key)}
|
||||
value={inputData[key]}
|
||||
/>
|
||||
{:else if value.customType === "automationFields"}
|
||||
<AutomationSelector
|
||||
on:change={e => onChange(e, key)}
|
||||
value={inputData[key]}
|
||||
{bindings}
|
||||
/>
|
||||
{:else if value.customType === "queryParams"}
|
||||
<QueryParamSelector
|
||||
on:change={e => onChange(e, key)}
|
||||
|
@ -468,39 +549,51 @@
|
|||
/>
|
||||
{:else if value.customType === "code"}
|
||||
<CodeEditorModal>
|
||||
{#if codeMode == EditorModes.JS}
|
||||
<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}
|
||||
{/if}
|
||||
<CodeEditor
|
||||
value={inputData[key]}
|
||||
on:change={e => {
|
||||
// need to pass without the value inside
|
||||
onChange({ detail: e.detail }, key)
|
||||
inputData[key] = e.detail
|
||||
}}
|
||||
completions={stepCompletions}
|
||||
mode={codeMode}
|
||||
autocompleteEnabled={codeMode != EditorModes.JS}
|
||||
height={500}
|
||||
/>
|
||||
<div class="messaging">
|
||||
{#if codeMode == EditorModes.Handlebars}
|
||||
<Icon name="FlashOn" />
|
||||
<div class="messaging-wrap">
|
||||
<div>
|
||||
Add available bindings by typing <strong>
|
||||
}}
|
||||
</strong>
|
||||
</div>
|
||||
<div class:js-editor={editingJs}>
|
||||
<div class:js-code={editingJs} style="width: 100%">
|
||||
<CodeEditor
|
||||
value={inputData[key]}
|
||||
on:change={e => {
|
||||
// need to pass without the value inside
|
||||
onChange({ detail: e.detail }, key)
|
||||
inputData[key] = e.detail
|
||||
}}
|
||||
completions={stepCompletions}
|
||||
mode={codeMode}
|
||||
autocompleteEnabled={codeMode !== EditorModes.JS}
|
||||
bind:getCaretPosition
|
||||
bind:insertAtPos
|
||||
height={500}
|
||||
/>
|
||||
<div class="messaging">
|
||||
{#if codeMode === EditorModes.Handlebars}
|
||||
<Icon name="FlashOn" />
|
||||
<div class="messaging-wrap">
|
||||
<div>
|
||||
Add available bindings by typing <strong>
|
||||
}}
|
||||
</strong>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{#if editingJs}
|
||||
<div class="js-binding-picker">
|
||||
<BindingPicker
|
||||
{bindings}
|
||||
allowHelpers={false}
|
||||
addBinding={binding =>
|
||||
bindingsHelpers.onSelectBinding(
|
||||
inputData[key],
|
||||
binding,
|
||||
{
|
||||
js: true,
|
||||
dontDecode: true,
|
||||
}
|
||||
)}
|
||||
mode="javascript"
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
@ -587,4 +680,20 @@
|
|||
.test :global(.drawer) {
|
||||
width: 10000px !important;
|
||||
}
|
||||
|
||||
.js-editor {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-grow: 1;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.js-code {
|
||||
flex: 7;
|
||||
}
|
||||
|
||||
.js-binding-picker {
|
||||
flex: 3;
|
||||
margin-top: calc((var(--spacing-xl) * -1) + 1px);
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -0,0 +1,87 @@
|
|||
<script>
|
||||
import { Select, Label } from "@budibase/bbui"
|
||||
import { createEventDispatcher } from "svelte"
|
||||
import { automationStore, selectedAutomation } from "stores/builder"
|
||||
import { TriggerStepID } from "constants/backend/automations"
|
||||
import DrawerBindableInput from "../../common/bindings/DrawerBindableInput.svelte"
|
||||
import AutomationBindingPanel from "../../common/bindings/ServerBindingPanel.svelte"
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
export let value
|
||||
export let bindings = []
|
||||
const onChangeAutomation = e => {
|
||||
value.automationId = e.detail
|
||||
dispatch("change", value)
|
||||
}
|
||||
|
||||
const onChange = (e, field) => {
|
||||
value[field] = e.detail
|
||||
dispatch("change", value)
|
||||
}
|
||||
$: if (value?.automationId == null) value = { automationId: "" }
|
||||
|
||||
$: automationFields =
|
||||
$automationStore.automations.find(
|
||||
automation => automation._id === value?.automationId
|
||||
)?.definition?.trigger?.inputs?.fields || []
|
||||
|
||||
$: filteredAutomations = $automationStore.automations.filter(
|
||||
automation =>
|
||||
automation.definition.trigger.stepId === TriggerStepID.APP &&
|
||||
automation._id !== $selectedAutomation._id
|
||||
)
|
||||
</script>
|
||||
|
||||
<div class="schema-field">
|
||||
<Label>Automation</Label>
|
||||
<div class="field-width">
|
||||
<Select
|
||||
on:change={onChangeAutomation}
|
||||
value={value.automationId}
|
||||
options={filteredAutomations}
|
||||
getOptionValue={automation => automation._id}
|
||||
getOptionLabel={automation => automation.name}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{#if Object.keys(automationFields)}
|
||||
{#each Object.keys(automationFields) as field}
|
||||
<div class="schema-field">
|
||||
<Label>{field}</Label>
|
||||
<div class="field-width">
|
||||
<DrawerBindableInput
|
||||
panel={AutomationBindingPanel}
|
||||
extraThin
|
||||
value={value[field]}
|
||||
on:change={e => onChange(e, field)}
|
||||
type="string"
|
||||
{bindings}
|
||||
fillWidth={true}
|
||||
updateOnChange={false}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
.field-width {
|
||||
width: 320px;
|
||||
}
|
||||
|
||||
.schema-field {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
flex: 1;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.schema-field :global(label) {
|
||||
text-transform: capitalize;
|
||||
}
|
||||
</style>
|
|
@ -1,16 +1,35 @@
|
|||
<script>
|
||||
import { Button, Select, Input, Label } from "@budibase/bbui"
|
||||
import { onMount, createEventDispatcher } from "svelte"
|
||||
import { flags } from "stores/backend"
|
||||
import { flags } from "stores/builder"
|
||||
import { helpers, REBOOT_CRON } from "@budibase/shared-core"
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
export let value
|
||||
let error
|
||||
|
||||
$: {
|
||||
const exists = CRON_EXPRESSIONS.some(cron => cron.value === value)
|
||||
const customIndex = CRON_EXPRESSIONS.findIndex(
|
||||
cron => cron.label === "Custom"
|
||||
)
|
||||
|
||||
if (!exists && customIndex === -1) {
|
||||
CRON_EXPRESSIONS[0] = { label: "Custom", value: value }
|
||||
} else if (exists && customIndex !== -1) {
|
||||
CRON_EXPRESSIONS.splice(customIndex, 1)
|
||||
}
|
||||
}
|
||||
|
||||
const onChange = e => {
|
||||
if (e.detail === value) {
|
||||
if (value !== REBOOT_CRON) {
|
||||
error = helpers.cron.validate(e.detail).err
|
||||
}
|
||||
if (e.detail === value || error) {
|
||||
return
|
||||
}
|
||||
|
||||
value = e.detail
|
||||
dispatch("change", e.detail)
|
||||
}
|
||||
|
@ -41,7 +60,7 @@
|
|||
if (!$flags.cloud) {
|
||||
CRON_EXPRESSIONS.push({
|
||||
label: "Every Budibase Reboot",
|
||||
value: "@reboot",
|
||||
value: REBOOT_CRON,
|
||||
})
|
||||
}
|
||||
})
|
||||
|
@ -49,6 +68,7 @@
|
|||
|
||||
<div class="block-field">
|
||||
<Input
|
||||
{error}
|
||||
on:change={onChange}
|
||||
{value}
|
||||
on:blur={() => (touched = true)}
|
||||
|
@ -64,7 +84,7 @@
|
|||
{#if presets}
|
||||
<Select
|
||||
on:change={onChange}
|
||||
{value}
|
||||
value={value || "Custom"}
|
||||
secondary
|
||||
extraThin
|
||||
label="Presets"
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<script>
|
||||
import { createEventDispatcher } from "svelte"
|
||||
import { queries } from "stores/backend"
|
||||
import { queries } from "stores/builder"
|
||||
import { Select, Label } from "@budibase/bbui"
|
||||
import DrawerBindableInput from "../../common/bindings/DrawerBindableInput.svelte"
|
||||
import AutomationBindingPanel from "../../common/bindings/ServerBindingPanel.svelte"
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<script>
|
||||
import { queries } from "stores/backend"
|
||||
import { queries } from "stores/builder"
|
||||
import { Select } from "@budibase/bbui"
|
||||
|
||||
export let value
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<script>
|
||||
import { tables } from "stores/backend"
|
||||
import { tables } from "stores/builder"
|
||||
import { Select, Checkbox, Label } from "@budibase/bbui"
|
||||
import { createEventDispatcher } from "svelte"
|
||||
import RowSelectorTypes from "./RowSelectorTypes.svelte"
|
||||
|
|
|
@ -41,7 +41,7 @@
|
|||
{ label: "False", value: "false" },
|
||||
]}
|
||||
/>
|
||||
{:else if schema.type === "array"}
|
||||
{:else if schemaHasOptions(schema) && schema.type === "array"}
|
||||
<Multiselect
|
||||
bind:value={value[field]}
|
||||
options={schema.constraints.inclusion}
|
||||
|
@ -77,7 +77,7 @@
|
|||
on:change={e => onChange(e, field)}
|
||||
useLabel={false}
|
||||
/>
|
||||
{:else if ["string", "number", "bigint", "barcodeqr"].includes(schema.type)}
|
||||
{:else if ["string", "number", "bigint", "barcodeqr", "array"].includes(schema.type)}
|
||||
<svelte:component
|
||||
this={isTestModal ? ModalBindableInput : DrawerBindableInput}
|
||||
panel={AutomationBindingPanel}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<script>
|
||||
import { tables } from "stores/backend"
|
||||
import { tables } from "stores/builder"
|
||||
import { Select } from "@budibase/bbui"
|
||||
import { createEventDispatcher } from "svelte"
|
||||
import { TableNames } from "constants"
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<script>
|
||||
import { Icon, notifications, ModalContent } from "@budibase/bbui"
|
||||
import { automationStore, selectedAutomation } from "builderStore"
|
||||
import { automationStore, selectedAutomation } from "stores/builder"
|
||||
import WebhookDisplay from "./WebhookDisplay.svelte"
|
||||
import { onMount, onDestroy } from "svelte"
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<script>
|
||||
import { API } from "api"
|
||||
import Table from "./Table.svelte"
|
||||
import { tables } from "stores/backend"
|
||||
import { tables } from "stores/builder"
|
||||
import { notifications } from "@budibase/bbui"
|
||||
|
||||
export let tableId
|
||||
|
|
|
@ -1,10 +1,9 @@
|
|||
<script>
|
||||
import { datasources, tables, integrations } from "stores/backend"
|
||||
import { datasources, tables, integrations, appStore } from "stores/builder"
|
||||
import EditRolesButton from "./buttons/EditRolesButton.svelte"
|
||||
import { TableNames } from "constants"
|
||||
import { Grid } from "@budibase/frontend-core"
|
||||
import { API } from "api"
|
||||
import { store } from "builderStore"
|
||||
import GridAddColumnModal from "components/backend/DataTable/modals/grid/GridCreateColumnModal.svelte"
|
||||
import GridCreateEditRowModal from "components/backend/DataTable/modals/grid/GridCreateEditRowModal.svelte"
|
||||
import GridEditUserModal from "components/backend/DataTable/modals/grid/GridEditUserModal.svelte"
|
||||
|
@ -59,14 +58,14 @@
|
|||
datasource={gridDatasource}
|
||||
canAddRows={!isUsersTable}
|
||||
canDeleteRows={!isUsersTable}
|
||||
canEditRows={!isUsersTable || !$store.features.disableUserMetadata}
|
||||
canEditColumns={!isUsersTable || !$store.features.disableUserMetadata}
|
||||
canEditRows={!isUsersTable || !$appStore.features.disableUserMetadata}
|
||||
canEditColumns={!isUsersTable || !$appStore.features.disableUserMetadata}
|
||||
schemaOverrides={isUsersTable ? userSchemaOverrides : null}
|
||||
showAvatars={false}
|
||||
on:updatedatasource={handleGridTableUpdate}
|
||||
>
|
||||
<svelte:fragment slot="filter">
|
||||
{#if isUsersTable && $store.features.disableUserMetadata}
|
||||
{#if isUsersTable && $appStore.features.disableUserMetadata}
|
||||
<GridUsersTableButton />
|
||||
{/if}
|
||||
<GridFilterButton />
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<script>
|
||||
import { API } from "api"
|
||||
import { tables } from "stores/backend"
|
||||
import { tables } from "stores/builder"
|
||||
|
||||
import Table from "./Table.svelte"
|
||||
import CalculateButton from "./buttons/CalculateButton.svelte"
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<script>
|
||||
import { viewsV2 } from "stores/backend"
|
||||
import { viewsV2 } from "stores/builder"
|
||||
import { Grid } from "@budibase/frontend-core"
|
||||
import { API } from "api"
|
||||
import GridCreateEditRowModal from "components/backend/DataTable/modals/grid/GridCreateEditRowModal.svelte"
|
||||
|
|
|
@ -1,10 +1,8 @@
|
|||
<script>
|
||||
import { ActionButton, notifications } from "@budibase/bbui"
|
||||
import CreateEditRelationshipModal from "../../Datasources/CreateEditRelationshipModal.svelte"
|
||||
import {
|
||||
datasources,
|
||||
tables as tablesStore,
|
||||
} from "../../../../stores/backend"
|
||||
|
||||
import { datasources, tables as tablesStore } from "stores/builder"
|
||||
import { createEventDispatcher } from "svelte"
|
||||
|
||||
export let table
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<script>
|
||||
import { ActionButton, Modal } from "@budibase/bbui"
|
||||
import { permissions } from "stores/backend"
|
||||
import { permissions } from "stores/builder"
|
||||
import ManageAccessModal from "../modals/ManageAccessModal.svelte"
|
||||
|
||||
export let resourceId
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<script>
|
||||
import { roles } from "stores/backend"
|
||||
import { roles } from "stores/builder"
|
||||
|
||||
export let value
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { FIELDS } from "constants/backend"
|
||||
import { tables } from "stores/backend"
|
||||
import { tables } from "stores/builder"
|
||||
import { get as svelteGet } from "svelte/store"
|
||||
|
||||
// currently supported level of relationship depth (server side)
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<script>
|
||||
import { Select, Label, notifications, ModalContent } from "@budibase/bbui"
|
||||
import { tables, views } from "stores/backend"
|
||||
import { tables, views } from "stores/builder"
|
||||
import { FIELDS } from "constants/backend"
|
||||
|
||||
const CALCULATIONS = [
|
||||
|
|
|
@ -15,7 +15,7 @@
|
|||
} from "@budibase/bbui"
|
||||
import { createEventDispatcher, getContext, onMount } from "svelte"
|
||||
import { cloneDeep } from "lodash/fp"
|
||||
import { tables, datasources } from "stores/backend"
|
||||
import { tables, datasources } from "stores/builder"
|
||||
import { TableNames, UNEDITABLE_USER_FIELDS } from "constants"
|
||||
import {
|
||||
FIELDS,
|
||||
|
@ -28,7 +28,7 @@
|
|||
PrettyRelationshipDefinitions,
|
||||
DB_TYPE_EXTERNAL,
|
||||
} from "constants/backend"
|
||||
import { getAutoColumnInformation, buildAutoColumn } from "builderStore/utils"
|
||||
import { getAutoColumnInformation, buildAutoColumn } from "helpers/utils"
|
||||
import ConfirmDialog from "components/common/ConfirmDialog.svelte"
|
||||
import ModalBindableInput from "components/common/bindings/ModalBindableInput.svelte"
|
||||
import { getBindings } from "components/backend/DataTable/formula"
|
||||
|
@ -172,22 +172,6 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
if (!savingColumn && !originalName) {
|
||||
let highestNumber = 0
|
||||
Object.keys(table.schema).forEach(columnName => {
|
||||
const columnNumber = extractColumnNumber(columnName)
|
||||
if (columnNumber > highestNumber) {
|
||||
highestNumber = columnNumber
|
||||
}
|
||||
return highestNumber
|
||||
})
|
||||
|
||||
if (highestNumber >= 1) {
|
||||
editableColumn.name = `Column 0${highestNumber + 1}`
|
||||
} else {
|
||||
editableColumn.name = "Column 01"
|
||||
}
|
||||
}
|
||||
|
||||
if (!savingColumn) {
|
||||
editableColumn.fieldId = makeFieldId(
|
||||
|
@ -389,11 +373,6 @@
|
|||
deleteColName = ""
|
||||
}
|
||||
|
||||
function extractColumnNumber(columnName) {
|
||||
const match = columnName.match(/Column (\d+)/)
|
||||
return match ? parseInt(match[1]) : 0
|
||||
}
|
||||
|
||||
function getAllowedTypes() {
|
||||
if (
|
||||
originalName &&
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<script>
|
||||
import { createEventDispatcher } from "svelte"
|
||||
import { tables } from "stores/backend"
|
||||
import { tables } from "stores/builder"
|
||||
import { ModalContent, keepOpen, notifications } from "@budibase/bbui"
|
||||
import RowFieldControl from "../RowFieldControl.svelte"
|
||||
import { API } from "api"
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<script>
|
||||
import { createEventDispatcher } from "svelte"
|
||||
import { tables, roles } from "stores/backend"
|
||||
import { tables, roles } from "stores/builder"
|
||||
import {
|
||||
notifications,
|
||||
keepOpen,
|
||||
|
|
|
@ -10,7 +10,7 @@
|
|||
import { onMount } from "svelte"
|
||||
import { API } from "api"
|
||||
import ErrorsBox from "components/common/ErrorsBox.svelte"
|
||||
import { roles } from "stores/backend"
|
||||
import { roles } from "stores/builder"
|
||||
|
||||
const BASE_ROLE = { _id: "", inherits: "BASIC", permissionId: "write" }
|
||||
|
||||
|
|
|
@ -10,7 +10,7 @@
|
|||
notifications,
|
||||
Icon,
|
||||
} from "@budibase/bbui"
|
||||
import { tables, views } from "stores/backend"
|
||||
import { tables, views } from "stores/builder"
|
||||
|
||||
const CONDITIONS = [
|
||||
{
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<script>
|
||||
import { Select, ModalContent, notifications } from "@budibase/bbui"
|
||||
import { tables, views } from "stores/backend"
|
||||
import { tables, views } from "stores/builder"
|
||||
import { FIELDS } from "constants/backend"
|
||||
|
||||
export let view = {}
|
||||
|
|
|
@ -13,7 +13,7 @@
|
|||
} from "@budibase/bbui"
|
||||
import { onMount, createEventDispatcher } from "svelte"
|
||||
import { FIELDS } from "constants/backend"
|
||||
import { generate } from "builderStore/schemaGenerator"
|
||||
import { generate } from "helpers/schemaGenerator"
|
||||
|
||||
export let schema = {}
|
||||
export let json
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<script>
|
||||
import { PermissionSource } from "@budibase/types"
|
||||
import { roles, permissions as permissionsStore } from "stores/backend"
|
||||
import { roles, permissions as permissionsStore } from "stores/builder"
|
||||
import {
|
||||
Label,
|
||||
Input,
|
||||
|
@ -13,6 +13,7 @@
|
|||
Icon,
|
||||
} from "@budibase/bbui"
|
||||
import { capitalise } from "helpers"
|
||||
import { getFormattedPlanName } from "helpers/planTitle"
|
||||
import { get } from "svelte/store"
|
||||
|
||||
export let resourceId
|
||||
|
@ -99,7 +100,9 @@
|
|||
{#if requiresPlanToModify}
|
||||
<span class="lock-tag">
|
||||
<Tags>
|
||||
<Tag icon="LockClosed">{capitalise(requiresPlanToModify)}</Tag>
|
||||
<Tag icon="LockClosed"
|
||||
>{getFormattedPlanName(requiresPlanToModify)}</Tag
|
||||
>
|
||||
</Tags>
|
||||
</span>
|
||||
{/if}
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
import { getContext } from "svelte"
|
||||
import { Input, notifications, ModalContent } from "@budibase/bbui"
|
||||
import { goto } from "@roxi/routify"
|
||||
import { viewsV2 } from "stores/backend"
|
||||
import { viewsV2 } from "stores/builder"
|
||||
|
||||
const { filter, sort, definition } = getContext("grid")
|
||||
|
||||
|
|
|
@ -1,15 +1,12 @@
|
|||
<script>
|
||||
import { ActionButton, Popover, Heading, Body, Button } from "@budibase/bbui"
|
||||
import { store } from "builderStore"
|
||||
import { builderStore } from "stores/builder"
|
||||
|
||||
let anchor
|
||||
let open = false
|
||||
|
||||
const openSidePanel = () => {
|
||||
store.update(state => ({
|
||||
...state,
|
||||
builderSidePanel: true,
|
||||
}))
|
||||
builderStore.showBuilderSidePanel()
|
||||
open = false
|
||||
}
|
||||
</script>
|
||||
|
|
|
@ -3,13 +3,13 @@
|
|||
import { Layout } from "@budibase/bbui"
|
||||
import { BUDIBASE_INTERNAL_DB_ID } from "constants/backend"
|
||||
import {
|
||||
database,
|
||||
datasources,
|
||||
queries,
|
||||
tables,
|
||||
views,
|
||||
viewsV2,
|
||||
} from "stores/backend"
|
||||
userSelectedResourceMap,
|
||||
} from "stores/builder"
|
||||
import EditDatasourcePopover from "./popovers/EditDatasourcePopover.svelte"
|
||||
import EditQueryPopover from "./popovers/EditQueryPopover.svelte"
|
||||
import NavItem from "components/common/NavItem.svelte"
|
||||
|
@ -21,7 +21,6 @@
|
|||
} from "helpers/data/utils"
|
||||
import IntegrationIcon from "./IntegrationIcon.svelte"
|
||||
import { TableNames } from "constants"
|
||||
import { userSelectedResourceMap } from "builderStore"
|
||||
import { enrichDatasources } from "./datasourceUtils"
|
||||
import { onMount } from "svelte"
|
||||
|
||||
|
@ -75,69 +74,67 @@
|
|||
searchTerm && !showAppUsersTable && !enrichedDataSources.find(ds => ds.show)
|
||||
</script>
|
||||
|
||||
{#if $database?._id}
|
||||
<div class="hierarchy-items-container">
|
||||
{#if showAppUsersTable}
|
||||
<NavItem
|
||||
icon="UserGroup"
|
||||
text={appUsersTableName}
|
||||
selected={$isActive("./table/:tableId") &&
|
||||
$tables.selected?._id === TableNames.USERS}
|
||||
on:click={() => selectTable(TableNames.USERS)}
|
||||
selectedBy={$userSelectedResourceMap[TableNames.USERS]}
|
||||
/>
|
||||
{/if}
|
||||
{#each enrichedDataSources.filter(ds => ds.show) as datasource}
|
||||
<NavItem
|
||||
border
|
||||
text={datasource.name}
|
||||
opened={datasource.open}
|
||||
selected={$isActive("./datasource") && datasource.selected}
|
||||
withArrow={true}
|
||||
on:click={() => selectDatasource(datasource)}
|
||||
on:iconClick={() => toggleNode(datasource)}
|
||||
selectedBy={$userSelectedResourceMap[datasource._id]}
|
||||
>
|
||||
<div class="datasource-icon" slot="icon">
|
||||
<IntegrationIcon
|
||||
integrationType={datasource.source}
|
||||
schema={datasource.schema}
|
||||
size="18"
|
||||
/>
|
||||
</div>
|
||||
{#if datasource._id !== BUDIBASE_INTERNAL_DB_ID}
|
||||
<EditDatasourcePopover {datasource} />
|
||||
{/if}
|
||||
</NavItem>
|
||||
|
||||
{#if datasource.open}
|
||||
<TableNavigator tables={datasource.tables} {selectTable} />
|
||||
{#each datasource.queries as query}
|
||||
<NavItem
|
||||
indentLevel={1}
|
||||
icon="SQLQuery"
|
||||
iconText={customQueryIconText(datasource, query)}
|
||||
iconColor={customQueryIconColor(datasource, query)}
|
||||
text={customQueryText(datasource, query)}
|
||||
selected={$isActive("./query/:queryId") &&
|
||||
$queries.selectedQueryId === query._id}
|
||||
on:click={() => $goto(`./query/${query._id}`)}
|
||||
selectedBy={$userSelectedResourceMap[query._id]}
|
||||
>
|
||||
<EditQueryPopover {query} />
|
||||
</NavItem>
|
||||
{/each}
|
||||
<div class="hierarchy-items-container">
|
||||
{#if showAppUsersTable}
|
||||
<NavItem
|
||||
icon="UserGroup"
|
||||
text={appUsersTableName}
|
||||
selected={$isActive("./table/:tableId") &&
|
||||
$tables.selected?._id === TableNames.USERS}
|
||||
on:click={() => selectTable(TableNames.USERS)}
|
||||
selectedBy={$userSelectedResourceMap[TableNames.USERS]}
|
||||
/>
|
||||
{/if}
|
||||
{#each enrichedDataSources.filter(ds => ds.show) as datasource}
|
||||
<NavItem
|
||||
border
|
||||
text={datasource.name}
|
||||
opened={datasource.open}
|
||||
selected={$isActive("./datasource") && datasource.selected}
|
||||
withArrow={true}
|
||||
on:click={() => selectDatasource(datasource)}
|
||||
on:iconClick={() => toggleNode(datasource)}
|
||||
selectedBy={$userSelectedResourceMap[datasource._id]}
|
||||
>
|
||||
<div class="datasource-icon" slot="icon">
|
||||
<IntegrationIcon
|
||||
integrationType={datasource.source}
|
||||
schema={datasource.schema}
|
||||
size="18"
|
||||
/>
|
||||
</div>
|
||||
{#if datasource._id !== BUDIBASE_INTERNAL_DB_ID}
|
||||
<EditDatasourcePopover {datasource} />
|
||||
{/if}
|
||||
{/each}
|
||||
{#if showNoResults}
|
||||
<Layout paddingY="none" paddingX="L">
|
||||
<div class="no-results">
|
||||
There aren't any datasources matching that name
|
||||
</div>
|
||||
</Layout>
|
||||
</NavItem>
|
||||
|
||||
{#if datasource.open}
|
||||
<TableNavigator tables={datasource.tables} {selectTable} />
|
||||
{#each datasource.queries as query}
|
||||
<NavItem
|
||||
indentLevel={1}
|
||||
icon="SQLQuery"
|
||||
iconText={customQueryIconText(datasource, query)}
|
||||
iconColor={customQueryIconColor(datasource, query)}
|
||||
text={customQueryText(datasource, query)}
|
||||
selected={$isActive("./query/:queryId") &&
|
||||
$queries.selectedQueryId === query._id}
|
||||
on:click={() => $goto(`./query/${query._id}`)}
|
||||
selectedBy={$userSelectedResourceMap[query._id]}
|
||||
>
|
||||
<EditQueryPopover {query} />
|
||||
</NavItem>
|
||||
{/each}
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
{/each}
|
||||
{#if showNoResults}
|
||||
<Layout paddingY="none" paddingX="L">
|
||||
<div class="no-results">
|
||||
There aren't any datasources matching that name
|
||||
</div>
|
||||
</Layout>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.hierarchy-items-container {
|
||||
|
|
|
@ -16,7 +16,7 @@ import Firebase from "./Firebase.svelte"
|
|||
import Redis from "./Redis.svelte"
|
||||
import Snowflake from "./Snowflake.svelte"
|
||||
import Custom from "./Custom.svelte"
|
||||
import { integrations } from "stores/backend"
|
||||
import { integrations } from "stores/builder"
|
||||
import { get } from "svelte/store"
|
||||
|
||||
const ICONS = {
|
||||
|
|
|
@ -12,7 +12,7 @@
|
|||
TextArea,
|
||||
Dropzone,
|
||||
} from "@budibase/bbui"
|
||||
import { datasources, queries } from "stores/backend"
|
||||
import { datasources, queries } from "stores/builder"
|
||||
import { writable } from "svelte/store"
|
||||
|
||||
export let navigateDatasource = false
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<script>
|
||||
import { get } from "svelte/store"
|
||||
import { datasources, integrations } from "stores/backend"
|
||||
import { datasources, integrations } from "stores/builder"
|
||||
import { notifications, Input, ModalContent, Modal } from "@budibase/bbui"
|
||||
import { integrationForDatasource } from "stores/selectors"
|
||||
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue