Merge branch 'master' of github.com:Budibase/budibase into table-width-setting
This commit is contained in:
commit
182b6463e0
|
@ -54,7 +54,8 @@
|
|||
"ignoreRestSiblings": true
|
||||
}
|
||||
],
|
||||
"local-rules/no-budibase-imports": "error"
|
||||
"no-redeclare": "off",
|
||||
"@typescript-eslint/no-redeclare": "error"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
|
|
@ -170,7 +170,8 @@ jobs:
|
|||
docker pull mongo:7.0-jammy &
|
||||
docker pull mariadb:lts &
|
||||
docker pull testcontainers/ryuk:0.5.1 &
|
||||
docker pull budibase/couchdb:v3.2.1-sql &
|
||||
docker pull budibase/couchdb:v3.2.1-sqs &
|
||||
docker pull minio/minio &
|
||||
docker pull redis &
|
||||
|
||||
wait $(jobs -p)
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
apiVersion: {{ ternary "autoscaling/v2" "autoscaling/v2beta2" (.Capabilities.APIVersions.Has "autoscaling/v2") }}
|
||||
kind: HorizontalPodAutoscaler
|
||||
metadata:
|
||||
name: {{ include "budibase.fullname" . }}-apps
|
||||
name: {{ include "budibase.fullname" . }}-automation-worker
|
||||
labels:
|
||||
{{- include "budibase.labels" . | nindent 4 }}
|
||||
spec:
|
||||
|
|
|
@ -46,7 +46,7 @@ export default async function setup() {
|
|||
await killContainers(containers)
|
||||
|
||||
try {
|
||||
let couchdb = new GenericContainer("budibase/couchdb:v3.2.1-sqs")
|
||||
const couchdb = new GenericContainer("budibase/couchdb:v3.2.1-sqs")
|
||||
.withExposedPorts(5984, 4984)
|
||||
.withEnvironment({
|
||||
COUCHDB_PASSWORD: "budibase",
|
||||
|
@ -69,7 +69,20 @@ export default async function setup() {
|
|||
).withStartupTimeout(20000)
|
||||
)
|
||||
|
||||
await couchdb.start()
|
||||
const minio = new GenericContainer("minio/minio")
|
||||
.withExposedPorts(9000)
|
||||
.withCommand(["server", "/data"])
|
||||
.withEnvironment({
|
||||
MINIO_ACCESS_KEY: "budibase",
|
||||
MINIO_SECRET_KEY: "budibase",
|
||||
})
|
||||
.withLabels({ "com.budibase": "true" })
|
||||
.withReuse()
|
||||
.withWaitStrategy(
|
||||
Wait.forHttp("/minio/health/ready", 9000).withStartupTimeout(10000)
|
||||
)
|
||||
|
||||
await Promise.all([couchdb.start(), minio.start()])
|
||||
} finally {
|
||||
lockfile.unlockSync(lockPath)
|
||||
}
|
||||
|
|
|
@ -70,10 +70,10 @@ sed -i "s#COUCHDB_ERLANG_COOKIE#${COUCHDB_ERLANG_COOKIE}#g" /opt/clouseau/clouse
|
|||
/opt/clouseau/bin/clouseau > /dev/stdout 2>&1 &
|
||||
|
||||
# Start CouchDB.
|
||||
/docker-entrypoint.sh /opt/couchdb/bin/couchdb &
|
||||
/docker-entrypoint.sh /opt/couchdb/bin/couchdb > /dev/stdout 2>&1 &
|
||||
|
||||
# Start SQS.
|
||||
/opt/sqs/sqs --server "http://localhost:5984" --data-dir ${DATA_DIR}/sqs --bind-address=0.0.0.0 &
|
||||
# Start SQS. Use 127.0.0.1 instead of localhost to avoid IPv6 issues.
|
||||
/opt/sqs/sqs --server "http://127.0.0.1:5984" --data-dir ${DATA_DIR}/sqs --bind-address=0.0.0.0 > /dev/stdout 2>&1 &
|
||||
|
||||
# Wait for CouchDB to start up.
|
||||
while [[ $(curl -s -w "%{http_code}\n" http://localhost:5984/_up -o /dev/null) -ne 200 ]]; do
|
||||
|
|
|
@ -0,0 +1,213 @@
|
|||
<p align="center">
|
||||
<a href="https://www.budibase.com">
|
||||
<img alt="Budibase" src="https://res.cloudinary.com/daog6scxm/image/upload/v1696515725/Branding/Assets/Symbol/RGB/Full%20Colour/Budibase_Symbol_RGB_FullColour_cbqvha_1_z5cwq2.svg" width="60" />
|
||||
</a>
|
||||
</p>
|
||||
<h1 align="center">
|
||||
Budibase
|
||||
</h1>
|
||||
|
||||
<h3 align="center">
|
||||
La piattaforma low-code che amerai utilizzare
|
||||
</h3>
|
||||
<p align="center">
|
||||
Budibase è una piattaforma low-code open source ed è il modo più semplice per creare strumenti interni che migliorano la produttività.
|
||||
</p>
|
||||
|
||||
<h3 align="center">
|
||||
🤖 🎨 🚀
|
||||
</h3>
|
||||
<br>
|
||||
|
||||
<p align="center">
|
||||
<img alt="Budibase design ui" src="https://res.cloudinary.com/daog6scxm/image/upload/v1633524049/ui/design-ui-wide-mobile_gdaveq.jpg">
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://github.com/Budibase/budibase/releases">
|
||||
<img alt="GitHub tutte le release" src="https://img.shields.io/github/downloads/Budibase/budibase/total">
|
||||
</a>
|
||||
<a href="https://github.com/Budibase/budibase/releases">
|
||||
<img alt="GitHub release (ordine cronologico)" 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="Segui @budibase" />
|
||||
</a>
|
||||
<img src="https://img.shields.io/badge/Contributor%20Covenant-v2.0%20adopted-ff69b4.svg" alt="Codice di condotta" />
|
||||
<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">Inizia</a>
|
||||
<span> · </span>
|
||||
<a href="https://docs.budibase.com">Documentazione</a>
|
||||
<span> · </span>
|
||||
<a href="https://github.com/Budibase/budibase/discussions?discussions_q=category%3AIdeas">Richieste di miglioramento</a>
|
||||
<span> · </span>
|
||||
<a href="https://github.com/Budibase/budibase/issues">Segnala un bug</a>
|
||||
<span> · </span>
|
||||
Supporto: <a href="https://github.com/Budibase/budibase/discussions">Discussioni</a>
|
||||
</h3>
|
||||
|
||||
<br /><br />
|
||||
## ✨ Funzionalità
|
||||
|
||||
### Costruisci e distribuisci software reale
|
||||
A differenza di altre piattaforme, con Budibase puoi costruire e distribuire applicazioni one-page. Le applicazioni Budibase sono altamente performanti e possono essere progettate in modo responsive, offrendo ai tuoi utenti un'esperienza eccezionale.
|
||||
<br /><br />
|
||||
|
||||
### Sorgente aperto ed estensibile
|
||||
Budibase è software open source - sotto licenza GPL v3. Questo dovrebbe rassicurarti sul fatto che Budibase sarà sempre lì. Puoi anche codificare in Budibase o fare fork e apportare modifiche a tuo piacimento, rendendolo un'esperienza amichevole per gli sviluppatori.
|
||||
<br /><br />
|
||||
|
||||
### Importa dati o inizia da zero
|
||||
Budibase può estrarre i suoi dati da diverse fonti, tra cui MongoDB, CouchDB, PostgreSQL, MySQL, Airtable, S3, DynamoDB o un'API REST. E a differenza di altre piattaforme, con Budibase puoi partire da zero e creare applicazioni aziendali senza alcuna fonte di dati. [Richiedi una nuova fonte di dati](https://github.com/Budibase/budibase/discussions?discussions_q=category%3AIdeas).
|
||||
|
||||
<p align="center">
|
||||
<img alt="Dati Budibase" src="https://res.cloudinary.com/daog6scxm/image/upload/v1636970242/Out%20of%20beta%20launch/data_n1tlhf.png">
|
||||
</p>
|
||||
<br /><br />
|
||||
|
||||
### Progetta e crea applicazioni utilizzando componenti predefiniti.
|
||||
|
||||
Budibase è dotato di componenti predefiniti belli e potenti che puoi utilizzare come mattoni per costruire la tua interfaccia utente. Esporremo anche molte delle tue opzioni di stile CSS preferite in modo che tu possa esprimere una creatività maggiore. [Richiedi un nuovo componente](https://github.com/Budibase/budibase/discussions?discussions_q=category%3AIdeas).
|
||||
|
||||
<p align="center">
|
||||
<img alt="Design Budibase" src="https://res.cloudinary.com/daog6scxm/image/upload/v1636970243/Out%20of%20beta%20launch/design-like-a-pro_qhlfeu.gif">
|
||||
</p>
|
||||
<br /><br />
|
||||
|
||||
### Automatizza processi, integra altri strumenti e collegati a webhook
|
||||
Risparmia tempo automatizzando processi manuali e flussi di lavoro. Che si tratti di connettersi a webhook o automatizzare email, basta dire a Budibase cosa fare e lasciarlo lavorare per te. Puoi facilmente [creare una nuova automazione per Budibase qui](https://github.com/Budibase/automations) o [Richiedere una nuova automazione](https://github.com/Budibase/budibase/discussions?discussions_q=category%3AIdeas).
|
||||
|
||||
<p align="center">
|
||||
<img alt="Automazioni Budibase" src="https://res.cloudinary.com/daog6scxm/image/upload/v1636970486/Out%20of%20beta%20launch/automation_riro7u.png">
|
||||
</p>
|
||||
<br /><br />
|
||||
|
||||
### Integrazione con i tuoi strumenti preferiti
|
||||
Budibase si integra con vari strumenti popolari, consentendoti di creare applicazioni che si adattano perfettamente alla tua stack tecnologica.
|
||||
|
||||
<p align="center">
|
||||
<img alt="Integrazioni Budibase" src="https://res.cloudinary.com/daog6scxm/image/upload/v1636970242/Out%20of%20beta%20launch/integrations_kc7dqt.png">
|
||||
</p>
|
||||
<br /><br />
|
||||
|
||||
### Paradiso degli amministratori
|
||||
Budibase è progettato per crescere. Con Budibase, puoi auto-ospitarti sulla tua infrastruttura e gestire globalmente utenti, home, SMTP, applicazioni, gruppi, aspetto e altro ancora. Puoi anche fornire agli utenti/gruppi un portale delle applicazioni e affidare la gestione degli utenti al responsabile del gruppo.
|
||||
|
||||
- Guarda il video promozionale: https://youtu.be/xoljVpty_Kw
|
||||
|
||||
<br /><br /><br />
|
||||
|
||||
## 🏁 Inizio
|
||||
|
||||
<img src="https://res.cloudinary.com/daog6scxm/image/upload/v1634808888/logo/deploy_npl9za.png" />
|
||||
|
||||
Implementa Budibase self-hosted nella tua infrastruttura esistente, utilizzando Docker, Kubernetes e Digital Ocean.
|
||||
Oppure utilizza Budibase Cloud se non hai bisogno di auto-ospitare e desideri iniziare rapidamente.
|
||||
|
||||
### [Inizia con Budibase](https://budibase.com)
|
||||
|
||||
|
||||
<br /><br />
|
||||
|
||||
## 🎓 Imparare Budibase
|
||||
|
||||
La documentazione Budibase [è qui](https://docs.budibase.com).
|
||||
<br />
|
||||
|
||||
|
||||
<br /><br />
|
||||
|
||||
## 💬 Comunità
|
||||
|
||||
Se hai domande o vuoi discutere con altri utenti di Budibase e unirti alla nostra comunità, vai su: [Discussioni Github](https://github.com/Budibase/budibase/discussions)
|
||||
|
||||
<br /><br /><br />
|
||||
|
||||
|
||||
## ❗ Codice di condotta
|
||||
|
||||
Budibase si impegna a offrire a tutti un'esperienza accogliente, diversificata e priva di molestie. Ci aspettiamo che tutti i membri della comunità Budibase rispettino i principi del nostro [**Codice di condotta**](https://github.com/Budibase/budibase/blob/HEAD/.github/CODE_OF_CONDUCT.md). Grazie per la tua attenzione.
|
||||
<br />
|
||||
|
||||
|
||||
<br /><br />
|
||||
|
||||
|
||||
## 🙌 Contribuire a Budibase
|
||||
|
||||
Che tu stia aprendo un rapporto di bug o creando una Pull request, ogni contributo è apprezzato e benvenuto. Se stai pensando di implementare una nuova funzionalità o modificare l'API, crea prima un Issue. In questo modo possiamo assicurarci che il tuo lavoro non sia inutile.
|
||||
|
||||
### Non sai da dove cominciare ?
|
||||
Un buon punto di partenza per contribuire è qui: [Progetti in corso](https://github.com/Budibase/budibase/projects/22).
|
||||
|
||||
### Come è organizzato il repo ?
|
||||
Budibase è un monorepo gestito da lerna. Lerna gestisce la costruzione e la pubblicazione dei pacchetti di Budibase. Ecco, a grandi linee, i pacchetti che compongono Budibase.
|
||||
|
||||
- [packages/builder](https://github.com/Budibase/budibase/tree/HEAD/packages/builder) - contiene il codice per l'applicazione svelte lato client di budibase builder.
|
||||
|
||||
- [packages/client](https://github.com/Budibase/budibase/tree/HEAD/packages/client) - Un modulo che viene eseguito nel browser e che è responsabile della lettura delle definizioni JSON e della creazione di applicazioni web viventi da esse.
|
||||
|
||||
- [packages/server](https://github.com/Budibase/budibase/tree/HEAD/packages/server) - Il server budibase. Questa applicazione Koa è responsabile del servizio del JS per le applicazioni builder e budibase, oltre a fornire l'API per l'interazione con il database e il filesystem.
|
||||
|
||||
Per ulteriori informazioni, vedere [CONTRIBUTING.md](https://github.com/Budibase/budibase/blob/HEAD/.github/CONTRIBUTING.md)
|
||||
|
||||
<br /><br />
|
||||
|
||||
|
||||
## 📝 Licenza
|
||||
|
||||
Budibase è open source, con licenza [GPL v3](https://www.gnu.org/licenses/gpl-3.0.en.html). Le librerie client e dei componenti sono con licenza [MPL](https://directory.fsf.org/wiki/License:MPL-2.0) - quindi le applicazioni che crei possono essere utilizzate con licenza come desideri.
|
||||
|
||||
<br /><br />
|
||||
|
||||
## ⭐ Stargazers nel tempo
|
||||
|
||||
[![Stargazers nel tempo](https://starchart.cc/Budibase/budibase.svg)](https://starchart.cc/Budibase/budibase)
|
||||
|
||||
Se riscontri problemi tra gli aggiornamenti del builder, utilizza la seguente guida [qui](https://github.com/Budibase/budibase/blob/HEAD/.github/CONTRIBUTING.md#troubleshooting) per pulire il tuo ambiente.
|
||||
|
||||
<br /><br />
|
||||
|
||||
## Contributeurs ✨
|
||||
|
||||
Grazie a queste meravigliose persone ([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="Codice">💻</a> <a href="https://github.com/Budibase/budibase/commits?author=shogunpurple" title="Documentazione">📖</a> <a href="https://github.com/Budibase/budibase/commits?author=shogunpurple" title="Test">⚠️</a> <a href="#infra-shogunpurple" title="Infrastruttura (Hosting, Strumenti di build, 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="Documentazione">📖</a> <a href="https://github.com/Budibase/budibase/commits?author=mike12345567" title="Codice">💻</a> <a href="https://github.com/Budibase/budibase/commits?author=mike12345567" title="Test">⚠️</a> <a href="#infra-mike12345567" title="Infrastruttura (Hosting, Strumenti di build, 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="Documentazione">📖</a> <a href="https://github.com/Budibase/budibase/commits?author=aptkingston" title="Codice">💻</a> <a href="https://github.com/Budibase/budibase/commits?author=aptkingston" title="Test">⚠️</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="Documentazione">📖</a> <a href="https://github.com/Budibase/budibase/commits?author=mjashanks" title="Codice">💻</a> <a href="https://github.com/Budibase/budibase/commits?author=mjashanks" title="Test">⚠️</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="Documentazione">📖</a> <a href="https://github.com/Budibase/budibase/commits?author=kevmodrome" title="Codice">💻</a> <a href="https://github.com/Budibase/budibase/commits?author=kevmodrome" title="Test">⚠️</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="Documentazione">📖</a> <a href="https://github.com/Budibase/budibase/commits?author=joebudi" title="Codice">💻</a> <a href="#content-joebudi" title="Contenuto">🖋</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="Codice">💻</a> <a href="https://github.com/Budibase/budibase/commits?author=Rory-Powell" title="Documentazione">📖</a> <a href="https://github.com/Budibase/budibase/commits?author=Rory-Powell" title="Test">⚠️</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="Codice">💻</a> <a href="https://github.com/Budibase/budibase/commits?author=PClmnt" title="Documentazione">📖</a> <a href="https://github.com/Budibase/budibase/commits?author=PClmnt" title="Test">⚠️</a></td>
|
||||
<td align="center"><a href="https://github.com/Conor-Mack"><img src="https://avatars.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="Codice">💻</a> <a href="https://github.com/Budibase/budibase/commits?author=Conor-Mack" title="Documentazione">📖</a></td>
|
||||
<td align="center"><a href="https://github.com/dominiccave"><img src="https://avatars.githubusercontent.com/u/17828738?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Dominic Cave</b></sub></a><br /><a href="https://github.com/Budibase/budibase/commits?author=dominiccave" title="Codice">💻</a> <a href="https://github.com/Budibase/budibase/commits?author=dominiccave" title="Documentazione">📖</a></td>
|
||||
<td align="center"><a href="https://github.com/Rabonaire"><img src="https://avatars.githubusercontent.com/u/10060936?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Rabonaire</b></sub></a><br /><a href="https://github.com/Budibase/budibase/commits?author=Rabonaire" title="Codice">💻</a> <a href="https://github.com/Budibase/budibase/commits?author=Rabonaire" title="Documentazione">📖</a> <a href="https://github.com/Budibase/budibase/issues?q=author%3ARabonaire" title="Report di bug">🐛</a></td>
|
||||
<td align="center"><a href="https://github.com/Alexsyeung"><img src="https://avatars.githubusercontent.com/u/31413823?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Alex Yeung</b></sub></a><br /><a href="https://github.com/Budibase/budibase/commits?author=Alexsyeung" title="Documentazione">📖</a> <a href="https://github.com/Budibase/budibase/commits?author=Alexsyeung" title="Codice">💻</a></td>
|
||||
<td align="center"><a href="https://github.com/SamWoodsIV"><img src="https://avatars.githubusercontent.com/u/25854138?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Sam Woods</b></sub></a><br /><a href="https://github.com/Budibase/budibase/commits?author=SamWoodsIV" title="Documentazione">📖</a> <a href="https://github.com/Budibase/budibase/commits?author=SamWoodsIV" title="Codice">💻</a> <a href="#infra-SamWoodsIV" title="Infrastruttura (Hosting, Strumenti di build, etc)">🚇</a></td>
|
||||
<td align="center"><a href="https://github.com/ScooterSwope"><img src="https://avatars.githubusercontent.com/u/21829556?v=4?s=100" width="100px;" alt=""/><br /><sub><b>ScooterSwope</b></sub></a><br /><a href="https://github.com/Budibase/budibase/commits?author=ScooterSwope" title="Codice">💻</a> <a href="https://github.com/Budibase/budibase/commits?author=ScooterSwope" title="Documentazione">📖</a></td>
|
||||
</tr>
|
||||
</table>
|
||||
<!-- markdownlint-enable -->
|
||||
<!-- prettier-ignore-end -->
|
||||
|
||||
<!-- ALL-CONTRIBUTORS-LIST:END -->
|
||||
|
||||
Questo progetto segue il [convenant del contribuente](https://github.com/Budibase/budibase/blob/master/CODE_OF_CONDUCT.md). Ogni contributo è il benvenuto!
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
|
@ -0,0 +1,211 @@
|
|||
<p align="center">
|
||||
<a href="https://www.budibase.com">
|
||||
<img alt="Budibase" src="https://res.cloudinary.com/daog6scxm/image/upload/v1696515725/Branding/Assets/Symbol/RGB/Full%20Colour/Budibase_Symbol_RGB_FullColour_cbqvha_1_z5cwq2.svg" width="60" />
|
||||
</a>
|
||||
</p>
|
||||
<h1 align="center">
|
||||
Budibase
|
||||
</h1>
|
||||
|
||||
<h3 align="center">
|
||||
A plataforma low-code que você vai adorar usar
|
||||
</h3>
|
||||
<p align="center">
|
||||
Budibase é uma plataforma low-code de código aberto e é a maneira mais fácil de criar ferramentas internas que melhoram a produtividade.
|
||||
</p>
|
||||
|
||||
<h3 align="center">
|
||||
🤖 🎨 🚀
|
||||
</h3>
|
||||
<br>
|
||||
|
||||
<p align="center">
|
||||
<img alt="Budibase design ui" src="https://res.cloudinary.com/daog6scxm/image/upload/v1633524049/ui/design-ui-wide-mobile_gdaveq.jpg">
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://github.com/Budibase/budibase/releases">
|
||||
<img alt="GitHub todos os releases" src="https://img.shields.io/github/downloads/Budibase/budibase/total">
|
||||
</a>
|
||||
<a href="https://github.com/Budibase/budibase/releases">
|
||||
<img alt="GitHub release (por ordem cronológica)" 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="Siga @budibase" />
|
||||
</a>
|
||||
<img src="https://img.shields.io/badge/Contributor%20Covenant-v2.0%20adopted-ff69b4.svg" alt="Código de Conduta" />
|
||||
<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">Começar</a>
|
||||
<span> · </span>
|
||||
<a href="https://docs.budibase.com">Documentação</a>
|
||||
<span> · </span>
|
||||
<a href="https://github.com/Budibase/budibase/discussions?discussions_q=category%3AIdeas">Solicitar melhorias</a>
|
||||
<span> · </span>
|
||||
<a href="https://github.com/Budibase/budibase/issues">Reportar um bug</a>
|
||||
<span> · </span>
|
||||
Suporte: <a href="https://github.com/Budibase/budibase/discussions">Discussões</a>
|
||||
</h3>
|
||||
|
||||
<br /><br />
|
||||
## ✨ Recursos
|
||||
|
||||
### Construa e implante um software real
|
||||
Ao contrário de outras plataformas, com o Budibase você constrói e implanta aplicativos de uma página. Os aplicativos Budibase são altamente performáticos e podem ser designados de forma responsiva, proporcionando uma experiência excepcional aos seus usuários.
|
||||
<br /><br />
|
||||
|
||||
### Código-fonte livre e extensível
|
||||
Budibase é software livre - sob a licença GPL v3. Isso deve lhe dar confiança de que o Budibase estará sempre disponível. Você também pode codificar no Budibase ou bifurcá-lo e fazer alterações conforme desejar, tornando-o amigável para desenvolvedores.
|
||||
<br /><br />
|
||||
|
||||
### Importar dados ou começar do zero
|
||||
Budibase pode extrair dados de várias fontes, incluindo MongoDB, CouchDB, PostgreSQL, MySQL, Airtable, S3, DynamoDB ou uma API REST. E ao contrário de outras plataformas, com o Budibase você pode começar do zero e criar aplicativos de negócios sem nenhuma fonte de dados. [Solicitar uma nova fonte de dados](https://github.com/Budibase/budibase/discussions?discussions_q=category%3AIdeas).
|
||||
|
||||
<p align="center">
|
||||
<img alt="Dados Budibase" src="https://res.cloudinary.com/daog6scxm/image/upload/v1636970242/Out%20of%20beta%20launch/data_n1tlhf.png">
|
||||
</p>
|
||||
<br /><br />
|
||||
|
||||
### Projetar e criar aplicativos usando componentes pré-definidos
|
||||
O Budibase vem com componentes lindamente projetados e poderosos que você pode usar como blocos de construção para criar sua interface do usuário. Também oferecemos muitas das suas opções de estilo CSS favoritas para que você possa mostrar sua criatividade. [Solicitar um novo componente](https://github.com/Budibase/budibase/discussions?discussions_q=category%3AIdeas).
|
||||
|
||||
<p align="center">
|
||||
<img alt="Design Budibase" src="https://res.cloudinary.com/daog6scxm/image/upload/v1636970243/Out%20of%20beta%20launch/design-like-a-pro_qhlfeu.gif">
|
||||
</p>
|
||||
<br /><br />
|
||||
|
||||
### Automatizar processos, integrar outras ferramentas e conectar webhooks
|
||||
Economize tempo automatizando processos manuais e fluxos de trabalho. Seja conectando-se a webhooks ou automatizando e-mails, basta dizer ao Budibase o que fazer e deixá-lo trabalhar para você. Você pode facilmente [criar uma nova automação para o Budibase aqui](https://github.com/Budibase/automations) ou [Solicitar uma nova automação](https://github.com/Budibase/budibase/discussions?discussions_q=category%3AIdeas).
|
||||
|
||||
<p align="center">
|
||||
<img alt="Automações Budibase" src="https://res.cloudinary.com/daog6scxm/image/upload/v1636970486/Out%20of%20beta%20launch/automation_riro7u.png">
|
||||
</p>
|
||||
<br /><br />
|
||||
|
||||
### Integração com suas ferramentas favoritas
|
||||
O Budibase se integra a várias ferramentas populares, permitindo que você crie aplicativos que se encaixam perfeitamente em sua pilha tecnológica.
|
||||
|
||||
<p align="center">
|
||||
<img alt="Integrações Budibase" src="https://res.cloudinary.com/daog6scxm/image/upload/v1636970242/Out%20of%20beta%20launch/integrations_kc7dqt.png">
|
||||
</p>
|
||||
<br /><br />
|
||||
|
||||
### Paraíso dos administradores
|
||||
O Budibase é projetado para escalar. Com o Budibase, você pode se auto-hospedar em sua própria infraestrutura e gerenciar globalmente usuários, home, SMTP, aplicativos, grupos, aparência e muito mais. Você também pode fornecer aos usuários/grupos um portal de aplicativos e delegar o gerenciamento de usuários ao líder do grupo.
|
||||
|
||||
- Assista ao vídeo promocional: https://youtu.be/xoljVpty_Kw
|
||||
|
||||
<br /><br /><br />
|
||||
|
||||
## 🏁 Começar
|
||||
|
||||
<img src="https://res.cloudinary.com/daog6scxm/image/upload/v1634808888/logo/deploy_npl9za.png" />
|
||||
|
||||
Implante o Budibase em auto-hospedagem em sua infraestrutura existente, usando Docker, Kubernetes e Digital Ocean.
|
||||
Ou use o Budibase Cloud se você não precisar se auto-hospedar e quiser começar rapidamente.
|
||||
|
||||
### [Começar com o Budibase](https://budibase.com)
|
||||
|
||||
|
||||
<br /><br />
|
||||
|
||||
## 🎓 Aprenda Budibase
|
||||
|
||||
A documentação Budibase [está aqui](https://docs.budibase.com).
|
||||
<br />
|
||||
|
||||
|
||||
<br /><br />
|
||||
|
||||
## 💬 Comunidade
|
||||
|
||||
Se você tiver alguma dúvida ou quiser conversar com outros usuários do Budibase e se juntar à nossa comunidade, visite [Discussões do Github](https://github.com/Budibase/budibase/discussions)
|
||||
|
||||
<br /><br /><br />
|
||||
|
||||
|
||||
## ❗ Código de Conduta
|
||||
|
||||
O Budibase está comprometido em oferecer a todos uma experiência acolhedora, diversificada e livre de assédio. Esperamos que todos os membros da comunidade Budibase sigam os princípios do nosso [**Código de Conduta**](https://github.com/Budibase/budibase/blob/HEAD/.github/CODE_OF_CONDUCT.md). Obrigado por ler.
|
||||
<br />
|
||||
|
||||
|
||||
<br /><br />
|
||||
|
||||
|
||||
## 🙌 Contribuindo para o Budibase
|
||||
|
||||
Seja abrindo uma issue ou criando um pull request, toda contribuição é apreciada e bem-vinda. Se você está pensando em implementar uma nova funcionalidade ou alterar a API, por favor, crie primeiro uma Issue. Assim, podemos garantir que seu trabalho não seja em vão.
|
||||
|
||||
### Não sabe por onde começar?
|
||||
Um bom lugar para começar a contribuir é aqui: [Projetos em andamento](https://github.com/Budibase/budibase/projects/22).
|
||||
|
||||
### Como o repositório está organizado?
|
||||
O Budibase é um monorepo gerenciado pelo lerna. O Lerna cuida da construção e publicação dos pacotes do Budibase. Aqui estão, em alto nível, os pacotes que compõem o Budibase.
|
||||
|
||||
- [packages/builder](https://github.com/Budibase/budibase/tree/HEAD/packages/builder) - contém o código para o aplicativo svelte do lado do cliente do budibase builder.
|
||||
|
||||
- [packages/client](https://github.com/Budibase/budibase/tree/HEAD/packages/client) - Um módulo que roda no navegador e é responsável por ler definições JSON e criar aplicativos web dinâmicos a partir delas.
|
||||
|
||||
- [packages/server](https://github.com/Budibase/budibase/tree/HEAD/packages/server) - O servidor budibase. Este aplicativo Koa é responsável por servir o JS para os aplicativos builder e budibase, bem como fornecer a API para interagir com o banco de dados e o sistema de arquivos.
|
||||
|
||||
Para mais informações, veja [CONTRIBUTING.md](https://github.com/Budibase/budibase/blob/HEAD/.github/CONTRIBUTING.md)
|
||||
|
||||
<br /><br />
|
||||
|
||||
|
||||
## 📝 Licença
|
||||
|
||||
O Budibase é open source, sob a licença [GPL v3](https://www.gnu.org/licenses/gpl-3.0.en.html). As bibliotecas do cliente e dos componentes estão licenciadas sob [MPL](https://directory.fsf.org/wiki/License:MPL-2.0) - para que os aplicativos que você cria possam ser usados sob licença como você desejar.
|
||||
|
||||
<br /><br />
|
||||
|
||||
## ⭐ Stargazers ao longo do tempo
|
||||
|
||||
[![Stargazers ao longo do tempo](https://starchart.cc/Budibase/budibase.svg)](https://starchart.cc/Budibase/budibase)
|
||||
|
||||
Se você tiver problemas entre as atualizações do builder, por favor, use o guia a seguir [aqui](https://github.com/Budibase/budibase/blob/HEAD/.github/CONTRIBUTING.md#troubleshooting) para limpar seu ambiente.
|
||||
|
||||
<br /><br />
|
||||
|
||||
## Contribuidores ✨
|
||||
|
||||
Agradecimentos a estas pessoas maravilhosas ([chave de emoji](https://allcontributors.org/docs/fr/emoji-key)):
|
||||
|
||||
<!-- ALL-CONTRIBUTORS-LIST:START - Do not remove or modify this section -->
|
||||
<!-- 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="Código">💻</a> <a href="https://github.com/Budibase/budibase/commits?author=shogunpurple" title="Documentação">📖</a> <a href="https://github.com/Budibase/budibase/commits?author=shogunpurple" title="Testes">⚠️</a> <a href="#infra-shogunpurple" title="Infraestrutura (Hospedagem, Ferramentas de Build, 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="Documentação">📖</a> <a href="https://github.com/Budibase/budibase/commits?author=mike12345567" title="Código">💻</a> <a href="https://github.com/Budibase/budibase/commits?author=mike12345567" title="Testes">⚠️</a> <a href="#infra-mike12345567" title="Infraestrutura (Hospedagem, Ferramentas de Build, 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="Documentação">📖</a> <a href="https://github.com/Budibase/budibase/commits?author=aptkingston" title="Código">💻</a> <a href="https://github.com/Budibase/budibase/commits?author=aptkingston" title="Testes">⚠️</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="Documentação">📖</a> <a href="https://github.com/Budibase/budibase/commits?author=mjashanks" title="Código">💻</a> <a href="https://github.com/Budibase/budibase/commits?author=mjashanks" title="Testes">⚠️</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="Documentação">📖</a> <a href="https://github.com/Budibase/budibase/commits?author=kevmodrome" title="Código">💻</a> <a href="https://github.com/Budibase/budibase/commits?author=kevmodrome" title="Testes">⚠️</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="Documentação">📖</a> <a href="https://github.com/Budibase/budibase/commits?author=joebudi" title="Código">💻</a> <a href="#content-joebudi" title="Conteúdo">🖋</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="Código">💻</a> <a href="https://github.com/Budibase/budibase/commits?author=Rory-Powell" title="Documentação">📖</a> <a href="https://github.com/Budibase/budibase/commits?author=Rory-Powell" title="Testes">⚠️</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="Código">💻</a> <a href="https://github.com/Budibase/budibase/commits?author=PClmnt" title="Documentação">📖</a> <a href="https://github.com/Budibase/budibase/commits?author=PClmnt" title="Testes">⚠️</a></td>
|
||||
<td align="center"><a href="https://github.com/Conor-Mack"><img src="https://avatars.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="Código">💻</a> <a href="https://github.com/Budibase/budibase/commits?author=Conor-Mack" title="Documentação">📖</a> <a href="https://github.com/Budibase/budibase/commits?author=Conor-Mack" title="Testes">⚠️</a></td>
|
||||
<td align="center"><a href="https://github.com/Grays-world"><img src="https://avatars.githubusercontent.com/u/89784014?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Grays-world</b></sub></a><br /><a href="https://github.com/Budibase/budibase/commits?author=Grays-world" title="Código">💻</a></td>
|
||||
<td align="center"><a href="https://github.com/syluss"><img src="https://avatars.githubusercontent.com/u/1770743?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Sylvain Galand</b></sub></a><br /><a href="https://github.com/Budibase/budibase/commits?author=syluss" title="Código">💻</a> <a href="https://github.com/Budibase/budibase/commits?author=syluss" title="Documentação">📖</a></td>
|
||||
<td align="center"><a href="https://github.com/John-Mullins1"><img src="https://avatars.githubusercontent.com/u/89561797?v=4?s=100" width="100px;" alt=""/><br /><sub><b>John Mullins</b></sub></a><br /><a href="https://github.com/Budibase/budibase/commits?author=John-Mullins1" title="Código">💻</a></td>
|
||||
<td align="center"><a href="https://github.com/Jakeboyd"><img src="https://avatars.githubusercontent.com/u/55934414?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Jakeboyd</b></sub></a><br /><a href="https://github.com/Budibase/budibase/commits?author=Jakeboyd" title="Código">💻</a></td>
|
||||
<td align="center"><a href="https://github.com/stevedoescode"><img src="https://avatars.githubusercontent.com/u/29486122?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Steve Bridle</b></sub></a><br /><a href="https://github.com/Budibase/budibase/commits?author=stevedoescode" title="Código">💻</a></td>
|
||||
</tr>
|
||||
</table>
|
||||
<!-- markdownlint-enable -->
|
||||
<!-- ALL-CONTRIBUTORS-LIST:END -->
|
||||
|
||||
<br /><br />
|
||||
|
||||
|
||||
## Licença
|
||||
|
||||
Distribuído sob a licença GPL v3.0. Veja `LICENSE` para mais informações.
|
||||
|
||||
</p>
|
|
@ -0,0 +1,207 @@
|
|||
<p align="center">
|
||||
<a href="https://www.budibase.com">
|
||||
<img alt="Budibase" src="https://res.cloudinary.com/daog6scxm/image/upload/v1696515725/Branding/Assets/Symbol/RGB/Full%20Colour/Budibase_Symbol_RGB_FullColour_cbqvha_1_z5cwq2.svg" width="60" />
|
||||
</a>
|
||||
</p>
|
||||
<h1 align="center">
|
||||
Budibase
|
||||
</h1>
|
||||
|
||||
<h3 align="center">
|
||||
Низкокодовая платформа, которую вы полюбите использовать
|
||||
</h3>
|
||||
<p align="center">
|
||||
Budibase - это открытая низкокодовая платформа, которая представляет собой самый простой способ создания внутренних инструментов, повышающих производительность.
|
||||
</p>
|
||||
|
||||
<h3 align="center">
|
||||
🤖 🎨 🚀
|
||||
</h3>
|
||||
<br>
|
||||
|
||||
<p align="center">
|
||||
<img alt="Budibase design ui" src="https://res.cloudinary.com/daog6scxm/image/upload/v1633524049/ui/design-ui-wide-mobile_gdaveq.jpg">
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://github.com/Budibase/budibase/releases">
|
||||
<img alt="GitHub все релизы" src="https://img.shields.io/github/downloads/Budibase/budibase/total">
|
||||
</a>
|
||||
<a href="https://github.com/Budibase/budibase/releases">
|
||||
<img alt="GitHub релизы (в хронологическом порядке)" 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="Подписаться на @budibase" />
|
||||
</a>
|
||||
<img src="https://img.shields.io/badge/Contributor%20Covenant-v2.0%20adopted-ff69b4.svg" alt="Кодекс поведения" />
|
||||
<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">Начать</a>
|
||||
<span> · </span>
|
||||
<a href="https://docs.budibase.com">Документация</a>
|
||||
<span> · </span>
|
||||
<a href="https://github.com/Budibase/budibase/discussions?discussions_q=category%3AIdeas">Запросы на улучшения</a>
|
||||
<span> · </span>
|
||||
<a href="https://github.com/Budibase/budibase/issues">Сообщить об ошибке</a>
|
||||
<span> · </span>
|
||||
Поддержка: <a href="https://github.com/Budibase/budibase/discussions">Обсуждения</a>
|
||||
</h3>
|
||||
|
||||
<br /><br />
|
||||
## ✨ Функциональные возможности
|
||||
|
||||
### Строим и развертываем настоящее программное обеспечение
|
||||
В отличие от других платформ, с помощью Budibase вы создаете и развертываете одностраничные приложения. Приложения Budibase имеют высокую производительность и могут быть адаптированы для разных устройств, обеспечивая вашим пользователям удивительный опыт.
|
||||
<br /><br />
|
||||
|
||||
### Открытый и расширяемый исходный код
|
||||
Budibase - это свободное программное обеспечение под лицензией GPL v3. Это должно вас уверить в том, что Budibase всегда будет здесь. Вы также можете писать код в Budibase или форкнуть его и вносить изменения по своему усмотрению, что сделает его дружелюбным для разработчиков.
|
||||
<br /><br />
|
||||
|
||||
### Импорт данных или начало с нуля
|
||||
Budibase может получать данные из различных источников, включая MongoDB, CouchDB, PostgreSQL, MySQL, Airtable, S3, DynamoDB или REST API. И в отличие от других платформ, с помощью Budibase вы можете начать с нуля и создавать бизнес-приложения без каких-либо источников данных. [Запросить новый источник данных](https://github.com/Budibase/budibase/discussions?discussions_q=category%3AIdeas).
|
||||
|
||||
<p align="center">
|
||||
<img alt="Budibase data" src="https://res.cloudinary.com/daog6scxm/image/upload/v1636970242/Out%20of%20beta%20launch/data_n1tlhf.png">
|
||||
</p>
|
||||
<br /><br />
|
||||
|
||||
### Проектирование и создание приложений с использованием предварительно определенных компонентов.
|
||||
|
||||
Budibase поставляется с красиво оформленными и мощными компонентами, которые вы можете использовать как строительные блоки для создания вашего пользовательского интерфейса. Мы также предоставляем множество ваших любимых опций стилей CSS, чтобы вы могли проявить больше креативности. [Запросить новый компонент](https://github.com/Budibase/budibase/discussions?discussions_q=category%3AIdeas).
|
||||
|
||||
<p align="center">
|
||||
<img alt="Budibase design" src="https://res.cloudinary.com/daog6scxm/image/upload/v1636970243/Out%20of%20beta%20launch/design-like-a-pro_qhlfeu.gif">
|
||||
</p>
|
||||
<br /><br />
|
||||
|
||||
### Автоматизация процессов, интеграция с другими инструментами и подключение к вебхукам
|
||||
Экономьте время, автоматизируя ручные процессы и рабочие потоки. Будь то подключение к вебхукам или автоматизация отправки электронных писем, просто скажите Budibase, что он должен делать, и позвольте ему работать за вас. Вы можете легко [создать новую автоматизацию для Budibase здесь](https://github.com/Budibase/automations) или [Запросить новую автоматизацию](https://github.com/Budibase/budibase/discussions?discussions_q=category%3AIdeas).
|
||||
|
||||
<p align="center">
|
||||
<img alt="Budibase automations" src="https://res.cloudinary.com/daog6scxm/image/upload/v1636970486/Out%20of%20beta%20launch/automation_riro7u.png">
|
||||
</p>
|
||||
<br /><br />
|
||||
|
||||
### Интеграция с вашими любимыми инструментами
|
||||
Budibase интегрируется с рядом популярных инструментов, что позволяет вам создавать приложения, которые идеально вписываются в вашу технологическую стопку.
|
||||
|
||||
<p align="center">
|
||||
<img alt="Budibase integrations" src="https://res.cloudinary.com/daog6scxm/image/upload/v1636970242/Out%20of%20beta%20launch/integrations_kc7dqt.png">
|
||||
</p>
|
||||
<br /><br />
|
||||
|
||||
### Рай для админов
|
||||
Budibase разработан для масштабирования. С Budibase вы можете самостоятельно размещать его на своей собственной инфраструктуре и глобально управлять пользователями, доменами, SMTP, приложениями, группами, внешним видом и многим другим. Вы также можете предоставить пользователям/группам портал приложений и поручить управление пользователями руководителю группы.
|
||||
|
||||
- Смотрите промо-видео: https://youtu.be/xoljVpty_Kw
|
||||
|
||||
<br /><br /><br />
|
||||
|
||||
## 🏁 Начало работы
|
||||
|
||||
<img src="https://res.cloudinary.com/daog6scxm/image/upload/v1634808888/logo/deploy_npl9za.png" />
|
||||
|
||||
Разверните Budibase на своей собственной инфраструктуре с использованием Docker, Kubernetes и Digital Ocean.
|
||||
Или используйте Budibase Cloud, если вам не нужно самостоятельно размещаться, и вы хотите быстро начать.
|
||||
|
||||
### [Начать работу с Budibase](https://budibase.com)
|
||||
|
||||
|
||||
<br /><br />
|
||||
|
||||
## 🎓 Изучение Budibase
|
||||
|
||||
Документация Budibase [здесь](https://docs.budibase.com).
|
||||
<br />
|
||||
|
||||
|
||||
<br /><br />
|
||||
|
||||
## 💬 Сообщество
|
||||
|
||||
Если у вас есть вопросы или вы хотите обсудить что-то с другими пользователями Budibase и присоединиться к нашему сообществу, пожалуйста, перейдите по следующей ссылке: [Обсуждения на GitHub](https://github.com/Budibase/budibase/discussions)
|
||||
|
||||
<br /><br /><br />
|
||||
|
||||
|
||||
## ❗ Кодекс поведения
|
||||
|
||||
Budibase обязуется обеспечить каждому дружелюбный, разнообразный и безопасный опыт. Мы ожидаем, что все члены сообщества Budibase будут следовать принципам нашего [**Кодекса поведения**](https://github.com/Budibase/budibase/blob/HEAD/.github/CODE_OF_CONDUCT.md). Спасибо за внимание.
|
||||
<br />
|
||||
|
||||
|
||||
<br /><br />
|
||||
|
||||
|
||||
## 🙌 Вклад в Budibase
|
||||
|
||||
Будь то открытие ошибки или создание запроса на включение изменений, любой вклад приветствуется и приветствуется. Если вы планируете реализовать новую функциональность или изменить API, сначала создайте Issue. Так мы сможем убедиться, что ваша работа не напрасна.
|
||||
|
||||
### Не знаете, с чего начать?
|
||||
Хорошее место для начала вклада - это здесь: [Текущие проекты](https://github.com/Budibase/budibase/projects/22).
|
||||
|
||||
### Как организован репозиторий?
|
||||
Budibase - это монорепозиторий, управляемый с помощью lerna. Lerna управляет сборкой и публикацией пакетов Budibase. Вот, в общих чертах, пакеты, из которых состоит Budibase.
|
||||
|
||||
- [packages/builder](https://github.com/Budibase/budibase/tree/HEAD/packages/builder) - содержит код клиентского приложения Svelte для Budibase builder.
|
||||
|
||||
- [packages/client](https://github.com/Budibase/budibase/tree/HEAD/packages/client) - Модуль, который запускается в браузере и отвечает за чтение JSON-определений и создание веб-приложений из них.
|
||||
|
||||
- [packages/server](https://github.com/Budibase/budibase/tree/HEAD/packages/server) - Сервер Budibase. Это приложение Koa отвечает за предоставление JS для строителей и приложений Budibase, а также предоставляет API для взаимодействия с базой данных и файловой системой.
|
||||
|
||||
Для получения дополнительной информации см. [CONTRIBUTING.md](https://github.com/Budibase/budibase/blob/HEAD/.github/CONTRIBUTING.md)
|
||||
|
||||
<br /><br />
|
||||
|
||||
|
||||
## 📝 Лицензия
|
||||
|
||||
Budibase является проектом с открытым исходным кодом, лицензированным по [GPL v3](https://www.gnu.org/licenses/gpl-3.0.en.html). Клиентские библиотеки и компоненты лицензируются по [MPL](https://directory.fsf.org/wiki/License:MPL-2.0), так что приложения, которые вы создаете, могут использоваться под любой лицензией, как вам угодно.
|
||||
|
||||
<br /><br />
|
||||
|
||||
## ⭐ Старгейзеры во времени
|
||||
|
||||
[![Stargazers во времени](https://starchart.cc/Budibase/budibase.svg)](https://starchart.cc/Budibase/budibase)
|
||||
|
||||
Если у вас возникли проблемы между обновлениями билдера, пожалуйста, используйте следующее руководство [здесь](https://github.com/Budibase/budibase/blob/HEAD/.github/CONTRIBUTING.md#troubleshooting), чтобы очистить ваше окружение.
|
||||
|
||||
<br /><br />
|
||||
|
||||
## Участники ✨
|
||||
|
||||
Благодарим этих замечательных людей ([ключи эмодзи](https://allcontributors.org/docs/ru/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="Код">💻</a> <a href="https://github.com/Budibase/budibase/commits?author=shogunpurple" title="Документация">📖</a> <a href="https://github.com/Budibase/budibase/commits?author=shogunpurple" title="Тесты">⚠️</a> <a href="#infra-shogunpurple" title="Инфраструктура (хостинг, средства сборки и т. д.)">🚇</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="Документация">📖</a> <a href="https://github.com/Budibase/budibase/commits?author=mike12345567" title="Код">💻</a> <a href="https://github.com/Budibase/budibase/commits?author=mike12345567" title="Тесты">⚠️</a> <a href="#infra-mike12345567" title="Инфраструктура (хостинг, средства сборки и т. д.)">🚇</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="Документация">📖</a> <a href="https://github.com/Budibase/budibase/commits?author=aptkingston" title="Код">💻</a> <a href="https://github.com/Budibase/budibase/commits?author=aptkingston" title="Тесты">⚠️</a> <a href="#design-aptkingston" title="Дизайн">🎨</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="Документация">📖</a> <a href="https://github.com/Budibase/budibase/commits?author=mjashanks" title="Код">💻</a> <a href="https://github.com/Budibase/budibase/commits?author=mjashanks" title="Тесты">⚠️</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="Документация">📖</a> <a href="https://github.com/Budibase/budibase/commits?author=kevmodrome" title="Код">💻</a> <a href="https://github.com/Budibase/budibase/commits?author=kevmodrome" title="Тесты">⚠️</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="#userTesting-joe14" title="User Testing">📓</a></td>
|
||||
<td align="center"><a href="https://github.com/ntkleynhans"><img src="https://avatars.githubusercontent.com/u/4908235?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Nico Kleynhans</b></sub></a><br /><a href="#design-ntkleynhans" title="Дизайн">🎨</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center"><a href="https://github.com/kkeithle"><img src="https://avatars.githubusercontent.com/u/18712925?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Keith Lee</b></sub></a><br /><a href="#design-kkeithle" title="Дизайн">🎨</a></td>
|
||||
<td align="center"><a href="https://github.com/Ben-Shabs"><img src="https://avatars.githubusercontent.com/u/26257661?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Ben-Shabs</b></sub></a><br /><a href="#userTesting-Ben-Shabs" title="User Testing">📓</a></td>
|
||||
<td align="center"><a href="https://www.reeceking.dev"><img src="https://avatars.githubusercontent.com/u/4020324?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Reece King</b></sub></a><br /><a href="#design-reeceking" title="Дизайн">🎨</a></td>
|
||||
<td align="center"><a href="https://github.com/gunjan5"><img src="https://avatars.githubusercontent.com/u/6934146?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Gunjan Chhabra</b></sub></a><br /><a href="#userTesting-gunjan5" title="User Testing">📓</a></td>
|
||||
<td align="center"><a href="https://github.com/stavros-liaskos"><img src="https://avatars.githubusercontent.com/u/29320217?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Stavros Liaskos</b></sub></a><br /><a href="#design-stavros-liaskos" title="Дизайн">🎨</a></td>
|
||||
<td align="center"><a href="https://github.com/theshu8"><img src="https://avatars.githubusercontent.com/u/28013049?v=4?s=100" width="100px;" alt=""/><br /><sub><b>theshu8</b></sub></a><br /><a href="#userTesting-theshu8" title="User Testing">📓</a></td>
|
||||
<td align="center"><a href="https://github.com/Kleebster"><img src="https://avatars.githubusercontent.com/u/63757547?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Kleebster</b></sub></a><br /><a href="#userTesting-Kleebster" title="User Testing">📓</a></td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<!-- markdownlint-enable -->
|
||||
<!-- prettier-ignore-end -->
|
||||
<!-- ALL-CONTRIBUTORS-LIST:END -->
|
||||
</p>
|
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"version": "2.24.1",
|
||||
"version": "2.26.1",
|
||||
"npmClient": "yarn",
|
||||
"packages": [
|
||||
"packages/*",
|
||||
|
|
|
@ -73,7 +73,6 @@
|
|||
"chance": "1.1.8",
|
||||
"ioredis-mock": "8.9.0",
|
||||
"jest": "29.7.0",
|
||||
"jest-environment-node": "29.7.0",
|
||||
"jest-serial-runner": "1.2.1",
|
||||
"pino-pretty": "10.0.0",
|
||||
"pouchdb-adapter-memory": "7.2.2",
|
||||
|
|
|
@ -69,7 +69,7 @@ async function populateUsersFromDB(
|
|||
export async function getUser(
|
||||
userId: string,
|
||||
tenantId?: string,
|
||||
populateUser?: any
|
||||
populateUser?: (userId: string, tenantId: string) => Promise<User>
|
||||
) {
|
||||
if (!populateUser) {
|
||||
populateUser = populateFromDB
|
||||
|
@ -83,7 +83,7 @@ export async function getUser(
|
|||
}
|
||||
const client = await redis.getUserClient()
|
||||
// try cache
|
||||
let user = await client.get(userId)
|
||||
let user: User = await client.get(userId)
|
||||
if (!user) {
|
||||
user = await populateUser(userId, tenantId)
|
||||
await client.store(userId, user, EXPIRY_SECONDS)
|
||||
|
|
|
@ -281,7 +281,7 @@ export function doInScimContext(task: any) {
|
|||
return newContext(updates, task)
|
||||
}
|
||||
|
||||
export async function ensureSnippetContext() {
|
||||
export async function ensureSnippetContext(enabled = !env.isTest()) {
|
||||
const ctx = getCurrentContext()
|
||||
|
||||
// If we've already added snippets to context, continue
|
||||
|
@ -292,7 +292,7 @@ export async function ensureSnippetContext() {
|
|||
// Otherwise get snippets for this app and update context
|
||||
let snippets: Snippet[] | undefined
|
||||
const db = getAppDB()
|
||||
if (db && !env.isTest()) {
|
||||
if (db && enabled) {
|
||||
const app = await db.get<App>(DocumentType.APP_METADATA)
|
||||
snippets = app.snippets
|
||||
}
|
||||
|
|
|
@ -3,11 +3,11 @@ import {
|
|||
AllDocsResponse,
|
||||
AnyDocument,
|
||||
Database,
|
||||
DatabaseOpts,
|
||||
DatabaseQueryOpts,
|
||||
DatabasePutOpts,
|
||||
DatabaseCreateIndexOpts,
|
||||
DatabaseDeleteIndexOpts,
|
||||
DatabaseOpts,
|
||||
DatabasePutOpts,
|
||||
DatabaseQueryOpts,
|
||||
Document,
|
||||
isDocument,
|
||||
RowResponse,
|
||||
|
@ -17,7 +17,7 @@ import {
|
|||
import { getCouchInfo } from "./connections"
|
||||
import { directCouchUrlCall } from "./utils"
|
||||
import { getPouchDB } from "./pouchDB"
|
||||
import { WriteStream, ReadStream } from "fs"
|
||||
import { ReadStream, WriteStream } from "fs"
|
||||
import { newid } from "../../docIds/newid"
|
||||
import { SQLITE_DESIGN_DOC_ID } from "../../constants"
|
||||
import { DDInstrumentedDatabase } from "../instrumentation"
|
||||
|
@ -38,6 +38,39 @@ function buildNano(couchInfo: { url: string; cookie: string }) {
|
|||
|
||||
type DBCall<T> = () => Promise<T>
|
||||
|
||||
class CouchDBError extends Error {
|
||||
status: number
|
||||
statusCode: number
|
||||
reason: string
|
||||
name: string
|
||||
errid: string
|
||||
error: string
|
||||
description: string
|
||||
|
||||
constructor(
|
||||
message: string,
|
||||
info: {
|
||||
status: number | undefined
|
||||
statusCode: number | undefined
|
||||
name: string
|
||||
errid: string
|
||||
description: string
|
||||
reason: string
|
||||
error: string
|
||||
}
|
||||
) {
|
||||
super(message)
|
||||
const statusCode = info.status || info.statusCode || 500
|
||||
this.status = statusCode
|
||||
this.statusCode = statusCode
|
||||
this.reason = info.reason
|
||||
this.name = info.name
|
||||
this.errid = info.errid
|
||||
this.description = info.description
|
||||
this.error = info.error
|
||||
}
|
||||
}
|
||||
|
||||
export function DatabaseWithConnection(
|
||||
dbName: string,
|
||||
connection: string,
|
||||
|
@ -119,7 +152,7 @@ export class DatabaseImpl implements Database {
|
|||
} catch (err: any) {
|
||||
// Handling race conditions
|
||||
if (err.statusCode !== 412) {
|
||||
throw err
|
||||
throw new CouchDBError(err.message, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -138,10 +171,9 @@ export class DatabaseImpl implements Database {
|
|||
if (err.statusCode === 404 && err.reason === DATABASE_NOT_FOUND) {
|
||||
await this.checkAndCreateDb()
|
||||
return await this.performCall(call)
|
||||
} else if (err.statusCode) {
|
||||
err.status = err.statusCode
|
||||
}
|
||||
throw err
|
||||
// stripping the error down the props which are safe/useful, drop everything else
|
||||
throw new CouchDBError(`CouchDB error: ${err.message}`, err)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -288,7 +320,7 @@ export class DatabaseImpl implements Database {
|
|||
if (err.statusCode === 404) {
|
||||
return
|
||||
} else {
|
||||
throw { ...err, status: err.statusCode }
|
||||
throw new CouchDBError(err.message, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -12,6 +12,10 @@ import { dataFilters } from "@budibase/shared-core"
|
|||
|
||||
export const removeKeyNumbering = dataFilters.removeKeyNumbering
|
||||
|
||||
function isEmpty(value: any) {
|
||||
return value == null || value === ""
|
||||
}
|
||||
|
||||
/**
|
||||
* Class to build lucene query URLs.
|
||||
* Optionally takes a base lucene query object.
|
||||
|
@ -282,15 +286,14 @@ export class QueryBuilder<T> {
|
|||
}
|
||||
|
||||
const equal = (key: string, value: any) => {
|
||||
// 0 evaluates to false, which means we would return all rows if we don't check it
|
||||
if (!value && value !== 0) {
|
||||
if (isEmpty(value)) {
|
||||
return null
|
||||
}
|
||||
return `${key}:${builder.preprocess(value, allPreProcessingOpts)}`
|
||||
}
|
||||
|
||||
const contains = (key: string, value: any, mode = "AND") => {
|
||||
if (!value || (Array.isArray(value) && value.length === 0)) {
|
||||
if (isEmpty(value)) {
|
||||
return null
|
||||
}
|
||||
if (!Array.isArray(value)) {
|
||||
|
@ -306,7 +309,7 @@ export class QueryBuilder<T> {
|
|||
}
|
||||
|
||||
const fuzzy = (key: string, value: any) => {
|
||||
if (!value) {
|
||||
if (isEmpty(value)) {
|
||||
return null
|
||||
}
|
||||
value = builder.preprocess(value, {
|
||||
|
@ -328,7 +331,7 @@ export class QueryBuilder<T> {
|
|||
}
|
||||
|
||||
const oneOf = (key: string, value: any) => {
|
||||
if (!value) {
|
||||
if (isEmpty(value)) {
|
||||
return `*:*`
|
||||
}
|
||||
if (!Array.isArray(value)) {
|
||||
|
@ -386,7 +389,7 @@ export class QueryBuilder<T> {
|
|||
// Construct the actual lucene search query string from JSON structure
|
||||
if (this.#query.string) {
|
||||
build(this.#query.string, (key: string, value: any) => {
|
||||
if (!value) {
|
||||
if (isEmpty(value)) {
|
||||
return null
|
||||
}
|
||||
value = builder.preprocess(value, {
|
||||
|
@ -399,7 +402,7 @@ export class QueryBuilder<T> {
|
|||
}
|
||||
if (this.#query.range) {
|
||||
build(this.#query.range, (key: string, value: any) => {
|
||||
if (!value) {
|
||||
if (isEmpty(value)) {
|
||||
return null
|
||||
}
|
||||
if (value.low == null || value.low === "") {
|
||||
|
@ -421,7 +424,7 @@ export class QueryBuilder<T> {
|
|||
}
|
||||
if (this.#query.notEqual) {
|
||||
build(this.#query.notEqual, (key: string, value: any) => {
|
||||
if (!value) {
|
||||
if (isEmpty(value)) {
|
||||
return null
|
||||
}
|
||||
if (typeof value === "boolean") {
|
||||
|
@ -431,10 +434,28 @@ export class QueryBuilder<T> {
|
|||
})
|
||||
}
|
||||
if (this.#query.empty) {
|
||||
build(this.#query.empty, (key: string) => `(*:* -${key}:["" TO *])`)
|
||||
build(this.#query.empty, (key: string) => {
|
||||
// Because the structure of an empty filter looks like this:
|
||||
// { empty: { someKey: null } }
|
||||
//
|
||||
// The check inside of `build` does not set `allFiltersEmpty`, which results
|
||||
// in weird behaviour when the empty filter is the only filter. We get around
|
||||
// this by setting `allFiltersEmpty` to false here.
|
||||
allFiltersEmpty = false
|
||||
return `(*:* -${key}:["" TO *])`
|
||||
})
|
||||
}
|
||||
if (this.#query.notEmpty) {
|
||||
build(this.#query.notEmpty, (key: string) => `${key}:["" TO *]`)
|
||||
build(this.#query.notEmpty, (key: string) => {
|
||||
// Because the structure of a notEmpty filter looks like this:
|
||||
// { notEmpty: { someKey: null } }
|
||||
//
|
||||
// The check inside of `build` does not set `allFiltersEmpty`, which results
|
||||
// in weird behaviour when the empty filter is the only filter. We get around
|
||||
// this by setting `allFiltersEmpty` to false here.
|
||||
allFiltersEmpty = false
|
||||
return `${key}:["" TO *]`
|
||||
})
|
||||
}
|
||||
if (this.#query.oneOf) {
|
||||
build(this.#query.oneOf, oneOf)
|
||||
|
|
|
@ -13,7 +13,7 @@ import { getGlobalDB, doInTenant } from "../context"
|
|||
import { decrypt } from "../security/encryption"
|
||||
import * as identity from "../context/identity"
|
||||
import env from "../environment"
|
||||
import { Ctx, EndpointMatcher, SessionCookie } from "@budibase/types"
|
||||
import { Ctx, EndpointMatcher, SessionCookie, User } from "@budibase/types"
|
||||
import { InvalidAPIKeyError, ErrorCode } from "../errors"
|
||||
import tracer from "dd-trace"
|
||||
|
||||
|
@ -41,7 +41,10 @@ function finalise(ctx: any, opts: FinaliseOpts = {}) {
|
|||
ctx.version = opts.version
|
||||
}
|
||||
|
||||
async function checkApiKey(apiKey: string, populateUser?: Function) {
|
||||
async function checkApiKey(
|
||||
apiKey: string,
|
||||
populateUser?: (userId: string, tenantId: string) => Promise<User>
|
||||
) {
|
||||
// check both the primary and the fallback internal api keys
|
||||
// this allows for rotation
|
||||
if (isValidInternalAPIKey(apiKey)) {
|
||||
|
@ -128,6 +131,7 @@ export default function (
|
|||
} else {
|
||||
user = await getUser(userId, session.tenantId)
|
||||
}
|
||||
// @ts-ignore
|
||||
user.csrfToken = session.csrfToken
|
||||
|
||||
if (session?.lastAccessedAt < timeMinusOneMinute()) {
|
||||
|
@ -167,19 +171,25 @@ export default function (
|
|||
authenticated = false
|
||||
}
|
||||
|
||||
if (user) {
|
||||
const isUser = (
|
||||
user: any
|
||||
): user is User & { budibaseAccess?: string } => {
|
||||
return user && user.email
|
||||
}
|
||||
|
||||
if (isUser(user)) {
|
||||
tracer.setUser({
|
||||
id: user?._id,
|
||||
tenantId: user?.tenantId,
|
||||
budibaseAccess: user?.budibaseAccess,
|
||||
status: user?.status,
|
||||
id: user._id!,
|
||||
tenantId: user.tenantId,
|
||||
budibaseAccess: user.budibaseAccess,
|
||||
status: user.status,
|
||||
})
|
||||
}
|
||||
|
||||
// isAuthenticated is a function, so use a variable to be able to check authed state
|
||||
finalise(ctx, { authenticated, user, internal, version, publicEndpoint })
|
||||
|
||||
if (user && user.email) {
|
||||
if (isUser(user)) {
|
||||
return identity.doInUserContext(user, ctx, next)
|
||||
} else {
|
||||
return next()
|
||||
|
|
|
@ -13,13 +13,14 @@ import { bucketTTLConfig, budibaseTempDir } from "./utils"
|
|||
import { v4 } from "uuid"
|
||||
import { APP_PREFIX, APP_DEV_PREFIX } from "../db"
|
||||
import fsp from "fs/promises"
|
||||
import { HeadObjectOutput } from "aws-sdk/clients/s3"
|
||||
|
||||
const streamPipeline = promisify(stream.pipeline)
|
||||
// use this as a temporary store of buckets that are being created
|
||||
const STATE = {
|
||||
bucketCreationPromises: {},
|
||||
}
|
||||
const signedFilePrefix = "/files/signed"
|
||||
export const SIGNED_FILE_PREFIX = "/files/signed"
|
||||
|
||||
type ListParams = {
|
||||
ContinuationToken?: string
|
||||
|
@ -40,8 +41,13 @@ type UploadParams = BaseUploadParams & {
|
|||
path?: string | PathLike
|
||||
}
|
||||
|
||||
type StreamUploadParams = BaseUploadParams & {
|
||||
stream: ReadStream
|
||||
export type StreamTypes =
|
||||
| ReadStream
|
||||
| NodeJS.ReadableStream
|
||||
| ReadableStream<Uint8Array>
|
||||
|
||||
export type StreamUploadParams = BaseUploadParams & {
|
||||
stream?: StreamTypes
|
||||
}
|
||||
|
||||
const CONTENT_TYPE_MAP: any = {
|
||||
|
@ -83,7 +89,7 @@ export function ObjectStore(
|
|||
bucket: string,
|
||||
opts: { presigning: boolean } = { presigning: false }
|
||||
) {
|
||||
const config: any = {
|
||||
const config: AWS.S3.ClientConfiguration = {
|
||||
s3ForcePathStyle: true,
|
||||
signatureVersion: "v4",
|
||||
apiVersion: "2006-03-01",
|
||||
|
@ -174,11 +180,9 @@ export async function upload({
|
|||
const objectStore = ObjectStore(bucketName)
|
||||
const bucketCreated = await createBucketIfNotExists(objectStore, bucketName)
|
||||
|
||||
if (ttl && (bucketCreated.created || bucketCreated.exists)) {
|
||||
if (ttl && bucketCreated.created) {
|
||||
let ttlConfig = bucketTTLConfig(bucketName, ttl)
|
||||
if (objectStore.putBucketLifecycleConfiguration) {
|
||||
await objectStore.putBucketLifecycleConfiguration(ttlConfig).promise()
|
||||
}
|
||||
await objectStore.putBucketLifecycleConfiguration(ttlConfig).promise()
|
||||
}
|
||||
|
||||
let contentType = type
|
||||
|
@ -222,11 +226,9 @@ export async function streamUpload({
|
|||
const objectStore = ObjectStore(bucketName)
|
||||
const bucketCreated = await createBucketIfNotExists(objectStore, bucketName)
|
||||
|
||||
if (ttl && (bucketCreated.created || bucketCreated.exists)) {
|
||||
if (ttl && bucketCreated.created) {
|
||||
let ttlConfig = bucketTTLConfig(bucketName, ttl)
|
||||
if (objectStore.putBucketLifecycleConfiguration) {
|
||||
await objectStore.putBucketLifecycleConfiguration(ttlConfig).promise()
|
||||
}
|
||||
await objectStore.putBucketLifecycleConfiguration(ttlConfig).promise()
|
||||
}
|
||||
|
||||
// Set content type for certain known extensions
|
||||
|
@ -333,7 +335,7 @@ export function getPresignedUrl(
|
|||
const signedUrl = new URL(url)
|
||||
const path = signedUrl.pathname
|
||||
const query = signedUrl.search
|
||||
return `${signedFilePrefix}${path}${query}`
|
||||
return `${SIGNED_FILE_PREFIX}${path}${query}`
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -521,6 +523,26 @@ export async function getReadStream(
|
|||
return client.getObject(params).createReadStream()
|
||||
}
|
||||
|
||||
export async function getObjectMetadata(
|
||||
bucket: string,
|
||||
path: string
|
||||
): Promise<HeadObjectOutput> {
|
||||
bucket = sanitizeBucket(bucket)
|
||||
path = sanitizeKey(path)
|
||||
|
||||
const client = ObjectStore(bucket)
|
||||
const params = {
|
||||
Bucket: bucket,
|
||||
Key: path,
|
||||
}
|
||||
|
||||
try {
|
||||
return await client.headObject(params).promise()
|
||||
} catch (err: any) {
|
||||
throw new Error("Unable to retrieve metadata from object")
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
Given a signed url like '/files/signed/tmp-files-attachments/app_123456/myfile.txt' extract
|
||||
the bucket and the path from it
|
||||
|
@ -530,7 +552,9 @@ export function extractBucketAndPath(
|
|||
): { bucket: string; path: string } | null {
|
||||
const baseUrl = url.split("?")[0]
|
||||
|
||||
const regex = new RegExp(`^${signedFilePrefix}/(?<bucket>[^/]+)/(?<path>.+)$`)
|
||||
const regex = new RegExp(
|
||||
`^${SIGNED_FILE_PREFIX}/(?<bucket>[^/]+)/(?<path>.+)$`
|
||||
)
|
||||
const match = baseUrl.match(regex)
|
||||
|
||||
if (match && match.groups) {
|
||||
|
|
|
@ -1,9 +1,14 @@
|
|||
import { join } from "path"
|
||||
import path, { join } from "path"
|
||||
import { tmpdir } from "os"
|
||||
import fs from "fs"
|
||||
import env from "../environment"
|
||||
import { PutBucketLifecycleConfigurationRequest } from "aws-sdk/clients/s3"
|
||||
|
||||
import * as objectStore from "./objectStore"
|
||||
import {
|
||||
AutomationAttachment,
|
||||
AutomationAttachmentContent,
|
||||
BucketedContent,
|
||||
} from "@budibase/types"
|
||||
/****************************************************
|
||||
* NOTE: When adding a new bucket - name *
|
||||
* sure that S3 usages (like budibase-infra) *
|
||||
|
@ -55,3 +60,50 @@ export const bucketTTLConfig = (
|
|||
|
||||
return params
|
||||
}
|
||||
|
||||
async function processUrlAttachment(
|
||||
attachment: AutomationAttachment
|
||||
): Promise<AutomationAttachmentContent> {
|
||||
const response = await fetch(attachment.url)
|
||||
if (!response.ok || !response.body) {
|
||||
throw new Error(`Unexpected response ${response.statusText}`)
|
||||
}
|
||||
const fallbackFilename = path.basename(new URL(attachment.url).pathname)
|
||||
return {
|
||||
filename: attachment.filename || fallbackFilename,
|
||||
content: response.body,
|
||||
}
|
||||
}
|
||||
|
||||
export async function processObjectStoreAttachment(
|
||||
attachment: AutomationAttachment
|
||||
): Promise<BucketedContent> {
|
||||
const result = objectStore.extractBucketAndPath(attachment.url)
|
||||
|
||||
if (result === null) {
|
||||
throw new Error("Invalid signed URL")
|
||||
}
|
||||
|
||||
const { bucket, path: objectPath } = result
|
||||
const readStream = await objectStore.getReadStream(bucket, objectPath)
|
||||
const fallbackFilename = path.basename(objectPath)
|
||||
return {
|
||||
bucket,
|
||||
path: objectPath,
|
||||
filename: attachment.filename || fallbackFilename,
|
||||
content: readStream,
|
||||
}
|
||||
}
|
||||
|
||||
export async function processAutomationAttachment(
|
||||
attachment: AutomationAttachment
|
||||
): Promise<AutomationAttachmentContent | BucketedContent> {
|
||||
const isFullyFormedUrl =
|
||||
attachment.url?.startsWith("http://") ||
|
||||
attachment.url?.startsWith("https://")
|
||||
if (isFullyFormedUrl) {
|
||||
return await processUrlAttachment(attachment)
|
||||
} else {
|
||||
return await processObjectStoreAttachment(attachment)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,6 +4,3 @@ export { generator } from "./structures"
|
|||
export * as testContainerUtils from "./testContainerUtils"
|
||||
export * as utils from "./utils"
|
||||
export * from "./jestUtils"
|
||||
import * as minio from "./minio"
|
||||
|
||||
export const objectStoreTestProviders = { minio }
|
||||
|
|
|
@ -1,34 +0,0 @@
|
|||
import { GenericContainer, Wait, StartedTestContainer } from "testcontainers"
|
||||
import { AbstractWaitStrategy } from "testcontainers/build/wait-strategies/wait-strategy"
|
||||
import env from "../../../src/environment"
|
||||
|
||||
let container: StartedTestContainer | undefined
|
||||
|
||||
class ObjectStoreWaitStrategy extends AbstractWaitStrategy {
|
||||
async waitUntilReady(container: any, boundPorts: any, startTime?: Date) {
|
||||
const logs = Wait.forListeningPorts()
|
||||
await logs.waitUntilReady(container, boundPorts, startTime)
|
||||
}
|
||||
}
|
||||
|
||||
export async function start(): Promise<void> {
|
||||
container = await new GenericContainer("minio/minio")
|
||||
.withExposedPorts(9000)
|
||||
.withCommand(["server", "/data"])
|
||||
.withEnvironment({
|
||||
MINIO_ACCESS_KEY: "budibase",
|
||||
MINIO_SECRET_KEY: "budibase",
|
||||
})
|
||||
.withWaitStrategy(new ObjectStoreWaitStrategy().withStartupTimeout(30000))
|
||||
.start()
|
||||
|
||||
const port = container.getMappedPort(9000)
|
||||
env._set("MINIO_URL", `http://0.0.0.0:${port}`)
|
||||
}
|
||||
|
||||
export async function stop() {
|
||||
if (container) {
|
||||
await container.stop()
|
||||
container = undefined
|
||||
}
|
||||
}
|
|
@ -86,10 +86,18 @@ export function setupEnv(...envs: any[]) {
|
|||
throw new Error("CouchDB SQL port not found")
|
||||
}
|
||||
|
||||
const minio = getContainerByImage("minio/minio")
|
||||
|
||||
const minioPort = getExposedV4Port(minio, 9000)
|
||||
if (!minioPort) {
|
||||
throw new Error("Minio port not found")
|
||||
}
|
||||
|
||||
const configs = [
|
||||
{ key: "COUCH_DB_PORT", value: `${couchPort}` },
|
||||
{ key: "COUCH_DB_URL", value: `http://127.0.0.1:${couchPort}` },
|
||||
{ key: "COUCH_DB_SQL_URL", value: `http://127.0.0.1:${couchSqlPort}` },
|
||||
{ key: "MINIO_URL", value: `http://127.0.0.1:${minioPort}` },
|
||||
]
|
||||
|
||||
for (const config of configs.filter(x => !!x.value)) {
|
||||
|
|
|
@ -8,6 +8,8 @@
|
|||
export let size = "S"
|
||||
export let extraButtonText
|
||||
export let extraButtonAction
|
||||
export let extraLinkText
|
||||
export let extraLinkAction
|
||||
export let showCloseButton = true
|
||||
|
||||
let show = true
|
||||
|
@ -28,8 +30,13 @@
|
|||
<use xlink:href="#spectrum-icon-18-{icon}" />
|
||||
</svg>
|
||||
<div class="spectrum-Toast-body">
|
||||
<div class="spectrum-Toast-content">
|
||||
<div class="spectrum-Toast-content row-content">
|
||||
<slot />
|
||||
{#if extraLinkText}
|
||||
<button class="link" on:click={extraLinkAction}>
|
||||
<u>{extraLinkText}</u>
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
{#if extraButtonText && extraButtonAction}
|
||||
<button
|
||||
|
@ -73,4 +80,23 @@
|
|||
.spectrum-Button {
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
|
||||
.row-content {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.link {
|
||||
background: none;
|
||||
border: none;
|
||||
margin: 0;
|
||||
margin-left: 0.5em;
|
||||
padding: 0;
|
||||
cursor: pointer;
|
||||
color: white;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
u {
|
||||
font-weight: 600;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -11,6 +11,7 @@
|
|||
export let error = null
|
||||
export let validate = null
|
||||
export let suffix = null
|
||||
export let validateOn = "change"
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
|
@ -24,7 +25,16 @@
|
|||
const newValue = e.target.value
|
||||
dispatch("change", newValue)
|
||||
value = newValue
|
||||
if (validate) {
|
||||
if (validate && (error || validateOn === "change")) {
|
||||
error = validate(newValue)
|
||||
}
|
||||
}
|
||||
|
||||
const onBlur = e => {
|
||||
focused = false
|
||||
const newValue = e.target.value
|
||||
dispatch("blur", newValue)
|
||||
if (validate && validateOn === "blur") {
|
||||
error = validate(newValue)
|
||||
}
|
||||
}
|
||||
|
@ -61,7 +71,7 @@
|
|||
type={type || "text"}
|
||||
on:input={onChange}
|
||||
on:focus={() => (focused = true)}
|
||||
on:blur={() => (focused = false)}
|
||||
on:blur={onBlur}
|
||||
class:placeholder
|
||||
bind:this={ref}
|
||||
/>
|
||||
|
|
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 804 KiB |
|
@ -93,7 +93,6 @@
|
|||
"identity-obj-proxy": "^3.0.0",
|
||||
"jest": "29.7.0",
|
||||
"jsdom": "^21.1.1",
|
||||
"ncp": "^2.0.0",
|
||||
"svelte-jester": "^1.3.2",
|
||||
"vite": "^4.5.0",
|
||||
"vite-plugin-static-copy": "^0.17.0",
|
||||
|
|
|
@ -48,6 +48,7 @@
|
|||
import { TriggerStepID, ActionStepID } from "constants/backend/automations"
|
||||
import { onMount } from "svelte"
|
||||
import { cloneDeep } from "lodash/fp"
|
||||
import { FIELDS } from "constants/backend"
|
||||
|
||||
export let block
|
||||
export let testData
|
||||
|
@ -228,6 +229,10 @@
|
|||
categoryName,
|
||||
bindingName
|
||||
) => {
|
||||
const field = Object.values(FIELDS).find(
|
||||
field => field.type === value.type && field.subtype === value.subtype
|
||||
)
|
||||
|
||||
return {
|
||||
readableBinding: bindingName
|
||||
? `${bindingName}.${name}`
|
||||
|
@ -238,7 +243,7 @@
|
|||
icon,
|
||||
category: categoryName,
|
||||
display: {
|
||||
type: value.type,
|
||||
type: field?.name || value.type,
|
||||
name,
|
||||
rank: isLoopBlock ? idx + 1 : idx - loopBlockCount,
|
||||
},
|
||||
|
@ -282,6 +287,7 @@
|
|||
for (const key in table?.schema) {
|
||||
schema[key] = {
|
||||
type: table.schema[key].type,
|
||||
subtype: table.schema[key].subtype,
|
||||
}
|
||||
}
|
||||
// remove the original binding
|
||||
|
@ -358,7 +364,8 @@
|
|||
value.customType !== "cron" &&
|
||||
value.customType !== "triggerSchema" &&
|
||||
value.customType !== "automationFields" &&
|
||||
value.type !== "attachment"
|
||||
value.type !== "attachment" &&
|
||||
value.type !== "attachment_single"
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -2,6 +2,8 @@
|
|||
import { tables } from "stores/builder"
|
||||
import { Select, Checkbox, Label } from "@budibase/bbui"
|
||||
import { createEventDispatcher } from "svelte"
|
||||
import { FieldType } from "@budibase/types"
|
||||
|
||||
import RowSelectorTypes from "./RowSelectorTypes.svelte"
|
||||
import DrawerBindableSlot from "../../common/bindings/DrawerBindableSlot.svelte"
|
||||
import AutomationBindingPanel from "../../common/bindings/ServerBindingPanel.svelte"
|
||||
|
@ -14,7 +16,6 @@
|
|||
export let bindings
|
||||
export let isTestModal
|
||||
export let isUpdateRow
|
||||
|
||||
$: parsedBindings = bindings.map(binding => {
|
||||
let clone = Object.assign({}, binding)
|
||||
clone.icon = "ShareAndroid"
|
||||
|
@ -26,15 +27,19 @@
|
|||
|
||||
$: {
|
||||
table = $tables.list.find(table => table._id === value?.tableId)
|
||||
schemaFields = Object.entries(table?.schema ?? {})
|
||||
// surface the schema so the user can see it in the json
|
||||
schemaFields.map(([, schema]) => {
|
||||
|
||||
// Just sorting attachment types to the bottom here for a cleaner UX
|
||||
schemaFields = Object.entries(table?.schema ?? {}).sort(
|
||||
([, schemaA], [, schemaB]) =>
|
||||
(schemaA.type === "attachment") - (schemaB.type === "attachment")
|
||||
)
|
||||
|
||||
schemaFields.forEach(([, schema]) => {
|
||||
if (!schema.autocolumn && !value[schema.name]) {
|
||||
value[schema.name] = ""
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const onChangeTable = e => {
|
||||
value["tableId"] = e.detail
|
||||
dispatch("change", value)
|
||||
|
@ -114,10 +119,16 @@
|
|||
</div>
|
||||
{#if schemaFields.length}
|
||||
{#each schemaFields as [field, schema]}
|
||||
{#if !schema.autocolumn && schema.type !== "attachment"}
|
||||
<div class="schema-fields">
|
||||
{#if !schema.autocolumn}
|
||||
<div
|
||||
class:schema-fields={schema.type !== FieldType.ATTACHMENTS &&
|
||||
schema.type !== FieldType.ATTACHMENT_SINGLE}
|
||||
>
|
||||
<Label>{field}</Label>
|
||||
<div class="field-width">
|
||||
<div
|
||||
class:field-width={schema.type !== FieldType.ATTACHMENTS &&
|
||||
schema.type !== FieldType.ATTACHMENT_SINGLE}
|
||||
>
|
||||
{#if isTestModal}
|
||||
<RowSelectorTypes
|
||||
{isTestModal}
|
||||
|
|
|
@ -1,10 +1,12 @@
|
|||
<script>
|
||||
import { Select, DatePicker, Multiselect, TextArea } from "@budibase/bbui"
|
||||
import { FieldType } from "@budibase/types"
|
||||
import LinkedRowSelector from "components/common/LinkedRowSelector.svelte"
|
||||
import DrawerBindableInput from "../../common/bindings/DrawerBindableInput.svelte"
|
||||
import ModalBindableInput from "../../common/bindings/ModalBindableInput.svelte"
|
||||
import AutomationBindingPanel from "../../common/bindings/ServerBindingPanel.svelte"
|
||||
import Editor from "components/integration/QueryEditor.svelte"
|
||||
import KeyValueBuilder from "components/integration/KeyValueBuilder.svelte"
|
||||
|
||||
export let onChange
|
||||
export let field
|
||||
|
@ -22,6 +24,27 @@
|
|||
function schemaHasOptions(schema) {
|
||||
return !!schema.constraints?.inclusion?.length
|
||||
}
|
||||
|
||||
const handleAttachmentParams = keyValuObj => {
|
||||
let params = {}
|
||||
|
||||
if (
|
||||
schema.type === FieldType.ATTACHMENT_SINGLE &&
|
||||
Object.keys(keyValuObj).length === 0
|
||||
) {
|
||||
return []
|
||||
}
|
||||
if (!Array.isArray(keyValuObj)) {
|
||||
keyValuObj = [keyValuObj]
|
||||
}
|
||||
|
||||
if (keyValuObj.length) {
|
||||
for (let param of keyValuObj) {
|
||||
params[param.url] = param.filename
|
||||
}
|
||||
}
|
||||
return params
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if schemaHasOptions(schema) && schema.type !== "array"}
|
||||
|
@ -77,6 +100,35 @@
|
|||
on:change={e => onChange(e, field)}
|
||||
useLabel={false}
|
||||
/>
|
||||
{:else if schema.type === FieldType.ATTACHMENTS || schema.type === FieldType.ATTACHMENT_SINGLE}
|
||||
<div class="attachment-field-spacinng">
|
||||
<KeyValueBuilder
|
||||
on:change={e =>
|
||||
onChange(
|
||||
{
|
||||
detail:
|
||||
schema.type === FieldType.ATTACHMENT_SINGLE
|
||||
? e.detail.length > 0
|
||||
? { url: e.detail[0].name, filename: e.detail[0].value }
|
||||
: {}
|
||||
: e.detail.map(({ name, value }) => ({
|
||||
url: name,
|
||||
filename: value,
|
||||
})),
|
||||
},
|
||||
field
|
||||
)}
|
||||
object={handleAttachmentParams(value[field])}
|
||||
allowJS
|
||||
{bindings}
|
||||
keyBindings
|
||||
customButtonText={"Add attachment"}
|
||||
keyPlaceholder={"URL"}
|
||||
valuePlaceholder={"Filename"}
|
||||
actionButtonDisabled={schema.type === FieldType.ATTACHMENT_SINGLE &&
|
||||
Object.keys(value[field]).length >= 1}
|
||||
/>
|
||||
</div>
|
||||
{:else if ["string", "number", "bigint", "barcodeqr", "array"].includes(schema.type)}
|
||||
<svelte:component
|
||||
this={isTestModal ? ModalBindableInput : DrawerBindableInput}
|
||||
|
@ -90,3 +142,10 @@
|
|||
title={schema.name}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
.attachment-field-spacinng {
|
||||
margin-top: var(--spacing-s);
|
||||
margin-bottom: var(--spacing-l);
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -1,7 +1,9 @@
|
|||
<script>
|
||||
import { createEventDispatcher } from "svelte"
|
||||
import { ActionButton, Modal, ModalContent } from "@budibase/bbui"
|
||||
import { ActionButton, Drawer, DrawerContent, Button } from "@budibase/bbui"
|
||||
import FilterBuilder from "components/design/settings/controls/FilterEditor/FilterBuilder.svelte"
|
||||
import { getUserBindings } from "dataBinding"
|
||||
import { makePropSafe } from "@budibase/string-templates"
|
||||
|
||||
export let schema
|
||||
export let filters
|
||||
|
@ -10,7 +12,7 @@
|
|||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
let modal
|
||||
let drawer
|
||||
|
||||
$: tempValue = filters || []
|
||||
$: schemaFields = Object.entries(schema || {}).map(
|
||||
|
@ -22,37 +24,53 @@
|
|||
|
||||
$: text = getText(filters)
|
||||
$: selected = tempValue.filter(x => !x.onEmptyFilter)?.length > 0
|
||||
|
||||
$: bindings = [
|
||||
{
|
||||
type: "context",
|
||||
runtimeBinding: `${makePropSafe("now")}`,
|
||||
readableBinding: `Date`,
|
||||
category: "Date",
|
||||
icon: "Date",
|
||||
display: {
|
||||
name: "Server date",
|
||||
},
|
||||
},
|
||||
...getUserBindings(),
|
||||
]
|
||||
const getText = filters => {
|
||||
const count = filters?.filter(filter => filter.field)?.length
|
||||
return count ? `Filter (${count})` : "Filter"
|
||||
}
|
||||
</script>
|
||||
|
||||
<ActionButton icon="Filter" quiet {disabled} on:click={modal.show} {selected}>
|
||||
<ActionButton icon="Filter" quiet {disabled} on:click={drawer.show} {selected}>
|
||||
{text}
|
||||
</ActionButton>
|
||||
<Modal bind:this={modal}>
|
||||
<ModalContent
|
||||
title="Filter"
|
||||
confirmText="Save"
|
||||
size="XL"
|
||||
onConfirm={() => dispatch("change", tempValue)}
|
||||
>
|
||||
<div class="wrapper">
|
||||
<FilterBuilder
|
||||
allowBindings={false}
|
||||
{filters}
|
||||
{schemaFields}
|
||||
datasource={{ type: "table", tableId }}
|
||||
on:change={e => (tempValue = e.detail)}
|
||||
/>
|
||||
</div>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
|
||||
<style>
|
||||
.wrapper :global(.main) {
|
||||
padding: 0;
|
||||
}
|
||||
</style>
|
||||
<Drawer
|
||||
bind:this={drawer}
|
||||
title="Filtering"
|
||||
on:drawerHide
|
||||
on:drawerShow
|
||||
forceModal
|
||||
>
|
||||
<Button
|
||||
cta
|
||||
slot="buttons"
|
||||
on:click={() => {
|
||||
dispatch("change", tempValue)
|
||||
drawer.hide()
|
||||
}}
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
<DrawerContent slot="body">
|
||||
<FilterBuilder
|
||||
{filters}
|
||||
{schemaFields}
|
||||
datasource={{ type: "table", tableId }}
|
||||
on:change={e => (tempValue = e.detail)}
|
||||
{bindings}
|
||||
/>
|
||||
</DrawerContent>
|
||||
</Drawer>
|
||||
|
|
|
@ -55,7 +55,7 @@ export function getBindings({
|
|||
)
|
||||
}
|
||||
const field = Object.values(FIELDS).find(
|
||||
field => field.type === schema.type
|
||||
field => field.type === schema.type && field.subtype === schema.subtype
|
||||
)
|
||||
|
||||
const label = path == null ? column : `${path}.0.${column}`
|
||||
|
|
|
@ -12,8 +12,13 @@
|
|||
OptionSelectDnD,
|
||||
Layout,
|
||||
AbsTooltip,
|
||||
ProgressCircle,
|
||||
} from "@budibase/bbui"
|
||||
import { SWITCHABLE_TYPES, ValidColumnNameRegex } from "@budibase/shared-core"
|
||||
import {
|
||||
SWITCHABLE_TYPES,
|
||||
ValidColumnNameRegex,
|
||||
helpers,
|
||||
} from "@budibase/shared-core"
|
||||
import { createEventDispatcher, getContext, onMount } from "svelte"
|
||||
import { cloneDeep } from "lodash/fp"
|
||||
import { tables, datasources } from "stores/builder"
|
||||
|
@ -30,8 +35,8 @@
|
|||
import { getBindings } from "components/backend/DataTable/formula"
|
||||
import JSONSchemaModal from "./JSONSchemaModal.svelte"
|
||||
import {
|
||||
FieldType,
|
||||
BBReferenceFieldSubType,
|
||||
FieldType,
|
||||
SourceName,
|
||||
} from "@budibase/types"
|
||||
import RelationshipSelector from "components/common/RelationshipSelector.svelte"
|
||||
|
@ -67,7 +72,6 @@
|
|||
let savingColumn
|
||||
let deleteColName
|
||||
let jsonSchemaModal
|
||||
let allowedTypes = []
|
||||
let editableColumn = {
|
||||
type: FIELDS.STRING.type,
|
||||
constraints: FIELDS.STRING.constraints,
|
||||
|
@ -175,6 +179,11 @@
|
|||
SWITCHABLE_TYPES[field.type] &&
|
||||
!editableColumn?.autocolumn)
|
||||
|
||||
$: allowedTypes = getAllowedTypes(datasource).map(t => ({
|
||||
fieldId: makeFieldId(t.type, t.subtype),
|
||||
...t,
|
||||
}))
|
||||
|
||||
const fieldDefinitions = Object.values(FIELDS).reduce(
|
||||
// Storing the fields by complex field id
|
||||
(acc, field) => ({
|
||||
|
@ -188,7 +197,10 @@
|
|||
// don't make field IDs for auto types
|
||||
if (type === AUTO_TYPE || autocolumn) {
|
||||
return type.toUpperCase()
|
||||
} else if (type === FieldType.BB_REFERENCE) {
|
||||
} else if (
|
||||
type === FieldType.BB_REFERENCE ||
|
||||
type === FieldType.BB_REFERENCE_SINGLE
|
||||
) {
|
||||
return `${type}${subtype || ""}`.toUpperCase()
|
||||
} else {
|
||||
return type.toUpperCase()
|
||||
|
@ -226,11 +238,6 @@
|
|||
editableColumn.subtype,
|
||||
editableColumn.autocolumn
|
||||
)
|
||||
|
||||
allowedTypes = getAllowedTypes().map(t => ({
|
||||
fieldId: makeFieldId(t.type, t.subtype),
|
||||
...t,
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -245,11 +252,11 @@
|
|||
}
|
||||
|
||||
async function saveColumn() {
|
||||
savingColumn = true
|
||||
if (errors?.length) {
|
||||
return
|
||||
}
|
||||
|
||||
savingColumn = true
|
||||
let saveColumn = cloneDeep(editableColumn)
|
||||
|
||||
delete saveColumn.fieldId
|
||||
|
@ -264,13 +271,6 @@
|
|||
if (saveColumn.type !== LINK_TYPE) {
|
||||
delete saveColumn.fieldName
|
||||
}
|
||||
if (isUsersColumn(saveColumn)) {
|
||||
if (saveColumn.subtype === BBReferenceFieldSubType.USER) {
|
||||
saveColumn.relationshipType = RelationshipType.ONE_TO_MANY
|
||||
} else if (saveColumn.subtype === BBReferenceFieldSubType.USERS) {
|
||||
saveColumn.relationshipType = RelationshipType.MANY_TO_MANY
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
await tables.saveField({
|
||||
|
@ -289,6 +289,8 @@
|
|||
}
|
||||
} catch (err) {
|
||||
notifications.error(`Error saving column: ${err.message}`)
|
||||
} finally {
|
||||
savingColumn = false
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -363,20 +365,36 @@
|
|||
deleteColName = ""
|
||||
}
|
||||
|
||||
function getAllowedTypes() {
|
||||
function getAllowedTypes(datasource) {
|
||||
if (originalName) {
|
||||
const possibleTypes = SWITCHABLE_TYPES[field.type] || [
|
||||
editableColumn.type,
|
||||
]
|
||||
let possibleTypes = SWITCHABLE_TYPES[field.type] || [editableColumn.type]
|
||||
if (helpers.schema.isDeprecatedSingleUserColumn(editableColumn)) {
|
||||
// This will handle old single users columns
|
||||
return [
|
||||
{
|
||||
...FIELDS.USER,
|
||||
type: FieldType.BB_REFERENCE,
|
||||
subtype: BBReferenceFieldSubType.USER,
|
||||
},
|
||||
]
|
||||
} else if (
|
||||
editableColumn.type === FieldType.BB_REFERENCE &&
|
||||
editableColumn.subtype === BBReferenceFieldSubType.USERS
|
||||
) {
|
||||
// This will handle old multi users columns
|
||||
return [
|
||||
{
|
||||
...FIELDS.USERS,
|
||||
subtype: BBReferenceFieldSubType.USERS,
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
return Object.entries(FIELDS)
|
||||
.filter(([_, field]) => possibleTypes.includes(field.type))
|
||||
.map(([_, fieldDefinition]) => fieldDefinition)
|
||||
}
|
||||
|
||||
const isUsers =
|
||||
editableColumn.type === FieldType.BB_REFERENCE &&
|
||||
editableColumn.subtype === BBReferenceFieldSubType.USERS
|
||||
|
||||
if (!externalTable) {
|
||||
return [
|
||||
FIELDS.STRING,
|
||||
|
@ -393,7 +411,8 @@
|
|||
FIELDS.LINK,
|
||||
FIELDS.FORMULA,
|
||||
FIELDS.JSON,
|
||||
isUsers ? FIELDS.USERS : FIELDS.USER,
|
||||
FIELDS.USER,
|
||||
FIELDS.USERS,
|
||||
FIELDS.AUTO,
|
||||
]
|
||||
} else {
|
||||
|
@ -407,8 +426,12 @@
|
|||
FIELDS.BOOLEAN,
|
||||
FIELDS.FORMULA,
|
||||
FIELDS.BIGINT,
|
||||
isUsers ? FIELDS.USERS : FIELDS.USER,
|
||||
FIELDS.USER,
|
||||
]
|
||||
|
||||
if (datasource && datasource.source !== SourceName.GOOGLE_SHEETS) {
|
||||
fields.push(FIELDS.USERS)
|
||||
}
|
||||
// no-sql or a spreadsheet
|
||||
if (!externalTable || table.sql) {
|
||||
fields = [...fields, FIELDS.LINK, FIELDS.ARRAY]
|
||||
|
@ -482,15 +505,6 @@
|
|||
return newError
|
||||
}
|
||||
|
||||
function isUsersColumn(column) {
|
||||
return (
|
||||
column.type === FieldType.BB_REFERENCE &&
|
||||
[BBReferenceFieldSubType.USER, BBReferenceFieldSubType.USERS].includes(
|
||||
column.subtype
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
mounted = true
|
||||
})
|
||||
|
@ -689,22 +703,6 @@
|
|||
<Button primary text on:click={openJsonSchemaEditor}
|
||||
>Open schema editor</Button
|
||||
>
|
||||
{:else if isUsersColumn(editableColumn) && datasource?.source !== SourceName.GOOGLE_SHEETS}
|
||||
<Toggle
|
||||
value={editableColumn.subtype === BBReferenceFieldSubType.USERS}
|
||||
on:change={e =>
|
||||
handleTypeChange(
|
||||
makeFieldId(
|
||||
FieldType.BB_REFERENCE,
|
||||
e.detail
|
||||
? BBReferenceFieldSubType.USERS
|
||||
: BBReferenceFieldSubType.USER
|
||||
)
|
||||
)}
|
||||
disabled={!isCreating}
|
||||
thin
|
||||
text="Allow multiple users"
|
||||
/>
|
||||
{/if}
|
||||
{#if editableColumn.type === AUTO_TYPE || editableColumn.autocolumn}
|
||||
<Select
|
||||
|
@ -739,7 +737,20 @@
|
|||
<Button quiet warning text on:click={confirmDelete}>Delete</Button>
|
||||
{/if}
|
||||
<Button secondary newStyles on:click={cancelEdit}>Cancel</Button>
|
||||
<Button disabled={invalid} newStyles cta on:click={saveColumn}>Save</Button>
|
||||
<Button
|
||||
disabled={invalid || savingColumn}
|
||||
newStyles
|
||||
cta
|
||||
on:click={saveColumn}
|
||||
>
|
||||
{#if savingColumn}
|
||||
<div class="save-loading">
|
||||
<ProgressCircle overBackground={true} size="S" />
|
||||
</div>
|
||||
{:else}
|
||||
Save
|
||||
{/if}
|
||||
</Button>
|
||||
</div>
|
||||
<Modal bind:this={jsonSchemaModal}>
|
||||
<JSONSchemaModal
|
||||
|
@ -804,4 +815,9 @@
|
|||
cursor: pointer;
|
||||
color: var(--spectrum-global-color-gray-900);
|
||||
}
|
||||
|
||||
.save-loading {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -13,7 +13,9 @@
|
|||
onMount(() => subscribe("edit-column", editColumn))
|
||||
</script>
|
||||
|
||||
<CreateEditColumn
|
||||
field={editableColumn}
|
||||
on:updatecolumns={rows.actions.refreshData}
|
||||
/>
|
||||
{#if editableColumn}
|
||||
<CreateEditColumn
|
||||
field={editableColumn}
|
||||
on:updatecolumns={rows.actions.refreshData}
|
||||
/>
|
||||
{/if}
|
||||
|
|
|
@ -59,13 +59,17 @@
|
|||
value: FieldType.ATTACHMENTS,
|
||||
},
|
||||
{
|
||||
label: "User",
|
||||
label: "Users",
|
||||
value: `${FieldType.BB_REFERENCE}${BBReferenceFieldSubType.USER}`,
|
||||
},
|
||||
{
|
||||
label: "Users",
|
||||
value: `${FieldType.BB_REFERENCE}${BBReferenceFieldSubType.USERS}`,
|
||||
},
|
||||
{
|
||||
label: "User",
|
||||
value: `${FieldType.BB_REFERENCE_SINGLE}${BBReferenceFieldSubType.USER}`,
|
||||
},
|
||||
]
|
||||
|
||||
$: {
|
||||
|
|
|
@ -23,6 +23,7 @@
|
|||
faQuestionCircle,
|
||||
faCircleCheck,
|
||||
faGear,
|
||||
faRectangleList,
|
||||
} from "@fortawesome/free-solid-svg-icons"
|
||||
import { faGithub, faDiscord } from "@fortawesome/free-brands-svg-icons"
|
||||
|
||||
|
@ -37,6 +38,7 @@
|
|||
faFileArrowUp,
|
||||
faChevronLeft,
|
||||
faCircleInfo,
|
||||
faRectangleList,
|
||||
|
||||
// -- Required for easyMDE use in the builder.
|
||||
faBold,
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
import { isEnabled, TENANT_FEATURE_FLAGS } from "helpers/featureFlags"
|
||||
import { licensing } from "stores/portal"
|
||||
import { isPremiumOrAbove } from "helpers/planTitle"
|
||||
import { ChangelogURL } from "constants"
|
||||
|
||||
$: premiumOrAboveLicense = isPremiumOrAbove($licensing?.license?.plan?.type)
|
||||
|
||||
|
@ -30,6 +31,13 @@
|
|||
<Body size="S">Help docs</Body>
|
||||
</a>
|
||||
<div class="divider" />
|
||||
<a target="_blank" href={ChangelogURL}>
|
||||
<div class="icon">
|
||||
<FontAwesomeIcon name="fa-solid fa-rectangle-list" />
|
||||
</div>
|
||||
<Body size="S">Changelog</Body>
|
||||
</a>
|
||||
<div class="divider" />
|
||||
<a
|
||||
target="_blank"
|
||||
href="https://github.com/Budibase/budibase/discussions"
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
readableToRuntimeBinding,
|
||||
runtimeToReadableBinding,
|
||||
} from "dataBinding"
|
||||
import { FieldType } from "@budibase/types"
|
||||
|
||||
import ClientBindingPanel from "components/common/bindings/ClientBindingPanel.svelte"
|
||||
import { createEventDispatcher, setContext } from "svelte"
|
||||
|
@ -102,6 +103,8 @@
|
|||
longform: value => !isJSBinding(value),
|
||||
json: value => !isJSBinding(value),
|
||||
boolean: isValidBoolean,
|
||||
attachment: false,
|
||||
attachment_single: false,
|
||||
}
|
||||
|
||||
const isValid = value => {
|
||||
|
@ -116,7 +119,16 @@
|
|||
if (type === "json" && !isJSBinding(value)) {
|
||||
return "json-slot-icon"
|
||||
}
|
||||
if (!["string", "number", "bigint", "barcodeqr"].includes(type)) {
|
||||
if (
|
||||
![
|
||||
"string",
|
||||
"number",
|
||||
"bigint",
|
||||
"barcodeqr",
|
||||
"attachment",
|
||||
"attachment_single",
|
||||
].includes(type)
|
||||
) {
|
||||
return "slot-icon"
|
||||
}
|
||||
return ""
|
||||
|
@ -157,7 +169,7 @@
|
|||
{updateOnChange}
|
||||
/>
|
||||
{/if}
|
||||
{#if !disabled && type !== "formula"}
|
||||
{#if !disabled && type !== "formula" && !disabled && type !== FieldType.ATTACHMENTS && !disabled && type !== FieldType.ATTACHMENT_SINGLE}
|
||||
<div
|
||||
class={`icon ${getIconClass(value, type)}`}
|
||||
on:click={() => {
|
||||
|
|
|
@ -7,10 +7,12 @@
|
|||
Body,
|
||||
Button,
|
||||
StatusLight,
|
||||
Link,
|
||||
} from "@budibase/bbui"
|
||||
import { appStore, initialise } from "stores/builder"
|
||||
import { API } from "api"
|
||||
import RevertModalVersionSelect from "./RevertModalVersionSelect.svelte"
|
||||
import { ChangelogURL } from "constants"
|
||||
|
||||
export function show() {
|
||||
updateModal.show()
|
||||
|
@ -106,6 +108,10 @@
|
|||
latest version available.
|
||||
</Body>
|
||||
{/if}
|
||||
<Body size="S">
|
||||
Find the changelog for the latest release
|
||||
<Link href={ChangelogURL} target="_blank">here</Link>
|
||||
</Body>
|
||||
{#if revertAvailable}
|
||||
<Body size="S">
|
||||
You can revert this app to version
|
||||
|
|
|
@ -49,17 +49,20 @@
|
|||
},
|
||||
]
|
||||
|
||||
$: tables = findAllMatchingComponents($selectedScreen?.props, component =>
|
||||
component._component.endsWith("table")
|
||||
)
|
||||
$: tableBlocks = findAllMatchingComponents(
|
||||
$: components = findAllMatchingComponents(
|
||||
$selectedScreen?.props,
|
||||
component => component._component.endsWith("tableblock")
|
||||
component => {
|
||||
const type = component._component
|
||||
return (
|
||||
type.endsWith("/table") ||
|
||||
type.endsWith("/tableblock") ||
|
||||
type.endsWith("/gridblock")
|
||||
)
|
||||
}
|
||||
)
|
||||
$: components = tables.concat(tableBlocks)
|
||||
$: componentOptions = components.map(table => ({
|
||||
label: table._instanceName,
|
||||
value: table._component.includes("tableblock")
|
||||
value: table._component.endsWith("/tableblock")
|
||||
? `${table._id}-table`
|
||||
: table._id,
|
||||
}))
|
||||
|
@ -69,6 +72,7 @@
|
|||
$: selectedTable = components.find(
|
||||
component => component._id === selectedTableId
|
||||
)
|
||||
$: parameters.rows = `{{ literal [${parameters.tableComponentId}].[selectedRows] }}`
|
||||
|
||||
onMount(() => {
|
||||
if (!parameters.type) {
|
||||
|
|
|
@ -47,4 +47,5 @@ export const FieldTypeToComponentMap = {
|
|||
[FieldType.JSON]: "jsonfield",
|
||||
[FieldType.BARCODEQR]: "codescanner",
|
||||
[FieldType.BB_REFERENCE]: "bbreferencefield",
|
||||
[FieldType.BB_REFERENCE_SINGLE]: "bbreferencesinglefield",
|
||||
}
|
||||
|
|
|
@ -37,6 +37,7 @@
|
|||
export let customButtonText = null
|
||||
export let keyBindings = false
|
||||
export let allowJS = false
|
||||
export let actionButtonDisabled = false
|
||||
export let compare = (option, value) => option === value
|
||||
|
||||
let fields = Object.entries(object || {}).map(([name, value]) => ({
|
||||
|
@ -189,7 +190,14 @@
|
|||
{/if}
|
||||
{#if !readOnly && !noAddButton}
|
||||
<div>
|
||||
<ActionButton icon="Add" secondary thin outline on:click={addEntry}>
|
||||
<ActionButton
|
||||
disabled={actionButtonDisabled}
|
||||
icon="Add"
|
||||
secondary
|
||||
thin
|
||||
outline
|
||||
on:click={addEntry}
|
||||
>
|
||||
{#if customButtonText}
|
||||
{customButtonText}
|
||||
{:else}
|
||||
|
|
|
@ -0,0 +1,43 @@
|
|||
<script>
|
||||
import "@spectrum-css/toast/dist/index-vars.css"
|
||||
import Portal from "svelte-portal"
|
||||
import { fly } from "svelte/transition"
|
||||
import { Banner, BANNER_TYPES } from "@budibase/bbui"
|
||||
import { licensing } from "stores/portal"
|
||||
|
||||
export let show = true
|
||||
|
||||
const oneDayInSeconds = 86400
|
||||
|
||||
$: license = $licensing.license
|
||||
|
||||
function daysUntilCancel() {
|
||||
const cancelAt = license?.billing?.subscription?.cancelAt
|
||||
const diffTime = Math.abs(cancelAt - new Date().getTime()) / 1000
|
||||
return Math.floor(diffTime / oneDayInSeconds)
|
||||
}
|
||||
</script>
|
||||
|
||||
<Portal target=".banner-container">
|
||||
<div class="banner">
|
||||
{#if show}
|
||||
<div transition:fly={{ y: -30 }}>
|
||||
<Banner
|
||||
type={BANNER_TYPES.INFO}
|
||||
extraLinkText={"Please select a plan."}
|
||||
extraLinkAction={$licensing.goToUpgradePage}
|
||||
showCloseButton={false}
|
||||
>
|
||||
Your free trial will end in {daysUntilCancel()} days.
|
||||
</Banner>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</Portal>
|
||||
|
||||
<style>
|
||||
.banner {
|
||||
pointer-events: none;
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
|
@ -12,7 +12,7 @@ const defaultCacheFn = key => {
|
|||
const upgradeAction = key => {
|
||||
return defaultNavigateAction(
|
||||
key,
|
||||
"Upgrade Plan",
|
||||
"Upgrade",
|
||||
`${get(admin).accountPortalUrl}/portal/upgrade`
|
||||
)
|
||||
}
|
||||
|
|
|
@ -0,0 +1,66 @@
|
|||
<script>
|
||||
import { Modal, ModalContent } from "@budibase/bbui"
|
||||
import FreeTrial from "../../../../assets/FreeTrial.svelte"
|
||||
import { get } from "svelte/store"
|
||||
import { auth, licensing } from "stores/portal"
|
||||
import { API } from "api"
|
||||
import { PlanType } from "@budibase/types"
|
||||
import { sdk } from "@budibase/shared-core"
|
||||
|
||||
let freeTrialModal
|
||||
|
||||
$: planType = $licensing?.license?.plan?.type
|
||||
$: showFreeTrialModal(planType, freeTrialModal)
|
||||
|
||||
const showFreeTrialModal = (planType, freeTrialModal) => {
|
||||
if (
|
||||
planType === PlanType.ENTERPRISE_BASIC_TRIAL &&
|
||||
!$auth.user?.freeTrialConfirmedAt &&
|
||||
sdk.users.isAdmin($auth.user)
|
||||
) {
|
||||
freeTrialModal?.show()
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<Modal bind:this={freeTrialModal} disableCancel={true}>
|
||||
<ModalContent
|
||||
confirmText="Get started"
|
||||
size="M"
|
||||
showCancelButton={false}
|
||||
showCloseIcon={false}
|
||||
onConfirm={async () => {
|
||||
if (get(auth).user) {
|
||||
try {
|
||||
await API.updateSelf({
|
||||
freeTrialConfirmedAt: new Date().toISOString(),
|
||||
})
|
||||
// Update the cached user
|
||||
await auth.getSelf()
|
||||
} finally {
|
||||
freeTrialModal.hide()
|
||||
}
|
||||
}
|
||||
}}
|
||||
>
|
||||
<h1>Experience all of Budibase with a free 14-day trial</h1>
|
||||
<div class="free-trial-text">
|
||||
We've upgraded you to a free 14-day trial that allows you to try all our
|
||||
features before deciding which plan is right for you.
|
||||
<p>
|
||||
At the end of your trial, we'll automatically downgrade you to the Free
|
||||
plan unless you choose to upgrade.
|
||||
</p>
|
||||
</div>
|
||||
<FreeTrial />
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
|
||||
<style>
|
||||
h1 {
|
||||
font-size: 26px;
|
||||
}
|
||||
.free-trial-text {
|
||||
font-size: 16px;
|
||||
}
|
||||
</style>
|
|
@ -159,15 +159,17 @@ export const FIELDS = {
|
|||
},
|
||||
USER: {
|
||||
name: "User",
|
||||
type: FieldType.BB_REFERENCE,
|
||||
type: FieldType.BB_REFERENCE_SINGLE,
|
||||
subtype: BBReferenceFieldSubType.USER,
|
||||
icon: TypeIconMap[FieldType.USER],
|
||||
icon: TypeIconMap[FieldType.BB_REFERENCE_SINGLE][
|
||||
BBReferenceFieldSubType.USER
|
||||
],
|
||||
},
|
||||
USERS: {
|
||||
name: "Users",
|
||||
name: "User List",
|
||||
type: FieldType.BB_REFERENCE,
|
||||
subtype: BBReferenceFieldSubType.USERS,
|
||||
icon: TypeIconMap[FieldType.USERS],
|
||||
subtype: BBReferenceFieldSubType.USER,
|
||||
icon: TypeIconMap[FieldType.BB_REFERENCE][BBReferenceFieldSubType.USER],
|
||||
constraints: {
|
||||
type: "array",
|
||||
},
|
||||
|
|
|
@ -70,3 +70,5 @@ export const PlanModel = {
|
|||
PER_USER: "perUser",
|
||||
DAY_PASS: "dayPass",
|
||||
}
|
||||
|
||||
export const ChangelogURL = "https://docs.budibase.com/changelog"
|
||||
|
|
|
@ -29,6 +29,7 @@ import { JSONUtils, Constants } from "@budibase/frontend-core"
|
|||
import ActionDefinitions from "components/design/settings/controls/ButtonActionEditor/manifest.json"
|
||||
import { environment, licensing } from "stores/portal"
|
||||
import { convertOldFieldFormat } from "components/design/settings/controls/FieldConfiguration/utils"
|
||||
import { FIELDS } from "constants/backend"
|
||||
|
||||
const { ContextScopes } = Constants
|
||||
|
||||
|
@ -491,7 +492,7 @@ const generateComponentContextBindings = (asset, componentContext) => {
|
|||
icon: bindingCategory.icon,
|
||||
display: {
|
||||
name: `${fieldSchema.name || key}`,
|
||||
type: fieldSchema.type,
|
||||
type: fieldSchema.display?.type || fieldSchema.type,
|
||||
},
|
||||
})
|
||||
})
|
||||
|
@ -1019,15 +1020,23 @@ export const getSchemaForDatasource = (asset, datasource, options) => {
|
|||
// are objects
|
||||
let fixedSchema = {}
|
||||
Object.entries(schema || {}).forEach(([fieldName, fieldSchema]) => {
|
||||
const field = Object.values(FIELDS).find(
|
||||
field =>
|
||||
field.type === fieldSchema.type &&
|
||||
field.subtype === fieldSchema.subtype
|
||||
)
|
||||
|
||||
if (typeof fieldSchema === "string") {
|
||||
fixedSchema[fieldName] = {
|
||||
type: fieldSchema,
|
||||
name: fieldName,
|
||||
display: { type: fieldSchema },
|
||||
}
|
||||
} else {
|
||||
fixedSchema[fieldName] = {
|
||||
...fieldSchema,
|
||||
name: fieldName,
|
||||
display: { type: field?.name || fieldSchema.type },
|
||||
}
|
||||
}
|
||||
})
|
||||
|
|
|
@ -20,6 +20,9 @@ export function getFormattedPlanName(userPlanType) {
|
|||
case PlanType.ENTERPRISE:
|
||||
planName = "Enterprise"
|
||||
break
|
||||
case PlanType.ENTERPRISE_BASIC_TRIAL:
|
||||
planName = "Trial"
|
||||
break
|
||||
default:
|
||||
planName = "Free" // Default to "Free" if the type is not explicitly handled
|
||||
}
|
||||
|
|
|
@ -32,6 +32,7 @@
|
|||
import { UserAvatars } from "@budibase/frontend-core"
|
||||
import { TOUR_KEYS } from "components/portal/onboarding/tours.js"
|
||||
import PreviewOverlay from "./_components/PreviewOverlay.svelte"
|
||||
import EnterpriseBasicTrialModal from "components/portal/onboarding/EnterpriseBasicTrialModal.svelte"
|
||||
|
||||
export let application
|
||||
|
||||
|
@ -103,6 +104,10 @@
|
|||
}
|
||||
|
||||
onMount(async () => {
|
||||
document.fonts.onloadingdone = e => {
|
||||
builderStore.loadFonts(e.fontfaces)
|
||||
}
|
||||
|
||||
if (!hasSynced && application) {
|
||||
try {
|
||||
await API.syncApp(application)
|
||||
|
@ -143,17 +148,19 @@
|
|||
/>
|
||||
</span>
|
||||
<Tabs {selected} size="M">
|
||||
{#each $layout.children as { path, title }}
|
||||
<TourWrap stepKeys={[`builder-${title}-section`]}>
|
||||
<Tab
|
||||
quiet
|
||||
selected={$isActive(path)}
|
||||
on:click={topItemNavigate(path)}
|
||||
title={capitalise(title)}
|
||||
id={`builder-${title}-tab`}
|
||||
/>
|
||||
</TourWrap>
|
||||
{/each}
|
||||
{#key $builderStore?.fonts}
|
||||
{#each $layout.children as { path, title }}
|
||||
<TourWrap stepKeys={[`builder-${title}-section`]}>
|
||||
<Tab
|
||||
quiet
|
||||
selected={$isActive(path)}
|
||||
on:click={topItemNavigate(path)}
|
||||
title={capitalise(title)}
|
||||
id={`builder-${title}-tab`}
|
||||
/>
|
||||
</TourWrap>
|
||||
{/each}
|
||||
{/key}
|
||||
</Tabs>
|
||||
</div>
|
||||
<div class="topcenternav">
|
||||
|
@ -192,6 +199,8 @@
|
|||
<CommandPalette />
|
||||
</Modal>
|
||||
|
||||
<EnterpriseBasicTrialModal />
|
||||
|
||||
<style>
|
||||
.back-to-apps {
|
||||
display: contents;
|
||||
|
|
|
@ -71,6 +71,7 @@
|
|||
"multifieldselect",
|
||||
"s3upload",
|
||||
"codescanner",
|
||||
"bbreferencesinglefield",
|
||||
"bbreferencefield"
|
||||
]
|
||||
},
|
||||
|
|
|
@ -98,14 +98,22 @@
|
|||
})
|
||||
}
|
||||
|
||||
async function fetchBackups(filters, page, dateRange) {
|
||||
const response = await backups.searchBackups({
|
||||
async function fetchBackups(filters, page, dateRange = []) {
|
||||
const body = {
|
||||
appId: $appStore.appId,
|
||||
...filters,
|
||||
page,
|
||||
startDate: dateRange[0],
|
||||
endDate: dateRange[1],
|
||||
})
|
||||
}
|
||||
|
||||
const [startDate, endDate] = dateRange
|
||||
if (startDate) {
|
||||
body.startDate = startDate
|
||||
}
|
||||
if (endDate) {
|
||||
body.endDate = endDate
|
||||
}
|
||||
|
||||
const response = await backups.searchBackups(body)
|
||||
pageInfo.fetched(response.hasNextPage, response.nextPage)
|
||||
|
||||
// flatten so we have an easier structure to use for the table schema
|
||||
|
@ -120,7 +128,7 @@
|
|||
})
|
||||
await fetchBackups(filterOpt, page)
|
||||
notifications.success(response.message)
|
||||
} catch {
|
||||
} catch (err) {
|
||||
notifications.error("Unable to create backup")
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
import { sdk } from "@budibase/shared-core"
|
||||
</script>
|
||||
|
||||
{#if isEnabled(TENANT_FEATURE_FLAGS.LICENSING) && !$licensing.isEnterprisePlan}
|
||||
{#if isEnabled(TENANT_FEATURE_FLAGS.LICENSING) && !$licensing.isEnterprisePlan && !$licensing.isEnterpriseTrial}
|
||||
{#if $admin.cloud && $auth?.user?.accountPortalAccess}
|
||||
<Button
|
||||
cta
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<script>
|
||||
import { isActive, redirect, goto, url } from "@roxi/routify"
|
||||
import { Icon, notifications, Tabs, Tab } from "@budibase/bbui"
|
||||
import { organisation, auth, menu, appsStore } from "stores/portal"
|
||||
import { organisation, auth, menu, appsStore, licensing } from "stores/portal"
|
||||
import { onMount } from "svelte"
|
||||
import UpgradeButton from "./_components/UpgradeButton.svelte"
|
||||
import MobileMenu from "./_components/MobileMenu.svelte"
|
||||
|
@ -10,6 +10,8 @@
|
|||
import HelpMenu from "components/common/HelpMenu.svelte"
|
||||
import VerificationPromptBanner from "components/common/VerificationPromptBanner.svelte"
|
||||
import { sdk } from "@budibase/shared-core"
|
||||
import EnterpriseBasicTrialBanner from "components/portal/licensing/EnterpriseBasicTrialBanner.svelte"
|
||||
import { Constants } from "@budibase/frontend-core"
|
||||
|
||||
let loaded = false
|
||||
let mobileMenuVisible = false
|
||||
|
@ -33,6 +35,14 @@
|
|||
const showMobileMenu = () => (mobileMenuVisible = true)
|
||||
const hideMobileMenu = () => (mobileMenuVisible = false)
|
||||
|
||||
const showFreeTrialBanner = () => {
|
||||
return (
|
||||
$licensing.license?.plan?.type ===
|
||||
Constants.PlanType.ENTERPRISE_BASIC_TRIAL &&
|
||||
sdk.users.isAdmin($auth.user)
|
||||
)
|
||||
}
|
||||
|
||||
onMount(async () => {
|
||||
// Prevent non-builders from accessing the portal
|
||||
if ($auth.user) {
|
||||
|
@ -58,6 +68,7 @@
|
|||
<HelpMenu />
|
||||
<div class="container">
|
||||
<VerificationPromptBanner />
|
||||
<EnterpriseBasicTrialBanner show={showFreeTrialBanner()} />
|
||||
<div class="nav">
|
||||
<div class="branding">
|
||||
<Logo />
|
||||
|
|
|
@ -29,6 +29,7 @@
|
|||
const manageUrl = `${$admin.accountPortalUrl}/portal/billing`
|
||||
|
||||
const WARN_USAGE = ["Queries", "Automations", "Rows", "Day Passes", "Users"]
|
||||
const oneDayInSeconds = 86400
|
||||
|
||||
const EXCLUDE_QUOTAS = {
|
||||
Queries: () => true,
|
||||
|
@ -104,24 +105,17 @@
|
|||
if (!timestamp) {
|
||||
return
|
||||
}
|
||||
const now = new Date()
|
||||
now.setHours(0)
|
||||
now.setMinutes(0)
|
||||
|
||||
const thenDate = new Date(timestamp)
|
||||
thenDate.setHours(0)
|
||||
thenDate.setMinutes(0)
|
||||
|
||||
const difference = thenDate.getTime() - now
|
||||
// return the difference in days
|
||||
return (difference / (1000 * 3600 * 24)).toFixed(0)
|
||||
const diffTime = Math.abs(timestamp - new Date().getTime()) / 1000
|
||||
return Math.floor(diffTime / oneDayInSeconds)
|
||||
}
|
||||
|
||||
const setTextRows = () => {
|
||||
textRows = []
|
||||
|
||||
if (cancelAt && !usesInvoicing) {
|
||||
textRows.push({ message: "Subscription has been cancelled" })
|
||||
if (plan?.type !== Constants.PlanType.ENTERPRISE_BASIC_TRIAL) {
|
||||
textRows.push({ message: "Subscription has been cancelled" })
|
||||
}
|
||||
textRows.push({
|
||||
message: `${getDaysRemaining(cancelAt)} days remaining`,
|
||||
tooltip: new Date(cancelAt),
|
||||
|
|
|
@ -166,10 +166,16 @@ const automationActions = store => ({
|
|||
await store.actions.save(newAutomation)
|
||||
},
|
||||
test: async (automation, testData) => {
|
||||
const result = await API.testAutomation({
|
||||
automationId: automation?._id,
|
||||
testData,
|
||||
})
|
||||
let result
|
||||
try {
|
||||
result = await API.testAutomation({
|
||||
automationId: automation?._id,
|
||||
testData,
|
||||
})
|
||||
} catch (err) {
|
||||
const message = err.message || err.status || JSON.stringify(err)
|
||||
throw `Automation test failed - ${message}`
|
||||
}
|
||||
if (!result?.trigger && !result?.steps?.length) {
|
||||
if (result?.err?.code === "usage_limit_exceeded") {
|
||||
throw "You have exceeded your automation quota"
|
||||
|
|
|
@ -14,6 +14,7 @@ export const INITIAL_BUILDER_STATE = {
|
|||
tourKey: null,
|
||||
tourStepKey: null,
|
||||
hoveredComponentId: null,
|
||||
fonts: null,
|
||||
}
|
||||
|
||||
export class BuilderStore extends BudiStore {
|
||||
|
@ -36,6 +37,16 @@ export class BuilderStore extends BudiStore {
|
|||
this.websocket
|
||||
}
|
||||
|
||||
loadFonts(fontFaces) {
|
||||
const ff = fontFaces.map(
|
||||
fontFace => `${fontFace.family}-${fontFace.weight}`
|
||||
)
|
||||
this.update(state => ({
|
||||
...state,
|
||||
fonts: [...(state.fonts || []), ...ff],
|
||||
}))
|
||||
}
|
||||
|
||||
init(app) {
|
||||
if (!app?.appId) {
|
||||
console.error("BuilderStore: No appId supplied for websocket")
|
||||
|
|
|
@ -440,6 +440,8 @@ export class ComponentStore extends BudiStore {
|
|||
return state
|
||||
})
|
||||
|
||||
componentTreeNodesStore.makeNodeVisible(componentInstance._id)
|
||||
|
||||
// Log event
|
||||
analytics.captureEvent(Events.COMPONENT_CREATED, {
|
||||
name: componentInstance._component,
|
||||
|
|
|
@ -103,6 +103,8 @@ export const createLicensingStore = () => {
|
|||
const isEnterprisePlan = planType === Constants.PlanType.ENTERPRISE
|
||||
const isFreePlan = planType === Constants.PlanType.FREE
|
||||
const isBusinessPlan = planType === Constants.PlanType.BUSINESS
|
||||
const isEnterpriseTrial =
|
||||
planType === Constants.PlanType.ENTERPRISE_BASIC_TRIAL
|
||||
const groupsEnabled = license.features.includes(
|
||||
Constants.Features.USER_GROUPS
|
||||
)
|
||||
|
@ -143,6 +145,7 @@ export const createLicensingStore = () => {
|
|||
isEnterprisePlan,
|
||||
isFreePlan,
|
||||
isBusinessPlan,
|
||||
isEnterpriseTrial,
|
||||
groupsEnabled,
|
||||
backupsEnabled,
|
||||
brandingEnabled,
|
||||
|
|
|
@ -4226,8 +4226,8 @@
|
|||
]
|
||||
},
|
||||
"attachmentfield": {
|
||||
"name": "Attachment list",
|
||||
"icon": "Attach",
|
||||
"name": "Attachment List",
|
||||
"icon": "DocumentFragmentGroup",
|
||||
"styles": ["size"],
|
||||
"requiredAncestors": ["form"],
|
||||
"editable": true,
|
||||
|
@ -4324,7 +4324,7 @@
|
|||
},
|
||||
"attachmentsinglefield": {
|
||||
"name": "Single Attachment",
|
||||
"icon": "Attach",
|
||||
"icon": "DocumentFragment",
|
||||
"styles": ["size"],
|
||||
"requiredAncestors": ["form"],
|
||||
"editable": true,
|
||||
|
@ -7023,16 +7023,28 @@
|
|||
]
|
||||
}
|
||||
],
|
||||
"context": {
|
||||
"type": "schema",
|
||||
"scope": "local"
|
||||
},
|
||||
"context": [
|
||||
{
|
||||
"type": "schema",
|
||||
"scope": "local"
|
||||
},
|
||||
{
|
||||
"type": "static",
|
||||
"values": [
|
||||
{
|
||||
"label": "Selected rows",
|
||||
"key": "selectedRows",
|
||||
"type": "array"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"actions": ["RefreshDatasource"]
|
||||
},
|
||||
"bbreferencefield": {
|
||||
"devComment": "As bb reference is only used for user subtype for now, we are using user for icon and labels",
|
||||
"name": "User Field",
|
||||
"icon": "User",
|
||||
"name": "User List Field",
|
||||
"icon": "UserGroup",
|
||||
"styles": ["size"],
|
||||
"requiredAncestors": ["form"],
|
||||
"editable": true,
|
||||
|
@ -7136,5 +7148,113 @@
|
|||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"bbreferencesinglefield": {
|
||||
"devComment": "As bb reference is only used for user subtype for now, we are using user for icon and labels",
|
||||
"name": "User Field",
|
||||
"icon": "User",
|
||||
"styles": ["size"],
|
||||
"requiredAncestors": ["form"],
|
||||
"editable": true,
|
||||
"size": {
|
||||
"width": 400,
|
||||
"height": 50
|
||||
},
|
||||
"settings": [
|
||||
{
|
||||
"type": "field/bb_reference_single",
|
||||
"label": "Field",
|
||||
"key": "field",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"label": "Label",
|
||||
"key": "label"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"label": "Placeholder",
|
||||
"key": "placeholder"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"label": "Default value",
|
||||
"key": "defaultValue"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"label": "Help text",
|
||||
"key": "helpText"
|
||||
},
|
||||
{
|
||||
"type": "event",
|
||||
"label": "On change",
|
||||
"key": "onChange",
|
||||
"context": [
|
||||
{
|
||||
"label": "Field Value",
|
||||
"key": "value"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "validation/link",
|
||||
"label": "Validation",
|
||||
"key": "validation"
|
||||
},
|
||||
{
|
||||
"type": "boolean",
|
||||
"label": "Search",
|
||||
"key": "autocomplete",
|
||||
"defaultValue": true
|
||||
},
|
||||
{
|
||||
"type": "boolean",
|
||||
"label": "Disabled",
|
||||
"key": "disabled",
|
||||
"defaultValue": false
|
||||
},
|
||||
{
|
||||
"type": "boolean",
|
||||
"label": "Read only",
|
||||
"key": "readonly",
|
||||
"defaultValue": false,
|
||||
"dependsOn": {
|
||||
"setting": "disabled",
|
||||
"value": true,
|
||||
"invert": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "select",
|
||||
"label": "Layout",
|
||||
"key": "span",
|
||||
"defaultValue": 6,
|
||||
"hidden": true,
|
||||
"showInBar": true,
|
||||
"barStyle": "buttons",
|
||||
"options": [
|
||||
{
|
||||
"label": "1 column",
|
||||
"value": 6,
|
||||
"barIcon": "Stop",
|
||||
"barTitle": "1 column"
|
||||
},
|
||||
{
|
||||
"label": "2 columns",
|
||||
"value": 3,
|
||||
"barIcon": "ColumnTwoA",
|
||||
"barTitle": "2 columns"
|
||||
},
|
||||
{
|
||||
"label": "3 columns",
|
||||
"value": 2,
|
||||
"barIcon": "ViewColumn",
|
||||
"barTitle": "3 columns"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
<script>
|
||||
// NOTE: this is not a block - it's just named as such to avoid confusing users,
|
||||
// because it functions similarly to one
|
||||
import { getContext } from "svelte"
|
||||
import { get } from "svelte/store"
|
||||
import { getContext, onMount } from "svelte"
|
||||
import { get, derived, readable } from "svelte/store"
|
||||
import { Grid } from "@budibase/frontend-core"
|
||||
|
||||
// table is actually any datasource, but called table for legacy compatibility
|
||||
|
@ -19,7 +19,6 @@
|
|||
export let columns = null
|
||||
export let onRowClick = null
|
||||
export let buttons = null
|
||||
export let repeat = null
|
||||
|
||||
const context = getContext("context")
|
||||
const component = getContext("component")
|
||||
|
@ -36,23 +35,24 @@
|
|||
} = getContext("sdk")
|
||||
|
||||
let grid
|
||||
let gridContext
|
||||
let resizedColumns = {}
|
||||
|
||||
$: columnWhitelist = parsedColumns
|
||||
?.filter(col => col.active)
|
||||
?.map(col => col.field)
|
||||
$: enrichedButtons = enrichButtons(buttons)
|
||||
$: parsedColumns = getParsedColumns(columns)
|
||||
$: columnWhitelist = parsedColumns.filter(x => x.active).map(x => x.field)
|
||||
$: schemaOverrides = getSchemaOverrides(parsedColumns, resizedColumns)
|
||||
$: enrichedButtons = enrichButtons(buttons)
|
||||
$: selectedRows = deriveSelectedRows(gridContext)
|
||||
$: height = $component.styles?.normal?.height || "408px"
|
||||
$: styles = getSanitisedStyles($component.styles)
|
||||
$: data = { selectedRows: $selectedRows }
|
||||
$: actions = [
|
||||
{
|
||||
type: ActionTypes.RefreshDatasource,
|
||||
callback: () => grid?.getContext()?.rows.actions.refreshData(),
|
||||
callback: () => gridContext?.rows.actions.refreshData(),
|
||||
metadata: { dataSource: table },
|
||||
},
|
||||
]
|
||||
$: height = $component.styles?.normal?.height || "408px"
|
||||
$: styles = getSanitisedStyles($component.styles)
|
||||
|
||||
// Provide additional data context for live binding eval
|
||||
export const getAdditionalDataContext = () => {
|
||||
|
@ -69,11 +69,14 @@
|
|||
|
||||
// Parses columns to fix older formats
|
||||
const getParsedColumns = columns => {
|
||||
if (!columns?.length) {
|
||||
return []
|
||||
}
|
||||
// If the first element has an active key all elements should be in the new format
|
||||
if (columns?.length && columns[0]?.active !== undefined) {
|
||||
if (columns[0].active !== undefined) {
|
||||
return columns
|
||||
}
|
||||
return columns?.map(column => ({
|
||||
return columns.map(column => ({
|
||||
label: column.displayName || column.name,
|
||||
field: column.name,
|
||||
active: true,
|
||||
|
@ -82,7 +85,7 @@
|
|||
|
||||
const getSchemaOverrides = (columns, resizedColumns) => {
|
||||
let overrides = {}
|
||||
columns?.forEach(column => {
|
||||
columns.forEach(column => {
|
||||
overrides[column.field] = {
|
||||
displayName: column.label,
|
||||
}
|
||||
|
@ -115,6 +118,23 @@
|
|||
}))
|
||||
}
|
||||
|
||||
const deriveSelectedRows = gridContext => {
|
||||
if (!gridContext) {
|
||||
return readable([])
|
||||
}
|
||||
return derived(
|
||||
[gridContext.selectedRows, gridContext.rowLookupMap, gridContext.rows],
|
||||
([$selectedRows, $rowLookupMap, $rows]) => {
|
||||
return Object.entries($selectedRows || {})
|
||||
.filter(([_, selected]) => selected)
|
||||
.map(([rowId]) => {
|
||||
const idx = $rowLookupMap[rowId]
|
||||
return gridContext.rows.actions.cleanRow($rows[idx])
|
||||
})
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
const getSanitisedStyles = styles => {
|
||||
return {
|
||||
...styles,
|
||||
|
@ -131,41 +151,45 @@
|
|||
const { column } = e.detail
|
||||
resizedColumns = { ...resizedColumns, [column]: true }
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
gridContext = grid.getContext()
|
||||
})
|
||||
</script>
|
||||
|
||||
<div use:styleable={styles} class:in-builder={$builderStore.inBuilder}>
|
||||
<span style="--height:{height};">
|
||||
<Provider {actions}>
|
||||
<Grid
|
||||
bind:this={grid}
|
||||
datasource={table}
|
||||
{API}
|
||||
{stripeRows}
|
||||
{quiet}
|
||||
{initialFilter}
|
||||
{initialSortColumn}
|
||||
{initialSortOrder}
|
||||
{fixedRowHeight}
|
||||
{columnWhitelist}
|
||||
{schemaOverrides}
|
||||
{repeat}
|
||||
canAddRows={allowAddRows}
|
||||
canEditRows={allowEditRows}
|
||||
canDeleteRows={allowDeleteRows}
|
||||
canEditColumns={false}
|
||||
canExpandRows={false}
|
||||
canSaveSchema={false}
|
||||
showControls={false}
|
||||
notifySuccess={notificationStore.actions.success}
|
||||
notifyError={notificationStore.actions.error}
|
||||
buttons={enrichedButtons}
|
||||
on:rowclick={e => onRowClick?.({ row: e.detail })}
|
||||
on:columnresize={onColumnResize}
|
||||
/>
|
||||
</Provider>
|
||||
<Grid
|
||||
bind:this={grid}
|
||||
datasource={table}
|
||||
{API}
|
||||
{stripeRows}
|
||||
{quiet}
|
||||
{initialFilter}
|
||||
{initialSortColumn}
|
||||
{initialSortOrder}
|
||||
{fixedRowHeight}
|
||||
{columnWhitelist}
|
||||
{schemaOverrides}
|
||||
canAddRows={allowAddRows}
|
||||
canEditRows={allowEditRows}
|
||||
canDeleteRows={allowDeleteRows}
|
||||
canEditColumns={false}
|
||||
canExpandRows={false}
|
||||
canSaveSchema={false}
|
||||
canSelectRows={true}
|
||||
showControls={false}
|
||||
notifySuccess={notificationStore.actions.success}
|
||||
notifyError={notificationStore.actions.error}
|
||||
buttons={enrichedButtons}
|
||||
on:rowclick={e => onRowClick?.({ row: e.detail })}
|
||||
on:columnresize={onColumnResize}
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<Provider {data} {actions} />
|
||||
|
||||
<style>
|
||||
div {
|
||||
display: flex;
|
||||
|
|
|
@ -21,6 +21,7 @@
|
|||
[FieldType.JSON]: "jsonfield",
|
||||
[FieldType.BARCODEQR]: "codescanner",
|
||||
[FieldType.BB_REFERENCE]: "bbreferencefield",
|
||||
[FieldType.BB_REFERENCE_SINGLE]: "bbreferencesinglefield",
|
||||
}
|
||||
|
||||
const getFieldSchema = field => {
|
||||
|
|
|
@ -1,8 +1,10 @@
|
|||
<script>
|
||||
import RelationshipField from "./RelationshipField.svelte"
|
||||
import { sdk } from "@budibase/shared-core"
|
||||
import { FieldType } from "@budibase/types"
|
||||
import RelationshipField from "./RelationshipField.svelte"
|
||||
|
||||
export let defaultValue
|
||||
export let type = FieldType.BB_REFERENCE
|
||||
|
||||
function updateUserIDs(value) {
|
||||
if (Array.isArray(value)) {
|
||||
|
@ -22,6 +24,7 @@
|
|||
|
||||
<RelationshipField
|
||||
{...$$props}
|
||||
{type}
|
||||
datasourceType={"user"}
|
||||
primaryDisplay={"email"}
|
||||
defaultValue={updateReferences(defaultValue)}
|
||||
|
|
|
@ -0,0 +1,6 @@
|
|||
<script>
|
||||
import { FieldType } from "@budibase/types"
|
||||
import BBReferenceField from "./BBReferenceField.svelte"
|
||||
</script>
|
||||
|
||||
<BBReferenceField {...$$restProps} type={FieldType.BB_REFERENCE_SINGLE} />
|
|
@ -1,9 +1,9 @@
|
|||
<script>
|
||||
import { CoreSelect, CoreMultiselect } from "@budibase/bbui"
|
||||
import { FieldType } from "@budibase/types"
|
||||
import { fetchData, Utils } from "@budibase/frontend-core"
|
||||
import { getContext, onMount } from "svelte"
|
||||
import { getContext } from "svelte"
|
||||
import Field from "./Field.svelte"
|
||||
import { FieldTypes } from "../../../constants"
|
||||
|
||||
const { API } = getContext("sdk")
|
||||
|
||||
|
@ -21,6 +21,7 @@
|
|||
export let primaryDisplay
|
||||
export let span
|
||||
export let helpText = null
|
||||
export let type = FieldType.LINK
|
||||
|
||||
let fieldState
|
||||
let fieldApi
|
||||
|
@ -28,12 +29,10 @@
|
|||
let tableDefinition
|
||||
let searchTerm
|
||||
let open
|
||||
let initialValue
|
||||
|
||||
$: type =
|
||||
datasourceType === "table" ? FieldTypes.LINK : FieldTypes.BB_REFERENCE
|
||||
|
||||
$: multiselect = fieldSchema?.relationshipType !== "one-to-many"
|
||||
$: multiselect =
|
||||
[FieldType.LINK, FieldType.BB_REFERENCE].includes(type) &&
|
||||
fieldSchema?.relationshipType !== "one-to-many"
|
||||
$: linkedTableId = fieldSchema?.tableId
|
||||
$: fetch = fetchData({
|
||||
API,
|
||||
|
@ -52,18 +51,19 @@
|
|||
? flatten(fieldState?.value) ?? []
|
||||
: flatten(fieldState?.value)?.[0]
|
||||
$: component = multiselect ? CoreMultiselect : CoreSelect
|
||||
$: expandedDefaultValue = expand(defaultValue)
|
||||
$: primaryDisplay = primaryDisplay || tableDefinition?.primaryDisplay
|
||||
|
||||
let optionsObj = {}
|
||||
let initialValuesProcessed
|
||||
let optionsObj
|
||||
|
||||
$: {
|
||||
if (!initialValuesProcessed && primaryDisplay) {
|
||||
if (primaryDisplay && fieldState && !optionsObj) {
|
||||
// Persist the initial values as options, allowing them to be present in the dropdown,
|
||||
// even if they are not in the inital fetch results
|
||||
initialValuesProcessed = true
|
||||
optionsObj = (fieldState?.value || []).reduce((accumulator, value) => {
|
||||
let valueAsSafeArray = fieldState.value || []
|
||||
if (!Array.isArray(valueAsSafeArray)) {
|
||||
valueAsSafeArray = [fieldState.value]
|
||||
}
|
||||
optionsObj = valueAsSafeArray.reduce((accumulator, value) => {
|
||||
// fieldState has to be an array of strings to be valid for an update
|
||||
// therefore we cannot guarantee value will be an object
|
||||
// https://linear.app/budibase/issue/BUDI-7577/refactor-the-relationshipfield-component-to-have-better-support-for
|
||||
|
@ -75,7 +75,7 @@
|
|||
[primaryDisplay]: value.primaryDisplay,
|
||||
}
|
||||
return accumulator
|
||||
}, optionsObj)
|
||||
}, {})
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -86,7 +86,7 @@
|
|||
accumulator[row._id] = row
|
||||
}
|
||||
return accumulator
|
||||
}, optionsObj)
|
||||
}, optionsObj || {})
|
||||
|
||||
return Object.values(result)
|
||||
}
|
||||
|
@ -110,17 +110,10 @@
|
|||
}
|
||||
|
||||
$: forceFetchRows(filter)
|
||||
$: debouncedFetchRows(
|
||||
searchTerm,
|
||||
primaryDisplay,
|
||||
initialValue || defaultValue
|
||||
)
|
||||
$: debouncedFetchRows(searchTerm, primaryDisplay, defaultValue)
|
||||
|
||||
const forceFetchRows = async () => {
|
||||
// if the filter has changed, then we need to reset the options, clear the selection, and re-fetch
|
||||
optionsObj = {}
|
||||
fieldApi?.setValue([])
|
||||
selectedValue = []
|
||||
debouncedFetchRows(searchTerm, primaryDisplay, defaultValue)
|
||||
}
|
||||
const fetchRows = async (searchTerm, primaryDisplay, defaultVal) => {
|
||||
|
@ -136,7 +129,7 @@
|
|||
if (defaultVal && !Array.isArray(defaultVal)) {
|
||||
defaultVal = defaultVal.split(",")
|
||||
}
|
||||
if (defaultVal && defaultVal.some(val => !optionsObj[val])) {
|
||||
if (defaultVal && optionsObj && defaultVal.some(val => !optionsObj[val])) {
|
||||
await fetch.update({
|
||||
query: { oneOf: { _id: defaultVal } },
|
||||
})
|
||||
|
@ -162,16 +155,13 @@
|
|||
if (!values) {
|
||||
return []
|
||||
}
|
||||
|
||||
if (!Array.isArray(values)) {
|
||||
values = [values]
|
||||
}
|
||||
values = values.map(value =>
|
||||
typeof value === "object" ? value._id : value
|
||||
)
|
||||
// Make sure field state is valid
|
||||
if (values?.length > 0) {
|
||||
fieldApi.setValue(values)
|
||||
}
|
||||
return values
|
||||
}
|
||||
|
||||
|
@ -179,25 +169,20 @@
|
|||
return row?.[primaryDisplay] || "-"
|
||||
}
|
||||
|
||||
const singleHandler = e => {
|
||||
handleChange(e.detail == null ? [] : [e.detail])
|
||||
}
|
||||
|
||||
const multiHandler = e => {
|
||||
handleChange(e.detail)
|
||||
}
|
||||
|
||||
const expand = values => {
|
||||
if (!values) {
|
||||
return []
|
||||
const handleChange = e => {
|
||||
let value = e.detail
|
||||
if (!multiselect) {
|
||||
value = value == null ? [] : [value]
|
||||
}
|
||||
|
||||
if (
|
||||
type === FieldType.BB_REFERENCE_SINGLE &&
|
||||
value &&
|
||||
Array.isArray(value)
|
||||
) {
|
||||
value = value[0] || null
|
||||
}
|
||||
if (Array.isArray(values)) {
|
||||
return values
|
||||
}
|
||||
return values.split(",").map(value => value.trim())
|
||||
}
|
||||
|
||||
const handleChange = value => {
|
||||
const changed = fieldApi.setValue(value)
|
||||
if (onChange && changed) {
|
||||
onChange({
|
||||
|
@ -211,16 +196,6 @@
|
|||
fetch.nextPage()
|
||||
}
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
// if the form is in 'Update' mode, then we need to fetch the matching row so that the value is correctly set
|
||||
if (fieldState?.value) {
|
||||
initialValue =
|
||||
fieldSchema?.relationshipType !== "one-to-many"
|
||||
? flatten(fieldState?.value) ?? []
|
||||
: flatten(fieldState?.value)?.[0]
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<Field
|
||||
|
@ -229,7 +204,7 @@
|
|||
{disabled}
|
||||
{readonly}
|
||||
{validation}
|
||||
defaultValue={expandedDefaultValue}
|
||||
{defaultValue}
|
||||
{type}
|
||||
{span}
|
||||
{helpText}
|
||||
|
@ -243,7 +218,7 @@
|
|||
options={enrichedOptions}
|
||||
{autocomplete}
|
||||
value={selectedValue}
|
||||
on:change={multiselect ? multiHandler : singleHandler}
|
||||
on:change={handleChange}
|
||||
on:loadMore={loadMore}
|
||||
id={fieldState.fieldId}
|
||||
disabled={fieldState.disabled}
|
||||
|
|
|
@ -17,3 +17,4 @@ export { default as jsonfield } from "./JSONField.svelte"
|
|||
export { default as s3upload } from "./S3Upload.svelte"
|
||||
export { default as codescanner } from "./CodeScannerField.svelte"
|
||||
export { default as bbreferencefield } from "./BBReferenceField.svelte"
|
||||
export { default as bbreferencesinglefield } from "./BBReferenceSingleField.svelte"
|
||||
|
|
|
@ -15,7 +15,7 @@ const createRowSelectionStore = () => {
|
|||
const componentId = Object.keys(selection).find(
|
||||
componentId => componentId === tableComponentId
|
||||
)
|
||||
return selection[componentId] || {}
|
||||
return selection[componentId]
|
||||
}
|
||||
|
||||
return {
|
||||
|
|
|
@ -333,31 +333,59 @@ const s3UploadHandler = async action => {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* For new configs, "rows" is defined and enriched to be the array of rows to
|
||||
* export. For old configs it will be undefined and we need to use the legacy
|
||||
* row selection store in combination with the tableComponentId parameter.
|
||||
*/
|
||||
const exportDataHandler = async action => {
|
||||
let selection = rowSelectionStore.actions.getSelection(
|
||||
action.parameters.tableComponentId
|
||||
)
|
||||
if (selection.selectedRows && selection.selectedRows.length > 0) {
|
||||
let { tableComponentId, rows, type, columns, delimiter, customHeaders } =
|
||||
action.parameters
|
||||
let tableId
|
||||
|
||||
// Handle legacy configs using the row selection store
|
||||
if (!rows?.length) {
|
||||
const selection = rowSelectionStore.actions.getSelection(tableComponentId)
|
||||
if (selection?.selectedRows?.length) {
|
||||
rows = selection.selectedRows
|
||||
tableId = selection.tableId
|
||||
}
|
||||
}
|
||||
|
||||
// Get table ID from first row if needed
|
||||
if (!tableId) {
|
||||
tableId = rows?.[0]?.tableId
|
||||
}
|
||||
|
||||
// Handle no rows selected
|
||||
if (!rows?.length) {
|
||||
notificationStore.actions.error("Please select at least one row")
|
||||
}
|
||||
// Handle case where we're not using a DS+
|
||||
else if (!tableId) {
|
||||
notificationStore.actions.error(
|
||||
"You can only export data from table datasources"
|
||||
)
|
||||
}
|
||||
// Happy path when we have both rows and table ID
|
||||
else {
|
||||
try {
|
||||
// Flatten rows if required
|
||||
if (typeof rows[0] !== "string") {
|
||||
rows = rows.map(row => row._id)
|
||||
}
|
||||
const data = await API.exportRows({
|
||||
tableId: selection.tableId,
|
||||
rows: selection.selectedRows,
|
||||
format: action.parameters.type,
|
||||
columns: action.parameters.columns?.map(
|
||||
column => column.name || column
|
||||
),
|
||||
delimiter: action.parameters.delimiter,
|
||||
customHeaders: action.parameters.customHeaders,
|
||||
tableId,
|
||||
rows,
|
||||
format: type,
|
||||
columns: columns?.map(column => column.name || column),
|
||||
delimiter,
|
||||
customHeaders,
|
||||
})
|
||||
download(
|
||||
new Blob([data], { type: "text/plain" }),
|
||||
`${selection.tableId}.${action.parameters.type}`
|
||||
)
|
||||
download(new Blob([data], { type: "text/plain" }), `${tableId}.${type}`)
|
||||
} catch (error) {
|
||||
notificationStore.actions.error("There was an error exporting the data")
|
||||
}
|
||||
} else {
|
||||
notificationStore.actions.error("Please select at least one row")
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -125,6 +125,7 @@
|
|||
filter.type = fieldSchema?.type
|
||||
filter.subtype = fieldSchema?.subtype
|
||||
filter.formulaType = fieldSchema?.formulaType
|
||||
filter.constraints = fieldSchema?.constraints
|
||||
|
||||
// Update external type based on field
|
||||
filter.externalType = getSchema(filter)?.externalType
|
||||
|
@ -281,7 +282,7 @@
|
|||
timeOnly={getSchema(filter)?.timeOnly}
|
||||
bind:value={filter.value}
|
||||
/>
|
||||
{:else if filter.type === FieldType.BB_REFERENCE}
|
||||
{:else if [FieldType.BB_REFERENCE, FieldType.BB_REFERENCE_SINGLE].includes(filter.type)}
|
||||
<FilterUsers
|
||||
bind:value={filter.value}
|
||||
multiselect={[
|
||||
|
@ -289,6 +290,7 @@
|
|||
OperatorOptions.ContainsAny.value,
|
||||
].includes(filter.operator)}
|
||||
disabled={filter.noValue}
|
||||
type={filter.valueType}
|
||||
/>
|
||||
{:else}
|
||||
<Input disabled />
|
||||
|
@ -325,8 +327,6 @@
|
|||
<style>
|
||||
.container {
|
||||
width: 100%;
|
||||
max-width: 1000px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
.fields {
|
||||
display: grid;
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
import { createAPIClient } from "../api"
|
||||
|
||||
export let API = createAPIClient()
|
||||
|
||||
export let value = null
|
||||
export let disabled
|
||||
export let multiselect = false
|
||||
|
@ -23,12 +24,14 @@
|
|||
$: component = multiselect ? Multiselect : Select
|
||||
</script>
|
||||
|
||||
<svelte:component
|
||||
this={component}
|
||||
bind:value
|
||||
autocomplete
|
||||
{options}
|
||||
getOptionLabel={option => option.email}
|
||||
getOptionValue={option => option._id}
|
||||
{disabled}
|
||||
/>
|
||||
<div class="user-control">
|
||||
<svelte:component
|
||||
this={component}
|
||||
bind:value
|
||||
autocomplete
|
||||
{options}
|
||||
getOptionLabel={option => option.email}
|
||||
getOptionValue={option => option._id}
|
||||
{disabled}
|
||||
/>
|
||||
</div>
|
||||
|
|
|
@ -1,19 +1,27 @@
|
|||
<script>
|
||||
import { getContext } from "svelte"
|
||||
import { helpers } from "@budibase/shared-core"
|
||||
import RelationshipCell from "./RelationshipCell.svelte"
|
||||
import { BBReferenceFieldSubType, RelationshipType } from "@budibase/types"
|
||||
import {
|
||||
BBReferenceFieldSubType,
|
||||
FieldType,
|
||||
RelationshipType,
|
||||
} from "@budibase/types"
|
||||
|
||||
export let api
|
||||
export let hideCounter = false
|
||||
export let schema
|
||||
|
||||
const { API } = getContext("grid")
|
||||
const { subtype } = $$props.schema
|
||||
const { type, subtype } = schema
|
||||
|
||||
const schema = {
|
||||
$: schema = {
|
||||
...$$props.schema,
|
||||
// This is not really used, just adding some content to be able to render the relationship cell
|
||||
tableId: "external",
|
||||
relationshipType:
|
||||
subtype === BBReferenceFieldSubType.USER
|
||||
type === FieldType.BB_REFERENCE_SINGLE ||
|
||||
helpers.schema.isDeprecatedSingleUserColumn(schema)
|
||||
? RelationshipType.ONE_TO_MANY
|
||||
: RelationshipType.MANY_TO_MANY,
|
||||
}
|
||||
|
@ -44,8 +52,9 @@
|
|||
|
||||
<RelationshipCell
|
||||
bind:api
|
||||
{...$$props}
|
||||
{...$$restProps}
|
||||
{schema}
|
||||
{searchFunction}
|
||||
primaryDisplay={"email"}
|
||||
{hideCounter}
|
||||
/>
|
||||
|
|
|
@ -0,0 +1,22 @@
|
|||
<script>
|
||||
import BbReferenceCell from "./BBReferenceCell.svelte"
|
||||
|
||||
export let value
|
||||
export let onChange
|
||||
export let api
|
||||
|
||||
$: arrayValue = (!Array.isArray(value) && value ? [value] : value) || []
|
||||
|
||||
$: onValueChange = value => {
|
||||
value = value[0] || null
|
||||
onChange(value)
|
||||
}
|
||||
</script>
|
||||
|
||||
<BbReferenceCell
|
||||
bind:api
|
||||
{...$$restProps}
|
||||
value={arrayValue}
|
||||
onChange={onValueChange}
|
||||
hideCounter={true}
|
||||
/>
|
|
@ -16,6 +16,8 @@
|
|||
const { config, dispatch, selectedRows } = getContext("grid")
|
||||
const svelteDispatch = createEventDispatcher()
|
||||
|
||||
$: selectionEnabled = $config.canSelectRows || $config.canDeleteRows
|
||||
|
||||
const select = e => {
|
||||
e.stopPropagation()
|
||||
svelteDispatch("select")
|
||||
|
@ -52,7 +54,7 @@
|
|||
<div
|
||||
on:click={select}
|
||||
class="checkbox"
|
||||
class:visible={$config.canDeleteRows &&
|
||||
class:visible={selectionEnabled &&
|
||||
(disableNumber || rowSelected || rowHovered || rowFocused)}
|
||||
>
|
||||
<Checkbox value={rowSelected} {disabled} />
|
||||
|
@ -60,7 +62,7 @@
|
|||
{#if !disableNumber}
|
||||
<div
|
||||
class="number"
|
||||
class:visible={!$config.canDeleteRows ||
|
||||
class:visible={!selectionEnabled ||
|
||||
!(rowSelected || rowHovered || rowFocused)}
|
||||
>
|
||||
{row.__idx + 1}
|
||||
|
@ -117,19 +119,11 @@
|
|||
.expand {
|
||||
margin-right: 4px;
|
||||
}
|
||||
.expand {
|
||||
.expand:not(.visible),
|
||||
.expand:not(.visible) :global(*) {
|
||||
opacity: 0;
|
||||
pointer-events: none !important;
|
||||
}
|
||||
.expand :global(.spectrum-Icon) {
|
||||
pointer-events: none;
|
||||
}
|
||||
.expand.visible {
|
||||
opacity: 1;
|
||||
}
|
||||
.expand.visible :global(.spectrum-Icon) {
|
||||
pointer-events: all;
|
||||
}
|
||||
|
||||
.delete:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
|
|
@ -17,6 +17,7 @@
|
|||
export let contentLines = 1
|
||||
export let searchFunction = API.searchTable
|
||||
export let primaryDisplay
|
||||
export let hideCounter = false
|
||||
|
||||
const color = getColor(0)
|
||||
|
||||
|
@ -263,7 +264,7 @@
|
|||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{#if value?.length}
|
||||
{#if !hideCounter && value?.length}
|
||||
<div class="count">
|
||||
{value?.length || 0}
|
||||
</div>
|
||||
|
|
|
@ -7,11 +7,6 @@
|
|||
} from "@budibase/bbui"
|
||||
import { getContext } from "svelte"
|
||||
import { ValidColumnNameRegex } from "@budibase/shared-core"
|
||||
import {
|
||||
BBReferenceFieldSubType,
|
||||
FieldType,
|
||||
RelationshipType,
|
||||
} from "@budibase/types"
|
||||
|
||||
const { API, definition, rows } = getContext("grid")
|
||||
|
||||
|
@ -33,20 +28,11 @@
|
|||
}
|
||||
|
||||
const migrateUserColumn = async () => {
|
||||
let subtype = BBReferenceFieldSubType.USERS
|
||||
if (column.schema.relationshipType === RelationshipType.ONE_TO_MANY) {
|
||||
subtype = BBReferenceFieldSubType.USER
|
||||
}
|
||||
|
||||
try {
|
||||
await API.migrateColumn({
|
||||
tableId: $definition._id,
|
||||
oldColumn: column.schema,
|
||||
newColumn: {
|
||||
name: newColumnName,
|
||||
type: FieldType.BB_REFERENCE,
|
||||
subtype,
|
||||
},
|
||||
oldColumn: column.schema.name,
|
||||
newColumn: newColumnName,
|
||||
})
|
||||
notifications.success("Column migrated")
|
||||
} catch (e) {
|
||||
|
|
|
@ -41,6 +41,7 @@
|
|||
export let canDeleteRows = true
|
||||
export let canEditColumns = true
|
||||
export let canSaveSchema = true
|
||||
export let canSelectRows = false
|
||||
export let stripeRows = false
|
||||
export let quiet = false
|
||||
export let collaboration = true
|
||||
|
@ -94,6 +95,7 @@
|
|||
canDeleteRows,
|
||||
canEditColumns,
|
||||
canSaveSchema,
|
||||
canSelectRows,
|
||||
stripeRows,
|
||||
quiet,
|
||||
collaboration,
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
<script>
|
||||
import { getContext } from "svelte"
|
||||
import DataCell from "../cells/DataCell.svelte"
|
||||
import { getCellID } from "../lib/utils"
|
||||
|
||||
export let row
|
||||
export let top = false
|
||||
|
@ -38,7 +39,7 @@
|
|||
on:click={() => dispatch("rowclick", rows.actions.cleanRow(row))}
|
||||
>
|
||||
{#each $visibleColumns as column, columnIdx}
|
||||
{@const cellId = `${row._id}-${column.name}`}
|
||||
{@const cellId = getCellID(row._id, column.name)}
|
||||
<DataCell
|
||||
{cellId}
|
||||
{column}
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
import { GutterWidth, NewRowID } from "../lib/constants"
|
||||
import GutterCell from "../cells/GutterCell.svelte"
|
||||
import KeyboardShortcut from "./KeyboardShortcut.svelte"
|
||||
import { getCellID } from "../lib/utils"
|
||||
|
||||
const {
|
||||
hoveredRowId,
|
||||
|
@ -70,7 +71,7 @@
|
|||
|
||||
// Select the first cell if possible
|
||||
if (firstColumn) {
|
||||
$focusedCellId = `${savedRow._id}-${firstColumn.name}`
|
||||
$focusedCellId = getCellID(savedRow._id, firstColumn.name)
|
||||
}
|
||||
}
|
||||
isAdding = false
|
||||
|
@ -118,7 +119,7 @@
|
|||
visible = true
|
||||
$hoveredRowId = NewRowID
|
||||
if (firstColumn) {
|
||||
$focusedCellId = `${NewRowID}-${firstColumn.name}`
|
||||
$focusedCellId = getCellID(NewRowID, firstColumn.name)
|
||||
}
|
||||
|
||||
// Attach key listener
|
||||
|
@ -194,7 +195,7 @@
|
|||
{/if}
|
||||
</GutterCell>
|
||||
{#if $stickyColumn}
|
||||
{@const cellId = `${NewRowID}-${$stickyColumn.name}`}
|
||||
{@const cellId = getCellID(NewRowID, $stickyColumn.name)}
|
||||
<DataCell
|
||||
{cellId}
|
||||
rowFocused
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
import { GutterWidth, BlankRowID } from "../lib/constants"
|
||||
import GutterCell from "../cells/GutterCell.svelte"
|
||||
import KeyboardShortcut from "./KeyboardShortcut.svelte"
|
||||
import { getCellID } from "../lib/utils"
|
||||
|
||||
const {
|
||||
rows,
|
||||
|
@ -71,7 +72,7 @@
|
|||
{@const rowSelected = !!$selectedRows[row._id]}
|
||||
{@const rowHovered = $hoveredRowId === row._id}
|
||||
{@const rowFocused = $focusedRow?._id === row._id}
|
||||
{@const cellId = `${row._id}-${$stickyColumn?.name}`}
|
||||
{@const cellId = getCellID(row._id, $stickyColumn?.name)}
|
||||
<div
|
||||
class="row"
|
||||
on:mouseenter={$isDragging ? null : () => ($hoveredRowId = row._id)}
|
||||
|
|
|
@ -13,6 +13,7 @@ import JSONCell from "../cells/JSONCell.svelte"
|
|||
import AttachmentCell from "../cells/AttachmentCell.svelte"
|
||||
import AttachmentSingleCell from "../cells/AttachmentSingleCell.svelte"
|
||||
import BBReferenceCell from "../cells/BBReferenceCell.svelte"
|
||||
import BBReferenceSingleCell from "../cells/BBReferenceSingleCell.svelte"
|
||||
|
||||
const TypeComponentMap = {
|
||||
[FieldType.STRING]: TextCell,
|
||||
|
@ -29,6 +30,7 @@ const TypeComponentMap = {
|
|||
[FieldType.FORMULA]: FormulaCell,
|
||||
[FieldType.JSON]: JSONCell,
|
||||
[FieldType.BB_REFERENCE]: BBReferenceCell,
|
||||
[FieldType.BB_REFERENCE_SINGLE]: BBReferenceSingleCell,
|
||||
}
|
||||
export const getCellRenderer = column => {
|
||||
return TypeComponentMap[column?.schema?.type] || TextCell
|
||||
|
|
|
@ -1,5 +1,23 @@
|
|||
import { helpers } from "@budibase/shared-core"
|
||||
import { TypeIconMap } from "../../../constants"
|
||||
|
||||
// we can't use "-" for joining the ID/field, as this can be present in the ID or column name
|
||||
// using something very unusual to avoid this problem
|
||||
const JOINING_CHARACTER = "‽‽"
|
||||
|
||||
export const parseCellID = cellId => {
|
||||
if (!cellId) {
|
||||
return { id: undefined, field: undefined }
|
||||
}
|
||||
const parts = cellId.split(JOINING_CHARACTER)
|
||||
const field = parts.pop()
|
||||
return { id: parts.join(JOINING_CHARACTER), field }
|
||||
}
|
||||
|
||||
export const getCellID = (rowId, fieldName) => {
|
||||
return `${rowId}${JOINING_CHARACTER}${fieldName}`
|
||||
}
|
||||
|
||||
export const getColor = (idx, opacity = 0.3) => {
|
||||
if (idx == null || idx === -1) {
|
||||
idx = 0
|
||||
|
@ -11,8 +29,12 @@ export const getColumnIcon = column => {
|
|||
if (column.schema.autocolumn) {
|
||||
return "MagicWand"
|
||||
}
|
||||
const { type, subtype } = column.schema
|
||||
|
||||
if (helpers.schema.isDeprecatedSingleUserColumn(column.schema)) {
|
||||
return "User"
|
||||
}
|
||||
|
||||
const { type, subtype } = column.schema
|
||||
const result =
|
||||
typeof TypeIconMap[type] === "object" && subtype
|
||||
? TypeIconMap[type][subtype]
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
import { getContext, onMount } from "svelte"
|
||||
import { debounce } from "../../../utils/utils"
|
||||
import { NewRowID } from "../lib/constants"
|
||||
import { getCellID, parseCellID } from "../lib/utils"
|
||||
|
||||
const {
|
||||
rows,
|
||||
|
@ -154,7 +155,7 @@
|
|||
if (!firstColumn) {
|
||||
return
|
||||
}
|
||||
focusedCellId.set(`${firstRow._id}-${firstColumn.name}`)
|
||||
focusedCellId.set(getCellID(firstRow._id, firstColumn.name))
|
||||
}
|
||||
|
||||
// Changes the focused cell by moving it left or right to a different column
|
||||
|
@ -163,8 +164,7 @@
|
|||
return
|
||||
}
|
||||
const cols = $visibleColumns
|
||||
const split = $focusedCellId.split("-")
|
||||
const columnName = split[1]
|
||||
const { id, field: columnName } = parseCellID($focusedCellId)
|
||||
let newColumnName
|
||||
if (columnName === $stickyColumn?.name) {
|
||||
const index = delta - 1
|
||||
|
@ -178,7 +178,7 @@
|
|||
}
|
||||
}
|
||||
if (newColumnName) {
|
||||
$focusedCellId = `${split[0]}-${newColumnName}`
|
||||
$focusedCellId = getCellID(id, newColumnName)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -189,8 +189,8 @@
|
|||
}
|
||||
const newRow = $rows[$focusedRow.__idx + delta]
|
||||
if (newRow) {
|
||||
const split = $focusedCellId.split("-")
|
||||
$focusedCellId = `${newRow._id}-${split[1]}`
|
||||
const { field } = parseCellID($focusedCellId)
|
||||
$focusedCellId = getCellID(newRow._id, field)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
import { getContext } from "svelte"
|
||||
import { NewRowID } from "../lib/constants"
|
||||
import GridPopover from "./GridPopover.svelte"
|
||||
import { getCellID } from "../lib/utils"
|
||||
|
||||
const {
|
||||
focusedRow,
|
||||
|
@ -41,7 +42,7 @@
|
|||
const newRow = await rows.actions.duplicateRow($focusedRow)
|
||||
if (newRow) {
|
||||
const column = $stickyColumn?.name || $columns[0].name
|
||||
$focusedCellId = `${newRow._id}-${column}`
|
||||
$focusedCellId = getCellID(newRow._id, column)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import { writable, derived, get } from "svelte/store"
|
||||
import { fetchData } from "../../../fetch"
|
||||
import { NewRowID, RowPageSize } from "../lib/constants"
|
||||
import { getCellID, parseCellID } from "../lib/utils"
|
||||
import { tick } from "svelte"
|
||||
import { Helpers } from "@budibase/bbui"
|
||||
|
||||
|
@ -206,7 +207,7 @@ export const createActions = context => {
|
|||
// If the server doesn't reply with a valid error, assume that the source
|
||||
// of the error is the focused cell's column
|
||||
if (!error?.json?.validationErrors && errorString) {
|
||||
const focusedColumn = get(focusedCellId)?.split("-")[1]
|
||||
const { field: focusedColumn } = parseCellID(get(focusedCellId))
|
||||
if (focusedColumn) {
|
||||
error = {
|
||||
json: {
|
||||
|
@ -245,7 +246,7 @@ export const createActions = context => {
|
|||
}
|
||||
// Set error against the cell
|
||||
validation.actions.setError(
|
||||
`${rowId}-${column}`,
|
||||
getCellID(rowId, column),
|
||||
Helpers.capitalise(err)
|
||||
)
|
||||
// Ensure the column is visible
|
||||
|
@ -265,7 +266,7 @@ export const createActions = context => {
|
|||
|
||||
// Focus the first cell with an error
|
||||
if (erroredColumns.length) {
|
||||
focusedCellId.set(`${rowId}-${erroredColumns[0]}`)
|
||||
focusedCellId.set(getCellID(rowId, erroredColumns[0]))
|
||||
}
|
||||
} else {
|
||||
get(notifications).error(errorString || "An unknown error occurred")
|
||||
|
@ -571,9 +572,10 @@ export const initialise = context => {
|
|||
return
|
||||
}
|
||||
// Stop if we changed row
|
||||
const oldRowId = id.split("-")[0]
|
||||
const oldColumn = id.split("-")[1]
|
||||
const newRowId = get(focusedCellId)?.split("-")[0]
|
||||
const split = parseCellID(id)
|
||||
const oldRowId = split.id
|
||||
const oldColumn = split.field
|
||||
const { id: newRowId } = parseCellID(get(focusedCellId))
|
||||
if (oldRowId !== newRowId) {
|
||||
return
|
||||
}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import { writable, derived, get } from "svelte/store"
|
||||
import { tick } from "svelte"
|
||||
import { Padding, GutterWidth, FocusedCellMinOffset } from "../lib/constants"
|
||||
import { parseCellID } from "../lib/utils"
|
||||
|
||||
export const createStores = () => {
|
||||
const scroll = writable({
|
||||
|
@ -176,7 +177,7 @@ export const initialise = context => {
|
|||
// Ensure horizontal position is viewable
|
||||
// Check horizontal position of columns next
|
||||
const $visibleColumns = get(visibleColumns)
|
||||
const columnName = $focusedCellId?.split("-")[1]
|
||||
const { field: columnName } = parseCellID($focusedCellId)
|
||||
const column = $visibleColumns.find(col => col.name === columnName)
|
||||
if (!column) {
|
||||
return
|
||||
|
|
|
@ -7,6 +7,7 @@ import {
|
|||
MediumRowHeight,
|
||||
NewRowID,
|
||||
} from "../lib/constants"
|
||||
import { parseCellID } from "../lib/utils"
|
||||
|
||||
export const createStores = context => {
|
||||
const { props } = context
|
||||
|
@ -25,7 +26,7 @@ export const createStores = context => {
|
|||
const focusedRowId = derived(
|
||||
focusedCellId,
|
||||
$focusedCellId => {
|
||||
return $focusedCellId?.split("-")[0]
|
||||
return parseCellID($focusedCellId)?.id
|
||||
},
|
||||
null
|
||||
)
|
||||
|
@ -72,7 +73,7 @@ export const deriveStores = context => {
|
|||
const focusedRow = derived(
|
||||
[focusedCellId, rowLookupMap, rows],
|
||||
([$focusedCellId, $rowLookupMap, $rows]) => {
|
||||
const rowId = $focusedCellId?.split("-")[0]
|
||||
const rowId = parseCellID($focusedCellId)?.id
|
||||
|
||||
// Edge case for new rows
|
||||
if (rowId === NewRowID) {
|
||||
|
@ -109,12 +110,11 @@ export const deriveStores = context => {
|
|||
}
|
||||
|
||||
export const createActions = context => {
|
||||
const { focusedCellId, selectedRows, hoveredRowId } = context
|
||||
const { focusedCellId, hoveredRowId } = context
|
||||
|
||||
// Callback when leaving the grid, deselecting all focussed or selected items
|
||||
const blur = () => {
|
||||
focusedCellId.set(null)
|
||||
selectedRows.set({})
|
||||
hoveredRowId.set(null)
|
||||
}
|
||||
|
||||
|
@ -152,7 +152,7 @@ export const initialise = context => {
|
|||
const hasRow = rows.actions.hasRow
|
||||
|
||||
// Check selected cell
|
||||
const selectedRowId = $focusedCellId?.split("-")[0]
|
||||
const selectedRowId = parseCellID($focusedCellId)?.id
|
||||
if (selectedRowId && !hasRow(selectedRowId)) {
|
||||
focusedCellId.set(null)
|
||||
}
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import { writable, get, derived } from "svelte/store"
|
||||
import { getCellID, parseCellID } from "../lib/utils"
|
||||
|
||||
// Normally we would break out actions into the explicit "createActions"
|
||||
// function, but for validation all these actions are pure so can go into
|
||||
|
@ -12,7 +13,7 @@ export const createStores = () => {
|
|||
Object.entries($validation).forEach(([key, error]) => {
|
||||
// Extract row ID from all errored cell IDs
|
||||
if (error) {
|
||||
map[key.split("-")[0]] = true
|
||||
map[parseCellID(key).id] = true
|
||||
}
|
||||
})
|
||||
return map
|
||||
|
@ -53,10 +54,10 @@ export const initialise = context => {
|
|||
const $stickyColumn = get(stickyColumn)
|
||||
validation.update(state => {
|
||||
$columns.forEach(column => {
|
||||
state[`${id}-${column.name}`] = null
|
||||
state[getCellID(id, column.name)] = null
|
||||
})
|
||||
if ($stickyColumn) {
|
||||
state[`${id}-${$stickyColumn.name}`] = null
|
||||
state[getCellID(id, stickyColumn.name)] = null
|
||||
}
|
||||
return state
|
||||
})
|
||||
|
|
|
@ -57,6 +57,7 @@ export const PlanType = {
|
|||
PRO: "pro",
|
||||
BUSINESS: "business",
|
||||
ENTERPRISE: "enterprise",
|
||||
ENTERPRISE_BASIC_TRIAL: "enterprise_basic_trial",
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -124,17 +125,18 @@ export const TypeIconMap = {
|
|||
[FieldType.ARRAY]: "Duplicate",
|
||||
[FieldType.NUMBER]: "123",
|
||||
[FieldType.BOOLEAN]: "Boolean",
|
||||
[FieldType.ATTACHMENTS]: "Attach",
|
||||
[FieldType.ATTACHMENT_SINGLE]: "Attach",
|
||||
[FieldType.ATTACHMENTS]: "DocumentFragmentGroup",
|
||||
[FieldType.ATTACHMENT_SINGLE]: "DocumentFragment",
|
||||
[FieldType.LINK]: "DataCorrelated",
|
||||
[FieldType.FORMULA]: "Calculator",
|
||||
[FieldType.JSON]: "Brackets",
|
||||
[FieldType.BIGINT]: "TagBold",
|
||||
[FieldType.AUTO]: "MagicWand",
|
||||
[FieldType.USER]: "User",
|
||||
[FieldType.USERS]: "UserGroup",
|
||||
[FieldType.BB_REFERENCE]: {
|
||||
[BBReferenceFieldSubType.USER]: "User",
|
||||
[BBReferenceFieldSubType.USER]: "UserGroup",
|
||||
[BBReferenceFieldSubType.USERS]: "UserGroup",
|
||||
},
|
||||
[FieldType.BB_REFERENCE_SINGLE]: {
|
||||
[BBReferenceFieldSubType.USER]: "User",
|
||||
},
|
||||
}
|
||||
|
|
|
@ -1 +1 @@
|
|||
Subproject commit 479879246aac5dd3073cc695945c62c41fae5b0e
|
||||
Subproject commit d3c3077011a8e20ed3c48dcd6301caca4120b6ac
|
|
@ -101,7 +101,6 @@
|
|||
"mysql2": "3.9.7",
|
||||
"node-fetch": "2.6.7",
|
||||
"object-sizeof": "2.6.1",
|
||||
"open": "8.4.0",
|
||||
"openai": "^3.2.1",
|
||||
"openapi-types": "9.3.1",
|
||||
"pg": "8.10.0",
|
||||
|
@ -113,12 +112,8 @@
|
|||
"server-destroy": "1.0.1",
|
||||
"snowflake-promise": "^4.5.0",
|
||||
"socket.io": "4.6.1",
|
||||
"sqlite3": "5.1.6",
|
||||
"swagger-parser": "10.0.3",
|
||||
"tar": "6.1.15",
|
||||
"to-json-schema": "0.2.5",
|
||||
"undici": "^6.0.1",
|
||||
"undici-types": "^6.0.1",
|
||||
"uuid": "^8.3.2",
|
||||
"validate.js": "0.13.1",
|
||||
"worker-farm": "1.7.0",
|
||||
|
@ -144,16 +139,13 @@
|
|||
"@types/supertest": "2.0.14",
|
||||
"@types/tar": "6.1.5",
|
||||
"@types/uuid": "8.3.4",
|
||||
"apidoc": "0.50.4",
|
||||
"copyfiles": "2.4.1",
|
||||
"docker-compose": "0.23.17",
|
||||
"jest": "29.7.0",
|
||||
"jest-openapi": "0.14.2",
|
||||
"jest-runner": "29.7.0",
|
||||
"nock": "13.5.4",
|
||||
"nodemon": "2.0.15",
|
||||
"openapi-typescript": "5.2.0",
|
||||
"path-to-regexp": "6.2.0",
|
||||
"rimraf": "3.0.2",
|
||||
"supertest": "6.3.3",
|
||||
"swagger-jsdoc": "6.1.0",
|
||||
|
|
|
@ -279,8 +279,7 @@ export async function trigger(ctx: UserCtx) {
|
|||
{
|
||||
fields: ctx.request.body.fields,
|
||||
timeout:
|
||||
ctx.request.body.timeout * 1000 ||
|
||||
env.getDefaults().AUTOMATION_SYNC_TIMEOUT,
|
||||
ctx.request.body.timeout * 1000 || env.AUTOMATION_THREAD_TIMEOUT,
|
||||
},
|
||||
{ getResponses: true }
|
||||
)
|
||||
|
|
|
@ -2,7 +2,7 @@ import stream from "stream"
|
|||
import archiver from "archiver"
|
||||
|
||||
import { quotas } from "@budibase/pro"
|
||||
import { objectStore } from "@budibase/backend-core"
|
||||
import { objectStore, context } from "@budibase/backend-core"
|
||||
import * as internal from "./internal"
|
||||
import * as external from "./external"
|
||||
import { isExternalTableID } from "../../../integrations/utils"
|
||||
|
@ -198,8 +198,18 @@ export async function destroy(ctx: UserCtx<DeleteRowRequest>) {
|
|||
export async function search(ctx: Ctx<SearchRowRequest, SearchRowResponse>) {
|
||||
const tableId = utils.getTableId(ctx)
|
||||
|
||||
await context.ensureSnippetContext(true)
|
||||
|
||||
const enrichedQuery = await utils.enrichSearchContext(
|
||||
{ ...ctx.request.body.query },
|
||||
{
|
||||
user: sdk.users.getUserContextBindings(ctx.user),
|
||||
}
|
||||
)
|
||||
|
||||
const searchParams: RowSearchParams = {
|
||||
...ctx.request.body,
|
||||
query: enrichedQuery,
|
||||
tableId,
|
||||
}
|
||||
|
||||
|
|
|
@ -1,20 +1,11 @@
|
|||
import { getRowParams } from "../../../db/utils"
|
||||
import {
|
||||
outputProcessing,
|
||||
processAutoColumn,
|
||||
processFormulas,
|
||||
} from "../../../utilities/rowProcessor"
|
||||
import { context, locks } from "@budibase/backend-core"
|
||||
import {
|
||||
Table,
|
||||
Row,
|
||||
LockType,
|
||||
LockName,
|
||||
FormulaType,
|
||||
FieldType,
|
||||
} from "@budibase/types"
|
||||
import { context } from "@budibase/backend-core"
|
||||
import { Table, Row, FormulaType, FieldType } from "@budibase/types"
|
||||
import * as linkRows from "../../../db/linkedRows"
|
||||
import sdk from "../../../sdk"
|
||||
import isEqual from "lodash/isEqual"
|
||||
import { cloneDeep } from "lodash/fp"
|
||||
|
||||
|
@ -151,30 +142,7 @@ export async function finaliseRow(
|
|||
// if another row has been written since processing this will
|
||||
// handle the auto ID clash
|
||||
if (oldTable && !isEqual(oldTable, table)) {
|
||||
try {
|
||||
await db.put(table)
|
||||
} catch (err: any) {
|
||||
if (err.status === 409) {
|
||||
// Some conflicts with the autocolumns occurred, we need to refetch the table and recalculate
|
||||
await locks.doWithLock(
|
||||
{
|
||||
type: LockType.AUTO_EXTEND,
|
||||
name: LockName.PROCESS_AUTO_COLUMNS,
|
||||
resource: table._id,
|
||||
},
|
||||
async () => {
|
||||
const latestTable = await sdk.tables.getTable(table._id!)
|
||||
let response = processAutoColumn(null, latestTable, row, {
|
||||
reprocessing: true,
|
||||
})
|
||||
await db.put(response.table)
|
||||
row = response.row
|
||||
}
|
||||
)
|
||||
} else {
|
||||
throw err
|
||||
}
|
||||
}
|
||||
await db.put(table)
|
||||
}
|
||||
const response = await db.put(row)
|
||||
// for response, calculate the formulas for the enriched row
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
// need to handle table name + field or just field, depending on if relationships used
|
||||
import { FieldType, Row, Table } from "@budibase/types"
|
||||
import { helpers } from "@budibase/shared-core"
|
||||
import { generateRowIdField } from "../../../../integrations/utils"
|
||||
import { CONSTANT_INTERNAL_ROW_COLS } from "../../../../db/utils"
|
||||
|
||||
|
@ -73,12 +74,15 @@ export function basicProcessing({
|
|||
// filter the row down to what is actually the row (not joined)
|
||||
for (let field of Object.values(table.schema)) {
|
||||
const fieldName = field.name
|
||||
const value = extractFieldValue({
|
||||
let value = extractFieldValue({
|
||||
row,
|
||||
tableName: table.name,
|
||||
fieldName,
|
||||
isLinked,
|
||||
})
|
||||
if (value instanceof Buffer) {
|
||||
value = value.toString()
|
||||
}
|
||||
// all responses include "select col as table.col" so that overlaps are handled
|
||||
if (value != null) {
|
||||
thisRow[fieldName] = value
|
||||
|
@ -104,12 +108,17 @@ export function basicProcessing({
|
|||
|
||||
export function fixArrayTypes(row: Row, table: Table) {
|
||||
for (let [fieldName, schema] of Object.entries(table.schema)) {
|
||||
if (schema.type === FieldType.ARRAY && typeof row[fieldName] === "string") {
|
||||
if (
|
||||
[FieldType.ARRAY, FieldType.BB_REFERENCE].includes(schema.type) &&
|
||||
typeof row[fieldName] === "string"
|
||||
) {
|
||||
try {
|
||||
row[fieldName] = JSON.parse(row[fieldName])
|
||||
} catch (err) {
|
||||
// couldn't convert back to array, ignore
|
||||
delete row[fieldName]
|
||||
if (!helpers.schema.isDeprecatedSingleUserColumn(schema)) {
|
||||
// couldn't convert back to array, ignore
|
||||
delete row[fieldName]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -22,7 +22,7 @@ import {
|
|||
getInternalRowId,
|
||||
} from "./basic"
|
||||
import sdk from "../../../../sdk"
|
||||
|
||||
import { processStringSync } from "@budibase/string-templates"
|
||||
import validateJs from "validate.js"
|
||||
|
||||
validateJs.extend(validateJs.validators.datetime, {
|
||||
|
@ -117,6 +117,19 @@ export async function validate(
|
|||
})
|
||||
}
|
||||
|
||||
function fixBooleanFields({ row, table }: { row: Row; table: Table }) {
|
||||
for (let col of Object.values(table.schema)) {
|
||||
if (col.type === FieldType.BOOLEAN) {
|
||||
if (row[col.name] === 1) {
|
||||
row[col.name] = true
|
||||
} else if (row[col.name] === 0) {
|
||||
row[col.name] = false
|
||||
}
|
||||
}
|
||||
}
|
||||
return row
|
||||
}
|
||||
|
||||
export async function sqlOutputProcessing(
|
||||
rows: DatasourcePlusQueryResponse,
|
||||
table: Table,
|
||||
|
@ -161,7 +174,9 @@ export async function sqlOutputProcessing(
|
|||
if (thisRow._id == null) {
|
||||
throw new Error("Unable to generate row ID for SQL rows")
|
||||
}
|
||||
finalRows[thisRow._id] = thisRow
|
||||
|
||||
finalRows[thisRow._id] = fixBooleanFields({ row: thisRow, table })
|
||||
|
||||
// do this at end once its been added to the final rows
|
||||
finalRows = await updateRelationshipColumns(
|
||||
table,
|
||||
|
@ -189,3 +204,63 @@ export async function sqlOutputProcessing(
|
|||
export function isUserMetadataTable(tableId: string) {
|
||||
return tableId === InternalTables.USER_METADATA
|
||||
}
|
||||
|
||||
export async function enrichArrayContext(
|
||||
fields: any[],
|
||||
inputs = {},
|
||||
helpers = true
|
||||
): Promise<any[]> {
|
||||
const map: Record<string, any> = {}
|
||||
for (let index in fields) {
|
||||
map[index] = fields[index]
|
||||
}
|
||||
const output = await enrichSearchContext(map, inputs, helpers)
|
||||
const outputArray: any[] = []
|
||||
for (let [key, value] of Object.entries(output)) {
|
||||
outputArray[parseInt(key)] = value
|
||||
}
|
||||
return outputArray
|
||||
}
|
||||
|
||||
export async function enrichSearchContext(
|
||||
fields: Record<string, any>,
|
||||
inputs = {},
|
||||
helpers = true
|
||||
): Promise<Record<string, any>> {
|
||||
const enrichedQuery: Record<string, any> = {}
|
||||
if (!fields || !inputs) {
|
||||
return enrichedQuery
|
||||
}
|
||||
const parameters = { ...inputs }
|
||||
|
||||
if (Array.isArray(fields)) {
|
||||
return enrichArrayContext(fields, inputs, helpers)
|
||||
}
|
||||
|
||||
// enrich the fields with dynamic parameters
|
||||
for (let key of Object.keys(fields)) {
|
||||
if (fields[key] == null) {
|
||||
enrichedQuery[key] = null
|
||||
continue
|
||||
}
|
||||
if (typeof fields[key] === "object") {
|
||||
// enrich nested fields object
|
||||
enrichedQuery[key] = await enrichSearchContext(
|
||||
fields[key],
|
||||
parameters,
|
||||
helpers
|
||||
)
|
||||
} else if (typeof fields[key] === "string") {
|
||||
// enrich string value as normal
|
||||
enrichedQuery[key] = processStringSync(fields[key], parameters, {
|
||||
noEscaping: true,
|
||||
noHelpers: !helpers,
|
||||
escapeNewlines: true,
|
||||
})
|
||||
} else {
|
||||
enrichedQuery[key] = fields[key]
|
||||
}
|
||||
}
|
||||
|
||||
return enrichedQuery
|
||||
}
|
||||
|
|
|
@ -9,7 +9,8 @@ import {
|
|||
} from "@budibase/types"
|
||||
import { dataFilters } from "@budibase/shared-core"
|
||||
import sdk from "../../../sdk"
|
||||
import { db } from "@budibase/backend-core"
|
||||
import { db, context } from "@budibase/backend-core"
|
||||
import { enrichSearchContext } from "./utils"
|
||||
|
||||
export async function searchView(
|
||||
ctx: UserCtx<SearchViewRowRequest, SearchRowResponse>
|
||||
|
@ -56,10 +57,16 @@ export async function searchView(
|
|||
})
|
||||
}
|
||||
|
||||
await context.ensureSnippetContext(true)
|
||||
|
||||
const enrichedQuery = await enrichSearchContext(query, {
|
||||
user: sdk.users.getUserContextBindings(ctx.user),
|
||||
})
|
||||
|
||||
const searchOptions: RequiredKeys<SearchViewRowRequest> &
|
||||
RequiredKeys<Pick<RowSearchParams, "tableId" | "query" | "fields">> = {
|
||||
tableId: view.tableId,
|
||||
query,
|
||||
query: enrichedQuery,
|
||||
fields: viewFields,
|
||||
...getSortOptions(body, view),
|
||||
limit: body.limit,
|
||||
|
|
|
@ -180,5 +180,5 @@ export async function migrate(ctx: UserCtx<MigrateRequest, MigrateResponse>) {
|
|||
}
|
||||
|
||||
ctx.status = 200
|
||||
ctx.body = { message: `Column ${oldColumn.name} migrated.` }
|
||||
ctx.body = { message: `Column ${oldColumn} migrated.` }
|
||||
}
|
||||
|
|
|
@ -4,10 +4,12 @@ import { APIError } from "@budibase/types"
|
|||
describe("/api/applications/:appId/sync", () => {
|
||||
let config = setup.getConfig()
|
||||
|
||||
afterAll(setup.afterAll)
|
||||
beforeAll(async () => {
|
||||
await config.init()
|
||||
})
|
||||
afterAll(async () => {
|
||||
setup.afterAll()
|
||||
})
|
||||
|
||||
describe("/api/attachments/process", () => {
|
||||
it("should accept an image file upload", async () => {
|
||||
|
@ -18,7 +20,8 @@ describe("/api/applications/:appId/sync", () => {
|
|||
expect(resp.length).toBe(1)
|
||||
|
||||
let upload = resp[0]
|
||||
expect(upload.url.endsWith(".jpg")).toBe(true)
|
||||
|
||||
expect(upload.url.split("?")[0].endsWith(".jpg")).toBe(true)
|
||||
expect(upload.extension).toBe("jpg")
|
||||
expect(upload.size).toBe(1)
|
||||
expect(upload.name).toBe("1px.jpg")
|
||||
|
|
|
@ -1,16 +1,18 @@
|
|||
import { mocks } from "@budibase/backend-core/tests"
|
||||
import tk from "timekeeper"
|
||||
import * as setup from "./utilities"
|
||||
import { events } from "@budibase/backend-core"
|
||||
import sdk from "../../../sdk"
|
||||
import { checkBuilderEndpoint } from "./utilities/TestFunctions"
|
||||
import { mocks } from "@budibase/backend-core/tests"
|
||||
|
||||
mocks.licenses.useBackups()
|
||||
|
||||
describe("/backups", () => {
|
||||
let config = setup.getConfig()
|
||||
|
||||
afterAll(setup.afterAll)
|
||||
afterAll(async () => {
|
||||
setup.afterAll()
|
||||
})
|
||||
|
||||
beforeEach(async () => {
|
||||
tk.reset()
|
||||
|
|
|
@ -16,6 +16,7 @@ import {
|
|||
SourceName,
|
||||
Table,
|
||||
TableSchema,
|
||||
SupportedSqlTypes,
|
||||
} from "@budibase/types"
|
||||
import { DatabaseName, getDatasource } from "../../../integrations/tests/utils"
|
||||
import { tableForDatasource } from "../../../tests/utilities/structures"
|
||||
|
@ -261,20 +262,6 @@ describe("/datasources", () => {
|
|||
})
|
||||
)
|
||||
|
||||
type SupportedSqlTypes =
|
||||
| FieldType.STRING
|
||||
| FieldType.BARCODEQR
|
||||
| FieldType.LONGFORM
|
||||
| FieldType.OPTIONS
|
||||
| FieldType.DATETIME
|
||||
| FieldType.NUMBER
|
||||
| FieldType.BOOLEAN
|
||||
| FieldType.FORMULA
|
||||
| FieldType.BIGINT
|
||||
| FieldType.BB_REFERENCE
|
||||
| FieldType.LINK
|
||||
| FieldType.ARRAY
|
||||
|
||||
const fullSchema: {
|
||||
[type in SupportedSqlTypes]: FieldSchema & { type: type }
|
||||
} = {
|
||||
|
@ -337,7 +324,12 @@ describe("/datasources", () => {
|
|||
[FieldType.BB_REFERENCE]: {
|
||||
name: "bb_reference",
|
||||
type: FieldType.BB_REFERENCE,
|
||||
subtype: BBReferenceFieldSubType.USERS,
|
||||
subtype: BBReferenceFieldSubType.USER,
|
||||
},
|
||||
[FieldType.BB_REFERENCE_SINGLE]: {
|
||||
name: "bb_reference_single",
|
||||
type: FieldType.BB_REFERENCE_SINGLE,
|
||||
subtype: BBReferenceFieldSubType.USER,
|
||||
},
|
||||
}
|
||||
|
||||
|
|
|
@ -32,8 +32,6 @@ import * as uuid from "uuid"
|
|||
const timestamp = new Date("2023-01-26T11:48:57.597Z").toISOString()
|
||||
tk.freeze(timestamp)
|
||||
|
||||
jest.unmock("mssql")
|
||||
|
||||
describe.each([
|
||||
["internal", undefined],
|
||||
[DatabaseName.POSTGRES, getDatasource(DatabaseName.POSTGRES)],
|
||||
|
@ -131,7 +129,13 @@ describe.each([
|
|||
|
||||
const assertRowUsage = async (expected: number) => {
|
||||
const usage = await getRowUsage()
|
||||
expect(usage).toBe(expected)
|
||||
|
||||
// Because our quota tracking is not perfect, we allow a 10% margin of
|
||||
// error. This is to account for the fact that parallel writes can result
|
||||
// in some quota updates getting lost. We don't have any need to solve this
|
||||
// right now, so we just allow for some error.
|
||||
expect(usage).toBeGreaterThan(expected * 0.9)
|
||||
expect(usage).toBeLessThan(expected * 1.1)
|
||||
}
|
||||
|
||||
const defaultRowFields = isInternal
|
||||
|
@ -194,39 +198,99 @@ describe.each([
|
|||
await assertRowUsage(rowUsage)
|
||||
})
|
||||
|
||||
it("increment row autoId per create row request", async () => {
|
||||
const rowUsage = await getRowUsage()
|
||||
isInternal &&
|
||||
it("increment row autoId per create row request", async () => {
|
||||
const rowUsage = await getRowUsage()
|
||||
|
||||
const newTable = await config.api.table.save(
|
||||
saveTableRequest({
|
||||
schema: {
|
||||
"Row ID": {
|
||||
name: "Row ID",
|
||||
type: FieldType.NUMBER,
|
||||
subtype: AutoFieldSubType.AUTO_ID,
|
||||
icon: "ri-magic-line",
|
||||
autocolumn: true,
|
||||
constraints: {
|
||||
type: "number",
|
||||
presence: true,
|
||||
numericality: {
|
||||
greaterThanOrEqualTo: "",
|
||||
lessThanOrEqualTo: "",
|
||||
const newTable = await config.api.table.save(
|
||||
saveTableRequest({
|
||||
schema: {
|
||||
"Row ID": {
|
||||
name: "Row ID",
|
||||
type: FieldType.NUMBER,
|
||||
subtype: AutoFieldSubType.AUTO_ID,
|
||||
icon: "ri-magic-line",
|
||||
autocolumn: true,
|
||||
constraints: {
|
||||
type: "number",
|
||||
presence: true,
|
||||
numericality: {
|
||||
greaterThanOrEqualTo: "",
|
||||
lessThanOrEqualTo: "",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
)
|
||||
})
|
||||
)
|
||||
|
||||
let previousId = 0
|
||||
for (let i = 0; i < 10; i++) {
|
||||
const row = await config.api.row.save(newTable._id!, {})
|
||||
expect(row["Row ID"]).toBeGreaterThan(previousId)
|
||||
previousId = row["Row ID"]
|
||||
}
|
||||
await assertRowUsage(rowUsage + 10)
|
||||
})
|
||||
let previousId = 0
|
||||
for (let i = 0; i < 10; i++) {
|
||||
const row = await config.api.row.save(newTable._id!, {})
|
||||
expect(row["Row ID"]).toBeGreaterThan(previousId)
|
||||
previousId = row["Row ID"]
|
||||
}
|
||||
await assertRowUsage(rowUsage + 10)
|
||||
})
|
||||
|
||||
isInternal &&
|
||||
it("should increment auto ID correctly when creating rows in parallel", async () => {
|
||||
const table = await config.api.table.save(
|
||||
saveTableRequest({
|
||||
schema: {
|
||||
"Row ID": {
|
||||
name: "Row ID",
|
||||
type: FieldType.NUMBER,
|
||||
subtype: AutoFieldSubType.AUTO_ID,
|
||||
icon: "ri-magic-line",
|
||||
autocolumn: true,
|
||||
constraints: {
|
||||
type: "number",
|
||||
presence: true,
|
||||
numericality: {
|
||||
greaterThanOrEqualTo: "",
|
||||
lessThanOrEqualTo: "",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
)
|
||||
|
||||
const sequence = Array(50)
|
||||
.fill(0)
|
||||
.map((_, i) => i + 1)
|
||||
|
||||
// This block of code is simulating users creating auto ID rows at the
|
||||
// same time. It's expected that this operation will sometimes return
|
||||
// a document conflict error (409), but the idea is to retry in those
|
||||
// situations. The code below does this a large number of times with
|
||||
// small, random delays between them to try and get through the list
|
||||
// as quickly as possible.
|
||||
await Promise.all(
|
||||
sequence.map(async () => {
|
||||
const attempts = 20
|
||||
for (let attempt = 0; attempt < attempts; attempt++) {
|
||||
try {
|
||||
await config.api.row.save(table._id!, {})
|
||||
return
|
||||
} catch (e) {
|
||||
await new Promise(r => setTimeout(r, Math.random() * 15))
|
||||
}
|
||||
}
|
||||
throw new Error(`Failed to create row after ${attempts} attempts`)
|
||||
})
|
||||
)
|
||||
|
||||
const rows = await config.api.row.fetch(table._id!)
|
||||
expect(rows).toHaveLength(50)
|
||||
|
||||
// The main purpose of this test is to ensure that even under pressure,
|
||||
// we maintain data integrity. An auto ID column should hand out
|
||||
// monotonically increasing unique integers no matter what.
|
||||
const ids = rows.map(r => r["Row ID"])
|
||||
expect(ids).toEqual(expect.arrayContaining(sequence))
|
||||
})
|
||||
|
||||
isInternal &&
|
||||
it("row values are coerced", async () => {
|
||||
|
@ -856,7 +920,7 @@ describe.each([
|
|||
await config.withEnv({ SELF_HOSTED: "true" }, async () => {
|
||||
return context.doInAppContext(config.getAppId(), async () => {
|
||||
const enriched = await outputProcessing(table, [row])
|
||||
expect((enriched as Row[])[0].attachment.url).toBe(
|
||||
expect((enriched as Row[])[0].attachment.url.split("?")[0]).toBe(
|
||||
`/files/signed/prod-budi-app-assets/${config.getProdAppId()}/attachments/${attachmentId}`
|
||||
)
|
||||
})
|
||||
|
@ -889,7 +953,7 @@ describe.each([
|
|||
await config.withEnv({ SELF_HOSTED: "true" }, async () => {
|
||||
return context.doInAppContext(config.getAppId(), async () => {
|
||||
const enriched = await outputProcessing(table, [row])
|
||||
expect((enriched as Row[])[0].attachment[0].url).toBe(
|
||||
expect((enriched as Row[])[0].attachment[0].url.split("?")[0]).toBe(
|
||||
`/files/signed/prod-budi-app-assets/${config.getProdAppId()}/attachments/${attachmentId}`
|
||||
)
|
||||
})
|
||||
|
|
|
@ -1,11 +1,13 @@
|
|||
import { tableForDatasource } from "../../../tests/utilities/structures"
|
||||
import { DatabaseName, getDatasource } from "../../../integrations/tests/utils"
|
||||
import { db as dbCore } from "@budibase/backend-core"
|
||||
|
||||
import * as setup from "./utilities"
|
||||
import {
|
||||
AutoFieldSubType,
|
||||
Datasource,
|
||||
EmptyFilterOption,
|
||||
BBReferenceFieldSubType,
|
||||
FieldType,
|
||||
RowSearchParams,
|
||||
SearchFilters,
|
||||
|
@ -13,10 +15,14 @@ import {
|
|||
SortType,
|
||||
Table,
|
||||
TableSchema,
|
||||
User,
|
||||
} from "@budibase/types"
|
||||
import _ from "lodash"
|
||||
import tk from "timekeeper"
|
||||
import { encodeJSBinding } from "@budibase/string-templates"
|
||||
|
||||
jest.unmock("mssql")
|
||||
const serverTime = new Date("2024-05-06T00:00:00.000Z")
|
||||
tk.freeze(serverTime)
|
||||
|
||||
describe.each([
|
||||
["lucene", undefined],
|
||||
|
@ -35,11 +41,25 @@ describe.each([
|
|||
let datasource: Datasource | undefined
|
||||
let table: Table
|
||||
|
||||
const snippets = [
|
||||
{
|
||||
name: "WeeksAgo",
|
||||
code: `return function (weeks) {\n const currentTime = new Date(${Date.now()});\n currentTime.setDate(currentTime.getDate()-(7 * (weeks || 1)));\n return currentTime.toISOString();\n}`,
|
||||
},
|
||||
]
|
||||
|
||||
beforeAll(async () => {
|
||||
if (isSqs) {
|
||||
envCleanup = config.setEnv({ SQS_SEARCH_ENABLE: "true" })
|
||||
}
|
||||
await config.init()
|
||||
|
||||
if (config.app?.appId) {
|
||||
config.app = await config.api.application.update(config.app?.appId, {
|
||||
snippets,
|
||||
})
|
||||
}
|
||||
|
||||
if (dsProvider) {
|
||||
datasource = await config.createDatasource({
|
||||
datasource: await dsProvider,
|
||||
|
@ -67,6 +87,22 @@ describe.each([
|
|||
class SearchAssertion {
|
||||
constructor(private readonly query: RowSearchParams) {}
|
||||
|
||||
private findRow(expectedRow: any, foundRows: any[]) {
|
||||
const row = foundRows.find(foundRow => _.isMatch(foundRow, expectedRow))
|
||||
if (!row) {
|
||||
const fields = Object.keys(expectedRow)
|
||||
// To make the error message more readable, we only include the fields
|
||||
// that are present in the expected row.
|
||||
const searchedObjects = foundRows.map(row => _.pick(row, fields))
|
||||
throw new Error(
|
||||
`Failed to find row: ${JSON.stringify(
|
||||
expectedRow
|
||||
)} in ${JSON.stringify(searchedObjects)}`
|
||||
)
|
||||
}
|
||||
return row
|
||||
}
|
||||
|
||||
// Asserts that the query returns rows matching exactly the set of rows
|
||||
// passed in. The order of the rows matters. Rows returned in an order
|
||||
// different to the one passed in will cause the assertion to fail. Extra
|
||||
|
@ -82,9 +118,7 @@ describe.each([
|
|||
// eslint-disable-next-line jest/no-standalone-expect
|
||||
expect(foundRows).toEqual(
|
||||
expectedRows.map((expectedRow: any) =>
|
||||
expect.objectContaining(
|
||||
foundRows.find(foundRow => _.isMatch(foundRow, expectedRow))
|
||||
)
|
||||
expect.objectContaining(this.findRow(expectedRow, foundRows))
|
||||
)
|
||||
)
|
||||
}
|
||||
|
@ -104,9 +138,7 @@ describe.each([
|
|||
expect(foundRows).toEqual(
|
||||
expect.arrayContaining(
|
||||
expectedRows.map((expectedRow: any) =>
|
||||
expect.objectContaining(
|
||||
foundRows.find(foundRow => _.isMatch(foundRow, expectedRow))
|
||||
)
|
||||
expect.objectContaining(this.findRow(expectedRow, foundRows))
|
||||
)
|
||||
)
|
||||
)
|
||||
|
@ -125,9 +157,7 @@ describe.each([
|
|||
expect(foundRows).toEqual(
|
||||
expect.arrayContaining(
|
||||
expectedRows.map((expectedRow: any) =>
|
||||
expect.objectContaining(
|
||||
foundRows.find(foundRow => _.isMatch(foundRow, expectedRow))
|
||||
)
|
||||
expect.objectContaining(this.findRow(expectedRow, foundRows))
|
||||
)
|
||||
)
|
||||
)
|
||||
|
@ -156,7 +186,406 @@ describe.each([
|
|||
return expectSearch({ query })
|
||||
}
|
||||
|
||||
describe("strings", () => {
|
||||
describe("boolean", () => {
|
||||
beforeAll(async () => {
|
||||
await createTable({
|
||||
isTrue: { name: "isTrue", type: FieldType.BOOLEAN },
|
||||
})
|
||||
await createRows([{ isTrue: true }, { isTrue: false }])
|
||||
})
|
||||
|
||||
describe("equal", () => {
|
||||
it("successfully finds true row", () =>
|
||||
expectQuery({ equal: { isTrue: true } }).toMatchExactly([
|
||||
{ isTrue: true },
|
||||
]))
|
||||
|
||||
it("successfully finds false row", () =>
|
||||
expectQuery({ equal: { isTrue: false } }).toMatchExactly([
|
||||
{ isTrue: false },
|
||||
]))
|
||||
})
|
||||
|
||||
describe("notEqual", () => {
|
||||
it("successfully finds false row", () =>
|
||||
expectQuery({ notEqual: { isTrue: true } }).toContainExactly([
|
||||
{ isTrue: false },
|
||||
]))
|
||||
|
||||
it("successfully finds true row", () =>
|
||||
expectQuery({ notEqual: { isTrue: false } }).toContainExactly([
|
||||
{ isTrue: true },
|
||||
]))
|
||||
})
|
||||
|
||||
describe("oneOf", () => {
|
||||
it("successfully finds true row", () =>
|
||||
expectQuery({ oneOf: { isTrue: [true] } }).toContainExactly([
|
||||
{ isTrue: true },
|
||||
]))
|
||||
|
||||
it("successfully finds false row", () =>
|
||||
expectQuery({ oneOf: { isTrue: [false] } }).toContainExactly([
|
||||
{ isTrue: false },
|
||||
]))
|
||||
})
|
||||
|
||||
describe("sort", () => {
|
||||
it("sorts ascending", () =>
|
||||
expectSearch({
|
||||
query: {},
|
||||
sort: "isTrue",
|
||||
sortOrder: SortOrder.ASCENDING,
|
||||
}).toMatchExactly([{ isTrue: false }, { isTrue: true }]))
|
||||
|
||||
it("sorts descending", () =>
|
||||
expectSearch({
|
||||
query: {},
|
||||
sort: "isTrue",
|
||||
sortOrder: SortOrder.DESCENDING,
|
||||
}).toMatchExactly([{ isTrue: true }, { isTrue: false }]))
|
||||
})
|
||||
})
|
||||
|
||||
// Ensure all bindings resolve and perform as expected
|
||||
describe("bindings", () => {
|
||||
let globalUsers: any = []
|
||||
|
||||
const future = new Date(serverTime.getTime())
|
||||
future.setDate(future.getDate() + 30)
|
||||
|
||||
const rows = (currentUser: User) => {
|
||||
return [
|
||||
{ name: "foo", appointment: "1982-01-05T00:00:00.000Z" },
|
||||
{ name: "bar", appointment: "1995-05-06T00:00:00.000Z" },
|
||||
{ name: currentUser.firstName, appointment: future.toISOString() },
|
||||
{ name: "serverDate", appointment: serverTime.toISOString() },
|
||||
{
|
||||
name: "single user, session user",
|
||||
single_user: JSON.stringify(currentUser),
|
||||
},
|
||||
{
|
||||
name: "single user",
|
||||
single_user: JSON.stringify(globalUsers[0]),
|
||||
},
|
||||
{
|
||||
name: "deprecated single user, session user",
|
||||
deprecated_single_user: JSON.stringify([currentUser]),
|
||||
},
|
||||
{
|
||||
name: "deprecated single user",
|
||||
deprecated_single_user: JSON.stringify([globalUsers[0]]),
|
||||
},
|
||||
{
|
||||
name: "multi user",
|
||||
multi_user: JSON.stringify(globalUsers),
|
||||
},
|
||||
{
|
||||
name: "multi user with session user",
|
||||
multi_user: JSON.stringify([...globalUsers, currentUser]),
|
||||
},
|
||||
{
|
||||
name: "deprecated multi user",
|
||||
deprecated_multi_user: JSON.stringify(globalUsers),
|
||||
},
|
||||
{
|
||||
name: "deprecated multi user with session user",
|
||||
deprecated_multi_user: JSON.stringify([...globalUsers, currentUser]),
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
beforeAll(async () => {
|
||||
// Set up some global users
|
||||
globalUsers = await Promise.all(
|
||||
Array(2)
|
||||
.fill(0)
|
||||
.map(async () => {
|
||||
const globalUser = await config.globalUser()
|
||||
const userMedataId = globalUser._id
|
||||
? dbCore.generateUserMetadataID(globalUser._id)
|
||||
: null
|
||||
return {
|
||||
_id: globalUser._id,
|
||||
_meta: userMedataId,
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
await createTable({
|
||||
name: { name: "name", type: FieldType.STRING },
|
||||
appointment: { name: "appointment", type: FieldType.DATETIME },
|
||||
single_user: {
|
||||
name: "single_user",
|
||||
type: FieldType.BB_REFERENCE_SINGLE,
|
||||
subtype: BBReferenceFieldSubType.USER,
|
||||
},
|
||||
deprecated_single_user: {
|
||||
name: "deprecated_single_user",
|
||||
type: FieldType.BB_REFERENCE,
|
||||
subtype: BBReferenceFieldSubType.USER,
|
||||
},
|
||||
multi_user: {
|
||||
name: "multi_user",
|
||||
type: FieldType.BB_REFERENCE,
|
||||
subtype: BBReferenceFieldSubType.USER,
|
||||
constraints: {
|
||||
type: "array",
|
||||
},
|
||||
},
|
||||
deprecated_multi_user: {
|
||||
name: "deprecated_multi_user",
|
||||
type: FieldType.BB_REFERENCE,
|
||||
subtype: BBReferenceFieldSubType.USERS,
|
||||
constraints: {
|
||||
type: "array",
|
||||
},
|
||||
},
|
||||
})
|
||||
await createRows(rows(config.getUser()))
|
||||
})
|
||||
|
||||
// !! Current User is auto generated per run
|
||||
it("should return all rows matching the session user firstname", async () => {
|
||||
await expectQuery({
|
||||
equal: { name: "{{ [user].firstName }}" },
|
||||
}).toContainExactly([
|
||||
{
|
||||
name: config.getUser().firstName,
|
||||
appointment: future.toISOString(),
|
||||
},
|
||||
])
|
||||
})
|
||||
|
||||
it("should parse the date binding and return all rows after the resolved value", async () => {
|
||||
await expectQuery({
|
||||
range: {
|
||||
appointment: {
|
||||
low: "{{ [now] }}",
|
||||
high: "9999-00-00T00:00:00.000Z",
|
||||
},
|
||||
},
|
||||
}).toContainExactly([
|
||||
{
|
||||
name: config.getUser().firstName,
|
||||
appointment: future.toISOString(),
|
||||
},
|
||||
{ name: "serverDate", appointment: serverTime.toISOString() },
|
||||
])
|
||||
})
|
||||
|
||||
it("should parse the date binding and return all rows before the resolved value", async () => {
|
||||
await expectQuery({
|
||||
range: {
|
||||
appointment: {
|
||||
low: "0000-00-00T00:00:00.000Z",
|
||||
high: "{{ [now] }}",
|
||||
},
|
||||
},
|
||||
}).toContainExactly([
|
||||
{ name: "foo", appointment: "1982-01-05T00:00:00.000Z" },
|
||||
{ name: "bar", appointment: "1995-05-06T00:00:00.000Z" },
|
||||
{ name: "serverDate", appointment: serverTime.toISOString() },
|
||||
])
|
||||
})
|
||||
|
||||
it("should parse the encoded js snippet. Return rows with appointments up to 1 week in the past", async () => {
|
||||
const jsBinding = "return snippets.WeeksAgo();"
|
||||
const encodedBinding = encodeJSBinding(jsBinding)
|
||||
|
||||
await expectQuery({
|
||||
range: {
|
||||
appointment: {
|
||||
low: "0000-00-00T00:00:00.000Z",
|
||||
high: encodedBinding,
|
||||
},
|
||||
},
|
||||
}).toContainExactly([
|
||||
{ name: "foo", appointment: "1982-01-05T00:00:00.000Z" },
|
||||
{ name: "bar", appointment: "1995-05-06T00:00:00.000Z" },
|
||||
])
|
||||
})
|
||||
|
||||
it("should parse the encoded js binding. Return rows with appointments 2 weeks in the past", async () => {
|
||||
const jsBinding =
|
||||
"const currentTime = new Date()\ncurrentTime.setDate(currentTime.getDate()-14);\nreturn currentTime.toISOString();"
|
||||
const encodedBinding = encodeJSBinding(jsBinding)
|
||||
|
||||
await expectQuery({
|
||||
range: {
|
||||
appointment: {
|
||||
low: "0000-00-00T00:00:00.000Z",
|
||||
high: encodedBinding,
|
||||
},
|
||||
},
|
||||
}).toContainExactly([
|
||||
{ name: "foo", appointment: "1982-01-05T00:00:00.000Z" },
|
||||
{ name: "bar", appointment: "1995-05-06T00:00:00.000Z" },
|
||||
])
|
||||
})
|
||||
|
||||
it("should match a single user row by the session user id", async () => {
|
||||
await expectQuery({
|
||||
equal: { single_user: "{{ [user]._id }}" },
|
||||
}).toContainExactly([
|
||||
{
|
||||
name: "single user, session user",
|
||||
single_user: { _id: config.getUser()._id },
|
||||
},
|
||||
])
|
||||
})
|
||||
|
||||
it("should match a deprecated single user row by the session user id", async () => {
|
||||
await expectQuery({
|
||||
equal: { deprecated_single_user: "{{ [user]._id }}" },
|
||||
}).toContainExactly([
|
||||
{
|
||||
name: "deprecated single user, session user",
|
||||
deprecated_single_user: [{ _id: config.getUser()._id }],
|
||||
},
|
||||
])
|
||||
})
|
||||
|
||||
// TODO(samwho): fix for SQS
|
||||
!isSqs &&
|
||||
it("should match the session user id in a multi user field", async () => {
|
||||
const allUsers = [...globalUsers, config.getUser()].map((user: any) => {
|
||||
return { _id: user._id }
|
||||
})
|
||||
|
||||
await expectQuery({
|
||||
contains: { multi_user: ["{{ [user]._id }}"] },
|
||||
}).toContainExactly([
|
||||
{
|
||||
name: "multi user with session user",
|
||||
multi_user: allUsers,
|
||||
},
|
||||
])
|
||||
})
|
||||
|
||||
// TODO(samwho): fix for SQS
|
||||
!isSqs &&
|
||||
it("should match the session user id in a deprecated multi user field", async () => {
|
||||
const allUsers = [...globalUsers, config.getUser()].map((user: any) => {
|
||||
return { _id: user._id }
|
||||
})
|
||||
|
||||
await expectQuery({
|
||||
contains: { deprecated_multi_user: ["{{ [user]._id }}"] },
|
||||
}).toContainExactly([
|
||||
{
|
||||
name: "deprecated multi user with session user",
|
||||
deprecated_multi_user: allUsers,
|
||||
},
|
||||
])
|
||||
})
|
||||
|
||||
// TODO(samwho): fix for SQS
|
||||
!isSqs &&
|
||||
it("should not match the session user id in a multi user field", async () => {
|
||||
await expectQuery({
|
||||
notContains: { multi_user: ["{{ [user]._id }}"] },
|
||||
notEmpty: { multi_user: true },
|
||||
}).toContainExactly([
|
||||
{
|
||||
name: "multi user",
|
||||
multi_user: globalUsers.map((user: any) => {
|
||||
return { _id: user._id }
|
||||
}),
|
||||
},
|
||||
])
|
||||
})
|
||||
|
||||
// TODO(samwho): fix for SQS
|
||||
!isSqs &&
|
||||
it("should not match the session user id in a deprecated multi user field", async () => {
|
||||
await expectQuery({
|
||||
notContains: { deprecated_multi_user: ["{{ [user]._id }}"] },
|
||||
notEmpty: { deprecated_multi_user: true },
|
||||
}).toContainExactly([
|
||||
{
|
||||
name: "deprecated multi user",
|
||||
deprecated_multi_user: globalUsers.map((user: any) => {
|
||||
return { _id: user._id }
|
||||
}),
|
||||
},
|
||||
])
|
||||
})
|
||||
|
||||
it("should match the session user id and a user table row id using helpers, user binding and a static user id.", async () => {
|
||||
await expectQuery({
|
||||
oneOf: {
|
||||
single_user: [
|
||||
"{{ default [user]._id '_empty_' }}",
|
||||
globalUsers[0]._id,
|
||||
],
|
||||
},
|
||||
}).toContainExactly([
|
||||
{
|
||||
name: "single user, session user",
|
||||
single_user: { _id: config.getUser()._id },
|
||||
},
|
||||
{
|
||||
name: "single user",
|
||||
single_user: { _id: globalUsers[0]._id },
|
||||
},
|
||||
])
|
||||
})
|
||||
|
||||
it("should match the session user id and a user table row id using helpers, user binding and a static user id. (deprecated single user)", async () => {
|
||||
await expectQuery({
|
||||
oneOf: {
|
||||
deprecated_single_user: [
|
||||
"{{ default [user]._id '_empty_' }}",
|
||||
globalUsers[0]._id,
|
||||
],
|
||||
},
|
||||
}).toContainExactly([
|
||||
{
|
||||
name: "deprecated single user, session user",
|
||||
deprecated_single_user: [{ _id: config.getUser()._id }],
|
||||
},
|
||||
{
|
||||
name: "deprecated single user",
|
||||
deprecated_single_user: [{ _id: globalUsers[0]._id }],
|
||||
},
|
||||
])
|
||||
})
|
||||
|
||||
it("should resolve 'default' helper to '_empty_' when binding resolves to nothing", async () => {
|
||||
await expectQuery({
|
||||
oneOf: {
|
||||
single_user: [
|
||||
"{{ default [user]._idx '_empty_' }}",
|
||||
globalUsers[0]._id,
|
||||
],
|
||||
},
|
||||
}).toContainExactly([
|
||||
{
|
||||
name: "single user",
|
||||
single_user: { _id: globalUsers[0]._id },
|
||||
},
|
||||
])
|
||||
})
|
||||
|
||||
it("should resolve 'default' helper to '_empty_' when binding resolves to nothing (deprecated single user)", async () => {
|
||||
await expectQuery({
|
||||
oneOf: {
|
||||
deprecated_single_user: [
|
||||
"{{ default [user]._idx '_empty_' }}",
|
||||
globalUsers[0]._id,
|
||||
],
|
||||
},
|
||||
}).toContainExactly([
|
||||
{
|
||||
name: "deprecated single user",
|
||||
deprecated_single_user: [{ _id: globalUsers[0]._id }],
|
||||
},
|
||||
])
|
||||
})
|
||||
})
|
||||
|
||||
describe.each([FieldType.STRING, FieldType.LONGFORM])("%s", () => {
|
||||
beforeAll(async () => {
|
||||
await createTable({
|
||||
name: { name: "name", type: FieldType.STRING },
|
||||
|
@ -252,6 +681,31 @@ describe.each([
|
|||
}).toFindNothing())
|
||||
})
|
||||
|
||||
describe("empty", () => {
|
||||
it("finds no empty rows", () =>
|
||||
expectQuery({ empty: { name: null } }).toFindNothing())
|
||||
|
||||
it("should not be affected by when filter empty behaviour", () =>
|
||||
expectQuery({
|
||||
empty: { name: null },
|
||||
onEmptyFilter: EmptyFilterOption.RETURN_ALL,
|
||||
}).toFindNothing())
|
||||
})
|
||||
|
||||
describe("notEmpty", () => {
|
||||
it("finds all non-empty rows", () =>
|
||||
expectQuery({ notEmpty: { name: null } }).toContainExactly([
|
||||
{ name: "foo" },
|
||||
{ name: "bar" },
|
||||
]))
|
||||
|
||||
it("should not be affected by when filter empty behaviour", () =>
|
||||
expectQuery({
|
||||
notEmpty: { name: null },
|
||||
onEmptyFilter: EmptyFilterOption.RETURN_NONE,
|
||||
}).toContainExactly([{ name: "foo" }, { name: "bar" }]))
|
||||
})
|
||||
|
||||
describe("sort", () => {
|
||||
it("sorts ascending", () =>
|
||||
expectSearch({
|
||||
|
@ -508,7 +962,7 @@ describe.each([
|
|||
})
|
||||
})
|
||||
|
||||
describe("array of strings", () => {
|
||||
describe.each([FieldType.ARRAY, FieldType.OPTIONS])("%s", () => {
|
||||
beforeAll(async () => {
|
||||
await createTable({
|
||||
numbers: {
|
||||
|
|
|
@ -1,3 +1,13 @@
|
|||
// Directly mock the AWS SDK
|
||||
jest.mock("aws-sdk", () => ({
|
||||
S3: jest.fn(() => ({
|
||||
getSignedUrl: jest.fn(
|
||||
(operation, params) => `http://example.com/${params.Bucket}/${params.Key}`
|
||||
),
|
||||
upload: jest.fn(() => ({ Contents: {} })),
|
||||
})),
|
||||
}))
|
||||
|
||||
const setup = require("./utilities")
|
||||
const { constants } = require("@budibase/backend-core")
|
||||
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue