Merge remote-tracking branch 'origin/master' into feature/updated-app-metadata-ux
This commit is contained in:
commit
fcb5d88eaf
|
@ -54,7 +54,8 @@
|
|||
"ignoreRestSiblings": true
|
||||
}
|
||||
],
|
||||
"local-rules/no-budibase-imports": "error"
|
||||
"no-redeclare": "off",
|
||||
"@typescript-eslint/no-redeclare": "error"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
|
|
@ -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.25.0",
|
||||
"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)
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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",
|
||||
}
|
||||
|
|
|
@ -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 },
|
||||
}
|
||||
}
|
||||
})
|
||||
|
|
|
@ -105,6 +105,10 @@
|
|||
}
|
||||
|
||||
onMount(async () => {
|
||||
document.fonts.onloadingdone = e => {
|
||||
builderStore.loadFonts(e.fontfaces)
|
||||
}
|
||||
|
||||
if (!hasSynced && application) {
|
||||
try {
|
||||
await API.syncApp(application)
|
||||
|
@ -145,17 +149,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">
|
||||
|
|
|
@ -71,6 +71,7 @@
|
|||
"multifieldselect",
|
||||
"s3upload",
|
||||
"codescanner",
|
||||
"bbreferencesinglefield",
|
||||
"bbreferencefield"
|
||||
]
|
||||
},
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -7017,16 +7017,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,
|
||||
|
@ -7130,5 +7142,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,17 +35,18 @@
|
|||
} = getContext("sdk")
|
||||
|
||||
let grid
|
||||
let gridContext
|
||||
|
||||
$: columnWhitelist = parsedColumns
|
||||
?.filter(col => col.active)
|
||||
?.map(col => col.field)
|
||||
$: parsedColumns = getParsedColumns(columns)
|
||||
$: columnWhitelist = parsedColumns.filter(x => x.active).map(x => x.field)
|
||||
$: schemaOverrides = getSchemaOverrides(parsedColumns)
|
||||
$: enrichedButtons = enrichButtons(buttons)
|
||||
$: parsedColumns = getParsedColumns(columns)
|
||||
$: selectedRows = deriveSelectedRows(gridContext)
|
||||
$: data = { selectedRows: $selectedRows }
|
||||
$: actions = [
|
||||
{
|
||||
type: ActionTypes.RefreshDatasource,
|
||||
callback: () => grid?.getContext()?.rows.actions.refreshData(),
|
||||
callback: () => gridContext?.rows.actions.refreshData(),
|
||||
metadata: { dataSource: table },
|
||||
},
|
||||
]
|
||||
|
@ -68,12 +68,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 +84,7 @@
|
|||
|
||||
const getSchemaOverrides = columns => {
|
||||
let overrides = {}
|
||||
columns?.forEach(column => {
|
||||
columns.forEach(column => {
|
||||
overrides[column.field] = {
|
||||
displayName: column.label,
|
||||
}
|
||||
|
@ -109,6 +111,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,
|
||||
|
@ -118,40 +137,44 @@
|
|||
},
|
||||
}
|
||||
}
|
||||
|
||||
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 })}
|
||||
/>
|
||||
</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 })}
|
||||
/>
|
||||
</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={[
|
||||
|
|
|
@ -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
|
||||
})
|
||||
|
|
|
@ -132,10 +132,11 @@ export const TypeIconMap = {
|
|||
[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 ff397e5454ad3361b25efdf14746c36dcbd3f409
|
||||
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 }
|
||||
)
|
||||
|
|
|
@ -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"
|
||||
|
||||
|
@ -107,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]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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.` }
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
}
|
||||
|
||||
|
|
|
@ -44,7 +44,7 @@ describe.each([
|
|||
const snippets = [
|
||||
{
|
||||
name: "WeeksAgo",
|
||||
code: "return function (weeks) {\n const currentTime = new Date();\n currentTime.setDate(currentTime.getDate()-(7 * (weeks || 1)));\n return currentTime.toISOString();\n}",
|
||||
code: `return function (weeks) {\n const currentTime = new Date(${Date.now()});\n currentTime.setDate(currentTime.getDate()-(7 * (weeks || 1)));\n return currentTime.toISOString();\n}`,
|
||||
},
|
||||
]
|
||||
|
||||
|
@ -262,11 +262,19 @@ describe.each([
|
|||
{ name: "serverDate", appointment: serverTime.toISOString() },
|
||||
{
|
||||
name: "single user, session user",
|
||||
single_user: JSON.stringify([currentUser]),
|
||||
single_user: JSON.stringify(currentUser),
|
||||
},
|
||||
{
|
||||
name: "single user",
|
||||
single_user: JSON.stringify([globalUsers[0]]),
|
||||
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",
|
||||
|
@ -276,6 +284,14 @@ describe.each([
|
|||
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]),
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
|
@ -301,13 +317,29 @@ describe.each([
|
|||
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()))
|
||||
|
@ -398,7 +430,18 @@ describe.each([
|
|||
}).toContainExactly([
|
||||
{
|
||||
name: "single user, session user",
|
||||
single_user: [{ _id: config.getUser()._id }],
|
||||
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 }],
|
||||
},
|
||||
])
|
||||
})
|
||||
|
@ -420,6 +463,23 @@ describe.each([
|
|||
])
|
||||
})
|
||||
|
||||
// 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 () => {
|
||||
|
@ -436,6 +496,22 @@ describe.each([
|
|||
])
|
||||
})
|
||||
|
||||
// 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: {
|
||||
|
@ -447,11 +523,31 @@ describe.each([
|
|||
}).toContainExactly([
|
||||
{
|
||||
name: "single user, session user",
|
||||
single_user: [{ _id: config.getUser()._id }],
|
||||
single_user: { _id: config.getUser()._id },
|
||||
},
|
||||
{
|
||||
name: "single user",
|
||||
single_user: [{ _id: globalUsers[0]._id }],
|
||||
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 }],
|
||||
},
|
||||
])
|
||||
})
|
||||
|
@ -467,7 +563,23 @@ describe.each([
|
|||
}).toContainExactly([
|
||||
{
|
||||
name: "single user",
|
||||
single_user: [{ _id: globalUsers[0]._id }],
|
||||
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 }],
|
||||
},
|
||||
])
|
||||
})
|
||||
|
|
|
@ -545,16 +545,16 @@ describe.each([
|
|||
)
|
||||
|
||||
await config.api.table.migrate(table._id!, {
|
||||
oldColumn: table.schema["user relationship"],
|
||||
newColumn: {
|
||||
name: "user column",
|
||||
type: FieldType.BB_REFERENCE,
|
||||
subtype: BBReferenceFieldSubType.USER,
|
||||
},
|
||||
oldColumn: "user relationship",
|
||||
newColumn: "user column",
|
||||
})
|
||||
|
||||
const migratedTable = await config.api.table.get(table._id!)
|
||||
expect(migratedTable.schema["user column"]).toBeDefined()
|
||||
expect(migratedTable.schema["user column"]).toEqual({
|
||||
name: "user column",
|
||||
type: FieldType.BB_REFERENCE_SINGLE,
|
||||
subtype: BBReferenceFieldSubType.USER,
|
||||
})
|
||||
expect(migratedTable.schema["user relationship"]).not.toBeDefined()
|
||||
|
||||
const migratedRows = await config.api.row.fetch(table._id!)
|
||||
|
@ -567,7 +567,7 @@ describe.each([
|
|||
expect(migratedRow["user column"]).toBeDefined()
|
||||
expect(migratedRow["user relationship"]).not.toBeDefined()
|
||||
expect(row["user relationship"][0]._id).toEqual(
|
||||
migratedRow["user column"][0]._id
|
||||
migratedRow["user column"]._id
|
||||
)
|
||||
}
|
||||
})
|
||||
|
@ -610,16 +610,19 @@ describe.each([
|
|||
)
|
||||
|
||||
await config.api.table.migrate(table._id!, {
|
||||
oldColumn: table.schema["user relationship"],
|
||||
newColumn: {
|
||||
name: "user column",
|
||||
type: FieldType.BB_REFERENCE,
|
||||
subtype: BBReferenceFieldSubType.USERS,
|
||||
},
|
||||
oldColumn: "user relationship",
|
||||
newColumn: "user column",
|
||||
})
|
||||
|
||||
const migratedTable = await config.api.table.get(table._id!)
|
||||
expect(migratedTable.schema["user column"]).toBeDefined()
|
||||
expect(migratedTable.schema["user column"]).toEqual({
|
||||
name: "user column",
|
||||
type: FieldType.BB_REFERENCE,
|
||||
subtype: BBReferenceFieldSubType.USER,
|
||||
constraints: {
|
||||
type: "array",
|
||||
},
|
||||
})
|
||||
expect(migratedTable.schema["user relationship"]).not.toBeDefined()
|
||||
|
||||
const migratedRow = await config.api.row.get(table._id!, testRow._id!)
|
||||
|
@ -662,16 +665,19 @@ describe.each([
|
|||
})
|
||||
|
||||
await config.api.table.migrate(table._id!, {
|
||||
oldColumn: table.schema["user relationship"],
|
||||
newColumn: {
|
||||
name: "user column",
|
||||
type: FieldType.BB_REFERENCE,
|
||||
subtype: BBReferenceFieldSubType.USERS,
|
||||
},
|
||||
oldColumn: "user relationship",
|
||||
newColumn: "user column",
|
||||
})
|
||||
|
||||
const migratedTable = await config.api.table.get(table._id!)
|
||||
expect(migratedTable.schema["user column"]).toBeDefined()
|
||||
expect(migratedTable.schema["user column"]).toEqual({
|
||||
name: "user column",
|
||||
type: FieldType.BB_REFERENCE,
|
||||
subtype: BBReferenceFieldSubType.USER,
|
||||
constraints: {
|
||||
type: "array",
|
||||
},
|
||||
})
|
||||
expect(migratedTable.schema["user relationship"]).not.toBeDefined()
|
||||
|
||||
const row1Migrated = await config.api.row.get(table._id!, row1._id!)
|
||||
|
@ -717,16 +723,19 @@ describe.each([
|
|||
})
|
||||
|
||||
await config.api.table.migrate(table._id!, {
|
||||
oldColumn: table.schema["user relationship"],
|
||||
newColumn: {
|
||||
name: "user column",
|
||||
type: FieldType.BB_REFERENCE,
|
||||
subtype: BBReferenceFieldSubType.USERS,
|
||||
},
|
||||
oldColumn: "user relationship",
|
||||
newColumn: "user column",
|
||||
})
|
||||
|
||||
const migratedTable = await config.api.table.get(table._id!)
|
||||
expect(migratedTable.schema["user column"]).toBeDefined()
|
||||
expect(migratedTable.schema["user column"]).toEqual({
|
||||
name: "user column",
|
||||
type: FieldType.BB_REFERENCE,
|
||||
subtype: BBReferenceFieldSubType.USER,
|
||||
constraints: {
|
||||
type: "array",
|
||||
},
|
||||
})
|
||||
expect(migratedTable.schema["user relationship"]).not.toBeDefined()
|
||||
|
||||
const row1Migrated = await config.api.row.get(table._id!, row1._id!)
|
||||
|
@ -776,12 +785,8 @@ describe.each([
|
|||
await config.api.table.migrate(
|
||||
table._id!,
|
||||
{
|
||||
oldColumn: table.schema["user relationship"],
|
||||
newColumn: {
|
||||
name: "",
|
||||
type: FieldType.BB_REFERENCE,
|
||||
subtype: BBReferenceFieldSubType.USERS,
|
||||
},
|
||||
oldColumn: "user relationship",
|
||||
newColumn: "",
|
||||
},
|
||||
{ status: 400 }
|
||||
)
|
||||
|
@ -791,12 +796,8 @@ describe.each([
|
|||
await config.api.table.migrate(
|
||||
table._id!,
|
||||
{
|
||||
oldColumn: table.schema["user relationship"],
|
||||
newColumn: {
|
||||
name: "_id",
|
||||
type: FieldType.BB_REFERENCE,
|
||||
subtype: BBReferenceFieldSubType.USERS,
|
||||
},
|
||||
oldColumn: "user relationship",
|
||||
newColumn: "_id",
|
||||
},
|
||||
{ status: 400 }
|
||||
)
|
||||
|
@ -806,12 +807,8 @@ describe.each([
|
|||
await config.api.table.migrate(
|
||||
table._id!,
|
||||
{
|
||||
oldColumn: table.schema["user relationship"],
|
||||
newColumn: {
|
||||
name: "num",
|
||||
type: FieldType.BB_REFERENCE,
|
||||
subtype: BBReferenceFieldSubType.USERS,
|
||||
},
|
||||
oldColumn: "user relationship",
|
||||
newColumn: "num",
|
||||
},
|
||||
{ status: 400 }
|
||||
)
|
||||
|
@ -821,16 +818,8 @@ describe.each([
|
|||
await config.api.table.migrate(
|
||||
table._id!,
|
||||
{
|
||||
oldColumn: {
|
||||
name: "not a column",
|
||||
type: FieldType.BB_REFERENCE,
|
||||
subtype: BBReferenceFieldSubType.USERS,
|
||||
},
|
||||
newColumn: {
|
||||
name: "new column",
|
||||
type: FieldType.BB_REFERENCE,
|
||||
subtype: BBReferenceFieldSubType.USERS,
|
||||
},
|
||||
oldColumn: "not a column",
|
||||
newColumn: "new column",
|
||||
},
|
||||
{ status: 400 }
|
||||
)
|
||||
|
|
|
@ -88,7 +88,7 @@ describe.each(
|
|||
let res = await setup.runStep(setup.actions.EXECUTE_QUERY.stepId, {
|
||||
query: { queryId: "wrong_id" },
|
||||
})
|
||||
expect(res.response).toEqual("Error: missing")
|
||||
expect(res.response).toEqual("Error: CouchDB error: missing")
|
||||
expect(res.success).toEqual(false)
|
||||
})
|
||||
})
|
||||
|
|
|
@ -20,7 +20,7 @@ function parseIntSafe(number?: string) {
|
|||
|
||||
const DEFAULTS = {
|
||||
QUERY_THREAD_TIMEOUT: 15000,
|
||||
AUTOMATION_THREAD_TIMEOUT: 12000,
|
||||
AUTOMATION_THREAD_TIMEOUT: 15000,
|
||||
AUTOMATION_SYNC_TIMEOUT: 120000,
|
||||
AUTOMATION_MAX_ITERATIONS: 200,
|
||||
JS_PER_EXECUTION_TIME_LIMIT_MS: 1500,
|
||||
|
@ -34,6 +34,10 @@ const DEFAULTS = {
|
|||
const QUERY_THREAD_TIMEOUT =
|
||||
parseIntSafe(process.env.QUERY_THREAD_TIMEOUT) ||
|
||||
DEFAULTS.QUERY_THREAD_TIMEOUT
|
||||
const DEFAULT_AUTOMATION_TIMEOUT =
|
||||
QUERY_THREAD_TIMEOUT > DEFAULTS.AUTOMATION_THREAD_TIMEOUT
|
||||
? QUERY_THREAD_TIMEOUT
|
||||
: DEFAULTS.AUTOMATION_THREAD_TIMEOUT
|
||||
const environment = {
|
||||
// features
|
||||
APP_FEATURES: process.env.APP_FEATURES,
|
||||
|
@ -75,9 +79,7 @@ const environment = {
|
|||
QUERY_THREAD_TIMEOUT: QUERY_THREAD_TIMEOUT,
|
||||
AUTOMATION_THREAD_TIMEOUT:
|
||||
parseIntSafe(process.env.AUTOMATION_THREAD_TIMEOUT) ||
|
||||
DEFAULTS.AUTOMATION_THREAD_TIMEOUT > QUERY_THREAD_TIMEOUT
|
||||
? DEFAULTS.AUTOMATION_THREAD_TIMEOUT
|
||||
: QUERY_THREAD_TIMEOUT,
|
||||
DEFAULT_AUTOMATION_TIMEOUT,
|
||||
BB_ADMIN_USER_EMAIL: process.env.BB_ADMIN_USER_EMAIL,
|
||||
BB_ADMIN_USER_PASSWORD: process.env.BB_ADMIN_USER_PASSWORD,
|
||||
PLUGINS_DIR: process.env.PLUGINS_DIR || DEFAULTS.PLUGINS_DIR,
|
||||
|
|
|
@ -12,7 +12,6 @@ import SqlTableQueryBuilder from "./sqlTable"
|
|||
import {
|
||||
BBReferenceFieldMetadata,
|
||||
FieldSchema,
|
||||
BBReferenceFieldSubType,
|
||||
FieldType,
|
||||
JsonFieldMetadata,
|
||||
Operation,
|
||||
|
@ -27,6 +26,7 @@ import {
|
|||
INTERNAL_TABLE_SOURCE_ID,
|
||||
} from "@budibase/types"
|
||||
import environment from "../../environment"
|
||||
import { helpers } from "@budibase/shared-core"
|
||||
|
||||
type QueryFunction = (query: SqlQuery | SqlQuery[], operation: Operation) => any
|
||||
|
||||
|
@ -787,7 +787,7 @@ class SqlQueryBuilder extends SqlTableQueryBuilder {
|
|||
return (
|
||||
field.type === FieldType.JSON ||
|
||||
(field.type === FieldType.BB_REFERENCE &&
|
||||
field.subtype === BBReferenceFieldSubType.USERS)
|
||||
!helpers.schema.isDeprecatedSingleUserColumn(field))
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
import { Knex, knex } from "knex"
|
||||
import {
|
||||
BBReferenceFieldSubType,
|
||||
FieldType,
|
||||
NumberFieldMetadata,
|
||||
Operation,
|
||||
|
@ -12,7 +11,7 @@ import {
|
|||
TableSourceType,
|
||||
} from "@budibase/types"
|
||||
import { breakExternalTableId, getNativeSql, SqlClient } from "../utils"
|
||||
import { utils } from "@budibase/shared-core"
|
||||
import { helpers, utils } from "@budibase/shared-core"
|
||||
import SchemaBuilder = Knex.SchemaBuilder
|
||||
import CreateTableBuilder = Knex.CreateTableBuilder
|
||||
|
||||
|
@ -54,27 +53,15 @@ function generateSchema(
|
|||
) {
|
||||
continue
|
||||
}
|
||||
switch (column.type) {
|
||||
const columnType = column.type
|
||||
switch (columnType) {
|
||||
case FieldType.STRING:
|
||||
case FieldType.OPTIONS:
|
||||
case FieldType.LONGFORM:
|
||||
case FieldType.BARCODEQR:
|
||||
case FieldType.BB_REFERENCE_SINGLE:
|
||||
schema.text(key)
|
||||
break
|
||||
case FieldType.BB_REFERENCE: {
|
||||
const subtype = column.subtype
|
||||
switch (subtype) {
|
||||
case BBReferenceFieldSubType.USER:
|
||||
schema.text(key)
|
||||
break
|
||||
case BBReferenceFieldSubType.USERS:
|
||||
schema.json(key)
|
||||
break
|
||||
default:
|
||||
throw utils.unreachable(subtype)
|
||||
}
|
||||
break
|
||||
}
|
||||
case FieldType.NUMBER:
|
||||
// if meta is specified then this is a junction table entry
|
||||
if (column.meta && column.meta.toKey && column.meta.toTable) {
|
||||
|
@ -97,7 +84,13 @@ function generateSchema(
|
|||
})
|
||||
break
|
||||
case FieldType.ARRAY:
|
||||
schema.json(key)
|
||||
case FieldType.BB_REFERENCE:
|
||||
if (helpers.schema.isDeprecatedSingleUserColumn(column)) {
|
||||
// This is still required for unit testing, in order to create "deprecated" schemas
|
||||
schema.text(key)
|
||||
} else {
|
||||
schema.json(key)
|
||||
}
|
||||
break
|
||||
case FieldType.LINK:
|
||||
// this side of the relationship doesn't need any SQL work
|
||||
|
@ -127,6 +120,18 @@ function generateSchema(
|
|||
.references(`${tableName}.${relatedPrimary}`)
|
||||
}
|
||||
break
|
||||
case FieldType.FORMULA:
|
||||
// This is allowed, but nothing to do on the external datasource
|
||||
break
|
||||
case FieldType.ATTACHMENTS:
|
||||
case FieldType.ATTACHMENT_SINGLE:
|
||||
case FieldType.AUTO:
|
||||
case FieldType.JSON:
|
||||
case FieldType.INTERNAL:
|
||||
throw `${column.type} is not a valid SQL type`
|
||||
|
||||
default:
|
||||
utils.unreachable(columnType)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -17,6 +17,7 @@ import {
|
|||
TableRequest,
|
||||
TableSourceType,
|
||||
DatasourcePlusQueryResponse,
|
||||
BBReferenceFieldSubType,
|
||||
} from "@budibase/types"
|
||||
import { OAuth2Client } from "google-auth-library"
|
||||
import {
|
||||
|
@ -52,17 +53,30 @@ interface AuthTokenResponse {
|
|||
access_token: string
|
||||
}
|
||||
|
||||
const ALLOWED_TYPES = [
|
||||
FieldType.STRING,
|
||||
FieldType.FORMULA,
|
||||
FieldType.NUMBER,
|
||||
FieldType.LONGFORM,
|
||||
FieldType.DATETIME,
|
||||
FieldType.OPTIONS,
|
||||
FieldType.BOOLEAN,
|
||||
FieldType.BARCODEQR,
|
||||
FieldType.BB_REFERENCE,
|
||||
]
|
||||
const isTypeAllowed: Record<FieldType, boolean> = {
|
||||
[FieldType.STRING]: true,
|
||||
[FieldType.FORMULA]: true,
|
||||
[FieldType.NUMBER]: true,
|
||||
[FieldType.LONGFORM]: true,
|
||||
[FieldType.DATETIME]: true,
|
||||
[FieldType.OPTIONS]: true,
|
||||
[FieldType.BOOLEAN]: true,
|
||||
[FieldType.BARCODEQR]: true,
|
||||
[FieldType.BB_REFERENCE]: true,
|
||||
[FieldType.BB_REFERENCE_SINGLE]: true,
|
||||
[FieldType.ARRAY]: false,
|
||||
[FieldType.ATTACHMENTS]: false,
|
||||
[FieldType.ATTACHMENT_SINGLE]: false,
|
||||
[FieldType.LINK]: false,
|
||||
[FieldType.AUTO]: false,
|
||||
[FieldType.JSON]: false,
|
||||
[FieldType.INTERNAL]: false,
|
||||
[FieldType.BIGINT]: false,
|
||||
}
|
||||
|
||||
const ALLOWED_TYPES = Object.entries(isTypeAllowed)
|
||||
.filter(([_, allowed]) => allowed)
|
||||
.map(([type]) => type as FieldType)
|
||||
|
||||
const SCHEMA: Integration = {
|
||||
plus: true,
|
||||
|
@ -350,6 +364,7 @@ class GoogleSheetsIntegration implements DatasourcePlus {
|
|||
rowIndex: json.extra?.idFilter?.equal?.rowNumber,
|
||||
sheet,
|
||||
row: json.body,
|
||||
table: json.meta.table,
|
||||
})
|
||||
case Operation.DELETE:
|
||||
return this.delete({
|
||||
|
@ -371,9 +386,11 @@ class GoogleSheetsIntegration implements DatasourcePlus {
|
|||
}
|
||||
|
||||
buildRowObject(headers: string[], values: string[], rowNumber: number) {
|
||||
const rowObject: { rowNumber: number; [key: string]: any } = { rowNumber }
|
||||
const rowObject: { rowNumber: number } & Row = {
|
||||
rowNumber,
|
||||
_id: rowNumber.toString(),
|
||||
}
|
||||
for (let i = 0; i < headers.length; i++) {
|
||||
rowObject._id = rowNumber
|
||||
rowObject[headers[i]] = values[i]
|
||||
}
|
||||
return rowObject
|
||||
|
@ -430,14 +447,6 @@ class GoogleSheetsIntegration implements DatasourcePlus {
|
|||
}
|
||||
}
|
||||
|
||||
// clear out deleted columns
|
||||
for (let key of sheet.headerValues) {
|
||||
if (!Object.keys(table.schema).includes(key)) {
|
||||
const idx = updatedHeaderValues.indexOf(key)
|
||||
updatedHeaderValues.splice(idx, 1)
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
await sheet.setHeaderRow(updatedHeaderValues)
|
||||
} catch (err) {
|
||||
|
@ -458,7 +467,7 @@ class GoogleSheetsIntegration implements DatasourcePlus {
|
|||
}
|
||||
}
|
||||
|
||||
async create(query: { sheet: string; row: any }) {
|
||||
async create(query: { sheet: string; row: Row }) {
|
||||
try {
|
||||
await this.connect()
|
||||
const sheet = this.client.sheetsByTitle[query.sheet]
|
||||
|
@ -474,7 +483,7 @@ class GoogleSheetsIntegration implements DatasourcePlus {
|
|||
}
|
||||
}
|
||||
|
||||
async createBulk(query: { sheet: string; rows: any[] }) {
|
||||
async createBulk(query: { sheet: string; rows: Row[] }) {
|
||||
try {
|
||||
await this.connect()
|
||||
const sheet = this.client.sheetsByTitle[query.sheet]
|
||||
|
@ -573,7 +582,12 @@ class GoogleSheetsIntegration implements DatasourcePlus {
|
|||
return { sheet, row }
|
||||
}
|
||||
|
||||
async update(query: { sheet: string; rowIndex: number; row: any }) {
|
||||
async update(query: {
|
||||
sheet: string
|
||||
rowIndex: number
|
||||
row: any
|
||||
table: Table
|
||||
}) {
|
||||
try {
|
||||
await this.connect()
|
||||
const { sheet, row } = await this.getRowByIndex(
|
||||
|
@ -589,6 +603,15 @@ class GoogleSheetsIntegration implements DatasourcePlus {
|
|||
if (row[key] === null) {
|
||||
row[key] = ""
|
||||
}
|
||||
|
||||
const { type, subtype, constraints } = query.table.schema[key]
|
||||
const isDeprecatedSingleUser =
|
||||
type === FieldType.BB_REFERENCE &&
|
||||
subtype === BBReferenceFieldSubType.USER &&
|
||||
constraints?.type !== "array"
|
||||
if (isDeprecatedSingleUser && Array.isArray(row[key])) {
|
||||
row[key] = row[key][0]
|
||||
}
|
||||
}
|
||||
await row.save()
|
||||
return [
|
||||
|
|
|
@ -129,10 +129,11 @@ describe("Google Sheets Integration", () => {
|
|||
})
|
||||
expect(sheet.loadHeaderRow).toHaveBeenCalledTimes(1)
|
||||
expect(sheet.setHeaderRow).toHaveBeenCalledTimes(1)
|
||||
expect(sheet.setHeaderRow).toHaveBeenCalledWith(["name"])
|
||||
|
||||
// No undefined are sent
|
||||
expect((sheet.setHeaderRow as any).mock.calls[0][0]).toHaveLength(1)
|
||||
expect(sheet.setHeaderRow).toHaveBeenCalledWith([
|
||||
"name",
|
||||
"description",
|
||||
"location",
|
||||
])
|
||||
})
|
||||
})
|
||||
|
||||
|
|
|
@ -383,6 +383,7 @@ function copyExistingPropsOver(
|
|||
case FieldType.ATTACHMENT_SINGLE:
|
||||
case FieldType.JSON:
|
||||
case FieldType.BB_REFERENCE:
|
||||
case FieldType.BB_REFERENCE_SINGLE:
|
||||
shouldKeepSchema = keepIfType(FieldType.JSON, FieldType.STRING)
|
||||
break
|
||||
|
||||
|
|
|
@ -79,7 +79,9 @@ export async function search(
|
|||
}
|
||||
|
||||
const table = await sdk.tables.getTable(options.tableId)
|
||||
options = searchInputMapping(table, options)
|
||||
options = searchInputMapping(table, options, {
|
||||
isSql: !!table.sql || !!env.SQS_SEARCH_ENABLE,
|
||||
})
|
||||
|
||||
if (isExternalTable) {
|
||||
return external.search(options, table)
|
||||
|
|
|
@ -19,7 +19,7 @@ const tableWithUserCol: Table = {
|
|||
schema: {
|
||||
user: {
|
||||
name: "user",
|
||||
type: FieldType.BB_REFERENCE,
|
||||
type: FieldType.BB_REFERENCE_SINGLE,
|
||||
subtype: BBReferenceFieldSubType.USER,
|
||||
},
|
||||
},
|
||||
|
@ -35,7 +35,7 @@ const tableWithUsersCol: Table = {
|
|||
user: {
|
||||
name: "user",
|
||||
type: FieldType.BB_REFERENCE,
|
||||
subtype: BBReferenceFieldSubType.USERS,
|
||||
subtype: BBReferenceFieldSubType.USER,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
|
|
@ -11,7 +11,7 @@ import {
|
|||
RowSearchParams,
|
||||
} from "@budibase/types"
|
||||
import { db as dbCore, context } from "@budibase/backend-core"
|
||||
import { utils } from "@budibase/shared-core"
|
||||
import { helpers, utils } from "@budibase/shared-core"
|
||||
|
||||
export async function paginatedSearch(
|
||||
query: SearchFilters,
|
||||
|
@ -49,13 +49,19 @@ function findColumnInQueries(
|
|||
}
|
||||
}
|
||||
|
||||
function userColumnMapping(column: string, options: RowSearchParams) {
|
||||
function userColumnMapping(
|
||||
column: string,
|
||||
options: RowSearchParams,
|
||||
isDeprecatedSingleUserColumn: boolean = false,
|
||||
isSql: boolean = false
|
||||
) {
|
||||
findColumnInQueries(column, options, (filterValue: any): any => {
|
||||
const isArray = Array.isArray(filterValue),
|
||||
isString = typeof filterValue === "string"
|
||||
if (!isString && !isArray) {
|
||||
return filterValue
|
||||
}
|
||||
|
||||
const processString = (input: string) => {
|
||||
const rowPrefix = DocumentType.ROW + SEPARATOR
|
||||
if (input.startsWith(rowPrefix)) {
|
||||
|
@ -64,40 +70,60 @@ function userColumnMapping(column: string, options: RowSearchParams) {
|
|||
return input
|
||||
}
|
||||
}
|
||||
|
||||
let wrapper = (s: string) => s
|
||||
if (isDeprecatedSingleUserColumn && filterValue && isSql) {
|
||||
// Decreated single users are stored as stringified arrays of a single value
|
||||
wrapper = (s: string) => JSON.stringify([s])
|
||||
}
|
||||
|
||||
if (isArray) {
|
||||
return filterValue.map(el => {
|
||||
if (typeof el === "string") {
|
||||
return processString(el)
|
||||
return wrapper(processString(el))
|
||||
} else {
|
||||
return el
|
||||
}
|
||||
})
|
||||
} else {
|
||||
return processString(filterValue)
|
||||
return wrapper(processString(filterValue))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// maps through the search parameters to check if any of the inputs are invalid
|
||||
// based on the table schema, converts them to something that is valid.
|
||||
export function searchInputMapping(table: Table, options: RowSearchParams) {
|
||||
export function searchInputMapping(
|
||||
table: Table,
|
||||
options: RowSearchParams,
|
||||
datasourceOptions: { isSql?: boolean } = {}
|
||||
) {
|
||||
if (!table?.schema) {
|
||||
return options
|
||||
}
|
||||
for (let [key, column] of Object.entries(table.schema)) {
|
||||
switch (column.type) {
|
||||
case FieldType.BB_REFERENCE: {
|
||||
case FieldType.BB_REFERENCE_SINGLE: {
|
||||
const subtype = column.subtype
|
||||
switch (subtype) {
|
||||
case BBReferenceFieldSubType.USER:
|
||||
case BBReferenceFieldSubType.USERS:
|
||||
userColumnMapping(key, options)
|
||||
break
|
||||
|
||||
default:
|
||||
utils.unreachable(subtype)
|
||||
}
|
||||
break
|
||||
}
|
||||
case FieldType.BB_REFERENCE: {
|
||||
userColumnMapping(
|
||||
key,
|
||||
options,
|
||||
helpers.schema.isDeprecatedSingleUserColumn(column),
|
||||
datasourceOptions.isSql
|
||||
)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
return options
|
||||
|
|
|
@ -45,6 +45,7 @@ const FieldTypeMap: Record<FieldType, SQLiteType> = {
|
|||
[FieldType.BIGINT]: SQLiteType.TEXT,
|
||||
// TODO: consider the difference between multi-user and single user types (subtyping)
|
||||
[FieldType.BB_REFERENCE]: SQLiteType.TEXT,
|
||||
[FieldType.BB_REFERENCE_SINGLE]: SQLiteType.TEXT,
|
||||
}
|
||||
|
||||
function buildRelationshipDefinitions(
|
||||
|
|
|
@ -4,7 +4,6 @@ import {
|
|||
FieldSchema,
|
||||
BBReferenceFieldSubType,
|
||||
InternalTable,
|
||||
isBBReferenceField,
|
||||
isRelationshipField,
|
||||
LinkDocument,
|
||||
LinkInfo,
|
||||
|
@ -12,6 +11,8 @@ import {
|
|||
RelationshipType,
|
||||
Row,
|
||||
Table,
|
||||
FieldType,
|
||||
BBReferenceSingleFieldMetadata,
|
||||
} from "@budibase/types"
|
||||
import sdk from "../../../sdk"
|
||||
import { isExternalTableID } from "../../../integrations/utils"
|
||||
|
@ -24,25 +25,58 @@ export interface MigrationResult {
|
|||
|
||||
export async function migrate(
|
||||
table: Table,
|
||||
oldColumn: FieldSchema,
|
||||
newColumn: FieldSchema
|
||||
oldColumnName: string,
|
||||
newColumnName: string
|
||||
): Promise<MigrationResult> {
|
||||
if (newColumn.name in table.schema) {
|
||||
throw new BadRequestError(`Column "${newColumn.name}" already exists`)
|
||||
if (newColumnName in table.schema) {
|
||||
throw new BadRequestError(`Column "${newColumnName}" already exists`)
|
||||
}
|
||||
|
||||
if (newColumn.name === "") {
|
||||
if (newColumnName === "") {
|
||||
throw new BadRequestError(`Column name cannot be empty`)
|
||||
}
|
||||
|
||||
if (dbCore.isInternalColumnName(newColumn.name)) {
|
||||
if (dbCore.isInternalColumnName(newColumnName)) {
|
||||
throw new BadRequestError(`Column name cannot be a reserved column name`)
|
||||
}
|
||||
|
||||
const oldColumn = table.schema[oldColumnName]
|
||||
|
||||
if (!oldColumn) {
|
||||
throw new BadRequestError(
|
||||
`Column "${oldColumnName}" does not exist on table "${table.name}"`
|
||||
)
|
||||
}
|
||||
|
||||
if (
|
||||
oldColumn.type !== FieldType.LINK ||
|
||||
oldColumn.tableId !== InternalTable.USER_METADATA
|
||||
) {
|
||||
throw new BadRequestError(
|
||||
`Only user relationship migration columns is currently supported`
|
||||
)
|
||||
}
|
||||
|
||||
const type =
|
||||
oldColumn.relationshipType === RelationshipType.ONE_TO_MANY
|
||||
? FieldType.BB_REFERENCE_SINGLE
|
||||
: FieldType.BB_REFERENCE
|
||||
const newColumn: FieldSchema = {
|
||||
name: newColumnName,
|
||||
type,
|
||||
subtype: BBReferenceFieldSubType.USER,
|
||||
}
|
||||
|
||||
if (newColumn.type === FieldType.BB_REFERENCE) {
|
||||
newColumn.constraints = {
|
||||
type: "array",
|
||||
}
|
||||
}
|
||||
|
||||
table.schema[newColumn.name] = newColumn
|
||||
table = await sdk.tables.saveTable(table)
|
||||
|
||||
let migrator = getColumnMigrator(table, oldColumn, newColumn)
|
||||
const migrator = getColumnMigrator(table, oldColumn, newColumn)
|
||||
try {
|
||||
return await migrator.doMigration()
|
||||
} catch (e) {
|
||||
|
@ -75,11 +109,14 @@ function getColumnMigrator(
|
|||
throw new BadRequestError(`Column "${oldColumn.name}" does not exist`)
|
||||
}
|
||||
|
||||
if (!isBBReferenceField(newColumn)) {
|
||||
if (
|
||||
newColumn.type !== FieldType.BB_REFERENCE_SINGLE &&
|
||||
newColumn.type !== FieldType.BB_REFERENCE
|
||||
) {
|
||||
throw new BadRequestError(`Column "${newColumn.name}" is not a user column`)
|
||||
}
|
||||
|
||||
if (newColumn.subtype !== "user" && newColumn.subtype !== "users") {
|
||||
if (newColumn.subtype !== BBReferenceFieldSubType.USER) {
|
||||
throw new BadRequestError(`Column "${newColumn.name}" is not a user column`)
|
||||
}
|
||||
|
||||
|
@ -96,7 +133,7 @@ function getColumnMigrator(
|
|||
}
|
||||
|
||||
if (oldColumn.relationshipType === RelationshipType.ONE_TO_MANY) {
|
||||
if (newColumn.subtype !== BBReferenceFieldSubType.USER) {
|
||||
if (newColumn.type !== FieldType.BB_REFERENCE_SINGLE) {
|
||||
throw new BadRequestError(
|
||||
`Column "${oldColumn.name}" is a one-to-many column but "${newColumn.name}" is not a single user column`
|
||||
)
|
||||
|
@ -107,22 +144,23 @@ function getColumnMigrator(
|
|||
oldColumn.relationshipType === RelationshipType.MANY_TO_MANY ||
|
||||
oldColumn.relationshipType === RelationshipType.MANY_TO_ONE
|
||||
) {
|
||||
if (newColumn.subtype !== BBReferenceFieldSubType.USERS) {
|
||||
if (newColumn.type !== FieldType.BB_REFERENCE) {
|
||||
throw new BadRequestError(
|
||||
`Column "${oldColumn.name}" is a ${oldColumn.relationshipType} column but "${newColumn.name}" is not a multi user column`
|
||||
)
|
||||
}
|
||||
|
||||
return new MultiUserColumnMigrator(table, oldColumn, newColumn)
|
||||
}
|
||||
|
||||
throw new BadRequestError(`Unknown migration type`)
|
||||
}
|
||||
|
||||
abstract class UserColumnMigrator implements ColumnMigrator {
|
||||
abstract class UserColumnMigrator<T> implements ColumnMigrator {
|
||||
constructor(
|
||||
protected table: Table,
|
||||
protected oldColumn: RelationshipFieldMetadata,
|
||||
protected newColumn: BBReferenceFieldMetadata
|
||||
protected newColumn: T
|
||||
) {}
|
||||
|
||||
abstract updateRow(row: Row, linkInfo: LinkInfo): void
|
||||
|
@ -192,7 +230,7 @@ abstract class UserColumnMigrator implements ColumnMigrator {
|
|||
}
|
||||
}
|
||||
|
||||
class SingleUserColumnMigrator extends UserColumnMigrator {
|
||||
class SingleUserColumnMigrator extends UserColumnMigrator<BBReferenceSingleFieldMetadata> {
|
||||
updateRow(row: Row, linkInfo: LinkInfo): void {
|
||||
row[this.newColumn.name] = dbCore.getGlobalIDFromUserMetadataID(
|
||||
linkInfo.rowId
|
||||
|
@ -200,7 +238,7 @@ class SingleUserColumnMigrator extends UserColumnMigrator {
|
|||
}
|
||||
}
|
||||
|
||||
class MultiUserColumnMigrator extends UserColumnMigrator {
|
||||
class MultiUserColumnMigrator extends UserColumnMigrator<BBReferenceFieldMetadata> {
|
||||
updateRow(row: Row, linkInfo: LinkInfo): void {
|
||||
if (!row[this.newColumn.name]) {
|
||||
row[this.newColumn.name] = []
|
||||
|
|
|
@ -9,26 +9,57 @@ import { InvalidBBRefError } from "./errors"
|
|||
|
||||
const ROW_PREFIX = DocumentType.ROW + SEPARATOR
|
||||
|
||||
export async function processInputBBReferences(
|
||||
value: string | string[] | { _id: string } | { _id: string }[],
|
||||
subtype: BBReferenceFieldSubType.USER | BBReferenceFieldSubType.USERS
|
||||
): Promise<string | string[] | null> {
|
||||
let referenceIds: string[] = []
|
||||
export async function processInputBBReference(
|
||||
value: string | { _id: string },
|
||||
subtype: BBReferenceFieldSubType.USER
|
||||
): Promise<string | null> {
|
||||
if (value && Array.isArray(value)) {
|
||||
throw "BB_REFERENCE_SINGLE cannot be an array"
|
||||
}
|
||||
let id = typeof value === "string" ? value : value?._id
|
||||
|
||||
if (Array.isArray(value)) {
|
||||
referenceIds.push(
|
||||
...value.map(idOrDoc =>
|
||||
typeof idOrDoc === "string" ? idOrDoc : idOrDoc._id
|
||||
)
|
||||
)
|
||||
} else if (typeof value !== "string") {
|
||||
referenceIds.push(value._id)
|
||||
if (!id) {
|
||||
return null
|
||||
}
|
||||
|
||||
switch (subtype) {
|
||||
case BBReferenceFieldSubType.USER: {
|
||||
if (id.startsWith(ROW_PREFIX)) {
|
||||
id = dbCore.getGlobalIDFromUserMetadataID(id)
|
||||
}
|
||||
|
||||
try {
|
||||
await cache.user.getUser(id)
|
||||
return id
|
||||
} catch (e: any) {
|
||||
if (e.statusCode === 404) {
|
||||
throw new InvalidBBRefError(id, BBReferenceFieldSubType.USER)
|
||||
}
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
default:
|
||||
throw utils.unreachable(subtype)
|
||||
}
|
||||
}
|
||||
export async function processInputBBReferences(
|
||||
value: string | string[] | { _id: string }[],
|
||||
subtype: BBReferenceFieldSubType
|
||||
): Promise<string[] | null> {
|
||||
if (!value || !value[0]) {
|
||||
return null
|
||||
}
|
||||
|
||||
let referenceIds
|
||||
if (typeof value === "string") {
|
||||
referenceIds = value
|
||||
.split(",")
|
||||
.map(u => u.trim())
|
||||
.filter(u => !!u)
|
||||
} else {
|
||||
referenceIds.push(
|
||||
...value
|
||||
.split(",")
|
||||
.filter(x => x)
|
||||
.map((id: string) => id.trim())
|
||||
referenceIds = value.map(idOrDoc =>
|
||||
typeof idOrDoc === "string" ? idOrDoc : idOrDoc._id
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -44,6 +75,8 @@ export async function processInputBBReferences(
|
|||
})
|
||||
|
||||
switch (subtype) {
|
||||
case undefined:
|
||||
throw "Subtype must be defined"
|
||||
case BBReferenceFieldSubType.USER:
|
||||
case BBReferenceFieldSubType.USERS: {
|
||||
const { notFoundIds } = await cache.user.getUsers(referenceIds)
|
||||
|
@ -55,11 +88,54 @@ export async function processInputBBReferences(
|
|||
)
|
||||
}
|
||||
|
||||
if (subtype === BBReferenceFieldSubType.USERS) {
|
||||
return referenceIds
|
||||
if (!referenceIds?.length) {
|
||||
return null
|
||||
}
|
||||
|
||||
return referenceIds.join(",") || null
|
||||
return referenceIds
|
||||
}
|
||||
default:
|
||||
throw utils.unreachable(subtype)
|
||||
}
|
||||
}
|
||||
|
||||
interface UserReferenceInfo {
|
||||
_id: string
|
||||
primaryDisplay: string
|
||||
email: string
|
||||
firstName?: string
|
||||
lastName?: string
|
||||
}
|
||||
|
||||
export async function processOutputBBReference(
|
||||
value: string | null | undefined,
|
||||
subtype: BBReferenceFieldSubType.USER
|
||||
): Promise<UserReferenceInfo | undefined> {
|
||||
if (!value) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
switch (subtype) {
|
||||
case BBReferenceFieldSubType.USER: {
|
||||
let user
|
||||
try {
|
||||
user = await cache.user.getUser(value as string)
|
||||
} catch (err: any) {
|
||||
if (err.statusCode !== 404) {
|
||||
throw err
|
||||
}
|
||||
}
|
||||
if (!user) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
return {
|
||||
_id: user._id!,
|
||||
primaryDisplay: user.email,
|
||||
email: user.email,
|
||||
firstName: user.firstName,
|
||||
lastName: user.lastName,
|
||||
}
|
||||
}
|
||||
default:
|
||||
throw utils.unreachable(subtype)
|
||||
|
@ -67,14 +143,12 @@ export async function processInputBBReferences(
|
|||
}
|
||||
|
||||
export async function processOutputBBReferences(
|
||||
value: string | string[],
|
||||
subtype: BBReferenceFieldSubType.USER | BBReferenceFieldSubType.USERS
|
||||
) {
|
||||
if (value === null || value === undefined) {
|
||||
// Already processed or nothing to process
|
||||
return value || undefined
|
||||
value: string | null | undefined,
|
||||
subtype: BBReferenceFieldSubType
|
||||
): Promise<UserReferenceInfo[] | undefined> {
|
||||
if (!value) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
const ids =
|
||||
typeof value === "string" ? value.split(",").filter(id => !!id) : value
|
||||
|
||||
|
@ -87,7 +161,7 @@ export async function processOutputBBReferences(
|
|||
}
|
||||
|
||||
return users.map(u => ({
|
||||
_id: u._id,
|
||||
_id: u._id!,
|
||||
primaryDisplay: u.email,
|
||||
email: u.email,
|
||||
firstName: u.firstName,
|
||||
|
|
|
@ -12,7 +12,9 @@ import {
|
|||
} from "@budibase/types"
|
||||
import { cloneDeep } from "lodash/fp"
|
||||
import {
|
||||
processInputBBReference,
|
||||
processInputBBReferences,
|
||||
processOutputBBReference,
|
||||
processOutputBBReferences,
|
||||
} from "./bbReferenceProcessor"
|
||||
import { isExternalTableID } from "../../integrations/utils"
|
||||
|
@ -160,10 +162,10 @@ export async function inputProcessing(
|
|||
if (attachment?.url) {
|
||||
delete clonedRow[key].url
|
||||
}
|
||||
}
|
||||
|
||||
if (field.type === FieldType.BB_REFERENCE && value) {
|
||||
} else if (field.type === FieldType.BB_REFERENCE && value) {
|
||||
clonedRow[key] = await processInputBBReferences(value, field.subtype)
|
||||
} else if (field.type === FieldType.BB_REFERENCE_SINGLE && value) {
|
||||
clonedRow[key] = await processInputBBReference(value, field.subtype)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -252,6 +254,16 @@ export async function outputProcessing<T extends Row[] | Row>(
|
|||
column.subtype
|
||||
)
|
||||
}
|
||||
} else if (
|
||||
!opts.skipBBReferences &&
|
||||
column.type == FieldType.BB_REFERENCE_SINGLE
|
||||
) {
|
||||
for (let row of enriched) {
|
||||
row[property] = await processOutputBBReference(
|
||||
row[property],
|
||||
column.subtype
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -2,7 +2,9 @@ import _ from "lodash"
|
|||
import * as backendCore from "@budibase/backend-core"
|
||||
import { BBReferenceFieldSubType, User } from "@budibase/types"
|
||||
import {
|
||||
processInputBBReference,
|
||||
processInputBBReferences,
|
||||
processOutputBBReference,
|
||||
processOutputBBReferences,
|
||||
} from "../bbReferenceProcessor"
|
||||
import {
|
||||
|
@ -22,6 +24,7 @@ jest.mock("@budibase/backend-core", (): typeof backendCore => {
|
|||
...actual.cache,
|
||||
user: {
|
||||
...actual.cache.user,
|
||||
getUser: jest.fn(actual.cache.user.getUser),
|
||||
getUsers: jest.fn(actual.cache.user.getUsers),
|
||||
},
|
||||
},
|
||||
|
@ -31,6 +34,9 @@ jest.mock("@budibase/backend-core", (): typeof backendCore => {
|
|||
const config = new DBTestConfiguration()
|
||||
|
||||
describe("bbReferenceProcessor", () => {
|
||||
const cacheGetUserSpy = backendCore.cache.user.getUser as jest.MockedFunction<
|
||||
typeof backendCore.cache.user.getUser
|
||||
>
|
||||
const cacheGetUsersSpy = backendCore.cache.user
|
||||
.getUsers as jest.MockedFunction<typeof backendCore.cache.user.getUsers>
|
||||
|
||||
|
@ -56,6 +62,64 @@ describe("bbReferenceProcessor", () => {
|
|||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
describe("processInputBBReference", () => {
|
||||
describe("subtype user", () => {
|
||||
it("validate valid string id", async () => {
|
||||
const user = _.sample(users)
|
||||
const userId = user!._id!
|
||||
|
||||
const result = await config.doInTenant(() =>
|
||||
processInputBBReference(userId, BBReferenceFieldSubType.USER)
|
||||
)
|
||||
|
||||
expect(result).toEqual(userId)
|
||||
expect(cacheGetUserSpy).toHaveBeenCalledTimes(1)
|
||||
expect(cacheGetUserSpy).toHaveBeenCalledWith(userId)
|
||||
})
|
||||
|
||||
it("throws an error given an invalid id", async () => {
|
||||
const userId = generator.guid()
|
||||
|
||||
await expect(
|
||||
config.doInTenant(() =>
|
||||
processInputBBReference(userId, BBReferenceFieldSubType.USER)
|
||||
)
|
||||
).rejects.toThrow(
|
||||
new InvalidBBRefError(userId, BBReferenceFieldSubType.USER)
|
||||
)
|
||||
})
|
||||
|
||||
it("validate valid user object", async () => {
|
||||
const userId = _.sample(users)!._id!
|
||||
|
||||
const result = await config.doInTenant(() =>
|
||||
processInputBBReference({ _id: userId }, BBReferenceFieldSubType.USER)
|
||||
)
|
||||
|
||||
expect(result).toEqual(userId)
|
||||
expect(cacheGetUserSpy).toHaveBeenCalledTimes(1)
|
||||
expect(cacheGetUserSpy).toHaveBeenCalledWith(userId)
|
||||
})
|
||||
|
||||
it("empty strings will return null", async () => {
|
||||
const result = await config.doInTenant(() =>
|
||||
processInputBBReference("", BBReferenceFieldSubType.USER)
|
||||
)
|
||||
|
||||
expect(result).toEqual(null)
|
||||
})
|
||||
|
||||
it("should convert user medata IDs to global IDs", async () => {
|
||||
const userId = _.sample(users)!._id!
|
||||
const userMetadataId = backendCore.db.generateUserMetadataID(userId)
|
||||
const result = await config.doInTenant(() =>
|
||||
processInputBBReference(userMetadataId, BBReferenceFieldSubType.USER)
|
||||
)
|
||||
expect(result).toBe(userId)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe("processInputBBReferences", () => {
|
||||
describe("subtype user", () => {
|
||||
it("validate valid string id", async () => {
|
||||
|
@ -66,7 +130,7 @@ describe("bbReferenceProcessor", () => {
|
|||
processInputBBReferences(userId, BBReferenceFieldSubType.USER)
|
||||
)
|
||||
|
||||
expect(result).toEqual(userId)
|
||||
expect(result).toEqual([userId])
|
||||
expect(cacheGetUsersSpy).toHaveBeenCalledTimes(1)
|
||||
expect(cacheGetUsersSpy).toHaveBeenCalledWith([userId])
|
||||
})
|
||||
|
@ -93,7 +157,7 @@ describe("bbReferenceProcessor", () => {
|
|||
processInputBBReferences(userIdCsv, BBReferenceFieldSubType.USER)
|
||||
)
|
||||
|
||||
expect(result).toEqual(userIds.join(","))
|
||||
expect(result).toEqual(userIds)
|
||||
expect(cacheGetUsersSpy).toHaveBeenCalledTimes(1)
|
||||
expect(cacheGetUsersSpy).toHaveBeenCalledWith(userIds)
|
||||
})
|
||||
|
@ -117,36 +181,26 @@ describe("bbReferenceProcessor", () => {
|
|||
)
|
||||
})
|
||||
|
||||
it("validate valid user object", async () => {
|
||||
const userId = _.sample(users)!._id!
|
||||
|
||||
const result = await config.doInTenant(() =>
|
||||
processInputBBReferences(
|
||||
{ _id: userId },
|
||||
BBReferenceFieldSubType.USER
|
||||
)
|
||||
)
|
||||
|
||||
expect(result).toEqual(userId)
|
||||
expect(cacheGetUsersSpy).toHaveBeenCalledTimes(1)
|
||||
expect(cacheGetUsersSpy).toHaveBeenCalledWith([userId])
|
||||
})
|
||||
|
||||
it("validate valid user object array", async () => {
|
||||
const userIds = _.sampleSize(users, 3).map(x => x._id!)
|
||||
const inputUsers = _.sampleSize(users, 3).map(u => ({ _id: u._id! }))
|
||||
const userIds = inputUsers.map(u => u._id)
|
||||
|
||||
const result = await config.doInTenant(() =>
|
||||
processInputBBReferences(userIds, BBReferenceFieldSubType.USER)
|
||||
processInputBBReferences(inputUsers, BBReferenceFieldSubType.USER)
|
||||
)
|
||||
|
||||
expect(result).toEqual(userIds.join(","))
|
||||
expect(result).toEqual(userIds)
|
||||
expect(cacheGetUsersSpy).toHaveBeenCalledTimes(1)
|
||||
expect(cacheGetUsersSpy).toHaveBeenCalledWith(userIds)
|
||||
})
|
||||
|
||||
it("empty strings will return null", async () => {
|
||||
const result = await config.doInTenant(() =>
|
||||
processInputBBReferences("", BBReferenceFieldSubType.USER)
|
||||
processInputBBReferences(
|
||||
"",
|
||||
|
||||
BBReferenceFieldSubType.USER
|
||||
)
|
||||
)
|
||||
|
||||
expect(result).toEqual(null)
|
||||
|
@ -166,7 +220,42 @@ describe("bbReferenceProcessor", () => {
|
|||
const result = await config.doInTenant(() =>
|
||||
processInputBBReferences(userMetadataId, BBReferenceFieldSubType.USER)
|
||||
)
|
||||
expect(result).toBe(userId)
|
||||
expect(result).toEqual([userId])
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe("processOutputBBReference", () => {
|
||||
describe("subtype user", () => {
|
||||
it("fetches user given a valid string id", async () => {
|
||||
const user = _.sample(users)!
|
||||
const userId = user._id!
|
||||
|
||||
const result = await config.doInTenant(() =>
|
||||
processOutputBBReference(userId, BBReferenceFieldSubType.USER)
|
||||
)
|
||||
|
||||
expect(result).toEqual({
|
||||
_id: user._id,
|
||||
primaryDisplay: user.email,
|
||||
email: user.email,
|
||||
firstName: user.firstName,
|
||||
lastName: user.lastName,
|
||||
})
|
||||
expect(cacheGetUserSpy).toHaveBeenCalledTimes(1)
|
||||
expect(cacheGetUserSpy).toHaveBeenCalledWith(userId)
|
||||
})
|
||||
|
||||
it("returns undefined given an unexisting user", async () => {
|
||||
const userId = generator.guid()
|
||||
|
||||
const result = await config.doInTenant(() =>
|
||||
processOutputBBReference(userId, BBReferenceFieldSubType.USER)
|
||||
)
|
||||
|
||||
expect(result).toBeUndefined()
|
||||
expect(cacheGetUserSpy).toHaveBeenCalledTimes(1)
|
||||
expect(cacheGetUserSpy).toHaveBeenCalledWith(userId)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
@ -221,6 +310,46 @@ describe("bbReferenceProcessor", () => {
|
|||
expect(cacheGetUsersSpy).toHaveBeenCalledTimes(1)
|
||||
expect(cacheGetUsersSpy).toHaveBeenCalledWith([userId1, userId2])
|
||||
})
|
||||
|
||||
it("trims unexisting users user given a valid string id csv", async () => {
|
||||
const [user1, user2] = _.sampleSize(users, 2)
|
||||
const userId1 = user1._id!
|
||||
const userId2 = user2._id!
|
||||
|
||||
const unexistingUserId1 = generator.guid()
|
||||
const unexistingUserId2 = generator.guid()
|
||||
|
||||
const input = [
|
||||
unexistingUserId1,
|
||||
userId1,
|
||||
unexistingUserId2,
|
||||
userId2,
|
||||
].join(",")
|
||||
|
||||
const result = await config.doInTenant(() =>
|
||||
processOutputBBReferences(input, BBReferenceFieldSubType.USER)
|
||||
)
|
||||
|
||||
expect(result).toHaveLength(2)
|
||||
expect(result).toEqual(
|
||||
expect.arrayContaining(
|
||||
[user1, user2].map(u => ({
|
||||
_id: u._id,
|
||||
primaryDisplay: u.email,
|
||||
email: u.email,
|
||||
firstName: u.firstName,
|
||||
lastName: u.lastName,
|
||||
}))
|
||||
)
|
||||
)
|
||||
expect(cacheGetUsersSpy).toHaveBeenCalledTimes(1)
|
||||
expect(cacheGetUsersSpy).toHaveBeenCalledWith([
|
||||
unexistingUserId1,
|
||||
userId1,
|
||||
unexistingUserId2,
|
||||
userId2,
|
||||
])
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
@ -10,7 +10,9 @@ import {
|
|||
import * as bbReferenceProcessor from "../bbReferenceProcessor"
|
||||
|
||||
jest.mock("../bbReferenceProcessor", (): typeof bbReferenceProcessor => ({
|
||||
processInputBBReference: jest.fn(),
|
||||
processInputBBReferences: jest.fn(),
|
||||
processOutputBBReference: jest.fn(),
|
||||
processOutputBBReferences: jest.fn(),
|
||||
}))
|
||||
|
||||
|
@ -19,7 +21,64 @@ describe("rowProcessor - inputProcessing", () => {
|
|||
jest.resetAllMocks()
|
||||
})
|
||||
|
||||
it("processes BB references if on the schema and it's populated", async () => {
|
||||
const processInputBBReferenceMock =
|
||||
bbReferenceProcessor.processInputBBReference as jest.Mock
|
||||
const processInputBBReferencesMock =
|
||||
bbReferenceProcessor.processInputBBReferences as jest.Mock
|
||||
|
||||
it("processes single BB references if on the schema and it's populated", async () => {
|
||||
const userId = generator.guid()
|
||||
|
||||
const table: Table = {
|
||||
_id: generator.guid(),
|
||||
name: "TestTable",
|
||||
type: "table",
|
||||
sourceId: INTERNAL_TABLE_SOURCE_ID,
|
||||
sourceType: TableSourceType.INTERNAL,
|
||||
schema: {
|
||||
name: {
|
||||
type: FieldType.STRING,
|
||||
name: "name",
|
||||
constraints: {
|
||||
presence: true,
|
||||
type: "string",
|
||||
},
|
||||
},
|
||||
user: {
|
||||
type: FieldType.BB_REFERENCE_SINGLE,
|
||||
subtype: BBReferenceFieldSubType.USER,
|
||||
name: "user",
|
||||
constraints: {
|
||||
presence: true,
|
||||
type: "string",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
const newRow = {
|
||||
name: "Jack",
|
||||
user: "123",
|
||||
}
|
||||
|
||||
const user = structures.users.user()
|
||||
|
||||
processInputBBReferenceMock.mockResolvedValue(user)
|
||||
|
||||
const { row } = await inputProcessing(userId, table, newRow)
|
||||
|
||||
expect(bbReferenceProcessor.processInputBBReference).toHaveBeenCalledTimes(
|
||||
1
|
||||
)
|
||||
expect(bbReferenceProcessor.processInputBBReference).toHaveBeenCalledWith(
|
||||
"123",
|
||||
"user"
|
||||
)
|
||||
|
||||
expect(row).toEqual({ ...newRow, user })
|
||||
})
|
||||
|
||||
it("processes multiple BB references if on the schema and it's populated", async () => {
|
||||
const userId = generator.guid()
|
||||
|
||||
const table: Table = {
|
||||
|
@ -56,9 +115,7 @@ describe("rowProcessor - inputProcessing", () => {
|
|||
|
||||
const user = structures.users.user()
|
||||
|
||||
;(
|
||||
bbReferenceProcessor.processInputBBReferences as jest.Mock
|
||||
).mockResolvedValue(user)
|
||||
processInputBBReferencesMock.mockResolvedValue(user)
|
||||
|
||||
const { row } = await inputProcessing(userId, table, newRow)
|
||||
|
||||
|
|
|
@ -11,7 +11,9 @@ import { generator, structures } from "@budibase/backend-core/tests"
|
|||
import * as bbReferenceProcessor from "../bbReferenceProcessor"
|
||||
|
||||
jest.mock("../bbReferenceProcessor", (): typeof bbReferenceProcessor => ({
|
||||
processInputBBReference: jest.fn(),
|
||||
processInputBBReferences: jest.fn(),
|
||||
processOutputBBReference: jest.fn(),
|
||||
processOutputBBReferences: jest.fn(),
|
||||
}))
|
||||
|
||||
|
@ -20,10 +22,12 @@ describe("rowProcessor - outputProcessing", () => {
|
|||
jest.resetAllMocks()
|
||||
})
|
||||
|
||||
const processOutputBBReferenceMock =
|
||||
bbReferenceProcessor.processOutputBBReference as jest.Mock
|
||||
const processOutputBBReferencesMock =
|
||||
bbReferenceProcessor.processOutputBBReferences as jest.Mock
|
||||
|
||||
it("fetches bb user references given a populated field", async () => {
|
||||
it("fetches single user references given a populated field", async () => {
|
||||
const table: Table = {
|
||||
_id: generator.guid(),
|
||||
name: "TestTable",
|
||||
|
@ -40,7 +44,7 @@ describe("rowProcessor - outputProcessing", () => {
|
|||
},
|
||||
},
|
||||
user: {
|
||||
type: FieldType.BB_REFERENCE,
|
||||
type: FieldType.BB_REFERENCE_SINGLE,
|
||||
subtype: BBReferenceFieldSubType.USER,
|
||||
name: "user",
|
||||
constraints: {
|
||||
|
@ -57,12 +61,61 @@ describe("rowProcessor - outputProcessing", () => {
|
|||
}
|
||||
|
||||
const user = structures.users.user()
|
||||
processOutputBBReferencesMock.mockResolvedValue(user)
|
||||
processOutputBBReferenceMock.mockResolvedValue(user)
|
||||
|
||||
const result = await outputProcessing(table, row, { squash: false })
|
||||
|
||||
expect(result).toEqual({ name: "Jack", user })
|
||||
|
||||
expect(bbReferenceProcessor.processOutputBBReference).toHaveBeenCalledTimes(
|
||||
1
|
||||
)
|
||||
expect(bbReferenceProcessor.processOutputBBReference).toHaveBeenCalledWith(
|
||||
"123",
|
||||
BBReferenceFieldSubType.USER
|
||||
)
|
||||
})
|
||||
|
||||
it("fetches users references given a populated field", async () => {
|
||||
const table: Table = {
|
||||
_id: generator.guid(),
|
||||
name: "TestTable",
|
||||
type: "table",
|
||||
sourceId: INTERNAL_TABLE_SOURCE_ID,
|
||||
sourceType: TableSourceType.INTERNAL,
|
||||
schema: {
|
||||
name: {
|
||||
type: FieldType.STRING,
|
||||
name: "name",
|
||||
constraints: {
|
||||
presence: true,
|
||||
type: "string",
|
||||
},
|
||||
},
|
||||
users: {
|
||||
type: FieldType.BB_REFERENCE,
|
||||
subtype: BBReferenceFieldSubType.USER,
|
||||
name: "users",
|
||||
constraints: {
|
||||
presence: false,
|
||||
type: "string",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
const row = {
|
||||
name: "Jack",
|
||||
users: "123",
|
||||
}
|
||||
|
||||
const users = [structures.users.user()]
|
||||
processOutputBBReferencesMock.mockResolvedValue(users)
|
||||
|
||||
const result = await outputProcessing(table, row, { squash: false })
|
||||
|
||||
expect(result).toEqual({ name: "Jack", users })
|
||||
|
||||
expect(
|
||||
bbReferenceProcessor.processOutputBBReferences
|
||||
).toHaveBeenCalledTimes(1)
|
||||
|
|
|
@ -54,6 +54,7 @@ export function validate(rows: Rows, schema: TableSchema): ValidationResults {
|
|||
type: columnType,
|
||||
subtype: columnSubtype,
|
||||
autocolumn: isAutoColumn,
|
||||
constraints,
|
||||
} = schema[columnName] || {}
|
||||
|
||||
// If the column had an invalid value we don't want to override it
|
||||
|
@ -61,6 +62,12 @@ export function validate(rows: Rows, schema: TableSchema): ValidationResults {
|
|||
return
|
||||
}
|
||||
|
||||
const isRequired =
|
||||
!!constraints &&
|
||||
((typeof constraints.presence !== "boolean" &&
|
||||
!constraints.presence?.allowEmpty) ||
|
||||
constraints.presence === true)
|
||||
|
||||
// If the columnType is not a string, then it's not present in the schema, and should be added to the invalid columns array
|
||||
if (typeof columnType !== "string") {
|
||||
results.invalidColumns.push(columnName)
|
||||
|
@ -92,8 +99,9 @@ export function validate(rows: Rows, schema: TableSchema): ValidationResults {
|
|||
) {
|
||||
results.schemaValidation[columnName] = false
|
||||
} else if (
|
||||
columnType === FieldType.BB_REFERENCE &&
|
||||
!isValidBBReference(columnData, columnSubtype)
|
||||
(columnType === FieldType.BB_REFERENCE ||
|
||||
columnType === FieldType.BB_REFERENCE_SINGLE) &&
|
||||
!isValidBBReference(columnData, columnType, columnSubtype, isRequired)
|
||||
) {
|
||||
results.schemaValidation[columnName] = false
|
||||
} else {
|
||||
|
@ -121,7 +129,7 @@ export function parse(rows: Rows, schema: TableSchema): Rows {
|
|||
return
|
||||
}
|
||||
|
||||
const { type: columnType, subtype: columnSubtype } = schema[columnName]
|
||||
const { type: columnType } = schema[columnName]
|
||||
if (columnType === FieldType.NUMBER) {
|
||||
// If provided must be a valid number
|
||||
parsedRow[columnName] = columnData ? Number(columnData) : columnData
|
||||
|
@ -131,22 +139,16 @@ export function parse(rows: Rows, schema: TableSchema): Rows {
|
|||
? new Date(columnData).toISOString()
|
||||
: columnData
|
||||
} else if (columnType === FieldType.BB_REFERENCE) {
|
||||
const parsedValues =
|
||||
!!columnData && parseCsvExport<{ _id: string }[]>(columnData)
|
||||
if (!parsedValues) {
|
||||
parsedRow[columnName] = undefined
|
||||
} else {
|
||||
switch (columnSubtype) {
|
||||
case BBReferenceFieldSubType.USER:
|
||||
parsedRow[columnName] = parsedValues[0]?._id
|
||||
break
|
||||
case BBReferenceFieldSubType.USERS:
|
||||
parsedRow[columnName] = parsedValues.map(u => u._id)
|
||||
break
|
||||
default:
|
||||
utils.unreachable(columnSubtype)
|
||||
}
|
||||
let parsedValues: { _id: string }[] = columnData || []
|
||||
if (columnData) {
|
||||
parsedValues = parseCsvExport<{ _id: string }[]>(columnData)
|
||||
}
|
||||
|
||||
parsedRow[columnName] = parsedValues?.map(u => u._id)
|
||||
} else if (columnType === FieldType.BB_REFERENCE_SINGLE) {
|
||||
const parsedValue =
|
||||
columnData && parseCsvExport<{ _id: string }>(columnData)
|
||||
parsedRow[columnName] = parsedValue?._id
|
||||
} else if (
|
||||
(columnType === FieldType.ATTACHMENTS ||
|
||||
columnType === FieldType.ATTACHMENT_SINGLE) &&
|
||||
|
@ -163,33 +165,37 @@ export function parse(rows: Rows, schema: TableSchema): Rows {
|
|||
}
|
||||
|
||||
function isValidBBReference(
|
||||
columnData: any,
|
||||
columnSubtype: BBReferenceFieldSubType.USER | BBReferenceFieldSubType.USERS
|
||||
data: any,
|
||||
type: FieldType.BB_REFERENCE | FieldType.BB_REFERENCE_SINGLE,
|
||||
subtype: BBReferenceFieldSubType,
|
||||
isRequired: boolean
|
||||
): boolean {
|
||||
switch (columnSubtype) {
|
||||
if (typeof data !== "string") {
|
||||
return false
|
||||
}
|
||||
|
||||
if (type === FieldType.BB_REFERENCE_SINGLE) {
|
||||
if (!data) {
|
||||
return !isRequired
|
||||
}
|
||||
const user = parseCsvExport<{ _id: string }>(data)
|
||||
return db.isGlobalUserID(user._id)
|
||||
}
|
||||
|
||||
switch (subtype) {
|
||||
case BBReferenceFieldSubType.USER:
|
||||
case BBReferenceFieldSubType.USERS: {
|
||||
if (typeof columnData !== "string") {
|
||||
return false
|
||||
}
|
||||
const userArray = parseCsvExport<{ _id: string }[]>(columnData)
|
||||
const userArray = parseCsvExport<{ _id: string }[]>(data)
|
||||
if (!Array.isArray(userArray)) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (
|
||||
columnSubtype === BBReferenceFieldSubType.USER &&
|
||||
userArray.length > 1
|
||||
) {
|
||||
return false
|
||||
}
|
||||
|
||||
const constainsWrongId = userArray.find(
|
||||
user => !db.isGlobalUserID(user._id)
|
||||
)
|
||||
return !constainsWrongId
|
||||
}
|
||||
default:
|
||||
throw utils.unreachable(columnSubtype)
|
||||
throw utils.unreachable(subtype)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,10 +9,11 @@ import {
|
|||
SearchFilterOperator,
|
||||
SortDirection,
|
||||
SortType,
|
||||
FieldConstraints,
|
||||
} from "@budibase/types"
|
||||
import dayjs from "dayjs"
|
||||
import { OperatorOptions, SqlNumberTypeRangeMap } from "./constants"
|
||||
import { deepGet } from "./helpers"
|
||||
import { deepGet, schema } from "./helpers"
|
||||
|
||||
const HBS_REGEX = /{{([^{].*?)}}/g
|
||||
|
||||
|
@ -24,9 +25,10 @@ export const getValidOperatorsForType = (
|
|||
type: FieldType
|
||||
subtype?: BBReferenceFieldSubType
|
||||
formulaType?: FormulaType
|
||||
constraints?: FieldConstraints
|
||||
},
|
||||
field: string,
|
||||
datasource: Datasource & { tableId: any }
|
||||
field?: string,
|
||||
datasource?: Datasource & { tableId: any }
|
||||
) => {
|
||||
const Op = OperatorOptions
|
||||
const stringOps = [
|
||||
|
@ -51,7 +53,7 @@ export const getValidOperatorsForType = (
|
|||
value: string
|
||||
label: string
|
||||
}[] = []
|
||||
const { type, subtype, formulaType } = fieldType
|
||||
const { type, formulaType } = fieldType
|
||||
if (type === FieldType.STRING) {
|
||||
ops = stringOps
|
||||
} else if (type === FieldType.NUMBER || type === FieldType.BIGINT) {
|
||||
|
@ -69,14 +71,11 @@ export const getValidOperatorsForType = (
|
|||
} else if (type === FieldType.FORMULA && formulaType === FormulaType.STATIC) {
|
||||
ops = stringOps.concat([Op.MoreThan, Op.LessThan])
|
||||
} else if (
|
||||
type === FieldType.BB_REFERENCE &&
|
||||
subtype == BBReferenceFieldSubType.USER
|
||||
type === FieldType.BB_REFERENCE_SINGLE ||
|
||||
schema.isDeprecatedSingleUserColumn(fieldType)
|
||||
) {
|
||||
ops = [Op.Equals, Op.NotEquals, Op.Empty, Op.NotEmpty, Op.In]
|
||||
} else if (
|
||||
type === FieldType.BB_REFERENCE &&
|
||||
subtype == BBReferenceFieldSubType.USERS
|
||||
) {
|
||||
} else if (type === FieldType.BB_REFERENCE) {
|
||||
ops = [Op.Contains, Op.NotContains, Op.ContainsAny, Op.Empty, Op.NotEmpty]
|
||||
}
|
||||
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
export * from "./helpers"
|
||||
export * from "./integrations"
|
||||
export * as cron from "./cron"
|
||||
export * as schema from "./schema"
|
||||
|
|
|
@ -0,0 +1,15 @@
|
|||
import {
|
||||
BBReferenceFieldSubType,
|
||||
FieldSchema,
|
||||
FieldType,
|
||||
} from "@budibase/types"
|
||||
|
||||
export function isDeprecatedSingleUserColumn(
|
||||
schema: Pick<FieldSchema, "type" | "subtype" | "constraints">
|
||||
) {
|
||||
const result =
|
||||
schema.type === FieldType.BB_REFERENCE &&
|
||||
schema.subtype === BBReferenceFieldSubType.USER &&
|
||||
schema.constraints?.type !== "array"
|
||||
return result
|
||||
}
|
|
@ -18,6 +18,7 @@ const allowDisplayColumnByType: Record<FieldType, boolean> = {
|
|||
[FieldType.LINK]: false,
|
||||
[FieldType.JSON]: false,
|
||||
[FieldType.BB_REFERENCE]: false,
|
||||
[FieldType.BB_REFERENCE_SINGLE]: false,
|
||||
}
|
||||
|
||||
const allowSortColumnByType: Record<FieldType, boolean> = {
|
||||
|
@ -39,6 +40,7 @@ const allowSortColumnByType: Record<FieldType, boolean> = {
|
|||
[FieldType.ARRAY]: false,
|
||||
[FieldType.LINK]: false,
|
||||
[FieldType.BB_REFERENCE]: false,
|
||||
[FieldType.BB_REFERENCE_SINGLE]: false,
|
||||
}
|
||||
|
||||
export function canBeDisplayColumn(type: FieldType): boolean {
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
import {
|
||||
FieldSchema,
|
||||
Row,
|
||||
Table,
|
||||
TableRequest,
|
||||
|
@ -31,8 +30,8 @@ export interface BulkImportResponse {
|
|||
}
|
||||
|
||||
export interface MigrateRequest {
|
||||
oldColumn: FieldSchema
|
||||
newColumn: FieldSchema
|
||||
oldColumn: string
|
||||
newColumn: string
|
||||
}
|
||||
|
||||
export interface MigrateResponse {
|
||||
|
|
|
@ -100,13 +100,17 @@ export enum FieldType {
|
|||
*/
|
||||
BIGINT = "bigint",
|
||||
/**
|
||||
* a JSON type, called User within Budibase. This type is used to represent a link to an internal Budibase
|
||||
* a JSON type, called Users within Budibase. It will hold an array of strings. This type is used to represent a link to an internal Budibase
|
||||
* resource, like a user or group, today only users are supported. This type will be represented as an
|
||||
* array of internal resource IDs (e.g. user IDs) within the row - this ID list will be enriched with
|
||||
* the full resources when rows are returned from the API. The full resources can be input to the API, or
|
||||
* an array of resource IDs, the API will squash these down and validate them before saving the row.
|
||||
*/
|
||||
BB_REFERENCE = "bb_reference",
|
||||
/**
|
||||
* a string type, called User within Budibase. Same logic as `bb_reference`, storing a single id as string instead of an array
|
||||
*/
|
||||
BB_REFERENCE_SINGLE = "bb_reference_single",
|
||||
}
|
||||
|
||||
export interface RowAttachment {
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
import { FieldType } from "../row"
|
||||
|
||||
export enum RelationshipType {
|
||||
ONE_TO_MANY = "one-to-many",
|
||||
MANY_TO_ONE = "many-to-one",
|
||||
|
@ -27,5 +29,21 @@ export enum FormulaType {
|
|||
|
||||
export enum BBReferenceFieldSubType {
|
||||
USER = "user",
|
||||
/** @deprecated this should not be used anymore, left here in order to support the existing usages */
|
||||
USERS = "users",
|
||||
}
|
||||
|
||||
export type SupportedSqlTypes =
|
||||
| FieldType.STRING
|
||||
| FieldType.BARCODEQR
|
||||
| FieldType.LONGFORM
|
||||
| FieldType.OPTIONS
|
||||
| FieldType.DATETIME
|
||||
| FieldType.NUMBER
|
||||
| FieldType.BOOLEAN
|
||||
| FieldType.FORMULA
|
||||
| FieldType.BIGINT
|
||||
| FieldType.BB_REFERENCE
|
||||
| FieldType.BB_REFERENCE_SINGLE
|
||||
| FieldType.LINK
|
||||
| FieldType.ARRAY
|
||||
|
|
|
@ -110,9 +110,14 @@ export interface FormulaFieldMetadata extends BaseFieldSchema {
|
|||
export interface BBReferenceFieldMetadata
|
||||
extends Omit<BaseFieldSchema, "subtype"> {
|
||||
type: FieldType.BB_REFERENCE
|
||||
subtype: BBReferenceFieldSubType.USER | BBReferenceFieldSubType.USERS
|
||||
subtype: BBReferenceFieldSubType
|
||||
relationshipType?: RelationshipType
|
||||
}
|
||||
export interface BBReferenceSingleFieldMetadata
|
||||
extends Omit<BaseFieldSchema, "subtype"> {
|
||||
type: FieldType.BB_REFERENCE_SINGLE
|
||||
subtype: Exclude<BBReferenceFieldSubType, BBReferenceFieldSubType.USERS>
|
||||
}
|
||||
|
||||
export interface AttachmentFieldMetadata extends BaseFieldSchema {
|
||||
type: FieldType.ATTACHMENTS
|
||||
|
@ -164,6 +169,7 @@ interface OtherFieldMetadata extends BaseFieldSchema {
|
|||
| FieldType.NUMBER
|
||||
| FieldType.LONGFORM
|
||||
| FieldType.BB_REFERENCE
|
||||
| FieldType.BB_REFERENCE_SINGLE
|
||||
| FieldType.ATTACHMENTS
|
||||
>
|
||||
}
|
||||
|
@ -179,6 +185,7 @@ export type FieldSchema =
|
|||
| BBReferenceFieldMetadata
|
||||
| JsonFieldMetadata
|
||||
| AttachmentFieldMetadata
|
||||
| BBReferenceSingleFieldMetadata
|
||||
|
||||
export interface TableSchema {
|
||||
[key: string]: FieldSchema
|
||||
|
@ -207,15 +214,3 @@ export function isManyToOne(
|
|||
): field is ManyToOneRelationshipFieldMetadata {
|
||||
return field.relationshipType === RelationshipType.MANY_TO_ONE
|
||||
}
|
||||
|
||||
export function isBBReferenceField(
|
||||
field: FieldSchema
|
||||
): field is BBReferenceFieldMetadata {
|
||||
return field.type === FieldType.BB_REFERENCE
|
||||
}
|
||||
|
||||
export function isAttachmentField(
|
||||
field: FieldSchema
|
||||
): field is AttachmentFieldMetadata {
|
||||
return field.type === FieldType.ATTACHMENTS
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue