Merge branch 'dynamic-picker-options' of github.com:Budibase/budibase into dynamic-picker-options

This commit is contained in:
Andrew Kingston 2021-08-17 11:37:50 +01:00
commit 8f676dd9bf
40 changed files with 1079 additions and 168 deletions

235
i18n/README.de.md Normal file
View File

@ -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!

View File

@ -1,5 +1,5 @@
{ {
"version": "0.9.105-alpha.9", "version": "0.9.105-alpha.14",
"npmClient": "yarn", "npmClient": "yarn",
"packages": [ "packages": [
"packages/*" "packages/*"

View File

@ -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",

View File

@ -1,7 +1,7 @@
{ {
"name": "@budibase/bbui", "name": "@budibase/bbui",
"description": "A UI solution used in the different Budibase projects.", "description": "A UI solution used in the different Budibase projects.",
"version": "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",

View File

@ -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);
} }

View File

@ -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",

View File

@ -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
} }

View File

@ -87,6 +87,7 @@
placeholder: setting.placeholder, placeholder: setting.placeholder,
}} }}
{bindings} {bindings}
{componentDefinition}
/> />
{/if} {/if}
{/each} {/each}

View File

@ -1,5 +0,0 @@
<script>
import FormFieldSelect from "./FormFieldSelect.svelte"
</script>
<FormFieldSelect {...$$props} on:change type="attachment" />

View File

@ -1,5 +0,0 @@
<script>
import FormFieldSelect from "./FormFieldSelect.svelte"
</script>
<FormFieldSelect {...$$props} on:change type="boolean" />

View File

@ -1,5 +0,0 @@
<script>
import FormFieldSelect from "./FormFieldSelect.svelte"
</script>
<FormFieldSelect {...$$props} on:change type="datetime" />

View File

@ -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])

View File

@ -1,5 +0,0 @@
<script>
import FormFieldSelect from "./FormFieldSelect.svelte"
</script>
<FormFieldSelect {...$$props} on:change type="longform" />

View File

@ -1,5 +0,0 @@
<script>
import FormFieldSelect from "./FormFieldSelect.svelte"
</script>
<FormFieldSelect {...$$props} on:change type="number" />

View File

@ -1,5 +0,0 @@
<script>
import FormFieldSelect from "./FormFieldSelect.svelte"
</script>
<FormFieldSelect {...$$props} on:change type="options" />

View File

@ -1,5 +0,0 @@
<script>
import FormFieldSelect from "./FormFieldSelect.svelte"
</script>
<FormFieldSelect {...$$props} on:change type="link" />

View File

@ -1,5 +0,0 @@
<script>
import FormFieldSelect from "./FormFieldSelect.svelte"
</script>
<FormFieldSelect {...$$props} on:change type="string" />

View File

@ -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>

View File

@ -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>

View File

@ -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 => {

View File

@ -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}

View File

@ -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": {

View File

@ -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"

View File

@ -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",

View File

@ -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

View File

@ -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)

View File

@ -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"
} }
] ]
}, },

View File

@ -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",

View File

@ -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)
}} }}

View File

@ -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

View File

@ -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

View File

@ -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}`

View File

@ -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,
}) })
} }

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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 !== ""
} }

View File

@ -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",

View File

@ -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",