Merge branch 'dynamic-picker-options' of github.com:Budibase/budibase into dynamic-picker-options
This commit is contained in:
commit
8f676dd9bf
|
@ -0,0 +1,235 @@
|
||||||
|
<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">
|
||||||
|
Entwickle, automatisiere und stelle interne Tools in Minuten bereit.
|
||||||
|
</h3>
|
||||||
|
<p align="center">
|
||||||
|
Budibase ist eine quelloffene Low-Code Plattform, die es Entwicklern und IT-Profis ermöglicht interne Tools auf eigener Infrastruktur zu entwickeln, zu automatisieren und bereitzustellen.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<h3 align="center">
|
||||||
|
🤖 🎨 🚀
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
<p align="center">
|
||||||
|
<img alt="Budibase design ui" src="https://i.imgur.com/5BnXPsN.png">
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p align="center">
|
||||||
|
<a href="https://github.com/Budibase/budibase/releases">
|
||||||
|
<img alt="GitHub all releases" src="https://img.shields.io/github/downloads/Budibase/budibase/total">
|
||||||
|
</a>
|
||||||
|
<a href="https://github.com/Budibase/budibase/releases">
|
||||||
|
<img alt="GitHub release (latest by date)" 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="Follow @budibase" />
|
||||||
|
</a>
|
||||||
|
<img src="https://img.shields.io/badge/Contributor%20Covenant-v2.0%20adopted-ff69b4.svg" alt="Code of conduct" />
|
||||||
|
<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">Los Geht's</a>
|
||||||
|
<span> · </span>
|
||||||
|
<a href="https://docs.budibase.com">Dokumentation</a>
|
||||||
|
<span> · </span>
|
||||||
|
<a href="https://github.com/Budibase/budibase/discussions?discussions_q=category%3AIdeas">Featureanfrage</a>
|
||||||
|
<span> · </span>
|
||||||
|
<a href="https://github.com/Budibase/budibase/issues">Einen Bug melden</a>
|
||||||
|
<span> · </span>
|
||||||
|
Support: <a href="https://github.com/Budibase/budibase/discussions">Github Discussions</a>
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
<br /><br />
|
||||||
|
## ✨ Features
|
||||||
|
|
||||||
|
- **Entwickle echte Webanwendungen.** Anders als ähnliche Plattformen entwickelst du mit Budibase echte Single-Page Webapplikationen (SPAs). Deine Budibase-Apps sind standardmäßig hochperformant und haben ein Responsive-Design für eine großartige Benutzererfahrung.
|
||||||
|
|
||||||
|
- **Quelloffen und erweiterbar.** Budibase ist quelloffen - lizenziert unter der GPL v3. Du kannst darauf vertrauen, dass Budibase auch in der Zukunft immer zur Verfügung steht. Budibase bietet eine Entwicklerfreundliche Plattform: du kannst Budibase erweitern, oder die Codebase forken und eigene Änderungen vornehmen.
|
||||||
|
|
||||||
|
- **Datenquellen einbinden oder von Null starten.** Budibase kann Daten aus vielen Quellen einbinden, unter anderem aus MongoDB, CouchDB, PostgreSQL, MySQL, Airtable, S3, DynamoDB, oder einer REST API. Und anders als ähnliche Plattformen erlaubt Budibase auch die App-Entwicklung komplett ohne Datenquellen mit einer internen Datenbank. Deine Datenquelle noch nicht dabei? [Frag einfach nach](https://github.com/Budibase/budibase/discussions?discussions_q=category%3AIdeas).
|
||||||
|
|
||||||
|
- **Designe und entwickle Apps mit leistungsfähigen Komponenten.** Budibase kommt fertig mit optisch ansprechenden und leistungsfähigen Komponenten, die als Bausteine für deine UI dienen. Außerdem kannst du die UI mit vielen CSS-Styles nach deinem Geschmack anpassen. Fehlt dir eine Komponente? [Frag uns hier](https://github.com/Budibase/budibase/discussions?discussions_q=category%3AIdeas).
|
||||||
|
|
||||||
|
- **Automatisiere Prozesse, integriere andere Tools und binde Web-APIs ein.** Spar dir Zeit, indem du manuelle Prozesse einfach automatisierst: Vom Verbinden mit Web-Hooks bis zum automatischen Senden von E-Mails, Budibase kann alles für dich erledigen. Eine Automatisierung ist noch nicht dabei? Du kannst einfach [deine eigene erstellen](https://github.com/Budibase/automations) oder [uns deine Idee mitteilen](https://github.com/Budibase/budibase/discussions?discussions_q=category%3AIdeas).
|
||||||
|
|
||||||
|
- **Ein Paradies für Systemadministratoren** Budibase ist von Grund auf für das Skalieren ausgelegt. Du kannst Budibase einfach auf deiner eigenen Infrastruktur hosten und global Benutzer, Onboarding, SMTP, Applikationen, Gruppen, UI-Themes und mehr verwalten. Du kannst außerdem ein übersichtliches App-Portal für deine Benutzer bereitstellen und das Benutzermanagement an Gruppen-Manager delegieren.
|
||||||
|
|
||||||
|
<br />
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<br />
|
||||||
|
|
||||||
|
## 🏁 Los geht's
|
||||||
|
Momentan existieren zwei Optionen mit Budibase loszulegen: Digital Ocean und Docker.
|
||||||
|
<br /><br />
|
||||||
|
|
||||||
|
### Los geht's mit Digital Ocean
|
||||||
|
Der einfachste und schnellste Weg loszulegen ist Digital Ocean:
|
||||||
|
<a href="https://marketplace.digitalocean.com/apps/budibase">1-Click Deploy auf Digital Ocean</a>
|
||||||
|
|
||||||
|
<a href="https://marketplace.digitalocean.com/apps/budibase">
|
||||||
|
<img src="https://user-images.githubusercontent.com/552074/87779219-5c3b7600-c824-11ea-9898-981a8ba94f6c.png" alt="digital ocean badge">
|
||||||
|
</a>
|
||||||
|
<br /><br />
|
||||||
|
|
||||||
|
### Los geht's mit Docker
|
||||||
|
Um loszulegen musst du bereits `docker` und `docker compose` auf deinem Computer installiert haben.
|
||||||
|
Sobald du Docker installiert hast brauchst du ca. 5 Minuten für diese 4 Schritte:
|
||||||
|
|
||||||
|
1. Installiere das Budibase CLI Tool.
|
||||||
|
```
|
||||||
|
$ npm i -g @budibase/cli
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
2. Installiere Budibase (wähle den Speicherort und den Port auf dem Budibase laufen soll.)
|
||||||
|
```
|
||||||
|
$ budi hosting --init
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
3. Führe Budibase aus.
|
||||||
|
```
|
||||||
|
$ budi hosting --start
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
4. Lege einen Admin-Benutzer an.
|
||||||
|
Gib die E-Mail und das Passwort für den neuen Admin-Benutzer ein.
|
||||||
|
|
||||||
|
Schon geschafft! Jetzt kann es losgehen mit der minutenschnellen Entwicklung deiner Tools. Für weitere Informationen und Tipps schau doch mal in unsere [Dokumentation](https://docs.budibase.com/getting-started).
|
||||||
|
|
||||||
|
<br />
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<br />
|
||||||
|
|
||||||
|
## 🎓 Budibase lernen
|
||||||
|
|
||||||
|
Die Budibase Dokumentation [findest du hier](https://docs.budibase.com).
|
||||||
|
<br />
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<br /><br />
|
||||||
|
|
||||||
|
## 💬 Community
|
||||||
|
|
||||||
|
Wenn du eine Frage hast, oder dich mit anderen Budibase-Nutzern unterhalten willst, schau doch mal in unsere
|
||||||
|
[Github Discussions](https://github.com/Budibase/budibase/discussions).
|
||||||
|
|
||||||
|
<img src="https://d33wubrfki0l68.cloudfront.net/e9241201fd89f9abbbdaac4fe44bb16312752abe/84013/img/hero-images/community.webp" />
|
||||||
|
|
||||||
|
<br /><br />
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<br />
|
||||||
|
|
||||||
|
## ❗ Verhaltenskodex
|
||||||
|
|
||||||
|
Budibase steht für eine einladende und vielfältige Community frei von Belästigung. Wir erwarten dass sich jeder in der Budibase-Community an unseren [**Verhaltenskodex**](https://github.com/Budibase/budibase/blob/HEAD/.github/CODE_OF_CONDUCT.md) hält. Bitte les ihn dir durch.
|
||||||
|
<br />
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<br />
|
||||||
|
|
||||||
|
## 🙌 Zu Budibase beitragen
|
||||||
|
|
||||||
|
Von einem gemeldeten Bug bis zum Erstellen einer Pull-Request: wir schätzen jeden Beitrag. Wenn du ein neues Feature implementieren willst oder eine Änderung an der API vornehmen willst, erstelle bitte zuerst ein Issue. So können wir sicherstellen, dass deine Arbeit nicht umsonst ist.
|
||||||
|
|
||||||
|
### Unsicher wo du anfangen sollst?
|
||||||
|
Gute Ideen für erste Beiträge zum Projekt [findest du hier](https://github.com/Budibase/budibase/projects/22).
|
||||||
|
|
||||||
|
### Wie die Repository strukturiert ist.
|
||||||
|
Budibase ist eine Monorepo, die von Lerna verwaltet wird. Lerna verwaltet das Erstellen und Veröffentlichen von Budibase-Paketen.
|
||||||
|
Grob besteht Budibase aus folgenden Modulen:
|
||||||
|
|
||||||
|
- [packages/builder](https://github.com/Budibase/budibase/tree/HEAD/packages/builder) - enthält Code für den clientseitigen Budibase Builder, mit dem Anwendungen erstellt werden.
|
||||||
|
|
||||||
|
- [packages/client](https://github.com/Budibase/budibase/tree/HEAD/packages/client) - Ein Modul, das im Browser läuft und aus JSON-Definitionen funktionsfähige Web-Apps erstellt.
|
||||||
|
|
||||||
|
- [packages/server](https://github.com/Budibase/budibase/tree/HEAD/packages/server) - Der Budibase Server. Diese Koa-Anwendung stellt den Javascript-Code für den Builder und den Client bereit, und bietet eine API für die Interaktion mit dem Budibase Backend, Datenbanken und dem Dateisystem.
|
||||||
|
|
||||||
|
Für mehr Informationen schau in die [CONTRIBUTING.md](https://github.com/Budibase/budibase/blob/HEAD/.github/CONTRIBUTING.md)
|
||||||
|
<br /><br />
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<br /><br />
|
||||||
|
|
||||||
|
## 📝 Lizenz
|
||||||
|
|
||||||
|
Budibase ist quelloffen, lizenziert unter der [GPL v3](https://www.gnu.org/licenses/gpl-3.0.en.html). Die Client- und Komponentenbibliotheken sind unter der [MPL](https://directory.fsf.org/wiki/License:MPL-2.0) lizenziert, damit du deine erstellten Apps unter deine präferierte Lizenz stellen kannst.
|
||||||
|
<br /><br />
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
|
||||||
|
<br />
|
||||||
|
|
||||||
|
## ⭐ Github-Sterne im Verlauf der Zeit
|
||||||
|
|
||||||
|
[![Stargazers over time](https://starchart.cc/Budibase/budibase.svg)](https://starchart.cc/Budibase/budibase)
|
||||||
|
|
||||||
|
Wenn du zwischen Updates des Builders Probleme auftreten, lies bitte den Guide [hier](https://github.com/Budibase/budibase/blob/HEAD/.github/CONTRIBUTING.md#troubleshooting), um deine Umgebung zurückzusetzen.
|
||||||
|
|
||||||
|
<br />
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<br /><br />
|
||||||
|
|
||||||
|
## Mitwirkende ✨
|
||||||
|
|
||||||
|
Vielen Dank an alle wundervollen Menschen, die zu Budibase beigetragen haben ([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 -->
|
||||||
|
|
||||||
|
Dieses Projekt folgt der [All-Contributors](https://github.com/all-contributors/all-contributors) Spezifikation. Wir heißen Beiträge aller Art willkommen!
|
|
@ -1,5 +1,5 @@
|
||||||
{
|
{
|
||||||
"version": "0.9.105-alpha.9",
|
"version": "0.9.105-alpha.14",
|
||||||
"npmClient": "yarn",
|
"npmClient": "yarn",
|
||||||
"packages": [
|
"packages": [
|
||||||
"packages/*"
|
"packages/*"
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@budibase/auth",
|
"name": "@budibase/auth",
|
||||||
"version": "0.9.105-alpha.9",
|
"version": "0.9.105-alpha.14",
|
||||||
"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": "0.9.105-alpha.9",
|
"version": "0.9.105-alpha.14",
|
||||||
"license": "AGPL-3.0",
|
"license": "AGPL-3.0",
|
||||||
"svelte": "src/index.js",
|
"svelte": "src/index.js",
|
||||||
"module": "dist/bbui.es.js",
|
"module": "dist/bbui.es.js",
|
||||||
|
|
|
@ -48,6 +48,9 @@
|
||||||
padding-top: var(--spacing-l);
|
padding-top: var(--spacing-l);
|
||||||
padding-bottom: var(--spacing-l);
|
padding-bottom: var(--spacing-l);
|
||||||
}
|
}
|
||||||
|
.gap-XXS {
|
||||||
|
grid-gap: var(--spacing-xs);
|
||||||
|
}
|
||||||
.gap-XS {
|
.gap-XS {
|
||||||
grid-gap: var(--spacing-s);
|
grid-gap: var(--spacing-s);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@budibase/builder",
|
"name": "@budibase/builder",
|
||||||
"version": "0.9.105-alpha.9",
|
"version": "0.9.105-alpha.14",
|
||||||
"license": "AGPL-3.0",
|
"license": "AGPL-3.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
@ -65,10 +65,10 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@budibase/bbui": "^0.9.105-alpha.9",
|
"@budibase/bbui": "^0.9.105-alpha.14",
|
||||||
"@budibase/client": "^0.9.105-alpha.9",
|
"@budibase/client": "^0.9.105-alpha.14",
|
||||||
"@budibase/colorpicker": "1.1.2",
|
"@budibase/colorpicker": "1.1.2",
|
||||||
"@budibase/string-templates": "^0.9.105-alpha.9",
|
"@budibase/string-templates": "^0.9.105-alpha.14",
|
||||||
"@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",
|
||||||
|
|
|
@ -31,7 +31,12 @@
|
||||||
.flat()
|
.flat()
|
||||||
// Prevent modal closing if there were errors
|
// Prevent modal closing if there were errors
|
||||||
return false
|
return false
|
||||||
} else if (rowResponse.status === 400 || rowResponse.status === 500) {
|
} else if (rowResponse.status === 400 && rowResponse.validationErrors) {
|
||||||
|
errors = Object.keys(rowResponse.validationErrors).map(field => ({
|
||||||
|
message: `${field} ${rowResponse.validationErrors[field][0]}`,
|
||||||
|
}))
|
||||||
|
return false
|
||||||
|
} else if (rowResponse.status === 500) {
|
||||||
errors = [{ message: rowResponse.message }]
|
errors = [{ message: rowResponse.message }]
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
|
@ -87,6 +87,7 @@
|
||||||
placeholder: setting.placeholder,
|
placeholder: setting.placeholder,
|
||||||
}}
|
}}
|
||||||
{bindings}
|
{bindings}
|
||||||
|
{componentDefinition}
|
||||||
/>
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
{/each}
|
{/each}
|
||||||
|
|
|
@ -1,5 +0,0 @@
|
||||||
<script>
|
|
||||||
import FormFieldSelect from "./FormFieldSelect.svelte"
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<FormFieldSelect {...$$props} on:change type="attachment" />
|
|
|
@ -1,5 +0,0 @@
|
||||||
<script>
|
|
||||||
import FormFieldSelect from "./FormFieldSelect.svelte"
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<FormFieldSelect {...$$props} on:change type="boolean" />
|
|
|
@ -1,5 +0,0 @@
|
||||||
<script>
|
|
||||||
import FormFieldSelect from "./FormFieldSelect.svelte"
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<FormFieldSelect {...$$props} on:change type="datetime" />
|
|
|
@ -23,6 +23,7 @@
|
||||||
const getOptions = (schema, fieldType) => {
|
const getOptions = (schema, fieldType) => {
|
||||||
let entries = Object.entries(schema ?? {})
|
let entries = Object.entries(schema ?? {})
|
||||||
if (fieldType) {
|
if (fieldType) {
|
||||||
|
fieldType = fieldType.split("/")[1]
|
||||||
entries = entries.filter(entry => entry[1].type === fieldType)
|
entries = entries.filter(entry => entry[1].type === fieldType)
|
||||||
}
|
}
|
||||||
return entries.map(entry => entry[0])
|
return entries.map(entry => entry[0])
|
||||||
|
|
|
@ -1,5 +0,0 @@
|
||||||
<script>
|
|
||||||
import FormFieldSelect from "./FormFieldSelect.svelte"
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<FormFieldSelect {...$$props} on:change type="longform" />
|
|
|
@ -1,5 +0,0 @@
|
||||||
<script>
|
|
||||||
import FormFieldSelect from "./FormFieldSelect.svelte"
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<FormFieldSelect {...$$props} on:change type="number" />
|
|
|
@ -1,5 +0,0 @@
|
||||||
<script>
|
|
||||||
import FormFieldSelect from "./FormFieldSelect.svelte"
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<FormFieldSelect {...$$props} on:change type="options" />
|
|
|
@ -1,5 +0,0 @@
|
||||||
<script>
|
|
||||||
import FormFieldSelect from "./FormFieldSelect.svelte"
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<FormFieldSelect {...$$props} on:change type="link" />
|
|
|
@ -1,5 +0,0 @@
|
||||||
<script>
|
|
||||||
import FormFieldSelect from "./FormFieldSelect.svelte"
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<FormFieldSelect {...$$props} on:change type="string" />
|
|
|
@ -0,0 +1,363 @@
|
||||||
|
<script>
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
Icon,
|
||||||
|
DrawerContent,
|
||||||
|
Layout,
|
||||||
|
Select,
|
||||||
|
Heading,
|
||||||
|
Body,
|
||||||
|
Input,
|
||||||
|
DatePicker,
|
||||||
|
} from "@budibase/bbui"
|
||||||
|
import { currentAsset, selectedComponent } from "builderStore"
|
||||||
|
import { findClosestMatchingComponent } from "builderStore/storeUtils"
|
||||||
|
import { getSchemaForDatasource } from "builderStore/dataBinding"
|
||||||
|
import DrawerBindableInput from "components/common/bindings/DrawerBindableInput.svelte"
|
||||||
|
import { generate } from "shortid"
|
||||||
|
|
||||||
|
export let rules = []
|
||||||
|
export let bindings = []
|
||||||
|
export let type
|
||||||
|
|
||||||
|
const Constraints = {
|
||||||
|
Required: {
|
||||||
|
label: "Required",
|
||||||
|
value: "required",
|
||||||
|
},
|
||||||
|
MinLength: {
|
||||||
|
label: "Min length",
|
||||||
|
value: "minLength",
|
||||||
|
},
|
||||||
|
MaxLength: {
|
||||||
|
label: "Max length",
|
||||||
|
value: "maxLength",
|
||||||
|
},
|
||||||
|
MaxValue: {
|
||||||
|
label: "Max value",
|
||||||
|
value: "maxValue",
|
||||||
|
},
|
||||||
|
MinValue: {
|
||||||
|
label: "Min value",
|
||||||
|
value: "minValue",
|
||||||
|
},
|
||||||
|
Equal: {
|
||||||
|
label: "Must equal",
|
||||||
|
value: "equal",
|
||||||
|
},
|
||||||
|
NotEqual: {
|
||||||
|
label: "Must not equal",
|
||||||
|
value: "notEqual",
|
||||||
|
},
|
||||||
|
Regex: {
|
||||||
|
label: "Must match regex",
|
||||||
|
value: "regex",
|
||||||
|
},
|
||||||
|
NotRegex: {
|
||||||
|
label: "Must not match regex",
|
||||||
|
value: "notRegex",
|
||||||
|
},
|
||||||
|
Contains: {
|
||||||
|
label: "Must contain row ID",
|
||||||
|
value: "contains",
|
||||||
|
},
|
||||||
|
NotContains: {
|
||||||
|
label: "Must not contain row ID",
|
||||||
|
value: "notContains",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
const ConstraintMap = {
|
||||||
|
["string"]: [
|
||||||
|
Constraints.Required,
|
||||||
|
Constraints.MaxLength,
|
||||||
|
Constraints.Equal,
|
||||||
|
Constraints.NotEqual,
|
||||||
|
Constraints.Regex,
|
||||||
|
Constraints.NotRegex,
|
||||||
|
],
|
||||||
|
["number"]: [
|
||||||
|
Constraints.Required,
|
||||||
|
Constraints.MaxValue,
|
||||||
|
Constraints.MinValue,
|
||||||
|
Constraints.Equal,
|
||||||
|
Constraints.NotEqual,
|
||||||
|
],
|
||||||
|
["boolean"]: [
|
||||||
|
Constraints.Required,
|
||||||
|
Constraints.Equal,
|
||||||
|
Constraints.NotEqual,
|
||||||
|
],
|
||||||
|
["datetime"]: [
|
||||||
|
Constraints.Required,
|
||||||
|
Constraints.MaxValue,
|
||||||
|
Constraints.MinValue,
|
||||||
|
Constraints.Equal,
|
||||||
|
Constraints.NotEqual,
|
||||||
|
],
|
||||||
|
["attachment"]: [Constraints.Required],
|
||||||
|
["link"]: [
|
||||||
|
Constraints.Required,
|
||||||
|
Constraints.Contains,
|
||||||
|
Constraints.NotContains,
|
||||||
|
Constraints.MinLength,
|
||||||
|
Constraints.MaxLength,
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
$: dataSourceSchema = getDataSourceSchema($currentAsset, $selectedComponent)
|
||||||
|
$: field = $selectedComponent?.field
|
||||||
|
$: schemaRules = parseRulesFromSchema(field, dataSourceSchema || {})
|
||||||
|
$: fieldType = type?.split("/")[1] || "string"
|
||||||
|
$: constraintOptions = getConstraintsForType(fieldType)
|
||||||
|
|
||||||
|
const getConstraintsForType = type => {
|
||||||
|
return ConstraintMap[type]
|
||||||
|
}
|
||||||
|
|
||||||
|
const getDataSourceSchema = (asset, component) => {
|
||||||
|
if (!asset || !component) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
const formParent = findClosestMatchingComponent(
|
||||||
|
asset.props,
|
||||||
|
component._id,
|
||||||
|
component => component._component.endsWith("/form")
|
||||||
|
)
|
||||||
|
return getSchemaForDatasource(asset, formParent?.dataSource)
|
||||||
|
}
|
||||||
|
|
||||||
|
const parseRulesFromSchema = (field, dataSourceSchema) => {
|
||||||
|
if (!field || !dataSourceSchema) {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
const fieldSchema = dataSourceSchema.schema?.[field]
|
||||||
|
const constraints = fieldSchema?.constraints
|
||||||
|
if (!constraints) {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
let rules = []
|
||||||
|
|
||||||
|
// Required constraint
|
||||||
|
if (
|
||||||
|
field === dataSourceSchema?.table?.primaryDisplay ||
|
||||||
|
constraints.presence?.allowEmpty === false
|
||||||
|
) {
|
||||||
|
rules.push({
|
||||||
|
constraint: "required",
|
||||||
|
error: "Required field",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// String length constraint
|
||||||
|
if (exists(constraints.length?.maximum)) {
|
||||||
|
const length = constraints.length.maximum
|
||||||
|
rules.push({
|
||||||
|
constraint: "maxLength",
|
||||||
|
value: length,
|
||||||
|
error: `Maximum ${length} characters`,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Min / max number constraint
|
||||||
|
if (exists(constraints.numericality?.greaterThanOrEqualTo)) {
|
||||||
|
const min = constraints.numericality.greaterThanOrEqualTo
|
||||||
|
rules.push({
|
||||||
|
constraint: "minValue",
|
||||||
|
value: min,
|
||||||
|
error: `Minimum value is ${min}`,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if (exists(constraints.numericality?.lessThanOrEqualTo)) {
|
||||||
|
const max = constraints.numericality.lessThanOrEqualTo
|
||||||
|
rules.push({
|
||||||
|
constraint: "maxValue",
|
||||||
|
value: max,
|
||||||
|
error: `Maximum value is ${max}`,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return rules
|
||||||
|
}
|
||||||
|
|
||||||
|
const exists = value => {
|
||||||
|
return value != null && value !== ""
|
||||||
|
}
|
||||||
|
|
||||||
|
const addRule = () => {
|
||||||
|
rules = [
|
||||||
|
...(rules || []),
|
||||||
|
{
|
||||||
|
valueType: "Binding",
|
||||||
|
type: fieldType,
|
||||||
|
id: generate(),
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
const removeRule = id => {
|
||||||
|
rules = rules.filter(link => link.id !== id)
|
||||||
|
}
|
||||||
|
|
||||||
|
const duplicateRule = id => {
|
||||||
|
const existingRule = rules.find(rule => rule.id === id)
|
||||||
|
const newRule = { ...existingRule, id: generate() }
|
||||||
|
rules = [...rules, newRule]
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<DrawerContent>
|
||||||
|
<div class="container">
|
||||||
|
<Layout noPadding gap="M">
|
||||||
|
<Layout noPadding gap={schemaRules?.length ? "S" : "XS"}>
|
||||||
|
<Heading size="XS">Schema validation rules</Heading>
|
||||||
|
{#if schemaRules?.length}
|
||||||
|
<div class="links">
|
||||||
|
{#each schemaRules as rule}
|
||||||
|
<div class="rule schema">
|
||||||
|
<Select
|
||||||
|
placeholder="Constraint"
|
||||||
|
value={rule.constraint}
|
||||||
|
options={constraintOptions}
|
||||||
|
disabled
|
||||||
|
/>
|
||||||
|
<Select
|
||||||
|
placeholder={null}
|
||||||
|
value="Value"
|
||||||
|
options={["Binding", "Value"]}
|
||||||
|
disabled
|
||||||
|
/>
|
||||||
|
<DrawerBindableInput
|
||||||
|
placeholder="Constraint value"
|
||||||
|
value={rule.value}
|
||||||
|
{bindings}
|
||||||
|
disabled
|
||||||
|
/>
|
||||||
|
<DrawerBindableInput
|
||||||
|
placeholder="Error message"
|
||||||
|
value={rule.error}
|
||||||
|
{bindings}
|
||||||
|
disabled
|
||||||
|
/>
|
||||||
|
<div />
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
<Body size="S">
|
||||||
|
There are no built-in validation rules from the schema.
|
||||||
|
</Body>
|
||||||
|
{/if}
|
||||||
|
</Layout>
|
||||||
|
<Layout noPadding gap="S">
|
||||||
|
<Heading size="XS">Custom validation rules</Heading>
|
||||||
|
{#if rules?.length}
|
||||||
|
<div class="links">
|
||||||
|
{#each rules as rule (rule.id)}
|
||||||
|
<div class="rule">
|
||||||
|
<Select
|
||||||
|
bind:value={rule.constraint}
|
||||||
|
options={constraintOptions}
|
||||||
|
placeholder="Constraint"
|
||||||
|
/>
|
||||||
|
<Select
|
||||||
|
disabled={rule.constraint === "required"}
|
||||||
|
placeholder={null}
|
||||||
|
bind:value={rule.valueType}
|
||||||
|
options={["Binding", "Value"]}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{#if rule.valueType === "Binding"}
|
||||||
|
<!-- Bindings always get a bindable input -->
|
||||||
|
<DrawerBindableInput
|
||||||
|
placeholder="Constraint value"
|
||||||
|
value={rule.value}
|
||||||
|
{bindings}
|
||||||
|
disabled={rule.constraint === "required"}
|
||||||
|
on:change={e => (rule.value = e.detail)}
|
||||||
|
/>
|
||||||
|
{:else if ["maxLength", "minLength", "regex", "notRegex", "contains", "notContains"].includes(rule.constraint)}
|
||||||
|
<!-- Certain constraints always need string values-->
|
||||||
|
<Input
|
||||||
|
bind:value={rule.value}
|
||||||
|
placeholder="Constraint value"
|
||||||
|
/>
|
||||||
|
{:else}
|
||||||
|
<!-- Otherwise we render a component based on the type -->
|
||||||
|
{#if ["string", "number", "options", "longform"].includes(rule.type)}
|
||||||
|
<Input
|
||||||
|
disabled={rule.constraint === "required"}
|
||||||
|
bind:value={rule.value}
|
||||||
|
placeholder="Constraint value"
|
||||||
|
/>
|
||||||
|
{:else if rule.type === "boolean"}
|
||||||
|
<Select
|
||||||
|
disabled={rule.constraint === "required"}
|
||||||
|
options={[
|
||||||
|
{ label: "True", value: "true" },
|
||||||
|
{ label: "False", value: "false" },
|
||||||
|
]}
|
||||||
|
bind:value={rule.value}
|
||||||
|
/>
|
||||||
|
{:else if rule.type === "datetime"}
|
||||||
|
<DatePicker
|
||||||
|
enableTime={false}
|
||||||
|
disabled={rule.constraint === "required"}
|
||||||
|
bind:value={rule.value}
|
||||||
|
/>
|
||||||
|
{:else}
|
||||||
|
<DrawerBindableInput disabled />
|
||||||
|
{/if}
|
||||||
|
{/if}
|
||||||
|
<DrawerBindableInput
|
||||||
|
placeholder="Error message"
|
||||||
|
value={rule.error}
|
||||||
|
{bindings}
|
||||||
|
on:change={e => (rule.error = e.detail)}
|
||||||
|
/>
|
||||||
|
<Icon
|
||||||
|
name="Duplicate"
|
||||||
|
hoverable
|
||||||
|
size="S"
|
||||||
|
on:click={() => duplicateRule(rule.id)}
|
||||||
|
/>
|
||||||
|
<Icon
|
||||||
|
name="Close"
|
||||||
|
hoverable
|
||||||
|
size="S"
|
||||||
|
on:click={() => removeRule(rule.id)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
<div class="button">
|
||||||
|
<Button secondary icon="Add" on:click={addRule}>Add Rule</Button>
|
||||||
|
</div>
|
||||||
|
</Layout>
|
||||||
|
</Layout>
|
||||||
|
</div>
|
||||||
|
</DrawerContent>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.container {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 1000px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
.links {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: flex-start;
|
||||||
|
align-items: stretch;
|
||||||
|
gap: var(--spacing-m);
|
||||||
|
}
|
||||||
|
|
||||||
|
.rule {
|
||||||
|
gap: var(--spacing-l);
|
||||||
|
display: grid;
|
||||||
|
align-items: center;
|
||||||
|
grid-template-columns: 190px 120px 1fr 1fr auto auto;
|
||||||
|
border-radius: var(--border-radius-s);
|
||||||
|
transition: background-color ease-in-out 130ms;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,33 @@
|
||||||
|
<script>
|
||||||
|
import { Button, ActionButton, Drawer } from "@budibase/bbui"
|
||||||
|
import { createEventDispatcher } from "svelte"
|
||||||
|
import ValidationDrawer from "./ValidationDrawer.svelte"
|
||||||
|
|
||||||
|
export let value = []
|
||||||
|
export let bindings = []
|
||||||
|
export let componentDefinition
|
||||||
|
export let type
|
||||||
|
|
||||||
|
let drawer
|
||||||
|
|
||||||
|
const dispatch = createEventDispatcher()
|
||||||
|
const save = () => {
|
||||||
|
dispatch("change", value)
|
||||||
|
drawer.hide()
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<ActionButton on:click={drawer.show}>Configure Validation</ActionButton>
|
||||||
|
<Drawer bind:this={drawer} title="Validation Rules">
|
||||||
|
<svelte:fragment slot="description">
|
||||||
|
Configure validation rules for this field.
|
||||||
|
</svelte:fragment>
|
||||||
|
<Button cta slot="buttons" on:click={save}>Save</Button>
|
||||||
|
<ValidationDrawer
|
||||||
|
slot="body"
|
||||||
|
bind:rules={value}
|
||||||
|
{type}
|
||||||
|
{bindings}
|
||||||
|
{componentDefinition}
|
||||||
|
/>
|
||||||
|
</Drawer>
|
|
@ -12,15 +12,9 @@ import SectionSelect from "./SectionSelect.svelte"
|
||||||
import NavigationEditor from "./NavigationEditor/NavigationEditor.svelte"
|
import NavigationEditor from "./NavigationEditor/NavigationEditor.svelte"
|
||||||
import FilterEditor from "./FilterEditor/FilterEditor.svelte"
|
import FilterEditor from "./FilterEditor/FilterEditor.svelte"
|
||||||
import URLSelect from "./URLSelect.svelte"
|
import URLSelect from "./URLSelect.svelte"
|
||||||
import StringFieldSelect from "./StringFieldSelect.svelte"
|
|
||||||
import NumberFieldSelect from "./NumberFieldSelect.svelte"
|
|
||||||
import OptionsFieldSelect from "./OptionsFieldSelect.svelte"
|
|
||||||
import BooleanFieldSelect from "./BooleanFieldSelect.svelte"
|
|
||||||
import LongFormFieldSelect from "./LongFormFieldSelect.svelte"
|
|
||||||
import DateTimeFieldSelect from "./DateTimeFieldSelect.svelte"
|
|
||||||
import AttachmentFieldSelect from "./AttachmentFieldSelect.svelte"
|
|
||||||
import RelationshipFieldSelect from "./RelationshipFieldSelect.svelte"
|
|
||||||
import OptionsEditor from "./OptionsEditor/OptionsEditor.svelte"
|
import OptionsEditor from "./OptionsEditor/OptionsEditor.svelte"
|
||||||
|
import FormFieldSelect from "./FormFieldSelect.svelte"
|
||||||
|
import ValidationEditor from "./ValidationEditor/ValidationEditor.svelte"
|
||||||
|
|
||||||
const componentMap = {
|
const componentMap = {
|
||||||
text: Input,
|
text: Input,
|
||||||
|
@ -41,14 +35,22 @@ const componentMap = {
|
||||||
navigation: NavigationEditor,
|
navigation: NavigationEditor,
|
||||||
filter: FilterEditor,
|
filter: FilterEditor,
|
||||||
url: URLSelect,
|
url: URLSelect,
|
||||||
"field/string": StringFieldSelect,
|
"field/string": FormFieldSelect,
|
||||||
"field/number": NumberFieldSelect,
|
"field/number": FormFieldSelect,
|
||||||
"field/options": OptionsFieldSelect,
|
"field/options": FormFieldSelect,
|
||||||
"field/boolean": BooleanFieldSelect,
|
"field/boolean": FormFieldSelect,
|
||||||
"field/longform": LongFormFieldSelect,
|
"field/longform": FormFieldSelect,
|
||||||
"field/datetime": DateTimeFieldSelect,
|
"field/datetime": FormFieldSelect,
|
||||||
"field/attachment": AttachmentFieldSelect,
|
"field/attachment": FormFieldSelect,
|
||||||
"field/link": RelationshipFieldSelect,
|
"field/link": FormFieldSelect,
|
||||||
|
// Some validation types are the same as others, so not all types are
|
||||||
|
// explicitly listed here. e.g. options uses string validation
|
||||||
|
"validation/string": ValidationEditor,
|
||||||
|
"validation/number": ValidationEditor,
|
||||||
|
"validation/boolean": ValidationEditor,
|
||||||
|
"validation/datetime": ValidationEditor,
|
||||||
|
"validation/attachment": ValidationEditor,
|
||||||
|
"validation/link": ValidationEditor,
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getComponentForSettingType = type => {
|
export const getComponentForSettingType = type => {
|
||||||
|
|
|
@ -53,7 +53,7 @@
|
||||||
label={def.label}
|
label={def.label}
|
||||||
key={def.key}
|
key={def.key}
|
||||||
value={deepGet($currentAsset, def.key)}
|
value={deepGet($currentAsset, def.key)}
|
||||||
on:change={event => setAssetProps(def.key, event.detail, def.parser)}
|
onChange={val => setAssetProps(def.key, val, def.parser)}
|
||||||
{bindings}
|
{bindings}
|
||||||
/>
|
/>
|
||||||
{/each}
|
{/each}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@budibase/cli",
|
"name": "@budibase/cli",
|
||||||
"version": "0.9.105-alpha.9",
|
"version": "0.9.105-alpha.14",
|
||||||
"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": {
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@budibase/client",
|
"name": "@budibase/client",
|
||||||
"version": "0.9.105-alpha.9",
|
"version": "0.9.105-alpha.14",
|
||||||
"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",
|
||||||
|
@ -18,9 +18,9 @@
|
||||||
"dev:builder": "rollup -cw"
|
"dev:builder": "rollup -cw"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@budibase/bbui": "^0.9.105-alpha.9",
|
"@budibase/bbui": "^0.9.105-alpha.14",
|
||||||
"@budibase/standard-components": "^0.9.105-alpha.9",
|
"@budibase/standard-components": "^0.9.105-alpha.14",
|
||||||
"@budibase/string-templates": "^0.9.105-alpha.9",
|
"@budibase/string-templates": "^0.9.105-alpha.14",
|
||||||
"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"
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"name": "@budibase/server",
|
"name": "@budibase/server",
|
||||||
"email": "hi@budibase.com",
|
"email": "hi@budibase.com",
|
||||||
"version": "0.9.105-alpha.9",
|
"version": "0.9.105-alpha.14",
|
||||||
"description": "Budibase Web Server",
|
"description": "Budibase Web Server",
|
||||||
"main": "src/index.js",
|
"main": "src/index.js",
|
||||||
"repository": {
|
"repository": {
|
||||||
|
@ -62,9 +62,9 @@
|
||||||
"author": "Budibase",
|
"author": "Budibase",
|
||||||
"license": "AGPL-3.0-or-later",
|
"license": "AGPL-3.0-or-later",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@budibase/auth": "^0.9.105-alpha.9",
|
"@budibase/auth": "^0.9.105-alpha.14",
|
||||||
"@budibase/client": "^0.9.105-alpha.9",
|
"@budibase/client": "^0.9.105-alpha.14",
|
||||||
"@budibase/string-templates": "^0.9.105-alpha.9",
|
"@budibase/string-templates": "^0.9.105-alpha.14",
|
||||||
"@elastic/elasticsearch": "7.10.0",
|
"@elastic/elasticsearch": "7.10.0",
|
||||||
"@koa/router": "8.0.0",
|
"@koa/router": "8.0.0",
|
||||||
"@sendgrid/mail": "7.1.1",
|
"@sendgrid/mail": "7.1.1",
|
||||||
|
@ -117,7 +117,7 @@
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/core": "^7.14.3",
|
"@babel/core": "^7.14.3",
|
||||||
"@babel/preset-env": "^7.14.4",
|
"@babel/preset-env": "^7.14.4",
|
||||||
"@budibase/standard-components": "^0.9.105-alpha.9",
|
"@budibase/standard-components": "^0.9.105-alpha.14",
|
||||||
"@jest/test-sequencer": "^24.8.0",
|
"@jest/test-sequencer": "^24.8.0",
|
||||||
"@types/bull": "^3.15.1",
|
"@types/bull": "^3.15.1",
|
||||||
"@types/jest": "^26.0.23",
|
"@types/jest": "^26.0.23",
|
||||||
|
|
|
@ -57,7 +57,7 @@ exports.patch = async ctx => {
|
||||||
})
|
})
|
||||||
|
|
||||||
if (!validateResult.valid) {
|
if (!validateResult.valid) {
|
||||||
throw validateResult.errors
|
throw { validation: validateResult.errors }
|
||||||
}
|
}
|
||||||
|
|
||||||
// returned row is cleaned and prepared for writing to DB
|
// returned row is cleaned and prepared for writing to DB
|
||||||
|
@ -105,7 +105,7 @@ exports.save = async function (ctx) {
|
||||||
})
|
})
|
||||||
|
|
||||||
if (!validateResult.valid) {
|
if (!validateResult.valid) {
|
||||||
throw validateResult.errors
|
throw { validation: validateResult.errors }
|
||||||
}
|
}
|
||||||
|
|
||||||
// make sure link rows are up to date
|
// make sure link rows are up to date
|
||||||
|
|
|
@ -58,6 +58,7 @@ router.use(async (ctx, next) => {
|
||||||
ctx.body = {
|
ctx.body = {
|
||||||
message: err.message,
|
message: err.message,
|
||||||
status: ctx.status,
|
status: ctx.status,
|
||||||
|
validationErrors: err.validation,
|
||||||
}
|
}
|
||||||
if (env.NODE_ENV !== "jest") {
|
if (env.NODE_ENV !== "jest") {
|
||||||
ctx.log.error(err)
|
ctx.log.error(err)
|
||||||
|
|
|
@ -1796,6 +1796,11 @@
|
||||||
"label": "Disabled",
|
"label": "Disabled",
|
||||||
"key": "disabled",
|
"key": "disabled",
|
||||||
"defaultValue": false
|
"defaultValue": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "validation/string",
|
||||||
|
"label": "Validation",
|
||||||
|
"key": "validation"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
@ -1830,6 +1835,11 @@
|
||||||
"label": "Disabled",
|
"label": "Disabled",
|
||||||
"key": "disabled",
|
"key": "disabled",
|
||||||
"defaultValue": false
|
"defaultValue": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "validation/number",
|
||||||
|
"label": "Validation",
|
||||||
|
"key": "validation"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
@ -1864,6 +1874,11 @@
|
||||||
"label": "Disabled",
|
"label": "Disabled",
|
||||||
"key": "disabled",
|
"key": "disabled",
|
||||||
"defaultValue": false
|
"defaultValue": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "validation/string",
|
||||||
|
"label": "Validation",
|
||||||
|
"key": "validation"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
@ -1972,6 +1987,11 @@
|
||||||
"setting": "optionsSource",
|
"setting": "optionsSource",
|
||||||
"value": "custom"
|
"value": "custom"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "validation/string",
|
||||||
|
"label": "Validation",
|
||||||
|
"key": "validation"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
@ -2029,6 +2049,11 @@
|
||||||
"label": "Disabled",
|
"label": "Disabled",
|
||||||
"key": "disabled",
|
"key": "disabled",
|
||||||
"defaultValue": false
|
"defaultValue": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "validation/boolean",
|
||||||
|
"label": "Validation",
|
||||||
|
"key": "validation"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
@ -2064,6 +2089,11 @@
|
||||||
"label": "Disabled",
|
"label": "Disabled",
|
||||||
"key": "disabled",
|
"key": "disabled",
|
||||||
"defaultValue": false
|
"defaultValue": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "validation/string",
|
||||||
|
"label": "Validation",
|
||||||
|
"key": "validation"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
@ -2104,6 +2134,11 @@
|
||||||
"label": "Disabled",
|
"label": "Disabled",
|
||||||
"key": "disabled",
|
"key": "disabled",
|
||||||
"defaultValue": false
|
"defaultValue": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "validation/datetime",
|
||||||
|
"label": "Validation",
|
||||||
|
"key": "validation"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
@ -2128,6 +2163,11 @@
|
||||||
"label": "Disabled",
|
"label": "Disabled",
|
||||||
"key": "disabled",
|
"key": "disabled",
|
||||||
"defaultValue": false
|
"defaultValue": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "validation/attachment",
|
||||||
|
"label": "Validation",
|
||||||
|
"key": "validation"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
@ -2157,6 +2197,11 @@
|
||||||
"label": "Disabled",
|
"label": "Disabled",
|
||||||
"key": "disabled",
|
"key": "disabled",
|
||||||
"defaultValue": false
|
"defaultValue": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "validation/link",
|
||||||
|
"label": "Validation",
|
||||||
|
"key": "validation"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|
|
@ -29,11 +29,11 @@
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"svelte"
|
"svelte"
|
||||||
],
|
],
|
||||||
"version": "0.9.105-alpha.9",
|
"version": "0.9.105-alpha.14",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"gitHead": "d1836a898cab3f8ab80ee6d8f42be1a9eed7dcdc",
|
"gitHead": "d1836a898cab3f8ab80ee6d8f42be1a9eed7dcdc",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@budibase/bbui": "^0.9.105-alpha.9",
|
"@budibase/bbui": "^0.9.105-alpha.14",
|
||||||
"@spectrum-css/button": "^3.0.3",
|
"@spectrum-css/button": "^3.0.3",
|
||||||
"@spectrum-css/card": "^3.0.3",
|
"@spectrum-css/card": "^3.0.3",
|
||||||
"@spectrum-css/divider": "^1.0.3",
|
"@spectrum-css/divider": "^1.0.3",
|
||||||
|
|
|
@ -6,6 +6,7 @@
|
||||||
export let field
|
export let field
|
||||||
export let label
|
export let label
|
||||||
export let disabled = false
|
export let disabled = false
|
||||||
|
export let validation
|
||||||
|
|
||||||
let fieldState
|
let fieldState
|
||||||
let fieldApi
|
let fieldApi
|
||||||
|
@ -35,6 +36,7 @@
|
||||||
{label}
|
{label}
|
||||||
{field}
|
{field}
|
||||||
{disabled}
|
{disabled}
|
||||||
|
{validation}
|
||||||
type="attachment"
|
type="attachment"
|
||||||
bind:fieldState
|
bind:fieldState
|
||||||
bind:fieldApi
|
bind:fieldApi
|
||||||
|
@ -44,6 +46,7 @@
|
||||||
<CoreDropzone
|
<CoreDropzone
|
||||||
value={$fieldState.value}
|
value={$fieldState.value}
|
||||||
disabled={$fieldState.disabled}
|
disabled={$fieldState.disabled}
|
||||||
|
error={$fieldState.error}
|
||||||
on:change={e => {
|
on:change={e => {
|
||||||
fieldApi.setValue(e.detail)
|
fieldApi.setValue(e.detail)
|
||||||
}}
|
}}
|
||||||
|
|
|
@ -7,6 +7,7 @@
|
||||||
export let text
|
export let text
|
||||||
export let disabled = false
|
export let disabled = false
|
||||||
export let size
|
export let size
|
||||||
|
export let validation
|
||||||
export let defaultValue
|
export let defaultValue
|
||||||
|
|
||||||
let fieldState
|
let fieldState
|
||||||
|
@ -30,6 +31,7 @@
|
||||||
{label}
|
{label}
|
||||||
{field}
|
{field}
|
||||||
{disabled}
|
{disabled}
|
||||||
|
{validation}
|
||||||
defaultValue={isTruthy(defaultValue)}
|
defaultValue={isTruthy(defaultValue)}
|
||||||
type="boolean"
|
type="boolean"
|
||||||
bind:fieldState
|
bind:fieldState
|
||||||
|
|
|
@ -7,6 +7,7 @@
|
||||||
export let placeholder
|
export let placeholder
|
||||||
export let disabled = false
|
export let disabled = false
|
||||||
export let enableTime = false
|
export let enableTime = false
|
||||||
|
export let validation
|
||||||
export let defaultValue
|
export let defaultValue
|
||||||
|
|
||||||
let fieldState
|
let fieldState
|
||||||
|
@ -42,6 +43,7 @@
|
||||||
{label}
|
{label}
|
||||||
{field}
|
{field}
|
||||||
{disabled}
|
{disabled}
|
||||||
|
{validation}
|
||||||
defaultValue={parseDate(defaultValue)}
|
defaultValue={parseDate(defaultValue)}
|
||||||
type="datetime"
|
type="datetime"
|
||||||
bind:fieldState
|
bind:fieldState
|
||||||
|
|
|
@ -11,6 +11,7 @@
|
||||||
export let defaultValue
|
export let defaultValue
|
||||||
export let type
|
export let type
|
||||||
export let disabled = false
|
export let disabled = false
|
||||||
|
export let validation
|
||||||
|
|
||||||
// Get contexts
|
// Get contexts
|
||||||
const formContext = getContext("form")
|
const formContext = getContext("form")
|
||||||
|
@ -21,13 +22,21 @@
|
||||||
// Register field with form
|
// Register field with form
|
||||||
const formApi = formContext?.formApi
|
const formApi = formContext?.formApi
|
||||||
const labelPosition = fieldGroupContext?.labelPosition || "above"
|
const labelPosition = fieldGroupContext?.labelPosition || "above"
|
||||||
const formField = formApi?.registerField(field, defaultValue, disabled)
|
const formField = formApi?.registerField(
|
||||||
|
field,
|
||||||
|
defaultValue,
|
||||||
|
disabled,
|
||||||
|
validation
|
||||||
|
)
|
||||||
|
|
||||||
// Expose field properties to parent component
|
// Expose field properties to parent component
|
||||||
fieldState = formField?.fieldState
|
fieldState = formField?.fieldState
|
||||||
fieldApi = formField?.fieldApi
|
fieldApi = formField?.fieldApi
|
||||||
fieldSchema = formField?.fieldSchema
|
fieldSchema = formField?.fieldSchema
|
||||||
|
|
||||||
|
// Keep validation rules up to date
|
||||||
|
$: fieldApi?.updateValidation(validation)
|
||||||
|
|
||||||
// Extract label position from field group context
|
// Extract label position from field group context
|
||||||
$: labelPositionClass =
|
$: labelPositionClass =
|
||||||
labelPosition === "above" ? "" : `spectrum-FieldLabel--${labelPosition}`
|
labelPosition === "above" ? "" : `spectrum-FieldLabel--${labelPosition}`
|
||||||
|
|
|
@ -21,7 +21,12 @@
|
||||||
|
|
||||||
// Form API contains functions to control the form
|
// Form API contains functions to control the form
|
||||||
const formApi = {
|
const formApi = {
|
||||||
registerField: (field, defaultValue = null, fieldDisabled = false) => {
|
registerField: (
|
||||||
|
field,
|
||||||
|
defaultValue = null,
|
||||||
|
fieldDisabled = false,
|
||||||
|
validationRules
|
||||||
|
) => {
|
||||||
if (!field) {
|
if (!field) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -30,17 +35,23 @@
|
||||||
const isAutoColumn = !!schema?.[field]?.autocolumn
|
const isAutoColumn = !!schema?.[field]?.autocolumn
|
||||||
|
|
||||||
// Create validation function based on field schema
|
// Create validation function based on field schema
|
||||||
const constraints = schema?.[field]?.constraints
|
const schemaConstraints = schema?.[field]?.constraints
|
||||||
const validate = createValidatorFromConstraints(constraints, field, table)
|
const validator = createValidatorFromConstraints(
|
||||||
|
schemaConstraints,
|
||||||
|
validationRules,
|
||||||
|
field,
|
||||||
|
table
|
||||||
|
)
|
||||||
|
|
||||||
// Construct field object
|
// Construct field object
|
||||||
fieldMap[field] = {
|
fieldMap[field] = {
|
||||||
fieldState: makeFieldState(
|
fieldState: makeFieldState(
|
||||||
field,
|
field,
|
||||||
|
validator,
|
||||||
defaultValue,
|
defaultValue,
|
||||||
disabled || fieldDisabled || isAutoColumn
|
disabled || fieldDisabled || isAutoColumn
|
||||||
),
|
),
|
||||||
fieldApi: makeFieldApi(field, defaultValue, validate),
|
fieldApi: makeFieldApi(field, defaultValue),
|
||||||
fieldSchema: schema?.[field] ?? {},
|
fieldSchema: schema?.[field] ?? {},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -83,9 +94,11 @@
|
||||||
]
|
]
|
||||||
|
|
||||||
// Creates an API for a specific field
|
// Creates an API for a specific field
|
||||||
const makeFieldApi = (field, defaultValue, validate) => {
|
const makeFieldApi = field => {
|
||||||
|
// Sets the value for a certain field and invokes validation
|
||||||
const setValue = (value, skipCheck = false) => {
|
const setValue = (value, skipCheck = false) => {
|
||||||
const { fieldState } = fieldMap[field]
|
const { fieldState } = fieldMap[field]
|
||||||
|
const { validator } = get(fieldState)
|
||||||
|
|
||||||
// Skip if the value is the same
|
// Skip if the value is the same
|
||||||
if (!skipCheck && get(fieldState).value === value) {
|
if (!skipCheck && get(fieldState).value === value) {
|
||||||
|
@ -93,7 +106,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update field state
|
// Update field state
|
||||||
const error = validate ? validate(value) : null
|
const error = validator ? validator(value) : null
|
||||||
fieldState.update(state => {
|
fieldState.update(state => {
|
||||||
state.value = value
|
state.value = value
|
||||||
state.error = error
|
state.error = error
|
||||||
|
@ -115,15 +128,20 @@
|
||||||
return !error
|
return !error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Clears the value of a certain field back to the initial value
|
||||||
const clearValue = () => {
|
const clearValue = () => {
|
||||||
const { fieldState } = fieldMap[field]
|
const { fieldState } = fieldMap[field]
|
||||||
|
const { defaultValue } = get(fieldState)
|
||||||
const newValue = initialValues[field] ?? defaultValue
|
const newValue = initialValues[field] ?? defaultValue
|
||||||
|
|
||||||
|
// Update field state
|
||||||
fieldState.update(state => {
|
fieldState.update(state => {
|
||||||
state.value = newValue
|
state.value = newValue
|
||||||
state.error = null
|
state.error = null
|
||||||
return state
|
return state
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Update form state
|
||||||
formState.update(state => {
|
formState.update(state => {
|
||||||
state.values = { ...state.values, [field]: newValue }
|
state.values = { ...state.values, [field]: newValue }
|
||||||
delete state.errors[field]
|
delete state.errors[field]
|
||||||
|
@ -132,9 +150,37 @@
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Updates the validator rules for a certain field
|
||||||
|
const updateValidation = validationRules => {
|
||||||
|
const { fieldState } = fieldMap[field]
|
||||||
|
const { value, error } = get(fieldState)
|
||||||
|
|
||||||
|
// Create new validator
|
||||||
|
const schemaConstraints = schema?.[field]?.constraints
|
||||||
|
const validator = createValidatorFromConstraints(
|
||||||
|
schemaConstraints,
|
||||||
|
validationRules,
|
||||||
|
field,
|
||||||
|
table
|
||||||
|
)
|
||||||
|
|
||||||
|
// Update validator
|
||||||
|
fieldState.update(state => {
|
||||||
|
state.validator = validator
|
||||||
|
return state
|
||||||
|
})
|
||||||
|
|
||||||
|
// If there is currently an error, run the validator again in case
|
||||||
|
// the error should be cleared by the new validation rules
|
||||||
|
if (error) {
|
||||||
|
setValue(value, true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
setValue,
|
setValue,
|
||||||
clearValue,
|
clearValue,
|
||||||
|
updateValidation,
|
||||||
validate: () => {
|
validate: () => {
|
||||||
const { fieldState } = fieldMap[field]
|
const { fieldState } = fieldMap[field]
|
||||||
setValue(get(fieldState).value, true)
|
setValue(get(fieldState).value, true)
|
||||||
|
@ -143,13 +189,15 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
// Creates observable state data about a specific field
|
// Creates observable state data about a specific field
|
||||||
const makeFieldState = (field, defaultValue, fieldDisabled) => {
|
const makeFieldState = (field, validator, defaultValue, fieldDisabled) => {
|
||||||
return writable({
|
return writable({
|
||||||
field,
|
field,
|
||||||
fieldId: `id-${generateID()}`,
|
fieldId: `id-${generateID()}`,
|
||||||
value: initialValues[field] ?? defaultValue,
|
value: initialValues[field] ?? defaultValue,
|
||||||
error: null,
|
error: null,
|
||||||
disabled: fieldDisabled,
|
disabled: fieldDisabled,
|
||||||
|
defaultValue,
|
||||||
|
validator,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -6,6 +6,7 @@
|
||||||
export let label
|
export let label
|
||||||
export let placeholder
|
export let placeholder
|
||||||
export let disabled = false
|
export let disabled = false
|
||||||
|
export let validation
|
||||||
export let defaultValue = ""
|
export let defaultValue = ""
|
||||||
|
|
||||||
let fieldState
|
let fieldState
|
||||||
|
@ -16,6 +17,7 @@
|
||||||
{label}
|
{label}
|
||||||
{field}
|
{field}
|
||||||
{disabled}
|
{disabled}
|
||||||
|
{validation}
|
||||||
{defaultValue}
|
{defaultValue}
|
||||||
type="longform"
|
type="longform"
|
||||||
bind:fieldState
|
bind:fieldState
|
||||||
|
|
|
@ -7,6 +7,7 @@
|
||||||
export let placeholder
|
export let placeholder
|
||||||
export let disabled = false
|
export let disabled = false
|
||||||
export let optionsType = "select"
|
export let optionsType = "select"
|
||||||
|
export let validation
|
||||||
export let defaultValue
|
export let defaultValue
|
||||||
export let optionsSource = "schema"
|
export let optionsSource = "schema"
|
||||||
export let dataProvider
|
export let dataProvider
|
||||||
|
@ -65,6 +66,7 @@
|
||||||
{field}
|
{field}
|
||||||
{label}
|
{label}
|
||||||
{disabled}
|
{disabled}
|
||||||
|
{validation}
|
||||||
{defaultValue}
|
{defaultValue}
|
||||||
type="options"
|
type="options"
|
||||||
bind:fieldState
|
bind:fieldState
|
||||||
|
|
|
@ -9,6 +9,7 @@
|
||||||
export let label
|
export let label
|
||||||
export let placeholder
|
export let placeholder
|
||||||
export let disabled = false
|
export let disabled = false
|
||||||
|
export let validation
|
||||||
|
|
||||||
let fieldState
|
let fieldState
|
||||||
let fieldApi
|
let fieldApi
|
||||||
|
@ -64,6 +65,7 @@
|
||||||
{label}
|
{label}
|
||||||
{field}
|
{field}
|
||||||
{disabled}
|
{disabled}
|
||||||
|
{validation}
|
||||||
type="link"
|
type="link"
|
||||||
bind:fieldState
|
bind:fieldState
|
||||||
bind:fieldApi
|
bind:fieldApi
|
||||||
|
|
|
@ -7,6 +7,7 @@
|
||||||
export let placeholder
|
export let placeholder
|
||||||
export let type = "text"
|
export let type = "text"
|
||||||
export let disabled = false
|
export let disabled = false
|
||||||
|
export let validation
|
||||||
export let defaultValue = ""
|
export let defaultValue = ""
|
||||||
|
|
||||||
let fieldState
|
let fieldState
|
||||||
|
@ -17,6 +18,7 @@
|
||||||
{label}
|
{label}
|
||||||
{field}
|
{field}
|
||||||
{disabled}
|
{disabled}
|
||||||
|
{validation}
|
||||||
{defaultValue}
|
{defaultValue}
|
||||||
type={type === "number" ? "number" : "string"}
|
type={type === "number" ? "number" : "string"}
|
||||||
bind:fieldState
|
bind:fieldState
|
||||||
|
|
|
@ -1,54 +1,108 @@
|
||||||
import flatpickr from "flatpickr"
|
import flatpickr from "flatpickr"
|
||||||
|
|
||||||
export const createValidatorFromConstraints = (constraints, field, table) => {
|
/**
|
||||||
let checks = []
|
* Creates a validation function from a combination of schema-level constraints
|
||||||
|
* and custom validation rules
|
||||||
|
* @param schemaConstraints any schema level constraints from the table
|
||||||
|
* @param customRules any custom validation rules
|
||||||
|
* @param field the field name we are evaluating
|
||||||
|
* @param table the definition of the table we are evaluating
|
||||||
|
* @returns {function} a validator function which accepts test values
|
||||||
|
*/
|
||||||
|
export const createValidatorFromConstraints = (
|
||||||
|
schemaConstraints,
|
||||||
|
customRules,
|
||||||
|
field,
|
||||||
|
table
|
||||||
|
) => {
|
||||||
|
let rules = []
|
||||||
|
|
||||||
if (constraints) {
|
// Convert schema constraints into validation rules
|
||||||
|
if (schemaConstraints) {
|
||||||
// Required constraint
|
// Required constraint
|
||||||
if (
|
if (
|
||||||
field === table?.primaryDisplay ||
|
field === table?.primaryDisplay ||
|
||||||
constraints.presence?.allowEmpty === false
|
schemaConstraints.presence?.allowEmpty === false
|
||||||
) {
|
) {
|
||||||
checks.push(presenceConstraint)
|
rules.push({
|
||||||
|
type: "string",
|
||||||
|
constraint: "required",
|
||||||
|
error: "Required",
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// String length constraint
|
// String length constraint
|
||||||
if (exists(constraints.length?.maximum)) {
|
if (exists(schemaConstraints.length?.maximum)) {
|
||||||
const length = constraints.length.maximum
|
const length = schemaConstraints.length.maximum
|
||||||
checks.push(lengthConstraint(length))
|
rules.push({
|
||||||
|
type: "string",
|
||||||
|
constraint: "length",
|
||||||
|
value: length,
|
||||||
|
error: `Maximum length is ${length}`,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// Min / max number constraint
|
// Min / max number constraint
|
||||||
if (exists(constraints.numericality?.greaterThanOrEqualTo)) {
|
if (exists(schemaConstraints.numericality?.greaterThanOrEqualTo)) {
|
||||||
const min = constraints.numericality.greaterThanOrEqualTo
|
const min = schemaConstraints.numericality.greaterThanOrEqualTo
|
||||||
checks.push(numericalConstraint(x => x >= min, `Minimum value is ${min}`))
|
rules.push({
|
||||||
|
type: "number",
|
||||||
|
constraint: "minValue",
|
||||||
|
value: min,
|
||||||
|
error: `Minimum value is ${min}`,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
if (exists(constraints.numericality?.lessThanOrEqualTo)) {
|
if (exists(schemaConstraints.numericality?.lessThanOrEqualTo)) {
|
||||||
const max = constraints.numericality.lessThanOrEqualTo
|
const max = schemaConstraints.numericality.lessThanOrEqualTo
|
||||||
checks.push(numericalConstraint(x => x <= max, `Maximum value is ${max}`))
|
rules.push({
|
||||||
|
type: "number",
|
||||||
|
constraint: "maxValue",
|
||||||
|
value: max,
|
||||||
|
error: `Maximum value is ${max}`,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// Inclusion constraint
|
// Inclusion constraint
|
||||||
if (exists(constraints.inclusion)) {
|
if (exists(schemaConstraints.inclusion)) {
|
||||||
const options = constraints.inclusion
|
const options = schemaConstraints.inclusion || []
|
||||||
checks.push(inclusionConstraint(options))
|
rules.push({
|
||||||
|
type: "string",
|
||||||
|
constraint: "inclusion",
|
||||||
|
value: options,
|
||||||
|
error: "Invalid value",
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// Date constraint
|
// Date constraint
|
||||||
if (exists(constraints.datetime?.earliest)) {
|
if (exists(schemaConstraints.datetime?.earliest)) {
|
||||||
const limit = constraints.datetime.earliest
|
const limit = schemaConstraints.datetime.earliest
|
||||||
checks.push(dateConstraint(limit, true))
|
const limitString = flatpickr.formatDate(new Date(limit), "F j Y, H:i")
|
||||||
|
rules.push({
|
||||||
|
type: "datetime",
|
||||||
|
constraint: "minValue",
|
||||||
|
value: limit,
|
||||||
|
error: `Earliest date is ${limitString}`,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
if (exists(constraints.datetime?.latest)) {
|
if (exists(schemaConstraints.datetime?.latest)) {
|
||||||
const limit = constraints.datetime.latest
|
const limit = schemaConstraints.datetime.latest
|
||||||
checks.push(dateConstraint(limit, false))
|
const limitString = flatpickr.formatDate(new Date(limit), "F j Y, H:i")
|
||||||
|
rules.push({
|
||||||
|
type: "datetime",
|
||||||
|
constraint: "maxValue",
|
||||||
|
value: limit,
|
||||||
|
error: `Latest date is ${limitString}`,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Add custom validation rules
|
||||||
|
rules = rules.concat(customRules || [])
|
||||||
|
|
||||||
// Evaluate each constraint
|
// Evaluate each constraint
|
||||||
return value => {
|
return value => {
|
||||||
for (let check of checks) {
|
for (let rule of rules) {
|
||||||
const error = check(value)
|
const error = evaluateRule(rule, value)
|
||||||
if (error) {
|
if (error) {
|
||||||
return error
|
return error
|
||||||
}
|
}
|
||||||
|
@ -57,61 +111,197 @@ export const createValidatorFromConstraints = (constraints, field, table) => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const exists = value => value != null && value !== ""
|
/**
|
||||||
|
* Evaluates a validation rule against a value and optionally returns
|
||||||
const presenceConstraint = value => {
|
* an error if the validation fails.
|
||||||
let invalid
|
* @param rule the rule object to evaluate
|
||||||
if (Array.isArray(value)) {
|
* @param value the value to validate against
|
||||||
invalid = value.length === 0
|
* @returns {null|*} an error if validation fails or null if it passes
|
||||||
} else {
|
*/
|
||||||
invalid = value == null || value === ""
|
const evaluateRule = (rule, value) => {
|
||||||
}
|
if (!rule) {
|
||||||
return invalid ? "Required" : null
|
|
||||||
}
|
|
||||||
|
|
||||||
const lengthConstraint = maxLength => value => {
|
|
||||||
if (value && value.length > maxLength) {
|
|
||||||
return `Maximum ${maxLength} characters`
|
|
||||||
}
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
const numericalConstraint = (constraint, error) => value => {
|
|
||||||
if (value == null || value === "") {
|
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Determine the correct handler for this rule
|
||||||
|
const handler = handlerMap[rule.constraint]
|
||||||
|
if (!handler) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
// Coerce input value into correct type
|
||||||
|
value = parseType(value, rule.type)
|
||||||
|
|
||||||
|
// Evaluate the rule
|
||||||
|
const pass = handler(value, rule)
|
||||||
|
return pass ? null : rule.error || "Error"
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parses a value to the specified type so that values are always compared
|
||||||
|
* in the same format.
|
||||||
|
* @param value the value to parse
|
||||||
|
* @param type the type to parse
|
||||||
|
* @returns {boolean|string|*|number|null} the parsed value, or null if invalid
|
||||||
|
*/
|
||||||
|
const parseType = (value, type) => {
|
||||||
|
// Treat nulls or empty strings as null
|
||||||
|
if (!exists(value) || !type) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse as string
|
||||||
|
if (type === "string") {
|
||||||
|
if (typeof value === "string" || Array.isArray(value)) {
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
if (value.length === 0) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
return `${value}`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse as number
|
||||||
|
if (type === "number") {
|
||||||
if (isNaN(value)) {
|
if (isNaN(value)) {
|
||||||
return "Must be a number"
|
return null
|
||||||
}
|
}
|
||||||
const number = parseFloat(value)
|
return parseFloat(value)
|
||||||
if (!constraint(number)) {
|
|
||||||
return error
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Parse as date
|
||||||
|
if (type === "datetime") {
|
||||||
|
if (value instanceof Date) {
|
||||||
|
return value.getTime()
|
||||||
|
}
|
||||||
|
const time = isNaN(value) ? Date.parse(value) : new Date(value).getTime()
|
||||||
|
return isNaN(time) ? null : time
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse as boolean
|
||||||
|
if (type === "boolean") {
|
||||||
|
if (typeof value === "string") {
|
||||||
|
return value.toLowerCase() === "true"
|
||||||
|
}
|
||||||
|
return value === true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse attachments, treating no elements as null
|
||||||
|
if (type === "attachment") {
|
||||||
|
if (!Array.isArray(value) || !value.length) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse links, treating no elements as null
|
||||||
|
if (type === "link") {
|
||||||
|
if (!Array.isArray(value) || !value.length) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
||||||
|
// If some unknown type, treat as null to avoid breaking validators
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
const inclusionConstraint =
|
// Evaluates a required constraint
|
||||||
(options = []) =>
|
const requiredHandler = value => {
|
||||||
value => {
|
return value != null
|
||||||
if (value == null || value === "") {
|
}
|
||||||
return null
|
|
||||||
}
|
// Evaluates a min length constraint
|
||||||
if (!options.includes(value)) {
|
const minLengthHandler = (value, rule) => {
|
||||||
return "Invalid value"
|
const limit = parseType(rule.value, "number")
|
||||||
}
|
return value && value.length >= limit
|
||||||
return null
|
}
|
||||||
}
|
|
||||||
|
// Evaluates a max length constraint
|
||||||
const dateConstraint = (dateString, isEarliest) => {
|
const maxLengthHandler = (value, rule) => {
|
||||||
const dateLimit = Date.parse(dateString)
|
const limit = parseType(rule.value, "number")
|
||||||
return value => {
|
return value == null || value.length <= limit
|
||||||
if (value == null || value === "") {
|
}
|
||||||
return null
|
|
||||||
}
|
// Evaluates a min value constraint
|
||||||
const dateValue = Date.parse(value)
|
const minValueHandler = (value, rule) => {
|
||||||
const valid = isEarliest ? dateValue >= dateLimit : dateValue <= dateLimit
|
// Use same type as the value so that things can be compared
|
||||||
const adjective = isEarliest ? "Earliest" : "Latest"
|
const limit = parseType(rule.value, rule.type)
|
||||||
const limitString = flatpickr.formatDate(new Date(dateLimit), "F j Y, H:i")
|
return value && value >= limit
|
||||||
return valid ? null : `${adjective} is ${limitString}`
|
}
|
||||||
}
|
|
||||||
|
// Evaluates a max value constraint
|
||||||
|
const maxValueHandler = (value, rule) => {
|
||||||
|
// Use same type as the value so that things can be compared
|
||||||
|
const limit = parseType(rule.value, rule.type)
|
||||||
|
return value == null || value <= limit
|
||||||
|
}
|
||||||
|
|
||||||
|
// Evaluates an inclusion constraint
|
||||||
|
const inclusionHandler = (value, rule) => {
|
||||||
|
return value == null || rule.value.includes(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Evaluates an equal constraint
|
||||||
|
const equalHandler = (value, rule) => {
|
||||||
|
const ruleValue = parseType(rule.value, rule.type)
|
||||||
|
return value === ruleValue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Evaluates a not equal constraint
|
||||||
|
const notEqualHandler = (value, rule) => {
|
||||||
|
const ruleValue = parseType(rule.value, rule.type)
|
||||||
|
if (value == null && ruleValue == null) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return value !== ruleValue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Evaluates a regex constraint
|
||||||
|
const regexHandler = (value, rule) => {
|
||||||
|
const regex = parseType(rule.value, "string")
|
||||||
|
return new RegExp(regex).test(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Evaluates a not regex constraint
|
||||||
|
const notRegexHandler = (value, rule) => {
|
||||||
|
return !regexHandler(value, rule)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Evaluates a contains constraint
|
||||||
|
const containsHandler = (value, rule) => {
|
||||||
|
const expectedValue = parseType(rule.value, "string")
|
||||||
|
return value && value.includes(expectedValue)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Evaluates a not contains constraint
|
||||||
|
const notContainsHandler = (value, rule) => {
|
||||||
|
return !containsHandler(value, rule)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Map of constraint types to handlers.
|
||||||
|
*/
|
||||||
|
const handlerMap = {
|
||||||
|
required: requiredHandler,
|
||||||
|
minLength: minLengthHandler,
|
||||||
|
maxLength: maxLengthHandler,
|
||||||
|
minValue: minValueHandler,
|
||||||
|
maxValue: maxValueHandler,
|
||||||
|
inclusion: inclusionHandler,
|
||||||
|
equal: equalHandler,
|
||||||
|
notEqual: notEqualHandler,
|
||||||
|
regex: regexHandler,
|
||||||
|
notRegex: notRegexHandler,
|
||||||
|
contains: containsHandler,
|
||||||
|
notContains: notContainsHandler,
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper to check for null, undefined or empty string values
|
||||||
|
* @param value the value to test
|
||||||
|
* @returns {boolean} whether the value exists or not
|
||||||
|
*/
|
||||||
|
const exists = value => {
|
||||||
|
return value != null && value !== ""
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@budibase/string-templates",
|
"name": "@budibase/string-templates",
|
||||||
"version": "0.9.105-alpha.9",
|
"version": "0.9.105-alpha.14",
|
||||||
"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,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"name": "@budibase/worker",
|
"name": "@budibase/worker",
|
||||||
"email": "hi@budibase.com",
|
"email": "hi@budibase.com",
|
||||||
"version": "0.9.105-alpha.9",
|
"version": "0.9.105-alpha.14",
|
||||||
"description": "Budibase background service",
|
"description": "Budibase background service",
|
||||||
"main": "src/index.js",
|
"main": "src/index.js",
|
||||||
"repository": {
|
"repository": {
|
||||||
|
@ -23,8 +23,8 @@
|
||||||
"author": "Budibase",
|
"author": "Budibase",
|
||||||
"license": "AGPL-3.0-or-later",
|
"license": "AGPL-3.0-or-later",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@budibase/auth": "^0.9.105-alpha.9",
|
"@budibase/auth": "^0.9.105-alpha.14",
|
||||||
"@budibase/string-templates": "^0.9.105-alpha.9",
|
"@budibase/string-templates": "^0.9.105-alpha.14",
|
||||||
"@koa/router": "^8.0.0",
|
"@koa/router": "^8.0.0",
|
||||||
"@techpass/passport-openidconnect": "^0.3.0",
|
"@techpass/passport-openidconnect": "^0.3.0",
|
||||||
"aws-sdk": "^2.811.0",
|
"aws-sdk": "^2.811.0",
|
||||||
|
|
Loading…
Reference in New Issue