Merge branch 'develop' of github.com:Budibase/budibase into rest-pagination

This commit is contained in:
Andrew Kingston 2022-01-04 14:34:09 +00:00
commit db55074b0a
95 changed files with 1975 additions and 437 deletions

View File

@ -16,26 +16,46 @@ jobs:
- name: Pull values.yaml from budibase-infra - name: Pull values.yaml from budibase-infra
run: | run: |
curl -H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" \ [c3a7a9d12] curl -H "Authorization: token ${{ secrets.GH_PERSONAL_TOKEN }}" \
-H 'Accept: application/vnd.github.v3.raw' \ -H 'Accept: application/vnd.github.v3.raw' \
-o values.production.yaml \ -o values.production.yaml \
-L https://api.github.com/repos/budibase/budibase-infra/contents/kubernetes/values.yaml -L https://api.github.com/repos/budibase/budibase-infra/contents/kubernetes/values.yaml
wc -l values.production.yaml
- name: Get the latest budibase release version if not specifically set - name: Get the latest budibase release version
if: ${{ !github.event.inputs.version }}
id: version id: version
run: | run: |
sudo apt-get install -y jq if [ -z "${{ github.event.inputs.version }}" ]; then
release_version=$(cat lerna.json | jq -r '.version') release_version=$(cat lerna.json | jq -r '.version')
echo "::set-output name=release_version::$release_version" else
release_version=${{ github.event.inputs.version }}
fi
echo "RELEASE_VERSION=$release_version" >> $GITHUB_ENV
- uses: peymanmortazavi/eks-helm-deploy@v1 - name: Configure AWS Credentials
uses: aws-actions/configure-aws-credentials@v1
with: with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
aws-region: eu-west-1 aws-region: eu-west-1
- name: Deploy to EKS
uses: craftech-io/eks-helm-deploy-action@v1
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS__KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
aws-region: eu-west-1
cluster-name: budibase-eks-production cluster-name: budibase-eks-production
config-files: values.production.yaml config-files: values.production.yaml
chart-path: charts/budibase
namespace: budibase namespace: budibase
values: appVersion=v${{ github.event.inputs.version || github.steps.version.outputs.release_version }} values: globals.appVersion=v${{ env.RELEASE_VERSION }}
name: budibase-prod name: budibase-prod
- name: Discord Webhook Action
uses: tsickert/discord-webhook@v4.0.0
with:
webhook-url: ${{ secrets.PROD_DEPLOY_WEBHOOK_URL }}
content: "Production Deployment Complete: ${{ env.RELEASE_VERSION }} deployed to Budibase Cloud."
embed-title: ${{ env.RELEASE_VERSION }}

59
.github/workflows/deploy-preprod.yml vendored Normal file
View File

@ -0,0 +1,59 @@
name: Budibase Release Preprod
on:
workflow_dispatch:
env:
POSTHOG_TOKEN: ${{ secrets.POSTHOG_TOKEN }}
INTERCOM_TOKEN: ${{ secrets.INTERCOM_TOKEN }}
POSTHOG_URL: ${{ secrets.POSTHOG_URL }}
SENTRY_DSN: ${{ secrets.SENTRY_DSN }}
jobs:
release:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Configure AWS Credentials
uses: aws-actions/configure-aws-credentials@v1
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
aws-region: eu-west-1
- name: 'Get Previous tag'
id: previoustag
uses: "WyriHaximus/github-action-get-previous-tag@v1"
# - name: Pull values.yaml from budibase-infra
# run: |
# curl -H "Authorization: token ${{ secrets.GH_PERSONAL_TOKEN }}" \ [c3a7a9d12]
# -H 'Accept: application/vnd.github.v3.raw' \
# -o values.preprod.yaml \
# -L https://api.github.com/repos/budibase/budibase-infra/contents/kubernetes/values.preprod.yaml
- name: Deploy to Preprod Environment
uses: deliverybot/helm@v1
with:
release: budibase-preprod
namespace: budibase
chart: charts/budibase
token: ${{ github.token }}
values: |
globals:
appVersion: ${{ steps.previoustag.outputs.tag }}
# value-files: >-
# [
# "charts/budibase/values.yaml"
# ]
env:
KUBECONFIG_FILE: '${{ secrets.PREPROD_KUBECONFIG }}'
- name: Discord Webhook Action
uses: tsickert/discord-webhook@v4.0.0
with:
webhook-url: ${{ secrets.PROD_DEPLOY_WEBHOOK_URL }}
content: "Preprod Deployment Complete: ${{ env.RELEASE_VERSION }} deployed to Budibase Pre-prod."
embed-title: ${{ env.RELEASE_VERSION }}

View File

@ -9,15 +9,16 @@ jobs:
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
with:
fetch_depth: 0
- name: Tag and release Docker images (Self Host) - name: Tag and release Docker images (Self Host)
run: | run: |
docker login -u $DOCKER_USER -p $DOCKER_PASSWORD docker login -u $DOCKER_USER -p $DOCKER_PASSWORD
# Get latest release version # Get latest release version
sudo apt-get install -y jq
release_version=$(cat lerna.json | jq -r '.version') release_version=$(cat lerna.json | jq -r '.version')
echo "::set-output name=release_version::$release_version" echo "RELEASE_VERSION=$release_version" >> $GITHUB_ENV
release_tag=v$release_version release_tag=v$release_version
# Pull apps and worker images # Pull apps and worker images
@ -25,16 +26,16 @@ jobs:
docker pull budibase/worker:$release_tag docker pull budibase/worker:$release_tag
# Tag apps and worker images # Tag apps and worker images
docker tag budibase/apps:$release_tag $SELF_HOST_TAG docker tag budibase/apps:$release_tag budibase/apps:$SELFHOST_TAG
docker tag budibase/worker:$release_tag $SELF_HOST_TAG docker tag budibase/worker:$release_tag budibase/worker:$SELFHOST_TAG
# Push images # Push images
docker push --all-tags budibase/apps docker push budibase/apps:$SELFHOST_TAG
docker push --all-tags budibase/worker docker push budibase/worker:$SELFHOST_TAG
env: env:
DOCKER_USER: ${{ secrets.DOCKER_USERNAME }} DOCKER_USER: ${{ secrets.DOCKER_USERNAME }}
DOCKER_PASSWORD: ${{ secrets.DOCKER_API_KEY }} DOCKER_PASSWORD: ${{ secrets.DOCKER_API_KEY }}
SELF_HOST_TAG: latest SELFHOST_TAG: latest
- name: Setup Helm - name: Setup Helm
uses: azure/setup-helm@v1 uses: azure/setup-helm@v1
@ -44,17 +45,25 @@ jobs:
# run: | # run: |
# git config user.name "Budibase Helm Bot" # git config user.name "Budibase Helm Bot"
# git config user.email "<>" # git config user.email "<>"
# helm package charts/budibase # mv budibase-${{ env.RELEASE_VERSION }}.tgz docs
# helm repo index docs
# git checkout gh-pages # git checkout gh-pages
# mv budibase-${{ github.steps.version.outputs.release_version }}.tgz docs
# git add -A # git add -A
# git commit -m "Helm Release: ${{ github.steps.version.outputs.release_version }}" # git commit -m "Helm Release: ${{ env.RELEASE_VERSION }}"
# git push # git push
# env: # env:
# GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# - name: Perform github release # - name: Deploy
# run: | # uses: peaceiris/actions-gh-pages@v3
# echo release # with:
# env: # github_token: ${{ secrets.GITHUB_TOKEN }}
# GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}" # publish_dir: ./public
# full_commit_message: "Helm Release: ${{ env.RELEASE_VERSION }}"
- name: Perform Github Release
uses: softprops/action-gh-release@v1
with:
name: v${{ env.RELEASE_VERSION }}
tag_name: v${{ env.RELEASE_VERSION }}
generate_release_notes: true

View File

@ -3,7 +3,7 @@ name: Budibase Release
on: on:
push: push:
branches: branches:
- master - test
env: env:
POSTHOG_TOKEN: ${{ secrets.POSTHOG_TOKEN }} POSTHOG_TOKEN: ${{ secrets.POSTHOG_TOKEN }}
@ -59,7 +59,7 @@ jobs:
# - name: Pull values.yaml from budibase-infra # - name: Pull values.yaml from budibase-infra
# run: | # run: |
# curl -H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" \ [c3a7a9d12] # curl -H "Authorization: token ${{ secrets.GH_PERSONAL_TOKEN }}" \ [c3a7a9d12]
# -H 'Accept: application/vnd.github.v3.raw' \ # -H 'Accept: application/vnd.github.v3.raw' \
# -o values.preprod.yaml \ # -o values.preprod.yaml \
# -L https://api.github.com/repos/budibase/budibase-infra/contents/kubernetes/values.preprod.yaml # -L https://api.github.com/repos/budibase/budibase-infra/contents/kubernetes/values.preprod.yaml
@ -72,10 +72,18 @@ jobs:
# chart: charts/budibase # chart: charts/budibase
# token: ${{ github.token }} # token: ${{ github.token }}
# values: | # values: |
# globals:
# appVersion: ${{ steps.previoustag.outputs.tag }} # appVersion: ${{ steps.previoustag.outputs.tag }}
# value-files: >- # # value-files: >-
# [ # # [
# "values.preprod.yaml" # # "charts/budibase/values.yaml"
# ] # # ]
# env: # env:
# KUBECONFIG_FILE: '${{ secrets.PREPROD_KUBECONFIG }}' # KUBECONFIG_FILE: '${{ secrets.PREPROD_KUBECONFIG }}'
# - name: Discord Webhook Action
# uses: tsickert/discord-webhook@v4.0.0
# with:
# webhook-url: ${{ secrets.PROD_DEPLOY_WEBHOOK_URL }}
# content: "Preprod Deployment Complete: ${{ env.RELEASE_VERSION }} deployed to Budibase Pre-prod."
# embed-title: ${{ env.RELEASE_VERSION }}

View File

@ -50,6 +50,14 @@ static_resources:
route: route:
cluster: app-service cluster: app-service
- match:
safe_regex:
google_re2: {}
regex: "/api/.*/export"
route:
timeout: 0s
cluster: app-service
- match: { path: "/api/deploy" } - match: { path: "/api/deploy" }
route: route:
timeout: 60s timeout: 60s

214
i18n/README.fr.md Normal file
View File

@ -0,0 +1,214 @@
<p align="center">
<a href="https://www.budibase.com">
<img alt="Budibase" src="https://d33wubrfki0l68.cloudfront.net/aac32159d7207b5085e74a7ef67afbb7027786c5/2b1fd/img/logo/bb-emblem.svg" width="60" />
</a>
</p>
<h1 align="center">
Budibase
</h1>
<h3 align="center">
La plateform low-code que vous aimerez utiliser
</h3>
<p align="center">
Budibase est une plateforme low-code open source et c'est la façon la plus facile de créer des outils internes qui améliore la productivité.
</p>
<h3 align="center">
🤖 🎨 🚀
</h3>
<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">
</p>
<p align="center">
<a href="https://github.com/Budibase/budibase/releases">
<img alt="GitHub toutes les releases" src="https://img.shields.io/github/downloads/Budibase/budibase/total">
</a>
<a href="https://github.com/Budibase/budibase/releases">
<img alt="GitHub release (par ordre chronologique)" src="https://img.shields.io/github/v/release/Budibase/budibase">
</a>
<a href="https://twitter.com/intent/follow?screen_name=budibase">
<img src="https://img.shields.io/twitter/follow/budibase?style=social" alt="Suivre @budibase" />
</a>
<img src="https://img.shields.io/badge/Contributor%20Covenant-v2.0%20adopted-ff69b4.svg" alt="Code de conduite" />
<a href="https://codecov.io/gh/Budibase/budibase">
<img src="https://codecov.io/gh/Budibase/budibase/graph/badge.svg?token=E8W2ZFXQOH"/>
</a>
</p>
<h3 align="center">
<a href="https://docs.budibase.com/getting-started">Commencer</a>
<span> · </span>
<a href="https://docs.budibase.com">Documentation</a>
<span> · </span>
<a href="https://github.com/Budibase/budibase/discussions?discussions_q=category%3AIdeas">Demandes d'amélioration</a>
<span> · </span>
<a href="https://github.com/Budibase/budibase/issues">Signaler un bug</a>
<span> · </span>
Support: <a href="https://github.com/Budibase/budibase/discussions">Discussions</a>
</h3>
<br /><br />
## ✨ Fontionnalités
### Construire et déployer un vrai logiciel
Contrairement à d'autres plateformes, avec Budibase vous construisez et déployez des applications one-page. Les applications Budibase sont très perfomantes et peuvent être designées de manière responsive, offrant ainsi à vos utilisateurs une expérience exceptionnelle.
<br /><br />
### Source libre et extensible
Budibase est un logiciel libre - sous licence GPL v3. Cela devrait vous rassurer sur le fait que Budibase sera toujours là. Vous pouvez également coder dans Budibase ou le forker et apporter des modifications à votre guise, ce qui en fera une expérience conviviale pour les développeurs.
<br /><br />
### Importer les données ou partir de zéro
Budibase peut tirer ses données de plusieurs sources, dont MongoDB, CouchDB, PostgreSQL, MySQL, Airtable, S3, DynamoDB ou une API REST. Et contrairement à d'autres plateformes, avec Budibase, vous pouvez partir de zéro et créer des applications métier sans aucune source de données. [Demander une nouvelle source de données](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">
</p>
<br /><br />
### Concevoir et créer des applications à l'aide de composants prédéfinis.
Budibase est livré avec des composants joliment conçus et puissants que vous pouvez utiliser comme des blocs de construction pour bâtir votre interface utilisateur. Nous exposons également un grand nombre de vos options de style CSS préférées afin que vous puissiez faire preuve d'une créativité accrue. [Demander un nouveau composant](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">
</p>
<br /><br />
### Automatiser les processus, intégrer d'autres outils et se connecter à des webhooks
Gagnez du temps en automatisant les processus manuels et les flux de travail. Qu'il s'agisse de se connecter à des webhooks ou d'automatiser des e-mails, il suffit de dire à Budibase ce qu'il doit faire et de le laisser travailler pour vous. Vous pouvez aisément [créer une nouvelle automatisation pour Budibase ici](https://github.com/Budibase/automations) ou [Demander une nouvelle automatisation](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>
<br /><br />
### Intégration avec vos outils préférés
Budibase s'intègre à un certain nombre d'outils populaires, ce qui vous permet de créer des applications qui s'adaptent parfaitement à votre pile technologique.
<p align="center">
<img alt="Budibase integrations" src="https://res.cloudinary.com/daog6scxm/image/upload/v1636970242/Out%20of%20beta%20launch/integrations_kc7dqt.png">
</p>
<br /><br />
### Paradis des admins
Budibase est conçu pour évoluer. Avec Budibase, vous pouvez vous auto-héberger sur votre propre infrastructure et gérer globalement les utilisateurs, l'accueil, le SMTP, les applications, les groupes, l'apparence et plus encore. Vous pouvez également fournir aux utilisateurs/groupes un portail d'applications et confier la gestion des utilisateurs au responsable du groupe.
- Regardez la vidéo de promotion: https://youtu.be/xoljVpty_Kw
<br /><br /><br />
## 🏁 Commencer
<img src="https://res.cloudinary.com/daog6scxm/image/upload/v1634808888/logo/deploy_npl9za.png" />
Déployez Budibase en auto-hébergement dans votre infrastructure existante, en utilisant Docker, Kubernetes et Digital Ocean.
Ou utilisez Budibase Cloud si vous n'avez pas besoin de vous auto-héberger, et que vous souhaitez démarrer rapidement.
### [Commencer avec Budibase](https://budibase.com)
<br /><br />
## 🎓 Apprendre Budibase
La documentation Budibase [est ic](https://docs.budibase.com).
<br />
<br /><br />
## 💬 Communauté
Si vous avez une question ou si vous souhaitez discuter avec d'autres utilisateurs de Budibase et rejoindre notre communauté, veuillez vous rendre à l'adresse suivante : [Discussions Github](https://github.com/Budibase/budibase/discussions)
<br /><br /><br />
## ❗ Code de conduite
Budibase s'engage à offrir à chacun une expérience accueillante, diversifiée et exempte de harcèlement. Nous attendons de tous les membres de la communauté Budibase qu'ils se conforment aux principes de notre [**Code de conduite**](https://github.com/Budibase/budibase/blob/HEAD/.github/CODE_OF_CONDUCT.md). Merci de le lire.
<br />
<br /><br />
## 🙌 Contribuer à Budibase
Qu'il s'agisse d'ouvrir un rapport de bug ou de créer une Pull request, toute contribution est appréciée et bienvenue. Si vous envisagez de mettre en œuvre une nouvelle fonctionnalité ou de modifier l'API, veuillez d'abord créer un Issue. Nous pourrons ainsi nous assurer que votre travail n'est pas vain.
### Vous ne savez pas par où commencer ?
Un bon endroit pour commencer à contribuer, c'est ici : [Projets en cours](https://github.com/Budibase/budibase/projects/22).
### Comment le repo est-il organisé ?
Budibase est une monorepo gérée par lerna. Lerna gère la construction et la publication des paquets de Budibase. Voici, à un haut niveau, les paquets qui composent Budibase.
- [packages/builder](https://github.com/Budibase/budibase/tree/HEAD/packages/builder) - contient le code pour l'application svelte côté client du budibase builder.
- [packages/client](https://github.com/Budibase/budibase/tree/HEAD/packages/client) - Un module qui s'exécute dans le navigateur et qui est chargé de lire les définitions JSON et de créer des applications web vivantes à partir de celles-ci..
- [packages/server](https://github.com/Budibase/budibase/tree/HEAD/packages/server) - Le serveur budibase. Cette application Koa est responsable de servir le JS pour les applications builder et budibase, ainsi que de fournir l'API pour l'interaction avec la base de données et le système de fichiers.
Pour plus d'informations, voir [CONTRIBUTING.md](https://github.com/Budibase/budibase/blob/HEAD/.github/CONTRIBUTING.md)
<br /><br />
## 📝 Licence
Budibase est open source, sous licence de [GPL v3](https://www.gnu.org/licenses/gpl-3.0.en.html). Les bibliothèques du client et des composants sont sous licence [MPL](https://directory.fsf.org/wiki/License:MPL-2.0) - afin que les applications que vous créez puissent être utilisées sous licence comme vous le souhaitez.
<br /><br />
## ⭐ Stargazers dans le temps
[![Stargazers dans le temps](https://starchart.cc/Budibase/budibase.svg)](https://starchart.cc/Budibase/budibase)
Si vous rencontrez des problèmes entre les mises à jour du builder, veuillez utiliser le guide suivant [ici](https://github.com/Budibase/budibase/blob/HEAD/.github/CONTRIBUTING.md#troubleshooting) pour nettoyer votre environnement.
<br /><br />
## Contributeurs ✨
Merci à ces personnes merveilleuses ([emoji key](https://allcontributors.org/docs/en/emoji-key)):
<!-- ALL-CONTRIBUTORS-LIST:START - Do not remove or modify this section -->
<!-- prettier-ignore-start -->
<!-- markdownlint-disable -->
<table>
<tr>
<td align="center"><a href="http://martinmck.com"><img src="https://avatars1.githubusercontent.com/u/11256663?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Martin McKeaveney</b></sub></a><br /><a href="https://github.com/Budibase/budibase/commits?author=shogunpurple" title="Code">💻</a> <a href="https://github.com/Budibase/budibase/commits?author=shogunpurple" title="Documentation">📖</a> <a href="https://github.com/Budibase/budibase/commits?author=shogunpurple" title="Tests">⚠️</a> <a href="#infra-shogunpurple" title="Infrastructure (Hosting, Build-Tools, etc)">🚇</a></td>
<td align="center"><a href="http://www.michaeldrury.co.uk/"><img src="https://avatars2.githubusercontent.com/u/4407001?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Michael Drury</b></sub></a><br /><a href="https://github.com/Budibase/budibase/commits?author=mike12345567" title="Documentation">📖</a> <a href="https://github.com/Budibase/budibase/commits?author=mike12345567" title="Code">💻</a> <a href="https://github.com/Budibase/budibase/commits?author=mike12345567" title="Tests">⚠️</a> <a href="#infra-mike12345567" title="Infrastructure (Hosting, Build-Tools, etc)">🚇</a></td>
<td align="center"><a href="https://github.com/aptkingston"><img src="https://avatars3.githubusercontent.com/u/9075550?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Andrew Kingston</b></sub></a><br /><a href="https://github.com/Budibase/budibase/commits?author=aptkingston" title="Documentation">📖</a> <a href="https://github.com/Budibase/budibase/commits?author=aptkingston" title="Code">💻</a> <a href="https://github.com/Budibase/budibase/commits?author=aptkingston" title="Tests">⚠️</a> <a href="#design-aptkingston" title="Design">🎨</a></td>
<td align="center"><a href="https://budibase.com/"><img src="https://avatars3.githubusercontent.com/u/3524181?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Michael Shanks</b></sub></a><br /><a href="https://github.com/Budibase/budibase/commits?author=mjashanks" title="Documentation">📖</a> <a href="https://github.com/Budibase/budibase/commits?author=mjashanks" title="Code">💻</a> <a href="https://github.com/Budibase/budibase/commits?author=mjashanks" title="Tests">⚠️</a></td>
<td align="center"><a href="https://github.com/kevmodrome"><img src="https://avatars3.githubusercontent.com/u/534488?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Kevin Åberg Kultalahti</b></sub></a><br /><a href="https://github.com/Budibase/budibase/commits?author=kevmodrome" title="Documentation">📖</a> <a href="https://github.com/Budibase/budibase/commits?author=kevmodrome" title="Code">💻</a> <a href="https://github.com/Budibase/budibase/commits?author=kevmodrome" title="Tests">⚠️</a></td>
<td align="center"><a href="https://www.budibase.com/"><img src="https://avatars2.githubusercontent.com/u/49767913?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Joe</b></sub></a><br /><a href="https://github.com/Budibase/budibase/commits?author=joebudi" title="Documentation">📖</a> <a href="https://github.com/Budibase/budibase/commits?author=joebudi" title="Code">💻</a> <a href="#content-joebudi" title="Content">🖋</a> <a href="#design-joebudi" title="Design">🎨</a></td>
<td align="center"><a href="https://github.com/Rory-Powell"><img src="https://avatars.githubusercontent.com/u/8755148?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Rory Powell</b></sub></a><br /><a href="https://github.com/Budibase/budibase/commits?author=Rory-Powell" title="Code">💻</a> <a href="https://github.com/Budibase/budibase/commits?author=Rory-Powell" title="Documentation">📖</a> <a href="https://github.com/Budibase/budibase/commits?author=Rory-Powell" title="Tests">⚠️</a></td>
</tr>
<tr>
<td align="center"><a href="https://github.com/PClmnt"><img src="https://avatars.githubusercontent.com/u/5665926?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Peter Clement</b></sub></a><br /><a href="https://github.com/Budibase/budibase/commits?author=PClmnt" title="Code">💻</a> <a href="https://github.com/Budibase/budibase/commits?author=PClmnt" title="Documentation">📖</a> <a href="https://github.com/Budibase/budibase/commits?author=PClmnt" title="Tests">⚠️</a></td>
<td align="center"><a href="https://github.com/Conor-Mack"><img src="https://avatars1.githubusercontent.com/u/36074859?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Conor_Mack</b></sub></a><br /><a href="https://github.com/Budibase/budibase/commits?author=Conor-Mack" title="Code">💻</a> <a href="https://github.com/Budibase/budibase/commits?author=Conor-Mack" title="Tests">⚠️</a></td>
<td align="center"><a href="https://github.com/pngwn"><img src="https://avatars1.githubusercontent.com/u/12937446?v=4?s=100" width="100px;" alt=""/><br /><sub><b>pngwn</b></sub></a><br /><a href="https://github.com/Budibase/budibase/commits?author=pngwn" title="Code">💻</a> <a href="https://github.com/Budibase/budibase/commits?author=pngwn" title="Tests">⚠️</a></td>
<td align="center"><a href="https://github.com/HugoLd"><img src="https://avatars0.githubusercontent.com/u/26521848?v=4?s=100" width="100px;" alt=""/><br /><sub><b>HugoLd</b></sub></a><br /><a href="https://github.com/Budibase/budibase/commits?author=HugoLd" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/victoriasloan"><img src="https://avatars.githubusercontent.com/u/9913651?v=4?s=100" width="100px;" alt=""/><br /><sub><b>victoriasloan</b></sub></a><br /><a href="https://github.com/Budibase/budibase/commits?author=victoriasloan" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/yashank09"><img src="https://avatars.githubusercontent.com/u/37672190?v=4?s=100" width="100px;" alt=""/><br /><sub><b>yashank09</b></sub></a><br /><a href="https://github.com/Budibase/budibase/commits?author=yashank09" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/SOVLOOKUP"><img src="https://avatars.githubusercontent.com/u/53158137?v=4?s=100" width="100px;" alt=""/><br /><sub><b>SOVLOOKUP</b></sub></a><br /><a href="https://github.com/Budibase/budibase/commits?author=SOVLOOKUP" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/seoulaja"><img src="https://avatars.githubusercontent.com/u/15101654?v=4?s=100" width="100px;" alt=""/><br /><sub><b>seoulaja</b></sub></a><br /><a href="#translation-seoulaja" title="Translation">🌍</a></td>
<td align="center"><a href="https://github.com/mslourens"><img src="https://avatars.githubusercontent.com/u/1907152?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Maurits Lourens</b></sub></a><br /><a href="https://github.com/Budibase/budibase/commits?author=mslourens" title="Tests">⚠️</a> <a href="https://github.com/Budibase/budibase/commits?author=mslourens" title="Code">💻</a></td>
</tr>
<tr>
<td align="center"><a href="https://github.com/Rory-Powell"><img src="https://avatars.githubusercontent.com/u/8755148?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Rory Powell</b></sub></a><br /><a href="#infra-Rory-Powell" title="Infrastructure (Hosting, Build-Tools, etc)">🚇</a> <a href="https://github.com/Budibase/budibase/commits?author=Rory-Powell" title="Tests">⚠️</a> <a href="https://github.com/Budibase/budibase/commits?author=Rory-Powell" title="Code">💻</a></td>
</tr>
</table>
<!-- markdownlint-restore -->
<!-- prettier-ignore-end -->
<!-- ALL-CONTRIBUTORS-LIST:END -->
Ce projet suit la spécification [all-contributors](https://allcontributors.org/docs/fr/overview). Les contributions de toute nature sont les bienvenues !

View File

@ -1,5 +1,5 @@
{ {
"version": "1.0.23", "version": "1.0.27-alpha.1",
"npmClient": "yarn", "npmClient": "yarn",
"packages": [ "packages": [
"packages/*" "packages/*"

View File

@ -23,8 +23,8 @@
"build": "lerna run build", "build": "lerna run build",
"publishdev": "lerna run publishdev", "publishdev": "lerna run publishdev",
"publishnpm": "yarn build && lerna publish --force-publish", "publishnpm": "yarn build && lerna publish --force-publish",
"release": "yarn build && lerna publish patch --yes --force-publish", "release": "lerna publish patch --yes --force-publish",
"release:develop": "yarn build && lerna publish prerelease --yes --force-publish --dist-tag develop", "release:develop": "lerna publish prerelease --yes --force-publish --dist-tag develop",
"restore": "yarn run clean && yarn run bootstrap && yarn run build", "restore": "yarn run clean && yarn run bootstrap && yarn run build",
"nuke": "yarn run nuke:packages && yarn run nuke:docker", "nuke": "yarn run nuke:packages && yarn run nuke:docker",
"nuke:packages": "yarn run restore", "nuke:packages": "yarn run restore",

View File

@ -1,6 +1,6 @@
{ {
"name": "@budibase/auth", "name": "@budibase/auth",
"version": "1.0.23", "version": "1.0.27-alpha.1",
"description": "Authentication middlewares for budibase builder and apps", "description": "Authentication middlewares for budibase builder and apps",
"main": "src/index.js", "main": "src/index.js",
"author": "Budibase", "author": "Budibase",

View File

@ -1,7 +1,7 @@
{ {
"name": "@budibase/bbui", "name": "@budibase/bbui",
"description": "A UI solution used in the different Budibase projects.", "description": "A UI solution used in the different Budibase projects.",
"version": "1.0.23", "version": "1.0.27-alpha.1",
"license": "MPL-2.0", "license": "MPL-2.0",
"svelte": "src/index.js", "svelte": "src/index.js",
"module": "dist/bbui.es.js", "module": "dist/bbui.es.js",

View File

@ -21,16 +21,8 @@
} }
visible = false visible = false
} }
function handleKey(e) {
if (visible && e.key === "Escape") {
hide()
}
}
</script> </script>
<svelte:window on:keydown={handleKey} />
{#if visible} {#if visible}
<Portal> <Portal>
<section class:fillWidth class="drawer" transition:slide> <section class:fillWidth class="drawer" transition:slide>

View File

@ -4,6 +4,7 @@
import CellRenderer from "./CellRenderer.svelte" import CellRenderer from "./CellRenderer.svelte"
import SelectEditRenderer from "./SelectEditRenderer.svelte" import SelectEditRenderer from "./SelectEditRenderer.svelte"
import { cloneDeep } from "lodash" import { cloneDeep } from "lodash"
import { deepGet } from "../utils/helpers"
/** /**
* The expected schema is our normal couch schemas for our tables. * The expected schema is our normal couch schemas for our tables.
@ -318,7 +319,7 @@
{customRenderers} {customRenderers}
{row} {row}
schema={schema[field]} schema={schema[field]}
value={row[field]} value={deepGet(row, field)}
on:clickrelationship on:clickrelationship
> >
<slot /> <slot />

View File

@ -77,3 +77,6 @@ export { default as clickOutside } from "./Actions/click_outside"
// Stores // Stores
export { notifications, createNotificationStore } from "./Stores/notifications" export { notifications, createNotificationStore } from "./Stores/notifications"
// Utils
export * from "./utils/helpers"

View File

@ -6,3 +6,61 @@ export const generateID = () => {
} }
export const capitalise = s => s.substring(0, 1).toUpperCase() + s.substring(1) export const capitalise = s => s.substring(0, 1).toUpperCase() + s.substring(1)
/**
* Gets a key within an object. The key supports dot syntax for retrieving deep
* fields - e.g. "a.b.c".
* Exact matches of keys with dots in them take precedence over nested keys of
* the same path - e.g. getting "a.b" from { "a.b": "foo", a: { b: "bar" } }
* will return "foo" over "bar".
* @param obj the object
* @param key the key
* @return {*|null} the value or null if a value was not found for this key
*/
export const deepGet = (obj, key) => {
if (!obj || !key) {
return null
}
if (Object.prototype.hasOwnProperty.call(obj, key)) {
return obj[key]
}
const split = key.split(".")
for (let i = 0; i < split.length; i++) {
obj = obj?.[split[i]]
}
return obj
}
/**
* Sets a key within an object. The key supports dot syntax for retrieving deep
* fields - e.g. "a.b.c".
* Exact matches of keys with dots in them take precedence over nested keys of
* the same path - e.g. setting "a.b" of { "a.b": "foo", a: { b: "bar" } }
* will override the value "foo" rather than "bar".
* If a deep path is specified and the parent keys don't exist then these will
* be created.
* @param obj the object
* @param key the key
* @param value the value
*/
export const deepSet = (obj, key, value) => {
if (!obj || !key) {
return
}
if (Object.prototype.hasOwnProperty.call(obj, key)) {
obj[key] = value
return
}
const split = key.split(".")
for (let i = 0; i < split.length - 1; i++) {
const nextKey = split[i]
if (obj && obj[nextKey] == null) {
obj[nextKey] = {}
}
obj = obj?.[nextKey]
}
if (!obj) {
return
}
obj[split[split.length - 1]] = value
}

View File

@ -1,6 +1,6 @@
{ {
"name": "@budibase/builder", "name": "@budibase/builder",
"version": "1.0.23", "version": "1.0.27-alpha.1",
"license": "GPL-3.0", "license": "GPL-3.0",
"private": true, "private": true,
"scripts": { "scripts": {
@ -65,10 +65,10 @@
} }
}, },
"dependencies": { "dependencies": {
"@budibase/bbui": "^1.0.23", "@budibase/bbui": "^1.0.27-alpha.1",
"@budibase/client": "^1.0.23", "@budibase/client": "^1.0.27-alpha.1",
"@budibase/colorpicker": "1.1.2", "@budibase/colorpicker": "1.1.2",
"@budibase/string-templates": "^1.0.23", "@budibase/string-templates": "^1.0.27-alpha.1",
"@sentry/browser": "5.19.1", "@sentry/browser": "5.19.1",
"@spectrum-css/page": "^3.0.1", "@spectrum-css/page": "^3.0.1",
"@spectrum-css/vars": "^3.0.1", "@spectrum-css/vars": "^3.0.1",

View File

@ -127,18 +127,37 @@ const searchComponentTree = (rootComponent, matchComponent) => {
} }
/** /**
* Searches a component's definition for a setting matching a certin predicate. * Searches a component's definition for a setting matching a certain predicate.
* These settings are cached because they cannot change at run time.
*/ */
let componentSettingCache = {}
export const getComponentSettings = componentType => { export const getComponentSettings = componentType => {
const def = store.actions.components.getDefinition(componentType) if (!componentType) {
if (!def) {
return [] return []
} }
let settings = def.settings?.filter(setting => !setting.section) ?? []
// Ensure whole component name is used
if (!componentType.startsWith("@budibase")) {
componentType = `@budibase/standard-components/${componentType}`
}
// Check if we have cached this type already
if (componentSettingCache[componentType]) {
return componentSettingCache[componentType]
}
// Otherwise get the settings and cache them
const def = store.actions.components.getDefinition(componentType)
let settings = []
if (def) {
settings = def.settings?.filter(setting => !setting.section) ?? []
def.settings def.settings
?.filter(setting => setting.section) ?.filter(setting => setting.section)
.forEach(section => { .forEach(section => {
settings = settings.concat(section.settings || []) settings = settings.concat(section.settings || [])
}) })
}
componentSettingCache[componentType] = settings
return settings return settings
} }

View File

@ -5,7 +5,7 @@ import {
findComponent, findComponent,
findComponentPath, findComponentPath,
getComponentSettings, getComponentSettings,
} from "./storeUtils" } from "./componentUtils"
import { store } from "builderStore" import { store } from "builderStore"
import { queries as queriesStores, tables as tablesStore } from "stores/backend" import { queries as queriesStores, tables as tablesStore } from "stores/backend"
import { import {
@ -15,6 +15,11 @@ import {
encodeJSBinding, encodeJSBinding,
} from "@budibase/string-templates" } from "@budibase/string-templates"
import { TableNames } from "../constants" import { TableNames } from "../constants"
import {
convertJSONSchemaToTableSchema,
getJSONArrayDatasourceSchema,
} from "./jsonUtils"
import ActionDefinitions from "components/design/PropertiesPanel/PropertyControls/ButtonActionEditor/manifest.json"
// Regex to match all instances of template strings // Regex to match all instances of template strings
const CAPTURE_VAR_INSIDE_TEMPLATE = /{{([^}]+)}}/g const CAPTURE_VAR_INSIDE_TEMPLATE = /{{([^}]+)}}/g
@ -186,6 +191,7 @@ const getProviderContextBindings = (asset, dataProviders) => {
} }
let schema let schema
let table
let readablePrefix let readablePrefix
let runtimeSuffix = context.suffix let runtimeSuffix = context.suffix
@ -209,8 +215,17 @@ const getProviderContextBindings = (asset, dataProviders) => {
} }
const info = getSchemaForDatasource(asset, datasource) const info = getSchemaForDatasource(asset, datasource)
schema = info.schema schema = info.schema
table = info.table
// For JSON arrays, use the array name as the readable prefix.
// Otherwise use the table name
if (datasource.type === "jsonarray") {
const split = datasource.label.split(".")
readablePrefix = split[split.length - 1]
} else {
readablePrefix = info.table?.name readablePrefix = info.table?.name
} }
}
if (!schema) { if (!schema) {
return return
} }
@ -229,7 +244,8 @@ const getProviderContextBindings = (asset, dataProviders) => {
const fieldSchema = schema[key] const fieldSchema = schema[key]
// Make safe runtime binding // Make safe runtime binding
const runtimeBinding = `${safeComponentId}.${makePropSafe(key)}` const safeKey = key.split(".").map(makePropSafe).join(".")
const runtimeBinding = `${safeComponentId}.${safeKey}`
// Optionally use a prefix with readable bindings // Optionally use a prefix with readable bindings
let readableBinding = component._instanceName let readableBinding = component._instanceName
@ -247,6 +263,8 @@ const getProviderContextBindings = (asset, dataProviders) => {
// datasource options, based on bindable properties // datasource options, based on bindable properties
fieldSchema, fieldSchema,
providerId, providerId,
// Table ID is used by JSON fields to know what table the field is in
tableId: table?._id,
}) })
}) })
}) })
@ -339,6 +357,36 @@ const getUrlBindings = asset => {
})) }))
} }
/**
* Gets all bindable properties exposed in a button actions flow up until
* the specified action ID.
*/
export const getButtonContextBindings = (actions, actionId) => {
// Get the steps leading up to this value
const index = actions?.findIndex(action => action.id === actionId)
if (index == null || index === -1) {
return []
}
const prevActions = actions.slice(0, index)
// Generate bindings for any steps which provide context
let bindings = []
prevActions.forEach((action, idx) => {
const def = ActionDefinitions.actions.find(
x => x.name === action["##eventHandlerType"]
)
if (def.context) {
def.context.forEach(contextValue => {
bindings.push({
readableBinding: `Action ${idx + 1}.${contextValue.label}`,
runtimeBinding: `actions.${idx}.${contextValue.value}`,
})
})
}
})
return bindings
}
/** /**
* Gets a schema for a datasource object. * Gets a schema for a datasource object.
*/ */
@ -347,16 +395,26 @@ export const getSchemaForDatasource = (asset, datasource, isForm = false) => {
if (datasource) { if (datasource) {
const { type } = datasource const { type } = datasource
const tables = get(tablesStore).list
// Determine the source table from the datasource type // Determine the entity which backs this datasource.
// "provider" datasources are those targeting another data provider
if (type === "provider") { if (type === "provider") {
const component = findComponent(asset.props, datasource.providerId) const component = findComponent(asset.props, datasource.providerId)
const source = getDatasourceForProvider(asset, component) const source = getDatasourceForProvider(asset, component)
return getSchemaForDatasource(asset, source, isForm) return getSchemaForDatasource(asset, source, isForm)
} else if (type === "query") { }
// "query" datasources are those targeting non-plus datasources or
// custom queries
else if (type === "query") {
const queries = get(queriesStores).list const queries = get(queriesStores).list
table = queries.find(query => query._id === datasource._id) table = queries.find(query => query._id === datasource._id)
} else if (type === "field") { }
// "field" datasources are array-like fields of rows, such as attachments
// or multi-select fields
else if (type === "field") {
table = { name: datasource.fieldName } table = { name: datasource.fieldName }
const { fieldType } = datasource const { fieldType } = datasource
if (fieldType === "attachment") { if (fieldType === "attachment") {
@ -375,12 +433,22 @@ export const getSchemaForDatasource = (asset, datasource, isForm = false) => {
}, },
} }
} }
} else { }
const tables = get(tablesStore).list
// "jsonarray" datasources are arrays inside JSON fields
else if (type === "jsonarray") {
table = tables.find(table => table._id === datasource.tableId)
let tableSchema = table?.schema
schema = getJSONArrayDatasourceSchema(tableSchema, datasource)
}
// Otherwise we assume we're targeting an internal table or a plus
// datasource, and we can treat it as a table with a schema
else {
table = tables.find(table => table._id === datasource.tableId) table = tables.find(table => table._id === datasource.tableId)
} }
// Determine the schema from the table if not already determined // Determine the schema from the backing entity if not already determined
if (table && !schema) { if (table && !schema) {
if (type === "view") { if (type === "view") {
schema = cloneDeep(table.views?.[datasource.name]?.schema) schema = cloneDeep(table.views?.[datasource.name]?.schema)
@ -397,6 +465,26 @@ export const getSchemaForDatasource = (asset, datasource, isForm = false) => {
} }
} }
// Check for any JSON fields so we can add any top level properties
if (schema) {
let jsonAdditions = {}
Object.keys(schema).forEach(fieldKey => {
const fieldSchema = schema[fieldKey]
if (fieldSchema?.type === "json") {
const jsonSchema = convertJSONSchemaToTableSchema(fieldSchema, {
squashObjects: true,
})
Object.keys(jsonSchema).forEach(jsonKey => {
jsonAdditions[`${fieldKey}.${jsonKey}`] = {
type: jsonSchema[jsonKey].type,
nestedJSON: true,
}
})
}
})
schema = { ...schema, ...jsonAdditions }
}
// Add _id and _rev fields for certain types // Add _id and _rev fields for certain types
if (schema && !isForm && ["table", "link"].includes(datasource.type)) { if (schema && !isForm && ["table", "link"].includes(datasource.type)) {
schema["_id"] = { type: "string" } schema["_id"] = { type: "string" }
@ -450,15 +538,58 @@ const buildFormSchema = component => {
return schema return schema
} }
/**
* Returns an array of the keys of any state variables which are set anywhere
* in the app.
*/
export const getAllStateVariables = () => {
// Get all component containing assets
let allAssets = []
allAssets = allAssets.concat(get(store).layouts || [])
allAssets = allAssets.concat(get(store).screens || [])
// Find all button action settings in all components
let eventSettings = []
allAssets.forEach(asset => {
findAllMatchingComponents(asset.props, component => {
const settings = getComponentSettings(component._component)
settings
.filter(setting => setting.type === "event")
.forEach(setting => {
eventSettings.push(component[setting.key])
})
})
})
// Extract all state keys from any "update state" actions in each setting
let bindingSet = new Set()
eventSettings.forEach(setting => {
if (!Array.isArray(setting)) {
return
}
setting.forEach(action => {
if (
action["##eventHandlerType"] === "Update State" &&
action.parameters?.type === "set" &&
action.parameters?.key &&
action.parameters?.value
) {
bindingSet.add(action.parameters.key)
}
})
})
return Array.from(bindingSet)
}
/** /**
* Recurses the input object to remove any instances of bindings. * Recurses the input object to remove any instances of bindings.
*/ */
export function removeBindings(obj) { export const removeBindings = (obj, replacement = "Invalid binding") => {
for (let [key, value] of Object.entries(obj)) { for (let [key, value] of Object.entries(obj)) {
if (value && typeof value === "object") { if (value && typeof value === "object") {
obj[key] = removeBindings(value) obj[key] = removeBindings(value, replacement)
} else if (typeof value === "string") { } else if (typeof value === "string") {
obj[key] = value.replace(CAPTURE_HBS_TEMPLATE, "Invalid binding") obj[key] = value.replace(CAPTURE_HBS_TEMPLATE, replacement)
} }
} }
return obj return obj
@ -468,8 +599,8 @@ export function removeBindings(obj) {
* When converting from readable to runtime it can sometimes add too many square brackets, * When converting from readable to runtime it can sometimes add too many square brackets,
* this makes sure that doesn't happen. * this makes sure that doesn't happen.
*/ */
function shouldReplaceBinding(currentValue, from, convertTo) { const shouldReplaceBinding = (currentValue, convertFrom, convertTo) => {
if (!currentValue?.includes(from)) { if (!currentValue?.includes(convertFrom)) {
return false return false
} }
if (convertTo === "readableBinding") { if (convertTo === "readableBinding") {
@ -478,7 +609,7 @@ function shouldReplaceBinding(currentValue, from, convertTo) {
// remove all the spaces, if the input is surrounded by spaces e.g. [ Auto ID ] then // remove all the spaces, if the input is surrounded by spaces e.g. [ Auto ID ] then
// this makes sure it is detected // this makes sure it is detected
const noSpaces = currentValue.replace(/\s+/g, "") const noSpaces = currentValue.replace(/\s+/g, "")
const fromNoSpaces = from.replace(/\s+/g, "") const fromNoSpaces = convertFrom.replace(/\s+/g, "")
const invalids = [ const invalids = [
`[${fromNoSpaces}]`, `[${fromNoSpaces}]`,
`"${fromNoSpaces}"`, `"${fromNoSpaces}"`,
@ -487,14 +618,21 @@ function shouldReplaceBinding(currentValue, from, convertTo) {
return !invalids.find(invalid => noSpaces?.includes(invalid)) return !invalids.find(invalid => noSpaces?.includes(invalid))
} }
function replaceBetween(string, start, end, replacement) { /**
* Utility function which replaces a string between given indices.
*/
const replaceBetween = (string, start, end, replacement) => {
return string.substring(0, start) + replacement + string.substring(end) return string.substring(0, start) + replacement + string.substring(end)
} }
/** /**
* utility function for the readableToRuntimeBinding and runtimeToReadableBinding. * Utility function for the readableToRuntimeBinding and runtimeToReadableBinding.
*/ */
function bindingReplacement(bindableProperties, textWithBindings, convertTo) { const bindingReplacement = (
bindableProperties,
textWithBindings,
convertTo
) => {
// Decide from base64 if using JS // Decide from base64 if using JS
const isJS = isJSBinding(textWithBindings) const isJS = isJSBinding(textWithBindings)
if (isJS) { if (isJS) {
@ -559,14 +697,17 @@ function bindingReplacement(bindableProperties, textWithBindings, convertTo) {
* Extracts a component ID from a handlebars expression setting of * Extracts a component ID from a handlebars expression setting of
* {{ literal [componentId] }} * {{ literal [componentId] }}
*/ */
function extractLiteralHandlebarsID(value) { const extractLiteralHandlebarsID = value => {
return value?.match(/{{\s*literal\s*\[+([^\]]+)].*}}/)?.[1] return value?.match(/{{\s*literal\s*\[+([^\]]+)].*}}/)?.[1]
} }
/** /**
* Converts a readable data binding into a runtime data binding * Converts a readable data binding into a runtime data binding
*/ */
export function readableToRuntimeBinding(bindableProperties, textWithBindings) { export const readableToRuntimeBinding = (
bindableProperties,
textWithBindings
) => {
return bindingReplacement( return bindingReplacement(
bindableProperties, bindableProperties,
textWithBindings, textWithBindings,
@ -577,56 +718,13 @@ export function readableToRuntimeBinding(bindableProperties, textWithBindings) {
/** /**
* Converts a runtime data binding into a readable data binding * Converts a runtime data binding into a readable data binding
*/ */
export function runtimeToReadableBinding(bindableProperties, textWithBindings) { export const runtimeToReadableBinding = (
bindableProperties,
textWithBindings
) => {
return bindingReplacement( return bindingReplacement(
bindableProperties, bindableProperties,
textWithBindings, textWithBindings,
"readableBinding" "readableBinding"
) )
} }
/**
* Returns an array of the keys of any state variables which are set anywhere
* in the app.
*/
export const getAllStateVariables = () => {
let allComponents = []
// Find all onClick settings in all layouts
get(store).layouts.forEach(layout => {
const components = findAllMatchingComponents(
layout.props,
c => c.onClick != null
)
allComponents = allComponents.concat(components || [])
})
// Find all onClick settings in all screens
get(store).screens.forEach(screen => {
const components = findAllMatchingComponents(
screen.props,
c => c.onClick != null
)
allComponents = allComponents.concat(components || [])
})
// Add state bindings for all state actions
let bindingSet = new Set()
allComponents.forEach(component => {
if (!Array.isArray(component.onClick)) {
return
}
component.onClick.forEach(action => {
if (
action["##eventHandlerType"] === "Update State" &&
action.parameters?.type === "set" &&
action.parameters?.key &&
action.parameters?.value
) {
bindingSet.add(action.parameters.key)
}
})
})
return Array.from(bindingSet)
}

View File

@ -4,7 +4,7 @@ import { getHostingStore } from "./store/hosting"
import { getThemeStore } from "./store/theme" import { getThemeStore } from "./store/theme"
import { derived, writable } from "svelte/store" import { derived, writable } from "svelte/store"
import { FrontendTypes, LAYOUT_NAMES } from "../constants" import { FrontendTypes, LAYOUT_NAMES } from "../constants"
import { findComponent } from "./storeUtils" import { findComponent } from "./componentUtils"
export const store = getFrontendStore() export const store = getFrontendStore()
export const automationStore = getAutomationStore() export const automationStore = getAutomationStore()

View File

@ -0,0 +1,121 @@
/**
* Gets the schema for a datasource which is targeting a JSON array, including
* nested JSON arrays. The returned schema is a squashed, table-like schema
* which is fully compatible with the rest of the platform.
* @param tableSchema the full schema for the table this JSON field is in
* @param datasource the datasource configuration
*/
export const getJSONArrayDatasourceSchema = (tableSchema, datasource) => {
let jsonSchema = tableSchema
let keysToSchema = []
// If we are already deep inside a JSON field then we need to account
// for the keys that brought us here, so we can get the schema for the
// depth we're actually at
if (datasource.prefixKeys) {
keysToSchema = datasource.prefixKeys.concat(["schema"])
}
// We parse the label of the datasource to work out where we are inside
// the structure. We can use this to know which part of the schema
// is available underneath our current position.
keysToSchema = keysToSchema.concat(datasource.label.split(".").slice(2))
// Follow the JSON key path until we reach the schema for the level
// we are at
for (let i = 0; i < keysToSchema.length; i++) {
jsonSchema = jsonSchema?.[keysToSchema[i]]
if (jsonSchema?.schema) {
jsonSchema = jsonSchema.schema
}
}
// We need to convert the JSON schema into a more typical looking table
// schema so that it works with the rest of the platform
return convertJSONSchemaToTableSchema(jsonSchema, {
squashObjects: true,
prefixKeys: keysToSchema,
})
}
/**
* Converts a JSON field schema (or sub-schema of a nested field) into a schema
* that looks like a typical table schema.
* @param jsonSchema the JSON field schema or sub-schema
* @param options
*/
export const convertJSONSchemaToTableSchema = (jsonSchema, options) => {
if (!jsonSchema) {
return null
}
// Add default options
options = { squashObjects: false, prefixKeys: null, ...options }
// Immediately strip the wrapper schema for objects, or wrap shallow values in
// a fake "value" schema
if (jsonSchema.schema) {
jsonSchema = jsonSchema.schema
} else {
jsonSchema = {
value: jsonSchema,
}
}
// Extract all deep keys from the schema
const keys = extractJSONSchemaKeys(jsonSchema, options.squashObjects)
// Form a full schema from all the deep schema keys
let schema = {}
keys.forEach(({ key, type }) => {
schema[key] = { type, name: key, prefixKeys: options.prefixKeys }
})
return schema
}
/**
* Recursively builds paths to all leaf fields in a JSON field schema structure,
* stopping when leaf nodes or arrays are reached.
* @param jsonSchema the JSON field schema or sub-schema
* @param squashObjects whether to recurse into objects or not
*/
const extractJSONSchemaKeys = (jsonSchema, squashObjects = false) => {
if (!jsonSchema || !Object.keys(jsonSchema).length) {
return []
}
// Iterate through every schema key
let keys = []
Object.keys(jsonSchema).forEach(key => {
const type = jsonSchema[key].type
// If we encounter an object, then only go deeper if we want to squash
// object paths
if (type === "json" && squashObjects) {
// Find all keys within this objects schema
const childKeys = extractJSONSchemaKeys(
jsonSchema[key].schema,
squashObjects
)
// Append child paths onto the current path to build the full path
keys = keys.concat(
childKeys.map(childKey => ({
key: `${key}.${childKey.key}`,
type: childKey.type,
}))
)
}
// Otherwise add this as a lead node.
// We transform array types from "array" into "jsonarray" here to avoid
// confusion with the existing "array" type that represents a multi-select.
else {
keys.push({
key,
type: type === "array" ? "jsonarray" : type,
})
}
})
return keys
}

View File

@ -0,0 +1,56 @@
import { FIELDS } from "constants/backend"
function baseConversion(type) {
if (type === "string") {
return {
type: FIELDS.STRING.type,
}
} else if (type === "boolean") {
return {
type: FIELDS.BOOLEAN.type,
}
} else if (type === "number") {
return {
type: FIELDS.NUMBER.type,
}
}
}
function recurse(schemaLevel = {}, objectLevel) {
if (!objectLevel) {
return null
}
const baseType = typeof objectLevel
if (baseType !== "object") {
return baseConversion(baseType)
}
for (let [key, value] of Object.entries(objectLevel)) {
const type = typeof value
// check array first, since arrays are objects
if (Array.isArray(value)) {
const schema = recurse(schemaLevel[key], value[0])
if (schema) {
schemaLevel[key] = {
type: FIELDS.ARRAY.type,
schema,
}
}
} else if (type === "object") {
const schema = recurse(schemaLevel[key], objectLevel[key])
if (schema) {
schemaLevel[key] = schema
}
} else {
schemaLevel[key] = baseConversion(type)
}
}
if (!schemaLevel.type) {
return { type: FIELDS.JSON.type, schema: schemaLevel }
} else {
return schemaLevel
}
}
export function generate(object) {
return recurse({}, object).schema
}

View File

@ -26,7 +26,7 @@ import {
findAllMatchingComponents, findAllMatchingComponents,
findComponent, findComponent,
getComponentSettings, getComponentSettings,
} from "../storeUtils" } from "../componentUtils"
import { uuid } from "../uuid" import { uuid } from "../uuid"
import { removeBindings } from "../dataBinding" import { removeBindings } from "../dataBinding"
@ -329,12 +329,12 @@ export const getFrontendStore = () => {
}, },
components: { components: {
select: component => { select: component => {
if (!component) { const asset = get(currentAsset)
if (!asset || !component) {
return return
} }
// If this is the root component, select the asset instead // If this is the root component, select the asset instead
const asset = get(currentAsset)
const parent = findComponentParent(asset.props, component._id) const parent = findComponentParent(asset.props, component._id)
if (parent == null) { if (parent == null) {
const state = get(store) const state = get(store)
@ -537,7 +537,7 @@ export const getFrontendStore = () => {
// immediately need to remove bindings, currently these aren't valid when pasted // immediately need to remove bindings, currently these aren't valid when pasted
if (!cut && !preserveBindings) { if (!cut && !preserveBindings) {
state.componentToPaste = removeBindings(state.componentToPaste) state.componentToPaste = removeBindings(state.componentToPaste, "")
} }
// Clone the component to paste // Clone the component to paste

View File

@ -137,6 +137,7 @@ const fieldTypeToComponentMap = {
datetime: "datetimefield", datetime: "datetimefield",
attachment: "attachmentfield", attachment: "attachmentfield",
link: "relationshipfield", link: "relationshipfield",
json: "jsonfield",
} }
export function makeDatasourceFormComponents(datasource) { export function makeDatasourceFormComponents(datasource) {
@ -146,7 +147,7 @@ export function makeDatasourceFormComponents(datasource) {
fields.forEach(field => { fields.forEach(field => {
const fieldSchema = schema[field] const fieldSchema = schema[field]
// skip autocolumns // skip autocolumns
if (fieldSchema.autocolumn) { if (fieldSchema.autocolumn || fieldSchema.nestedJSON) {
return return
} }
const fieldType = const fieldType =

View File

@ -6,16 +6,20 @@
Toggle, Toggle,
TextArea, TextArea,
Multiselect, Multiselect,
Label,
} from "@budibase/bbui" } from "@budibase/bbui"
import Dropzone from "components/common/Dropzone.svelte" import Dropzone from "components/common/Dropzone.svelte"
import { capitalise } from "helpers" import { capitalise } from "helpers"
import LinkedRowSelector from "components/common/LinkedRowSelector.svelte" import LinkedRowSelector from "components/common/LinkedRowSelector.svelte"
import Editor from "../../integration/QueryEditor.svelte"
export let defaultValue export let defaultValue
export let meta export let meta
export let value = defaultValue || (meta.type === "boolean" ? false : "") export let value = defaultValue || (meta.type === "boolean" ? false : "")
export let readonly export let readonly
$: stringVal =
typeof value === "object" ? JSON.stringify(value, null, 2) : value
$: type = meta?.type $: type = meta?.type
$: label = meta.name ? capitalise(meta.name) : "" $: label = meta.name ? capitalise(meta.name) : ""
</script> </script>
@ -40,6 +44,14 @@
<LinkedRowSelector bind:linkedRows={value} schema={meta} /> <LinkedRowSelector bind:linkedRows={value} schema={meta} />
{:else if type === "longform"} {:else if type === "longform"}
<TextArea {label} bind:value /> <TextArea {label} bind:value />
{:else if type === "json"}
<Label>{label}</Label>
<Editor
editorHeight="250"
mode="json"
on:change={({ detail }) => (value = detail.value)}
value={stringVal}
/>
{:else} {:else}
<Input <Input
{label} {label}

View File

@ -9,6 +9,7 @@
DatePicker, DatePicker,
ModalContent, ModalContent,
Context, Context,
Modal,
notifications, notifications,
} from "@budibase/bbui" } from "@budibase/bbui"
import { createEventDispatcher } from "svelte" import { createEventDispatcher } from "svelte"
@ -32,12 +33,14 @@
import ModalBindableInput from "components/common/bindings/ModalBindableInput.svelte" import ModalBindableInput from "components/common/bindings/ModalBindableInput.svelte"
import { getBindings } from "components/backend/DataTable/formula" import { getBindings } from "components/backend/DataTable/formula"
import { getContext } from "svelte" import { getContext } from "svelte"
import JSONSchemaModal from "./JSONSchemaModal.svelte"
const AUTO_TYPE = "auto" const AUTO_TYPE = "auto"
const FORMULA_TYPE = FIELDS.FORMULA.type const FORMULA_TYPE = FIELDS.FORMULA.type
const LINK_TYPE = FIELDS.LINK.type const LINK_TYPE = FIELDS.LINK.type
const STRING_TYPE = FIELDS.STRING.type const STRING_TYPE = FIELDS.STRING.type
const NUMBER_TYPE = FIELDS.NUMBER.type const NUMBER_TYPE = FIELDS.NUMBER.type
const JSON_TYPE = FIELDS.JSON.type
const DATE_TYPE = FIELDS.DATETIME.type const DATE_TYPE = FIELDS.DATETIME.type
const dispatch = createEventDispatcher() const dispatch = createEventDispatcher()
@ -64,6 +67,7 @@
let confirmDeleteDialog let confirmDeleteDialog
let deletion let deletion
let deleteColName let deleteColName
let jsonSchemaModal
$: checkConstraints(field) $: checkConstraints(field)
$: required = !!field?.constraints?.presence || primaryDisplay $: required = !!field?.constraints?.presence || primaryDisplay
@ -79,10 +83,14 @@
// used to select what different options can be displayed for column type // used to select what different options can be displayed for column type
$: canBeSearched = $: canBeSearched =
field.type !== LINK_TYPE && field.type !== LINK_TYPE &&
field.type !== JSON_TYPE &&
field.subtype !== AUTO_COLUMN_SUB_TYPES.CREATED_BY && field.subtype !== AUTO_COLUMN_SUB_TYPES.CREATED_BY &&
field.subtype !== AUTO_COLUMN_SUB_TYPES.UPDATED_BY && field.subtype !== AUTO_COLUMN_SUB_TYPES.UPDATED_BY &&
field.type !== FORMULA_TYPE field.type !== FORMULA_TYPE
$: canBeDisplay = field.type !== LINK_TYPE && field.type !== AUTO_TYPE $: canBeDisplay =
field.type !== LINK_TYPE &&
field.type !== AUTO_TYPE &&
field.type !== JSON_TYPE
$: canBeRequired = $: canBeRequired =
field.type !== LINK_TYPE && !uneditable && field.type !== AUTO_TYPE field.type !== LINK_TYPE && !uneditable && field.type !== AUTO_TYPE
$: relationshipOptions = getRelationshipOptions(field) $: relationshipOptions = getRelationshipOptions(field)
@ -176,6 +184,10 @@
} }
} }
function openJsonSchemaEditor() {
jsonSchemaModal.show()
}
function confirmDelete() { function confirmDelete() {
confirmDeleteDialog.show() confirmDeleteDialog.show()
deletion = true deletion = true
@ -430,6 +442,10 @@
getOptionLabel={option => option[1].name} getOptionLabel={option => option[1].name}
getOptionValue={option => option[0]} getOptionValue={option => option[0]}
/> />
{:else if field.type === JSON_TYPE}
<Button primary text on:click={openJsonSchemaEditor}
>Open schema editor</Button
>
{/if} {/if}
<div slot="footer"> <div slot="footer">
@ -438,6 +454,16 @@
{/if} {/if}
</div> </div>
</ModalContent> </ModalContent>
<Modal bind:this={jsonSchemaModal}>
<JSONSchemaModal
schema={field.schema}
json={field.json}
on:save={({ detail }) => {
field.schema = detail.schema
field.json = detail.json
}}
/>
</Modal>
<ConfirmDialog <ConfirmDialog
bind:this={confirmDeleteDialog} bind:this={confirmDeleteDialog}
okText="Delete Column" okText="Delete Column"

View File

@ -0,0 +1,129 @@
<script>
import Editor from "components/integration/QueryEditor.svelte"
import {
ModalContent,
Tabs,
Tab,
Button,
Input,
Select,
Body,
Layout,
} from "@budibase/bbui"
import { onMount, createEventDispatcher } from "svelte"
import { FIELDS } from "constants/backend"
import { generate } from "builderStore/schemaGenerator"
export let schema = {}
export let json
let dispatcher = createEventDispatcher()
let mode = "Form"
let fieldCount = 0
let fieldKeys = {},
fieldTypes = {}
let keyValueOptions = [
{ label: "String", value: FIELDS.STRING.type },
{ label: "Number", value: FIELDS.NUMBER.type },
{ label: "Boolean", value: FIELDS.BOOLEAN.type },
{ label: "Object", value: FIELDS.JSON.type },
{ label: "Array", value: FIELDS.ARRAY.type },
]
let invalid = false
async function onJsonUpdate({ detail }) {
const input = detail.value
json = input
try {
// check json valid first
let inputJson = JSON.parse(input)
schema = generate(inputJson)
updateCounts()
invalid = false
} catch (err) {
// json not currently valid
invalid = true
}
}
function updateCounts() {
if (!schema) {
schema = {}
}
let i = 0
for (let [key, value] of Object.entries(schema)) {
fieldKeys[i] = key
fieldTypes[i] = value.type
i++
}
fieldCount = i
}
function saveSchema() {
for (let i of Object.keys(fieldKeys)) {
const key = fieldKeys[i]
// they were added to schema, rather than generated
if (!schema[key]) {
schema[key] = {
type: fieldTypes[i],
}
}
}
dispatcher("save", { schema, json })
}
onMount(() => {
updateCounts()
})
</script>
<ModalContent
title={"JSON Schema Editor"}
confirmText="Save Column"
onConfirm={saveSchema}
bind:disabled={invalid}
size="L"
>
<Tabs selected={mode} noPadding>
<Tab title="Form">
{#each Array(fieldCount) as _, i}
<div class="horizontal">
<Input outline label="Key" bind:value={fieldKeys[i]} />
<Select
label="Type"
options={keyValueOptions}
bind:value={fieldTypes[i]}
getOptionValue={field => field.value}
getOptionLabel={field => field.label}
/>
</div>
{/each}
<div class:add-field-btn={fieldCount !== 0}>
<Button primary text on:click={() => fieldCount++}>Add Field</Button>
</div>
</Tab>
<Tab title="JSON">
<Layout noPadding gap="XS">
<Body size="S">
Provide a sample JSON blob here to automatically determine your
schema.
</Body>
<Editor mode="json" on:change={onJsonUpdate} value={json} />
</Layout>
</Tab>
</Tabs>
</ModalContent>
<style>
.horizontal {
display: grid;
grid-template-columns: 30% 1fr;
grid-gap: var(--spacing-s);
align-items: center;
}
.add-field-btn {
margin-top: var(--spacing-xl);
}
</style>

View File

@ -14,7 +14,7 @@
notifications, notifications,
} from "@budibase/bbui" } from "@budibase/bbui"
import ErrorSVG from "assets/error.svg?raw" import ErrorSVG from "assets/error.svg?raw"
import { findComponent, findComponentPath } from "builderStore/storeUtils" import { findComponent, findComponentPath } from "builderStore/componentUtils"
let iframe let iframe
let layout let layout
@ -69,15 +69,7 @@
previewDevice: $store.previewDevice, previewDevice: $store.previewDevice,
messagePassing: $store.clientFeatures.messagePassing, messagePassing: $store.clientFeatures.messagePassing,
} }
// Saving pages and screens to the DB causes them to have _revs.
// These revisions change every time a save happens and causes
// these reactive statements to fire, even though the actual
// definition hasn't changed.
// By deleting all _rev properties we can avoid this and increase
// performance.
$: json = JSON.stringify(previewData) $: json = JSON.stringify(previewData)
$: strippedJson = json.replace(/"_rev":\s*"[^"]+"/g, `"_rev":""`)
// Update the iframe with the builder info to render the correct preview // Update the iframe with the builder info to render the correct preview
const refreshContent = message => { const refreshContent = message => {
@ -87,7 +79,7 @@
} }
// Refresh the preview when required // Refresh the preview when required
$: refreshContent(strippedJson) $: refreshContent(json)
function receiveMessage(message) { function receiveMessage(message) {
const handlers = { const handlers = {
@ -102,7 +94,7 @@
if (!$store.clientFeatures.intelligentLoading) { if (!$store.clientFeatures.intelligentLoading) {
loading = false loading = false
} }
refreshContent(strippedJson) refreshContent(json)
}, },
[MessageTypes.ERROR]: event => { [MessageTypes.ERROR]: event => {
// Catch any app errors // Catch any app errors

View File

@ -43,7 +43,8 @@
"attachmentfield", "attachmentfield",
"relationshipfield", "relationshipfield",
"daterangepicker", "daterangepicker",
"multifieldselect" "multifieldselect",
"jsonfield"
] ]
}, },
{ {
@ -73,6 +74,7 @@
"heading", "heading",
"text", "text",
"button", "button",
"tag",
"divider", "divider",
"image", "image",
"backgroundimage", "backgroundimage",

View File

@ -49,6 +49,7 @@ export default `
try { try {
parsed = JSON.parse(event.data) parsed = JSON.parse(event.data)
} catch (error) { } catch (error) {
console.error("Client received invalid JSON")
// Ignore // Ignore
} }
if (!parsed) { if (!parsed) {

View File

@ -2,7 +2,7 @@
import { get } from "svelte/store" import { get } from "svelte/store"
import { store, currentAsset } from "builderStore" import { store, currentAsset } from "builderStore"
import ConfirmDialog from "components/common/ConfirmDialog.svelte" import ConfirmDialog from "components/common/ConfirmDialog.svelte"
import { findComponentParent } from "builderStore/storeUtils" import { findComponentParent } from "builderStore/componentUtils"
import { ActionMenu, MenuItem, Icon, notifications } from "@budibase/bbui" import { ActionMenu, MenuItem, Icon, notifications } from "@budibase/bbui"
export let component export let component

View File

@ -1,6 +1,6 @@
import { writable, get } from "svelte/store" import { writable, get } from "svelte/store"
import { store as frontendStore } from "builderStore" import { store as frontendStore } from "builderStore"
import { findComponentPath } from "builderStore/storeUtils" import { findComponentPath } from "builderStore/componentUtils"
export const DropEffect = { export const DropEffect = {
MOVE: "move", MOVE: "move",

View File

@ -63,7 +63,14 @@
// If no specific value is depended upon, check if a value exists at all // If no specific value is depended upon, check if a value exists at all
// for the dependent setting // for the dependent setting
if (dependantValue == null) { if (dependantValue == null) {
return !isEmpty(componentInstance[dependantSetting]) const currentValue = componentInstance[dependantSetting]
if (currentValue === false) {
return false
}
if (currentValue === true) {
return true
}
return !isEmpty(currentValue)
} }
// Otherwise check the value matches // Otherwise check the value matches

View File

@ -9,8 +9,9 @@
ActionMenu, ActionMenu,
MenuItem, MenuItem,
} from "@budibase/bbui" } from "@budibase/bbui"
import { getAvailableActions } from "./actions" import { getAvailableActions } from "./index"
import { generate } from "shortid" import { generate } from "shortid"
import { getButtonContextBindings } from "builderStore/dataBinding"
const flipDurationMs = 150 const flipDurationMs = 150
const EVENT_TYPE_KEY = "##eventHandlerType" const EVENT_TYPE_KEY = "##eventHandlerType"
@ -19,7 +20,16 @@
export let actions export let actions
export let bindings = [] export let bindings = []
// dndzone needs an id on the array items, so this adds some temporary ones. let selectedAction = actions?.length ? actions[0] : null
// These are ephemeral bindings which only exist while executing actions
$: buttonContextBindings = getButtonContextBindings(
actions,
selectedAction?.id
)
$: allBindings = buttonContextBindings.concat(bindings)
// Assign a unique ID to each action
$: { $: {
if (actions) { if (actions) {
actions.forEach(action => { actions.forEach(action => {
@ -30,8 +40,6 @@
} }
} }
let selectedAction = actions?.length ? actions[0] : null
$: selectedActionComponent = $: selectedActionComponent =
selectedAction && selectedAction &&
actionTypes.find(t => t.name === selectedAction[EVENT_TYPE_KEY])?.component actionTypes.find(t => t.name === selectedAction[EVENT_TYPE_KEY])?.component
@ -122,7 +130,7 @@
<svelte:component <svelte:component
this={selectedActionComponent} this={selectedActionComponent}
parameters={selectedAction.parameters} parameters={selectedAction.parameters}
{bindings} bindings={allBindings}
/> />
</div> </div>
{/if} {/if}

View File

@ -2,7 +2,7 @@
import { ActionButton, Button, Drawer } from "@budibase/bbui" import { ActionButton, Button, Drawer } from "@budibase/bbui"
import { createEventDispatcher } from "svelte" import { createEventDispatcher } from "svelte"
import { notifications } from "@budibase/bbui" import { notifications } from "@budibase/bbui"
import EventEditor from "./EventEditor.svelte" import ButtonActionDrawer from "./ButtonActionDrawer.svelte"
import { automationStore } from "builderStore" import { automationStore } from "builderStore"
import { cloneDeep } from "lodash/fp" import { cloneDeep } from "lodash/fp"
@ -67,7 +67,7 @@
Define what actions to run. Define what actions to run.
</svelte:fragment> </svelte:fragment>
<Button cta slot="buttons" on:click={saveEventData}>Save</Button> <Button cta slot="buttons" on:click={saveEventData}>Save</Button>
<EventEditor <ButtonActionDrawer
slot="body" slot="body"
bind:actions={tmpValue} bind:actions={tmpValue}
eventType={name} eventType={name}

View File

@ -0,0 +1,152 @@
<script>
import { Select, Label, Body, Checkbox, Input } from "@budibase/bbui"
import { store, currentAsset } from "builderStore"
import { tables } from "stores/backend"
import {
getContextProviderComponents,
getSchemaForDatasource,
} from "builderStore/dataBinding"
import SaveFields from "./SaveFields.svelte"
export let parameters
export let bindings = []
$: formComponents = getContextProviderComponents(
$currentAsset,
$store.selectedComponentId,
"form"
)
$: schemaComponents = getContextProviderComponents(
$currentAsset,
$store.selectedComponentId,
"schema"
)
$: providerOptions = getProviderOptions(formComponents, schemaComponents)
$: schemaFields = getSchemaFields($currentAsset, parameters?.tableId)
$: tableOptions = $tables.list || []
// Gets a context definition of a certain type from a component definition
const extractComponentContext = (component, contextType) => {
const def = store.actions.components.getDefinition(component?._component)
if (!def) {
return null
}
const contexts = Array.isArray(def.context) ? def.context : [def.context]
return contexts.find(context => context?.type === contextType)
}
// Gets options for valid context keys which provide valid data to submit
const getProviderOptions = (formComponents, schemaComponents) => {
const formContexts = formComponents.map(component => ({
component,
context: extractComponentContext(component, "form"),
}))
const schemaContexts = schemaComponents.map(component => ({
component,
context: extractComponentContext(component, "schema"),
}))
const allContexts = formContexts.concat(schemaContexts)
return allContexts.map(({ component, context }) => {
let runtimeBinding = component._id
if (context.suffix) {
runtimeBinding += `-${context.suffix}`
}
return {
label: component._instanceName,
value: runtimeBinding,
}
})
}
const getSchemaFields = (asset, tableId) => {
const { schema } = getSchemaForDatasource(asset, { type: "table", tableId })
delete schema._id
delete schema._rev
return Object.values(schema || {})
}
const onFieldsChanged = e => {
parameters.fields = e.detail
}
</script>
<div class="root">
<Body size="S">
Choose the data source that provides the row you would like to duplicate.
<br />
You can always add or override fields manually.
</Body>
<div class="params">
<Label small>Data Source</Label>
<Select
bind:value={parameters.providerId}
options={providerOptions}
placeholder="None"
/>
<Label small>Duplicate to Table</Label>
<Select
bind:value={parameters.tableId}
options={tableOptions}
getOptionLabel={option => option.name}
getOptionValue={option => option._id}
/>
<Label small />
<Checkbox text="Require confirmation" bind:value={parameters.confirm} />
{#if parameters.confirm}
<Label small>Confirm text</Label>
<Input
placeholder="Are you sure you want to duplicate this row?"
bind:value={parameters.confirmText}
/>
{/if}
</div>
{#if parameters.tableId}
<div class="fields">
<SaveFields
parameterFields={parameters.fields}
{schemaFields}
on:change={onFieldsChanged}
{bindings}
/>
</div>
{/if}
</div>
<style>
.root {
width: 100%;
max-width: 800px;
margin: 0 auto;
display: flex;
flex-direction: column;
justify-content: flex-start;
align-items: stretch;
gap: var(--spacing-xl);
}
.root :global(p) {
line-height: 1.5;
}
.params {
display: grid;
column-gap: var(--spacing-l);
row-gap: var(--spacing-s);
grid-template-columns: 100px 1fr;
align-items: center;
}
.fields {
display: grid;
column-gap: var(--spacing-l);
row-gap: var(--spacing-s);
grid-template-columns: 100px 1fr auto 1fr auto;
align-items: center;
}
</style>

View File

@ -0,0 +1,13 @@
export { default as NavigateTo } from "./NavigateTo.svelte"
export { default as SaveRow } from "./SaveRow.svelte"
export { default as DeleteRow } from "./DeleteRow.svelte"
export { default as ExecuteQuery } from "./ExecuteQuery.svelte"
export { default as TriggerAutomation } from "./TriggerAutomation.svelte"
export { default as ValidateForm } from "./ValidateForm.svelte"
export { default as LogOut } from "./LogOut.svelte"
export { default as ClearForm } from "./ClearForm.svelte"
export { default as CloseScreenModal } from "./CloseScreenModal.svelte"
export { default as ChangeFormStep } from "./ChangeFormStep.svelte"
export { default as UpdateState } from "./UpdateState.svelte"
export { default as RefreshDataProvider } from "./RefreshDataProvider.svelte"
export { default as DuplicateRow } from "./DuplicateRow.svelte"

View File

@ -0,0 +1,33 @@
import * as ActionComponents from "./actions"
import { get } from "svelte/store"
import { store } from "builderStore"
import ActionDefinitions from "./manifest.json"
// Defines which actions are available to configure in the front end.
// Unfortunately the "name" property is used as the identifier so please don't
// change them.
// The client library removes any spaces when processing actions, so they can
// be considered as camel case too.
// There is technical debt here to sanitize all these and standardise them
// across the packages but it's a breaking change to existing apps.
export const getAvailableActions = (getAllActions = false) => {
return ActionDefinitions.actions
.filter(action => {
// Filter down actions to those supported by the current client lib version
if (getAllActions || !action.dependsOnFeature) {
return true
}
return get(store).clientFeatures?.[action.dependsOnFeature] === true
})
.map(action => {
// Then enrich the actions with real components
return {
...action,
component: ActionComponents[action.component],
}
})
.filter(action => {
// Then strip any old actions for which we don't have constructors
return action.component != null
})
}

View File

@ -0,0 +1,75 @@
{
"actions": [
{
"name": "Save Row",
"component": "SaveRow",
"context": [
{
"label": "Saved row",
"value": "row"
}
]
},
{
"name": "Duplicate Row",
"component": "DuplicateRow",
"context": [
{
"label": "Duplicated row",
"value": "row"
}
]
},
{
"name": "Delete Row",
"component": "DeleteRow"
},
{
"name": "Navigate To",
"component": "NavigateTo"
},
{
"name": "Execute Query",
"component": "ExecuteQuery",
"context": [
{
"label": "Query result",
"value": "result"
}
]
},
{
"name": "Trigger Automation",
"component": "TriggerAutomation"
},
{
"name": "Validate Form",
"component": "ValidateForm"
},
{
"name": "Log Out",
"component": "LogOut"
},
{
"name": "Clear Form",
"component": "ClearForm"
},
{
"name": "Close Screen Modal",
"component": "CloseScreenModal"
},
{
"name": "Change Form Step",
"component": "ChangeFormStep"
},
{
"name": "Refresh Data Provider",
"component": "RefreshDataProvider"
},
{
"name": "Update State",
"component": "UpdateState",
"dependsOnFeature": "state"
}
]
}

View File

@ -16,7 +16,7 @@
import { selectedComponent } from "builderStore" import { selectedComponent } from "builderStore"
import { getComponentForSettingType } from "./componentSettings" import { getComponentForSettingType } from "./componentSettings"
import PropertyControl from "./PropertyControl.svelte" import PropertyControl from "./PropertyControl.svelte"
import { getComponentSettings } from "builderStore/storeUtils" import { getComponentSettings } from "builderStore/componentUtils"
export let conditions = [] export let conditions = []
export let bindings = [] export let bindings = []

View File

@ -2,7 +2,7 @@
import { Select } from "@budibase/bbui" import { Select } from "@budibase/bbui"
import { makePropSafe } from "@budibase/string-templates" import { makePropSafe } from "@budibase/string-templates"
import { currentAsset, store } from "builderStore" import { currentAsset, store } from "builderStore"
import { findComponentPath } from "builderStore/storeUtils" import { findComponentPath } from "builderStore/componentUtils"
import { createEventDispatcher, onMount } from "svelte" import { createEventDispatcher, onMount } from "svelte"
export let value export let value

View File

@ -48,9 +48,7 @@
return [...acc, ...viewsArr] return [...acc, ...viewsArr]
}, []) }, [])
$: queries = $queriesStore.list $: queries = $queriesStore.list
.filter( .filter(q => showAllQueries || q.queryVerb === "read" || q.readable)
query => showAllQueries || query.queryVerb === "read" || query.readable
)
.map(query => ({ .map(query => ({
label: query.name, label: query.name,
name: query.name, name: query.name,
@ -104,6 +102,22 @@
value: `{{ literal ${runtimeBinding} }}`, value: `{{ literal ${runtimeBinding} }}`,
} }
}) })
$: jsonArrays = bindings
.filter(x => x.fieldSchema?.type === "jsonarray")
.map(binding => {
const { providerId, readableBinding, runtimeBinding, tableId } = binding
const { name, type, prefixKeys } = binding.fieldSchema
return {
providerId,
label: readableBinding,
fieldName: name,
fieldType: type,
tableId,
prefixKeys,
type: "jsonarray",
value: `{{ literal ${runtimeBinding} }}`,
}
})
const handleSelected = selected => { const handleSelected = selected => {
dispatch("change", selected) dispatch("change", selected)
@ -230,6 +244,17 @@
{/each} {/each}
</ul> </ul>
{/if} {/if}
{#if jsonArrays?.length}
<Divider size="S" />
<div class="title">
<Heading size="XS">JSON Arrays</Heading>
</div>
<ul>
{#each jsonArrays as field}
<li on:click={() => handleSelected(field)}>{field.label}</li>
{/each}
</ul>
{/if}
{#if dataProviders?.length} {#if dataProviders?.length}
<Divider size="S" /> <Divider size="S" />
<div class="title"> <div class="title">

View File

@ -1,80 +0,0 @@
import { store } from "builderStore"
import { get } from "svelte/store"
import NavigateTo from "./NavigateTo.svelte"
import SaveRow from "./SaveRow.svelte"
import DeleteRow from "./DeleteRow.svelte"
import ExecuteQuery from "./ExecuteQuery.svelte"
import TriggerAutomation from "./TriggerAutomation.svelte"
import ValidateForm from "./ValidateForm.svelte"
import LogOut from "./LogOut.svelte"
import ClearForm from "./ClearForm.svelte"
import CloseScreenModal from "./CloseScreenModal.svelte"
import ChangeFormStep from "./ChangeFormStep.svelte"
import UpdateStateStep from "./UpdateState.svelte"
import RefreshDataProvider from "./RefreshDataProvider.svelte"
// Defines which actions are available to configure in the front end.
// Unfortunately the "name" property is used as the identifier so please don't
// change them.
// The client library removes any spaces when processing actions, so they can
// be considered as camel case too.
// There is technical debt here to sanitize all these and standardise them
// across the packages but it's a breaking change to existing apps.
export const getAvailableActions = () => {
let actions = [
{
name: "Save Row",
component: SaveRow,
},
{
name: "Delete Row",
component: DeleteRow,
},
{
name: "Navigate To",
component: NavigateTo,
},
{
name: "Execute Query",
component: ExecuteQuery,
},
{
name: "Trigger Automation",
component: TriggerAutomation,
},
{
name: "Validate Form",
component: ValidateForm,
},
{
name: "Log Out",
component: LogOut,
},
{
name: "Clear Form",
component: ClearForm,
},
{
name: "Close Screen Modal",
component: CloseScreenModal,
},
{
name: "Change Form Step",
component: ChangeFormStep,
},
{
name: "Refresh Data Provider",
component: RefreshDataProvider,
},
]
if (get(store).clientFeatures?.state) {
actions.push({
name: "Update State",
component: UpdateStateStep,
})
}
return actions
}

View File

@ -1,2 +0,0 @@
import EventsEditor from "./EventPropertyControl.svelte"
export default EventsEditor

View File

@ -21,7 +21,7 @@
export let panel = ClientBindingPanel export let panel = ClientBindingPanel
export let allowBindings = true export let allowBindings = true
const BannedTypes = ["link", "attachment", "formula"] const BannedTypes = ["link", "attachment", "formula", "json", "jsonarray"]
$: fieldOptions = (schemaFields ?? []) $: fieldOptions = (schemaFields ?? [])
.filter(field => !BannedTypes.includes(field.type)) .filter(field => !BannedTypes.includes(field.type))

View File

@ -5,7 +5,7 @@
getSchemaForDatasource, getSchemaForDatasource,
} from "builderStore/dataBinding" } from "builderStore/dataBinding"
import { currentAsset } from "builderStore" import { currentAsset } from "builderStore"
import { findClosestMatchingComponent } from "builderStore/storeUtils" import { findClosestMatchingComponent } from "builderStore/componentUtils"
export let componentInstance export let componentInstance
export let value export let value

View File

@ -1,7 +1,7 @@
<script> <script>
import { ActionButton } from "@budibase/bbui" import { ActionButton } from "@budibase/bbui"
import { currentAsset, store } from "builderStore" import { currentAsset, store } from "builderStore"
import { findClosestMatchingComponent } from "builderStore/storeUtils" import { findClosestMatchingComponent } from "builderStore/componentUtils"
import { makeDatasourceFormComponents } from "builderStore/store/screenTemplates/utils/commonComponents" import { makeDatasourceFormComponents } from "builderStore/store/screenTemplates/utils/commonComponents"
import ConfirmDialog from "components/common/ConfirmDialog.svelte" import ConfirmDialog from "components/common/ConfirmDialog.svelte"

View File

@ -11,7 +11,7 @@
DatePicker, DatePicker,
} from "@budibase/bbui" } from "@budibase/bbui"
import { currentAsset, selectedComponent } from "builderStore" import { currentAsset, selectedComponent } from "builderStore"
import { findClosestMatchingComponent } from "builderStore/storeUtils" import { findClosestMatchingComponent } from "builderStore/componentUtils"
import { getSchemaForDatasource } from "builderStore/dataBinding" import { getSchemaForDatasource } from "builderStore/dataBinding"
import DrawerBindableInput from "components/common/bindings/DrawerBindableInput.svelte" import DrawerBindableInput from "components/common/bindings/DrawerBindableInput.svelte"
import { generate } from "shortid" import { generate } from "shortid"

View File

@ -1,7 +1,7 @@
import { Checkbox, Select, Stepper } from "@budibase/bbui" import { Checkbox, Select, Stepper } from "@budibase/bbui"
import DataSourceSelect from "./DataSourceSelect.svelte" import DataSourceSelect from "./DataSourceSelect.svelte"
import DataProviderSelect from "./DataProviderSelect.svelte" import DataProviderSelect from "./DataProviderSelect.svelte"
import EventsEditor from "./EventsEditor" import ButtonActionEditor from "./ButtonActionEditor/ButtonActionEditor.svelte"
import TableSelect from "./TableSelect.svelte" import TableSelect from "./TableSelect.svelte"
import ColorPicker from "./ColorPicker.svelte" import ColorPicker from "./ColorPicker.svelte"
import { IconSelect } from "./IconSelect" import { IconSelect } from "./IconSelect"
@ -24,7 +24,7 @@ const componentMap = {
dataProvider: DataProviderSelect, dataProvider: DataProviderSelect,
boolean: Checkbox, boolean: Checkbox,
number: Stepper, number: Stepper,
event: EventsEditor, event: ButtonActionEditor,
table: TableSelect, table: TableSelect,
color: ColorPicker, color: ColorPicker,
icon: IconSelect, icon: IconSelect,
@ -45,6 +45,7 @@ const componentMap = {
"field/attachment": FormFieldSelect, "field/attachment": FormFieldSelect,
"field/link": FormFieldSelect, "field/link": FormFieldSelect,
"field/array": FormFieldSelect, "field/array": FormFieldSelect,
"field/json": FormFieldSelect,
// Some validation types are the same as others, so not all types are // Some validation types are the same as others, so not all types are
// explicitly listed here. e.g. options uses string validation // explicitly listed here. e.g. options uses string validation
"validation/string": ValidationEditor, "validation/string": ValidationEditor,

View File

@ -89,6 +89,14 @@ export const FIELDS = {
presence: false, presence: false,
}, },
}, },
JSON: {
name: "JSON",
type: "json",
constraints: {
type: "object",
presence: false,
},
},
} }
export const AUTO_COLUMN_SUB_TYPES = { export const AUTO_COLUMN_SUB_TYPES = {

View File

@ -1,4 +1,5 @@
import { NoEmptyFilterStrings } from "../constants/lucene" import { NoEmptyFilterStrings } from "../constants/lucene"
import { deepGet } from "@budibase/bbui"
/** /**
* Removes any fields that contain empty strings that would cause inconsistent * Removes any fields that contain empty strings that would cause inconsistent
@ -96,9 +97,13 @@ export const buildLuceneQuery = filter => {
* @param query the JSON lucene query * @param query the JSON lucene query
*/ */
export const luceneQuery = (docs, query) => { export const luceneQuery = (docs, query) => {
if (!docs || !Array.isArray(docs)) {
return []
}
if (!query) { if (!query) {
return docs return docs
} }
// make query consistent first // make query consistent first
query = cleanupQuery(query) query = cleanupQuery(query)
@ -106,7 +111,9 @@ export const luceneQuery = (docs, query) => {
const match = (type, failFn) => doc => { const match = (type, failFn) => doc => {
const filters = Object.entries(query[type] || {}) const filters = Object.entries(query[type] || {})
for (let i = 0; i < filters.length; i++) { for (let i = 0; i < filters.length; i++) {
if (failFn(filters[i][0], filters[i][1], doc)) { const [key, testValue] = filters[i]
const docValue = deepGet(doc, key)
if (failFn(docValue, testValue)) {
return false return false
} }
} }
@ -114,38 +121,42 @@ export const luceneQuery = (docs, query) => {
} }
// Process a string match (fails if the value does not start with the string) // Process a string match (fails if the value does not start with the string)
const stringMatch = match("string", (key, value, doc) => { const stringMatch = match("string", (docValue, testValue) => {
return !doc[key] || !doc[key].toLowerCase().startsWith(value?.toLowerCase()) return (
!docValue || !docValue?.toLowerCase().startsWith(testValue?.toLowerCase())
)
}) })
// Process a fuzzy match (treat the same as starts with when running locally) // Process a fuzzy match (treat the same as starts with when running locally)
const fuzzyMatch = match("fuzzy", (key, value, doc) => { const fuzzyMatch = match("fuzzy", (docValue, testValue) => {
return !doc[key] || !doc[key].startsWith(value) return (
!docValue || !docValue?.toLowerCase().startsWith(testValue?.toLowerCase())
)
}) })
// Process a range match // Process a range match
const rangeMatch = match("range", (key, value, doc) => { const rangeMatch = match("range", (docValue, testValue) => {
return !doc[key] || doc[key] < value.low || doc[key] > value.high return !docValue || docValue < testValue.low || docValue > testValue.high
}) })
// Process an equal match (fails if the value is different) // Process an equal match (fails if the value is different)
const equalMatch = match("equal", (key, value, doc) => { const equalMatch = match("equal", (docValue, testValue) => {
return value != null && value !== "" && doc[key] !== value return testValue != null && testValue !== "" && docValue !== testValue
}) })
// Process a not-equal match (fails if the value is the same) // Process a not-equal match (fails if the value is the same)
const notEqualMatch = match("notEqual", (key, value, doc) => { const notEqualMatch = match("notEqual", (docValue, testValue) => {
return value != null && value !== "" && doc[key] === value return testValue != null && testValue !== "" && docValue === testValue
}) })
// Process an empty match (fails if the value is not empty) // Process an empty match (fails if the value is not empty)
const emptyMatch = match("empty", (key, value, doc) => { const emptyMatch = match("empty", docValue => {
return doc[key] != null && doc[key] !== "" return docValue != null && docValue !== ""
}) })
// Process a not-empty match (fails is the value is empty) // Process a not-empty match (fails is the value is empty)
const notEmptyMatch = match("notEmpty", (key, value, doc) => { const notEmptyMatch = match("notEmpty", docValue => {
return doc[key] == null || doc[key] === "" return docValue == null || docValue === ""
}) })
// Match a document against all criteria // Match a document against all criteria

View File

@ -13,7 +13,7 @@
import FrontendNavigatePane from "components/design/NavigationPanel/FrontendNavigatePane.svelte" import FrontendNavigatePane from "components/design/NavigationPanel/FrontendNavigatePane.svelte"
import { goto, leftover, params } from "@roxi/routify" import { goto, leftover, params } from "@roxi/routify"
import { FrontendTypes } from "constants" import { FrontendTypes } from "constants"
import { findComponent, findComponentPath } from "builderStore/storeUtils" import { findComponent, findComponentPath } from "builderStore/componentUtils"
import { get } from "svelte/store" import { get } from "svelte/store"
import AppThemeSelect from "components/design/AppPreview/AppThemeSelect.svelte" import AppThemeSelect from "components/design/AppPreview/AppThemeSelect.svelte"
import ThemeEditor from "components/design/AppPreview/ThemeEditor.svelte" import ThemeEditor from "components/design/AppPreview/ThemeEditor.svelte"

View File

@ -1,6 +1,6 @@
{ {
"name": "@budibase/cli", "name": "@budibase/cli",
"version": "1.0.23", "version": "1.0.27-alpha.1",
"description": "Budibase CLI, for developers, self hosting and migrations.", "description": "Budibase CLI, for developers, self hosting and migrations.",
"main": "src/index.js", "main": "src/index.js",
"bin": { "bin": {

View File

@ -500,9 +500,20 @@
"defaultValue": "M" "defaultValue": "M"
} }
], ],
"context": { "context": [
{
"type": "schema" "type": "schema"
},
{
"type": "static",
"values": [
{
"label": "Row Index",
"key": "index"
} }
]
}
]
}, },
"stackedlist": { "stackedlist": {
"deprecated": true, "deprecated": true,
@ -808,6 +819,57 @@
} }
] ]
}, },
"tag": {
"name": "Tag",
"icon": "TextParagraph",
"showSettingsBar": true,
"settings": [
{
"type": "text",
"label": "Text",
"key": "text"
},
{
"type": "select",
"label": "Size",
"key": "size",
"defaultValue": "M",
"showInBar": true,
"barStyle": "picker",
"options": [{
"label": "Small",
"value": "S"
}, {
"label": "Medium",
"value": "M"
}, {
"label": "Large",
"value": "L"
}]
},
{
"type": "color",
"label": "Color",
"key": "color",
"showInBar": true,
"barSeparator": false
},
{
"type": "boolean",
"label": "Show delete icon",
"key": "closable",
"showInBar": true,
"barIcon": "TagItalic",
"barTitle": "Show delete icon"
},
{
"type": "event",
"label": "On click delete icon",
"key": "onClick",
"dependsOn": "closable"
}
]
},
"image": { "image": {
"name": "Image", "name": "Image",
"description": "A basic component for displaying images", "description": "A basic component for displaying images",
@ -1775,6 +1837,10 @@
{ {
"type": "static", "type": "static",
"values": [ "values": [
{
"label": "Value",
"key": "__value"
},
{ {
"label": "Valid", "label": "Valid",
"key": "__valid" "key": "__valid"
@ -2407,6 +2473,40 @@
} }
] ]
}, },
"jsonfield": {
"name": "JSON Field",
"icon": "Brackets",
"styles": ["size"],
"editable": true,
"settings": [
{
"type": "field/json",
"label": "Field",
"key": "field"
},
{
"type": "text",
"label": "Label",
"key": "label"
},
{
"type": "text",
"label": "Placeholder",
"key": "placeholder"
},
{
"type": "text",
"label": "Default value",
"key": "defaultValue"
},
{
"type": "boolean",
"label": "Disabled",
"key": "disabled",
"defaultValue": false
}
]
},
"dataprovider": { "dataprovider": {
"name": "Data Provider", "name": "Data Provider",
"info": "Pagination is only available for data stored in tables.", "info": "Pagination is only available for data stored in tables.",
@ -3200,6 +3300,16 @@
} }
] ]
}, },
{
"type": "static",
"suffix": "repeater",
"values": [
{
"label": "Row Index",
"key": "index"
}
]
},
{ {
"type": "schema", "type": "schema",
"suffix": "repeater" "suffix": "repeater"

View File

@ -1,6 +1,6 @@
{ {
"name": "@budibase/client", "name": "@budibase/client",
"version": "1.0.23", "version": "1.0.27-alpha.1",
"license": "MPL-2.0", "license": "MPL-2.0",
"module": "dist/budibase-client.js", "module": "dist/budibase-client.js",
"main": "dist/budibase-client.js", "main": "dist/budibase-client.js",
@ -19,9 +19,9 @@
"dev:builder": "rollup -cw" "dev:builder": "rollup -cw"
}, },
"dependencies": { "dependencies": {
"@budibase/bbui": "^1.0.23", "@budibase/bbui": "^1.0.27-alpha.1",
"@budibase/standard-components": "^0.9.139", "@budibase/standard-components": "^0.9.139",
"@budibase/string-templates": "^1.0.23", "@budibase/string-templates": "^1.0.27-alpha.1",
"regexparam": "^1.3.0", "regexparam": "^1.3.0",
"shortid": "^2.2.15", "shortid": "^2.2.15",
"svelte-spa-router": "^3.0.5" "svelte-spa-router": "^3.0.5"
@ -35,6 +35,7 @@
"@spectrum-css/divider": "^1.0.3", "@spectrum-css/divider": "^1.0.3",
"@spectrum-css/link": "^3.1.3", "@spectrum-css/link": "^3.1.3",
"@spectrum-css/page": "^3.0.1", "@spectrum-css/page": "^3.0.1",
"@spectrum-css/tag": "^3.1.4",
"@spectrum-css/typography": "^3.0.2", "@spectrum-css/typography": "^3.0.2",
"@spectrum-css/vars": "^3.0.1", "@spectrum-css/vars": "^3.0.1",
"apexcharts": "^3.22.1", "apexcharts": "^3.22.1",

View File

@ -21,8 +21,8 @@
{#if $component.empty} {#if $component.empty}
<Placeholder /> <Placeholder />
{:else if rows.length > 0} {:else if rows.length > 0}
{#each rows as row} {#each rows as row, index}
<Provider data={row}> <Provider data={{ ...row, index }}>
<slot /> <slot />
</Provider> </Provider>
{/each} {/each}

View File

@ -0,0 +1,59 @@
<script>
import "@spectrum-css/tag/dist/index-vars.css"
import { ClearButton } from "@budibase/bbui"
import { getContext } from "svelte"
export let onClick
export let text = ""
export let color
export let closable = false
export let size = "M"
const component = getContext("component")
const { styleable, builderStore } = getContext("sdk")
// Add color styles to main styles object, otherwise the styleable helper
// overrides the color when it's passed as inline style.
$: styles = enrichStyles($component.styles, color)
$: componentText = getComponentText(text, $builderStore, $component)
const getComponentText = (text, builderState, componentState) => {
if (!builderState.inBuilder || componentState.editing) {
return text || " "
}
return text || componentState.name || "Placeholder text"
}
const enrichStyles = (styles, color) => {
if (!color) {
return styles
}
return {
...styles,
normal: {
...styles?.normal,
"background-color": color,
"border-color": color,
color: "white",
"--spectrum-clearbutton-medium-icon-color": "white",
},
}
}
</script>
<div class="spectrum-Tag spectrum-Tag--size{size}" use:styleable={styles}>
<span class="spectrum-Tag-label">{componentText}</span>
{#if closable}
<ClearButton on:click={onClick} />
{/if}
</div>
<style>
.spectrum-Tag--sizeS,
.spectrum-Tag--sizeM {
padding: 0 var(--spectrum-global-dimension-size-100);
}
.spectrum-Tag--sizeL {
padding: 0 var(--spectrum-global-dimension-size-150);
}
</style>

View File

@ -74,7 +74,7 @@
enrichedFilter.push({ enrichedFilter.push({
field: column.name, field: column.name,
operator: column.type === "string" ? "string" : "equal", operator: column.type === "string" ? "string" : "equal",
type: "string", type: column.type === "string" ? "string" : "number",
valueType: "Binding", valueType: "Binding",
value: `{{ [${formId}].[${column.name}] }}`, value: `{{ [${formId}].[${column.name}] }}`,
}) })

View File

@ -62,7 +62,7 @@
enrichedFilter.push({ enrichedFilter.push({
field: column.name, field: column.name,
operator: column.type === "string" ? "string" : "equal", operator: column.type === "string" ? "string" : "equal",
type: "string", type: column.type === "string" ? "string" : "number",
valueType: "Binding", valueType: "Binding",
value: `{{ ${safe(formId)}.${safe(column.name)} }}`, value: `{{ ${safe(formId)}.${safe(column.name)} }}`,
}) })

View File

@ -19,7 +19,7 @@
export let schemaFields export let schemaFields
export let filters = [] export let filters = []
const BannedTypes = ["link", "attachment", "formula"] const BannedTypes = ["link", "attachment", "formula", "json"]
$: fieldOptions = (schemaFields ?? []) $: fieldOptions = (schemaFields ?? [])
.filter(field => !BannedTypes.includes(field.type)) .filter(field => !BannedTypes.includes(field.type))

View File

@ -3,6 +3,8 @@
import { derived, get, writable } from "svelte/store" import { derived, get, writable } from "svelte/store"
import { createValidatorFromConstraints } from "./validation" import { createValidatorFromConstraints } from "./validation"
import { generateID } from "utils/helpers" import { generateID } from "utils/helpers"
import { deepGet, deepSet } from "@budibase/bbui"
import { cloneDeep } from "lodash/fp"
export let dataSource export let dataSource
export let disabled = false export let disabled = false
@ -49,6 +51,20 @@
}) })
} }
// Derive value of whole form
$: formValue = deriveFormValue(initialValues, $values, $enrichments)
// Create data context to provide
$: dataContext = {
...formValue,
// These static values are prefixed to avoid clashes with actual columns
__value: formValue,
__valid: valid,
__currentStep: $currentStep,
__currentStepValid: $currentStepValid,
}
// Generates a derived store from an array of fields, comprised of a map of // Generates a derived store from an array of fields, comprised of a map of
// extracted values from the field array // extracted values from the field array
const deriveFieldProperty = (fieldStores, getProp) => { const deriveFieldProperty = (fieldStores, getProp) => {
@ -78,6 +94,35 @@
}) })
} }
// Derive the overall form value and deeply set all field paths so that we
// can support things like JSON fields.
const deriveFormValue = (initialValues, values, enrichments) => {
let formValue = cloneDeep(initialValues || {})
// We need to sort the keys to avoid a JSON field overwriting a nested field
const sortedFields = Object.entries(values || {})
.map(([key, value]) => {
const field = getField(key)
return {
key,
value,
lastUpdate: get(field).fieldState?.lastUpdate || 0,
}
})
.sort((a, b) => {
return a.lastUpdate > b.lastUpdate
})
// Merge all values and enrichments into a single value
sortedFields.forEach(({ key, value }) => {
deepSet(formValue, key, value)
})
Object.entries(enrichments || {}).forEach(([key, value]) => {
deepSet(formValue, key, value)
})
return formValue
}
// Searches the field array for a certain field // Searches the field array for a certain field
const getField = name => { const getField = name => {
return fields.find(field => get(field).name === name) return fields.find(field => get(field).name === name)
@ -97,13 +142,20 @@
} }
// If we've already registered this field then keep some existing state // If we've already registered this field then keep some existing state
let initialValue = initialValues[field] ?? defaultValue let initialValue = deepGet(initialValues, field) ?? defaultValue
let fieldId = `id-${generateID()}` let fieldId = `id-${generateID()}`
const existingField = getField(field) const existingField = getField(field)
if (existingField) { if (existingField) {
const { fieldState } = get(existingField) const { fieldState } = get(existingField)
initialValue = fieldState.value ?? initialValue
fieldId = fieldState.fieldId fieldId = fieldState.fieldId
// Use new default value if default value changed,
// otherwise use the current value if possible
if (defaultValue !== fieldState.defaultValue) {
initialValue = defaultValue
} else {
initialValue = fieldState.value ?? initialValue
}
} }
// Auto columns are always disabled // Auto columns are always disabled
@ -130,6 +182,7 @@
disabled: disabled || fieldDisabled || isAutoColumn, disabled: disabled || fieldDisabled || isAutoColumn,
defaultValue, defaultValue,
validator, validator,
lastUpdate: Date.now(),
}, },
fieldApi: makeFieldApi(field, defaultValue), fieldApi: makeFieldApi(field, defaultValue),
fieldSchema: schema?.[field] ?? {}, fieldSchema: schema?.[field] ?? {},
@ -204,6 +257,7 @@
fieldInfo.update(state => { fieldInfo.update(state => {
state.fieldState.value = value state.fieldState.value = value
state.fieldState.error = error state.fieldState.error = error
state.fieldState.lastUpdate = Date.now()
return state return state
}) })
@ -220,6 +274,7 @@
fieldInfo.update(state => { fieldInfo.update(state => {
state.fieldState.value = newValue state.fieldState.value = newValue
state.fieldState.error = null state.fieldState.error = null
state.fieldState.lastUpdate = Date.now()
return state return state
}) })
} }
@ -299,18 +354,6 @@
{ type: ActionTypes.ClearForm, callback: formApi.clear }, { type: ActionTypes.ClearForm, callback: formApi.clear },
{ type: ActionTypes.ChangeFormStep, callback: formApi.changeStep }, { type: ActionTypes.ChangeFormStep, callback: formApi.changeStep },
] ]
// Create data context to provide
$: dataContext = {
...initialValues,
...$values,
...$enrichments,
// These static values are prefixed to avoid clashes with actual columns
__valid: valid,
__currentStep: $currentStep,
__currentStepValid: $currentStepValid,
}
</script> </script>
<Provider {actions} data={dataContext}> <Provider {actions} data={dataContext}>

View File

@ -0,0 +1,71 @@
<script>
import { CoreTextArea } from "@budibase/bbui"
import Field from "./Field.svelte"
import { getContext } from "svelte"
export let field
export let label
export let placeholder
export let disabled = false
export let defaultValue = ""
const component = getContext("component")
const validation = [
{
constraint: "json",
type: "json",
error: "JSON syntax is invalid",
},
]
let fieldState
let fieldApi
$: height = $component.styles?.normal?.height || "124px"
const serialiseValue = value => {
return JSON.stringify(value || undefined, null, 4) || ""
}
const parseValue = value => {
try {
return JSON.parse(value)
} catch (error) {
return value
}
}
</script>
<Field
{label}
{field}
{disabled}
{validation}
{defaultValue}
type="json"
bind:fieldState
bind:fieldApi
>
{#if fieldState}
<div style="--height: {height};">
<CoreTextArea
value={serialiseValue(fieldState.value)}
on:change={e => fieldApi.setValue(parseValue(e.detail))}
disabled={fieldState.disabled}
error={fieldState.error}
id={fieldState.fieldId}
{placeholder}
/>
</div>
{/if}
</Field>
<style>
:global(.spectrum-Form-itemField .spectrum-Textfield--multiline) {
min-height: calc(var(--height) - 24px);
}
:global(.spectrum-Form--labelsAbove
.spectrum-Form-itemField
.spectrum-Textfield--multiline) {
min-height: calc(var(--height) - 24px);
}
</style>

View File

@ -1,6 +1,7 @@
<script> <script>
import { CoreTextArea } from "@budibase/bbui" import { CoreTextArea } from "@budibase/bbui"
import Field from "./Field.svelte" import Field from "./Field.svelte"
import { getContext } from "svelte"
export let field export let field
export let label export let label
@ -11,6 +12,9 @@
let fieldState let fieldState
let fieldApi let fieldApi
const component = getContext("component")
$: height = $component.styles?.normal?.height || "124px"
</script> </script>
<Field <Field
@ -24,6 +28,7 @@
bind:fieldApi bind:fieldApi
> >
{#if fieldState} {#if fieldState}
<div style="--height: {height};">
<CoreTextArea <CoreTextArea
value={fieldState.value} value={fieldState.value}
on:change={e => fieldApi.setValue(e.detail)} on:change={e => fieldApi.setValue(e.detail)}
@ -32,5 +37,17 @@
id={fieldState.fieldId} id={fieldState.fieldId}
{placeholder} {placeholder}
/> />
</div>
{/if} {/if}
</Field> </Field>
<style>
:global(.spectrum-Form-itemField .spectrum-Textfield--multiline) {
min-height: calc(var(--height) - 24px);
}
:global(.spectrum-Form--labelsAbove
.spectrum-Form-itemField
.spectrum-Textfield--multiline) {
min-height: calc(var(--height) - 24px);
}
</style>

View File

@ -11,3 +11,4 @@ export { default as attachmentfield } from "./AttachmentField.svelte"
export { default as relationshipfield } from "./RelationshipField.svelte" export { default as relationshipfield } from "./RelationshipField.svelte"
export { default as passwordfield } from "./PasswordField.svelte" export { default as passwordfield } from "./PasswordField.svelte"
export { default as formstep } from "./FormStep.svelte" export { default as formstep } from "./FormStep.svelte"
export { default as jsonfield } from "./JSONField.svelte"

View File

@ -206,6 +206,7 @@ const parseType = (value, type) => {
return value return value
} }
// Parse array, treating no elements as null
if (type === FieldTypes.ARRAY) { if (type === FieldTypes.ARRAY) {
if (!Array.isArray(value) || !value.length) { if (!Array.isArray(value) || !value.length) {
return null return null
@ -213,6 +214,12 @@ const parseType = (value, type) => {
return value return value
} }
// For JSON we don't touch the value at all as we want to verify it in its
// raw form
if (type === FieldTypes.JSON) {
return value
}
// If some unknown type, treat as null to avoid breaking validators // If some unknown type, treat as null to avoid breaking validators
return null return null
} }
@ -290,6 +297,19 @@ const notContainsHandler = (value, rule) => {
return !containsHandler(value, rule) return !containsHandler(value, rule)
} }
// Evaluates a constraint that the value must be a valid json object
const jsonHandler = value => {
if (typeof value !== "object" || Array.isArray(value)) {
return false
}
try {
JSON.parse(JSON.stringify(value))
return true
} catch (error) {
return false
}
}
/** /**
* Map of constraint types to handlers. * Map of constraint types to handlers.
*/ */
@ -306,6 +326,7 @@ const handlerMap = {
notRegex: notRegexHandler, notRegex: notRegexHandler,
contains: containsHandler, contains: containsHandler,
notContains: notContainsHandler, notContains: notContainsHandler,
json: jsonHandler,
} }
/** /**

View File

@ -29,6 +29,7 @@ export { default as backgroundimage } from "./BackgroundImage.svelte"
export { default as daterangepicker } from "./DateRangePicker.svelte" export { default as daterangepicker } from "./DateRangePicker.svelte"
export { default as cardstat } from "./CardStat.svelte" export { default as cardstat } from "./CardStat.svelte"
export { default as spectrumcard } from "./SpectrumCard.svelte" export { default as spectrumcard } from "./SpectrumCard.svelte"
export { default as tag } from "./Tag.svelte"
export * from "./charts" export * from "./charts"
export * from "./forms" export * from "./forms"
export * from "./table" export * from "./table"

View File

@ -24,6 +24,7 @@
class:flipped class:flipped
class:line class:line
style="top: {top}px; left: {left}px; width: {width}px; height: {height}px; --color: {color}; --zIndex: {zIndex};" style="top: {top}px; left: {left}px; width: {width}px; height: {height}px; --color: {color}; --zIndex: {zIndex};"
class:withText={!!text}
> >
{#if text} {#if text}
<div class="text" class:flipped class:line class:right={alignRight}> <div class="text" class:flipped class:line class:right={alignRight}>
@ -39,12 +40,12 @@
z-index: var(--zIndex); z-index: var(--zIndex);
border: 2px solid var(--color); border: 2px solid var(--color);
pointer-events: none; pointer-events: none;
border-top-right-radius: 4px; border-radius: 4px;
border-top-left-radius: 0;
border-bottom-left-radius: 4px;
border-bottom-right-radius: 4px;
} }
.indicator.flipped { .indicator.withText {
border-top-left-radius: 0;
}
.indicator.withText.flipped {
border-top-left-radius: 4px; border-top-left-radius: 4px;
} }
.indicator.line { .indicator.line {
@ -74,8 +75,7 @@
border-radius: 4px; border-radius: 4px;
} }
.text.flipped { .text.flipped {
border-top-left-radius: 4px; border-radius: 4px;
border-bottom-left-radius: 4px;
transform: translateY(0%); transform: translateY(0%);
top: -2px; top: -2px;
} }

View File

@ -13,6 +13,7 @@ export const FieldTypes = {
ATTACHMENT: "attachment", ATTACHMENT: "attachment",
LINK: "link", LINK: "link",
FORMULA: "formula", FORMULA: "formula",
JSON: "json",
} }
export const UnsortableTypes = [ export const UnsortableTypes = [

View File

@ -61,7 +61,8 @@ export const createDataSourceStore = () => {
// Emit this as a window event, so parent screens which are iframing us in // Emit this as a window event, so parent screens which are iframing us in
// can also invalidate the same datasource // can also invalidate the same datasource
if (get(routeStore).queryParams?.peek) { const inModal = get(routeStore).queryParams?.peek
if (inModal) {
window.parent.postMessage({ window.parent.postMessage({
type: "invalidate-datasource", type: "invalidate-datasource",
detail: { dataSourceId }, detail: { dataSourceId },

View File

@ -8,20 +8,49 @@ import {
} from "stores" } from "stores"
import { saveRow, deleteRow, executeQuery, triggerAutomation } from "api" import { saveRow, deleteRow, executeQuery, triggerAutomation } from "api"
import { ActionTypes } from "constants" import { ActionTypes } from "constants"
import { enrichDataBindings } from "./enrichDataBinding"
import { deepSet } from "@budibase/bbui"
const saveRowHandler = async (action, context) => { const saveRowHandler = async (action, context) => {
const { fields, providerId, tableId } = action.parameters const { fields, providerId, tableId } = action.parameters
let payload
if (providerId) { if (providerId) {
let draft = context[providerId] payload = { ...context[providerId] }
} else {
payload = {}
}
if (fields) { if (fields) {
for (let [field, value] of Object.entries(fields)) { for (let [field, value] of Object.entries(fields)) {
draft[field] = value deepSet(payload, field, value)
} }
} }
if (tableId) { if (tableId) {
draft.tableId = tableId payload.tableId = tableId
}
const row = await saveRow(payload)
return {
row,
}
}
const duplicateRowHandler = async (action, context) => {
const { fields, providerId, tableId } = action.parameters
if (providerId) {
let payload = { ...context[providerId] }
if (fields) {
for (let [field, value] of Object.entries(fields)) {
deepSet(payload, field, value)
}
}
if (tableId) {
payload.tableId = tableId
}
delete payload._id
delete payload._rev
const row = await saveRow(payload)
return {
row,
} }
await saveRow(draft)
} }
} }
@ -46,11 +75,12 @@ const navigationHandler = action => {
const queryExecutionHandler = async action => { const queryExecutionHandler = async action => {
const { datasourceId, queryId, queryParams } = action.parameters const { datasourceId, queryId, queryParams } = action.parameters
await executeQuery({ const result = await executeQuery({
datasourceId, datasourceId,
queryId, queryId,
parameters: queryParams, parameters: queryParams,
}) })
return { result }
} }
const executeActionHandler = async ( const executeActionHandler = async (
@ -129,6 +159,7 @@ const updateStateHandler = action => {
const handlerMap = { const handlerMap = {
["Save Row"]: saveRowHandler, ["Save Row"]: saveRowHandler,
["Duplicate Row"]: duplicateRowHandler,
["Delete Row"]: deleteRowHandler, ["Delete Row"]: deleteRowHandler,
["Navigate To"]: navigationHandler, ["Navigate To"]: navigationHandler,
["Execute Query"]: queryExecutionHandler, ["Execute Query"]: queryExecutionHandler,
@ -165,12 +196,27 @@ export const enrichButtonActions = (actions, context) => {
return actions return actions
} }
// Button context is built up as actions are executed.
// Inherit any previous button context which may have come from actions
// before a confirmable action since this breaks the chain.
let buttonContext = context.actions || []
const handlers = actions.map(def => handlerMap[def["##eventHandlerType"]]) const handlers = actions.map(def => handlerMap[def["##eventHandlerType"]])
return async () => { return async () => {
for (let i = 0; i < handlers.length; i++) { for (let i = 0; i < handlers.length; i++) {
try { try {
const action = actions[i] // Skip any non-existent action definitions
const callback = async () => handlers[i](action, context) if (!handlers[i]) {
continue
}
// Built total context for this action
const totalContext = { ...context, actions: buttonContext }
// Get and enrich this button action with the total context
let action = actions[i]
action = enrichDataBindings(action, totalContext)
const callback = async () => handlers[i](action, totalContext)
// If this action is confirmable, show confirmation and await a // If this action is confirmable, show confirmation and await a
// callback to execute further actions // callback to execute further actions
@ -185,7 +231,15 @@ export const enrichButtonActions = (actions, context) => {
// then execute the rest of the actions in the chain // then execute the rest of the actions in the chain
const result = await callback() const result = await callback()
if (result !== false) { if (result !== false) {
const next = enrichButtonActions(actions.slice(i + 1), context) // Generate a new total context to pass into the next enrichment
buttonContext.push(result)
const newContext = { ...context, actions: buttonContext }
// Enrich and call the next button action
const next = enrichButtonActions(
actions.slice(i + 1),
newContext
)
await next() await next()
} }
} }
@ -201,6 +255,8 @@ export const enrichButtonActions = (actions, context) => {
const result = await callback() const result = await callback()
if (result === false) { if (result === false) {
return return
} else {
buttonContext.push(result)
} }
} }
} catch (error) { } catch (error) {

View File

@ -32,35 +32,56 @@ export const enrichProps = (props, context) => {
data: context[context.closestComponentId], data: context[context.closestComponentId],
} }
// Enrich all data bindings in top level props // We want to exclude any button actions from enrichment at this stage.
let enrichedProps = enrichDataBindings(props, totalContext) // Extract top level button action settings.
let normalProps = { ...props }
// Enrich click actions if they exist let actionProps = {}
Object.keys(enrichedProps).forEach(prop => { Object.keys(normalProps).forEach(prop => {
if (prop?.toLowerCase().includes("onclick")) { if (prop?.toLowerCase().includes("onclick")) {
enrichedProps[prop] = enrichButtonActions( actionProps[prop] = normalProps[prop]
enrichedProps[prop], delete normalProps[prop]
totalContext
)
} }
}) })
// Enrich any click actions in conditions // Handle conditional UI separately after normal settings
if (enrichedProps._conditions) { let conditions = normalProps._conditions
enrichedProps._conditions.forEach(condition => { delete normalProps._conditions
// Enrich all props except button actions
let enrichedProps = enrichDataBindings(normalProps, totalContext)
// Enrich button actions.
// Actions are enriched into a function at this stage, but actual data
// binding enrichment is done dynamically at runtime.
Object.keys(actionProps).forEach(prop => {
enrichedProps[prop] = enrichButtonActions(actionProps[prop], totalContext)
})
// Conditions
if (conditions?.length) {
let enrichedConditions = []
conditions.forEach(condition => {
if (condition.setting?.toLowerCase().includes("onclick")) { if (condition.setting?.toLowerCase().includes("onclick")) {
condition.settingValue = enrichButtonActions( // Copy and remove the setting value from the condition as it needs
// enriched separately
let toEnrich = { ...condition }
delete toEnrich.settingValue
// Join the condition back together
enrichedConditions.push({
...enrichDataBindings(toEnrich, totalContext),
settingValue: enrichButtonActions(
condition.settingValue, condition.settingValue,
totalContext totalContext
) ),
rand: Math.random(),
// If there is an onclick function in here then it won't be serialised })
// properly, and therefore will not be updated properly. } else {
// The solution to this is add a rand which will ensure diffs happen // Normal condition
// every time. enrichedConditions.push(enrichDataBindings(condition, totalContext))
condition.rand = Math.random()
} }
}) })
enrichedProps._conditions = enrichedConditions
} }
return enrichedProps return enrichedProps

View File

@ -1,3 +1,8 @@
import {
convertJSONSchemaToTableSchema,
getJSONArrayDatasourceSchema,
} from "builder/src/builderStore/jsonUtils"
import { fetchTableDefinition } from "api"
import TableFetch from "./fetch/TableFetch.js" import TableFetch from "./fetch/TableFetch.js"
import ViewFetch from "./fetch/ViewFetch.js" import ViewFetch from "./fetch/ViewFetch.js"
import QueryFetch from "./fetch/QueryFetch.js" import QueryFetch from "./fetch/QueryFetch.js"
@ -7,16 +12,17 @@ import QueryFetch from "./fetch/QueryFetch.js"
*/ */
export const fetchDatasourceSchema = async datasource => { export const fetchDatasourceSchema = async datasource => {
const type = datasource?.type const type = datasource?.type
let schema
// Nested providers should already have exposed their own schema // Nested providers should already have exposed their own schema
if (type === "provider") { if (type === "provider") {
return datasource.value?.schema schema = datasource.value?.schema
} }
// Field sources have their schema statically defined // Field sources have their schema statically defined
if (type === "field") { if (type === "field") {
if (datasource.fieldType === "attachment") { if (datasource.fieldType === "attachment") {
return { schema = {
url: { url: {
type: "string", type: "string",
}, },
@ -25,7 +31,7 @@ export const fetchDatasourceSchema = async datasource => {
}, },
} }
} else if (datasource.fieldType === "array") { } else if (datasource.fieldType === "array") {
return { schema = {
value: { value: {
type: "string", type: "string",
}, },
@ -33,7 +39,14 @@ export const fetchDatasourceSchema = async datasource => {
} }
} }
// All normal datasource schema can use their corresponsing implementations // JSON arrays need their table definitions fetched.
// We can then extract their schema as a subset of the table schema.
if (type === "jsonarray") {
const table = await fetchTableDefinition(datasource.tableId)
schema = getJSONArrayDatasourceSchema(table?.schema, datasource)
}
// All normal datasource schema can use their corresponding implementations
// in the data fetch classes // in the data fetch classes
const handler = { const handler = {
table: TableFetch, table: TableFetch,
@ -43,7 +56,27 @@ export const fetchDatasourceSchema = async datasource => {
}[type] }[type]
if (handler) { if (handler) {
const definition = await handler.getDefinition(datasource) const definition = await handler.getDefinition(datasource)
return handler.getSchema(datasource, definition) schema = handler.getSchema(datasource, definition)
}
// Check for any JSON fields so we can add any top level properties
if (schema) {
let jsonAdditions = {}
Object.keys(schema).forEach(fieldKey => {
const fieldSchema = schema[fieldKey]
if (fieldSchema?.type === "json") {
const jsonSchema = convertJSONSchemaToTableSchema(fieldSchema, {
squashObjects: true,
})
Object.keys(jsonSchema).forEach(jsonKey => {
jsonAdditions[`${fieldKey}.${jsonKey}`] = {
type: jsonSchema[jsonKey].type,
nestedJSON: true,
}
})
}
})
return { ...schema, ...jsonAdditions }
} }
return null return null

View File

@ -305,6 +305,11 @@
resolved "https://registry.yarnpkg.com/@spectrum-css/tabs/-/tabs-3.1.5.tgz#cc82e69c1fc721902345178231fb95d05938b983" resolved "https://registry.yarnpkg.com/@spectrum-css/tabs/-/tabs-3.1.5.tgz#cc82e69c1fc721902345178231fb95d05938b983"
integrity sha512-UtfW8bA1quYnJM6v/lp6AVYGnQFkiUix2FHAf/4VHVrk4mh7ydtLiXS0IR3Kx+t/S8FWdSdSQHDZ8tHbY1ZLZg== integrity sha512-UtfW8bA1quYnJM6v/lp6AVYGnQFkiUix2FHAf/4VHVrk4mh7ydtLiXS0IR3Kx+t/S8FWdSdSQHDZ8tHbY1ZLZg==
"@spectrum-css/tag@^3.1.4":
version "3.1.4"
resolved "https://registry.yarnpkg.com/@spectrum-css/tag/-/tag-3.1.4.tgz#334384dd789ddf0562679cae62ef763883480ac5"
integrity sha512-9dYBMhCEkjy+p75XJIfCA2/zU4JAqsJrL7fkYIDXakS6/BzeVtIvAW/6JaIHtLIA9lrj0Sn4m+ZjceKnZNIv1w==
"@spectrum-css/tags@^3.0.2": "@spectrum-css/tags@^3.0.2":
version "3.0.3" version "3.0.3"
resolved "https://registry.yarnpkg.com/@spectrum-css/tags/-/tags-3.0.3.tgz#fc76d2735cdc442de91b7eb3bee49a928c0767ac" resolved "https://registry.yarnpkg.com/@spectrum-css/tags/-/tags-3.0.3.tgz#fc76d2735cdc442de91b7eb3bee49a928c0767ac"

View File

@ -1,7 +1,7 @@
{ {
"name": "@budibase/server", "name": "@budibase/server",
"email": "hi@budibase.com", "email": "hi@budibase.com",
"version": "1.0.23", "version": "1.0.27-alpha.1",
"description": "Budibase Web Server", "description": "Budibase Web Server",
"main": "src/index.ts", "main": "src/index.ts",
"repository": { "repository": {
@ -69,9 +69,10 @@
"author": "Budibase", "author": "Budibase",
"license": "GPL-3.0", "license": "GPL-3.0",
"dependencies": { "dependencies": {
"@budibase/auth": "^1.0.23", "@apidevtools/swagger-parser": "^10.0.3",
"@budibase/client": "^1.0.23", "@budibase/auth": "^1.0.27-alpha.1",
"@budibase/string-templates": "^1.0.23", "@budibase/client": "^1.0.27-alpha.1",
"@budibase/string-templates": "^1.0.27-alpha.1",
"@bull-board/api": "^3.7.0", "@bull-board/api": "^3.7.0",
"@bull-board/koa": "^3.7.0", "@bull-board/koa": "^3.7.0",
"@elastic/elasticsearch": "7.10.0", "@elastic/elasticsearch": "7.10.0",

View File

@ -50,10 +50,10 @@ exports.validate = async ({ appId, tableId, row, table }) => {
const errors = {} const errors = {}
for (let fieldName of Object.keys(table.schema)) { for (let fieldName of Object.keys(table.schema)) {
const constraints = cloneDeep(table.schema[fieldName].constraints) const constraints = cloneDeep(table.schema[fieldName].constraints)
const type = table.schema[fieldName].type
// special case for options, need to always allow unselected (null) // special case for options, need to always allow unselected (null)
if ( if (
table.schema[fieldName].type === (type === FieldTypes.OPTIONS || type === FieldTypes.ARRAY) &&
(FieldTypes.OPTIONS || FieldTypes.ARRAY) &&
constraints.inclusion constraints.inclusion
) { ) {
constraints.inclusion.push(null) constraints.inclusion.push(null)
@ -61,17 +61,20 @@ exports.validate = async ({ appId, tableId, row, table }) => {
let res let res
// Validate.js doesn't seem to handle array // Validate.js doesn't seem to handle array
if ( if (type === FieldTypes.ARRAY && row[fieldName] && row[fieldName].length) {
table.schema[fieldName].type === FieldTypes.ARRAY &&
row[fieldName] &&
row[fieldName].length
) {
row[fieldName].map(val => { row[fieldName].map(val => {
if (!constraints.inclusion.includes(val)) { if (!constraints.inclusion.includes(val)) {
errors[fieldName] = "Field not in list" errors[fieldName] = "Field not in list"
} }
}) })
} else if (table.schema[fieldName].type === FieldTypes.FORMULA) { } else if (type === FieldTypes.JSON && typeof row[fieldName] === "string") {
// this should only happen if there is an error
try {
JSON.parse(row[fieldName])
} catch (err) {
errors[fieldName] = [`Contains invalid JSON`]
}
} else if (type === FieldTypes.FORMULA) {
res = validateJs.single( res = validateJs.single(
processStringSync(table.schema[fieldName].formula, row), processStringSync(table.schema[fieldName].formula, row),
constraints constraints

View File

@ -81,6 +81,18 @@ const TYPE_TRANSFORM_MAP = {
[FieldTypes.AUTO]: { [FieldTypes.AUTO]: {
parse: () => undefined, parse: () => undefined,
}, },
[FieldTypes.JSON]: {
parse: input => {
try {
if (input === "") {
return undefined
}
return JSON.parse(input)
} catch (err) {
return input
}
},
},
} }
/** /**

View File

@ -27,7 +27,7 @@
resolved "https://registry.yarnpkg.com/@apidevtools/swagger-methods/-/swagger-methods-3.0.2.tgz#b789a362e055b0340d04712eafe7027ddc1ac267" resolved "https://registry.yarnpkg.com/@apidevtools/swagger-methods/-/swagger-methods-3.0.2.tgz#b789a362e055b0340d04712eafe7027ddc1ac267"
integrity sha512-QAkD5kK2b1WfjDS/UQn/qQkbwF31uqRjPTrsCs5ZG9BQGAkjwvqGFjjPqAuzac/IYzpPtRzjCP1WrTuAIjMrXg== integrity sha512-QAkD5kK2b1WfjDS/UQn/qQkbwF31uqRjPTrsCs5ZG9BQGAkjwvqGFjjPqAuzac/IYzpPtRzjCP1WrTuAIjMrXg==
"@apidevtools/swagger-parser@10.0.3": "@apidevtools/swagger-parser@10.0.3", "@apidevtools/swagger-parser@^10.0.3":
version "10.0.3" version "10.0.3"
resolved "https://registry.yarnpkg.com/@apidevtools/swagger-parser/-/swagger-parser-10.0.3.tgz#32057ae99487872c4dd96b314a1ab4b95d89eaf5" resolved "https://registry.yarnpkg.com/@apidevtools/swagger-parser/-/swagger-parser-10.0.3.tgz#32057ae99487872c4dd96b314a1ab4b95d89eaf5"
integrity sha512-sNiLY51vZOmSPFZA5TF35KZ2HbgYklQnTSDnkghamzLb3EkNtcQnrBQEj5AOCxHpTtXpqMCRM1CrmV2rG6nw4g== integrity sha512-sNiLY51vZOmSPFZA5TF35KZ2HbgYklQnTSDnkghamzLb3EkNtcQnrBQEj5AOCxHpTtXpqMCRM1CrmV2rG6nw4g==
@ -983,10 +983,10 @@
resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39" resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39"
integrity sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw== integrity sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==
"@budibase/auth@^1.0.23": "@budibase/auth@^1.0.27-alpha.1":
version "1.0.24" version "1.0.33"
resolved "https://registry.yarnpkg.com/@budibase/auth/-/auth-1.0.24.tgz#69a015a7fe9dcc9ea9e471a7d76d2c0544de0b16" resolved "https://registry.yarnpkg.com/@budibase/auth/-/auth-1.0.33.tgz#bb0c527ebca852c001c7e163a9cdfce49984adab"
integrity sha512-pX+x+ib5X8s9ek4TZVmddRXlCE3kFmK5EH7Qk0Wmcn9tZwbaZy1XXd4qlJ7aFGSmvy1VNbz5AXIyRtliescaEQ== integrity sha512-CJcBspSB6B4UwXenGfzCiBh4wWPq9ZrRZm+Dqo774Yw7dfa/hHisfFiRAuz0vHxZl6AStLUsTBNynrzt7bs7Mg==
dependencies: dependencies:
"@techpass/passport-openidconnect" "^0.3.0" "@techpass/passport-openidconnect" "^0.3.0"
aws-sdk "^2.901.0" aws-sdk "^2.901.0"
@ -1056,10 +1056,10 @@
svelte-flatpickr "^3.2.3" svelte-flatpickr "^3.2.3"
svelte-portal "^1.0.0" svelte-portal "^1.0.0"
"@budibase/bbui@^1.0.24": "@budibase/bbui@^1.0.33":
version "1.0.24" version "1.0.33"
resolved "https://registry.yarnpkg.com/@budibase/bbui/-/bbui-1.0.24.tgz#15a976b500c4103bc3e94ed665c6c557bb6271bf" resolved "https://registry.yarnpkg.com/@budibase/bbui/-/bbui-1.0.33.tgz#e815c4883dbc9d28d31900160fc74aafc81d02c1"
integrity sha512-4zr+fSChOelHDqNNM2OiWWbWOL57JGBVBx333GiMr2P7N0MoepjwHEp9MlITWl5mUfS8rxgWTC9mSSMpRdpSUQ== integrity sha512-2W3Ub8J8brSuhwtXhznszTa54cTyUdDNMFDCnRrHseaDV05XrSykdndXYyWi4XC+FveV82AdvSICxgd6alRGzg==
dependencies: dependencies:
"@adobe/spectrum-css-workflow-icons" "^1.2.1" "@adobe/spectrum-css-workflow-icons" "^1.2.1"
"@spectrum-css/actionbutton" "^1.0.1" "@spectrum-css/actionbutton" "^1.0.1"
@ -1106,14 +1106,14 @@
svelte-flatpickr "^3.2.3" svelte-flatpickr "^3.2.3"
svelte-portal "^1.0.0" svelte-portal "^1.0.0"
"@budibase/client@^1.0.23": "@budibase/client@^1.0.27-alpha.1":
version "1.0.24" version "1.0.33"
resolved "https://registry.yarnpkg.com/@budibase/client/-/client-1.0.24.tgz#69be5b66cc701c6018c816728975a076230c052a" resolved "https://registry.yarnpkg.com/@budibase/client/-/client-1.0.33.tgz#715aaaf15f13d9e40960fffea44002e817d8a569"
integrity sha512-ppb+exifHCfuDGqs1cQ0kzc3wWaOGkVgq/8Rdb7R7BLIOKYkB67L3FvkqTl+39wrc32CpmzOXXQ73keE4l6P8Q== integrity sha512-4+xM/ZTI247JbJc1noQS58VFh8AqfuQK1ZUSZIowuol1TtDu5dLGN7BysUFUe0mJZoDB/L+cB1I//43XqaSWDw==
dependencies: dependencies:
"@budibase/bbui" "^1.0.24" "@budibase/bbui" "^1.0.33"
"@budibase/standard-components" "^0.9.139" "@budibase/standard-components" "^0.9.139"
"@budibase/string-templates" "^1.0.24" "@budibase/string-templates" "^1.0.33"
regexparam "^1.3.0" regexparam "^1.3.0"
shortid "^2.2.15" shortid "^2.2.15"
svelte-spa-router "^3.0.5" svelte-spa-router "^3.0.5"
@ -1163,10 +1163,10 @@
svelte-apexcharts "^1.0.2" svelte-apexcharts "^1.0.2"
svelte-flatpickr "^3.1.0" svelte-flatpickr "^3.1.0"
"@budibase/string-templates@^1.0.23", "@budibase/string-templates@^1.0.24": "@budibase/string-templates@^1.0.27-alpha.1", "@budibase/string-templates@^1.0.33":
version "1.0.24" version "1.0.33"
resolved "https://registry.yarnpkg.com/@budibase/string-templates/-/string-templates-1.0.24.tgz#8f317ff18159d8f8e36179de33252e019c85c267" resolved "https://registry.yarnpkg.com/@budibase/string-templates/-/string-templates-1.0.33.tgz#7c39f0821d86ca1b26e8d5e7eca4256f24b09de0"
integrity sha512-KAwyYxtm3Mrsm44MhchSxOtB8ucZ2JI93VPBQn0hjdzHtMuAkadtFNPZFeD3pKpCjDmI13XuW+BmJqCF3B0tYg== integrity sha512-uvDJvVtAaBcMKnVylWlzGk+CzPXH+TIKojGAfgjCqfcBsc9GlyCCBLtANe8osi9Bdr1SsKOW9Fi5u9FTToNMJw==
dependencies: dependencies:
"@budibase/handlebars-helpers" "^0.11.7" "@budibase/handlebars-helpers" "^0.11.7"
dayjs "^1.10.4" dayjs "^1.10.4"
@ -3304,9 +3304,9 @@ aws-sdk@^2.767.0:
xml2js "0.4.19" xml2js "0.4.19"
aws-sdk@^2.901.0: aws-sdk@^2.901.0:
version "2.1046.0" version "2.1049.0"
resolved "https://registry.yarnpkg.com/aws-sdk/-/aws-sdk-2.1046.0.tgz#9147b0fa1c86acbebd1a061e951ab5012f4499d7" resolved "https://registry.yarnpkg.com/aws-sdk/-/aws-sdk-2.1049.0.tgz#8146dcdf3a1ab603e50ff961169ee8abc537d48e"
integrity sha512-ocwHclMXdIA+NWocUyvp9Ild3/zy2vr5mHp3mTyodf0WU5lzBE8PocCVLSWhMAXLxyia83xv2y5f5AzAcetbqA== integrity sha512-+wls9iNlotMeoZepwgR0yPzXsjXzr2ijoi5ERmsPWfMTFMHkm6INndBtSkm6fpu/NZnl+7EaPPES2yhaqnhoJg==
dependencies: dependencies:
buffer "4.9.2" buffer "4.9.2"
events "1.1.1" events "1.1.1"

View File

@ -1,6 +1,6 @@
{ {
"name": "@budibase/string-templates", "name": "@budibase/string-templates",
"version": "1.0.23", "version": "1.0.27-alpha.1",
"description": "Handlebars wrapper for Budibase templating.", "description": "Handlebars wrapper for Budibase templating.",
"main": "src/index.cjs", "main": "src/index.cjs",
"module": "dist/bundle.mjs", "module": "dist/bundle.mjs",

View File

@ -1,4 +1,5 @@
const { atob } = require("../utilities") const { atob } = require("../utilities")
const { cloneDeep } = require("lodash/fp")
// The method of executing JS scripts depends on the bundle being built. // The method of executing JS scripts depends on the bundle being built.
// This setter is used in the entrypoint (either index.cjs or index.mjs). // This setter is used in the entrypoint (either index.cjs or index.mjs).
@ -38,8 +39,12 @@ module.exports.processJS = (handlebars, context) => {
// This is required to allow the final `return` statement to be valid. // This is required to allow the final `return` statement to be valid.
const js = `function run(){${atob(handlebars)}};run();` const js = `function run(){${atob(handlebars)}};run();`
// Our $ context function gets a value from context // Our $ context function gets a value from context.
const sandboxContext = { $: path => getContextValue(path, context) } // We clone the context to avoid mutation in the binding affecting real
// app context.
const sandboxContext = {
$: path => getContextValue(path, cloneDeep(context)),
}
// Create a sandbox with out context and run the JS // Create a sandbox with out context and run the JS
return runJS(js, sandboxContext) return runJS(js, sandboxContext)

View File

@ -1,12 +1,13 @@
const handlebars = require("handlebars") const handlebars = require("handlebars")
const { registerAll } = require("./helpers/index") const { registerAll } = require("./helpers/index")
const processors = require("./processors") const processors = require("./processors")
const { removeHandlebarsStatements, atob, btoa } = require("./utilities") const { atob, btoa } = require("./utilities")
const manifest = require("../manifest.json") const manifest = require("../manifest.json")
const hbsInstance = handlebars.create() const hbsInstance = handlebars.create()
registerAll(hbsInstance) registerAll(hbsInstance)
const hbsInstanceNoHelpers = handlebars.create() const hbsInstanceNoHelpers = handlebars.create()
const defaultOpts = { noHelpers: false }
/** /**
* utility function to check if the object is valid * utility function to check if the object is valid
@ -28,11 +29,7 @@ function testObject(object) {
* @param {object} opts optional - specify some options for processing. * @param {object} opts optional - specify some options for processing.
* @returns {Promise<object|array>} The structure input, as fully updated as possible. * @returns {Promise<object|array>} The structure input, as fully updated as possible.
*/ */
module.exports.processObject = async ( module.exports.processObject = async (object, context, opts) => {
object,
context,
opts = { noHelpers: false }
) => {
testObject(object) testObject(object)
for (let key of Object.keys(object || {})) { for (let key of Object.keys(object || {})) {
if (object[key] != null) { if (object[key] != null) {
@ -63,11 +60,7 @@ module.exports.processObject = async (
* @param {object} opts optional - specify some options for processing. * @param {object} opts optional - specify some options for processing.
* @returns {Promise<string>} The enriched string, all templates should have been replaced if they can be. * @returns {Promise<string>} The enriched string, all templates should have been replaced if they can be.
*/ */
module.exports.processString = async ( module.exports.processString = async (string, context, opts) => {
string,
context,
opts = { noHelpers: false }
) => {
// TODO: carry out any async calls before carrying out async call // TODO: carry out any async calls before carrying out async call
return module.exports.processStringSync(string, context, opts) return module.exports.processStringSync(string, context, opts)
} }
@ -81,11 +74,7 @@ module.exports.processString = async (
* @param {object} opts optional - specify some options for processing. * @param {object} opts optional - specify some options for processing.
* @returns {object|array} The structure input, as fully updated as possible. * @returns {object|array} The structure input, as fully updated as possible.
*/ */
module.exports.processObjectSync = ( module.exports.processObjectSync = (object, context, opts) => {
object,
context,
opts = { noHelpers: false }
) => {
testObject(object) testObject(object)
for (let key of Object.keys(object || {})) { for (let key of Object.keys(object || {})) {
let val = object[key] let val = object[key]
@ -106,26 +95,20 @@ module.exports.processObjectSync = (
* @param {object} opts optional - specify some options for processing. * @param {object} opts optional - specify some options for processing.
* @returns {string} The enriched string, all templates should have been replaced if they can be. * @returns {string} The enriched string, all templates should have been replaced if they can be.
*/ */
module.exports.processStringSync = ( module.exports.processStringSync = (string, context, opts) => {
string, opts = { ...defaultOpts, ...opts }
context,
opts = { noHelpers: false } // take a copy of input in case of error
) => {
if (!exports.isValid(string)) {
return string
}
// take a copy of input incase error
const input = string const input = string
if (typeof string !== "string") { if (typeof string !== "string") {
throw "Cannot process non-string types." throw "Cannot process non-string types."
} }
try { try {
const noHelpers = opts && opts.noHelpers
// finalising adds a helper, can't do this with no helpers // finalising adds a helper, can't do this with no helpers
const shouldFinalise = !noHelpers const shouldFinalise = !opts.noHelpers
string = processors.preprocess(string, shouldFinalise) string = processors.preprocess(string, shouldFinalise)
// this does not throw an error when template can't be fulfilled, have to try correct beforehand // this does not throw an error when template can't be fulfilled, have to try correct beforehand
const instance = noHelpers ? hbsInstanceNoHelpers : hbsInstance const instance = opts.noHelpers ? hbsInstanceNoHelpers : hbsInstance
const template = instance.compile(string, { const template = instance.compile(string, {
strict: false, strict: false,
}) })
@ -136,7 +119,7 @@ module.exports.processStringSync = (
}) })
) )
} catch (err) { } catch (err) {
return removeHandlebarsStatements(input) return input
} }
} }
@ -155,7 +138,8 @@ module.exports.makePropSafe = property => {
* @param opts optional - specify some options for processing. * @param opts optional - specify some options for processing.
* @returns {boolean} Whether or not the input string is valid. * @returns {boolean} Whether or not the input string is valid.
*/ */
module.exports.isValid = (string, opts = { noHelpers: false }) => { module.exports.isValid = (string, opts) => {
opts = { ...defaultOpts, ...opts }
const validCases = [ const validCases = [
"string", "string",
"number", "number",
@ -169,7 +153,7 @@ module.exports.isValid = (string, opts = { noHelpers: false }) => {
// don't really need a real context to check if its valid // don't really need a real context to check if its valid
const context = {} const context = {}
try { try {
const instance = opts && opts.noHelpers ? hbsInstanceNoHelpers : hbsInstance const instance = opts.noHelpers ? hbsInstanceNoHelpers : hbsInstance
instance.compile(processors.preprocess(string, false))(context) instance.compile(processors.preprocess(string, false))(context)
return true return true
} catch (err) { } catch (err) {

View File

@ -10,7 +10,10 @@ module.exports.swapStrings = (string, start, length, swap) => {
return string.slice(0, start) + swap + string.slice(start + length) return string.slice(0, start) + swap + string.slice(start + length)
} }
module.exports.removeHandlebarsStatements = string => { module.exports.removeHandlebarsStatements = (
string,
replacement = "Invalid binding"
) => {
let regexp = new RegExp(exports.FIND_HBS_REGEX) let regexp = new RegExp(exports.FIND_HBS_REGEX)
let matches = string.match(regexp) let matches = string.match(regexp)
if (matches == null) { if (matches == null) {
@ -18,7 +21,7 @@ module.exports.removeHandlebarsStatements = string => {
} }
for (let match of matches) { for (let match of matches) {
const idx = string.indexOf(match) const idx = string.indexOf(match)
string = exports.swapStrings(string, idx, match.length, "Invalid Binding") string = exports.swapStrings(string, idx, match.length, replacement)
} }
return string return string
} }

View File

@ -13,10 +13,14 @@ describe("test the custom helpers we have applied", () => {
describe("test that it can run without helpers", () => { describe("test that it can run without helpers", () => {
it("should be able to run without helpers", async () => { it("should be able to run without helpers", async () => {
const output = await processString("{{ avg 1 1 1 }}", {}, { noHelpers: true }) const output = await processString(
"{{ avg 1 1 1 }}",
{},
{ noHelpers: true }
)
const valid = await processString("{{ avg 1 1 1 }}", {}) const valid = await processString("{{ avg 1 1 1 }}", {})
expect(valid).toBe("1") expect(valid).toBe("1")
expect(output).toBe("Invalid Binding") expect(output).toBe("{{ avg 1 1 1 }}")
}) })
}) })
@ -185,17 +189,22 @@ describe("test the date helpers", () => {
it("should test the timezone capabilities", async () => { it("should test the timezone capabilities", async () => {
const date = new Date(1611577535000) const date = new Date(1611577535000)
const output = await processString("{{ date time 'HH-mm-ss Z' 'America/New_York' }}", { const output = await processString(
"{{ date time 'HH-mm-ss Z' 'America/New_York' }}",
{
time: date.toUTCString(), time: date.toUTCString(),
}) }
const formatted = new dayjs(date).tz("America/New_York").format("HH-mm-ss Z") )
const formatted = new dayjs(date)
.tz("America/New_York")
.format("HH-mm-ss Z")
expect(output).toBe(formatted) expect(output).toBe(formatted)
}) })
it("should guess the users timezone when not specified", async () => { it("should guess the users timezone when not specified", async () => {
const date = new Date() const date = new Date()
const output = await processString("{{ date time 'Z' }}", { const output = await processString("{{ date time 'Z' }}", {
time: date.toUTCString() time: date.toUTCString(),
}) })
const timezone = dayjs.tz.guess() const timezone = dayjs.tz.guess()
const offset = new dayjs(date).tz(timezone).format("Z") const offset = new dayjs(date).tz(timezone).format("Z")
@ -307,12 +316,12 @@ describe("test the comparison helpers", () => {
describe("Test the object/array helper", () => { describe("Test the object/array helper", () => {
it("should allow plucking from an array of objects", async () => { it("should allow plucking from an array of objects", async () => {
const context = { const context = {
items: [ items: [{ price: 20 }, { price: 30 }],
{ price: 20 },
{ price: 30 },
]
} }
const output = await processString("{{ literal ( sum ( pluck items 'price' ) ) }}", context) const output = await processString(
"{{ literal ( sum ( pluck items 'price' ) ) }}",
context
)
expect(output).toBe(50) expect(output).toBe(50)
}) })
@ -450,7 +459,7 @@ describe("Cover a few complex use cases", () => {
rowCount: 8, rowCount: 8,
} }
const output = await processObject(input, tableJson) const output = await processObject(input, tableJson)
expect(output.dataProvider).not.toBe("Invalid Binding") expect(output.dataProvider).not.toBe("Invalid binding")
}) })
it("should be able to handle external ids", async () => { it("should be able to handle external ids", async () => {

View File

@ -1,7 +1,7 @@
{ {
"name": "@budibase/worker", "name": "@budibase/worker",
"email": "hi@budibase.com", "email": "hi@budibase.com",
"version": "1.0.23", "version": "1.0.27-alpha.1",
"description": "Budibase background service", "description": "Budibase background service",
"main": "src/index.js", "main": "src/index.js",
"repository": { "repository": {
@ -29,8 +29,8 @@
"author": "Budibase", "author": "Budibase",
"license": "GPL-3.0", "license": "GPL-3.0",
"dependencies": { "dependencies": {
"@budibase/auth": "^1.0.23", "@budibase/auth": "^1.0.27-alpha.1",
"@budibase/string-templates": "^1.0.23", "@budibase/string-templates": "^1.0.27-alpha.1",
"@koa/router": "^8.0.0", "@koa/router": "^8.0.0",
"@sentry/node": "^6.0.0", "@sentry/node": "^6.0.0",
"@techpass/passport-openidconnect": "^0.3.0", "@techpass/passport-openidconnect": "^0.3.0",

View File

@ -8,7 +8,7 @@ jest.mock("nodemailer")
const nodemailer = require("nodemailer") const nodemailer = require("nodemailer")
nodemailer.createTransport.mockReturnValue({ nodemailer.createTransport.mockReturnValue({
sendMail: sendMailMock, sendMail: sendMailMock,
verify: jest.fn() verify: jest.fn(),
}) })
describe("/api/global/email", () => { describe("/api/global/email", () => {
@ -39,6 +39,6 @@ describe("/api/global/email", () => {
expect(sendMailMock).toHaveBeenCalled() expect(sendMailMock).toHaveBeenCalled()
const emailCall = sendMailMock.mock.calls[0][0] const emailCall = sendMailMock.mock.calls[0][0]
expect(emailCall.subject).toBe("Hello!") expect(emailCall.subject).toBe("Hello!")
expect(emailCall.html).not.toContain("Invalid Binding") expect(emailCall.html).not.toContain("Invalid binding")
}) })
}) })