Merge branch 'develop' of github.com:Budibase/budibase into feature/query-variables
This commit is contained in:
commit
18ff7a9cfb
|
@ -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.27-alpha.0",
|
"version": "1.0.27-alpha.2",
|
||||||
"npmClient": "yarn",
|
"npmClient": "yarn",
|
||||||
"packages": [
|
"packages": [
|
||||||
"packages/*"
|
"packages/*"
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@budibase/auth",
|
"name": "@budibase/auth",
|
||||||
"version": "1.0.27-alpha.0",
|
"version": "1.0.27-alpha.2",
|
||||||
"description": "Authentication middlewares for budibase builder and apps",
|
"description": "Authentication middlewares for budibase builder and apps",
|
||||||
"main": "src/index.js",
|
"main": "src/index.js",
|
||||||
"author": "Budibase",
|
"author": "Budibase",
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"name": "@budibase/bbui",
|
"name": "@budibase/bbui",
|
||||||
"description": "A UI solution used in the different Budibase projects.",
|
"description": "A UI solution used in the different Budibase projects.",
|
||||||
"version": "1.0.27-alpha.0",
|
"version": "1.0.27-alpha.2",
|
||||||
"license": "MPL-2.0",
|
"license": "MPL-2.0",
|
||||||
"svelte": "src/index.js",
|
"svelte": "src/index.js",
|
||||||
"module": "dist/bbui.es.js",
|
"module": "dist/bbui.es.js",
|
||||||
|
|
|
@ -21,16 +21,8 @@
|
||||||
}
|
}
|
||||||
visible = false
|
visible = false
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleKey(e) {
|
|
||||||
if (visible && e.key === "Escape") {
|
|
||||||
hide()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svelte:window on:keydown={handleKey} />
|
|
||||||
|
|
||||||
{#if visible}
|
{#if visible}
|
||||||
<Portal>
|
<Portal>
|
||||||
<section class:fillWidth class="drawer" transition:slide>
|
<section class:fillWidth class="drawer" transition:slide>
|
||||||
|
|
|
@ -4,6 +4,7 @@
|
||||||
import CellRenderer from "./CellRenderer.svelte"
|
import CellRenderer from "./CellRenderer.svelte"
|
||||||
import SelectEditRenderer from "./SelectEditRenderer.svelte"
|
import SelectEditRenderer from "./SelectEditRenderer.svelte"
|
||||||
import { cloneDeep } from "lodash"
|
import { cloneDeep } from "lodash"
|
||||||
|
import { deepGet } from "../utils/helpers"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The expected schema is our normal couch schemas for our tables.
|
* The expected schema is our normal couch schemas for our tables.
|
||||||
|
@ -318,7 +319,7 @@
|
||||||
{customRenderers}
|
{customRenderers}
|
||||||
{row}
|
{row}
|
||||||
schema={schema[field]}
|
schema={schema[field]}
|
||||||
value={row[field]}
|
value={deepGet(row, field)}
|
||||||
on:clickrelationship
|
on:clickrelationship
|
||||||
>
|
>
|
||||||
<slot />
|
<slot />
|
||||||
|
|
|
@ -81,3 +81,6 @@ export { default as clickOutside } from "./Actions/click_outside"
|
||||||
|
|
||||||
// Stores
|
// Stores
|
||||||
export { notifications, createNotificationStore } from "./Stores/notifications"
|
export { notifications, createNotificationStore } from "./Stores/notifications"
|
||||||
|
|
||||||
|
// Utils
|
||||||
|
export * from "./utils/helpers"
|
||||||
|
|
|
@ -6,3 +6,61 @@ export const generateID = () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
export const capitalise = s => s.substring(0, 1).toUpperCase() + s.substring(1)
|
export const capitalise = s => s.substring(0, 1).toUpperCase() + s.substring(1)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets a key within an object. The key supports dot syntax for retrieving deep
|
||||||
|
* fields - e.g. "a.b.c".
|
||||||
|
* Exact matches of keys with dots in them take precedence over nested keys of
|
||||||
|
* the same path - e.g. getting "a.b" from { "a.b": "foo", a: { b: "bar" } }
|
||||||
|
* will return "foo" over "bar".
|
||||||
|
* @param obj the object
|
||||||
|
* @param key the key
|
||||||
|
* @return {*|null} the value or null if a value was not found for this key
|
||||||
|
*/
|
||||||
|
export const deepGet = (obj, key) => {
|
||||||
|
if (!obj || !key) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
if (Object.prototype.hasOwnProperty.call(obj, key)) {
|
||||||
|
return obj[key]
|
||||||
|
}
|
||||||
|
const split = key.split(".")
|
||||||
|
for (let i = 0; i < split.length; i++) {
|
||||||
|
obj = obj?.[split[i]]
|
||||||
|
}
|
||||||
|
return obj
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets a key within an object. The key supports dot syntax for retrieving deep
|
||||||
|
* fields - e.g. "a.b.c".
|
||||||
|
* Exact matches of keys with dots in them take precedence over nested keys of
|
||||||
|
* the same path - e.g. setting "a.b" of { "a.b": "foo", a: { b: "bar" } }
|
||||||
|
* will override the value "foo" rather than "bar".
|
||||||
|
* If a deep path is specified and the parent keys don't exist then these will
|
||||||
|
* be created.
|
||||||
|
* @param obj the object
|
||||||
|
* @param key the key
|
||||||
|
* @param value the value
|
||||||
|
*/
|
||||||
|
export const deepSet = (obj, key, value) => {
|
||||||
|
if (!obj || !key) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (Object.prototype.hasOwnProperty.call(obj, key)) {
|
||||||
|
obj[key] = value
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const split = key.split(".")
|
||||||
|
for (let i = 0; i < split.length - 1; i++) {
|
||||||
|
const nextKey = split[i]
|
||||||
|
if (obj && obj[nextKey] == null) {
|
||||||
|
obj[nextKey] = {}
|
||||||
|
}
|
||||||
|
obj = obj?.[nextKey]
|
||||||
|
}
|
||||||
|
if (!obj) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
obj[split[split.length - 1]] = value
|
||||||
|
}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@budibase/builder",
|
"name": "@budibase/builder",
|
||||||
"version": "1.0.27-alpha.0",
|
"version": "1.0.27-alpha.2",
|
||||||
"license": "GPL-3.0",
|
"license": "GPL-3.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
@ -65,10 +65,10 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@budibase/bbui": "^1.0.27-alpha.0",
|
"@budibase/bbui": "^1.0.27-alpha.2",
|
||||||
"@budibase/client": "^1.0.27-alpha.0",
|
"@budibase/client": "^1.0.27-alpha.2",
|
||||||
"@budibase/colorpicker": "1.1.2",
|
"@budibase/colorpicker": "1.1.2",
|
||||||
"@budibase/string-templates": "^1.0.27-alpha.0",
|
"@budibase/string-templates": "^1.0.27-alpha.2",
|
||||||
"@sentry/browser": "5.19.1",
|
"@sentry/browser": "5.19.1",
|
||||||
"@spectrum-css/page": "^3.0.1",
|
"@spectrum-css/page": "^3.0.1",
|
||||||
"@spectrum-css/vars": "^3.0.1",
|
"@spectrum-css/vars": "^3.0.1",
|
||||||
|
|
|
@ -127,18 +127,37 @@ const searchComponentTree = (rootComponent, matchComponent) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Searches a component's definition for a setting matching a certin predicate.
|
* Searches a component's definition for a setting matching a certain predicate.
|
||||||
|
* These settings are cached because they cannot change at run time.
|
||||||
*/
|
*/
|
||||||
|
let componentSettingCache = {}
|
||||||
export const getComponentSettings = componentType => {
|
export const getComponentSettings = componentType => {
|
||||||
const def = store.actions.components.getDefinition(componentType)
|
if (!componentType) {
|
||||||
if (!def) {
|
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
let settings = def.settings?.filter(setting => !setting.section) ?? []
|
|
||||||
|
// Ensure whole component name is used
|
||||||
|
if (!componentType.startsWith("@budibase")) {
|
||||||
|
componentType = `@budibase/standard-components/${componentType}`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if we have cached this type already
|
||||||
|
if (componentSettingCache[componentType]) {
|
||||||
|
return componentSettingCache[componentType]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise get the settings and cache them
|
||||||
|
const def = store.actions.components.getDefinition(componentType)
|
||||||
|
let settings = []
|
||||||
|
if (def) {
|
||||||
|
settings = def.settings?.filter(setting => !setting.section) ?? []
|
||||||
def.settings
|
def.settings
|
||||||
?.filter(setting => setting.section)
|
?.filter(setting => setting.section)
|
||||||
.forEach(section => {
|
.forEach(section => {
|
||||||
settings = settings.concat(section.settings || [])
|
settings = settings.concat(section.settings || [])
|
||||||
})
|
})
|
||||||
|
}
|
||||||
|
componentSettingCache[componentType] = settings
|
||||||
|
|
||||||
return settings
|
return settings
|
||||||
}
|
}
|
|
@ -5,7 +5,7 @@ import {
|
||||||
findComponent,
|
findComponent,
|
||||||
findComponentPath,
|
findComponentPath,
|
||||||
getComponentSettings,
|
getComponentSettings,
|
||||||
} from "./storeUtils"
|
} from "./componentUtils"
|
||||||
import { store } from "builderStore"
|
import { store } from "builderStore"
|
||||||
import { queries as queriesStores, tables as tablesStore } from "stores/backend"
|
import { queries as queriesStores, tables as tablesStore } from "stores/backend"
|
||||||
import {
|
import {
|
||||||
|
@ -15,6 +15,11 @@ import {
|
||||||
encodeJSBinding,
|
encodeJSBinding,
|
||||||
} from "@budibase/string-templates"
|
} from "@budibase/string-templates"
|
||||||
import { TableNames } from "../constants"
|
import { TableNames } from "../constants"
|
||||||
|
import {
|
||||||
|
convertJSONSchemaToTableSchema,
|
||||||
|
getJSONArrayDatasourceSchema,
|
||||||
|
} from "./jsonUtils"
|
||||||
|
import ActionDefinitions from "components/design/PropertiesPanel/PropertyControls/ButtonActionEditor/manifest.json"
|
||||||
|
|
||||||
// Regex to match all instances of template strings
|
// Regex to match all instances of template strings
|
||||||
const CAPTURE_VAR_INSIDE_TEMPLATE = /{{([^}]+)}}/g
|
const CAPTURE_VAR_INSIDE_TEMPLATE = /{{([^}]+)}}/g
|
||||||
|
@ -186,6 +191,7 @@ const getProviderContextBindings = (asset, dataProviders) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
let schema
|
let schema
|
||||||
|
let table
|
||||||
let readablePrefix
|
let readablePrefix
|
||||||
let runtimeSuffix = context.suffix
|
let runtimeSuffix = context.suffix
|
||||||
|
|
||||||
|
@ -209,8 +215,17 @@ const getProviderContextBindings = (asset, dataProviders) => {
|
||||||
}
|
}
|
||||||
const info = getSchemaForDatasource(asset, datasource)
|
const info = getSchemaForDatasource(asset, datasource)
|
||||||
schema = info.schema
|
schema = info.schema
|
||||||
|
table = info.table
|
||||||
|
|
||||||
|
// For JSON arrays, use the array name as the readable prefix.
|
||||||
|
// Otherwise use the table name
|
||||||
|
if (datasource.type === "jsonarray") {
|
||||||
|
const split = datasource.label.split(".")
|
||||||
|
readablePrefix = split[split.length - 1]
|
||||||
|
} else {
|
||||||
readablePrefix = info.table?.name
|
readablePrefix = info.table?.name
|
||||||
}
|
}
|
||||||
|
}
|
||||||
if (!schema) {
|
if (!schema) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -229,7 +244,8 @@ const getProviderContextBindings = (asset, dataProviders) => {
|
||||||
const fieldSchema = schema[key]
|
const fieldSchema = schema[key]
|
||||||
|
|
||||||
// Make safe runtime binding
|
// Make safe runtime binding
|
||||||
const runtimeBinding = `${safeComponentId}.${makePropSafe(key)}`
|
const safeKey = key.split(".").map(makePropSafe).join(".")
|
||||||
|
const runtimeBinding = `${safeComponentId}.${safeKey}`
|
||||||
|
|
||||||
// Optionally use a prefix with readable bindings
|
// Optionally use a prefix with readable bindings
|
||||||
let readableBinding = component._instanceName
|
let readableBinding = component._instanceName
|
||||||
|
@ -247,6 +263,8 @@ const getProviderContextBindings = (asset, dataProviders) => {
|
||||||
// datasource options, based on bindable properties
|
// datasource options, based on bindable properties
|
||||||
fieldSchema,
|
fieldSchema,
|
||||||
providerId,
|
providerId,
|
||||||
|
// Table ID is used by JSON fields to know what table the field is in
|
||||||
|
tableId: table?._id,
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
@ -339,6 +357,36 @@ const getUrlBindings = asset => {
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets all bindable properties exposed in a button actions flow up until
|
||||||
|
* the specified action ID.
|
||||||
|
*/
|
||||||
|
export const getButtonContextBindings = (actions, actionId) => {
|
||||||
|
// Get the steps leading up to this value
|
||||||
|
const index = actions?.findIndex(action => action.id === actionId)
|
||||||
|
if (index == null || index === -1) {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
const prevActions = actions.slice(0, index)
|
||||||
|
|
||||||
|
// Generate bindings for any steps which provide context
|
||||||
|
let bindings = []
|
||||||
|
prevActions.forEach((action, idx) => {
|
||||||
|
const def = ActionDefinitions.actions.find(
|
||||||
|
x => x.name === action["##eventHandlerType"]
|
||||||
|
)
|
||||||
|
if (def.context) {
|
||||||
|
def.context.forEach(contextValue => {
|
||||||
|
bindings.push({
|
||||||
|
readableBinding: `Action ${idx + 1}.${contextValue.label}`,
|
||||||
|
runtimeBinding: `actions.${idx}.${contextValue.value}`,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return bindings
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets a schema for a datasource object.
|
* Gets a schema for a datasource object.
|
||||||
*/
|
*/
|
||||||
|
@ -347,16 +395,26 @@ export const getSchemaForDatasource = (asset, datasource, isForm = false) => {
|
||||||
|
|
||||||
if (datasource) {
|
if (datasource) {
|
||||||
const { type } = datasource
|
const { type } = datasource
|
||||||
|
const tables = get(tablesStore).list
|
||||||
|
|
||||||
// Determine the source table from the datasource type
|
// Determine the entity which backs this datasource.
|
||||||
|
// "provider" datasources are those targeting another data provider
|
||||||
if (type === "provider") {
|
if (type === "provider") {
|
||||||
const component = findComponent(asset.props, datasource.providerId)
|
const component = findComponent(asset.props, datasource.providerId)
|
||||||
const source = getDatasourceForProvider(asset, component)
|
const source = getDatasourceForProvider(asset, component)
|
||||||
return getSchemaForDatasource(asset, source, isForm)
|
return getSchemaForDatasource(asset, source, isForm)
|
||||||
} else if (type === "query") {
|
}
|
||||||
|
|
||||||
|
// "query" datasources are those targeting non-plus datasources or
|
||||||
|
// custom queries
|
||||||
|
else if (type === "query") {
|
||||||
const queries = get(queriesStores).list
|
const queries = get(queriesStores).list
|
||||||
table = queries.find(query => query._id === datasource._id)
|
table = queries.find(query => query._id === datasource._id)
|
||||||
} else if (type === "field") {
|
}
|
||||||
|
|
||||||
|
// "field" datasources are array-like fields of rows, such as attachments
|
||||||
|
// or multi-select fields
|
||||||
|
else if (type === "field") {
|
||||||
table = { name: datasource.fieldName }
|
table = { name: datasource.fieldName }
|
||||||
const { fieldType } = datasource
|
const { fieldType } = datasource
|
||||||
if (fieldType === "attachment") {
|
if (fieldType === "attachment") {
|
||||||
|
@ -375,12 +433,22 @@ export const getSchemaForDatasource = (asset, datasource, isForm = false) => {
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
}
|
||||||
const tables = get(tablesStore).list
|
|
||||||
|
// "jsonarray" datasources are arrays inside JSON fields
|
||||||
|
else if (type === "jsonarray") {
|
||||||
|
table = tables.find(table => table._id === datasource.tableId)
|
||||||
|
let tableSchema = table?.schema
|
||||||
|
schema = getJSONArrayDatasourceSchema(tableSchema, datasource)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise we assume we're targeting an internal table or a plus
|
||||||
|
// datasource, and we can treat it as a table with a schema
|
||||||
|
else {
|
||||||
table = tables.find(table => table._id === datasource.tableId)
|
table = tables.find(table => table._id === datasource.tableId)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Determine the schema from the table if not already determined
|
// Determine the schema from the backing entity if not already determined
|
||||||
if (table && !schema) {
|
if (table && !schema) {
|
||||||
if (type === "view") {
|
if (type === "view") {
|
||||||
schema = cloneDeep(table.views?.[datasource.name]?.schema)
|
schema = cloneDeep(table.views?.[datasource.name]?.schema)
|
||||||
|
@ -397,6 +465,26 @@ export const getSchemaForDatasource = (asset, datasource, isForm = false) => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check for any JSON fields so we can add any top level properties
|
||||||
|
if (schema) {
|
||||||
|
let jsonAdditions = {}
|
||||||
|
Object.keys(schema).forEach(fieldKey => {
|
||||||
|
const fieldSchema = schema[fieldKey]
|
||||||
|
if (fieldSchema?.type === "json") {
|
||||||
|
const jsonSchema = convertJSONSchemaToTableSchema(fieldSchema, {
|
||||||
|
squashObjects: true,
|
||||||
|
})
|
||||||
|
Object.keys(jsonSchema).forEach(jsonKey => {
|
||||||
|
jsonAdditions[`${fieldKey}.${jsonKey}`] = {
|
||||||
|
type: jsonSchema[jsonKey].type,
|
||||||
|
nestedJSON: true,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
schema = { ...schema, ...jsonAdditions }
|
||||||
|
}
|
||||||
|
|
||||||
// Add _id and _rev fields for certain types
|
// Add _id and _rev fields for certain types
|
||||||
if (schema && !isForm && ["table", "link"].includes(datasource.type)) {
|
if (schema && !isForm && ["table", "link"].includes(datasource.type)) {
|
||||||
schema["_id"] = { type: "string" }
|
schema["_id"] = { type: "string" }
|
||||||
|
@ -450,15 +538,58 @@ const buildFormSchema = component => {
|
||||||
return schema
|
return schema
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns an array of the keys of any state variables which are set anywhere
|
||||||
|
* in the app.
|
||||||
|
*/
|
||||||
|
export const getAllStateVariables = () => {
|
||||||
|
// Get all component containing assets
|
||||||
|
let allAssets = []
|
||||||
|
allAssets = allAssets.concat(get(store).layouts || [])
|
||||||
|
allAssets = allAssets.concat(get(store).screens || [])
|
||||||
|
|
||||||
|
// Find all button action settings in all components
|
||||||
|
let eventSettings = []
|
||||||
|
allAssets.forEach(asset => {
|
||||||
|
findAllMatchingComponents(asset.props, component => {
|
||||||
|
const settings = getComponentSettings(component._component)
|
||||||
|
settings
|
||||||
|
.filter(setting => setting.type === "event")
|
||||||
|
.forEach(setting => {
|
||||||
|
eventSettings.push(component[setting.key])
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
// Extract all state keys from any "update state" actions in each setting
|
||||||
|
let bindingSet = new Set()
|
||||||
|
eventSettings.forEach(setting => {
|
||||||
|
if (!Array.isArray(setting)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
setting.forEach(action => {
|
||||||
|
if (
|
||||||
|
action["##eventHandlerType"] === "Update State" &&
|
||||||
|
action.parameters?.type === "set" &&
|
||||||
|
action.parameters?.key &&
|
||||||
|
action.parameters?.value
|
||||||
|
) {
|
||||||
|
bindingSet.add(action.parameters.key)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
return Array.from(bindingSet)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Recurses the input object to remove any instances of bindings.
|
* Recurses the input object to remove any instances of bindings.
|
||||||
*/
|
*/
|
||||||
export function removeBindings(obj) {
|
export const removeBindings = (obj, replacement = "Invalid binding") => {
|
||||||
for (let [key, value] of Object.entries(obj)) {
|
for (let [key, value] of Object.entries(obj)) {
|
||||||
if (value && typeof value === "object") {
|
if (value && typeof value === "object") {
|
||||||
obj[key] = removeBindings(value)
|
obj[key] = removeBindings(value, replacement)
|
||||||
} else if (typeof value === "string") {
|
} else if (typeof value === "string") {
|
||||||
obj[key] = value.replace(CAPTURE_HBS_TEMPLATE, "Invalid binding")
|
obj[key] = value.replace(CAPTURE_HBS_TEMPLATE, replacement)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return obj
|
return obj
|
||||||
|
@ -468,8 +599,8 @@ export function removeBindings(obj) {
|
||||||
* When converting from readable to runtime it can sometimes add too many square brackets,
|
* When converting from readable to runtime it can sometimes add too many square brackets,
|
||||||
* this makes sure that doesn't happen.
|
* this makes sure that doesn't happen.
|
||||||
*/
|
*/
|
||||||
function shouldReplaceBinding(currentValue, from, convertTo) {
|
const shouldReplaceBinding = (currentValue, convertFrom, convertTo) => {
|
||||||
if (!currentValue?.includes(from)) {
|
if (!currentValue?.includes(convertFrom)) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
if (convertTo === "readableBinding") {
|
if (convertTo === "readableBinding") {
|
||||||
|
@ -478,7 +609,7 @@ function shouldReplaceBinding(currentValue, from, convertTo) {
|
||||||
// remove all the spaces, if the input is surrounded by spaces e.g. [ Auto ID ] then
|
// remove all the spaces, if the input is surrounded by spaces e.g. [ Auto ID ] then
|
||||||
// this makes sure it is detected
|
// this makes sure it is detected
|
||||||
const noSpaces = currentValue.replace(/\s+/g, "")
|
const noSpaces = currentValue.replace(/\s+/g, "")
|
||||||
const fromNoSpaces = from.replace(/\s+/g, "")
|
const fromNoSpaces = convertFrom.replace(/\s+/g, "")
|
||||||
const invalids = [
|
const invalids = [
|
||||||
`[${fromNoSpaces}]`,
|
`[${fromNoSpaces}]`,
|
||||||
`"${fromNoSpaces}"`,
|
`"${fromNoSpaces}"`,
|
||||||
|
@ -487,14 +618,21 @@ function shouldReplaceBinding(currentValue, from, convertTo) {
|
||||||
return !invalids.find(invalid => noSpaces?.includes(invalid))
|
return !invalids.find(invalid => noSpaces?.includes(invalid))
|
||||||
}
|
}
|
||||||
|
|
||||||
function replaceBetween(string, start, end, replacement) {
|
/**
|
||||||
|
* Utility function which replaces a string between given indices.
|
||||||
|
*/
|
||||||
|
const replaceBetween = (string, start, end, replacement) => {
|
||||||
return string.substring(0, start) + replacement + string.substring(end)
|
return string.substring(0, start) + replacement + string.substring(end)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* utility function for the readableToRuntimeBinding and runtimeToReadableBinding.
|
* Utility function for the readableToRuntimeBinding and runtimeToReadableBinding.
|
||||||
*/
|
*/
|
||||||
function bindingReplacement(bindableProperties, textWithBindings, convertTo) {
|
const bindingReplacement = (
|
||||||
|
bindableProperties,
|
||||||
|
textWithBindings,
|
||||||
|
convertTo
|
||||||
|
) => {
|
||||||
// Decide from base64 if using JS
|
// Decide from base64 if using JS
|
||||||
const isJS = isJSBinding(textWithBindings)
|
const isJS = isJSBinding(textWithBindings)
|
||||||
if (isJS) {
|
if (isJS) {
|
||||||
|
@ -559,14 +697,17 @@ function bindingReplacement(bindableProperties, textWithBindings, convertTo) {
|
||||||
* Extracts a component ID from a handlebars expression setting of
|
* Extracts a component ID from a handlebars expression setting of
|
||||||
* {{ literal [componentId] }}
|
* {{ literal [componentId] }}
|
||||||
*/
|
*/
|
||||||
function extractLiteralHandlebarsID(value) {
|
const extractLiteralHandlebarsID = value => {
|
||||||
return value?.match(/{{\s*literal\s*\[+([^\]]+)].*}}/)?.[1]
|
return value?.match(/{{\s*literal\s*\[+([^\]]+)].*}}/)?.[1]
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Converts a readable data binding into a runtime data binding
|
* Converts a readable data binding into a runtime data binding
|
||||||
*/
|
*/
|
||||||
export function readableToRuntimeBinding(bindableProperties, textWithBindings) {
|
export const readableToRuntimeBinding = (
|
||||||
|
bindableProperties,
|
||||||
|
textWithBindings
|
||||||
|
) => {
|
||||||
return bindingReplacement(
|
return bindingReplacement(
|
||||||
bindableProperties,
|
bindableProperties,
|
||||||
textWithBindings,
|
textWithBindings,
|
||||||
|
@ -577,56 +718,13 @@ export function readableToRuntimeBinding(bindableProperties, textWithBindings) {
|
||||||
/**
|
/**
|
||||||
* Converts a runtime data binding into a readable data binding
|
* Converts a runtime data binding into a readable data binding
|
||||||
*/
|
*/
|
||||||
export function runtimeToReadableBinding(bindableProperties, textWithBindings) {
|
export const runtimeToReadableBinding = (
|
||||||
|
bindableProperties,
|
||||||
|
textWithBindings
|
||||||
|
) => {
|
||||||
return bindingReplacement(
|
return bindingReplacement(
|
||||||
bindableProperties,
|
bindableProperties,
|
||||||
textWithBindings,
|
textWithBindings,
|
||||||
"readableBinding"
|
"readableBinding"
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns an array of the keys of any state variables which are set anywhere
|
|
||||||
* in the app.
|
|
||||||
*/
|
|
||||||
export const getAllStateVariables = () => {
|
|
||||||
let allComponents = []
|
|
||||||
|
|
||||||
// Find all onClick settings in all layouts
|
|
||||||
get(store).layouts.forEach(layout => {
|
|
||||||
const components = findAllMatchingComponents(
|
|
||||||
layout.props,
|
|
||||||
c => c.onClick != null
|
|
||||||
)
|
|
||||||
allComponents = allComponents.concat(components || [])
|
|
||||||
})
|
|
||||||
|
|
||||||
// Find all onClick settings in all screens
|
|
||||||
get(store).screens.forEach(screen => {
|
|
||||||
const components = findAllMatchingComponents(
|
|
||||||
screen.props,
|
|
||||||
c => c.onClick != null
|
|
||||||
)
|
|
||||||
allComponents = allComponents.concat(components || [])
|
|
||||||
})
|
|
||||||
|
|
||||||
// Add state bindings for all state actions
|
|
||||||
let bindingSet = new Set()
|
|
||||||
allComponents.forEach(component => {
|
|
||||||
if (!Array.isArray(component.onClick)) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
component.onClick.forEach(action => {
|
|
||||||
if (
|
|
||||||
action["##eventHandlerType"] === "Update State" &&
|
|
||||||
action.parameters?.type === "set" &&
|
|
||||||
action.parameters?.key &&
|
|
||||||
action.parameters?.value
|
|
||||||
) {
|
|
||||||
bindingSet.add(action.parameters.key)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
return Array.from(bindingSet)
|
|
||||||
}
|
|
||||||
|
|
|
@ -4,7 +4,7 @@ import { getHostingStore } from "./store/hosting"
|
||||||
import { getThemeStore } from "./store/theme"
|
import { getThemeStore } from "./store/theme"
|
||||||
import { derived, writable } from "svelte/store"
|
import { derived, writable } from "svelte/store"
|
||||||
import { FrontendTypes, LAYOUT_NAMES } from "../constants"
|
import { FrontendTypes, LAYOUT_NAMES } from "../constants"
|
||||||
import { findComponent } from "./storeUtils"
|
import { findComponent } from "./componentUtils"
|
||||||
|
|
||||||
export const store = getFrontendStore()
|
export const store = getFrontendStore()
|
||||||
export const automationStore = getAutomationStore()
|
export const automationStore = getAutomationStore()
|
||||||
|
|
|
@ -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,
|
findAllMatchingComponents,
|
||||||
findComponent,
|
findComponent,
|
||||||
getComponentSettings,
|
getComponentSettings,
|
||||||
} from "../storeUtils"
|
} from "../componentUtils"
|
||||||
import { uuid } from "../uuid"
|
import { uuid } from "../uuid"
|
||||||
import { removeBindings } from "../dataBinding"
|
import { removeBindings } from "../dataBinding"
|
||||||
|
|
||||||
|
@ -329,12 +329,12 @@ export const getFrontendStore = () => {
|
||||||
},
|
},
|
||||||
components: {
|
components: {
|
||||||
select: component => {
|
select: component => {
|
||||||
if (!component) {
|
const asset = get(currentAsset)
|
||||||
|
if (!asset || !component) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// If this is the root component, select the asset instead
|
// If this is the root component, select the asset instead
|
||||||
const asset = get(currentAsset)
|
|
||||||
const parent = findComponentParent(asset.props, component._id)
|
const parent = findComponentParent(asset.props, component._id)
|
||||||
if (parent == null) {
|
if (parent == null) {
|
||||||
const state = get(store)
|
const state = get(store)
|
||||||
|
@ -537,7 +537,7 @@ export const getFrontendStore = () => {
|
||||||
|
|
||||||
// immediately need to remove bindings, currently these aren't valid when pasted
|
// immediately need to remove bindings, currently these aren't valid when pasted
|
||||||
if (!cut && !preserveBindings) {
|
if (!cut && !preserveBindings) {
|
||||||
state.componentToPaste = removeBindings(state.componentToPaste)
|
state.componentToPaste = removeBindings(state.componentToPaste, "")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clone the component to paste
|
// Clone the component to paste
|
||||||
|
|
|
@ -137,6 +137,7 @@ const fieldTypeToComponentMap = {
|
||||||
datetime: "datetimefield",
|
datetime: "datetimefield",
|
||||||
attachment: "attachmentfield",
|
attachment: "attachmentfield",
|
||||||
link: "relationshipfield",
|
link: "relationshipfield",
|
||||||
|
json: "jsonfield",
|
||||||
}
|
}
|
||||||
|
|
||||||
export function makeDatasourceFormComponents(datasource) {
|
export function makeDatasourceFormComponents(datasource) {
|
||||||
|
@ -146,7 +147,7 @@ export function makeDatasourceFormComponents(datasource) {
|
||||||
fields.forEach(field => {
|
fields.forEach(field => {
|
||||||
const fieldSchema = schema[field]
|
const fieldSchema = schema[field]
|
||||||
// skip autocolumns
|
// skip autocolumns
|
||||||
if (fieldSchema.autocolumn) {
|
if (fieldSchema.autocolumn || fieldSchema.nestedJSON) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const fieldType =
|
const fieldType =
|
||||||
|
|
|
@ -6,16 +6,20 @@
|
||||||
Toggle,
|
Toggle,
|
||||||
TextArea,
|
TextArea,
|
||||||
Multiselect,
|
Multiselect,
|
||||||
|
Label,
|
||||||
} from "@budibase/bbui"
|
} from "@budibase/bbui"
|
||||||
import Dropzone from "components/common/Dropzone.svelte"
|
import Dropzone from "components/common/Dropzone.svelte"
|
||||||
import { capitalise } from "helpers"
|
import { capitalise } from "helpers"
|
||||||
import LinkedRowSelector from "components/common/LinkedRowSelector.svelte"
|
import LinkedRowSelector from "components/common/LinkedRowSelector.svelte"
|
||||||
|
import Editor from "../../integration/QueryEditor.svelte"
|
||||||
|
|
||||||
export let defaultValue
|
export let defaultValue
|
||||||
export let meta
|
export let meta
|
||||||
export let value = defaultValue || (meta.type === "boolean" ? false : "")
|
export let value = defaultValue || (meta.type === "boolean" ? false : "")
|
||||||
export let readonly
|
export let readonly
|
||||||
|
|
||||||
|
$: stringVal =
|
||||||
|
typeof value === "object" ? JSON.stringify(value, null, 2) : value
|
||||||
$: type = meta?.type
|
$: type = meta?.type
|
||||||
$: label = meta.name ? capitalise(meta.name) : ""
|
$: label = meta.name ? capitalise(meta.name) : ""
|
||||||
</script>
|
</script>
|
||||||
|
@ -40,6 +44,14 @@
|
||||||
<LinkedRowSelector bind:linkedRows={value} schema={meta} />
|
<LinkedRowSelector bind:linkedRows={value} schema={meta} />
|
||||||
{:else if type === "longform"}
|
{:else if type === "longform"}
|
||||||
<TextArea {label} bind:value />
|
<TextArea {label} bind:value />
|
||||||
|
{:else if type === "json"}
|
||||||
|
<Label>{label}</Label>
|
||||||
|
<Editor
|
||||||
|
editorHeight="250"
|
||||||
|
mode="json"
|
||||||
|
on:change={({ detail }) => (value = detail.value)}
|
||||||
|
value={stringVal}
|
||||||
|
/>
|
||||||
{:else}
|
{:else}
|
||||||
<Input
|
<Input
|
||||||
{label}
|
{label}
|
||||||
|
|
|
@ -9,6 +9,7 @@
|
||||||
DatePicker,
|
DatePicker,
|
||||||
ModalContent,
|
ModalContent,
|
||||||
Context,
|
Context,
|
||||||
|
Modal,
|
||||||
notifications,
|
notifications,
|
||||||
} from "@budibase/bbui"
|
} from "@budibase/bbui"
|
||||||
import { createEventDispatcher } from "svelte"
|
import { createEventDispatcher } from "svelte"
|
||||||
|
@ -32,12 +33,14 @@
|
||||||
import ModalBindableInput from "components/common/bindings/ModalBindableInput.svelte"
|
import ModalBindableInput from "components/common/bindings/ModalBindableInput.svelte"
|
||||||
import { getBindings } from "components/backend/DataTable/formula"
|
import { getBindings } from "components/backend/DataTable/formula"
|
||||||
import { getContext } from "svelte"
|
import { getContext } from "svelte"
|
||||||
|
import JSONSchemaModal from "./JSONSchemaModal.svelte"
|
||||||
|
|
||||||
const AUTO_TYPE = "auto"
|
const AUTO_TYPE = "auto"
|
||||||
const FORMULA_TYPE = FIELDS.FORMULA.type
|
const FORMULA_TYPE = FIELDS.FORMULA.type
|
||||||
const LINK_TYPE = FIELDS.LINK.type
|
const LINK_TYPE = FIELDS.LINK.type
|
||||||
const STRING_TYPE = FIELDS.STRING.type
|
const STRING_TYPE = FIELDS.STRING.type
|
||||||
const NUMBER_TYPE = FIELDS.NUMBER.type
|
const NUMBER_TYPE = FIELDS.NUMBER.type
|
||||||
|
const JSON_TYPE = FIELDS.JSON.type
|
||||||
const DATE_TYPE = FIELDS.DATETIME.type
|
const DATE_TYPE = FIELDS.DATETIME.type
|
||||||
|
|
||||||
const dispatch = createEventDispatcher()
|
const dispatch = createEventDispatcher()
|
||||||
|
@ -64,6 +67,7 @@
|
||||||
let confirmDeleteDialog
|
let confirmDeleteDialog
|
||||||
let deletion
|
let deletion
|
||||||
let deleteColName
|
let deleteColName
|
||||||
|
let jsonSchemaModal
|
||||||
|
|
||||||
$: checkConstraints(field)
|
$: checkConstraints(field)
|
||||||
$: required = !!field?.constraints?.presence || primaryDisplay
|
$: required = !!field?.constraints?.presence || primaryDisplay
|
||||||
|
@ -79,10 +83,14 @@
|
||||||
// used to select what different options can be displayed for column type
|
// used to select what different options can be displayed for column type
|
||||||
$: canBeSearched =
|
$: canBeSearched =
|
||||||
field.type !== LINK_TYPE &&
|
field.type !== LINK_TYPE &&
|
||||||
|
field.type !== JSON_TYPE &&
|
||||||
field.subtype !== AUTO_COLUMN_SUB_TYPES.CREATED_BY &&
|
field.subtype !== AUTO_COLUMN_SUB_TYPES.CREATED_BY &&
|
||||||
field.subtype !== AUTO_COLUMN_SUB_TYPES.UPDATED_BY &&
|
field.subtype !== AUTO_COLUMN_SUB_TYPES.UPDATED_BY &&
|
||||||
field.type !== FORMULA_TYPE
|
field.type !== FORMULA_TYPE
|
||||||
$: canBeDisplay = field.type !== LINK_TYPE && field.type !== AUTO_TYPE
|
$: canBeDisplay =
|
||||||
|
field.type !== LINK_TYPE &&
|
||||||
|
field.type !== AUTO_TYPE &&
|
||||||
|
field.type !== JSON_TYPE
|
||||||
$: canBeRequired =
|
$: canBeRequired =
|
||||||
field.type !== LINK_TYPE && !uneditable && field.type !== AUTO_TYPE
|
field.type !== LINK_TYPE && !uneditable && field.type !== AUTO_TYPE
|
||||||
$: relationshipOptions = getRelationshipOptions(field)
|
$: relationshipOptions = getRelationshipOptions(field)
|
||||||
|
@ -176,6 +184,10 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function openJsonSchemaEditor() {
|
||||||
|
jsonSchemaModal.show()
|
||||||
|
}
|
||||||
|
|
||||||
function confirmDelete() {
|
function confirmDelete() {
|
||||||
confirmDeleteDialog.show()
|
confirmDeleteDialog.show()
|
||||||
deletion = true
|
deletion = true
|
||||||
|
@ -430,6 +442,10 @@
|
||||||
getOptionLabel={option => option[1].name}
|
getOptionLabel={option => option[1].name}
|
||||||
getOptionValue={option => option[0]}
|
getOptionValue={option => option[0]}
|
||||||
/>
|
/>
|
||||||
|
{:else if field.type === JSON_TYPE}
|
||||||
|
<Button primary text on:click={openJsonSchemaEditor}
|
||||||
|
>Open schema editor</Button
|
||||||
|
>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<div slot="footer">
|
<div slot="footer">
|
||||||
|
@ -438,6 +454,16 @@
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</ModalContent>
|
</ModalContent>
|
||||||
|
<Modal bind:this={jsonSchemaModal}>
|
||||||
|
<JSONSchemaModal
|
||||||
|
schema={field.schema}
|
||||||
|
json={field.json}
|
||||||
|
on:save={({ detail }) => {
|
||||||
|
field.schema = detail.schema
|
||||||
|
field.json = detail.json
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Modal>
|
||||||
<ConfirmDialog
|
<ConfirmDialog
|
||||||
bind:this={confirmDeleteDialog}
|
bind:this={confirmDeleteDialog}
|
||||||
okText="Delete Column"
|
okText="Delete Column"
|
||||||
|
|
|
@ -6,7 +6,7 @@
|
||||||
import ErrorsBox from "components/common/ErrorsBox.svelte"
|
import ErrorsBox from "components/common/ErrorsBox.svelte"
|
||||||
import { roles } from "stores/backend"
|
import { roles } from "stores/backend"
|
||||||
|
|
||||||
const BASE_ROLE = { _id: "", inherits: "BASIC", permissionId: "Read/Write" }
|
const BASE_ROLE = { _id: "", inherits: "BASIC", permissionId: "write" }
|
||||||
|
|
||||||
let basePermissions = []
|
let basePermissions = []
|
||||||
let selectedRole = BASE_ROLE
|
let selectedRole = BASE_ROLE
|
||||||
|
|
|
@ -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,
|
notifications,
|
||||||
} from "@budibase/bbui"
|
} from "@budibase/bbui"
|
||||||
import ErrorSVG from "assets/error.svg?raw"
|
import ErrorSVG from "assets/error.svg?raw"
|
||||||
import { findComponent, findComponentPath } from "builderStore/storeUtils"
|
import { findComponent, findComponentPath } from "builderStore/componentUtils"
|
||||||
|
|
||||||
let iframe
|
let iframe
|
||||||
let layout
|
let layout
|
||||||
|
@ -69,15 +69,7 @@
|
||||||
previewDevice: $store.previewDevice,
|
previewDevice: $store.previewDevice,
|
||||||
messagePassing: $store.clientFeatures.messagePassing,
|
messagePassing: $store.clientFeatures.messagePassing,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Saving pages and screens to the DB causes them to have _revs.
|
|
||||||
// These revisions change every time a save happens and causes
|
|
||||||
// these reactive statements to fire, even though the actual
|
|
||||||
// definition hasn't changed.
|
|
||||||
// By deleting all _rev properties we can avoid this and increase
|
|
||||||
// performance.
|
|
||||||
$: json = JSON.stringify(previewData)
|
$: json = JSON.stringify(previewData)
|
||||||
$: strippedJson = json.replace(/"_rev":\s*"[^"]+"/g, `"_rev":""`)
|
|
||||||
|
|
||||||
// Update the iframe with the builder info to render the correct preview
|
// Update the iframe with the builder info to render the correct preview
|
||||||
const refreshContent = message => {
|
const refreshContent = message => {
|
||||||
|
@ -87,7 +79,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
// Refresh the preview when required
|
// Refresh the preview when required
|
||||||
$: refreshContent(strippedJson)
|
$: refreshContent(json)
|
||||||
|
|
||||||
function receiveMessage(message) {
|
function receiveMessage(message) {
|
||||||
const handlers = {
|
const handlers = {
|
||||||
|
@ -102,7 +94,7 @@
|
||||||
if (!$store.clientFeatures.intelligentLoading) {
|
if (!$store.clientFeatures.intelligentLoading) {
|
||||||
loading = false
|
loading = false
|
||||||
}
|
}
|
||||||
refreshContent(strippedJson)
|
refreshContent(json)
|
||||||
},
|
},
|
||||||
[MessageTypes.ERROR]: event => {
|
[MessageTypes.ERROR]: event => {
|
||||||
// Catch any app errors
|
// Catch any app errors
|
||||||
|
|
|
@ -43,7 +43,8 @@
|
||||||
"attachmentfield",
|
"attachmentfield",
|
||||||
"relationshipfield",
|
"relationshipfield",
|
||||||
"daterangepicker",
|
"daterangepicker",
|
||||||
"multifieldselect"
|
"multifieldselect",
|
||||||
|
"jsonfield"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -73,6 +74,7 @@
|
||||||
"heading",
|
"heading",
|
||||||
"text",
|
"text",
|
||||||
"button",
|
"button",
|
||||||
|
"tag",
|
||||||
"divider",
|
"divider",
|
||||||
"image",
|
"image",
|
||||||
"backgroundimage",
|
"backgroundimage",
|
||||||
|
|
|
@ -49,6 +49,7 @@ export default `
|
||||||
try {
|
try {
|
||||||
parsed = JSON.parse(event.data)
|
parsed = JSON.parse(event.data)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
console.error("Client received invalid JSON")
|
||||||
// Ignore
|
// Ignore
|
||||||
}
|
}
|
||||||
if (!parsed) {
|
if (!parsed) {
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
import { get } from "svelte/store"
|
import { get } from "svelte/store"
|
||||||
import { store, currentAsset } from "builderStore"
|
import { store, currentAsset } from "builderStore"
|
||||||
import ConfirmDialog from "components/common/ConfirmDialog.svelte"
|
import ConfirmDialog from "components/common/ConfirmDialog.svelte"
|
||||||
import { findComponentParent } from "builderStore/storeUtils"
|
import { findComponentParent } from "builderStore/componentUtils"
|
||||||
import { ActionMenu, MenuItem, Icon, notifications } from "@budibase/bbui"
|
import { ActionMenu, MenuItem, Icon, notifications } from "@budibase/bbui"
|
||||||
|
|
||||||
export let component
|
export let component
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { writable, get } from "svelte/store"
|
import { writable, get } from "svelte/store"
|
||||||
import { store as frontendStore } from "builderStore"
|
import { store as frontendStore } from "builderStore"
|
||||||
import { findComponentPath } from "builderStore/storeUtils"
|
import { findComponentPath } from "builderStore/componentUtils"
|
||||||
|
|
||||||
export const DropEffect = {
|
export const DropEffect = {
|
||||||
MOVE: "move",
|
MOVE: "move",
|
||||||
|
|
|
@ -63,7 +63,14 @@
|
||||||
// If no specific value is depended upon, check if a value exists at all
|
// If no specific value is depended upon, check if a value exists at all
|
||||||
// for the dependent setting
|
// for the dependent setting
|
||||||
if (dependantValue == null) {
|
if (dependantValue == null) {
|
||||||
return !isEmpty(componentInstance[dependantSetting])
|
const currentValue = componentInstance[dependantSetting]
|
||||||
|
if (currentValue === false) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if (currentValue === true) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return !isEmpty(currentValue)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Otherwise check the value matches
|
// Otherwise check the value matches
|
||||||
|
|
|
@ -9,8 +9,9 @@
|
||||||
ActionMenu,
|
ActionMenu,
|
||||||
MenuItem,
|
MenuItem,
|
||||||
} from "@budibase/bbui"
|
} from "@budibase/bbui"
|
||||||
import { getAvailableActions } from "./actions"
|
import { getAvailableActions } from "./index"
|
||||||
import { generate } from "shortid"
|
import { generate } from "shortid"
|
||||||
|
import { getButtonContextBindings } from "builderStore/dataBinding"
|
||||||
|
|
||||||
const flipDurationMs = 150
|
const flipDurationMs = 150
|
||||||
const EVENT_TYPE_KEY = "##eventHandlerType"
|
const EVENT_TYPE_KEY = "##eventHandlerType"
|
||||||
|
@ -19,7 +20,16 @@
|
||||||
export let actions
|
export let actions
|
||||||
export let bindings = []
|
export let bindings = []
|
||||||
|
|
||||||
// dndzone needs an id on the array items, so this adds some temporary ones.
|
let selectedAction = actions?.length ? actions[0] : null
|
||||||
|
|
||||||
|
// These are ephemeral bindings which only exist while executing actions
|
||||||
|
$: buttonContextBindings = getButtonContextBindings(
|
||||||
|
actions,
|
||||||
|
selectedAction?.id
|
||||||
|
)
|
||||||
|
$: allBindings = buttonContextBindings.concat(bindings)
|
||||||
|
|
||||||
|
// Assign a unique ID to each action
|
||||||
$: {
|
$: {
|
||||||
if (actions) {
|
if (actions) {
|
||||||
actions.forEach(action => {
|
actions.forEach(action => {
|
||||||
|
@ -30,8 +40,6 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let selectedAction = actions?.length ? actions[0] : null
|
|
||||||
|
|
||||||
$: selectedActionComponent =
|
$: selectedActionComponent =
|
||||||
selectedAction &&
|
selectedAction &&
|
||||||
actionTypes.find(t => t.name === selectedAction[EVENT_TYPE_KEY])?.component
|
actionTypes.find(t => t.name === selectedAction[EVENT_TYPE_KEY])?.component
|
||||||
|
@ -122,7 +130,7 @@
|
||||||
<svelte:component
|
<svelte:component
|
||||||
this={selectedActionComponent}
|
this={selectedActionComponent}
|
||||||
parameters={selectedAction.parameters}
|
parameters={selectedAction.parameters}
|
||||||
{bindings}
|
bindings={allBindings}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
|
@ -2,7 +2,7 @@
|
||||||
import { ActionButton, Button, Drawer } from "@budibase/bbui"
|
import { ActionButton, Button, Drawer } from "@budibase/bbui"
|
||||||
import { createEventDispatcher } from "svelte"
|
import { createEventDispatcher } from "svelte"
|
||||||
import { notifications } from "@budibase/bbui"
|
import { notifications } from "@budibase/bbui"
|
||||||
import EventEditor from "./EventEditor.svelte"
|
import ButtonActionDrawer from "./ButtonActionDrawer.svelte"
|
||||||
import { automationStore } from "builderStore"
|
import { automationStore } from "builderStore"
|
||||||
import { cloneDeep } from "lodash/fp"
|
import { cloneDeep } from "lodash/fp"
|
||||||
|
|
||||||
|
@ -67,7 +67,7 @@
|
||||||
Define what actions to run.
|
Define what actions to run.
|
||||||
</svelte:fragment>
|
</svelte:fragment>
|
||||||
<Button cta slot="buttons" on:click={saveEventData}>Save</Button>
|
<Button cta slot="buttons" on:click={saveEventData}>Save</Button>
|
||||||
<EventEditor
|
<ButtonActionDrawer
|
||||||
slot="body"
|
slot="body"
|
||||||
bind:actions={tmpValue}
|
bind:actions={tmpValue}
|
||||||
eventType={name}
|
eventType={name}
|
|
@ -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 { selectedComponent } from "builderStore"
|
||||||
import { getComponentForSettingType } from "./componentSettings"
|
import { getComponentForSettingType } from "./componentSettings"
|
||||||
import PropertyControl from "./PropertyControl.svelte"
|
import PropertyControl from "./PropertyControl.svelte"
|
||||||
import { getComponentSettings } from "builderStore/storeUtils"
|
import { getComponentSettings } from "builderStore/componentUtils"
|
||||||
|
|
||||||
export let conditions = []
|
export let conditions = []
|
||||||
export let bindings = []
|
export let bindings = []
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
import { Select } from "@budibase/bbui"
|
import { Select } from "@budibase/bbui"
|
||||||
import { makePropSafe } from "@budibase/string-templates"
|
import { makePropSafe } from "@budibase/string-templates"
|
||||||
import { currentAsset, store } from "builderStore"
|
import { currentAsset, store } from "builderStore"
|
||||||
import { findComponentPath } from "builderStore/storeUtils"
|
import { findComponentPath } from "builderStore/componentUtils"
|
||||||
import { createEventDispatcher, onMount } from "svelte"
|
import { createEventDispatcher, onMount } from "svelte"
|
||||||
|
|
||||||
export let value
|
export let value
|
||||||
|
|
|
@ -48,9 +48,7 @@
|
||||||
return [...acc, ...viewsArr]
|
return [...acc, ...viewsArr]
|
||||||
}, [])
|
}, [])
|
||||||
$: queries = $queriesStore.list
|
$: queries = $queriesStore.list
|
||||||
.filter(
|
.filter(q => showAllQueries || q.queryVerb === "read" || q.readable)
|
||||||
query => showAllQueries || query.queryVerb === "read" || query.readable
|
|
||||||
)
|
|
||||||
.map(query => ({
|
.map(query => ({
|
||||||
label: query.name,
|
label: query.name,
|
||||||
name: query.name,
|
name: query.name,
|
||||||
|
@ -104,6 +102,22 @@
|
||||||
value: `{{ literal ${runtimeBinding} }}`,
|
value: `{{ literal ${runtimeBinding} }}`,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
$: jsonArrays = bindings
|
||||||
|
.filter(x => x.fieldSchema?.type === "jsonarray")
|
||||||
|
.map(binding => {
|
||||||
|
const { providerId, readableBinding, runtimeBinding, tableId } = binding
|
||||||
|
const { name, type, prefixKeys } = binding.fieldSchema
|
||||||
|
return {
|
||||||
|
providerId,
|
||||||
|
label: readableBinding,
|
||||||
|
fieldName: name,
|
||||||
|
fieldType: type,
|
||||||
|
tableId,
|
||||||
|
prefixKeys,
|
||||||
|
type: "jsonarray",
|
||||||
|
value: `{{ literal ${runtimeBinding} }}`,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
const handleSelected = selected => {
|
const handleSelected = selected => {
|
||||||
dispatch("change", selected)
|
dispatch("change", selected)
|
||||||
|
@ -230,6 +244,17 @@
|
||||||
{/each}
|
{/each}
|
||||||
</ul>
|
</ul>
|
||||||
{/if}
|
{/if}
|
||||||
|
{#if jsonArrays?.length}
|
||||||
|
<Divider size="S" />
|
||||||
|
<div class="title">
|
||||||
|
<Heading size="XS">JSON Arrays</Heading>
|
||||||
|
</div>
|
||||||
|
<ul>
|
||||||
|
{#each jsonArrays as field}
|
||||||
|
<li on:click={() => handleSelected(field)}>{field.label}</li>
|
||||||
|
{/each}
|
||||||
|
</ul>
|
||||||
|
{/if}
|
||||||
{#if dataProviders?.length}
|
{#if dataProviders?.length}
|
||||||
<Divider size="S" />
|
<Divider size="S" />
|
||||||
<div class="title">
|
<div class="title">
|
||||||
|
|
|
@ -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 panel = ClientBindingPanel
|
||||||
export let allowBindings = true
|
export let allowBindings = true
|
||||||
|
|
||||||
const BannedTypes = ["link", "attachment", "formula"]
|
const BannedTypes = ["link", "attachment", "formula", "json", "jsonarray"]
|
||||||
|
|
||||||
$: fieldOptions = (schemaFields ?? [])
|
$: fieldOptions = (schemaFields ?? [])
|
||||||
.filter(field => !BannedTypes.includes(field.type))
|
.filter(field => !BannedTypes.includes(field.type))
|
||||||
|
|
|
@ -5,7 +5,7 @@
|
||||||
getSchemaForDatasource,
|
getSchemaForDatasource,
|
||||||
} from "builderStore/dataBinding"
|
} from "builderStore/dataBinding"
|
||||||
import { currentAsset } from "builderStore"
|
import { currentAsset } from "builderStore"
|
||||||
import { findClosestMatchingComponent } from "builderStore/storeUtils"
|
import { findClosestMatchingComponent } from "builderStore/componentUtils"
|
||||||
|
|
||||||
export let componentInstance
|
export let componentInstance
|
||||||
export let value
|
export let value
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
<script>
|
<script>
|
||||||
import { ActionButton } from "@budibase/bbui"
|
import { ActionButton } from "@budibase/bbui"
|
||||||
import { currentAsset, store } from "builderStore"
|
import { currentAsset, store } from "builderStore"
|
||||||
import { findClosestMatchingComponent } from "builderStore/storeUtils"
|
import { findClosestMatchingComponent } from "builderStore/componentUtils"
|
||||||
import { makeDatasourceFormComponents } from "builderStore/store/screenTemplates/utils/commonComponents"
|
import { makeDatasourceFormComponents } from "builderStore/store/screenTemplates/utils/commonComponents"
|
||||||
import ConfirmDialog from "components/common/ConfirmDialog.svelte"
|
import ConfirmDialog from "components/common/ConfirmDialog.svelte"
|
||||||
|
|
||||||
|
|
|
@ -11,7 +11,7 @@
|
||||||
DatePicker,
|
DatePicker,
|
||||||
} from "@budibase/bbui"
|
} from "@budibase/bbui"
|
||||||
import { currentAsset, selectedComponent } from "builderStore"
|
import { currentAsset, selectedComponent } from "builderStore"
|
||||||
import { findClosestMatchingComponent } from "builderStore/storeUtils"
|
import { findClosestMatchingComponent } from "builderStore/componentUtils"
|
||||||
import { getSchemaForDatasource } from "builderStore/dataBinding"
|
import { getSchemaForDatasource } from "builderStore/dataBinding"
|
||||||
import DrawerBindableInput from "components/common/bindings/DrawerBindableInput.svelte"
|
import DrawerBindableInput from "components/common/bindings/DrawerBindableInput.svelte"
|
||||||
import { generate } from "shortid"
|
import { generate } from "shortid"
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { Checkbox, Select, Stepper } from "@budibase/bbui"
|
import { Checkbox, Select, Stepper } from "@budibase/bbui"
|
||||||
import DataSourceSelect from "./DataSourceSelect.svelte"
|
import DataSourceSelect from "./DataSourceSelect.svelte"
|
||||||
import DataProviderSelect from "./DataProviderSelect.svelte"
|
import DataProviderSelect from "./DataProviderSelect.svelte"
|
||||||
import EventsEditor from "./EventsEditor"
|
import ButtonActionEditor from "./ButtonActionEditor/ButtonActionEditor.svelte"
|
||||||
import TableSelect from "./TableSelect.svelte"
|
import TableSelect from "./TableSelect.svelte"
|
||||||
import ColorPicker from "./ColorPicker.svelte"
|
import ColorPicker from "./ColorPicker.svelte"
|
||||||
import { IconSelect } from "./IconSelect"
|
import { IconSelect } from "./IconSelect"
|
||||||
|
@ -24,7 +24,7 @@ const componentMap = {
|
||||||
dataProvider: DataProviderSelect,
|
dataProvider: DataProviderSelect,
|
||||||
boolean: Checkbox,
|
boolean: Checkbox,
|
||||||
number: Stepper,
|
number: Stepper,
|
||||||
event: EventsEditor,
|
event: ButtonActionEditor,
|
||||||
table: TableSelect,
|
table: TableSelect,
|
||||||
color: ColorPicker,
|
color: ColorPicker,
|
||||||
icon: IconSelect,
|
icon: IconSelect,
|
||||||
|
@ -45,6 +45,7 @@ const componentMap = {
|
||||||
"field/attachment": FormFieldSelect,
|
"field/attachment": FormFieldSelect,
|
||||||
"field/link": FormFieldSelect,
|
"field/link": FormFieldSelect,
|
||||||
"field/array": FormFieldSelect,
|
"field/array": FormFieldSelect,
|
||||||
|
"field/json": FormFieldSelect,
|
||||||
// Some validation types are the same as others, so not all types are
|
// Some validation types are the same as others, so not all types are
|
||||||
// explicitly listed here. e.g. options uses string validation
|
// explicitly listed here. e.g. options uses string validation
|
||||||
"validation/string": ValidationEditor,
|
"validation/string": ValidationEditor,
|
||||||
|
|
|
@ -89,6 +89,14 @@ export const FIELDS = {
|
||||||
presence: false,
|
presence: false,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
JSON: {
|
||||||
|
name: "JSON",
|
||||||
|
type: "json",
|
||||||
|
constraints: {
|
||||||
|
type: "object",
|
||||||
|
presence: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
export const AUTO_COLUMN_SUB_TYPES = {
|
export const AUTO_COLUMN_SUB_TYPES = {
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import { NoEmptyFilterStrings } from "../constants/lucene"
|
import { NoEmptyFilterStrings } from "../constants/lucene"
|
||||||
|
import { deepGet } from "@budibase/bbui"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Removes any fields that contain empty strings that would cause inconsistent
|
* Removes any fields that contain empty strings that would cause inconsistent
|
||||||
|
@ -96,9 +97,13 @@ export const buildLuceneQuery = filter => {
|
||||||
* @param query the JSON lucene query
|
* @param query the JSON lucene query
|
||||||
*/
|
*/
|
||||||
export const luceneQuery = (docs, query) => {
|
export const luceneQuery = (docs, query) => {
|
||||||
|
if (!docs || !Array.isArray(docs)) {
|
||||||
|
return []
|
||||||
|
}
|
||||||
if (!query) {
|
if (!query) {
|
||||||
return docs
|
return docs
|
||||||
}
|
}
|
||||||
|
|
||||||
// make query consistent first
|
// make query consistent first
|
||||||
query = cleanupQuery(query)
|
query = cleanupQuery(query)
|
||||||
|
|
||||||
|
@ -106,7 +111,9 @@ export const luceneQuery = (docs, query) => {
|
||||||
const match = (type, failFn) => doc => {
|
const match = (type, failFn) => doc => {
|
||||||
const filters = Object.entries(query[type] || {})
|
const filters = Object.entries(query[type] || {})
|
||||||
for (let i = 0; i < filters.length; i++) {
|
for (let i = 0; i < filters.length; i++) {
|
||||||
if (failFn(filters[i][0], filters[i][1], doc)) {
|
const [key, testValue] = filters[i]
|
||||||
|
const docValue = deepGet(doc, key)
|
||||||
|
if (failFn(docValue, testValue)) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -114,38 +121,38 @@ export const luceneQuery = (docs, query) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Process a string match (fails if the value does not start with the string)
|
// Process a string match (fails if the value does not start with the string)
|
||||||
const stringMatch = match("string", (key, value, doc) => {
|
const stringMatch = match("string", (docValue, testValue) => {
|
||||||
return !doc[key] || !doc[key].startsWith(value)
|
return !docValue || !docValue.startsWith(testValue)
|
||||||
})
|
})
|
||||||
|
|
||||||
// Process a fuzzy match (treat the same as starts with when running locally)
|
// Process a fuzzy match (treat the same as starts with when running locally)
|
||||||
const fuzzyMatch = match("fuzzy", (key, value, doc) => {
|
const fuzzyMatch = match("fuzzy", (docValue, testValue) => {
|
||||||
return !doc[key] || !doc[key].startsWith(value)
|
return !docValue || !docValue.startsWith(testValue)
|
||||||
})
|
})
|
||||||
|
|
||||||
// Process a range match
|
// Process a range match
|
||||||
const rangeMatch = match("range", (key, value, doc) => {
|
const rangeMatch = match("range", (docValue, testValue) => {
|
||||||
return !doc[key] || doc[key] < value.low || doc[key] > value.high
|
return !docValue || docValue < testValue.low || docValue > testValue.high
|
||||||
})
|
})
|
||||||
|
|
||||||
// Process an equal match (fails if the value is different)
|
// Process an equal match (fails if the value is different)
|
||||||
const equalMatch = match("equal", (key, value, doc) => {
|
const equalMatch = match("equal", (docValue, testValue) => {
|
||||||
return value != null && value !== "" && doc[key] !== value
|
return testValue != null && testValue !== "" && docValue !== testValue
|
||||||
})
|
})
|
||||||
|
|
||||||
// Process a not-equal match (fails if the value is the same)
|
// Process a not-equal match (fails if the value is the same)
|
||||||
const notEqualMatch = match("notEqual", (key, value, doc) => {
|
const notEqualMatch = match("notEqual", (docValue, testValue) => {
|
||||||
return value != null && value !== "" && doc[key] === value
|
return testValue != null && testValue !== "" && docValue === testValue
|
||||||
})
|
})
|
||||||
|
|
||||||
// Process an empty match (fails if the value is not empty)
|
// Process an empty match (fails if the value is not empty)
|
||||||
const emptyMatch = match("empty", (key, value, doc) => {
|
const emptyMatch = match("empty", docValue => {
|
||||||
return doc[key] != null && doc[key] !== ""
|
return docValue != null && docValue !== ""
|
||||||
})
|
})
|
||||||
|
|
||||||
// Process a not-empty match (fails is the value is empty)
|
// Process a not-empty match (fails is the value is empty)
|
||||||
const notEmptyMatch = match("notEmpty", (key, value, doc) => {
|
const notEmptyMatch = match("notEmpty", docValue => {
|
||||||
return doc[key] == null || doc[key] === ""
|
return docValue == null || docValue === ""
|
||||||
})
|
})
|
||||||
|
|
||||||
// Match a document against all criteria
|
// Match a document against all criteria
|
||||||
|
|
|
@ -13,7 +13,7 @@
|
||||||
import FrontendNavigatePane from "components/design/NavigationPanel/FrontendNavigatePane.svelte"
|
import FrontendNavigatePane from "components/design/NavigationPanel/FrontendNavigatePane.svelte"
|
||||||
import { goto, leftover, params } from "@roxi/routify"
|
import { goto, leftover, params } from "@roxi/routify"
|
||||||
import { FrontendTypes } from "constants"
|
import { FrontendTypes } from "constants"
|
||||||
import { findComponent, findComponentPath } from "builderStore/storeUtils"
|
import { findComponent, findComponentPath } from "builderStore/componentUtils"
|
||||||
import { get } from "svelte/store"
|
import { get } from "svelte/store"
|
||||||
import AppThemeSelect from "components/design/AppPreview/AppThemeSelect.svelte"
|
import AppThemeSelect from "components/design/AppPreview/AppThemeSelect.svelte"
|
||||||
import ThemeEditor from "components/design/AppPreview/ThemeEditor.svelte"
|
import ThemeEditor from "components/design/AppPreview/ThemeEditor.svelte"
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@budibase/cli",
|
"name": "@budibase/cli",
|
||||||
"version": "1.0.27-alpha.0",
|
"version": "1.0.27-alpha.2",
|
||||||
"description": "Budibase CLI, for developers, self hosting and migrations.",
|
"description": "Budibase CLI, for developers, self hosting and migrations.",
|
||||||
"main": "src/index.js",
|
"main": "src/index.js",
|
||||||
"bin": {
|
"bin": {
|
||||||
|
|
|
@ -500,9 +500,20 @@
|
||||||
"defaultValue": "M"
|
"defaultValue": "M"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"context": {
|
"context": [
|
||||||
|
{
|
||||||
"type": "schema"
|
"type": "schema"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "static",
|
||||||
|
"values": [
|
||||||
|
{
|
||||||
|
"label": "Row Index",
|
||||||
|
"key": "index"
|
||||||
}
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
},
|
},
|
||||||
"stackedlist": {
|
"stackedlist": {
|
||||||
"deprecated": true,
|
"deprecated": true,
|
||||||
|
@ -808,6 +819,57 @@
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"tag": {
|
||||||
|
"name": "Tag",
|
||||||
|
"icon": "TextParagraph",
|
||||||
|
"showSettingsBar": true,
|
||||||
|
"settings": [
|
||||||
|
{
|
||||||
|
"type": "text",
|
||||||
|
"label": "Text",
|
||||||
|
"key": "text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "select",
|
||||||
|
"label": "Size",
|
||||||
|
"key": "size",
|
||||||
|
"defaultValue": "M",
|
||||||
|
"showInBar": true,
|
||||||
|
"barStyle": "picker",
|
||||||
|
"options": [{
|
||||||
|
"label": "Small",
|
||||||
|
"value": "S"
|
||||||
|
}, {
|
||||||
|
"label": "Medium",
|
||||||
|
"value": "M"
|
||||||
|
}, {
|
||||||
|
"label": "Large",
|
||||||
|
"value": "L"
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "color",
|
||||||
|
"label": "Color",
|
||||||
|
"key": "color",
|
||||||
|
"showInBar": true,
|
||||||
|
"barSeparator": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "boolean",
|
||||||
|
"label": "Show delete icon",
|
||||||
|
"key": "closable",
|
||||||
|
"showInBar": true,
|
||||||
|
"barIcon": "TagItalic",
|
||||||
|
"barTitle": "Show delete icon"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "event",
|
||||||
|
"label": "On click delete icon",
|
||||||
|
"key": "onClick",
|
||||||
|
"dependsOn": "closable"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
"image": {
|
"image": {
|
||||||
"name": "Image",
|
"name": "Image",
|
||||||
"description": "A basic component for displaying images",
|
"description": "A basic component for displaying images",
|
||||||
|
@ -1775,6 +1837,10 @@
|
||||||
{
|
{
|
||||||
"type": "static",
|
"type": "static",
|
||||||
"values": [
|
"values": [
|
||||||
|
{
|
||||||
|
"label": "Value",
|
||||||
|
"key": "__value"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"label": "Valid",
|
"label": "Valid",
|
||||||
"key": "__valid"
|
"key": "__valid"
|
||||||
|
@ -2407,6 +2473,40 @@
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"jsonfield": {
|
||||||
|
"name": "JSON Field",
|
||||||
|
"icon": "Brackets",
|
||||||
|
"styles": ["size"],
|
||||||
|
"editable": true,
|
||||||
|
"settings": [
|
||||||
|
{
|
||||||
|
"type": "field/json",
|
||||||
|
"label": "Field",
|
||||||
|
"key": "field"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "text",
|
||||||
|
"label": "Label",
|
||||||
|
"key": "label"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "text",
|
||||||
|
"label": "Placeholder",
|
||||||
|
"key": "placeholder"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "text",
|
||||||
|
"label": "Default value",
|
||||||
|
"key": "defaultValue"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "boolean",
|
||||||
|
"label": "Disabled",
|
||||||
|
"key": "disabled",
|
||||||
|
"defaultValue": false
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
"dataprovider": {
|
"dataprovider": {
|
||||||
"name": "Data Provider",
|
"name": "Data Provider",
|
||||||
"info": "Pagination is only available for data stored in tables.",
|
"info": "Pagination is only available for data stored in tables.",
|
||||||
|
@ -3200,6 +3300,16 @@
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"type": "static",
|
||||||
|
"suffix": "repeater",
|
||||||
|
"values": [
|
||||||
|
{
|
||||||
|
"label": "Row Index",
|
||||||
|
"key": "index"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"type": "schema",
|
"type": "schema",
|
||||||
"suffix": "repeater"
|
"suffix": "repeater"
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@budibase/client",
|
"name": "@budibase/client",
|
||||||
"version": "1.0.27-alpha.0",
|
"version": "1.0.27-alpha.2",
|
||||||
"license": "MPL-2.0",
|
"license": "MPL-2.0",
|
||||||
"module": "dist/budibase-client.js",
|
"module": "dist/budibase-client.js",
|
||||||
"main": "dist/budibase-client.js",
|
"main": "dist/budibase-client.js",
|
||||||
|
@ -19,9 +19,9 @@
|
||||||
"dev:builder": "rollup -cw"
|
"dev:builder": "rollup -cw"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@budibase/bbui": "^1.0.27-alpha.0",
|
"@budibase/bbui": "^1.0.27-alpha.2",
|
||||||
"@budibase/standard-components": "^0.9.139",
|
"@budibase/standard-components": "^0.9.139",
|
||||||
"@budibase/string-templates": "^1.0.27-alpha.0",
|
"@budibase/string-templates": "^1.0.27-alpha.2",
|
||||||
"regexparam": "^1.3.0",
|
"regexparam": "^1.3.0",
|
||||||
"shortid": "^2.2.15",
|
"shortid": "^2.2.15",
|
||||||
"svelte-spa-router": "^3.0.5"
|
"svelte-spa-router": "^3.0.5"
|
||||||
|
@ -35,6 +35,7 @@
|
||||||
"@spectrum-css/divider": "^1.0.3",
|
"@spectrum-css/divider": "^1.0.3",
|
||||||
"@spectrum-css/link": "^3.1.3",
|
"@spectrum-css/link": "^3.1.3",
|
||||||
"@spectrum-css/page": "^3.0.1",
|
"@spectrum-css/page": "^3.0.1",
|
||||||
|
"@spectrum-css/tag": "^3.1.4",
|
||||||
"@spectrum-css/typography": "^3.0.2",
|
"@spectrum-css/typography": "^3.0.2",
|
||||||
"@spectrum-css/vars": "^3.0.1",
|
"@spectrum-css/vars": "^3.0.1",
|
||||||
"apexcharts": "^3.22.1",
|
"apexcharts": "^3.22.1",
|
||||||
|
|
|
@ -4,6 +4,10 @@ import { fetchViewData } from "./views"
|
||||||
import { fetchRelationshipData } from "./relationships"
|
import { fetchRelationshipData } from "./relationships"
|
||||||
import { FieldTypes } from "../constants"
|
import { FieldTypes } from "../constants"
|
||||||
import { executeQuery, fetchQueryDefinition } from "./queries"
|
import { executeQuery, fetchQueryDefinition } from "./queries"
|
||||||
|
import {
|
||||||
|
convertJSONSchemaToTableSchema,
|
||||||
|
getJSONArrayDatasourceSchema,
|
||||||
|
} from "builder/src/builderStore/jsonUtils"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fetches all rows for a particular Budibase data source.
|
* Fetches all rows for a particular Budibase data source.
|
||||||
|
@ -55,16 +59,17 @@ export const fetchDatasourceSchema = async dataSource => {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
const { type } = dataSource
|
const { type } = dataSource
|
||||||
|
let schema
|
||||||
|
|
||||||
// Nested providers should already have exposed their own schema
|
// Nested providers should already have exposed their own schema
|
||||||
if (type === "provider") {
|
if (type === "provider") {
|
||||||
return dataSource.value?.schema
|
schema = dataSource.value?.schema
|
||||||
}
|
}
|
||||||
|
|
||||||
// Field sources have their schema statically defined
|
// Field sources have their schema statically defined
|
||||||
if (type === "field") {
|
if (type === "field") {
|
||||||
if (dataSource.fieldType === "attachment") {
|
if (dataSource.fieldType === "attachment") {
|
||||||
return {
|
schema = {
|
||||||
url: {
|
url: {
|
||||||
type: "string",
|
type: "string",
|
||||||
},
|
},
|
||||||
|
@ -73,7 +78,7 @@ export const fetchDatasourceSchema = async dataSource => {
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
} else if (dataSource.fieldType === "array") {
|
} else if (dataSource.fieldType === "array") {
|
||||||
return {
|
schema = {
|
||||||
value: {
|
value: {
|
||||||
type: "string",
|
type: "string",
|
||||||
},
|
},
|
||||||
|
@ -81,20 +86,48 @@ export const fetchDatasourceSchema = async dataSource => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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)
|
||||||
|
}
|
||||||
|
|
||||||
// Tables, views and links can be fetched by table ID
|
// Tables, views and links can be fetched by table ID
|
||||||
if (
|
if (
|
||||||
(type === "table" || type === "view" || type === "link") &&
|
(type === "table" || type === "view" || type === "link") &&
|
||||||
dataSource.tableId
|
dataSource.tableId
|
||||||
) {
|
) {
|
||||||
const table = await fetchTableDefinition(dataSource.tableId)
|
const table = await fetchTableDefinition(dataSource.tableId)
|
||||||
return table?.schema
|
schema = table?.schema
|
||||||
}
|
}
|
||||||
|
|
||||||
// Queries can be fetched by query ID
|
// Queries can be fetched by query ID
|
||||||
if (type === "query" && dataSource._id) {
|
if (type === "query" && dataSource._id) {
|
||||||
const definition = await fetchQueryDefinition(dataSource._id)
|
const definition = await fetchQueryDefinition(dataSource._id)
|
||||||
return definition?.schema
|
schema = definition?.schema
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Sanity check
|
||||||
|
if (!schema) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check for any JSON fields so we can add any top level properties
|
||||||
|
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 }
|
||||||
|
}
|
||||||
|
|
|
@ -196,9 +196,12 @@
|
||||||
// For providers referencing another provider, just use the rows it
|
// For providers referencing another provider, just use the rows it
|
||||||
// provides
|
// provides
|
||||||
allRows = dataSource?.value?.rows || []
|
allRows = dataSource?.value?.rows || []
|
||||||
} else if (dataSource?.type === "field") {
|
} else if (
|
||||||
// Field sources will be available from context.
|
dataSource?.type === "field" ||
|
||||||
// Enrich non object elements into object to ensure a valid schema.
|
dataSource?.type === "jsonarray"
|
||||||
|
) {
|
||||||
|
// These sources will be available directly from context.
|
||||||
|
// Enrich non object elements into objects to ensure a valid schema.
|
||||||
const data = dataSource?.value || []
|
const data = dataSource?.value || []
|
||||||
if (Array.isArray(data) && data[0] && typeof data[0] !== "object") {
|
if (Array.isArray(data) && data[0] && typeof data[0] !== "object") {
|
||||||
allRows = data.map(value => ({ value }))
|
allRows = data.map(value => ({ value }))
|
||||||
|
|
|
@ -21,8 +21,8 @@
|
||||||
{#if $component.empty}
|
{#if $component.empty}
|
||||||
<Placeholder />
|
<Placeholder />
|
||||||
{:else if rows.length > 0}
|
{:else if rows.length > 0}
|
||||||
{#each rows as row}
|
{#each rows as row, index}
|
||||||
<Provider data={row}>
|
<Provider data={{ ...row, index }}>
|
||||||
<slot />
|
<slot />
|
||||||
</Provider>
|
</Provider>
|
||||||
{/each}
|
{/each}
|
||||||
|
|
|
@ -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>
|
|
@ -73,7 +73,7 @@
|
||||||
enrichedFilter.push({
|
enrichedFilter.push({
|
||||||
field: column.name,
|
field: column.name,
|
||||||
operator: column.type === "string" ? "string" : "equal",
|
operator: column.type === "string" ? "string" : "equal",
|
||||||
type: "string",
|
type: column.type === "string" ? "string" : "number",
|
||||||
valueType: "Binding",
|
valueType: "Binding",
|
||||||
value: `{{ [${formId}].[${column.name}] }}`,
|
value: `{{ [${formId}].[${column.name}] }}`,
|
||||||
})
|
})
|
||||||
|
|
|
@ -61,7 +61,7 @@
|
||||||
enrichedFilter.push({
|
enrichedFilter.push({
|
||||||
field: column.name,
|
field: column.name,
|
||||||
operator: column.type === "string" ? "string" : "equal",
|
operator: column.type === "string" ? "string" : "equal",
|
||||||
type: "string",
|
type: column.type === "string" ? "string" : "number",
|
||||||
valueType: "Binding",
|
valueType: "Binding",
|
||||||
value: `{{ ${safe(formId)}.${safe(column.name)} }}`,
|
value: `{{ ${safe(formId)}.${safe(column.name)} }}`,
|
||||||
})
|
})
|
||||||
|
|
|
@ -19,7 +19,7 @@
|
||||||
export let schemaFields
|
export let schemaFields
|
||||||
export let filters = []
|
export let filters = []
|
||||||
|
|
||||||
const BannedTypes = ["link", "attachment", "formula"]
|
const BannedTypes = ["link", "attachment", "formula", "json"]
|
||||||
|
|
||||||
$: fieldOptions = (schemaFields ?? [])
|
$: fieldOptions = (schemaFields ?? [])
|
||||||
.filter(field => !BannedTypes.includes(field.type))
|
.filter(field => !BannedTypes.includes(field.type))
|
||||||
|
|
|
@ -3,6 +3,8 @@
|
||||||
import { derived, get, writable } from "svelte/store"
|
import { derived, get, writable } from "svelte/store"
|
||||||
import { createValidatorFromConstraints } from "./validation"
|
import { createValidatorFromConstraints } from "./validation"
|
||||||
import { generateID } from "utils/helpers"
|
import { generateID } from "utils/helpers"
|
||||||
|
import { deepGet, deepSet } from "@budibase/bbui"
|
||||||
|
import { cloneDeep } from "lodash/fp"
|
||||||
|
|
||||||
export let dataSource
|
export let dataSource
|
||||||
export let disabled = false
|
export let disabled = false
|
||||||
|
@ -49,6 +51,20 @@
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Derive value of whole form
|
||||||
|
$: formValue = deriveFormValue(initialValues, $values, $enrichments)
|
||||||
|
|
||||||
|
// Create data context to provide
|
||||||
|
$: dataContext = {
|
||||||
|
...formValue,
|
||||||
|
|
||||||
|
// These static values are prefixed to avoid clashes with actual columns
|
||||||
|
__value: formValue,
|
||||||
|
__valid: valid,
|
||||||
|
__currentStep: $currentStep,
|
||||||
|
__currentStepValid: $currentStepValid,
|
||||||
|
}
|
||||||
|
|
||||||
// Generates a derived store from an array of fields, comprised of a map of
|
// Generates a derived store from an array of fields, comprised of a map of
|
||||||
// extracted values from the field array
|
// extracted values from the field array
|
||||||
const deriveFieldProperty = (fieldStores, getProp) => {
|
const deriveFieldProperty = (fieldStores, getProp) => {
|
||||||
|
@ -78,6 +94,35 @@
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Derive the overall form value and deeply set all field paths so that we
|
||||||
|
// can support things like JSON fields.
|
||||||
|
const deriveFormValue = (initialValues, values, enrichments) => {
|
||||||
|
let formValue = cloneDeep(initialValues || {})
|
||||||
|
|
||||||
|
// We need to sort the keys to avoid a JSON field overwriting a nested field
|
||||||
|
const sortedFields = Object.entries(values || {})
|
||||||
|
.map(([key, value]) => {
|
||||||
|
const field = getField(key)
|
||||||
|
return {
|
||||||
|
key,
|
||||||
|
value,
|
||||||
|
lastUpdate: get(field).fieldState?.lastUpdate || 0,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.sort((a, b) => {
|
||||||
|
return a.lastUpdate > b.lastUpdate
|
||||||
|
})
|
||||||
|
|
||||||
|
// Merge all values and enrichments into a single value
|
||||||
|
sortedFields.forEach(({ key, value }) => {
|
||||||
|
deepSet(formValue, key, value)
|
||||||
|
})
|
||||||
|
Object.entries(enrichments || {}).forEach(([key, value]) => {
|
||||||
|
deepSet(formValue, key, value)
|
||||||
|
})
|
||||||
|
return formValue
|
||||||
|
}
|
||||||
|
|
||||||
// Searches the field array for a certain field
|
// Searches the field array for a certain field
|
||||||
const getField = name => {
|
const getField = name => {
|
||||||
return fields.find(field => get(field).name === name)
|
return fields.find(field => get(field).name === name)
|
||||||
|
@ -97,13 +142,20 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
// If we've already registered this field then keep some existing state
|
// If we've already registered this field then keep some existing state
|
||||||
let initialValue = initialValues[field] ?? defaultValue
|
let initialValue = deepGet(initialValues, field) ?? defaultValue
|
||||||
let fieldId = `id-${generateID()}`
|
let fieldId = `id-${generateID()}`
|
||||||
const existingField = getField(field)
|
const existingField = getField(field)
|
||||||
if (existingField) {
|
if (existingField) {
|
||||||
const { fieldState } = get(existingField)
|
const { fieldState } = get(existingField)
|
||||||
initialValue = fieldState.value ?? initialValue
|
|
||||||
fieldId = fieldState.fieldId
|
fieldId = fieldState.fieldId
|
||||||
|
|
||||||
|
// Use new default value if default value changed,
|
||||||
|
// otherwise use the current value if possible
|
||||||
|
if (defaultValue !== fieldState.defaultValue) {
|
||||||
|
initialValue = defaultValue
|
||||||
|
} else {
|
||||||
|
initialValue = fieldState.value ?? initialValue
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Auto columns are always disabled
|
// Auto columns are always disabled
|
||||||
|
@ -130,6 +182,7 @@
|
||||||
disabled: disabled || fieldDisabled || isAutoColumn,
|
disabled: disabled || fieldDisabled || isAutoColumn,
|
||||||
defaultValue,
|
defaultValue,
|
||||||
validator,
|
validator,
|
||||||
|
lastUpdate: Date.now(),
|
||||||
},
|
},
|
||||||
fieldApi: makeFieldApi(field, defaultValue),
|
fieldApi: makeFieldApi(field, defaultValue),
|
||||||
fieldSchema: schema?.[field] ?? {},
|
fieldSchema: schema?.[field] ?? {},
|
||||||
|
@ -204,6 +257,7 @@
|
||||||
fieldInfo.update(state => {
|
fieldInfo.update(state => {
|
||||||
state.fieldState.value = value
|
state.fieldState.value = value
|
||||||
state.fieldState.error = error
|
state.fieldState.error = error
|
||||||
|
state.fieldState.lastUpdate = Date.now()
|
||||||
return state
|
return state
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -220,6 +274,7 @@
|
||||||
fieldInfo.update(state => {
|
fieldInfo.update(state => {
|
||||||
state.fieldState.value = newValue
|
state.fieldState.value = newValue
|
||||||
state.fieldState.error = null
|
state.fieldState.error = null
|
||||||
|
state.fieldState.lastUpdate = Date.now()
|
||||||
return state
|
return state
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -299,18 +354,6 @@
|
||||||
{ type: ActionTypes.ClearForm, callback: formApi.clear },
|
{ type: ActionTypes.ClearForm, callback: formApi.clear },
|
||||||
{ type: ActionTypes.ChangeFormStep, callback: formApi.changeStep },
|
{ type: ActionTypes.ChangeFormStep, callback: formApi.changeStep },
|
||||||
]
|
]
|
||||||
|
|
||||||
// Create data context to provide
|
|
||||||
$: dataContext = {
|
|
||||||
...initialValues,
|
|
||||||
...$values,
|
|
||||||
...$enrichments,
|
|
||||||
|
|
||||||
// These static values are prefixed to avoid clashes with actual columns
|
|
||||||
__valid: valid,
|
|
||||||
__currentStep: $currentStep,
|
|
||||||
__currentStepValid: $currentStepValid,
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Provider {actions} data={dataContext}>
|
<Provider {actions} data={dataContext}>
|
||||||
|
|
|
@ -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>
|
<script>
|
||||||
import { CoreTextArea } from "@budibase/bbui"
|
import { CoreTextArea } from "@budibase/bbui"
|
||||||
import Field from "./Field.svelte"
|
import Field from "./Field.svelte"
|
||||||
|
import { getContext } from "svelte"
|
||||||
|
|
||||||
export let field
|
export let field
|
||||||
export let label
|
export let label
|
||||||
|
@ -11,6 +12,9 @@
|
||||||
|
|
||||||
let fieldState
|
let fieldState
|
||||||
let fieldApi
|
let fieldApi
|
||||||
|
|
||||||
|
const component = getContext("component")
|
||||||
|
$: height = $component.styles?.normal?.height || "124px"
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Field
|
<Field
|
||||||
|
@ -24,6 +28,7 @@
|
||||||
bind:fieldApi
|
bind:fieldApi
|
||||||
>
|
>
|
||||||
{#if fieldState}
|
{#if fieldState}
|
||||||
|
<div style="--height: {height};">
|
||||||
<CoreTextArea
|
<CoreTextArea
|
||||||
value={fieldState.value}
|
value={fieldState.value}
|
||||||
on:change={e => fieldApi.setValue(e.detail)}
|
on:change={e => fieldApi.setValue(e.detail)}
|
||||||
|
@ -32,5 +37,17 @@
|
||||||
id={fieldState.fieldId}
|
id={fieldState.fieldId}
|
||||||
{placeholder}
|
{placeholder}
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</Field>
|
</Field>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
:global(.spectrum-Form-itemField .spectrum-Textfield--multiline) {
|
||||||
|
min-height: calc(var(--height) - 24px);
|
||||||
|
}
|
||||||
|
:global(.spectrum-Form--labelsAbove
|
||||||
|
.spectrum-Form-itemField
|
||||||
|
.spectrum-Textfield--multiline) {
|
||||||
|
min-height: calc(var(--height) - 24px);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
|
@ -11,3 +11,4 @@ export { default as attachmentfield } from "./AttachmentField.svelte"
|
||||||
export { default as relationshipfield } from "./RelationshipField.svelte"
|
export { default as relationshipfield } from "./RelationshipField.svelte"
|
||||||
export { default as passwordfield } from "./PasswordField.svelte"
|
export { default as passwordfield } from "./PasswordField.svelte"
|
||||||
export { default as formstep } from "./FormStep.svelte"
|
export { default as formstep } from "./FormStep.svelte"
|
||||||
|
export { default as jsonfield } from "./JSONField.svelte"
|
||||||
|
|
|
@ -206,6 +206,7 @@ const parseType = (value, type) => {
|
||||||
return value
|
return value
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Parse array, treating no elements as null
|
||||||
if (type === FieldTypes.ARRAY) {
|
if (type === FieldTypes.ARRAY) {
|
||||||
if (!Array.isArray(value) || !value.length) {
|
if (!Array.isArray(value) || !value.length) {
|
||||||
return null
|
return null
|
||||||
|
@ -213,6 +214,12 @@ const parseType = (value, type) => {
|
||||||
return value
|
return value
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// For JSON we don't touch the value at all as we want to verify it in its
|
||||||
|
// raw form
|
||||||
|
if (type === FieldTypes.JSON) {
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
||||||
// If some unknown type, treat as null to avoid breaking validators
|
// If some unknown type, treat as null to avoid breaking validators
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
@ -290,6 +297,19 @@ const notContainsHandler = (value, rule) => {
|
||||||
return !containsHandler(value, rule)
|
return !containsHandler(value, rule)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Evaluates a constraint that the value must be a valid json object
|
||||||
|
const jsonHandler = value => {
|
||||||
|
if (typeof value !== "object" || Array.isArray(value)) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
JSON.parse(JSON.stringify(value))
|
||||||
|
return true
|
||||||
|
} catch (error) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Map of constraint types to handlers.
|
* Map of constraint types to handlers.
|
||||||
*/
|
*/
|
||||||
|
@ -306,6 +326,7 @@ const handlerMap = {
|
||||||
notRegex: notRegexHandler,
|
notRegex: notRegexHandler,
|
||||||
contains: containsHandler,
|
contains: containsHandler,
|
||||||
notContains: notContainsHandler,
|
notContains: notContainsHandler,
|
||||||
|
json: jsonHandler,
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -29,6 +29,7 @@ export { default as backgroundimage } from "./BackgroundImage.svelte"
|
||||||
export { default as daterangepicker } from "./DateRangePicker.svelte"
|
export { default as daterangepicker } from "./DateRangePicker.svelte"
|
||||||
export { default as cardstat } from "./CardStat.svelte"
|
export { default as cardstat } from "./CardStat.svelte"
|
||||||
export { default as spectrumcard } from "./SpectrumCard.svelte"
|
export { default as spectrumcard } from "./SpectrumCard.svelte"
|
||||||
|
export { default as tag } from "./Tag.svelte"
|
||||||
export * from "./charts"
|
export * from "./charts"
|
||||||
export * from "./forms"
|
export * from "./forms"
|
||||||
export * from "./table"
|
export * from "./table"
|
||||||
|
|
|
@ -24,6 +24,7 @@
|
||||||
class:flipped
|
class:flipped
|
||||||
class:line
|
class:line
|
||||||
style="top: {top}px; left: {left}px; width: {width}px; height: {height}px; --color: {color}; --zIndex: {zIndex};"
|
style="top: {top}px; left: {left}px; width: {width}px; height: {height}px; --color: {color}; --zIndex: {zIndex};"
|
||||||
|
class:withText={!!text}
|
||||||
>
|
>
|
||||||
{#if text}
|
{#if text}
|
||||||
<div class="text" class:flipped class:line class:right={alignRight}>
|
<div class="text" class:flipped class:line class:right={alignRight}>
|
||||||
|
@ -39,12 +40,12 @@
|
||||||
z-index: var(--zIndex);
|
z-index: var(--zIndex);
|
||||||
border: 2px solid var(--color);
|
border: 2px solid var(--color);
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
border-top-right-radius: 4px;
|
border-radius: 4px;
|
||||||
border-top-left-radius: 0;
|
|
||||||
border-bottom-left-radius: 4px;
|
|
||||||
border-bottom-right-radius: 4px;
|
|
||||||
}
|
}
|
||||||
.indicator.flipped {
|
.indicator.withText {
|
||||||
|
border-top-left-radius: 0;
|
||||||
|
}
|
||||||
|
.indicator.withText.flipped {
|
||||||
border-top-left-radius: 4px;
|
border-top-left-radius: 4px;
|
||||||
}
|
}
|
||||||
.indicator.line {
|
.indicator.line {
|
||||||
|
@ -74,8 +75,7 @@
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
}
|
}
|
||||||
.text.flipped {
|
.text.flipped {
|
||||||
border-top-left-radius: 4px;
|
border-radius: 4px;
|
||||||
border-bottom-left-radius: 4px;
|
|
||||||
transform: translateY(0%);
|
transform: translateY(0%);
|
||||||
top: -2px;
|
top: -2px;
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,6 +13,7 @@ export const FieldTypes = {
|
||||||
ATTACHMENT: "attachment",
|
ATTACHMENT: "attachment",
|
||||||
LINK: "link",
|
LINK: "link",
|
||||||
FORMULA: "formula",
|
FORMULA: "formula",
|
||||||
|
JSON: "json",
|
||||||
}
|
}
|
||||||
|
|
||||||
export const UnsortableTypes = [
|
export const UnsortableTypes = [
|
||||||
|
|
|
@ -61,7 +61,8 @@ export const createDataSourceStore = () => {
|
||||||
|
|
||||||
// Emit this as a window event, so parent screens which are iframing us in
|
// Emit this as a window event, so parent screens which are iframing us in
|
||||||
// can also invalidate the same datasource
|
// can also invalidate the same datasource
|
||||||
if (get(routeStore).queryParams?.peek) {
|
const inModal = get(routeStore).queryParams?.peek
|
||||||
|
if (inModal) {
|
||||||
window.parent.postMessage({
|
window.parent.postMessage({
|
||||||
type: "invalidate-datasource",
|
type: "invalidate-datasource",
|
||||||
detail: { dataSourceId },
|
detail: { dataSourceId },
|
||||||
|
|
|
@ -8,20 +8,49 @@ import {
|
||||||
} from "stores"
|
} from "stores"
|
||||||
import { saveRow, deleteRow, executeQuery, triggerAutomation } from "api"
|
import { saveRow, deleteRow, executeQuery, triggerAutomation } from "api"
|
||||||
import { ActionTypes } from "constants"
|
import { ActionTypes } from "constants"
|
||||||
|
import { enrichDataBindings } from "./enrichDataBinding"
|
||||||
|
import { deepSet } from "@budibase/bbui"
|
||||||
|
|
||||||
const saveRowHandler = async (action, context) => {
|
const saveRowHandler = async (action, context) => {
|
||||||
const { fields, providerId, tableId } = action.parameters
|
const { fields, providerId, tableId } = action.parameters
|
||||||
|
let payload
|
||||||
if (providerId) {
|
if (providerId) {
|
||||||
let draft = context[providerId]
|
payload = { ...context[providerId] }
|
||||||
|
} else {
|
||||||
|
payload = {}
|
||||||
|
}
|
||||||
if (fields) {
|
if (fields) {
|
||||||
for (let [field, value] of Object.entries(fields)) {
|
for (let [field, value] of Object.entries(fields)) {
|
||||||
draft[field] = value
|
deepSet(payload, field, value)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (tableId) {
|
if (tableId) {
|
||||||
draft.tableId = tableId
|
payload.tableId = tableId
|
||||||
|
}
|
||||||
|
const row = await saveRow(payload)
|
||||||
|
return {
|
||||||
|
row,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const duplicateRowHandler = async (action, context) => {
|
||||||
|
const { fields, providerId, tableId } = action.parameters
|
||||||
|
if (providerId) {
|
||||||
|
let payload = { ...context[providerId] }
|
||||||
|
if (fields) {
|
||||||
|
for (let [field, value] of Object.entries(fields)) {
|
||||||
|
deepSet(payload, field, value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (tableId) {
|
||||||
|
payload.tableId = tableId
|
||||||
|
}
|
||||||
|
delete payload._id
|
||||||
|
delete payload._rev
|
||||||
|
const row = await saveRow(payload)
|
||||||
|
return {
|
||||||
|
row,
|
||||||
}
|
}
|
||||||
await saveRow(draft)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -46,11 +75,12 @@ const navigationHandler = action => {
|
||||||
|
|
||||||
const queryExecutionHandler = async action => {
|
const queryExecutionHandler = async action => {
|
||||||
const { datasourceId, queryId, queryParams } = action.parameters
|
const { datasourceId, queryId, queryParams } = action.parameters
|
||||||
await executeQuery({
|
const result = await executeQuery({
|
||||||
datasourceId,
|
datasourceId,
|
||||||
queryId,
|
queryId,
|
||||||
parameters: queryParams,
|
parameters: queryParams,
|
||||||
})
|
})
|
||||||
|
return { result }
|
||||||
}
|
}
|
||||||
|
|
||||||
const executeActionHandler = async (
|
const executeActionHandler = async (
|
||||||
|
@ -129,6 +159,7 @@ const updateStateHandler = action => {
|
||||||
|
|
||||||
const handlerMap = {
|
const handlerMap = {
|
||||||
["Save Row"]: saveRowHandler,
|
["Save Row"]: saveRowHandler,
|
||||||
|
["Duplicate Row"]: duplicateRowHandler,
|
||||||
["Delete Row"]: deleteRowHandler,
|
["Delete Row"]: deleteRowHandler,
|
||||||
["Navigate To"]: navigationHandler,
|
["Navigate To"]: navigationHandler,
|
||||||
["Execute Query"]: queryExecutionHandler,
|
["Execute Query"]: queryExecutionHandler,
|
||||||
|
@ -165,12 +196,27 @@ export const enrichButtonActions = (actions, context) => {
|
||||||
return actions
|
return actions
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Button context is built up as actions are executed.
|
||||||
|
// Inherit any previous button context which may have come from actions
|
||||||
|
// before a confirmable action since this breaks the chain.
|
||||||
|
let buttonContext = context.actions || []
|
||||||
|
|
||||||
const handlers = actions.map(def => handlerMap[def["##eventHandlerType"]])
|
const handlers = actions.map(def => handlerMap[def["##eventHandlerType"]])
|
||||||
return async () => {
|
return async () => {
|
||||||
for (let i = 0; i < handlers.length; i++) {
|
for (let i = 0; i < handlers.length; i++) {
|
||||||
try {
|
try {
|
||||||
const action = actions[i]
|
// Skip any non-existent action definitions
|
||||||
const callback = async () => handlers[i](action, context)
|
if (!handlers[i]) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Built total context for this action
|
||||||
|
const totalContext = { ...context, actions: buttonContext }
|
||||||
|
|
||||||
|
// Get and enrich this button action with the total context
|
||||||
|
let action = actions[i]
|
||||||
|
action = enrichDataBindings(action, totalContext)
|
||||||
|
const callback = async () => handlers[i](action, totalContext)
|
||||||
|
|
||||||
// If this action is confirmable, show confirmation and await a
|
// If this action is confirmable, show confirmation and await a
|
||||||
// callback to execute further actions
|
// callback to execute further actions
|
||||||
|
@ -185,7 +231,15 @@ export const enrichButtonActions = (actions, context) => {
|
||||||
// then execute the rest of the actions in the chain
|
// then execute the rest of the actions in the chain
|
||||||
const result = await callback()
|
const result = await callback()
|
||||||
if (result !== false) {
|
if (result !== false) {
|
||||||
const next = enrichButtonActions(actions.slice(i + 1), context)
|
// Generate a new total context to pass into the next enrichment
|
||||||
|
buttonContext.push(result)
|
||||||
|
const newContext = { ...context, actions: buttonContext }
|
||||||
|
|
||||||
|
// Enrich and call the next button action
|
||||||
|
const next = enrichButtonActions(
|
||||||
|
actions.slice(i + 1),
|
||||||
|
newContext
|
||||||
|
)
|
||||||
await next()
|
await next()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -201,6 +255,8 @@ export const enrichButtonActions = (actions, context) => {
|
||||||
const result = await callback()
|
const result = await callback()
|
||||||
if (result === false) {
|
if (result === false) {
|
||||||
return
|
return
|
||||||
|
} else {
|
||||||
|
buttonContext.push(result)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|
|
@ -32,35 +32,56 @@ export const enrichProps = (props, context) => {
|
||||||
data: context[context.closestComponentId],
|
data: context[context.closestComponentId],
|
||||||
}
|
}
|
||||||
|
|
||||||
// Enrich all data bindings in top level props
|
// We want to exclude any button actions from enrichment at this stage.
|
||||||
let enrichedProps = enrichDataBindings(props, totalContext)
|
// Extract top level button action settings.
|
||||||
|
let normalProps = { ...props }
|
||||||
// Enrich click actions if they exist
|
let actionProps = {}
|
||||||
Object.keys(enrichedProps).forEach(prop => {
|
Object.keys(normalProps).forEach(prop => {
|
||||||
if (prop?.toLowerCase().includes("onclick")) {
|
if (prop?.toLowerCase().includes("onclick")) {
|
||||||
enrichedProps[prop] = enrichButtonActions(
|
actionProps[prop] = normalProps[prop]
|
||||||
enrichedProps[prop],
|
delete normalProps[prop]
|
||||||
totalContext
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
// Enrich any click actions in conditions
|
// Handle conditional UI separately after normal settings
|
||||||
if (enrichedProps._conditions) {
|
let conditions = normalProps._conditions
|
||||||
enrichedProps._conditions.forEach(condition => {
|
delete normalProps._conditions
|
||||||
|
|
||||||
|
// Enrich all props except button actions
|
||||||
|
let enrichedProps = enrichDataBindings(normalProps, totalContext)
|
||||||
|
|
||||||
|
// Enrich button actions.
|
||||||
|
// Actions are enriched into a function at this stage, but actual data
|
||||||
|
// binding enrichment is done dynamically at runtime.
|
||||||
|
Object.keys(actionProps).forEach(prop => {
|
||||||
|
enrichedProps[prop] = enrichButtonActions(actionProps[prop], totalContext)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Conditions
|
||||||
|
if (conditions?.length) {
|
||||||
|
let enrichedConditions = []
|
||||||
|
conditions.forEach(condition => {
|
||||||
if (condition.setting?.toLowerCase().includes("onclick")) {
|
if (condition.setting?.toLowerCase().includes("onclick")) {
|
||||||
condition.settingValue = enrichButtonActions(
|
// Copy and remove the setting value from the condition as it needs
|
||||||
|
// enriched separately
|
||||||
|
let toEnrich = { ...condition }
|
||||||
|
delete toEnrich.settingValue
|
||||||
|
|
||||||
|
// Join the condition back together
|
||||||
|
enrichedConditions.push({
|
||||||
|
...enrichDataBindings(toEnrich, totalContext),
|
||||||
|
settingValue: enrichButtonActions(
|
||||||
condition.settingValue,
|
condition.settingValue,
|
||||||
totalContext
|
totalContext
|
||||||
)
|
),
|
||||||
|
rand: Math.random(),
|
||||||
// If there is an onclick function in here then it won't be serialised
|
})
|
||||||
// properly, and therefore will not be updated properly.
|
} else {
|
||||||
// The solution to this is add a rand which will ensure diffs happen
|
// Normal condition
|
||||||
// every time.
|
enrichedConditions.push(enrichDataBindings(condition, totalContext))
|
||||||
condition.rand = Math.random()
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
enrichedProps._conditions = enrichedConditions
|
||||||
}
|
}
|
||||||
|
|
||||||
return enrichedProps
|
return enrichedProps
|
||||||
|
|
|
@ -305,6 +305,11 @@
|
||||||
resolved "https://registry.yarnpkg.com/@spectrum-css/tabs/-/tabs-3.1.5.tgz#cc82e69c1fc721902345178231fb95d05938b983"
|
resolved "https://registry.yarnpkg.com/@spectrum-css/tabs/-/tabs-3.1.5.tgz#cc82e69c1fc721902345178231fb95d05938b983"
|
||||||
integrity sha512-UtfW8bA1quYnJM6v/lp6AVYGnQFkiUix2FHAf/4VHVrk4mh7ydtLiXS0IR3Kx+t/S8FWdSdSQHDZ8tHbY1ZLZg==
|
integrity sha512-UtfW8bA1quYnJM6v/lp6AVYGnQFkiUix2FHAf/4VHVrk4mh7ydtLiXS0IR3Kx+t/S8FWdSdSQHDZ8tHbY1ZLZg==
|
||||||
|
|
||||||
|
"@spectrum-css/tag@^3.1.4":
|
||||||
|
version "3.1.4"
|
||||||
|
resolved "https://registry.yarnpkg.com/@spectrum-css/tag/-/tag-3.1.4.tgz#334384dd789ddf0562679cae62ef763883480ac5"
|
||||||
|
integrity sha512-9dYBMhCEkjy+p75XJIfCA2/zU4JAqsJrL7fkYIDXakS6/BzeVtIvAW/6JaIHtLIA9lrj0Sn4m+ZjceKnZNIv1w==
|
||||||
|
|
||||||
"@spectrum-css/tags@^3.0.2":
|
"@spectrum-css/tags@^3.0.2":
|
||||||
version "3.0.3"
|
version "3.0.3"
|
||||||
resolved "https://registry.yarnpkg.com/@spectrum-css/tags/-/tags-3.0.3.tgz#fc76d2735cdc442de91b7eb3bee49a928c0767ac"
|
resolved "https://registry.yarnpkg.com/@spectrum-css/tags/-/tags-3.0.3.tgz#fc76d2735cdc442de91b7eb3bee49a928c0767ac"
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"name": "@budibase/server",
|
"name": "@budibase/server",
|
||||||
"email": "hi@budibase.com",
|
"email": "hi@budibase.com",
|
||||||
"version": "1.0.27-alpha.0",
|
"version": "1.0.27-alpha.2",
|
||||||
"description": "Budibase Web Server",
|
"description": "Budibase Web Server",
|
||||||
"main": "src/index.ts",
|
"main": "src/index.ts",
|
||||||
"repository": {
|
"repository": {
|
||||||
|
@ -70,9 +70,9 @@
|
||||||
"license": "GPL-3.0",
|
"license": "GPL-3.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@apidevtools/swagger-parser": "^10.0.3",
|
"@apidevtools/swagger-parser": "^10.0.3",
|
||||||
"@budibase/auth": "^1.0.27-alpha.0",
|
"@budibase/auth": "^1.0.27-alpha.2",
|
||||||
"@budibase/client": "^1.0.27-alpha.0",
|
"@budibase/client": "^1.0.27-alpha.2",
|
||||||
"@budibase/string-templates": "^1.0.27-alpha.0",
|
"@budibase/string-templates": "^1.0.27-alpha.2",
|
||||||
"@bull-board/api": "^3.7.0",
|
"@bull-board/api": "^3.7.0",
|
||||||
"@bull-board/koa": "^3.7.0",
|
"@bull-board/koa": "^3.7.0",
|
||||||
"@elastic/elasticsearch": "7.10.0",
|
"@elastic/elasticsearch": "7.10.0",
|
||||||
|
|
|
@ -191,7 +191,8 @@ class QueryBuilder {
|
||||||
}
|
}
|
||||||
if (this.query.equal) {
|
if (this.query.equal) {
|
||||||
build(this.query.equal, (key, value) => {
|
build(this.query.equal, (key, value) => {
|
||||||
if (!value) {
|
// 0 evaluates to false, which means we would return all rows if we don't check it
|
||||||
|
if (!value && value !== 0) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
return `${key}:${builder.preprocess(value, allPreProcessingOpts)}`
|
return `${key}:${builder.preprocess(value, allPreProcessingOpts)}`
|
||||||
|
|
|
@ -50,10 +50,10 @@ exports.validate = async ({ appId, tableId, row, table }) => {
|
||||||
const errors = {}
|
const errors = {}
|
||||||
for (let fieldName of Object.keys(table.schema)) {
|
for (let fieldName of Object.keys(table.schema)) {
|
||||||
const constraints = cloneDeep(table.schema[fieldName].constraints)
|
const constraints = cloneDeep(table.schema[fieldName].constraints)
|
||||||
|
const type = table.schema[fieldName].type
|
||||||
// special case for options, need to always allow unselected (null)
|
// special case for options, need to always allow unselected (null)
|
||||||
if (
|
if (
|
||||||
table.schema[fieldName].type ===
|
(type === FieldTypes.OPTIONS || type === FieldTypes.ARRAY) &&
|
||||||
(FieldTypes.OPTIONS || FieldTypes.ARRAY) &&
|
|
||||||
constraints.inclusion
|
constraints.inclusion
|
||||||
) {
|
) {
|
||||||
constraints.inclusion.push(null)
|
constraints.inclusion.push(null)
|
||||||
|
@ -61,17 +61,20 @@ exports.validate = async ({ appId, tableId, row, table }) => {
|
||||||
let res
|
let res
|
||||||
|
|
||||||
// Validate.js doesn't seem to handle array
|
// Validate.js doesn't seem to handle array
|
||||||
if (
|
if (type === FieldTypes.ARRAY && row[fieldName] && row[fieldName].length) {
|
||||||
table.schema[fieldName].type === FieldTypes.ARRAY &&
|
|
||||||
row[fieldName] &&
|
|
||||||
row[fieldName].length
|
|
||||||
) {
|
|
||||||
row[fieldName].map(val => {
|
row[fieldName].map(val => {
|
||||||
if (!constraints.inclusion.includes(val)) {
|
if (!constraints.inclusion.includes(val)) {
|
||||||
errors[fieldName] = "Field not in list"
|
errors[fieldName] = "Field not in list"
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
} else if (table.schema[fieldName].type === FieldTypes.FORMULA) {
|
} else if (type === FieldTypes.JSON && typeof row[fieldName] === "string") {
|
||||||
|
// this should only happen if there is an error
|
||||||
|
try {
|
||||||
|
JSON.parse(row[fieldName])
|
||||||
|
} catch (err) {
|
||||||
|
errors[fieldName] = [`Contains invalid JSON`]
|
||||||
|
}
|
||||||
|
} else if (type === FieldTypes.FORMULA) {
|
||||||
res = validateJs.single(
|
res = validateJs.single(
|
||||||
processStringSync(table.schema[fieldName].formula, row),
|
processStringSync(table.schema[fieldName].formula, row),
|
||||||
constraints
|
constraints
|
||||||
|
|
|
@ -81,6 +81,18 @@ const TYPE_TRANSFORM_MAP = {
|
||||||
[FieldTypes.AUTO]: {
|
[FieldTypes.AUTO]: {
|
||||||
parse: () => undefined,
|
parse: () => undefined,
|
||||||
},
|
},
|
||||||
|
[FieldTypes.JSON]: {
|
||||||
|
parse: input => {
|
||||||
|
try {
|
||||||
|
if (input === "") {
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
return JSON.parse(input)
|
||||||
|
} catch (err) {
|
||||||
|
return input
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@budibase/string-templates",
|
"name": "@budibase/string-templates",
|
||||||
"version": "1.0.27-alpha.0",
|
"version": "1.0.27-alpha.2",
|
||||||
"description": "Handlebars wrapper for Budibase templating.",
|
"description": "Handlebars wrapper for Budibase templating.",
|
||||||
"main": "src/index.cjs",
|
"main": "src/index.cjs",
|
||||||
"module": "dist/bundle.mjs",
|
"module": "dist/bundle.mjs",
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
const { atob } = require("../utilities")
|
const { atob } = require("../utilities")
|
||||||
|
const { cloneDeep } = require("lodash/fp")
|
||||||
|
|
||||||
// The method of executing JS scripts depends on the bundle being built.
|
// The method of executing JS scripts depends on the bundle being built.
|
||||||
// This setter is used in the entrypoint (either index.cjs or index.mjs).
|
// This setter is used in the entrypoint (either index.cjs or index.mjs).
|
||||||
|
@ -38,8 +39,12 @@ module.exports.processJS = (handlebars, context) => {
|
||||||
// This is required to allow the final `return` statement to be valid.
|
// This is required to allow the final `return` statement to be valid.
|
||||||
const js = `function run(){${atob(handlebars)}};run();`
|
const js = `function run(){${atob(handlebars)}};run();`
|
||||||
|
|
||||||
// Our $ context function gets a value from context
|
// Our $ context function gets a value from context.
|
||||||
const sandboxContext = { $: path => getContextValue(path, context) }
|
// We clone the context to avoid mutation in the binding affecting real
|
||||||
|
// app context.
|
||||||
|
const sandboxContext = {
|
||||||
|
$: path => getContextValue(path, cloneDeep(context)),
|
||||||
|
}
|
||||||
|
|
||||||
// Create a sandbox with out context and run the JS
|
// Create a sandbox with out context and run the JS
|
||||||
return runJS(js, sandboxContext)
|
return runJS(js, sandboxContext)
|
||||||
|
|
|
@ -1,12 +1,13 @@
|
||||||
const handlebars = require("handlebars")
|
const handlebars = require("handlebars")
|
||||||
const { registerAll } = require("./helpers/index")
|
const { registerAll } = require("./helpers/index")
|
||||||
const processors = require("./processors")
|
const processors = require("./processors")
|
||||||
const { removeHandlebarsStatements, atob, btoa } = require("./utilities")
|
const { atob, btoa } = require("./utilities")
|
||||||
const manifest = require("../manifest.json")
|
const manifest = require("../manifest.json")
|
||||||
|
|
||||||
const hbsInstance = handlebars.create()
|
const hbsInstance = handlebars.create()
|
||||||
registerAll(hbsInstance)
|
registerAll(hbsInstance)
|
||||||
const hbsInstanceNoHelpers = handlebars.create()
|
const hbsInstanceNoHelpers = handlebars.create()
|
||||||
|
const defaultOpts = { noHelpers: false }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* utility function to check if the object is valid
|
* utility function to check if the object is valid
|
||||||
|
@ -28,11 +29,7 @@ function testObject(object) {
|
||||||
* @param {object} opts optional - specify some options for processing.
|
* @param {object} opts optional - specify some options for processing.
|
||||||
* @returns {Promise<object|array>} The structure input, as fully updated as possible.
|
* @returns {Promise<object|array>} The structure input, as fully updated as possible.
|
||||||
*/
|
*/
|
||||||
module.exports.processObject = async (
|
module.exports.processObject = async (object, context, opts) => {
|
||||||
object,
|
|
||||||
context,
|
|
||||||
opts = { noHelpers: false }
|
|
||||||
) => {
|
|
||||||
testObject(object)
|
testObject(object)
|
||||||
for (let key of Object.keys(object || {})) {
|
for (let key of Object.keys(object || {})) {
|
||||||
if (object[key] != null) {
|
if (object[key] != null) {
|
||||||
|
@ -63,11 +60,7 @@ module.exports.processObject = async (
|
||||||
* @param {object} opts optional - specify some options for processing.
|
* @param {object} opts optional - specify some options for processing.
|
||||||
* @returns {Promise<string>} The enriched string, all templates should have been replaced if they can be.
|
* @returns {Promise<string>} The enriched string, all templates should have been replaced if they can be.
|
||||||
*/
|
*/
|
||||||
module.exports.processString = async (
|
module.exports.processString = async (string, context, opts) => {
|
||||||
string,
|
|
||||||
context,
|
|
||||||
opts = { noHelpers: false }
|
|
||||||
) => {
|
|
||||||
// TODO: carry out any async calls before carrying out async call
|
// TODO: carry out any async calls before carrying out async call
|
||||||
return module.exports.processStringSync(string, context, opts)
|
return module.exports.processStringSync(string, context, opts)
|
||||||
}
|
}
|
||||||
|
@ -81,11 +74,7 @@ module.exports.processString = async (
|
||||||
* @param {object} opts optional - specify some options for processing.
|
* @param {object} opts optional - specify some options for processing.
|
||||||
* @returns {object|array} The structure input, as fully updated as possible.
|
* @returns {object|array} The structure input, as fully updated as possible.
|
||||||
*/
|
*/
|
||||||
module.exports.processObjectSync = (
|
module.exports.processObjectSync = (object, context, opts) => {
|
||||||
object,
|
|
||||||
context,
|
|
||||||
opts = { noHelpers: false }
|
|
||||||
) => {
|
|
||||||
testObject(object)
|
testObject(object)
|
||||||
for (let key of Object.keys(object || {})) {
|
for (let key of Object.keys(object || {})) {
|
||||||
let val = object[key]
|
let val = object[key]
|
||||||
|
@ -106,26 +95,20 @@ module.exports.processObjectSync = (
|
||||||
* @param {object} opts optional - specify some options for processing.
|
* @param {object} opts optional - specify some options for processing.
|
||||||
* @returns {string} The enriched string, all templates should have been replaced if they can be.
|
* @returns {string} The enriched string, all templates should have been replaced if they can be.
|
||||||
*/
|
*/
|
||||||
module.exports.processStringSync = (
|
module.exports.processStringSync = (string, context, opts) => {
|
||||||
string,
|
opts = { ...defaultOpts, ...opts }
|
||||||
context,
|
|
||||||
opts = { noHelpers: false }
|
// take a copy of input in case of error
|
||||||
) => {
|
|
||||||
if (!exports.isValid(string)) {
|
|
||||||
return string
|
|
||||||
}
|
|
||||||
// take a copy of input incase error
|
|
||||||
const input = string
|
const input = string
|
||||||
if (typeof string !== "string") {
|
if (typeof string !== "string") {
|
||||||
throw "Cannot process non-string types."
|
throw "Cannot process non-string types."
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
const noHelpers = opts && opts.noHelpers
|
|
||||||
// finalising adds a helper, can't do this with no helpers
|
// finalising adds a helper, can't do this with no helpers
|
||||||
const shouldFinalise = !noHelpers
|
const shouldFinalise = !opts.noHelpers
|
||||||
string = processors.preprocess(string, shouldFinalise)
|
string = processors.preprocess(string, shouldFinalise)
|
||||||
// this does not throw an error when template can't be fulfilled, have to try correct beforehand
|
// this does not throw an error when template can't be fulfilled, have to try correct beforehand
|
||||||
const instance = noHelpers ? hbsInstanceNoHelpers : hbsInstance
|
const instance = opts.noHelpers ? hbsInstanceNoHelpers : hbsInstance
|
||||||
const template = instance.compile(string, {
|
const template = instance.compile(string, {
|
||||||
strict: false,
|
strict: false,
|
||||||
})
|
})
|
||||||
|
@ -136,7 +119,7 @@ module.exports.processStringSync = (
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
return removeHandlebarsStatements(input)
|
return input
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -155,7 +138,8 @@ module.exports.makePropSafe = property => {
|
||||||
* @param opts optional - specify some options for processing.
|
* @param opts optional - specify some options for processing.
|
||||||
* @returns {boolean} Whether or not the input string is valid.
|
* @returns {boolean} Whether or not the input string is valid.
|
||||||
*/
|
*/
|
||||||
module.exports.isValid = (string, opts = { noHelpers: false }) => {
|
module.exports.isValid = (string, opts) => {
|
||||||
|
opts = { ...defaultOpts, ...opts }
|
||||||
const validCases = [
|
const validCases = [
|
||||||
"string",
|
"string",
|
||||||
"number",
|
"number",
|
||||||
|
@ -169,7 +153,7 @@ module.exports.isValid = (string, opts = { noHelpers: false }) => {
|
||||||
// don't really need a real context to check if its valid
|
// don't really need a real context to check if its valid
|
||||||
const context = {}
|
const context = {}
|
||||||
try {
|
try {
|
||||||
const instance = opts && opts.noHelpers ? hbsInstanceNoHelpers : hbsInstance
|
const instance = opts.noHelpers ? hbsInstanceNoHelpers : hbsInstance
|
||||||
instance.compile(processors.preprocess(string, false))(context)
|
instance.compile(processors.preprocess(string, false))(context)
|
||||||
return true
|
return true
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|
|
@ -10,7 +10,10 @@ module.exports.swapStrings = (string, start, length, swap) => {
|
||||||
return string.slice(0, start) + swap + string.slice(start + length)
|
return string.slice(0, start) + swap + string.slice(start + length)
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports.removeHandlebarsStatements = string => {
|
module.exports.removeHandlebarsStatements = (
|
||||||
|
string,
|
||||||
|
replacement = "Invalid binding"
|
||||||
|
) => {
|
||||||
let regexp = new RegExp(exports.FIND_HBS_REGEX)
|
let regexp = new RegExp(exports.FIND_HBS_REGEX)
|
||||||
let matches = string.match(regexp)
|
let matches = string.match(regexp)
|
||||||
if (matches == null) {
|
if (matches == null) {
|
||||||
|
@ -18,7 +21,7 @@ module.exports.removeHandlebarsStatements = string => {
|
||||||
}
|
}
|
||||||
for (let match of matches) {
|
for (let match of matches) {
|
||||||
const idx = string.indexOf(match)
|
const idx = string.indexOf(match)
|
||||||
string = exports.swapStrings(string, idx, match.length, "Invalid Binding")
|
string = exports.swapStrings(string, idx, match.length, replacement)
|
||||||
}
|
}
|
||||||
return string
|
return string
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,10 +13,14 @@ describe("test the custom helpers we have applied", () => {
|
||||||
|
|
||||||
describe("test that it can run without helpers", () => {
|
describe("test that it can run without helpers", () => {
|
||||||
it("should be able to run without helpers", async () => {
|
it("should be able to run without helpers", async () => {
|
||||||
const output = await processString("{{ avg 1 1 1 }}", {}, { noHelpers: true })
|
const output = await processString(
|
||||||
|
"{{ avg 1 1 1 }}",
|
||||||
|
{},
|
||||||
|
{ noHelpers: true }
|
||||||
|
)
|
||||||
const valid = await processString("{{ avg 1 1 1 }}", {})
|
const valid = await processString("{{ avg 1 1 1 }}", {})
|
||||||
expect(valid).toBe("1")
|
expect(valid).toBe("1")
|
||||||
expect(output).toBe("Invalid Binding")
|
expect(output).toBe("{{ avg 1 1 1 }}")
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -185,17 +189,22 @@ describe("test the date helpers", () => {
|
||||||
|
|
||||||
it("should test the timezone capabilities", async () => {
|
it("should test the timezone capabilities", async () => {
|
||||||
const date = new Date(1611577535000)
|
const date = new Date(1611577535000)
|
||||||
const output = await processString("{{ date time 'HH-mm-ss Z' 'America/New_York' }}", {
|
const output = await processString(
|
||||||
|
"{{ date time 'HH-mm-ss Z' 'America/New_York' }}",
|
||||||
|
{
|
||||||
time: date.toUTCString(),
|
time: date.toUTCString(),
|
||||||
})
|
}
|
||||||
const formatted = new dayjs(date).tz("America/New_York").format("HH-mm-ss Z")
|
)
|
||||||
|
const formatted = new dayjs(date)
|
||||||
|
.tz("America/New_York")
|
||||||
|
.format("HH-mm-ss Z")
|
||||||
expect(output).toBe(formatted)
|
expect(output).toBe(formatted)
|
||||||
})
|
})
|
||||||
|
|
||||||
it("should guess the users timezone when not specified", async () => {
|
it("should guess the users timezone when not specified", async () => {
|
||||||
const date = new Date()
|
const date = new Date()
|
||||||
const output = await processString("{{ date time 'Z' }}", {
|
const output = await processString("{{ date time 'Z' }}", {
|
||||||
time: date.toUTCString()
|
time: date.toUTCString(),
|
||||||
})
|
})
|
||||||
const timezone = dayjs.tz.guess()
|
const timezone = dayjs.tz.guess()
|
||||||
const offset = new dayjs(date).tz(timezone).format("Z")
|
const offset = new dayjs(date).tz(timezone).format("Z")
|
||||||
|
@ -307,12 +316,12 @@ describe("test the comparison helpers", () => {
|
||||||
describe("Test the object/array helper", () => {
|
describe("Test the object/array helper", () => {
|
||||||
it("should allow plucking from an array of objects", async () => {
|
it("should allow plucking from an array of objects", async () => {
|
||||||
const context = {
|
const context = {
|
||||||
items: [
|
items: [{ price: 20 }, { price: 30 }],
|
||||||
{ price: 20 },
|
|
||||||
{ price: 30 },
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
const output = await processString("{{ literal ( sum ( pluck items 'price' ) ) }}", context)
|
const output = await processString(
|
||||||
|
"{{ literal ( sum ( pluck items 'price' ) ) }}",
|
||||||
|
context
|
||||||
|
)
|
||||||
expect(output).toBe(50)
|
expect(output).toBe(50)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -450,7 +459,7 @@ describe("Cover a few complex use cases", () => {
|
||||||
rowCount: 8,
|
rowCount: 8,
|
||||||
}
|
}
|
||||||
const output = await processObject(input, tableJson)
|
const output = await processObject(input, tableJson)
|
||||||
expect(output.dataProvider).not.toBe("Invalid Binding")
|
expect(output.dataProvider).not.toBe("Invalid binding")
|
||||||
})
|
})
|
||||||
|
|
||||||
it("should be able to handle external ids", async () => {
|
it("should be able to handle external ids", async () => {
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"name": "@budibase/worker",
|
"name": "@budibase/worker",
|
||||||
"email": "hi@budibase.com",
|
"email": "hi@budibase.com",
|
||||||
"version": "1.0.27-alpha.0",
|
"version": "1.0.27-alpha.2",
|
||||||
"description": "Budibase background service",
|
"description": "Budibase background service",
|
||||||
"main": "src/index.js",
|
"main": "src/index.js",
|
||||||
"repository": {
|
"repository": {
|
||||||
|
@ -29,8 +29,8 @@
|
||||||
"author": "Budibase",
|
"author": "Budibase",
|
||||||
"license": "GPL-3.0",
|
"license": "GPL-3.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@budibase/auth": "^1.0.27-alpha.0",
|
"@budibase/auth": "^1.0.27-alpha.2",
|
||||||
"@budibase/string-templates": "^1.0.27-alpha.0",
|
"@budibase/string-templates": "^1.0.27-alpha.2",
|
||||||
"@koa/router": "^8.0.0",
|
"@koa/router": "^8.0.0",
|
||||||
"@sentry/node": "^6.0.0",
|
"@sentry/node": "^6.0.0",
|
||||||
"@techpass/passport-openidconnect": "^0.3.0",
|
"@techpass/passport-openidconnect": "^0.3.0",
|
||||||
|
|
|
@ -8,7 +8,7 @@ jest.mock("nodemailer")
|
||||||
const nodemailer = require("nodemailer")
|
const nodemailer = require("nodemailer")
|
||||||
nodemailer.createTransport.mockReturnValue({
|
nodemailer.createTransport.mockReturnValue({
|
||||||
sendMail: sendMailMock,
|
sendMail: sendMailMock,
|
||||||
verify: jest.fn()
|
verify: jest.fn(),
|
||||||
})
|
})
|
||||||
|
|
||||||
describe("/api/global/email", () => {
|
describe("/api/global/email", () => {
|
||||||
|
@ -39,6 +39,6 @@ describe("/api/global/email", () => {
|
||||||
expect(sendMailMock).toHaveBeenCalled()
|
expect(sendMailMock).toHaveBeenCalled()
|
||||||
const emailCall = sendMailMock.mock.calls[0][0]
|
const emailCall = sendMailMock.mock.calls[0][0]
|
||||||
expect(emailCall.subject).toBe("Hello!")
|
expect(emailCall.subject).toBe("Hello!")
|
||||||
expect(emailCall.html).not.toContain("Invalid Binding")
|
expect(emailCall.html).not.toContain("Invalid binding")
|
||||||
})
|
})
|
||||||
})
|
})
|
Loading…
Reference in New Issue