Merge branch 'develop' of github.com:Budibase/budibase into rest-pagination
This commit is contained in:
commit
db55074b0a
|
@ -16,26 +16,46 @@ jobs:
|
|||
|
||||
- name: Pull values.yaml from budibase-infra
|
||||
run: |
|
||||
curl -H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" \ [c3a7a9d12]
|
||||
curl -H "Authorization: token ${{ secrets.GH_PERSONAL_TOKEN }}" \
|
||||
-H 'Accept: application/vnd.github.v3.raw' \
|
||||
-o values.production.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
|
||||
if: ${{ !github.event.inputs.version }}
|
||||
- name: Get the latest budibase release version
|
||||
id: version
|
||||
run: |
|
||||
sudo apt-get install -y jq
|
||||
if [ -z "${{ github.event.inputs.version }}" ]; then
|
||||
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:
|
||||
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
|
||||
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
|
||||
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
|
||||
config-files: values.production.yaml
|
||||
chart-path: charts/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: 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 }}
|
||||
|
||||
|
|
|
@ -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 }}
|
|
@ -9,15 +9,16 @@ jobs:
|
|||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
with:
|
||||
fetch_depth: 0
|
||||
|
||||
- name: Tag and release Docker images (Self Host)
|
||||
run: |
|
||||
docker login -u $DOCKER_USER -p $DOCKER_PASSWORD
|
||||
|
||||
# Get latest release version
|
||||
sudo apt-get install -y jq
|
||||
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
|
||||
|
||||
# Pull apps and worker images
|
||||
|
@ -25,16 +26,16 @@ jobs:
|
|||
docker pull budibase/worker:$release_tag
|
||||
|
||||
# Tag apps and worker images
|
||||
docker tag budibase/apps:$release_tag $SELF_HOST_TAG
|
||||
docker tag budibase/worker:$release_tag $SELF_HOST_TAG
|
||||
docker tag budibase/apps:$release_tag budibase/apps:$SELFHOST_TAG
|
||||
docker tag budibase/worker:$release_tag budibase/worker:$SELFHOST_TAG
|
||||
|
||||
# Push images
|
||||
docker push --all-tags budibase/apps
|
||||
docker push --all-tags budibase/worker
|
||||
docker push budibase/apps:$SELFHOST_TAG
|
||||
docker push budibase/worker:$SELFHOST_TAG
|
||||
env:
|
||||
DOCKER_USER: ${{ secrets.DOCKER_USERNAME }}
|
||||
DOCKER_PASSWORD: ${{ secrets.DOCKER_API_KEY }}
|
||||
SELF_HOST_TAG: latest
|
||||
SELFHOST_TAG: latest
|
||||
|
||||
- name: Setup Helm
|
||||
uses: azure/setup-helm@v1
|
||||
|
@ -44,17 +45,25 @@ jobs:
|
|||
# run: |
|
||||
# git config user.name "Budibase Helm Bot"
|
||||
# git config user.email "<>"
|
||||
# helm package charts/budibase
|
||||
# mv budibase-${{ env.RELEASE_VERSION }}.tgz docs
|
||||
# helm repo index docs
|
||||
# git checkout gh-pages
|
||||
# mv budibase-${{ github.steps.version.outputs.release_version }}.tgz docs
|
||||
# git add -A
|
||||
# git commit -m "Helm Release: ${{ github.steps.version.outputs.release_version }}"
|
||||
# git commit -m "Helm Release: ${{ env.RELEASE_VERSION }}"
|
||||
# git push
|
||||
# env:
|
||||
# GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
# - name: Perform github release
|
||||
# run: |
|
||||
# echo release
|
||||
# env:
|
||||
# GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}"
|
||||
# - name: Deploy
|
||||
# uses: peaceiris/actions-gh-pages@v3
|
||||
# with:
|
||||
# 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
|
|
@ -3,7 +3,7 @@ name: Budibase Release
|
|||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
- test
|
||||
|
||||
env:
|
||||
POSTHOG_TOKEN: ${{ secrets.POSTHOG_TOKEN }}
|
||||
|
@ -59,7 +59,7 @@ jobs:
|
|||
|
||||
# - name: Pull values.yaml from budibase-infra
|
||||
# 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' \
|
||||
# -o 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
|
||||
# token: ${{ github.token }}
|
||||
# values: |
|
||||
# globals:
|
||||
# appVersion: ${{ steps.previoustag.outputs.tag }}
|
||||
# value-files: >-
|
||||
# [
|
||||
# "values.preprod.yaml"
|
||||
# ]
|
||||
# # 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 }}
|
||||
|
|
|
@ -50,6 +50,14 @@ static_resources:
|
|||
route:
|
||||
cluster: app-service
|
||||
|
||||
- match:
|
||||
safe_regex:
|
||||
google_re2: {}
|
||||
regex: "/api/.*/export"
|
||||
route:
|
||||
timeout: 0s
|
||||
cluster: app-service
|
||||
|
||||
- match: { path: "/api/deploy" }
|
||||
route:
|
||||
timeout: 60s
|
||||
|
|
|
@ -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 !
|
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"version": "1.0.23",
|
||||
"version": "1.0.27-alpha.1",
|
||||
"npmClient": "yarn",
|
||||
"packages": [
|
||||
"packages/*"
|
||||
|
|
|
@ -23,8 +23,8 @@
|
|||
"build": "lerna run build",
|
||||
"publishdev": "lerna run publishdev",
|
||||
"publishnpm": "yarn build && lerna publish --force-publish",
|
||||
"release": "yarn build && lerna publish patch --yes --force-publish",
|
||||
"release:develop": "yarn build && lerna publish prerelease --yes --force-publish --dist-tag develop",
|
||||
"release": "lerna publish patch --yes --force-publish",
|
||||
"release:develop": "lerna publish prerelease --yes --force-publish --dist-tag develop",
|
||||
"restore": "yarn run clean && yarn run bootstrap && yarn run build",
|
||||
"nuke": "yarn run nuke:packages && yarn run nuke:docker",
|
||||
"nuke:packages": "yarn run restore",
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@budibase/auth",
|
||||
"version": "1.0.23",
|
||||
"version": "1.0.27-alpha.1",
|
||||
"description": "Authentication middlewares for budibase builder and apps",
|
||||
"main": "src/index.js",
|
||||
"author": "Budibase",
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "@budibase/bbui",
|
||||
"description": "A UI solution used in the different Budibase projects.",
|
||||
"version": "1.0.23",
|
||||
"version": "1.0.27-alpha.1",
|
||||
"license": "MPL-2.0",
|
||||
"svelte": "src/index.js",
|
||||
"module": "dist/bbui.es.js",
|
||||
|
|
|
@ -21,16 +21,8 @@
|
|||
}
|
||||
visible = false
|
||||
}
|
||||
|
||||
function handleKey(e) {
|
||||
if (visible && e.key === "Escape") {
|
||||
hide()
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:window on:keydown={handleKey} />
|
||||
|
||||
{#if visible}
|
||||
<Portal>
|
||||
<section class:fillWidth class="drawer" transition:slide>
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
import CellRenderer from "./CellRenderer.svelte"
|
||||
import SelectEditRenderer from "./SelectEditRenderer.svelte"
|
||||
import { cloneDeep } from "lodash"
|
||||
import { deepGet } from "../utils/helpers"
|
||||
|
||||
/**
|
||||
* The expected schema is our normal couch schemas for our tables.
|
||||
|
@ -318,7 +319,7 @@
|
|||
{customRenderers}
|
||||
{row}
|
||||
schema={schema[field]}
|
||||
value={row[field]}
|
||||
value={deepGet(row, field)}
|
||||
on:clickrelationship
|
||||
>
|
||||
<slot />
|
||||
|
|
|
@ -77,3 +77,6 @@ export { default as clickOutside } from "./Actions/click_outside"
|
|||
|
||||
// Stores
|
||||
export { notifications, createNotificationStore } from "./Stores/notifications"
|
||||
|
||||
// Utils
|
||||
export * from "./utils/helpers"
|
||||
|
|
|
@ -6,3 +6,61 @@ export const generateID = () => {
|
|||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@budibase/builder",
|
||||
"version": "1.0.23",
|
||||
"version": "1.0.27-alpha.1",
|
||||
"license": "GPL-3.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
|
@ -65,10 +65,10 @@
|
|||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"@budibase/bbui": "^1.0.23",
|
||||
"@budibase/client": "^1.0.23",
|
||||
"@budibase/bbui": "^1.0.27-alpha.1",
|
||||
"@budibase/client": "^1.0.27-alpha.1",
|
||||
"@budibase/colorpicker": "1.1.2",
|
||||
"@budibase/string-templates": "^1.0.23",
|
||||
"@budibase/string-templates": "^1.0.27-alpha.1",
|
||||
"@sentry/browser": "5.19.1",
|
||||
"@spectrum-css/page": "^3.0.1",
|
||||
"@spectrum-css/vars": "^3.0.1",
|
||||
|
|
|
@ -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 => {
|
||||
const def = store.actions.components.getDefinition(componentType)
|
||||
if (!def) {
|
||||
if (!componentType) {
|
||||
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
|
||||
?.filter(setting => setting.section)
|
||||
.forEach(section => {
|
||||
settings = settings.concat(section.settings || [])
|
||||
})
|
||||
}
|
||||
componentSettingCache[componentType] = settings
|
||||
|
||||
return settings
|
||||
}
|
|
@ -5,7 +5,7 @@ import {
|
|||
findComponent,
|
||||
findComponentPath,
|
||||
getComponentSettings,
|
||||
} from "./storeUtils"
|
||||
} from "./componentUtils"
|
||||
import { store } from "builderStore"
|
||||
import { queries as queriesStores, tables as tablesStore } from "stores/backend"
|
||||
import {
|
||||
|
@ -15,6 +15,11 @@ import {
|
|||
encodeJSBinding,
|
||||
} from "@budibase/string-templates"
|
||||
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
|
||||
const CAPTURE_VAR_INSIDE_TEMPLATE = /{{([^}]+)}}/g
|
||||
|
@ -186,6 +191,7 @@ const getProviderContextBindings = (asset, dataProviders) => {
|
|||
}
|
||||
|
||||
let schema
|
||||
let table
|
||||
let readablePrefix
|
||||
let runtimeSuffix = context.suffix
|
||||
|
||||
|
@ -209,8 +215,17 @@ const getProviderContextBindings = (asset, dataProviders) => {
|
|||
}
|
||||
const info = getSchemaForDatasource(asset, datasource)
|
||||
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
|
||||
}
|
||||
}
|
||||
if (!schema) {
|
||||
return
|
||||
}
|
||||
|
@ -229,7 +244,8 @@ const getProviderContextBindings = (asset, dataProviders) => {
|
|||
const fieldSchema = schema[key]
|
||||
|
||||
// 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
|
||||
let readableBinding = component._instanceName
|
||||
|
@ -247,6 +263,8 @@ const getProviderContextBindings = (asset, dataProviders) => {
|
|||
// datasource options, based on bindable properties
|
||||
fieldSchema,
|
||||
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.
|
||||
*/
|
||||
|
@ -347,16 +395,26 @@ export const getSchemaForDatasource = (asset, datasource, isForm = false) => {
|
|||
|
||||
if (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") {
|
||||
const component = findComponent(asset.props, datasource.providerId)
|
||||
const source = getDatasourceForProvider(asset, component)
|
||||
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
|
||||
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 }
|
||||
const { fieldType } = datasource
|
||||
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)
|
||||
}
|
||||
|
||||
// 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 (type === "view") {
|
||||
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
|
||||
if (schema && !isForm && ["table", "link"].includes(datasource.type)) {
|
||||
schema["_id"] = { type: "string" }
|
||||
|
@ -450,15 +538,58 @@ const buildFormSchema = component => {
|
|||
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.
|
||||
*/
|
||||
export function removeBindings(obj) {
|
||||
export const removeBindings = (obj, replacement = "Invalid binding") => {
|
||||
for (let [key, value] of Object.entries(obj)) {
|
||||
if (value && typeof value === "object") {
|
||||
obj[key] = removeBindings(value)
|
||||
obj[key] = removeBindings(value, replacement)
|
||||
} else if (typeof value === "string") {
|
||||
obj[key] = value.replace(CAPTURE_HBS_TEMPLATE, "Invalid binding")
|
||||
obj[key] = value.replace(CAPTURE_HBS_TEMPLATE, replacement)
|
||||
}
|
||||
}
|
||||
return obj
|
||||
|
@ -468,8 +599,8 @@ export function removeBindings(obj) {
|
|||
* When converting from readable to runtime it can sometimes add too many square brackets,
|
||||
* this makes sure that doesn't happen.
|
||||
*/
|
||||
function shouldReplaceBinding(currentValue, from, convertTo) {
|
||||
if (!currentValue?.includes(from)) {
|
||||
const shouldReplaceBinding = (currentValue, convertFrom, convertTo) => {
|
||||
if (!currentValue?.includes(convertFrom)) {
|
||||
return false
|
||||
}
|
||||
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
|
||||
// this makes sure it is detected
|
||||
const noSpaces = currentValue.replace(/\s+/g, "")
|
||||
const fromNoSpaces = from.replace(/\s+/g, "")
|
||||
const fromNoSpaces = convertFrom.replace(/\s+/g, "")
|
||||
const invalids = [
|
||||
`[${fromNoSpaces}]`,
|
||||
`"${fromNoSpaces}"`,
|
||||
|
@ -487,14 +618,21 @@ function shouldReplaceBinding(currentValue, from, convertTo) {
|
|||
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)
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
const isJS = isJSBinding(textWithBindings)
|
||||
if (isJS) {
|
||||
|
@ -559,14 +697,17 @@ function bindingReplacement(bindableProperties, textWithBindings, convertTo) {
|
|||
* Extracts a component ID from a handlebars expression setting of
|
||||
* {{ literal [componentId] }}
|
||||
*/
|
||||
function extractLiteralHandlebarsID(value) {
|
||||
const extractLiteralHandlebarsID = value => {
|
||||
return value?.match(/{{\s*literal\s*\[+([^\]]+)].*}}/)?.[1]
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a readable data binding into a runtime data binding
|
||||
*/
|
||||
export function readableToRuntimeBinding(bindableProperties, textWithBindings) {
|
||||
export const readableToRuntimeBinding = (
|
||||
bindableProperties,
|
||||
textWithBindings
|
||||
) => {
|
||||
return bindingReplacement(
|
||||
bindableProperties,
|
||||
textWithBindings,
|
||||
|
@ -577,56 +718,13 @@ export function readableToRuntimeBinding(bindableProperties, textWithBindings) {
|
|||
/**
|
||||
* Converts a runtime data binding into a readable data binding
|
||||
*/
|
||||
export function runtimeToReadableBinding(bindableProperties, textWithBindings) {
|
||||
export const runtimeToReadableBinding = (
|
||||
bindableProperties,
|
||||
textWithBindings
|
||||
) => {
|
||||
return bindingReplacement(
|
||||
bindableProperties,
|
||||
textWithBindings,
|
||||
"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)
|
||||
}
|
||||
|
|
|
@ -4,7 +4,7 @@ import { getHostingStore } from "./store/hosting"
|
|||
import { getThemeStore } from "./store/theme"
|
||||
import { derived, writable } from "svelte/store"
|
||||
import { FrontendTypes, LAYOUT_NAMES } from "../constants"
|
||||
import { findComponent } from "./storeUtils"
|
||||
import { findComponent } from "./componentUtils"
|
||||
|
||||
export const store = getFrontendStore()
|
||||
export const automationStore = getAutomationStore()
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -26,7 +26,7 @@ import {
|
|||
findAllMatchingComponents,
|
||||
findComponent,
|
||||
getComponentSettings,
|
||||
} from "../storeUtils"
|
||||
} from "../componentUtils"
|
||||
import { uuid } from "../uuid"
|
||||
import { removeBindings } from "../dataBinding"
|
||||
|
||||
|
@ -329,12 +329,12 @@ export const getFrontendStore = () => {
|
|||
},
|
||||
components: {
|
||||
select: component => {
|
||||
if (!component) {
|
||||
const asset = get(currentAsset)
|
||||
if (!asset || !component) {
|
||||
return
|
||||
}
|
||||
|
||||
// If this is the root component, select the asset instead
|
||||
const asset = get(currentAsset)
|
||||
const parent = findComponentParent(asset.props, component._id)
|
||||
if (parent == null) {
|
||||
const state = get(store)
|
||||
|
@ -537,7 +537,7 @@ export const getFrontendStore = () => {
|
|||
|
||||
// immediately need to remove bindings, currently these aren't valid when pasted
|
||||
if (!cut && !preserveBindings) {
|
||||
state.componentToPaste = removeBindings(state.componentToPaste)
|
||||
state.componentToPaste = removeBindings(state.componentToPaste, "")
|
||||
}
|
||||
|
||||
// Clone the component to paste
|
||||
|
|
|
@ -137,6 +137,7 @@ const fieldTypeToComponentMap = {
|
|||
datetime: "datetimefield",
|
||||
attachment: "attachmentfield",
|
||||
link: "relationshipfield",
|
||||
json: "jsonfield",
|
||||
}
|
||||
|
||||
export function makeDatasourceFormComponents(datasource) {
|
||||
|
@ -146,7 +147,7 @@ export function makeDatasourceFormComponents(datasource) {
|
|||
fields.forEach(field => {
|
||||
const fieldSchema = schema[field]
|
||||
// skip autocolumns
|
||||
if (fieldSchema.autocolumn) {
|
||||
if (fieldSchema.autocolumn || fieldSchema.nestedJSON) {
|
||||
return
|
||||
}
|
||||
const fieldType =
|
||||
|
|
|
@ -6,16 +6,20 @@
|
|||
Toggle,
|
||||
TextArea,
|
||||
Multiselect,
|
||||
Label,
|
||||
} from "@budibase/bbui"
|
||||
import Dropzone from "components/common/Dropzone.svelte"
|
||||
import { capitalise } from "helpers"
|
||||
import LinkedRowSelector from "components/common/LinkedRowSelector.svelte"
|
||||
import Editor from "../../integration/QueryEditor.svelte"
|
||||
|
||||
export let defaultValue
|
||||
export let meta
|
||||
export let value = defaultValue || (meta.type === "boolean" ? false : "")
|
||||
export let readonly
|
||||
|
||||
$: stringVal =
|
||||
typeof value === "object" ? JSON.stringify(value, null, 2) : value
|
||||
$: type = meta?.type
|
||||
$: label = meta.name ? capitalise(meta.name) : ""
|
||||
</script>
|
||||
|
@ -40,6 +44,14 @@
|
|||
<LinkedRowSelector bind:linkedRows={value} schema={meta} />
|
||||
{:else if type === "longform"}
|
||||
<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}
|
||||
<Input
|
||||
{label}
|
||||
|
|
|
@ -9,6 +9,7 @@
|
|||
DatePicker,
|
||||
ModalContent,
|
||||
Context,
|
||||
Modal,
|
||||
notifications,
|
||||
} from "@budibase/bbui"
|
||||
import { createEventDispatcher } from "svelte"
|
||||
|
@ -32,12 +33,14 @@
|
|||
import ModalBindableInput from "components/common/bindings/ModalBindableInput.svelte"
|
||||
import { getBindings } from "components/backend/DataTable/formula"
|
||||
import { getContext } from "svelte"
|
||||
import JSONSchemaModal from "./JSONSchemaModal.svelte"
|
||||
|
||||
const AUTO_TYPE = "auto"
|
||||
const FORMULA_TYPE = FIELDS.FORMULA.type
|
||||
const LINK_TYPE = FIELDS.LINK.type
|
||||
const STRING_TYPE = FIELDS.STRING.type
|
||||
const NUMBER_TYPE = FIELDS.NUMBER.type
|
||||
const JSON_TYPE = FIELDS.JSON.type
|
||||
const DATE_TYPE = FIELDS.DATETIME.type
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
@ -64,6 +67,7 @@
|
|||
let confirmDeleteDialog
|
||||
let deletion
|
||||
let deleteColName
|
||||
let jsonSchemaModal
|
||||
|
||||
$: checkConstraints(field)
|
||||
$: required = !!field?.constraints?.presence || primaryDisplay
|
||||
|
@ -79,10 +83,14 @@
|
|||
// used to select what different options can be displayed for column type
|
||||
$: canBeSearched =
|
||||
field.type !== LINK_TYPE &&
|
||||
field.type !== JSON_TYPE &&
|
||||
field.subtype !== AUTO_COLUMN_SUB_TYPES.CREATED_BY &&
|
||||
field.subtype !== AUTO_COLUMN_SUB_TYPES.UPDATED_BY &&
|
||||
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 =
|
||||
field.type !== LINK_TYPE && !uneditable && field.type !== AUTO_TYPE
|
||||
$: relationshipOptions = getRelationshipOptions(field)
|
||||
|
@ -176,6 +184,10 @@
|
|||
}
|
||||
}
|
||||
|
||||
function openJsonSchemaEditor() {
|
||||
jsonSchemaModal.show()
|
||||
}
|
||||
|
||||
function confirmDelete() {
|
||||
confirmDeleteDialog.show()
|
||||
deletion = true
|
||||
|
@ -430,6 +442,10 @@
|
|||
getOptionLabel={option => option[1].name}
|
||||
getOptionValue={option => option[0]}
|
||||
/>
|
||||
{:else if field.type === JSON_TYPE}
|
||||
<Button primary text on:click={openJsonSchemaEditor}
|
||||
>Open schema editor</Button
|
||||
>
|
||||
{/if}
|
||||
|
||||
<div slot="footer">
|
||||
|
@ -438,6 +454,16 @@
|
|||
{/if}
|
||||
</div>
|
||||
</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
|
||||
bind:this={confirmDeleteDialog}
|
||||
okText="Delete Column"
|
||||
|
|
|
@ -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>
|
|
@ -14,7 +14,7 @@
|
|||
notifications,
|
||||
} from "@budibase/bbui"
|
||||
import ErrorSVG from "assets/error.svg?raw"
|
||||
import { findComponent, findComponentPath } from "builderStore/storeUtils"
|
||||
import { findComponent, findComponentPath } from "builderStore/componentUtils"
|
||||
|
||||
let iframe
|
||||
let layout
|
||||
|
@ -69,15 +69,7 @@
|
|||
previewDevice: $store.previewDevice,
|
||||
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)
|
||||
$: strippedJson = json.replace(/"_rev":\s*"[^"]+"/g, `"_rev":""`)
|
||||
|
||||
// Update the iframe with the builder info to render the correct preview
|
||||
const refreshContent = message => {
|
||||
|
@ -87,7 +79,7 @@
|
|||
}
|
||||
|
||||
// Refresh the preview when required
|
||||
$: refreshContent(strippedJson)
|
||||
$: refreshContent(json)
|
||||
|
||||
function receiveMessage(message) {
|
||||
const handlers = {
|
||||
|
@ -102,7 +94,7 @@
|
|||
if (!$store.clientFeatures.intelligentLoading) {
|
||||
loading = false
|
||||
}
|
||||
refreshContent(strippedJson)
|
||||
refreshContent(json)
|
||||
},
|
||||
[MessageTypes.ERROR]: event => {
|
||||
// Catch any app errors
|
||||
|
|
|
@ -43,7 +43,8 @@
|
|||
"attachmentfield",
|
||||
"relationshipfield",
|
||||
"daterangepicker",
|
||||
"multifieldselect"
|
||||
"multifieldselect",
|
||||
"jsonfield"
|
||||
]
|
||||
},
|
||||
{
|
||||
|
@ -73,6 +74,7 @@
|
|||
"heading",
|
||||
"text",
|
||||
"button",
|
||||
"tag",
|
||||
"divider",
|
||||
"image",
|
||||
"backgroundimage",
|
||||
|
|
|
@ -49,6 +49,7 @@ export default `
|
|||
try {
|
||||
parsed = JSON.parse(event.data)
|
||||
} catch (error) {
|
||||
console.error("Client received invalid JSON")
|
||||
// Ignore
|
||||
}
|
||||
if (!parsed) {
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
import { get } from "svelte/store"
|
||||
import { store, currentAsset } from "builderStore"
|
||||
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"
|
||||
|
||||
export let component
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { writable, get } from "svelte/store"
|
||||
import { store as frontendStore } from "builderStore"
|
||||
import { findComponentPath } from "builderStore/storeUtils"
|
||||
import { findComponentPath } from "builderStore/componentUtils"
|
||||
|
||||
export const DropEffect = {
|
||||
MOVE: "move",
|
||||
|
|
|
@ -63,7 +63,14 @@
|
|||
// If no specific value is depended upon, check if a value exists at all
|
||||
// for the dependent setting
|
||||
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
|
||||
|
|
|
@ -9,8 +9,9 @@
|
|||
ActionMenu,
|
||||
MenuItem,
|
||||
} from "@budibase/bbui"
|
||||
import { getAvailableActions } from "./actions"
|
||||
import { getAvailableActions } from "./index"
|
||||
import { generate } from "shortid"
|
||||
import { getButtonContextBindings } from "builderStore/dataBinding"
|
||||
|
||||
const flipDurationMs = 150
|
||||
const EVENT_TYPE_KEY = "##eventHandlerType"
|
||||
|
@ -19,7 +20,16 @@
|
|||
export let actions
|
||||
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) {
|
||||
actions.forEach(action => {
|
||||
|
@ -30,8 +40,6 @@
|
|||
}
|
||||
}
|
||||
|
||||
let selectedAction = actions?.length ? actions[0] : null
|
||||
|
||||
$: selectedActionComponent =
|
||||
selectedAction &&
|
||||
actionTypes.find(t => t.name === selectedAction[EVENT_TYPE_KEY])?.component
|
||||
|
@ -122,7 +130,7 @@
|
|||
<svelte:component
|
||||
this={selectedActionComponent}
|
||||
parameters={selectedAction.parameters}
|
||||
{bindings}
|
||||
bindings={allBindings}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
|
@ -2,7 +2,7 @@
|
|||
import { ActionButton, Button, Drawer } from "@budibase/bbui"
|
||||
import { createEventDispatcher } from "svelte"
|
||||
import { notifications } from "@budibase/bbui"
|
||||
import EventEditor from "./EventEditor.svelte"
|
||||
import ButtonActionDrawer from "./ButtonActionDrawer.svelte"
|
||||
import { automationStore } from "builderStore"
|
||||
import { cloneDeep } from "lodash/fp"
|
||||
|
||||
|
@ -67,7 +67,7 @@
|
|||
Define what actions to run.
|
||||
</svelte:fragment>
|
||||
<Button cta slot="buttons" on:click={saveEventData}>Save</Button>
|
||||
<EventEditor
|
||||
<ButtonActionDrawer
|
||||
slot="body"
|
||||
bind:actions={tmpValue}
|
||||
eventType={name}
|
|
@ -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>
|
|
@ -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"
|
|
@ -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
|
||||
})
|
||||
}
|
|
@ -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"
|
||||
}
|
||||
]
|
||||
}
|
|
@ -16,7 +16,7 @@
|
|||
import { selectedComponent } from "builderStore"
|
||||
import { getComponentForSettingType } from "./componentSettings"
|
||||
import PropertyControl from "./PropertyControl.svelte"
|
||||
import { getComponentSettings } from "builderStore/storeUtils"
|
||||
import { getComponentSettings } from "builderStore/componentUtils"
|
||||
|
||||
export let conditions = []
|
||||
export let bindings = []
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
import { Select } from "@budibase/bbui"
|
||||
import { makePropSafe } from "@budibase/string-templates"
|
||||
import { currentAsset, store } from "builderStore"
|
||||
import { findComponentPath } from "builderStore/storeUtils"
|
||||
import { findComponentPath } from "builderStore/componentUtils"
|
||||
import { createEventDispatcher, onMount } from "svelte"
|
||||
|
||||
export let value
|
||||
|
|
|
@ -48,9 +48,7 @@
|
|||
return [...acc, ...viewsArr]
|
||||
}, [])
|
||||
$: queries = $queriesStore.list
|
||||
.filter(
|
||||
query => showAllQueries || query.queryVerb === "read" || query.readable
|
||||
)
|
||||
.filter(q => showAllQueries || q.queryVerb === "read" || q.readable)
|
||||
.map(query => ({
|
||||
label: query.name,
|
||||
name: query.name,
|
||||
|
@ -104,6 +102,22 @@
|
|||
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 => {
|
||||
dispatch("change", selected)
|
||||
|
@ -230,6 +244,17 @@
|
|||
{/each}
|
||||
</ul>
|
||||
{/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}
|
||||
<Divider size="S" />
|
||||
<div class="title">
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -1,2 +0,0 @@
|
|||
import EventsEditor from "./EventPropertyControl.svelte"
|
||||
export default EventsEditor
|
|
@ -21,7 +21,7 @@
|
|||
export let panel = ClientBindingPanel
|
||||
export let allowBindings = true
|
||||
|
||||
const BannedTypes = ["link", "attachment", "formula"]
|
||||
const BannedTypes = ["link", "attachment", "formula", "json", "jsonarray"]
|
||||
|
||||
$: fieldOptions = (schemaFields ?? [])
|
||||
.filter(field => !BannedTypes.includes(field.type))
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
getSchemaForDatasource,
|
||||
} from "builderStore/dataBinding"
|
||||
import { currentAsset } from "builderStore"
|
||||
import { findClosestMatchingComponent } from "builderStore/storeUtils"
|
||||
import { findClosestMatchingComponent } from "builderStore/componentUtils"
|
||||
|
||||
export let componentInstance
|
||||
export let value
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<script>
|
||||
import { ActionButton } from "@budibase/bbui"
|
||||
import { currentAsset, store } from "builderStore"
|
||||
import { findClosestMatchingComponent } from "builderStore/storeUtils"
|
||||
import { findClosestMatchingComponent } from "builderStore/componentUtils"
|
||||
import { makeDatasourceFormComponents } from "builderStore/store/screenTemplates/utils/commonComponents"
|
||||
import ConfirmDialog from "components/common/ConfirmDialog.svelte"
|
||||
|
||||
|
|
|
@ -11,7 +11,7 @@
|
|||
DatePicker,
|
||||
} from "@budibase/bbui"
|
||||
import { currentAsset, selectedComponent } from "builderStore"
|
||||
import { findClosestMatchingComponent } from "builderStore/storeUtils"
|
||||
import { findClosestMatchingComponent } from "builderStore/componentUtils"
|
||||
import { getSchemaForDatasource } from "builderStore/dataBinding"
|
||||
import DrawerBindableInput from "components/common/bindings/DrawerBindableInput.svelte"
|
||||
import { generate } from "shortid"
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { Checkbox, Select, Stepper } from "@budibase/bbui"
|
||||
import DataSourceSelect from "./DataSourceSelect.svelte"
|
||||
import DataProviderSelect from "./DataProviderSelect.svelte"
|
||||
import EventsEditor from "./EventsEditor"
|
||||
import ButtonActionEditor from "./ButtonActionEditor/ButtonActionEditor.svelte"
|
||||
import TableSelect from "./TableSelect.svelte"
|
||||
import ColorPicker from "./ColorPicker.svelte"
|
||||
import { IconSelect } from "./IconSelect"
|
||||
|
@ -24,7 +24,7 @@ const componentMap = {
|
|||
dataProvider: DataProviderSelect,
|
||||
boolean: Checkbox,
|
||||
number: Stepper,
|
||||
event: EventsEditor,
|
||||
event: ButtonActionEditor,
|
||||
table: TableSelect,
|
||||
color: ColorPicker,
|
||||
icon: IconSelect,
|
||||
|
@ -45,6 +45,7 @@ const componentMap = {
|
|||
"field/attachment": FormFieldSelect,
|
||||
"field/link": FormFieldSelect,
|
||||
"field/array": FormFieldSelect,
|
||||
"field/json": FormFieldSelect,
|
||||
// Some validation types are the same as others, so not all types are
|
||||
// explicitly listed here. e.g. options uses string validation
|
||||
"validation/string": ValidationEditor,
|
||||
|
|
|
@ -89,6 +89,14 @@ export const FIELDS = {
|
|||
presence: false,
|
||||
},
|
||||
},
|
||||
JSON: {
|
||||
name: "JSON",
|
||||
type: "json",
|
||||
constraints: {
|
||||
type: "object",
|
||||
presence: false,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
export const AUTO_COLUMN_SUB_TYPES = {
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import { NoEmptyFilterStrings } from "../constants/lucene"
|
||||
import { deepGet } from "@budibase/bbui"
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
export const luceneQuery = (docs, query) => {
|
||||
if (!docs || !Array.isArray(docs)) {
|
||||
return []
|
||||
}
|
||||
if (!query) {
|
||||
return docs
|
||||
}
|
||||
|
||||
// make query consistent first
|
||||
query = cleanupQuery(query)
|
||||
|
||||
|
@ -106,7 +111,9 @@ export const luceneQuery = (docs, query) => {
|
|||
const match = (type, failFn) => doc => {
|
||||
const filters = Object.entries(query[type] || {})
|
||||
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
|
||||
}
|
||||
}
|
||||
|
@ -114,38 +121,42 @@ export const luceneQuery = (docs, query) => {
|
|||
}
|
||||
|
||||
// Process a string match (fails if the value does not start with the string)
|
||||
const stringMatch = match("string", (key, value, doc) => {
|
||||
return !doc[key] || !doc[key].toLowerCase().startsWith(value?.toLowerCase())
|
||||
const stringMatch = match("string", (docValue, testValue) => {
|
||||
return (
|
||||
!docValue || !docValue?.toLowerCase().startsWith(testValue?.toLowerCase())
|
||||
)
|
||||
})
|
||||
|
||||
// Process a fuzzy match (treat the same as starts with when running locally)
|
||||
const fuzzyMatch = match("fuzzy", (key, value, doc) => {
|
||||
return !doc[key] || !doc[key].startsWith(value)
|
||||
const fuzzyMatch = match("fuzzy", (docValue, testValue) => {
|
||||
return (
|
||||
!docValue || !docValue?.toLowerCase().startsWith(testValue?.toLowerCase())
|
||||
)
|
||||
})
|
||||
|
||||
// Process a range match
|
||||
const rangeMatch = match("range", (key, value, doc) => {
|
||||
return !doc[key] || doc[key] < value.low || doc[key] > value.high
|
||||
const rangeMatch = match("range", (docValue, testValue) => {
|
||||
return !docValue || docValue < testValue.low || docValue > testValue.high
|
||||
})
|
||||
|
||||
// Process an equal match (fails if the value is different)
|
||||
const equalMatch = match("equal", (key, value, doc) => {
|
||||
return value != null && value !== "" && doc[key] !== value
|
||||
const equalMatch = match("equal", (docValue, testValue) => {
|
||||
return testValue != null && testValue !== "" && docValue !== testValue
|
||||
})
|
||||
|
||||
// Process a not-equal match (fails if the value is the same)
|
||||
const notEqualMatch = match("notEqual", (key, value, doc) => {
|
||||
return value != null && value !== "" && doc[key] === value
|
||||
const notEqualMatch = match("notEqual", (docValue, testValue) => {
|
||||
return testValue != null && testValue !== "" && docValue === testValue
|
||||
})
|
||||
|
||||
// Process an empty match (fails if the value is not empty)
|
||||
const emptyMatch = match("empty", (key, value, doc) => {
|
||||
return doc[key] != null && doc[key] !== ""
|
||||
const emptyMatch = match("empty", docValue => {
|
||||
return docValue != null && docValue !== ""
|
||||
})
|
||||
|
||||
// Process a not-empty match (fails is the value is empty)
|
||||
const notEmptyMatch = match("notEmpty", (key, value, doc) => {
|
||||
return doc[key] == null || doc[key] === ""
|
||||
const notEmptyMatch = match("notEmpty", docValue => {
|
||||
return docValue == null || docValue === ""
|
||||
})
|
||||
|
||||
// Match a document against all criteria
|
||||
|
|
|
@ -13,7 +13,7 @@
|
|||
import FrontendNavigatePane from "components/design/NavigationPanel/FrontendNavigatePane.svelte"
|
||||
import { goto, leftover, params } from "@roxi/routify"
|
||||
import { FrontendTypes } from "constants"
|
||||
import { findComponent, findComponentPath } from "builderStore/storeUtils"
|
||||
import { findComponent, findComponentPath } from "builderStore/componentUtils"
|
||||
import { get } from "svelte/store"
|
||||
import AppThemeSelect from "components/design/AppPreview/AppThemeSelect.svelte"
|
||||
import ThemeEditor from "components/design/AppPreview/ThemeEditor.svelte"
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@budibase/cli",
|
||||
"version": "1.0.23",
|
||||
"version": "1.0.27-alpha.1",
|
||||
"description": "Budibase CLI, for developers, self hosting and migrations.",
|
||||
"main": "src/index.js",
|
||||
"bin": {
|
||||
|
|
|
@ -500,9 +500,20 @@
|
|||
"defaultValue": "M"
|
||||
}
|
||||
],
|
||||
"context": {
|
||||
"context": [
|
||||
{
|
||||
"type": "schema"
|
||||
},
|
||||
{
|
||||
"type": "static",
|
||||
"values": [
|
||||
{
|
||||
"label": "Row Index",
|
||||
"key": "index"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"stackedlist": {
|
||||
"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": {
|
||||
"name": "Image",
|
||||
"description": "A basic component for displaying images",
|
||||
|
@ -1775,6 +1837,10 @@
|
|||
{
|
||||
"type": "static",
|
||||
"values": [
|
||||
{
|
||||
"label": "Value",
|
||||
"key": "__value"
|
||||
},
|
||||
{
|
||||
"label": "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": {
|
||||
"name": "Data Provider",
|
||||
"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",
|
||||
"suffix": "repeater"
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@budibase/client",
|
||||
"version": "1.0.23",
|
||||
"version": "1.0.27-alpha.1",
|
||||
"license": "MPL-2.0",
|
||||
"module": "dist/budibase-client.js",
|
||||
"main": "dist/budibase-client.js",
|
||||
|
@ -19,9 +19,9 @@
|
|||
"dev:builder": "rollup -cw"
|
||||
},
|
||||
"dependencies": {
|
||||
"@budibase/bbui": "^1.0.23",
|
||||
"@budibase/bbui": "^1.0.27-alpha.1",
|
||||
"@budibase/standard-components": "^0.9.139",
|
||||
"@budibase/string-templates": "^1.0.23",
|
||||
"@budibase/string-templates": "^1.0.27-alpha.1",
|
||||
"regexparam": "^1.3.0",
|
||||
"shortid": "^2.2.15",
|
||||
"svelte-spa-router": "^3.0.5"
|
||||
|
@ -35,6 +35,7 @@
|
|||
"@spectrum-css/divider": "^1.0.3",
|
||||
"@spectrum-css/link": "^3.1.3",
|
||||
"@spectrum-css/page": "^3.0.1",
|
||||
"@spectrum-css/tag": "^3.1.4",
|
||||
"@spectrum-css/typography": "^3.0.2",
|
||||
"@spectrum-css/vars": "^3.0.1",
|
||||
"apexcharts": "^3.22.1",
|
||||
|
|
|
@ -21,8 +21,8 @@
|
|||
{#if $component.empty}
|
||||
<Placeholder />
|
||||
{:else if rows.length > 0}
|
||||
{#each rows as row}
|
||||
<Provider data={row}>
|
||||
{#each rows as row, index}
|
||||
<Provider data={{ ...row, index }}>
|
||||
<slot />
|
||||
</Provider>
|
||||
{/each}
|
||||
|
|
|
@ -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>
|
|
@ -74,7 +74,7 @@
|
|||
enrichedFilter.push({
|
||||
field: column.name,
|
||||
operator: column.type === "string" ? "string" : "equal",
|
||||
type: "string",
|
||||
type: column.type === "string" ? "string" : "number",
|
||||
valueType: "Binding",
|
||||
value: `{{ [${formId}].[${column.name}] }}`,
|
||||
})
|
||||
|
|
|
@ -62,7 +62,7 @@
|
|||
enrichedFilter.push({
|
||||
field: column.name,
|
||||
operator: column.type === "string" ? "string" : "equal",
|
||||
type: "string",
|
||||
type: column.type === "string" ? "string" : "number",
|
||||
valueType: "Binding",
|
||||
value: `{{ ${safe(formId)}.${safe(column.name)} }}`,
|
||||
})
|
||||
|
|
|
@ -19,7 +19,7 @@
|
|||
export let schemaFields
|
||||
export let filters = []
|
||||
|
||||
const BannedTypes = ["link", "attachment", "formula"]
|
||||
const BannedTypes = ["link", "attachment", "formula", "json"]
|
||||
|
||||
$: fieldOptions = (schemaFields ?? [])
|
||||
.filter(field => !BannedTypes.includes(field.type))
|
||||
|
|
|
@ -3,6 +3,8 @@
|
|||
import { derived, get, writable } from "svelte/store"
|
||||
import { createValidatorFromConstraints } from "./validation"
|
||||
import { generateID } from "utils/helpers"
|
||||
import { deepGet, deepSet } from "@budibase/bbui"
|
||||
import { cloneDeep } from "lodash/fp"
|
||||
|
||||
export let dataSource
|
||||
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
|
||||
// extracted values from the field array
|
||||
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
|
||||
const getField = 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
|
||||
let initialValue = initialValues[field] ?? defaultValue
|
||||
let initialValue = deepGet(initialValues, field) ?? defaultValue
|
||||
let fieldId = `id-${generateID()}`
|
||||
const existingField = getField(field)
|
||||
if (existingField) {
|
||||
const { fieldState } = get(existingField)
|
||||
initialValue = fieldState.value ?? initialValue
|
||||
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
|
||||
|
@ -130,6 +182,7 @@
|
|||
disabled: disabled || fieldDisabled || isAutoColumn,
|
||||
defaultValue,
|
||||
validator,
|
||||
lastUpdate: Date.now(),
|
||||
},
|
||||
fieldApi: makeFieldApi(field, defaultValue),
|
||||
fieldSchema: schema?.[field] ?? {},
|
||||
|
@ -204,6 +257,7 @@
|
|||
fieldInfo.update(state => {
|
||||
state.fieldState.value = value
|
||||
state.fieldState.error = error
|
||||
state.fieldState.lastUpdate = Date.now()
|
||||
return state
|
||||
})
|
||||
|
||||
|
@ -220,6 +274,7 @@
|
|||
fieldInfo.update(state => {
|
||||
state.fieldState.value = newValue
|
||||
state.fieldState.error = null
|
||||
state.fieldState.lastUpdate = Date.now()
|
||||
return state
|
||||
})
|
||||
}
|
||||
|
@ -299,18 +354,6 @@
|
|||
{ type: ActionTypes.ClearForm, callback: formApi.clear },
|
||||
{ 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>
|
||||
|
||||
<Provider {actions} data={dataContext}>
|
||||
|
|
|
@ -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>
|
|
@ -1,6 +1,7 @@
|
|||
<script>
|
||||
import { CoreTextArea } from "@budibase/bbui"
|
||||
import Field from "./Field.svelte"
|
||||
import { getContext } from "svelte"
|
||||
|
||||
export let field
|
||||
export let label
|
||||
|
@ -11,6 +12,9 @@
|
|||
|
||||
let fieldState
|
||||
let fieldApi
|
||||
|
||||
const component = getContext("component")
|
||||
$: height = $component.styles?.normal?.height || "124px"
|
||||
</script>
|
||||
|
||||
<Field
|
||||
|
@ -24,6 +28,7 @@
|
|||
bind:fieldApi
|
||||
>
|
||||
{#if fieldState}
|
||||
<div style="--height: {height};">
|
||||
<CoreTextArea
|
||||
value={fieldState.value}
|
||||
on:change={e => fieldApi.setValue(e.detail)}
|
||||
|
@ -32,5 +37,17 @@
|
|||
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>
|
||||
|
|
|
@ -11,3 +11,4 @@ export { default as attachmentfield } from "./AttachmentField.svelte"
|
|||
export { default as relationshipfield } from "./RelationshipField.svelte"
|
||||
export { default as passwordfield } from "./PasswordField.svelte"
|
||||
export { default as formstep } from "./FormStep.svelte"
|
||||
export { default as jsonfield } from "./JSONField.svelte"
|
||||
|
|
|
@ -206,6 +206,7 @@ const parseType = (value, type) => {
|
|||
return value
|
||||
}
|
||||
|
||||
// Parse array, treating no elements as null
|
||||
if (type === FieldTypes.ARRAY) {
|
||||
if (!Array.isArray(value) || !value.length) {
|
||||
return null
|
||||
|
@ -213,6 +214,12 @@ const parseType = (value, type) => {
|
|||
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
|
||||
return null
|
||||
}
|
||||
|
@ -290,6 +297,19 @@ const notContainsHandler = (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.
|
||||
*/
|
||||
|
@ -306,6 +326,7 @@ const handlerMap = {
|
|||
notRegex: notRegexHandler,
|
||||
contains: containsHandler,
|
||||
notContains: notContainsHandler,
|
||||
json: jsonHandler,
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -29,6 +29,7 @@ export { default as backgroundimage } from "./BackgroundImage.svelte"
|
|||
export { default as daterangepicker } from "./DateRangePicker.svelte"
|
||||
export { default as cardstat } from "./CardStat.svelte"
|
||||
export { default as spectrumcard } from "./SpectrumCard.svelte"
|
||||
export { default as tag } from "./Tag.svelte"
|
||||
export * from "./charts"
|
||||
export * from "./forms"
|
||||
export * from "./table"
|
||||
|
|
|
@ -24,6 +24,7 @@
|
|||
class:flipped
|
||||
class:line
|
||||
style="top: {top}px; left: {left}px; width: {width}px; height: {height}px; --color: {color}; --zIndex: {zIndex};"
|
||||
class:withText={!!text}
|
||||
>
|
||||
{#if text}
|
||||
<div class="text" class:flipped class:line class:right={alignRight}>
|
||||
|
@ -39,12 +40,12 @@
|
|||
z-index: var(--zIndex);
|
||||
border: 2px solid var(--color);
|
||||
pointer-events: none;
|
||||
border-top-right-radius: 4px;
|
||||
border-top-left-radius: 0;
|
||||
border-bottom-left-radius: 4px;
|
||||
border-bottom-right-radius: 4px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
.indicator.flipped {
|
||||
.indicator.withText {
|
||||
border-top-left-radius: 0;
|
||||
}
|
||||
.indicator.withText.flipped {
|
||||
border-top-left-radius: 4px;
|
||||
}
|
||||
.indicator.line {
|
||||
|
@ -74,8 +75,7 @@
|
|||
border-radius: 4px;
|
||||
}
|
||||
.text.flipped {
|
||||
border-top-left-radius: 4px;
|
||||
border-bottom-left-radius: 4px;
|
||||
border-radius: 4px;
|
||||
transform: translateY(0%);
|
||||
top: -2px;
|
||||
}
|
||||
|
|
|
@ -13,6 +13,7 @@ export const FieldTypes = {
|
|||
ATTACHMENT: "attachment",
|
||||
LINK: "link",
|
||||
FORMULA: "formula",
|
||||
JSON: "json",
|
||||
}
|
||||
|
||||
export const UnsortableTypes = [
|
||||
|
|
|
@ -61,7 +61,8 @@ export const createDataSourceStore = () => {
|
|||
|
||||
// Emit this as a window event, so parent screens which are iframing us in
|
||||
// can also invalidate the same datasource
|
||||
if (get(routeStore).queryParams?.peek) {
|
||||
const inModal = get(routeStore).queryParams?.peek
|
||||
if (inModal) {
|
||||
window.parent.postMessage({
|
||||
type: "invalidate-datasource",
|
||||
detail: { dataSourceId },
|
||||
|
|
|
@ -8,20 +8,49 @@ import {
|
|||
} from "stores"
|
||||
import { saveRow, deleteRow, executeQuery, triggerAutomation } from "api"
|
||||
import { ActionTypes } from "constants"
|
||||
import { enrichDataBindings } from "./enrichDataBinding"
|
||||
import { deepSet } from "@budibase/bbui"
|
||||
|
||||
const saveRowHandler = async (action, context) => {
|
||||
const { fields, providerId, tableId } = action.parameters
|
||||
let payload
|
||||
if (providerId) {
|
||||
let draft = context[providerId]
|
||||
payload = { ...context[providerId] }
|
||||
} else {
|
||||
payload = {}
|
||||
}
|
||||
if (fields) {
|
||||
for (let [field, value] of Object.entries(fields)) {
|
||||
draft[field] = value
|
||||
deepSet(payload, field, value)
|
||||
}
|
||||
}
|
||||
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 { datasourceId, queryId, queryParams } = action.parameters
|
||||
await executeQuery({
|
||||
const result = await executeQuery({
|
||||
datasourceId,
|
||||
queryId,
|
||||
parameters: queryParams,
|
||||
})
|
||||
return { result }
|
||||
}
|
||||
|
||||
const executeActionHandler = async (
|
||||
|
@ -129,6 +159,7 @@ const updateStateHandler = action => {
|
|||
|
||||
const handlerMap = {
|
||||
["Save Row"]: saveRowHandler,
|
||||
["Duplicate Row"]: duplicateRowHandler,
|
||||
["Delete Row"]: deleteRowHandler,
|
||||
["Navigate To"]: navigationHandler,
|
||||
["Execute Query"]: queryExecutionHandler,
|
||||
|
@ -165,12 +196,27 @@ export const enrichButtonActions = (actions, context) => {
|
|||
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"]])
|
||||
return async () => {
|
||||
for (let i = 0; i < handlers.length; i++) {
|
||||
try {
|
||||
const action = actions[i]
|
||||
const callback = async () => handlers[i](action, context)
|
||||
// Skip any non-existent action definitions
|
||||
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
|
||||
// callback to execute further actions
|
||||
|
@ -185,7 +231,15 @@ export const enrichButtonActions = (actions, context) => {
|
|||
// then execute the rest of the actions in the chain
|
||||
const result = await callback()
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
@ -201,6 +255,8 @@ export const enrichButtonActions = (actions, context) => {
|
|||
const result = await callback()
|
||||
if (result === false) {
|
||||
return
|
||||
} else {
|
||||
buttonContext.push(result)
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
|
|
|
@ -32,35 +32,56 @@ export const enrichProps = (props, context) => {
|
|||
data: context[context.closestComponentId],
|
||||
}
|
||||
|
||||
// Enrich all data bindings in top level props
|
||||
let enrichedProps = enrichDataBindings(props, totalContext)
|
||||
|
||||
// Enrich click actions if they exist
|
||||
Object.keys(enrichedProps).forEach(prop => {
|
||||
// We want to exclude any button actions from enrichment at this stage.
|
||||
// Extract top level button action settings.
|
||||
let normalProps = { ...props }
|
||||
let actionProps = {}
|
||||
Object.keys(normalProps).forEach(prop => {
|
||||
if (prop?.toLowerCase().includes("onclick")) {
|
||||
enrichedProps[prop] = enrichButtonActions(
|
||||
enrichedProps[prop],
|
||||
totalContext
|
||||
)
|
||||
actionProps[prop] = normalProps[prop]
|
||||
delete normalProps[prop]
|
||||
}
|
||||
})
|
||||
|
||||
// Enrich any click actions in conditions
|
||||
if (enrichedProps._conditions) {
|
||||
enrichedProps._conditions.forEach(condition => {
|
||||
// Handle conditional UI separately after normal settings
|
||||
let conditions = normalProps._conditions
|
||||
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")) {
|
||||
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,
|
||||
totalContext
|
||||
)
|
||||
|
||||
// If there is an onclick function in here then it won't be serialised
|
||||
// properly, and therefore will not be updated properly.
|
||||
// The solution to this is add a rand which will ensure diffs happen
|
||||
// every time.
|
||||
condition.rand = Math.random()
|
||||
),
|
||||
rand: Math.random(),
|
||||
})
|
||||
} else {
|
||||
// Normal condition
|
||||
enrichedConditions.push(enrichDataBindings(condition, totalContext))
|
||||
}
|
||||
})
|
||||
enrichedProps._conditions = enrichedConditions
|
||||
}
|
||||
|
||||
return enrichedProps
|
||||
|
|
|
@ -1,3 +1,8 @@
|
|||
import {
|
||||
convertJSONSchemaToTableSchema,
|
||||
getJSONArrayDatasourceSchema,
|
||||
} from "builder/src/builderStore/jsonUtils"
|
||||
import { fetchTableDefinition } from "api"
|
||||
import TableFetch from "./fetch/TableFetch.js"
|
||||
import ViewFetch from "./fetch/ViewFetch.js"
|
||||
import QueryFetch from "./fetch/QueryFetch.js"
|
||||
|
@ -7,16 +12,17 @@ import QueryFetch from "./fetch/QueryFetch.js"
|
|||
*/
|
||||
export const fetchDatasourceSchema = async datasource => {
|
||||
const type = datasource?.type
|
||||
let schema
|
||||
|
||||
// Nested providers should already have exposed their own schema
|
||||
if (type === "provider") {
|
||||
return datasource.value?.schema
|
||||
schema = datasource.value?.schema
|
||||
}
|
||||
|
||||
// Field sources have their schema statically defined
|
||||
if (type === "field") {
|
||||
if (datasource.fieldType === "attachment") {
|
||||
return {
|
||||
schema = {
|
||||
url: {
|
||||
type: "string",
|
||||
},
|
||||
|
@ -25,7 +31,7 @@ export const fetchDatasourceSchema = async datasource => {
|
|||
},
|
||||
}
|
||||
} else if (datasource.fieldType === "array") {
|
||||
return {
|
||||
schema = {
|
||||
value: {
|
||||
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
|
||||
const handler = {
|
||||
table: TableFetch,
|
||||
|
@ -43,7 +56,27 @@ export const fetchDatasourceSchema = async datasource => {
|
|||
}[type]
|
||||
if (handler) {
|
||||
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
|
||||
|
|
|
@ -305,6 +305,11 @@
|
|||
resolved "https://registry.yarnpkg.com/@spectrum-css/tabs/-/tabs-3.1.5.tgz#cc82e69c1fc721902345178231fb95d05938b983"
|
||||
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":
|
||||
version "3.0.3"
|
||||
resolved "https://registry.yarnpkg.com/@spectrum-css/tags/-/tags-3.0.3.tgz#fc76d2735cdc442de91b7eb3bee49a928c0767ac"
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "@budibase/server",
|
||||
"email": "hi@budibase.com",
|
||||
"version": "1.0.23",
|
||||
"version": "1.0.27-alpha.1",
|
||||
"description": "Budibase Web Server",
|
||||
"main": "src/index.ts",
|
||||
"repository": {
|
||||
|
@ -69,9 +69,10 @@
|
|||
"author": "Budibase",
|
||||
"license": "GPL-3.0",
|
||||
"dependencies": {
|
||||
"@budibase/auth": "^1.0.23",
|
||||
"@budibase/client": "^1.0.23",
|
||||
"@budibase/string-templates": "^1.0.23",
|
||||
"@apidevtools/swagger-parser": "^10.0.3",
|
||||
"@budibase/auth": "^1.0.27-alpha.1",
|
||||
"@budibase/client": "^1.0.27-alpha.1",
|
||||
"@budibase/string-templates": "^1.0.27-alpha.1",
|
||||
"@bull-board/api": "^3.7.0",
|
||||
"@bull-board/koa": "^3.7.0",
|
||||
"@elastic/elasticsearch": "7.10.0",
|
||||
|
|
|
@ -50,10 +50,10 @@ exports.validate = async ({ appId, tableId, row, table }) => {
|
|||
const errors = {}
|
||||
for (let fieldName of Object.keys(table.schema)) {
|
||||
const constraints = cloneDeep(table.schema[fieldName].constraints)
|
||||
const type = table.schema[fieldName].type
|
||||
// special case for options, need to always allow unselected (null)
|
||||
if (
|
||||
table.schema[fieldName].type ===
|
||||
(FieldTypes.OPTIONS || FieldTypes.ARRAY) &&
|
||||
(type === FieldTypes.OPTIONS || type === FieldTypes.ARRAY) &&
|
||||
constraints.inclusion
|
||||
) {
|
||||
constraints.inclusion.push(null)
|
||||
|
@ -61,17 +61,20 @@ exports.validate = async ({ appId, tableId, row, table }) => {
|
|||
let res
|
||||
|
||||
// Validate.js doesn't seem to handle array
|
||||
if (
|
||||
table.schema[fieldName].type === FieldTypes.ARRAY &&
|
||||
row[fieldName] &&
|
||||
row[fieldName].length
|
||||
) {
|
||||
if (type === FieldTypes.ARRAY && row[fieldName] && row[fieldName].length) {
|
||||
row[fieldName].map(val => {
|
||||
if (!constraints.inclusion.includes(val)) {
|
||||
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(
|
||||
processStringSync(table.schema[fieldName].formula, row),
|
||||
constraints
|
||||
|
|
|
@ -81,6 +81,18 @@ const TYPE_TRANSFORM_MAP = {
|
|||
[FieldTypes.AUTO]: {
|
||||
parse: () => undefined,
|
||||
},
|
||||
[FieldTypes.JSON]: {
|
||||
parse: input => {
|
||||
try {
|
||||
if (input === "") {
|
||||
return undefined
|
||||
}
|
||||
return JSON.parse(input)
|
||||
} catch (err) {
|
||||
return input
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -27,7 +27,7 @@
|
|||
resolved "https://registry.yarnpkg.com/@apidevtools/swagger-methods/-/swagger-methods-3.0.2.tgz#b789a362e055b0340d04712eafe7027ddc1ac267"
|
||||
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"
|
||||
resolved "https://registry.yarnpkg.com/@apidevtools/swagger-parser/-/swagger-parser-10.0.3.tgz#32057ae99487872c4dd96b314a1ab4b95d89eaf5"
|
||||
integrity sha512-sNiLY51vZOmSPFZA5TF35KZ2HbgYklQnTSDnkghamzLb3EkNtcQnrBQEj5AOCxHpTtXpqMCRM1CrmV2rG6nw4g==
|
||||
|
@ -983,10 +983,10 @@
|
|||
resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39"
|
||||
integrity sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==
|
||||
|
||||
"@budibase/auth@^1.0.23":
|
||||
version "1.0.24"
|
||||
resolved "https://registry.yarnpkg.com/@budibase/auth/-/auth-1.0.24.tgz#69a015a7fe9dcc9ea9e471a7d76d2c0544de0b16"
|
||||
integrity sha512-pX+x+ib5X8s9ek4TZVmddRXlCE3kFmK5EH7Qk0Wmcn9tZwbaZy1XXd4qlJ7aFGSmvy1VNbz5AXIyRtliescaEQ==
|
||||
"@budibase/auth@^1.0.27-alpha.1":
|
||||
version "1.0.33"
|
||||
resolved "https://registry.yarnpkg.com/@budibase/auth/-/auth-1.0.33.tgz#bb0c527ebca852c001c7e163a9cdfce49984adab"
|
||||
integrity sha512-CJcBspSB6B4UwXenGfzCiBh4wWPq9ZrRZm+Dqo774Yw7dfa/hHisfFiRAuz0vHxZl6AStLUsTBNynrzt7bs7Mg==
|
||||
dependencies:
|
||||
"@techpass/passport-openidconnect" "^0.3.0"
|
||||
aws-sdk "^2.901.0"
|
||||
|
@ -1056,10 +1056,10 @@
|
|||
svelte-flatpickr "^3.2.3"
|
||||
svelte-portal "^1.0.0"
|
||||
|
||||
"@budibase/bbui@^1.0.24":
|
||||
version "1.0.24"
|
||||
resolved "https://registry.yarnpkg.com/@budibase/bbui/-/bbui-1.0.24.tgz#15a976b500c4103bc3e94ed665c6c557bb6271bf"
|
||||
integrity sha512-4zr+fSChOelHDqNNM2OiWWbWOL57JGBVBx333GiMr2P7N0MoepjwHEp9MlITWl5mUfS8rxgWTC9mSSMpRdpSUQ==
|
||||
"@budibase/bbui@^1.0.33":
|
||||
version "1.0.33"
|
||||
resolved "https://registry.yarnpkg.com/@budibase/bbui/-/bbui-1.0.33.tgz#e815c4883dbc9d28d31900160fc74aafc81d02c1"
|
||||
integrity sha512-2W3Ub8J8brSuhwtXhznszTa54cTyUdDNMFDCnRrHseaDV05XrSykdndXYyWi4XC+FveV82AdvSICxgd6alRGzg==
|
||||
dependencies:
|
||||
"@adobe/spectrum-css-workflow-icons" "^1.2.1"
|
||||
"@spectrum-css/actionbutton" "^1.0.1"
|
||||
|
@ -1106,14 +1106,14 @@
|
|||
svelte-flatpickr "^3.2.3"
|
||||
svelte-portal "^1.0.0"
|
||||
|
||||
"@budibase/client@^1.0.23":
|
||||
version "1.0.24"
|
||||
resolved "https://registry.yarnpkg.com/@budibase/client/-/client-1.0.24.tgz#69be5b66cc701c6018c816728975a076230c052a"
|
||||
integrity sha512-ppb+exifHCfuDGqs1cQ0kzc3wWaOGkVgq/8Rdb7R7BLIOKYkB67L3FvkqTl+39wrc32CpmzOXXQ73keE4l6P8Q==
|
||||
"@budibase/client@^1.0.27-alpha.1":
|
||||
version "1.0.33"
|
||||
resolved "https://registry.yarnpkg.com/@budibase/client/-/client-1.0.33.tgz#715aaaf15f13d9e40960fffea44002e817d8a569"
|
||||
integrity sha512-4+xM/ZTI247JbJc1noQS58VFh8AqfuQK1ZUSZIowuol1TtDu5dLGN7BysUFUe0mJZoDB/L+cB1I//43XqaSWDw==
|
||||
dependencies:
|
||||
"@budibase/bbui" "^1.0.24"
|
||||
"@budibase/bbui" "^1.0.33"
|
||||
"@budibase/standard-components" "^0.9.139"
|
||||
"@budibase/string-templates" "^1.0.24"
|
||||
"@budibase/string-templates" "^1.0.33"
|
||||
regexparam "^1.3.0"
|
||||
shortid "^2.2.15"
|
||||
svelte-spa-router "^3.0.5"
|
||||
|
@ -1163,10 +1163,10 @@
|
|||
svelte-apexcharts "^1.0.2"
|
||||
svelte-flatpickr "^3.1.0"
|
||||
|
||||
"@budibase/string-templates@^1.0.23", "@budibase/string-templates@^1.0.24":
|
||||
version "1.0.24"
|
||||
resolved "https://registry.yarnpkg.com/@budibase/string-templates/-/string-templates-1.0.24.tgz#8f317ff18159d8f8e36179de33252e019c85c267"
|
||||
integrity sha512-KAwyYxtm3Mrsm44MhchSxOtB8ucZ2JI93VPBQn0hjdzHtMuAkadtFNPZFeD3pKpCjDmI13XuW+BmJqCF3B0tYg==
|
||||
"@budibase/string-templates@^1.0.27-alpha.1", "@budibase/string-templates@^1.0.33":
|
||||
version "1.0.33"
|
||||
resolved "https://registry.yarnpkg.com/@budibase/string-templates/-/string-templates-1.0.33.tgz#7c39f0821d86ca1b26e8d5e7eca4256f24b09de0"
|
||||
integrity sha512-uvDJvVtAaBcMKnVylWlzGk+CzPXH+TIKojGAfgjCqfcBsc9GlyCCBLtANe8osi9Bdr1SsKOW9Fi5u9FTToNMJw==
|
||||
dependencies:
|
||||
"@budibase/handlebars-helpers" "^0.11.7"
|
||||
dayjs "^1.10.4"
|
||||
|
@ -3304,9 +3304,9 @@ aws-sdk@^2.767.0:
|
|||
xml2js "0.4.19"
|
||||
|
||||
aws-sdk@^2.901.0:
|
||||
version "2.1046.0"
|
||||
resolved "https://registry.yarnpkg.com/aws-sdk/-/aws-sdk-2.1046.0.tgz#9147b0fa1c86acbebd1a061e951ab5012f4499d7"
|
||||
integrity sha512-ocwHclMXdIA+NWocUyvp9Ild3/zy2vr5mHp3mTyodf0WU5lzBE8PocCVLSWhMAXLxyia83xv2y5f5AzAcetbqA==
|
||||
version "2.1049.0"
|
||||
resolved "https://registry.yarnpkg.com/aws-sdk/-/aws-sdk-2.1049.0.tgz#8146dcdf3a1ab603e50ff961169ee8abc537d48e"
|
||||
integrity sha512-+wls9iNlotMeoZepwgR0yPzXsjXzr2ijoi5ERmsPWfMTFMHkm6INndBtSkm6fpu/NZnl+7EaPPES2yhaqnhoJg==
|
||||
dependencies:
|
||||
buffer "4.9.2"
|
||||
events "1.1.1"
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@budibase/string-templates",
|
||||
"version": "1.0.23",
|
||||
"version": "1.0.27-alpha.1",
|
||||
"description": "Handlebars wrapper for Budibase templating.",
|
||||
"main": "src/index.cjs",
|
||||
"module": "dist/bundle.mjs",
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
const { atob } = require("../utilities")
|
||||
const { cloneDeep } = require("lodash/fp")
|
||||
|
||||
// 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).
|
||||
|
@ -38,8 +39,12 @@ module.exports.processJS = (handlebars, context) => {
|
|||
// This is required to allow the final `return` statement to be valid.
|
||||
const js = `function run(){${atob(handlebars)}};run();`
|
||||
|
||||
// Our $ context function gets a value from context
|
||||
const sandboxContext = { $: path => getContextValue(path, context) }
|
||||
// Our $ context function gets a value from 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
|
||||
return runJS(js, sandboxContext)
|
||||
|
|
|
@ -1,12 +1,13 @@
|
|||
const handlebars = require("handlebars")
|
||||
const { registerAll } = require("./helpers/index")
|
||||
const processors = require("./processors")
|
||||
const { removeHandlebarsStatements, atob, btoa } = require("./utilities")
|
||||
const { atob, btoa } = require("./utilities")
|
||||
const manifest = require("../manifest.json")
|
||||
|
||||
const hbsInstance = handlebars.create()
|
||||
registerAll(hbsInstance)
|
||||
const hbsInstanceNoHelpers = handlebars.create()
|
||||
const defaultOpts = { noHelpers: false }
|
||||
|
||||
/**
|
||||
* 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.
|
||||
* @returns {Promise<object|array>} The structure input, as fully updated as possible.
|
||||
*/
|
||||
module.exports.processObject = async (
|
||||
object,
|
||||
context,
|
||||
opts = { noHelpers: false }
|
||||
) => {
|
||||
module.exports.processObject = async (object, context, opts) => {
|
||||
testObject(object)
|
||||
for (let key of Object.keys(object || {})) {
|
||||
if (object[key] != null) {
|
||||
|
@ -63,11 +60,7 @@ module.exports.processObject = async (
|
|||
* @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.
|
||||
*/
|
||||
module.exports.processString = async (
|
||||
string,
|
||||
context,
|
||||
opts = { noHelpers: false }
|
||||
) => {
|
||||
module.exports.processString = async (string, context, opts) => {
|
||||
// TODO: carry out any async calls before carrying out async call
|
||||
return module.exports.processStringSync(string, context, opts)
|
||||
}
|
||||
|
@ -81,11 +74,7 @@ module.exports.processString = async (
|
|||
* @param {object} opts optional - specify some options for processing.
|
||||
* @returns {object|array} The structure input, as fully updated as possible.
|
||||
*/
|
||||
module.exports.processObjectSync = (
|
||||
object,
|
||||
context,
|
||||
opts = { noHelpers: false }
|
||||
) => {
|
||||
module.exports.processObjectSync = (object, context, opts) => {
|
||||
testObject(object)
|
||||
for (let key of Object.keys(object || {})) {
|
||||
let val = object[key]
|
||||
|
@ -106,26 +95,20 @@ module.exports.processObjectSync = (
|
|||
* @param {object} opts optional - specify some options for processing.
|
||||
* @returns {string} The enriched string, all templates should have been replaced if they can be.
|
||||
*/
|
||||
module.exports.processStringSync = (
|
||||
string,
|
||||
context,
|
||||
opts = { noHelpers: false }
|
||||
) => {
|
||||
if (!exports.isValid(string)) {
|
||||
return string
|
||||
}
|
||||
// take a copy of input incase error
|
||||
module.exports.processStringSync = (string, context, opts) => {
|
||||
opts = { ...defaultOpts, ...opts }
|
||||
|
||||
// take a copy of input in case of error
|
||||
const input = string
|
||||
if (typeof string !== "string") {
|
||||
throw "Cannot process non-string types."
|
||||
}
|
||||
try {
|
||||
const noHelpers = opts && opts.noHelpers
|
||||
// finalising adds a helper, can't do this with no helpers
|
||||
const shouldFinalise = !noHelpers
|
||||
const shouldFinalise = !opts.noHelpers
|
||||
string = processors.preprocess(string, shouldFinalise)
|
||||
// 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, {
|
||||
strict: false,
|
||||
})
|
||||
|
@ -136,7 +119,7 @@ module.exports.processStringSync = (
|
|||
})
|
||||
)
|
||||
} catch (err) {
|
||||
return removeHandlebarsStatements(input)
|
||||
return input
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -155,7 +138,8 @@ module.exports.makePropSafe = property => {
|
|||
* @param opts optional - specify some options for processing.
|
||||
* @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 = [
|
||||
"string",
|
||||
"number",
|
||||
|
@ -169,7 +153,7 @@ module.exports.isValid = (string, opts = { noHelpers: false }) => {
|
|||
// don't really need a real context to check if its valid
|
||||
const context = {}
|
||||
try {
|
||||
const instance = opts && opts.noHelpers ? hbsInstanceNoHelpers : hbsInstance
|
||||
const instance = opts.noHelpers ? hbsInstanceNoHelpers : hbsInstance
|
||||
instance.compile(processors.preprocess(string, false))(context)
|
||||
return true
|
||||
} catch (err) {
|
||||
|
|
|
@ -10,7 +10,10 @@ module.exports.swapStrings = (string, start, length, swap) => {
|
|||
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 matches = string.match(regexp)
|
||||
if (matches == null) {
|
||||
|
@ -18,7 +21,7 @@ module.exports.removeHandlebarsStatements = string => {
|
|||
}
|
||||
for (let match of matches) {
|
||||
const idx = string.indexOf(match)
|
||||
string = exports.swapStrings(string, idx, match.length, "Invalid Binding")
|
||||
string = exports.swapStrings(string, idx, match.length, replacement)
|
||||
}
|
||||
return string
|
||||
}
|
||||
|
|
|
@ -13,10 +13,14 @@ describe("test the custom helpers we have applied", () => {
|
|||
|
||||
describe("test that it can run without helpers", () => {
|
||||
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 }}", {})
|
||||
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 () => {
|
||||
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(),
|
||||
})
|
||||
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)
|
||||
})
|
||||
|
||||
it("should guess the users timezone when not specified", async () => {
|
||||
const date = new Date()
|
||||
const output = await processString("{{ date time 'Z' }}", {
|
||||
time: date.toUTCString()
|
||||
time: date.toUTCString(),
|
||||
})
|
||||
const timezone = dayjs.tz.guess()
|
||||
const offset = new dayjs(date).tz(timezone).format("Z")
|
||||
|
@ -307,12 +316,12 @@ describe("test the comparison helpers", () => {
|
|||
describe("Test the object/array helper", () => {
|
||||
it("should allow plucking from an array of objects", async () => {
|
||||
const context = {
|
||||
items: [
|
||||
{ price: 20 },
|
||||
{ price: 30 },
|
||||
]
|
||||
items: [{ 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)
|
||||
})
|
||||
|
||||
|
@ -450,7 +459,7 @@ describe("Cover a few complex use cases", () => {
|
|||
rowCount: 8,
|
||||
}
|
||||
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 () => {
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "@budibase/worker",
|
||||
"email": "hi@budibase.com",
|
||||
"version": "1.0.23",
|
||||
"version": "1.0.27-alpha.1",
|
||||
"description": "Budibase background service",
|
||||
"main": "src/index.js",
|
||||
"repository": {
|
||||
|
@ -29,8 +29,8 @@
|
|||
"author": "Budibase",
|
||||
"license": "GPL-3.0",
|
||||
"dependencies": {
|
||||
"@budibase/auth": "^1.0.23",
|
||||
"@budibase/string-templates": "^1.0.23",
|
||||
"@budibase/auth": "^1.0.27-alpha.1",
|
||||
"@budibase/string-templates": "^1.0.27-alpha.1",
|
||||
"@koa/router": "^8.0.0",
|
||||
"@sentry/node": "^6.0.0",
|
||||
"@techpass/passport-openidconnect": "^0.3.0",
|
||||
|
|
|
@ -8,7 +8,7 @@ jest.mock("nodemailer")
|
|||
const nodemailer = require("nodemailer")
|
||||
nodemailer.createTransport.mockReturnValue({
|
||||
sendMail: sendMailMock,
|
||||
verify: jest.fn()
|
||||
verify: jest.fn(),
|
||||
})
|
||||
|
||||
describe("/api/global/email", () => {
|
||||
|
@ -39,6 +39,6 @@ describe("/api/global/email", () => {
|
|||
expect(sendMailMock).toHaveBeenCalled()
|
||||
const emailCall = sendMailMock.mock.calls[0][0]
|
||||
expect(emailCall.subject).toBe("Hello!")
|
||||
expect(emailCall.html).not.toContain("Invalid Binding")
|
||||
expect(emailCall.html).not.toContain("Invalid binding")
|
||||
})
|
||||
})
|
Loading…
Reference in New Issue