Merge branch 'master' into prefill-cards
This commit is contained in:
commit
8d5d459c13
|
@ -54,7 +54,10 @@
|
||||||
"ignoreRestSiblings": true
|
"ignoreRestSiblings": true
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"local-rules/no-budibase-imports": "error"
|
"no-redeclare": "off",
|
||||||
|
"@typescript-eslint/no-redeclare": "error",
|
||||||
|
// have to turn this off to allow function overloading in typescript
|
||||||
|
"no-dupe-class-members": "off"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -87,7 +90,9 @@
|
||||||
"jest/expect-expect": "off",
|
"jest/expect-expect": "off",
|
||||||
// We do this in some tests where the behaviour of internal tables
|
// We do this in some tests where the behaviour of internal tables
|
||||||
// differs to external, but the API is broadly the same
|
// differs to external, but the API is broadly the same
|
||||||
"jest/no-conditional-expect": "off"
|
"jest/no-conditional-expect": "off",
|
||||||
|
// have to turn this off to allow function overloading in typescript
|
||||||
|
"no-dupe-class-members": "off"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
|
@ -8,4 +8,5 @@ Contributors
|
||||||
* Andrew Kingston - [@aptkingston](https://github.com/aptkingston)
|
* Andrew Kingston - [@aptkingston](https://github.com/aptkingston)
|
||||||
* Michael Drury - [@mike12345567](https://github.com/mike12345567)
|
* Michael Drury - [@mike12345567](https://github.com/mike12345567)
|
||||||
* Peter Clement - [@PClmnt](https://github.com/PClmnt)
|
* Peter Clement - [@PClmnt](https://github.com/PClmnt)
|
||||||
* Rory Powell - [@Rory-Powell](https://github.com/Rory-Powell)
|
* Rory Powell - [@Rory-Powell](https://github.com/Rory-Powell)
|
||||||
|
* Michaël St-Georges [@CSLTech](https://github.com/CSLTech)
|
|
@ -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.4",
|
||||||
"npmClient": "yarn",
|
"npmClient": "yarn",
|
||||||
"packages": [
|
"packages": [
|
||||||
"packages/*",
|
"packages/*",
|
||||||
|
|
|
@ -73,7 +73,6 @@
|
||||||
"chance": "1.1.8",
|
"chance": "1.1.8",
|
||||||
"ioredis-mock": "8.9.0",
|
"ioredis-mock": "8.9.0",
|
||||||
"jest": "29.7.0",
|
"jest": "29.7.0",
|
||||||
"jest-environment-node": "29.7.0",
|
|
||||||
"jest-serial-runner": "1.2.1",
|
"jest-serial-runner": "1.2.1",
|
||||||
"pino-pretty": "10.0.0",
|
"pino-pretty": "10.0.0",
|
||||||
"pouchdb-adapter-memory": "7.2.2",
|
"pouchdb-adapter-memory": "7.2.2",
|
||||||
|
|
|
@ -69,7 +69,7 @@ async function populateUsersFromDB(
|
||||||
export async function getUser(
|
export async function getUser(
|
||||||
userId: string,
|
userId: string,
|
||||||
tenantId?: string,
|
tenantId?: string,
|
||||||
populateUser?: any
|
populateUser?: (userId: string, tenantId: string) => Promise<User>
|
||||||
) {
|
) {
|
||||||
if (!populateUser) {
|
if (!populateUser) {
|
||||||
populateUser = populateFromDB
|
populateUser = populateFromDB
|
||||||
|
@ -83,7 +83,7 @@ export async function getUser(
|
||||||
}
|
}
|
||||||
const client = await redis.getUserClient()
|
const client = await redis.getUserClient()
|
||||||
// try cache
|
// try cache
|
||||||
let user = await client.get(userId)
|
let user: User = await client.get(userId)
|
||||||
if (!user) {
|
if (!user) {
|
||||||
user = await populateUser(userId, tenantId)
|
user = await populateUser(userId, tenantId)
|
||||||
await client.store(userId, user, EXPIRY_SECONDS)
|
await client.store(userId, user, EXPIRY_SECONDS)
|
||||||
|
|
|
@ -1,4 +1,6 @@
|
||||||
import { IdentityContext, Snippet, VM } from "@budibase/types"
|
import { IdentityContext, Snippet, VM } from "@budibase/types"
|
||||||
|
import { OAuth2Client } from "google-auth-library"
|
||||||
|
import { GoogleSpreadsheet } from "google-spreadsheet"
|
||||||
|
|
||||||
// keep this out of Budibase types, don't want to expose context info
|
// keep this out of Budibase types, don't want to expose context info
|
||||||
export type ContextMap = {
|
export type ContextMap = {
|
||||||
|
@ -12,4 +14,8 @@ export type ContextMap = {
|
||||||
vm?: VM
|
vm?: VM
|
||||||
cleanup?: (() => void | Promise<void>)[]
|
cleanup?: (() => void | Promise<void>)[]
|
||||||
snippets?: Snippet[]
|
snippets?: Snippet[]
|
||||||
|
googleSheets?: {
|
||||||
|
oauthClient: OAuth2Client
|
||||||
|
clients: Record<string, GoogleSpreadsheet>
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,14 +1,31 @@
|
||||||
import PouchDB from "pouchdb"
|
import PouchDB from "pouchdb"
|
||||||
import { getPouchDB, closePouchDB } from "./couch"
|
import { getPouchDB, closePouchDB } from "./couch"
|
||||||
import { DocumentType } from "../constants"
|
import { DocumentType } from "@budibase/types"
|
||||||
|
|
||||||
|
enum ReplicationDirection {
|
||||||
|
TO_PRODUCTION = "toProduction",
|
||||||
|
TO_DEV = "toDev",
|
||||||
|
}
|
||||||
|
|
||||||
class Replication {
|
class Replication {
|
||||||
source: PouchDB.Database
|
source: PouchDB.Database
|
||||||
target: PouchDB.Database
|
target: PouchDB.Database
|
||||||
|
direction: ReplicationDirection | undefined
|
||||||
|
|
||||||
constructor({ source, target }: { source: string; target: string }) {
|
constructor({ source, target }: { source: string; target: string }) {
|
||||||
this.source = getPouchDB(source)
|
this.source = getPouchDB(source)
|
||||||
this.target = getPouchDB(target)
|
this.target = getPouchDB(target)
|
||||||
|
if (
|
||||||
|
source.startsWith(DocumentType.APP_DEV) &&
|
||||||
|
target.startsWith(DocumentType.APP)
|
||||||
|
) {
|
||||||
|
this.direction = ReplicationDirection.TO_PRODUCTION
|
||||||
|
} else if (
|
||||||
|
source.startsWith(DocumentType.APP) &&
|
||||||
|
target.startsWith(DocumentType.APP_DEV)
|
||||||
|
) {
|
||||||
|
this.direction = ReplicationDirection.TO_DEV
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async close() {
|
async close() {
|
||||||
|
@ -40,12 +57,18 @@ class Replication {
|
||||||
}
|
}
|
||||||
|
|
||||||
const filter = opts.filter
|
const filter = opts.filter
|
||||||
|
const direction = this.direction
|
||||||
|
const toDev = direction === ReplicationDirection.TO_DEV
|
||||||
delete opts.filter
|
delete opts.filter
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...opts,
|
...opts,
|
||||||
filter: (doc: any, params: any) => {
|
filter: (doc: any, params: any) => {
|
||||||
if (doc._id && doc._id.startsWith(DocumentType.AUTOMATION_LOG)) {
|
// don't sync design documents
|
||||||
|
if (toDev && doc._id?.startsWith("_design")) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if (doc._id?.startsWith(DocumentType.AUTOMATION_LOG)) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
if (doc._id === DocumentType.APP_METADATA) {
|
if (doc._id === DocumentType.APP_METADATA) {
|
||||||
|
|
|
@ -12,6 +12,7 @@ import {
|
||||||
isDocument,
|
isDocument,
|
||||||
RowResponse,
|
RowResponse,
|
||||||
RowValue,
|
RowValue,
|
||||||
|
SQLiteDefinition,
|
||||||
SqlQueryBinding,
|
SqlQueryBinding,
|
||||||
} from "@budibase/types"
|
} from "@budibase/types"
|
||||||
import { getCouchInfo } from "./connections"
|
import { getCouchInfo } from "./connections"
|
||||||
|
@ -21,6 +22,8 @@ import { ReadStream, WriteStream } from "fs"
|
||||||
import { newid } from "../../docIds/newid"
|
import { newid } from "../../docIds/newid"
|
||||||
import { SQLITE_DESIGN_DOC_ID } from "../../constants"
|
import { SQLITE_DESIGN_DOC_ID } from "../../constants"
|
||||||
import { DDInstrumentedDatabase } from "../instrumentation"
|
import { DDInstrumentedDatabase } from "../instrumentation"
|
||||||
|
import { checkSlashesInUrl } from "../../helpers"
|
||||||
|
import env from "../../environment"
|
||||||
|
|
||||||
const DATABASE_NOT_FOUND = "Database does not exist."
|
const DATABASE_NOT_FOUND = "Database does not exist."
|
||||||
|
|
||||||
|
@ -281,25 +284,61 @@ export class DatabaseImpl implements Database {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async _sqlQuery<T>(
|
||||||
|
url: string,
|
||||||
|
method: "POST" | "GET",
|
||||||
|
body?: Record<string, any>
|
||||||
|
): Promise<T> {
|
||||||
|
url = checkSlashesInUrl(`${this.couchInfo.sqlUrl}/${url}`)
|
||||||
|
const args: { url: string; method: string; cookie: string; body?: any } = {
|
||||||
|
url,
|
||||||
|
method,
|
||||||
|
cookie: this.couchInfo.cookie,
|
||||||
|
}
|
||||||
|
if (body) {
|
||||||
|
args.body = body
|
||||||
|
}
|
||||||
|
return this.performCall(() => {
|
||||||
|
return async () => {
|
||||||
|
const response = await directCouchUrlCall(args)
|
||||||
|
const json = await response.json()
|
||||||
|
if (response.status > 300) {
|
||||||
|
throw json
|
||||||
|
}
|
||||||
|
return json as T
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
async sql<T extends Document>(
|
async sql<T extends Document>(
|
||||||
sql: string,
|
sql: string,
|
||||||
parameters?: SqlQueryBinding
|
parameters?: SqlQueryBinding
|
||||||
): Promise<T[]> {
|
): Promise<T[]> {
|
||||||
const dbName = this.name
|
const dbName = this.name
|
||||||
const url = `/${dbName}/${SQLITE_DESIGN_DOC_ID}`
|
const url = `/${dbName}/${SQLITE_DESIGN_DOC_ID}`
|
||||||
const response = await directCouchUrlCall({
|
return await this._sqlQuery<T[]>(url, "POST", {
|
||||||
url: `${this.couchInfo.sqlUrl}/${url}`,
|
query: sql,
|
||||||
method: "POST",
|
args: parameters,
|
||||||
cookie: this.couchInfo.cookie,
|
|
||||||
body: {
|
|
||||||
query: sql,
|
|
||||||
args: parameters,
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
if (response.status > 300) {
|
}
|
||||||
throw new Error(await response.text())
|
|
||||||
|
// checks design document is accurate (cleans up tables)
|
||||||
|
// this will check the design document and remove anything from
|
||||||
|
// disk which is not supposed to be there
|
||||||
|
async sqlDiskCleanup(): Promise<void> {
|
||||||
|
const dbName = this.name
|
||||||
|
const url = `/${dbName}/_cleanup`
|
||||||
|
return await this._sqlQuery<void>(url, "POST")
|
||||||
|
}
|
||||||
|
|
||||||
|
// removes a document from sqlite
|
||||||
|
async sqlPurgeDocument(docIds: string[] | string): Promise<void> {
|
||||||
|
if (!Array.isArray(docIds)) {
|
||||||
|
docIds = [docIds]
|
||||||
}
|
}
|
||||||
return (await response.json()) as T[]
|
const dbName = this.name
|
||||||
|
const url = `/${dbName}/_purge`
|
||||||
|
return await this._sqlQuery<void>(url, "POST", { docs: docIds })
|
||||||
}
|
}
|
||||||
|
|
||||||
async query<T extends Document>(
|
async query<T extends Document>(
|
||||||
|
@ -314,6 +353,17 @@ export class DatabaseImpl implements Database {
|
||||||
|
|
||||||
async destroy() {
|
async destroy() {
|
||||||
try {
|
try {
|
||||||
|
if (env.SQS_SEARCH_ENABLE) {
|
||||||
|
// delete the design document, then run the cleanup operation
|
||||||
|
try {
|
||||||
|
const definition = await this.get<SQLiteDefinition>(
|
||||||
|
SQLITE_DESIGN_DOC_ID
|
||||||
|
)
|
||||||
|
await this.remove(SQLITE_DESIGN_DOC_ID, definition._rev)
|
||||||
|
} finally {
|
||||||
|
await this.sqlDiskCleanup()
|
||||||
|
}
|
||||||
|
}
|
||||||
return await this.nano().db.destroy(this.name)
|
return await this.nano().db.destroy(this.name)
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
// didn't exist, don't worry
|
// didn't exist, don't worry
|
||||||
|
|
|
@ -21,7 +21,7 @@ export async function directCouchUrlCall({
|
||||||
url: string
|
url: string
|
||||||
cookie: string
|
cookie: string
|
||||||
method: string
|
method: string
|
||||||
body?: any
|
body?: Record<string, any>
|
||||||
}) {
|
}) {
|
||||||
const params: any = {
|
const params: any = {
|
||||||
method: method,
|
method: method,
|
||||||
|
|
|
@ -56,12 +56,17 @@ export class DDInstrumentedDatabase implements Database {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
remove(idOrDoc: Document): Promise<DocumentDestroyResponse>
|
||||||
|
remove(idOrDoc: string, rev?: string): Promise<DocumentDestroyResponse>
|
||||||
remove(
|
remove(
|
||||||
id: string | Document,
|
idOrDoc: string | Document,
|
||||||
rev?: string | undefined
|
rev?: string
|
||||||
): Promise<DocumentDestroyResponse> {
|
): Promise<DocumentDestroyResponse> {
|
||||||
return tracer.trace("db.remove", span => {
|
return tracer.trace("db.remove", span => {
|
||||||
span?.addTags({ db_name: this.name, doc_id: id })
|
span?.addTags({ db_name: this.name, doc_id: idOrDoc })
|
||||||
|
const isDocument = typeof idOrDoc === "object"
|
||||||
|
const id = isDocument ? idOrDoc._id! : idOrDoc
|
||||||
|
rev = isDocument ? idOrDoc._rev : rev
|
||||||
return this.db.remove(id, rev)
|
return this.db.remove(id, rev)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -160,4 +165,18 @@ export class DDInstrumentedDatabase implements Database {
|
||||||
return this.db.sql(sql, parameters)
|
return this.db.sql(sql, parameters)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
sqlPurgeDocument(docIds: string[] | string): Promise<void> {
|
||||||
|
return tracer.trace("db.sqlPurgeDocument", span => {
|
||||||
|
span?.addTags({ db_name: this.name })
|
||||||
|
return this.db.sqlPurgeDocument(docIds)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
sqlDiskCleanup(): Promise<void> {
|
||||||
|
return tracer.trace("db.sqlDiskCleanup", span => {
|
||||||
|
span?.addTags({ db_name: this.name })
|
||||||
|
return this.db.sqlDiskCleanup()
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -109,6 +109,7 @@ const environment = {
|
||||||
API_ENCRYPTION_KEY: getAPIEncryptionKey(),
|
API_ENCRYPTION_KEY: getAPIEncryptionKey(),
|
||||||
COUCH_DB_URL: process.env.COUCH_DB_URL || "http://localhost:4005",
|
COUCH_DB_URL: process.env.COUCH_DB_URL || "http://localhost:4005",
|
||||||
COUCH_DB_SQL_URL: process.env.COUCH_DB_SQL_URL || "http://localhost:4006",
|
COUCH_DB_SQL_URL: process.env.COUCH_DB_SQL_URL || "http://localhost:4006",
|
||||||
|
SQS_SEARCH_ENABLE: process.env.SQS_SEARCH_ENABLE,
|
||||||
COUCH_DB_USERNAME: process.env.COUCH_DB_USER,
|
COUCH_DB_USERNAME: process.env.COUCH_DB_USER,
|
||||||
COUCH_DB_PASSWORD: process.env.COUCH_DB_PASSWORD,
|
COUCH_DB_PASSWORD: process.env.COUCH_DB_PASSWORD,
|
||||||
GOOGLE_CLIENT_ID: process.env.GOOGLE_CLIENT_ID,
|
GOOGLE_CLIENT_ID: process.env.GOOGLE_CLIENT_ID,
|
||||||
|
|
|
@ -13,7 +13,7 @@ import { getGlobalDB, doInTenant } from "../context"
|
||||||
import { decrypt } from "../security/encryption"
|
import { decrypt } from "../security/encryption"
|
||||||
import * as identity from "../context/identity"
|
import * as identity from "../context/identity"
|
||||||
import env from "../environment"
|
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 { InvalidAPIKeyError, ErrorCode } from "../errors"
|
||||||
import tracer from "dd-trace"
|
import tracer from "dd-trace"
|
||||||
|
|
||||||
|
@ -41,7 +41,10 @@ function finalise(ctx: any, opts: FinaliseOpts = {}) {
|
||||||
ctx.version = opts.version
|
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
|
// check both the primary and the fallback internal api keys
|
||||||
// this allows for rotation
|
// this allows for rotation
|
||||||
if (isValidInternalAPIKey(apiKey)) {
|
if (isValidInternalAPIKey(apiKey)) {
|
||||||
|
@ -128,6 +131,7 @@ export default function (
|
||||||
} else {
|
} else {
|
||||||
user = await getUser(userId, session.tenantId)
|
user = await getUser(userId, session.tenantId)
|
||||||
}
|
}
|
||||||
|
// @ts-ignore
|
||||||
user.csrfToken = session.csrfToken
|
user.csrfToken = session.csrfToken
|
||||||
|
|
||||||
if (session?.lastAccessedAt < timeMinusOneMinute()) {
|
if (session?.lastAccessedAt < timeMinusOneMinute()) {
|
||||||
|
@ -167,19 +171,25 @@ export default function (
|
||||||
authenticated = false
|
authenticated = false
|
||||||
}
|
}
|
||||||
|
|
||||||
if (user) {
|
const isUser = (
|
||||||
|
user: any
|
||||||
|
): user is User & { budibaseAccess?: string } => {
|
||||||
|
return user && user.email
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isUser(user)) {
|
||||||
tracer.setUser({
|
tracer.setUser({
|
||||||
id: user?._id,
|
id: user._id!,
|
||||||
tenantId: user?.tenantId,
|
tenantId: user.tenantId,
|
||||||
budibaseAccess: user?.budibaseAccess,
|
budibaseAccess: user.budibaseAccess,
|
||||||
status: user?.status,
|
status: user.status,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// isAuthenticated is a function, so use a variable to be able to check authed state
|
// isAuthenticated is a function, so use a variable to be able to check authed state
|
||||||
finalise(ctx, { authenticated, user, internal, version, publicEndpoint })
|
finalise(ctx, { authenticated, user, internal, version, publicEndpoint })
|
||||||
|
|
||||||
if (user && user.email) {
|
if (isUser(user)) {
|
||||||
return identity.doInUserContext(user, ctx, next)
|
return identity.doInUserContext(user, ctx, next)
|
||||||
} else {
|
} else {
|
||||||
return next()
|
return next()
|
||||||
|
|
|
@ -9,6 +9,9 @@ import {
|
||||||
AutomationAttachmentContent,
|
AutomationAttachmentContent,
|
||||||
BucketedContent,
|
BucketedContent,
|
||||||
} from "@budibase/types"
|
} from "@budibase/types"
|
||||||
|
import stream from "stream"
|
||||||
|
import streamWeb from "node:stream/web"
|
||||||
|
|
||||||
/****************************************************
|
/****************************************************
|
||||||
* NOTE: When adding a new bucket - name *
|
* NOTE: When adding a new bucket - name *
|
||||||
* sure that S3 usages (like budibase-infra) *
|
* sure that S3 usages (like budibase-infra) *
|
||||||
|
@ -53,12 +56,10 @@ export const bucketTTLConfig = (
|
||||||
Rules: [lifecycleRule],
|
Rules: [lifecycleRule],
|
||||||
}
|
}
|
||||||
|
|
||||||
const params = {
|
return {
|
||||||
Bucket: bucketName,
|
Bucket: bucketName,
|
||||||
LifecycleConfiguration: lifecycleConfiguration,
|
LifecycleConfiguration: lifecycleConfiguration,
|
||||||
}
|
}
|
||||||
|
|
||||||
return params
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function processUrlAttachment(
|
async function processUrlAttachment(
|
||||||
|
@ -69,9 +70,12 @@ async function processUrlAttachment(
|
||||||
throw new Error(`Unexpected response ${response.statusText}`)
|
throw new Error(`Unexpected response ${response.statusText}`)
|
||||||
}
|
}
|
||||||
const fallbackFilename = path.basename(new URL(attachment.url).pathname)
|
const fallbackFilename = path.basename(new URL(attachment.url).pathname)
|
||||||
|
if (!response.body) {
|
||||||
|
throw new Error("No response received for attachment")
|
||||||
|
}
|
||||||
return {
|
return {
|
||||||
filename: attachment.filename || fallbackFilename,
|
filename: attachment.filename || fallbackFilename,
|
||||||
content: response.body,
|
content: stream.Readable.fromWeb(response.body as streamWeb.ReadableStream),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -492,7 +492,7 @@ export class UserDB {
|
||||||
|
|
||||||
await platform.users.removeUser(dbUser)
|
await platform.users.removeUser(dbUser)
|
||||||
|
|
||||||
await db.remove(userId, dbUser._rev)
|
await db.remove(userId, dbUser._rev!)
|
||||||
|
|
||||||
const creatorsToDelete = (await isCreator(dbUser)) ? 1 : 0
|
const creatorsToDelete = (await isCreator(dbUser)) ? 1 : 0
|
||||||
await UserDB.quotas.removeUsers(1, creatorsToDelete)
|
await UserDB.quotas.removeUsers(1, creatorsToDelete)
|
||||||
|
|
|
@ -155,6 +155,8 @@ export default function positionDropdown(element, opts) {
|
||||||
applyXStrategy(Strategies.StartToEnd)
|
applyXStrategy(Strategies.StartToEnd)
|
||||||
} else if (align === "left-outside") {
|
} else if (align === "left-outside") {
|
||||||
applyXStrategy(Strategies.EndToStart)
|
applyXStrategy(Strategies.EndToStart)
|
||||||
|
} else if (align === "center") {
|
||||||
|
applyXStrategy(Strategies.MidPoint)
|
||||||
} else {
|
} else {
|
||||||
applyXStrategy(Strategies.StartToStart)
|
applyXStrategy(Strategies.StartToStart)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,40 +1,28 @@
|
||||||
<script>
|
<script>
|
||||||
import { getContext, onMount, createEventDispatcher } from "svelte"
|
import { getContext, onDestroy } from "svelte"
|
||||||
import Portal from "svelte-portal"
|
import Portal from "svelte-portal"
|
||||||
|
|
||||||
export let title
|
export let title
|
||||||
export let icon = ""
|
export let icon = ""
|
||||||
export let id
|
export let id
|
||||||
|
|
||||||
const dispatch = createEventDispatcher()
|
|
||||||
let selected = getContext("tab")
|
let selected = getContext("tab")
|
||||||
let tab_internal
|
let observer
|
||||||
let tabInfo
|
let ref
|
||||||
|
|
||||||
const setTabInfo = () => {
|
$: isSelected = $selected.title === title
|
||||||
// If the tabs are being rendered inside a component which uses
|
$: {
|
||||||
// a svelte transition to enter, then this initial getBoundingClientRect
|
if (isSelected && ref) {
|
||||||
// will return an incorrect position.
|
observe()
|
||||||
// We just need to get this off the main thread to fix this, by using
|
} else {
|
||||||
// a 0ms timeout.
|
stopObserving()
|
||||||
setTimeout(() => {
|
}
|
||||||
tabInfo = tab_internal?.getBoundingClientRect()
|
|
||||||
if (tabInfo && $selected.title === title) {
|
|
||||||
$selected.info = tabInfo
|
|
||||||
}
|
|
||||||
}, 0)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onMount(() => {
|
const setTabInfo = () => {
|
||||||
setTabInfo()
|
const tabInfo = ref?.getBoundingClientRect()
|
||||||
})
|
if (tabInfo) {
|
||||||
|
$selected.info = tabInfo
|
||||||
//Ensure that the underline is in the correct location
|
|
||||||
$: {
|
|
||||||
if ($selected.title === title && tab_internal) {
|
|
||||||
if ($selected.info?.left !== tab_internal.getBoundingClientRect().left) {
|
|
||||||
setTabInfo()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -42,10 +30,25 @@
|
||||||
$selected = {
|
$selected = {
|
||||||
...$selected,
|
...$selected,
|
||||||
title,
|
title,
|
||||||
info: tab_internal.getBoundingClientRect(),
|
info: ref.getBoundingClientRect(),
|
||||||
}
|
}
|
||||||
dispatch("click")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const observe = () => {
|
||||||
|
if (!observer) {
|
||||||
|
observer = new ResizeObserver(setTabInfo)
|
||||||
|
observer.observe(ref)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const stopObserving = () => {
|
||||||
|
if (observer) {
|
||||||
|
observer.unobserve(ref)
|
||||||
|
observer = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onDestroy(stopObserving)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||||
|
@ -53,11 +56,12 @@
|
||||||
<!-- svelte-ignore a11y-no-noninteractive-tabindex -->
|
<!-- svelte-ignore a11y-no-noninteractive-tabindex -->
|
||||||
<div
|
<div
|
||||||
{id}
|
{id}
|
||||||
bind:this={tab_internal}
|
bind:this={ref}
|
||||||
on:click={onClick}
|
on:click={onClick}
|
||||||
class:is-selected={$selected.title === title}
|
on:click
|
||||||
class="spectrum-Tabs-item"
|
class="spectrum-Tabs-item"
|
||||||
class:emphasized={$selected.title === title && $selected.emphasized}
|
class:is-selected={isSelected}
|
||||||
|
class:emphasized={isSelected && $selected.emphasized}
|
||||||
tabindex="0"
|
tabindex="0"
|
||||||
>
|
>
|
||||||
{#if icon}
|
{#if icon}
|
||||||
|
@ -72,7 +76,8 @@
|
||||||
{/if}
|
{/if}
|
||||||
<span class="spectrum-Tabs-itemLabel">{title}</span>
|
<span class="spectrum-Tabs-itemLabel">{title}</span>
|
||||||
</div>
|
</div>
|
||||||
{#if $selected.title === title}
|
|
||||||
|
{#if isSelected}
|
||||||
<Portal target=".spectrum-Tabs-content-{$selected.id}">
|
<Portal target=".spectrum-Tabs-content-{$selected.id}">
|
||||||
<slot />
|
<slot />
|
||||||
</Portal>
|
</Portal>
|
||||||
|
|
|
@ -93,7 +93,6 @@
|
||||||
"identity-obj-proxy": "^3.0.0",
|
"identity-obj-proxy": "^3.0.0",
|
||||||
"jest": "29.7.0",
|
"jest": "29.7.0",
|
||||||
"jsdom": "^21.1.1",
|
"jsdom": "^21.1.1",
|
||||||
"ncp": "^2.0.0",
|
|
||||||
"svelte-jester": "^1.3.2",
|
"svelte-jester": "^1.3.2",
|
||||||
"vite": "^4.5.0",
|
"vite": "^4.5.0",
|
||||||
"vite-plugin-static-copy": "^0.17.0",
|
"vite-plugin-static-copy": "^0.17.0",
|
||||||
|
|
|
@ -48,6 +48,7 @@
|
||||||
import { TriggerStepID, ActionStepID } from "constants/backend/automations"
|
import { TriggerStepID, ActionStepID } from "constants/backend/automations"
|
||||||
import { onMount } from "svelte"
|
import { onMount } from "svelte"
|
||||||
import { cloneDeep } from "lodash/fp"
|
import { cloneDeep } from "lodash/fp"
|
||||||
|
import { FIELDS } from "constants/backend"
|
||||||
|
|
||||||
export let block
|
export let block
|
||||||
export let testData
|
export let testData
|
||||||
|
@ -228,6 +229,10 @@
|
||||||
categoryName,
|
categoryName,
|
||||||
bindingName
|
bindingName
|
||||||
) => {
|
) => {
|
||||||
|
const field = Object.values(FIELDS).find(
|
||||||
|
field => field.type === value.type && field.subtype === value.subtype
|
||||||
|
)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
readableBinding: bindingName
|
readableBinding: bindingName
|
||||||
? `${bindingName}.${name}`
|
? `${bindingName}.${name}`
|
||||||
|
@ -238,7 +243,7 @@
|
||||||
icon,
|
icon,
|
||||||
category: categoryName,
|
category: categoryName,
|
||||||
display: {
|
display: {
|
||||||
type: value.type,
|
type: field?.name || value.type,
|
||||||
name,
|
name,
|
||||||
rank: isLoopBlock ? idx + 1 : idx - loopBlockCount,
|
rank: isLoopBlock ? idx + 1 : idx - loopBlockCount,
|
||||||
},
|
},
|
||||||
|
@ -282,6 +287,7 @@
|
||||||
for (const key in table?.schema) {
|
for (const key in table?.schema) {
|
||||||
schema[key] = {
|
schema[key] = {
|
||||||
type: table.schema[key].type,
|
type: table.schema[key].type,
|
||||||
|
subtype: table.schema[key].subtype,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// remove the original binding
|
// remove the original binding
|
||||||
|
@ -368,6 +374,16 @@
|
||||||
return `${value.title || (key === "row" ? "Table" : key)} ${requiredSuffix}`
|
return `${value.title || (key === "row" ? "Table" : key)} ${requiredSuffix}`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function handleAttachmentParams(keyValueObj) {
|
||||||
|
let params = {}
|
||||||
|
if (keyValueObj?.length) {
|
||||||
|
for (let param of keyValueObj) {
|
||||||
|
params[param.url] = param.filename
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return params
|
||||||
|
}
|
||||||
|
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
try {
|
try {
|
||||||
await environment.loadVariables()
|
await environment.loadVariables()
|
||||||
|
@ -375,15 +391,6 @@
|
||||||
console.error(error)
|
console.error(error)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
const handleAttachmentParams = keyValuObj => {
|
|
||||||
let params = {}
|
|
||||||
if (keyValuObj?.length) {
|
|
||||||
for (let param of keyValuObj) {
|
|
||||||
params[param.url] = param.filename
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return params
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="fields">
|
<div class="fields">
|
||||||
|
|
|
@ -25,21 +25,21 @@
|
||||||
return !!schema.constraints?.inclusion?.length
|
return !!schema.constraints?.inclusion?.length
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleAttachmentParams = keyValuObj => {
|
function handleAttachmentParams(keyValueObj) {
|
||||||
let params = {}
|
let params = {}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
schema.type === FieldType.ATTACHMENT_SINGLE &&
|
schema.type === FieldType.ATTACHMENT_SINGLE &&
|
||||||
Object.keys(keyValuObj).length === 0
|
Object.keys(keyValueObj).length === 0
|
||||||
) {
|
) {
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
if (!Array.isArray(keyValuObj)) {
|
if (!Array.isArray(keyValueObj) && keyValueObj) {
|
||||||
keyValuObj = [keyValuObj]
|
keyValueObj = [keyValueObj]
|
||||||
}
|
}
|
||||||
|
|
||||||
if (keyValuObj.length) {
|
if (keyValueObj.length) {
|
||||||
for (let param of keyValuObj) {
|
for (let param of keyValueObj) {
|
||||||
params[param.url] = param.filename
|
params[param.url] = param.filename
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,9 +14,8 @@
|
||||||
import LinkedRowSelector from "components/common/LinkedRowSelector.svelte"
|
import LinkedRowSelector from "components/common/LinkedRowSelector.svelte"
|
||||||
import Editor from "../../integration/QueryEditor.svelte"
|
import Editor from "../../integration/QueryEditor.svelte"
|
||||||
|
|
||||||
export let defaultValue
|
|
||||||
export let meta
|
export let meta
|
||||||
export let value = defaultValue || (meta.type === "boolean" ? false : "")
|
export let value
|
||||||
export let readonly
|
export let readonly
|
||||||
export let error
|
export let error
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
<script>
|
<script>
|
||||||
import { datasources, tables, integrations, appStore } from "stores/builder"
|
import { datasources, tables, integrations, appStore } from "stores/builder"
|
||||||
|
import { admin } from "stores/portal"
|
||||||
import EditRolesButton from "./buttons/EditRolesButton.svelte"
|
import EditRolesButton from "./buttons/EditRolesButton.svelte"
|
||||||
import { TableNames } from "constants"
|
import { TableNames } from "constants"
|
||||||
import { Grid } from "@budibase/frontend-core"
|
import { Grid } from "@budibase/frontend-core"
|
||||||
|
@ -63,6 +64,7 @@
|
||||||
schemaOverrides={isUsersTable ? userSchemaOverrides : null}
|
schemaOverrides={isUsersTable ? userSchemaOverrides : null}
|
||||||
showAvatars={false}
|
showAvatars={false}
|
||||||
on:updatedatasource={handleGridTableUpdate}
|
on:updatedatasource={handleGridTableUpdate}
|
||||||
|
isCloud={$admin.cloud}
|
||||||
>
|
>
|
||||||
<svelte:fragment slot="filter">
|
<svelte:fragment slot="filter">
|
||||||
{#if isUsersTable && $appStore.features.disableUserMetadata}
|
{#if isUsersTable && $appStore.features.disableUserMetadata}
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
<script>
|
<script>
|
||||||
import { viewsV2 } from "stores/builder"
|
import { viewsV2 } from "stores/builder"
|
||||||
|
import { admin } from "stores/portal"
|
||||||
import { Grid } from "@budibase/frontend-core"
|
import { Grid } from "@budibase/frontend-core"
|
||||||
import { API } from "api"
|
import { API } from "api"
|
||||||
import GridCreateEditRowModal from "components/backend/DataTable/modals/grid/GridCreateEditRowModal.svelte"
|
import GridCreateEditRowModal from "components/backend/DataTable/modals/grid/GridCreateEditRowModal.svelte"
|
||||||
|
@ -26,6 +27,7 @@
|
||||||
allowDeleteRows
|
allowDeleteRows
|
||||||
showAvatars={false}
|
showAvatars={false}
|
||||||
on:updatedatasource={handleGridViewUpdate}
|
on:updatedatasource={handleGridViewUpdate}
|
||||||
|
isCloud={$admin.cloud}
|
||||||
>
|
>
|
||||||
<svelte:fragment slot="filter">
|
<svelte:fragment slot="filter">
|
||||||
<GridFilterButton />
|
<GridFilterButton />
|
||||||
|
|
|
@ -55,7 +55,7 @@ export function getBindings({
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
const field = Object.values(FIELDS).find(
|
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}`
|
const label = path == null ? column : `${path}.0.${column}`
|
||||||
|
|
|
@ -12,8 +12,13 @@
|
||||||
OptionSelectDnD,
|
OptionSelectDnD,
|
||||||
Layout,
|
Layout,
|
||||||
AbsTooltip,
|
AbsTooltip,
|
||||||
|
ProgressCircle,
|
||||||
} from "@budibase/bbui"
|
} 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 { createEventDispatcher, getContext, onMount } from "svelte"
|
||||||
import { cloneDeep } from "lodash/fp"
|
import { cloneDeep } from "lodash/fp"
|
||||||
import { tables, datasources } from "stores/builder"
|
import { tables, datasources } from "stores/builder"
|
||||||
|
@ -30,8 +35,8 @@
|
||||||
import { getBindings } from "components/backend/DataTable/formula"
|
import { getBindings } from "components/backend/DataTable/formula"
|
||||||
import JSONSchemaModal from "./JSONSchemaModal.svelte"
|
import JSONSchemaModal from "./JSONSchemaModal.svelte"
|
||||||
import {
|
import {
|
||||||
FieldType,
|
|
||||||
BBReferenceFieldSubType,
|
BBReferenceFieldSubType,
|
||||||
|
FieldType,
|
||||||
SourceName,
|
SourceName,
|
||||||
} from "@budibase/types"
|
} from "@budibase/types"
|
||||||
import RelationshipSelector from "components/common/RelationshipSelector.svelte"
|
import RelationshipSelector from "components/common/RelationshipSelector.svelte"
|
||||||
|
@ -67,7 +72,6 @@
|
||||||
let savingColumn
|
let savingColumn
|
||||||
let deleteColName
|
let deleteColName
|
||||||
let jsonSchemaModal
|
let jsonSchemaModal
|
||||||
let allowedTypes = []
|
|
||||||
let editableColumn = {
|
let editableColumn = {
|
||||||
type: FIELDS.STRING.type,
|
type: FIELDS.STRING.type,
|
||||||
constraints: FIELDS.STRING.constraints,
|
constraints: FIELDS.STRING.constraints,
|
||||||
|
@ -175,6 +179,11 @@
|
||||||
SWITCHABLE_TYPES[field.type] &&
|
SWITCHABLE_TYPES[field.type] &&
|
||||||
!editableColumn?.autocolumn)
|
!editableColumn?.autocolumn)
|
||||||
|
|
||||||
|
$: allowedTypes = getAllowedTypes(datasource).map(t => ({
|
||||||
|
fieldId: makeFieldId(t.type, t.subtype),
|
||||||
|
...t,
|
||||||
|
}))
|
||||||
|
|
||||||
const fieldDefinitions = Object.values(FIELDS).reduce(
|
const fieldDefinitions = Object.values(FIELDS).reduce(
|
||||||
// Storing the fields by complex field id
|
// Storing the fields by complex field id
|
||||||
(acc, field) => ({
|
(acc, field) => ({
|
||||||
|
@ -188,7 +197,10 @@
|
||||||
// don't make field IDs for auto types
|
// don't make field IDs for auto types
|
||||||
if (type === AUTO_TYPE || autocolumn) {
|
if (type === AUTO_TYPE || autocolumn) {
|
||||||
return type.toUpperCase()
|
return type.toUpperCase()
|
||||||
} else if (type === FieldType.BB_REFERENCE) {
|
} else if (
|
||||||
|
type === FieldType.BB_REFERENCE ||
|
||||||
|
type === FieldType.BB_REFERENCE_SINGLE
|
||||||
|
) {
|
||||||
return `${type}${subtype || ""}`.toUpperCase()
|
return `${type}${subtype || ""}`.toUpperCase()
|
||||||
} else {
|
} else {
|
||||||
return type.toUpperCase()
|
return type.toUpperCase()
|
||||||
|
@ -226,11 +238,6 @@
|
||||||
editableColumn.subtype,
|
editableColumn.subtype,
|
||||||
editableColumn.autocolumn
|
editableColumn.autocolumn
|
||||||
)
|
)
|
||||||
|
|
||||||
allowedTypes = getAllowedTypes().map(t => ({
|
|
||||||
fieldId: makeFieldId(t.type, t.subtype),
|
|
||||||
...t,
|
|
||||||
}))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -245,11 +252,11 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
async function saveColumn() {
|
async function saveColumn() {
|
||||||
savingColumn = true
|
|
||||||
if (errors?.length) {
|
if (errors?.length) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
savingColumn = true
|
||||||
let saveColumn = cloneDeep(editableColumn)
|
let saveColumn = cloneDeep(editableColumn)
|
||||||
|
|
||||||
delete saveColumn.fieldId
|
delete saveColumn.fieldId
|
||||||
|
@ -264,13 +271,6 @@
|
||||||
if (saveColumn.type !== LINK_TYPE) {
|
if (saveColumn.type !== LINK_TYPE) {
|
||||||
delete saveColumn.fieldName
|
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 {
|
try {
|
||||||
await tables.saveField({
|
await tables.saveField({
|
||||||
|
@ -289,6 +289,8 @@
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
notifications.error(`Error saving column: ${err.message}`)
|
notifications.error(`Error saving column: ${err.message}`)
|
||||||
|
} finally {
|
||||||
|
savingColumn = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -363,56 +365,83 @@
|
||||||
deleteColName = ""
|
deleteColName = ""
|
||||||
}
|
}
|
||||||
|
|
||||||
function getAllowedTypes() {
|
function getAllowedTypes(datasource) {
|
||||||
if (originalName) {
|
if (originalName) {
|
||||||
const possibleTypes = SWITCHABLE_TYPES[field.type] || [
|
let possibleTypes = SWITCHABLE_TYPES[field.type] || [editableColumn.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)
|
return Object.entries(FIELDS)
|
||||||
.filter(([_, field]) => possibleTypes.includes(field.type))
|
.filter(([_, field]) => possibleTypes.includes(field.type))
|
||||||
.map(([_, fieldDefinition]) => fieldDefinition)
|
.map(([_, fieldDefinition]) => fieldDefinition)
|
||||||
}
|
}
|
||||||
|
|
||||||
const isUsers =
|
|
||||||
editableColumn.type === FieldType.BB_REFERENCE &&
|
|
||||||
editableColumn.subtype === BBReferenceFieldSubType.USERS
|
|
||||||
|
|
||||||
if (!externalTable) {
|
if (!externalTable) {
|
||||||
return [
|
return [
|
||||||
FIELDS.STRING,
|
FIELDS.STRING,
|
||||||
FIELDS.BARCODEQR,
|
FIELDS.NUMBER,
|
||||||
FIELDS.LONGFORM,
|
|
||||||
FIELDS.OPTIONS,
|
FIELDS.OPTIONS,
|
||||||
FIELDS.ARRAY,
|
FIELDS.ARRAY,
|
||||||
FIELDS.NUMBER,
|
|
||||||
FIELDS.BIGINT,
|
|
||||||
FIELDS.BOOLEAN,
|
FIELDS.BOOLEAN,
|
||||||
FIELDS.DATETIME,
|
FIELDS.DATETIME,
|
||||||
|
FIELDS.LINK,
|
||||||
|
FIELDS.LONGFORM,
|
||||||
|
FIELDS.USER,
|
||||||
|
FIELDS.USERS,
|
||||||
FIELDS.ATTACHMENT_SINGLE,
|
FIELDS.ATTACHMENT_SINGLE,
|
||||||
FIELDS.ATTACHMENTS,
|
FIELDS.ATTACHMENTS,
|
||||||
FIELDS.LINK,
|
|
||||||
FIELDS.FORMULA,
|
FIELDS.FORMULA,
|
||||||
FIELDS.JSON,
|
FIELDS.JSON,
|
||||||
isUsers ? FIELDS.USERS : FIELDS.USER,
|
FIELDS.BARCODEQR,
|
||||||
|
FIELDS.BIGINT,
|
||||||
FIELDS.AUTO,
|
FIELDS.AUTO,
|
||||||
]
|
]
|
||||||
} else {
|
} else {
|
||||||
let fields = [
|
let fields = [
|
||||||
FIELDS.STRING,
|
FIELDS.STRING,
|
||||||
FIELDS.BARCODEQR,
|
|
||||||
FIELDS.LONGFORM,
|
|
||||||
FIELDS.OPTIONS,
|
|
||||||
FIELDS.DATETIME,
|
|
||||||
FIELDS.NUMBER,
|
FIELDS.NUMBER,
|
||||||
|
FIELDS.OPTIONS,
|
||||||
|
FIELDS.ARRAY,
|
||||||
FIELDS.BOOLEAN,
|
FIELDS.BOOLEAN,
|
||||||
|
FIELDS.DATETIME,
|
||||||
|
FIELDS.LINK,
|
||||||
|
FIELDS.LONGFORM,
|
||||||
|
FIELDS.USER,
|
||||||
|
FIELDS.USERS,
|
||||||
FIELDS.FORMULA,
|
FIELDS.FORMULA,
|
||||||
|
FIELDS.BARCODEQR,
|
||||||
FIELDS.BIGINT,
|
FIELDS.BIGINT,
|
||||||
isUsers ? FIELDS.USERS : FIELDS.USER,
|
|
||||||
]
|
]
|
||||||
// no-sql or a spreadsheet
|
|
||||||
if (!externalTable || table.sql) {
|
// Filter out multiple users for google sheets
|
||||||
fields = [...fields, FIELDS.LINK, FIELDS.ARRAY]
|
if (datasource?.source === SourceName.GOOGLE_SHEETS) {
|
||||||
|
fields = fields.filter(x => x !== FIELDS.USERS)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Filter out SQL-specific types for non-SQL datasources
|
||||||
|
if (!table.sql) {
|
||||||
|
fields = fields.filter(x => x !== FIELDS.LINK && x !== FIELDS.ARRAY)
|
||||||
|
}
|
||||||
|
|
||||||
return fields
|
return fields
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -482,15 +511,6 @@
|
||||||
return newError
|
return newError
|
||||||
}
|
}
|
||||||
|
|
||||||
function isUsersColumn(column) {
|
|
||||||
return (
|
|
||||||
column.type === FieldType.BB_REFERENCE &&
|
|
||||||
[BBReferenceFieldSubType.USER, BBReferenceFieldSubType.USERS].includes(
|
|
||||||
column.subtype
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
mounted = true
|
mounted = true
|
||||||
})
|
})
|
||||||
|
@ -689,22 +709,6 @@
|
||||||
<Button primary text on:click={openJsonSchemaEditor}
|
<Button primary text on:click={openJsonSchemaEditor}
|
||||||
>Open schema editor</Button
|
>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}
|
||||||
{#if editableColumn.type === AUTO_TYPE || editableColumn.autocolumn}
|
{#if editableColumn.type === AUTO_TYPE || editableColumn.autocolumn}
|
||||||
<Select
|
<Select
|
||||||
|
@ -739,7 +743,20 @@
|
||||||
<Button quiet warning text on:click={confirmDelete}>Delete</Button>
|
<Button quiet warning text on:click={confirmDelete}>Delete</Button>
|
||||||
{/if}
|
{/if}
|
||||||
<Button secondary newStyles on:click={cancelEdit}>Cancel</Button>
|
<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>
|
</div>
|
||||||
<Modal bind:this={jsonSchemaModal}>
|
<Modal bind:this={jsonSchemaModal}>
|
||||||
<JSONSchemaModal
|
<JSONSchemaModal
|
||||||
|
@ -804,4 +821,9 @@
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
color: var(--spectrum-global-color-gray-900);
|
color: var(--spectrum-global-color-gray-900);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.save-loading {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -13,7 +13,9 @@
|
||||||
onMount(() => subscribe("edit-column", editColumn))
|
onMount(() => subscribe("edit-column", editColumn))
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<CreateEditColumn
|
{#if editableColumn}
|
||||||
field={editableColumn}
|
<CreateEditColumn
|
||||||
on:updatecolumns={rows.actions.refreshData}
|
field={editableColumn}
|
||||||
/>
|
on:updatecolumns={rows.actions.refreshData}
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
|
|
|
@ -86,8 +86,9 @@ export const createValidatedConfigStore = (integration, config) => {
|
||||||
([$configStore, $errorsStore, $selectedValidatorsStore]) => {
|
([$configStore, $errorsStore, $selectedValidatorsStore]) => {
|
||||||
const validatedConfig = []
|
const validatedConfig = []
|
||||||
|
|
||||||
|
const allowedRestKeys = ["rejectUnauthorized", "downloadImages"]
|
||||||
Object.entries(integration.datasource).forEach(([key, properties]) => {
|
Object.entries(integration.datasource).forEach(([key, properties]) => {
|
||||||
if (integration.name === "REST" && key !== "rejectUnauthorized") {
|
if (integration.name === "REST" && !allowedRestKeys.includes(key)) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -59,13 +59,17 @@
|
||||||
value: FieldType.ATTACHMENTS,
|
value: FieldType.ATTACHMENTS,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "User",
|
label: "Users",
|
||||||
value: `${FieldType.BB_REFERENCE}${BBReferenceFieldSubType.USER}`,
|
value: `${FieldType.BB_REFERENCE}${BBReferenceFieldSubType.USER}`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "Users",
|
label: "Users",
|
||||||
value: `${FieldType.BB_REFERENCE}${BBReferenceFieldSubType.USERS}`,
|
value: `${FieldType.BB_REFERENCE}${BBReferenceFieldSubType.USERS}`,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
label: "User",
|
||||||
|
value: `${FieldType.BB_REFERENCE_SINGLE}${BBReferenceFieldSubType.USER}`,
|
||||||
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
$: {
|
$: {
|
||||||
|
|
|
@ -0,0 +1,50 @@
|
||||||
|
import { it, expect, describe, vi } from "vitest"
|
||||||
|
import Dropzone from "./Dropzone.svelte"
|
||||||
|
import { render, fireEvent } from "@testing-library/svelte"
|
||||||
|
import { notifications } from "@budibase/bbui"
|
||||||
|
import { admin } from "stores/portal"
|
||||||
|
|
||||||
|
vi.spyOn(notifications, "error").mockImplementation(() => {})
|
||||||
|
|
||||||
|
describe("Dropzone", () => {
|
||||||
|
let instance = null
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
vi.restoreAllMocks()
|
||||||
|
})
|
||||||
|
|
||||||
|
it("that the Dropzone is rendered", () => {
|
||||||
|
instance = render(Dropzone, {})
|
||||||
|
expect(instance).toBeDefined()
|
||||||
|
})
|
||||||
|
|
||||||
|
it("Ensure the correct error message is shown when uploading the file in cloud", async () => {
|
||||||
|
admin.subscribe = vi.fn().mockImplementation(callback => {
|
||||||
|
callback({ cloud: true })
|
||||||
|
return () => {}
|
||||||
|
})
|
||||||
|
instance = render(Dropzone, { props: { fileSizeLimit: 1000000 } }) // 1MB
|
||||||
|
const fileInput = instance.getByLabelText("Select a file to upload")
|
||||||
|
const file = new File(["hello".repeat(2000000)], "hello.png", {
|
||||||
|
type: "image/png",
|
||||||
|
})
|
||||||
|
await fireEvent.change(fileInput, { target: { files: [file] } })
|
||||||
|
expect(notifications.error).toHaveBeenCalledWith(
|
||||||
|
"Files cannot exceed 1MB. Please try again with smaller files."
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("Ensure the file size error message is not shown when running on self host", async () => {
|
||||||
|
admin.subscribe = vi.fn().mockImplementation(callback => {
|
||||||
|
callback({ cloud: false })
|
||||||
|
return () => {}
|
||||||
|
})
|
||||||
|
instance = render(Dropzone, { props: { fileSizeLimit: 1000000 } }) // 1MB
|
||||||
|
const fileInput = instance.getByLabelText("Select a file to upload")
|
||||||
|
const file = new File(["hello".repeat(2000000)], "hello.png", {
|
||||||
|
type: "image/png",
|
||||||
|
})
|
||||||
|
await fireEvent.change(fileInput, { target: { files: [file] } })
|
||||||
|
expect(notifications.error).not.toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
})
|
|
@ -1,9 +1,11 @@
|
||||||
<script>
|
<script>
|
||||||
import { Dropzone, notifications } from "@budibase/bbui"
|
import { Dropzone, notifications } from "@budibase/bbui"
|
||||||
|
import { admin } from "stores/portal"
|
||||||
import { API } from "api"
|
import { API } from "api"
|
||||||
|
|
||||||
export let value = []
|
export let value = []
|
||||||
export let label
|
export let label
|
||||||
|
export let fileSizeLimit = undefined
|
||||||
|
|
||||||
const BYTES_IN_MB = 1000000
|
const BYTES_IN_MB = 1000000
|
||||||
|
|
||||||
|
@ -34,5 +36,6 @@
|
||||||
{label}
|
{label}
|
||||||
{...$$restProps}
|
{...$$restProps}
|
||||||
{processFiles}
|
{processFiles}
|
||||||
{handleFileTooLarge}
|
handleFileTooLarge={$admin.cloud ? handleFileTooLarge : null}
|
||||||
|
{fileSizeLimit}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -7,6 +7,7 @@
|
||||||
export let app
|
export let app
|
||||||
export let color
|
export let color
|
||||||
export let autoSave = false
|
export let autoSave = false
|
||||||
|
export let disabled = false
|
||||||
|
|
||||||
let modal
|
let modal
|
||||||
</script>
|
</script>
|
||||||
|
@ -14,12 +15,16 @@
|
||||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||||
<div class="editable-icon">
|
<div class="editable-icon">
|
||||||
<div class="hover" on:click={modal.show}>
|
{#if !disabled}
|
||||||
<Icon name="Edit" {size} color="var(--spectrum-global-color-gray-600)" />
|
<div class="hover" on:click={modal.show}>
|
||||||
</div>
|
<Icon name="Edit" {size} color="var(--spectrum-global-color-gray-600)" />
|
||||||
<div class="normal">
|
</div>
|
||||||
|
<div class="normal">
|
||||||
|
<Icon name={name || "Apps"} {size} {color} />
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
<Icon {name} {size} {color} />
|
<Icon {name} {size} {color} />
|
||||||
</div>
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Modal bind:this={modal}>
|
<Modal bind:this={modal}>
|
||||||
|
|
|
@ -23,6 +23,7 @@
|
||||||
faQuestionCircle,
|
faQuestionCircle,
|
||||||
faCircleCheck,
|
faCircleCheck,
|
||||||
faGear,
|
faGear,
|
||||||
|
faRectangleList,
|
||||||
} from "@fortawesome/free-solid-svg-icons"
|
} from "@fortawesome/free-solid-svg-icons"
|
||||||
import { faGithub, faDiscord } from "@fortawesome/free-brands-svg-icons"
|
import { faGithub, faDiscord } from "@fortawesome/free-brands-svg-icons"
|
||||||
|
|
||||||
|
@ -37,6 +38,7 @@
|
||||||
faFileArrowUp,
|
faFileArrowUp,
|
||||||
faChevronLeft,
|
faChevronLeft,
|
||||||
faCircleInfo,
|
faCircleInfo,
|
||||||
|
faRectangleList,
|
||||||
|
|
||||||
// -- Required for easyMDE use in the builder.
|
// -- Required for easyMDE use in the builder.
|
||||||
faBold,
|
faBold,
|
||||||
|
|
|
@ -4,6 +4,7 @@
|
||||||
import { isEnabled, TENANT_FEATURE_FLAGS } from "helpers/featureFlags"
|
import { isEnabled, TENANT_FEATURE_FLAGS } from "helpers/featureFlags"
|
||||||
import { licensing } from "stores/portal"
|
import { licensing } from "stores/portal"
|
||||||
import { isPremiumOrAbove } from "helpers/planTitle"
|
import { isPremiumOrAbove } from "helpers/planTitle"
|
||||||
|
import { ChangelogURL } from "constants"
|
||||||
|
|
||||||
$: premiumOrAboveLicense = isPremiumOrAbove($licensing?.license?.plan?.type)
|
$: premiumOrAboveLicense = isPremiumOrAbove($licensing?.license?.plan?.type)
|
||||||
|
|
||||||
|
@ -30,6 +31,13 @@
|
||||||
<Body size="S">Help docs</Body>
|
<Body size="S">Help docs</Body>
|
||||||
</a>
|
</a>
|
||||||
<div class="divider" />
|
<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
|
<a
|
||||||
target="_blank"
|
target="_blank"
|
||||||
href="https://github.com/Budibase/budibase/discussions"
|
href="https://github.com/Budibase/budibase/discussions"
|
||||||
|
|
|
@ -0,0 +1,214 @@
|
||||||
|
<script>
|
||||||
|
import { Button, Label, Icon, Input, notifications } from "@budibase/bbui"
|
||||||
|
import { AppStatus } from "constants"
|
||||||
|
import { appStore, initialise } from "stores/builder"
|
||||||
|
import { appsStore } from "stores/portal"
|
||||||
|
import { API } from "api"
|
||||||
|
import { writable } from "svelte/store"
|
||||||
|
import { createValidationStore } from "helpers/validation/yup"
|
||||||
|
import * as appValidation from "helpers/validation/yup/app"
|
||||||
|
import EditableIcon from "components/common/EditableIcon.svelte"
|
||||||
|
import { isEqual } from "lodash"
|
||||||
|
import { createEventDispatcher } from "svelte"
|
||||||
|
|
||||||
|
export let alignActions = "left"
|
||||||
|
|
||||||
|
const values = writable({})
|
||||||
|
const validation = createValidationStore()
|
||||||
|
const dispatch = createEventDispatcher()
|
||||||
|
|
||||||
|
let updating = false
|
||||||
|
let edited = false
|
||||||
|
let initialised = false
|
||||||
|
|
||||||
|
$: filteredApps = $appsStore.apps.filter(app => app.devId == $appStore.appId)
|
||||||
|
$: app = filteredApps.length ? filteredApps[0] : {}
|
||||||
|
$: appDeployed = app?.status === AppStatus.DEPLOYED
|
||||||
|
|
||||||
|
$: appName = $appStore.name
|
||||||
|
$: appURL = $appStore.url
|
||||||
|
$: appIconName = $appStore.icon?.name
|
||||||
|
$: appIconColor = $appStore.icon?.color
|
||||||
|
|
||||||
|
$: appMeta = {
|
||||||
|
name: appName,
|
||||||
|
url: appURL,
|
||||||
|
iconName: appIconName,
|
||||||
|
iconColor: appIconColor,
|
||||||
|
}
|
||||||
|
|
||||||
|
const initForm = appMeta => {
|
||||||
|
edited = false
|
||||||
|
values.set({
|
||||||
|
...appMeta,
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!initialised) {
|
||||||
|
setupValidation()
|
||||||
|
initialised = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const validate = (vals, appMeta) => {
|
||||||
|
const { url } = vals || {}
|
||||||
|
validation.check({
|
||||||
|
...vals,
|
||||||
|
url: url?.[0] === "/" ? url.substring(1, url.length) : url,
|
||||||
|
})
|
||||||
|
edited = !isEqual(vals, appMeta)
|
||||||
|
}
|
||||||
|
|
||||||
|
// On app/apps update, reset the state.
|
||||||
|
$: initForm(appMeta)
|
||||||
|
$: validate($values, appMeta)
|
||||||
|
|
||||||
|
const resolveAppUrl = (template, name) => {
|
||||||
|
let parsedName
|
||||||
|
const resolvedName = resolveAppName(null, name)
|
||||||
|
parsedName = resolvedName ? resolvedName.toLowerCase() : ""
|
||||||
|
const parsedUrl = parsedName ? parsedName.replace(/\s+/g, "-") : ""
|
||||||
|
return encodeURI(parsedUrl)
|
||||||
|
}
|
||||||
|
|
||||||
|
const nameToUrl = appName => {
|
||||||
|
let resolvedUrl = resolveAppUrl(null, appName)
|
||||||
|
tidyUrl(resolvedUrl)
|
||||||
|
}
|
||||||
|
|
||||||
|
const resolveAppName = (template, name) => {
|
||||||
|
if (template && !name) {
|
||||||
|
return template.name
|
||||||
|
}
|
||||||
|
return name ? name.trim() : null
|
||||||
|
}
|
||||||
|
|
||||||
|
const tidyUrl = url => {
|
||||||
|
if (url && !url.startsWith("/")) {
|
||||||
|
url = `/${url}`
|
||||||
|
}
|
||||||
|
$values.url = url === "" ? null : url
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateIcon = e => {
|
||||||
|
const { name, color } = e.detail
|
||||||
|
$values.iconColor = color
|
||||||
|
$values.iconName = name
|
||||||
|
}
|
||||||
|
|
||||||
|
const setupValidation = async () => {
|
||||||
|
appValidation.name(validation, {
|
||||||
|
apps: $appsStore.apps,
|
||||||
|
currentApp: app,
|
||||||
|
})
|
||||||
|
appValidation.url(validation, {
|
||||||
|
apps: $appsStore.apps,
|
||||||
|
currentApp: app,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async function updateApp() {
|
||||||
|
try {
|
||||||
|
await appsStore.save($appStore.appId, {
|
||||||
|
name: $values.name?.trim(),
|
||||||
|
url: $values.url?.trim(),
|
||||||
|
icon: {
|
||||||
|
name: $values.iconName,
|
||||||
|
color: $values.iconColor,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
await initialiseApp()
|
||||||
|
notifications.success("App update successful")
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error)
|
||||||
|
notifications.error("Error updating app")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const initialiseApp = async () => {
|
||||||
|
const applicationPkg = await API.fetchAppPackage($appStore.appId)
|
||||||
|
await initialise(applicationPkg)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="form">
|
||||||
|
<div class="fields">
|
||||||
|
<div class="field">
|
||||||
|
<Label size="L">Name</Label>
|
||||||
|
<Input
|
||||||
|
bind:value={$values.name}
|
||||||
|
error={$validation.touched.name && $validation.errors.name}
|
||||||
|
on:blur={() => ($validation.touched.name = true)}
|
||||||
|
on:change={nameToUrl($values.name)}
|
||||||
|
disabled={appDeployed}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<Label size="L">URL</Label>
|
||||||
|
<Input
|
||||||
|
bind:value={$values.url}
|
||||||
|
error={$validation.touched.url && $validation.errors.url}
|
||||||
|
on:blur={() => ($validation.touched.url = true)}
|
||||||
|
on:change={tidyUrl($values.url)}
|
||||||
|
placeholder={$values.url
|
||||||
|
? $values.url
|
||||||
|
: `/${resolveAppUrl(null, $values.name)}`}
|
||||||
|
disabled={appDeployed}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<Label size="L">Icon</Label>
|
||||||
|
<EditableIcon
|
||||||
|
{app}
|
||||||
|
size="XL"
|
||||||
|
name={$values.iconName}
|
||||||
|
color={$values.iconColor}
|
||||||
|
on:change={updateIcon}
|
||||||
|
disabled={appDeployed}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="actions" class:right={alignActions === "right"}>
|
||||||
|
{#if !appDeployed}
|
||||||
|
<Button
|
||||||
|
cta
|
||||||
|
on:click={async () => {
|
||||||
|
updating = true
|
||||||
|
await updateApp()
|
||||||
|
updating = false
|
||||||
|
dispatch("updated")
|
||||||
|
}}
|
||||||
|
disabled={appDeployed || updating || !edited || !$validation.valid}
|
||||||
|
>
|
||||||
|
Save
|
||||||
|
</Button>
|
||||||
|
{:else}
|
||||||
|
<div class="edit-info">
|
||||||
|
<Icon size="S" name="Info" /> Unpublish your app to edit name and URL
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.actions {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
.actions.right {
|
||||||
|
justify-content: end;
|
||||||
|
}
|
||||||
|
.fields {
|
||||||
|
display: grid;
|
||||||
|
grid-gap: var(--spacing-l);
|
||||||
|
}
|
||||||
|
.field {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 80px 220px;
|
||||||
|
grid-gap: var(--spacing-l);
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
.edit-info {
|
||||||
|
display: flex;
|
||||||
|
gap: var(--spacing-s);
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,68 @@
|
||||||
|
<script>
|
||||||
|
import { Popover, Layout, Icon } from "@budibase/bbui"
|
||||||
|
import UpdateAppForm from "./UpdateAppForm.svelte"
|
||||||
|
|
||||||
|
let formPopover
|
||||||
|
let formPopoverAnchor
|
||||||
|
let formPopoverOpen = false
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div bind:this={formPopoverAnchor}>
|
||||||
|
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||||
|
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||||
|
<div
|
||||||
|
class="app-heading"
|
||||||
|
class:editing={formPopoverOpen}
|
||||||
|
on:click={() => {
|
||||||
|
formPopover.show()
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
<span class="edit-icon">
|
||||||
|
<Icon size="S" name="Edit" color={"var(--grey-7)"} />
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Popover
|
||||||
|
customZindex={998}
|
||||||
|
bind:this={formPopover}
|
||||||
|
align="center"
|
||||||
|
anchor={formPopoverAnchor}
|
||||||
|
offset={20}
|
||||||
|
on:close={() => {
|
||||||
|
formPopoverOpen = false
|
||||||
|
}}
|
||||||
|
on:open={() => {
|
||||||
|
formPopoverOpen = true
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Layout noPadding gap="M">
|
||||||
|
<div class="popover-content">
|
||||||
|
<UpdateAppForm
|
||||||
|
on:updated={() => {
|
||||||
|
formPopover.hide()
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Layout>
|
||||||
|
</Popover>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.popover-content {
|
||||||
|
padding: var(--spacing-xl);
|
||||||
|
}
|
||||||
|
.app-heading {
|
||||||
|
display: flex;
|
||||||
|
cursor: pointer;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--spacing-s);
|
||||||
|
}
|
||||||
|
.edit-icon {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.app-heading:hover .edit-icon,
|
||||||
|
.app-heading.editing .edit-icon {
|
||||||
|
display: inline;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -8,13 +8,11 @@
|
||||||
ActionButton,
|
ActionButton,
|
||||||
Icon,
|
Icon,
|
||||||
Link,
|
Link,
|
||||||
Modal,
|
|
||||||
StatusLight,
|
StatusLight,
|
||||||
AbsTooltip,
|
AbsTooltip,
|
||||||
} from "@budibase/bbui"
|
} from "@budibase/bbui"
|
||||||
import RevertModal from "components/deploy/RevertModal.svelte"
|
import RevertModal from "components/deploy/RevertModal.svelte"
|
||||||
import VersionModal from "components/deploy/VersionModal.svelte"
|
import VersionModal from "components/deploy/VersionModal.svelte"
|
||||||
import UpdateAppModal from "components/start/UpdateAppModal.svelte"
|
|
||||||
import { processStringSync } from "@budibase/string-templates"
|
import { processStringSync } from "@budibase/string-templates"
|
||||||
import ConfirmDialog from "components/common/ConfirmDialog.svelte"
|
import ConfirmDialog from "components/common/ConfirmDialog.svelte"
|
||||||
import analytics, { Events, EventSource } from "analytics"
|
import analytics, { Events, EventSource } from "analytics"
|
||||||
|
@ -26,7 +24,6 @@
|
||||||
isOnlyUser,
|
isOnlyUser,
|
||||||
appStore,
|
appStore,
|
||||||
deploymentStore,
|
deploymentStore,
|
||||||
initialise,
|
|
||||||
sortedScreens,
|
sortedScreens,
|
||||||
} from "stores/builder"
|
} from "stores/builder"
|
||||||
import TourWrap from "components/portal/onboarding/TourWrap.svelte"
|
import TourWrap from "components/portal/onboarding/TourWrap.svelte"
|
||||||
|
@ -37,7 +34,6 @@
|
||||||
export let loaded
|
export let loaded
|
||||||
|
|
||||||
let unpublishModal
|
let unpublishModal
|
||||||
let updateAppModal
|
|
||||||
let revertModal
|
let revertModal
|
||||||
let versionModal
|
let versionModal
|
||||||
let appActionPopover
|
let appActionPopover
|
||||||
|
@ -61,11 +57,6 @@
|
||||||
$: canPublish = !publishing && loaded && $sortedScreens.length > 0
|
$: canPublish = !publishing && loaded && $sortedScreens.length > 0
|
||||||
$: lastDeployed = getLastDeployedString($deploymentStore, lastOpened)
|
$: lastDeployed = getLastDeployedString($deploymentStore, lastOpened)
|
||||||
|
|
||||||
const initialiseApp = async () => {
|
|
||||||
const applicationPkg = await API.fetchAppPackage($appStore.devId)
|
|
||||||
await initialise(applicationPkg)
|
|
||||||
}
|
|
||||||
|
|
||||||
const getLastDeployedString = deployments => {
|
const getLastDeployedString = deployments => {
|
||||||
return deployments?.length
|
return deployments?.length
|
||||||
? processStringSync("Published {{ duration time 'millisecond' }} ago", {
|
? processStringSync("Published {{ duration time 'millisecond' }} ago", {
|
||||||
|
@ -247,16 +238,12 @@
|
||||||
appActionPopover.hide()
|
appActionPopover.hide()
|
||||||
if (isPublished) {
|
if (isPublished) {
|
||||||
viewApp()
|
viewApp()
|
||||||
} else {
|
|
||||||
updateAppModal.show()
|
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{$appStore.url}
|
{$appStore.url}
|
||||||
{#if isPublished}
|
{#if isPublished}
|
||||||
<Icon size="S" name="LinkOut" />
|
<Icon size="S" name="LinkOut" />
|
||||||
{:else}
|
|
||||||
<Icon size="S" name="Edit" />
|
|
||||||
{/if}
|
{/if}
|
||||||
</span>
|
</span>
|
||||||
</Body>
|
</Body>
|
||||||
|
@ -330,20 +317,6 @@
|
||||||
Are you sure you want to unpublish the app <b>{selectedApp?.name}</b>?
|
Are you sure you want to unpublish the app <b>{selectedApp?.name}</b>?
|
||||||
</ConfirmDialog>
|
</ConfirmDialog>
|
||||||
|
|
||||||
<Modal bind:this={updateAppModal} padding={false} width="600px">
|
|
||||||
<UpdateAppModal
|
|
||||||
app={{
|
|
||||||
name: $appStore.name,
|
|
||||||
url: $appStore.url,
|
|
||||||
icon: $appStore.icon,
|
|
||||||
appId: $appStore.appId,
|
|
||||||
}}
|
|
||||||
onUpdateComplete={async () => {
|
|
||||||
await initialiseApp()
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</Modal>
|
|
||||||
|
|
||||||
<RevertModal bind:this={revertModal} />
|
<RevertModal bind:this={revertModal} />
|
||||||
<VersionModal hideIcon bind:this={versionModal} />
|
<VersionModal hideIcon bind:this={versionModal} />
|
||||||
|
|
||||||
|
|
|
@ -7,10 +7,12 @@
|
||||||
Body,
|
Body,
|
||||||
Button,
|
Button,
|
||||||
StatusLight,
|
StatusLight,
|
||||||
|
Link,
|
||||||
} from "@budibase/bbui"
|
} from "@budibase/bbui"
|
||||||
import { appStore, initialise } from "stores/builder"
|
import { appStore, initialise } from "stores/builder"
|
||||||
import { API } from "api"
|
import { API } from "api"
|
||||||
import RevertModalVersionSelect from "./RevertModalVersionSelect.svelte"
|
import RevertModalVersionSelect from "./RevertModalVersionSelect.svelte"
|
||||||
|
import { ChangelogURL } from "constants"
|
||||||
|
|
||||||
export function show() {
|
export function show() {
|
||||||
updateModal.show()
|
updateModal.show()
|
||||||
|
@ -106,6 +108,10 @@
|
||||||
latest version available.
|
latest version available.
|
||||||
</Body>
|
</Body>
|
||||||
{/if}
|
{/if}
|
||||||
|
<Body size="S">
|
||||||
|
Find the changelog for the latest release
|
||||||
|
<Link href={ChangelogURL} target="_blank">here</Link>
|
||||||
|
</Body>
|
||||||
{#if revertAvailable}
|
{#if revertAvailable}
|
||||||
<Body size="S">
|
<Body size="S">
|
||||||
You can revert this app to version
|
You can revert this app to version
|
||||||
|
|
|
@ -49,17 +49,20 @@
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
$: tables = findAllMatchingComponents($selectedScreen?.props, component =>
|
$: components = findAllMatchingComponents(
|
||||||
component._component.endsWith("table")
|
|
||||||
)
|
|
||||||
$: tableBlocks = findAllMatchingComponents(
|
|
||||||
$selectedScreen?.props,
|
$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 => ({
|
$: componentOptions = components.map(table => ({
|
||||||
label: table._instanceName,
|
label: table._instanceName,
|
||||||
value: table._component.includes("tableblock")
|
value: table._component.endsWith("/tableblock")
|
||||||
? `${table._id}-table`
|
? `${table._id}-table`
|
||||||
: table._id,
|
: table._id,
|
||||||
}))
|
}))
|
||||||
|
@ -69,6 +72,7 @@
|
||||||
$: selectedTable = components.find(
|
$: selectedTable = components.find(
|
||||||
component => component._id === selectedTableId
|
component => component._id === selectedTableId
|
||||||
)
|
)
|
||||||
|
$: parameters.rows = `{{ literal [${parameters.tableComponentId}].[selectedRows] }}`
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
if (!parameters.type) {
|
if (!parameters.type) {
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
<script>
|
<script>
|
||||||
import EditComponentPopover from "../EditComponentPopover/EditComponentPopover.svelte"
|
import EditComponentPopover from "../EditComponentPopover.svelte"
|
||||||
import { Icon } from "@budibase/bbui"
|
import { Icon } from "@budibase/bbui"
|
||||||
import { runtimeToReadableBinding } from "dataBinding"
|
import { runtimeToReadableBinding } from "dataBinding"
|
||||||
import { isJSBinding } from "@budibase/string-templates"
|
import { isJSBinding } from "@budibase/string-templates"
|
||||||
|
|
|
@ -100,9 +100,6 @@
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
get(store).actions.select(draggableItem.id)
|
get(store).actions.select(draggableItem.id)
|
||||||
}}
|
}}
|
||||||
on:mousedown={() => {
|
|
||||||
get(store).actions.select()
|
|
||||||
}}
|
|
||||||
bind:this={anchors[draggableItem.id]}
|
bind:this={anchors[draggableItem.id]}
|
||||||
class:highlighted={draggableItem.id === $store.selected}
|
class:highlighted={draggableItem.id === $store.selected}
|
||||||
>
|
>
|
||||||
|
|
|
@ -3,7 +3,6 @@
|
||||||
import { componentStore } from "stores/builder"
|
import { componentStore } from "stores/builder"
|
||||||
import { cloneDeep } from "lodash/fp"
|
import { cloneDeep } from "lodash/fp"
|
||||||
import { createEventDispatcher, getContext } from "svelte"
|
import { createEventDispatcher, getContext } from "svelte"
|
||||||
import { customPositionHandler } from "."
|
|
||||||
import ComponentSettingsSection from "pages/builder/app/[application]/design/[screenId]/[componentId]/_components/Component/ComponentSettingsSection.svelte"
|
import ComponentSettingsSection from "pages/builder/app/[application]/design/[screenId]/[componentId]/_components/Component/ComponentSettingsSection.svelte"
|
||||||
|
|
||||||
export let anchor
|
export let anchor
|
||||||
|
@ -18,76 +17,74 @@
|
||||||
|
|
||||||
let popover
|
let popover
|
||||||
let drawers = []
|
let drawers = []
|
||||||
let open = false
|
let isOpen = false
|
||||||
|
|
||||||
// Auto hide the component when another item is selected
|
// Auto hide the component when another item is selected
|
||||||
$: if (open && $draggable.selected !== componentInstance._id) {
|
$: if (open && $draggable.selected !== componentInstance._id) {
|
||||||
popover.hide()
|
close()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Open automatically if the component is marked as selected
|
// Open automatically if the component is marked as selected
|
||||||
$: if (!open && $draggable.selected === componentInstance._id && popover) {
|
$: if (!open && $draggable.selected === componentInstance._id && popover) {
|
||||||
popover.show()
|
open()
|
||||||
open = true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
$: componentDef = componentStore.getDefinition(componentInstance._component)
|
$: componentDef = componentStore.getDefinition(componentInstance._component)
|
||||||
$: parsedComponentDef = processComponentDefinitionSettings(componentDef)
|
$: parsedComponentDef = processComponentDefinitionSettings(componentDef)
|
||||||
|
|
||||||
|
const open = () => {
|
||||||
|
isOpen = true
|
||||||
|
drawers = []
|
||||||
|
$draggable.actions.select(componentInstance._id)
|
||||||
|
}
|
||||||
|
|
||||||
|
const close = () => {
|
||||||
|
// Slight delay allows us to be able to properly toggle open/close state by
|
||||||
|
// clicking again on the settings icon
|
||||||
|
setTimeout(() => {
|
||||||
|
isOpen = false
|
||||||
|
if ($draggable.selected === componentInstance._id) {
|
||||||
|
$draggable.actions.select()
|
||||||
|
}
|
||||||
|
}, 10)
|
||||||
|
}
|
||||||
|
|
||||||
|
const toggleOpen = () => {
|
||||||
|
if (isOpen) {
|
||||||
|
close()
|
||||||
|
} else {
|
||||||
|
open()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const processComponentDefinitionSettings = componentDef => {
|
const processComponentDefinitionSettings = componentDef => {
|
||||||
if (!componentDef) {
|
if (!componentDef) {
|
||||||
return {}
|
return {}
|
||||||
}
|
}
|
||||||
const clone = cloneDeep(componentDef)
|
const clone = cloneDeep(componentDef)
|
||||||
|
|
||||||
if (typeof parseSettings === "function") {
|
if (typeof parseSettings === "function") {
|
||||||
clone.settings = parseSettings(clone.settings)
|
clone.settings = parseSettings(clone.settings)
|
||||||
}
|
}
|
||||||
|
|
||||||
return clone
|
return clone
|
||||||
}
|
}
|
||||||
|
|
||||||
const updateSetting = async (setting, value) => {
|
const updateSetting = async (setting, value) => {
|
||||||
const nestedComponentInstance = cloneDeep(componentInstance)
|
const nestedComponentInstance = cloneDeep(componentInstance)
|
||||||
|
|
||||||
const patchFn = componentStore.updateComponentSetting(setting.key, value)
|
const patchFn = componentStore.updateComponentSetting(setting.key, value)
|
||||||
patchFn(nestedComponentInstance)
|
patchFn(nestedComponentInstance)
|
||||||
|
|
||||||
dispatch("change", nestedComponentInstance)
|
dispatch("change", nestedComponentInstance)
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Icon
|
<Icon name="Settings" hoverable size="S" on:click={toggleOpen} />
|
||||||
name="Settings"
|
|
||||||
hoverable
|
|
||||||
size="S"
|
|
||||||
on:click={() => {
|
|
||||||
if (!open) {
|
|
||||||
popover.show()
|
|
||||||
open = true
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Popover
|
<Popover
|
||||||
bind:this={popover}
|
open={isOpen}
|
||||||
on:open={() => {
|
on:close={close}
|
||||||
drawers = []
|
|
||||||
$draggable.actions.select(componentInstance._id)
|
|
||||||
}}
|
|
||||||
on:close={() => {
|
|
||||||
open = false
|
|
||||||
if ($draggable.selected === componentInstance._id) {
|
|
||||||
$draggable.actions.select()
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
{anchor}
|
{anchor}
|
||||||
align="left-outside"
|
align="left-outside"
|
||||||
showPopover={drawers.length === 0}
|
showPopover={drawers.length === 0}
|
||||||
clickOutsideOverride={drawers.length > 0}
|
clickOutsideOverride={drawers.length > 0}
|
||||||
maxHeight={600}
|
maxHeight={600}
|
||||||
offset={18}
|
offset={18}
|
||||||
handlePostionUpdate={customPositionHandler}
|
|
||||||
>
|
>
|
||||||
<span class="popover-wrap">
|
<span class="popover-wrap">
|
||||||
<Layout noPadding noGap>
|
<Layout noPadding noGap>
|
|
@ -1,18 +0,0 @@
|
||||||
export const customPositionHandler = (anchorBounds, eleBounds, cfg) => {
|
|
||||||
let { left, top, offset } = cfg
|
|
||||||
let percentageOffset = 30
|
|
||||||
// left-outside
|
|
||||||
left = anchorBounds.left - eleBounds.width - (offset || 5)
|
|
||||||
|
|
||||||
// shift up from the anchor, if space allows
|
|
||||||
let offsetPos = Math.floor(eleBounds.height / 100) * percentageOffset
|
|
||||||
let defaultTop = anchorBounds.top - offsetPos
|
|
||||||
|
|
||||||
if (window.innerHeight - defaultTop < eleBounds.height) {
|
|
||||||
top = window.innerHeight - eleBounds.height - 5
|
|
||||||
} else {
|
|
||||||
top = anchorBounds.top - offsetPos
|
|
||||||
}
|
|
||||||
|
|
||||||
return { ...cfg, left, top }
|
|
||||||
}
|
|
|
@ -1,5 +1,5 @@
|
||||||
<script>
|
<script>
|
||||||
import EditComponentPopover from "../EditComponentPopover/EditComponentPopover.svelte"
|
import EditComponentPopover from "../EditComponentPopover.svelte"
|
||||||
import { Toggle, Icon } from "@budibase/bbui"
|
import { Toggle, Icon } from "@budibase/bbui"
|
||||||
import { createEventDispatcher } from "svelte"
|
import { createEventDispatcher } from "svelte"
|
||||||
import { cloneDeep } from "lodash/fp"
|
import { cloneDeep } from "lodash/fp"
|
||||||
|
|
|
@ -47,4 +47,5 @@ export const FieldTypeToComponentMap = {
|
||||||
[FieldType.JSON]: "jsonfield",
|
[FieldType.JSON]: "jsonfield",
|
||||||
[FieldType.BARCODEQR]: "codescanner",
|
[FieldType.BARCODEQR]: "codescanner",
|
||||||
[FieldType.BB_REFERENCE]: "bbreferencefield",
|
[FieldType.BB_REFERENCE]: "bbreferencefield",
|
||||||
|
[FieldType.BB_REFERENCE_SINGLE]: "bbreferencesinglefield",
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
<script>
|
<script>
|
||||||
import EditComponentPopover from "../EditComponentPopover/EditComponentPopover.svelte"
|
import EditComponentPopover from "../EditComponentPopover.svelte"
|
||||||
import { Toggle, Icon } from "@budibase/bbui"
|
import { Toggle, Icon } from "@budibase/bbui"
|
||||||
import { createEventDispatcher } from "svelte"
|
import { createEventDispatcher } from "svelte"
|
||||||
import { cloneDeep } from "lodash/fp"
|
import { cloneDeep } from "lodash/fp"
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
<script>
|
<script>
|
||||||
import EditComponentPopover from "../EditComponentPopover/EditComponentPopover.svelte"
|
import EditComponentPopover from "../EditComponentPopover.svelte"
|
||||||
import { Icon } from "@budibase/bbui"
|
import { Icon } from "@budibase/bbui"
|
||||||
import { setContext } from "svelte"
|
import { setContext } from "svelte"
|
||||||
import { writable } from "svelte/store"
|
import { writable } from "svelte/store"
|
||||||
|
|
|
@ -67,6 +67,7 @@ const toGridFormat = draggableListColumns => {
|
||||||
label: entry.label,
|
label: entry.label,
|
||||||
field: entry.field,
|
field: entry.field,
|
||||||
active: entry.active,
|
active: entry.active,
|
||||||
|
width: entry.width,
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -81,6 +82,7 @@ const toDraggableListFormat = (gridFormatColumns, createComponent, schema) => {
|
||||||
field: column.field,
|
field: column.field,
|
||||||
label: column.label,
|
label: column.label,
|
||||||
columnType: schema[column.field].type,
|
columnType: schema[column.field].type,
|
||||||
|
width: column.width,
|
||||||
},
|
},
|
||||||
{}
|
{}
|
||||||
)
|
)
|
||||||
|
|
|
@ -5,7 +5,7 @@
|
||||||
export let value = null
|
export let value = null
|
||||||
|
|
||||||
$: dataSources = $datasources.list
|
$: dataSources = $datasources.list
|
||||||
.filter(ds => ds.source === "S3" && !ds.config?.endpoint)
|
.filter(ds => ds.source === "S3")
|
||||||
.map(ds => ({
|
.map(ds => ({
|
||||||
label: ds.name,
|
label: ds.name,
|
||||||
value: ds._id,
|
value: ds._id,
|
||||||
|
|
|
@ -2,21 +2,21 @@
|
||||||
import { Modal, ModalContent } from "@budibase/bbui"
|
import { Modal, ModalContent } from "@budibase/bbui"
|
||||||
import FreeTrial from "../../../../assets/FreeTrial.svelte"
|
import FreeTrial from "../../../../assets/FreeTrial.svelte"
|
||||||
import { get } from "svelte/store"
|
import { get } from "svelte/store"
|
||||||
import { auth, licensing } from "stores/portal"
|
import { auth, licensing, admin } from "stores/portal"
|
||||||
import { API } from "api"
|
import { API } from "api"
|
||||||
import { PlanType } from "@budibase/types"
|
import { PlanType } from "@budibase/types"
|
||||||
import { sdk } from "@budibase/shared-core"
|
|
||||||
|
|
||||||
let freeTrialModal
|
let freeTrialModal
|
||||||
|
|
||||||
$: planType = $licensing?.license?.plan?.type
|
$: planType = $licensing?.license?.plan?.type
|
||||||
$: showFreeTrialModal(planType, freeTrialModal)
|
$: showFreeTrialModal(planType, freeTrialModal)
|
||||||
|
$: isOwner = $auth.accountPortalAccess && $admin.cloud
|
||||||
|
|
||||||
const showFreeTrialModal = (planType, freeTrialModal) => {
|
const showFreeTrialModal = (planType, freeTrialModal) => {
|
||||||
if (
|
if (
|
||||||
planType === PlanType.ENTERPRISE_BASIC_TRIAL &&
|
planType === PlanType.ENTERPRISE_BASIC_TRIAL &&
|
||||||
!$auth.user?.freeTrialConfirmedAt &&
|
!$auth.user?.freeTrialConfirmedAt &&
|
||||||
sdk.users.isAdmin($auth.user)
|
isOwner
|
||||||
) {
|
) {
|
||||||
freeTrialModal?.show()
|
freeTrialModal?.show()
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,7 +10,6 @@ import {
|
||||||
NewFormSteps,
|
NewFormSteps,
|
||||||
} from "./steps"
|
} from "./steps"
|
||||||
import { API } from "api"
|
import { API } from "api"
|
||||||
import { customPositionHandler } from "components/design/settings/controls/EditComponentPopover"
|
|
||||||
|
|
||||||
const ONBOARDING_EVENT_PREFIX = "onboarding"
|
const ONBOARDING_EVENT_PREFIX = "onboarding"
|
||||||
|
|
||||||
|
@ -187,7 +186,6 @@ const getTours = () => {
|
||||||
tourEvent(TOUR_STEP_KEYS.BUILDER_FORM_CREATE_STEPS)
|
tourEvent(TOUR_STEP_KEYS.BUILDER_FORM_CREATE_STEPS)
|
||||||
builderStore.highlightSetting("steps", "info")
|
builderStore.highlightSetting("steps", "info")
|
||||||
},
|
},
|
||||||
positionHandler: customPositionHandler,
|
|
||||||
align: "left-outside",
|
align: "left-outside",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
@ -203,7 +201,6 @@ const getTours = () => {
|
||||||
tourEvent(TOUR_STEP_KEYS.BUILDER_FORM_ROW_ID)
|
tourEvent(TOUR_STEP_KEYS.BUILDER_FORM_ROW_ID)
|
||||||
builderStore.highlightSetting("rowId", "info")
|
builderStore.highlightSetting("rowId", "info")
|
||||||
},
|
},
|
||||||
positionHandler: customPositionHandler,
|
|
||||||
align: "left-outside",
|
align: "left-outside",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -219,7 +216,6 @@ const getTours = () => {
|
||||||
tourEvent(TOUR_STEP_KEYS.BUILDER_FORM_VIEW_UPDATE_STEPS)
|
tourEvent(TOUR_STEP_KEYS.BUILDER_FORM_VIEW_UPDATE_STEPS)
|
||||||
builderStore.highlightSetting("steps", "info")
|
builderStore.highlightSetting("steps", "info")
|
||||||
},
|
},
|
||||||
positionHandler: customPositionHandler,
|
|
||||||
align: "left-outside",
|
align: "left-outside",
|
||||||
scrollIntoView: true,
|
scrollIntoView: true,
|
||||||
},
|
},
|
||||||
|
|
|
@ -1,151 +0,0 @@
|
||||||
<script>
|
|
||||||
import { writable, get as svelteGet } from "svelte/store"
|
|
||||||
import {
|
|
||||||
notifications,
|
|
||||||
Input,
|
|
||||||
ModalContent,
|
|
||||||
Layout,
|
|
||||||
Label,
|
|
||||||
} from "@budibase/bbui"
|
|
||||||
import { appsStore } from "stores/portal"
|
|
||||||
import { onMount } from "svelte"
|
|
||||||
import { createValidationStore } from "helpers/validation/yup"
|
|
||||||
import * as appValidation from "helpers/validation/yup/app"
|
|
||||||
import EditableIcon from "../common/EditableIcon.svelte"
|
|
||||||
|
|
||||||
export let app
|
|
||||||
export let onUpdateComplete
|
|
||||||
|
|
||||||
$: appIdParts = app.appId ? app.appId?.split("_") : []
|
|
||||||
$: appId = appIdParts.slice(-1)[0]
|
|
||||||
|
|
||||||
const values = writable({
|
|
||||||
name: app.name,
|
|
||||||
url: app.url,
|
|
||||||
iconName: app.icon?.name,
|
|
||||||
iconColor: app.icon?.color,
|
|
||||||
})
|
|
||||||
const validation = createValidationStore()
|
|
||||||
|
|
||||||
$: {
|
|
||||||
const { url } = $values
|
|
||||||
|
|
||||||
validation.check({
|
|
||||||
...$values,
|
|
||||||
url: url?.[0] === "/" ? url.substring(1, url.length) : url,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const setupValidation = async () => {
|
|
||||||
const applications = svelteGet(appsStore).apps
|
|
||||||
appValidation.name(validation, {
|
|
||||||
apps: applications,
|
|
||||||
currentApp: {
|
|
||||||
...app,
|
|
||||||
appId,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
appValidation.url(validation, {
|
|
||||||
apps: applications,
|
|
||||||
currentApp: {
|
|
||||||
...app,
|
|
||||||
appId,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
// init validation
|
|
||||||
const { url } = $values
|
|
||||||
validation.check({
|
|
||||||
...$values,
|
|
||||||
url: url?.[0] === "/" ? url.substring(1, url.length) : url,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
async function updateApp() {
|
|
||||||
try {
|
|
||||||
await appsStore.save(app.appId, {
|
|
||||||
name: $values.name?.trim(),
|
|
||||||
url: $values.url?.trim(),
|
|
||||||
icon: {
|
|
||||||
name: $values.iconName,
|
|
||||||
color: $values.iconColor,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
if (typeof onUpdateComplete == "function") {
|
|
||||||
onUpdateComplete()
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error(error)
|
|
||||||
notifications.error("Error updating app")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const resolveAppUrl = (template, name) => {
|
|
||||||
let parsedName
|
|
||||||
const resolvedName = resolveAppName(null, name)
|
|
||||||
parsedName = resolvedName ? resolvedName.toLowerCase() : ""
|
|
||||||
const parsedUrl = parsedName ? parsedName.replace(/\s+/g, "-") : ""
|
|
||||||
return encodeURI(parsedUrl)
|
|
||||||
}
|
|
||||||
|
|
||||||
const resolveAppName = (template, name) => {
|
|
||||||
if (template && !name) {
|
|
||||||
return template.name
|
|
||||||
}
|
|
||||||
return name ? name.trim() : null
|
|
||||||
}
|
|
||||||
|
|
||||||
const tidyUrl = url => {
|
|
||||||
if (url && !url.startsWith("/")) {
|
|
||||||
url = `/${url}`
|
|
||||||
}
|
|
||||||
$values.url = url === "" ? null : url
|
|
||||||
}
|
|
||||||
|
|
||||||
const nameToUrl = appName => {
|
|
||||||
let resolvedUrl = resolveAppUrl(null, appName)
|
|
||||||
tidyUrl(resolvedUrl)
|
|
||||||
}
|
|
||||||
|
|
||||||
const updateIcon = e => {
|
|
||||||
const { name, color } = e.detail
|
|
||||||
$values.iconColor = color
|
|
||||||
$values.iconName = name
|
|
||||||
}
|
|
||||||
|
|
||||||
onMount(setupValidation)
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<ModalContent
|
|
||||||
title="Edit name and URL"
|
|
||||||
confirmText="Save"
|
|
||||||
onConfirm={updateApp}
|
|
||||||
disabled={!$validation.valid}
|
|
||||||
>
|
|
||||||
<Input
|
|
||||||
bind:value={$values.name}
|
|
||||||
error={$validation.touched.name && $validation.errors.name}
|
|
||||||
on:blur={() => ($validation.touched.name = true)}
|
|
||||||
on:change={nameToUrl($values.name)}
|
|
||||||
label="Name"
|
|
||||||
/>
|
|
||||||
<Layout noPadding gap="XS">
|
|
||||||
<Label>Icon</Label>
|
|
||||||
<EditableIcon
|
|
||||||
{app}
|
|
||||||
size="XL"
|
|
||||||
name={$values.iconName}
|
|
||||||
color={$values.iconColor}
|
|
||||||
on:change={updateIcon}
|
|
||||||
/>
|
|
||||||
</Layout>
|
|
||||||
<Input
|
|
||||||
bind:value={$values.url}
|
|
||||||
error={$validation.touched.url && $validation.errors.url}
|
|
||||||
on:blur={() => ($validation.touched.url = true)}
|
|
||||||
on:change={tidyUrl($values.url)}
|
|
||||||
label="URL"
|
|
||||||
placeholder={$values.url
|
|
||||||
? $values.url
|
|
||||||
: `/${resolveAppUrl(null, $values.name)}`}
|
|
||||||
/>
|
|
||||||
</ModalContent>
|
|
|
@ -33,7 +33,7 @@ export const FIELDS = {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
BARCODEQR: {
|
BARCODEQR: {
|
||||||
name: "Barcode/QR",
|
name: "Barcode / QR",
|
||||||
type: FieldType.BARCODEQR,
|
type: FieldType.BARCODEQR,
|
||||||
icon: TypeIconMap[FieldType.BARCODEQR],
|
icon: TypeIconMap[FieldType.BARCODEQR],
|
||||||
constraints: {
|
constraints: {
|
||||||
|
@ -43,7 +43,7 @@ export const FIELDS = {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
LONGFORM: {
|
LONGFORM: {
|
||||||
name: "Long Form Text",
|
name: "Long form text",
|
||||||
type: FieldType.LONGFORM,
|
type: FieldType.LONGFORM,
|
||||||
icon: TypeIconMap[FieldType.LONGFORM],
|
icon: TypeIconMap[FieldType.LONGFORM],
|
||||||
constraints: {
|
constraints: {
|
||||||
|
@ -53,7 +53,7 @@ export const FIELDS = {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
OPTIONS: {
|
OPTIONS: {
|
||||||
name: "Options",
|
name: "Single select",
|
||||||
type: FieldType.OPTIONS,
|
type: FieldType.OPTIONS,
|
||||||
icon: TypeIconMap[FieldType.OPTIONS],
|
icon: TypeIconMap[FieldType.OPTIONS],
|
||||||
constraints: {
|
constraints: {
|
||||||
|
@ -63,7 +63,7 @@ export const FIELDS = {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
ARRAY: {
|
ARRAY: {
|
||||||
name: "Multi-select",
|
name: "Multi select",
|
||||||
type: FieldType.ARRAY,
|
type: FieldType.ARRAY,
|
||||||
icon: TypeIconMap[FieldType.ARRAY],
|
icon: TypeIconMap[FieldType.ARRAY],
|
||||||
constraints: {
|
constraints: {
|
||||||
|
@ -83,7 +83,7 @@ export const FIELDS = {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
BIGINT: {
|
BIGINT: {
|
||||||
name: "BigInt",
|
name: "Big integer",
|
||||||
type: FieldType.BIGINT,
|
type: FieldType.BIGINT,
|
||||||
icon: TypeIconMap[FieldType.BIGINT],
|
icon: TypeIconMap[FieldType.BIGINT],
|
||||||
},
|
},
|
||||||
|
@ -97,7 +97,7 @@ export const FIELDS = {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
DATETIME: {
|
DATETIME: {
|
||||||
name: "Date/Time",
|
name: "Date / time",
|
||||||
type: FieldType.DATETIME,
|
type: FieldType.DATETIME,
|
||||||
icon: TypeIconMap[FieldType.DATETIME],
|
icon: TypeIconMap[FieldType.DATETIME],
|
||||||
constraints: {
|
constraints: {
|
||||||
|
@ -111,7 +111,7 @@ export const FIELDS = {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
ATTACHMENT_SINGLE: {
|
ATTACHMENT_SINGLE: {
|
||||||
name: "Attachment",
|
name: "Single attachment",
|
||||||
type: FieldType.ATTACHMENT_SINGLE,
|
type: FieldType.ATTACHMENT_SINGLE,
|
||||||
icon: TypeIconMap[FieldType.ATTACHMENT_SINGLE],
|
icon: TypeIconMap[FieldType.ATTACHMENT_SINGLE],
|
||||||
constraints: {
|
constraints: {
|
||||||
|
@ -119,7 +119,7 @@ export const FIELDS = {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
ATTACHMENTS: {
|
ATTACHMENTS: {
|
||||||
name: "Attachment List",
|
name: "Multi attachment",
|
||||||
type: FieldType.ATTACHMENTS,
|
type: FieldType.ATTACHMENTS,
|
||||||
icon: TypeIconMap[FieldType.ATTACHMENTS],
|
icon: TypeIconMap[FieldType.ATTACHMENTS],
|
||||||
constraints: {
|
constraints: {
|
||||||
|
@ -137,7 +137,7 @@ export const FIELDS = {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
AUTO: {
|
AUTO: {
|
||||||
name: "Auto Column",
|
name: "Auto column",
|
||||||
type: FieldType.AUTO,
|
type: FieldType.AUTO,
|
||||||
icon: TypeIconMap[FieldType.AUTO],
|
icon: TypeIconMap[FieldType.AUTO],
|
||||||
constraints: {},
|
constraints: {},
|
||||||
|
@ -158,16 +158,18 @@ export const FIELDS = {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
USER: {
|
USER: {
|
||||||
name: "User",
|
name: "Single user",
|
||||||
type: FieldType.BB_REFERENCE,
|
type: FieldType.BB_REFERENCE_SINGLE,
|
||||||
subtype: BBReferenceFieldSubType.USER,
|
subtype: BBReferenceFieldSubType.USER,
|
||||||
icon: TypeIconMap[FieldType.USER],
|
icon: TypeIconMap[FieldType.BB_REFERENCE_SINGLE][
|
||||||
|
BBReferenceFieldSubType.USER
|
||||||
|
],
|
||||||
},
|
},
|
||||||
USERS: {
|
USERS: {
|
||||||
name: "Users",
|
name: "Multi user",
|
||||||
type: FieldType.BB_REFERENCE,
|
type: FieldType.BB_REFERENCE,
|
||||||
subtype: BBReferenceFieldSubType.USERS,
|
subtype: BBReferenceFieldSubType.USER,
|
||||||
icon: TypeIconMap[FieldType.USERS],
|
icon: TypeIconMap[FieldType.BB_REFERENCE][BBReferenceFieldSubType.USER],
|
||||||
constraints: {
|
constraints: {
|
||||||
type: "array",
|
type: "array",
|
||||||
},
|
},
|
||||||
|
|
|
@ -70,3 +70,5 @@ export const PlanModel = {
|
||||||
PER_USER: "perUser",
|
PER_USER: "perUser",
|
||||||
DAY_PASS: "dayPass",
|
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 ActionDefinitions from "components/design/settings/controls/ButtonActionEditor/manifest.json"
|
||||||
import { environment, licensing } from "stores/portal"
|
import { environment, licensing } from "stores/portal"
|
||||||
import { convertOldFieldFormat } from "components/design/settings/controls/FieldConfiguration/utils"
|
import { convertOldFieldFormat } from "components/design/settings/controls/FieldConfiguration/utils"
|
||||||
|
import { FIELDS } from "constants/backend"
|
||||||
|
|
||||||
const { ContextScopes } = Constants
|
const { ContextScopes } = Constants
|
||||||
|
|
||||||
|
@ -491,7 +492,7 @@ const generateComponentContextBindings = (asset, componentContext) => {
|
||||||
icon: bindingCategory.icon,
|
icon: bindingCategory.icon,
|
||||||
display: {
|
display: {
|
||||||
name: `${fieldSchema.name || key}`,
|
name: `${fieldSchema.name || key}`,
|
||||||
type: fieldSchema.type,
|
type: fieldSchema.display?.type || fieldSchema.type,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
@ -829,7 +830,7 @@ export const getActionBindings = (actions, actionId) => {
|
||||||
* @return {{schema: Object, table: Object}}
|
* @return {{schema: Object, table: Object}}
|
||||||
*/
|
*/
|
||||||
export const getSchemaForDatasourcePlus = (resourceId, options) => {
|
export const getSchemaForDatasourcePlus = (resourceId, options) => {
|
||||||
const isViewV2 = resourceId?.includes("view_")
|
const isViewV2 = resourceId?.startsWith("view_")
|
||||||
const datasource = isViewV2
|
const datasource = isViewV2
|
||||||
? {
|
? {
|
||||||
type: "viewV2",
|
type: "viewV2",
|
||||||
|
@ -1019,15 +1020,23 @@ export const getSchemaForDatasource = (asset, datasource, options) => {
|
||||||
// are objects
|
// are objects
|
||||||
let fixedSchema = {}
|
let fixedSchema = {}
|
||||||
Object.entries(schema || {}).forEach(([fieldName, fieldSchema]) => {
|
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") {
|
if (typeof fieldSchema === "string") {
|
||||||
fixedSchema[fieldName] = {
|
fixedSchema[fieldName] = {
|
||||||
type: fieldSchema,
|
type: fieldSchema,
|
||||||
name: fieldName,
|
name: fieldName,
|
||||||
|
display: { type: fieldSchema },
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
fixedSchema[fieldName] = {
|
fixedSchema[fieldName] = {
|
||||||
...fieldSchema,
|
...fieldSchema,
|
||||||
name: fieldName,
|
name: fieldName,
|
||||||
|
display: { type: field?.name || fieldSchema.type },
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
|
@ -19,11 +19,10 @@ export const name = (validation, { apps, currentApp } = { apps: [] }) => {
|
||||||
// exit early, above validator will fail
|
// exit early, above validator will fail
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
if (currentApp) {
|
|
||||||
// filter out the current app if present
|
|
||||||
apps = apps.filter(app => app.appId !== currentApp.appId)
|
|
||||||
}
|
|
||||||
return !apps
|
return !apps
|
||||||
|
.filter(app => {
|
||||||
|
return app.appId !== currentApp?.appId
|
||||||
|
})
|
||||||
.map(app => app.name)
|
.map(app => app.name)
|
||||||
.some(appName => appName.toLowerCase() === value.toLowerCase())
|
.some(appName => appName.toLowerCase() === value.toLowerCase())
|
||||||
}
|
}
|
||||||
|
|
|
@ -33,6 +33,7 @@
|
||||||
import { TOUR_KEYS } from "components/portal/onboarding/tours.js"
|
import { TOUR_KEYS } from "components/portal/onboarding/tours.js"
|
||||||
import PreviewOverlay from "./_components/PreviewOverlay.svelte"
|
import PreviewOverlay from "./_components/PreviewOverlay.svelte"
|
||||||
import EnterpriseBasicTrialModal from "components/portal/onboarding/EnterpriseBasicTrialModal.svelte"
|
import EnterpriseBasicTrialModal from "components/portal/onboarding/EnterpriseBasicTrialModal.svelte"
|
||||||
|
import UpdateAppTopNav from "components/common/UpdateAppTopNav.svelte"
|
||||||
|
|
||||||
export let application
|
export let application
|
||||||
|
|
||||||
|
@ -158,7 +159,11 @@
|
||||||
</Tabs>
|
</Tabs>
|
||||||
</div>
|
</div>
|
||||||
<div class="topcenternav">
|
<div class="topcenternav">
|
||||||
<Heading size="XS">{$appStore.name}</Heading>
|
<div class="app-name">
|
||||||
|
<UpdateAppTopNav {application}>
|
||||||
|
<Heading noPadding size="XS">{$appStore.name}</Heading>
|
||||||
|
</UpdateAppTopNav>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="toprightnav">
|
<div class="toprightnav">
|
||||||
<span>
|
<span>
|
||||||
|
@ -247,7 +252,6 @@
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
padding: 0px var(--spacing-m);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.topleftnav {
|
.topleftnav {
|
||||||
|
|
|
@ -71,6 +71,7 @@
|
||||||
"multifieldselect",
|
"multifieldselect",
|
||||||
"s3upload",
|
"s3upload",
|
||||||
"codescanner",
|
"codescanner",
|
||||||
|
"bbreferencesinglefield",
|
||||||
"bbreferencefield"
|
"bbreferencefield"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|
|
@ -1,30 +1,6 @@
|
||||||
<script>
|
<script>
|
||||||
import {
|
import { Layout, Divider, Heading, Body } from "@budibase/bbui"
|
||||||
Layout,
|
import UpdateAppForm from "components/common/UpdateAppForm.svelte"
|
||||||
Divider,
|
|
||||||
Heading,
|
|
||||||
Body,
|
|
||||||
Button,
|
|
||||||
Label,
|
|
||||||
Modal,
|
|
||||||
Icon,
|
|
||||||
} from "@budibase/bbui"
|
|
||||||
import { AppStatus } from "constants"
|
|
||||||
import { appStore, initialise } from "stores/builder"
|
|
||||||
import { appsStore } from "stores/portal"
|
|
||||||
import UpdateAppModal from "components/start/UpdateAppModal.svelte"
|
|
||||||
import { API } from "api"
|
|
||||||
|
|
||||||
let updatingModal
|
|
||||||
|
|
||||||
$: filteredApps = $appsStore.apps.filter(app => app.devId == $appStore.appId)
|
|
||||||
$: app = filteredApps.length ? filteredApps[0] : {}
|
|
||||||
$: appDeployed = app?.status === AppStatus.DEPLOYED
|
|
||||||
|
|
||||||
const initialiseApp = async () => {
|
|
||||||
const applicationPkg = await API.fetchAppPackage($appStore.appId)
|
|
||||||
await initialise(applicationPkg)
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Layout noPadding>
|
<Layout noPadding>
|
||||||
|
@ -33,61 +9,5 @@
|
||||||
<Body>Edit your app's name and URL</Body>
|
<Body>Edit your app's name and URL</Body>
|
||||||
</Layout>
|
</Layout>
|
||||||
<Divider />
|
<Divider />
|
||||||
|
<UpdateAppForm />
|
||||||
<Layout noPadding gap="XXS">
|
|
||||||
<Label size="L">Name</Label>
|
|
||||||
<Body>{$appStore?.name}</Body>
|
|
||||||
</Layout>
|
|
||||||
|
|
||||||
<Layout noPadding gap="XS">
|
|
||||||
<Label size="L">Icon</Label>
|
|
||||||
<div class="icon">
|
|
||||||
<Icon
|
|
||||||
size="L"
|
|
||||||
name={$appStore?.icon?.name || "Apps"}
|
|
||||||
color={$appStore?.icon?.color}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</Layout>
|
|
||||||
|
|
||||||
<Layout noPadding gap="XXS">
|
|
||||||
<Label size="L">URL</Label>
|
|
||||||
<Body>{$appStore.url}</Body>
|
|
||||||
</Layout>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<Button
|
|
||||||
cta
|
|
||||||
on:click={() => {
|
|
||||||
updatingModal.show()
|
|
||||||
}}
|
|
||||||
disabled={appDeployed}
|
|
||||||
tooltip={appDeployed
|
|
||||||
? "You must unpublish your app to make changes"
|
|
||||||
: null}
|
|
||||||
>
|
|
||||||
Edit
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</Layout>
|
</Layout>
|
||||||
|
|
||||||
<Modal bind:this={updatingModal} padding={false} width="600px">
|
|
||||||
<UpdateAppModal
|
|
||||||
app={{
|
|
||||||
name: $appStore.name,
|
|
||||||
url: $appStore.url,
|
|
||||||
icon: $appStore.icon,
|
|
||||||
appId: $appStore.appId,
|
|
||||||
}}
|
|
||||||
onUpdateComplete={async () => {
|
|
||||||
await initialiseApp()
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</Modal>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
.icon {
|
|
||||||
display: flex;
|
|
||||||
justify-content: flex-start;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
|
@ -1,7 +1,14 @@
|
||||||
<script>
|
<script>
|
||||||
import { isActive, redirect, goto, url } from "@roxi/routify"
|
import { isActive, redirect, goto, url } from "@roxi/routify"
|
||||||
import { Icon, notifications, Tabs, Tab } from "@budibase/bbui"
|
import { Icon, notifications, Tabs, Tab } from "@budibase/bbui"
|
||||||
import { organisation, auth, menu, appsStore, licensing } from "stores/portal"
|
import {
|
||||||
|
organisation,
|
||||||
|
auth,
|
||||||
|
menu,
|
||||||
|
appsStore,
|
||||||
|
licensing,
|
||||||
|
admin,
|
||||||
|
} from "stores/portal"
|
||||||
import { onMount } from "svelte"
|
import { onMount } from "svelte"
|
||||||
import UpgradeButton from "./_components/UpgradeButton.svelte"
|
import UpgradeButton from "./_components/UpgradeButton.svelte"
|
||||||
import MobileMenu from "./_components/MobileMenu.svelte"
|
import MobileMenu from "./_components/MobileMenu.svelte"
|
||||||
|
@ -20,6 +27,7 @@
|
||||||
$: $url(), updateActiveTab($menu)
|
$: $url(), updateActiveTab($menu)
|
||||||
$: isOnboarding =
|
$: isOnboarding =
|
||||||
!$appsStore.apps.length && sdk.users.hasBuilderPermissions($auth.user)
|
!$appsStore.apps.length && sdk.users.hasBuilderPermissions($auth.user)
|
||||||
|
$: isOwner = $auth.accountPortalAccess && $admin.cloud
|
||||||
|
|
||||||
const updateActiveTab = menu => {
|
const updateActiveTab = menu => {
|
||||||
for (let entry of menu) {
|
for (let entry of menu) {
|
||||||
|
@ -38,8 +46,7 @@
|
||||||
const showFreeTrialBanner = () => {
|
const showFreeTrialBanner = () => {
|
||||||
return (
|
return (
|
||||||
$licensing.license?.plan?.type ===
|
$licensing.license?.plan?.type ===
|
||||||
Constants.PlanType.ENTERPRISE_BASIC_TRIAL &&
|
Constants.PlanType.ENTERPRISE_BASIC_TRIAL && isOwner
|
||||||
sdk.users.isAdmin($auth.user)
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -166,10 +166,16 @@ const automationActions = store => ({
|
||||||
await store.actions.save(newAutomation)
|
await store.actions.save(newAutomation)
|
||||||
},
|
},
|
||||||
test: async (automation, testData) => {
|
test: async (automation, testData) => {
|
||||||
const result = await API.testAutomation({
|
let result
|
||||||
automationId: automation?._id,
|
try {
|
||||||
testData,
|
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?.trigger && !result?.steps?.length) {
|
||||||
if (result?.err?.code === "usage_limit_exceeded") {
|
if (result?.err?.code === "usage_limit_exceeded") {
|
||||||
throw "You have exceeded your automation quota"
|
throw "You have exceeded your automation quota"
|
||||||
|
|
|
@ -516,6 +516,8 @@ export class ComponentStore extends BudiStore {
|
||||||
return state
|
return state
|
||||||
})
|
})
|
||||||
|
|
||||||
|
componentTreeNodesStore.makeNodeVisible(componentInstance._id)
|
||||||
|
|
||||||
// Log event
|
// Log event
|
||||||
analytics.captureEvent(Events.COMPONENT_CREATED, {
|
analytics.captureEvent(Events.COMPONENT_CREATED, {
|
||||||
name: componentInstance._component,
|
name: componentInstance._component,
|
||||||
|
|
|
@ -131,7 +131,7 @@ export class AppsStore extends BudiStore {
|
||||||
if (updatedAppIndex !== -1) {
|
if (updatedAppIndex !== -1) {
|
||||||
let updatedApp = state.apps[updatedAppIndex]
|
let updatedApp = state.apps[updatedAppIndex]
|
||||||
updatedApp = { ...updatedApp, ...value }
|
updatedApp = { ...updatedApp, ...value }
|
||||||
state.apps = state.apps.splice(updatedAppIndex, 1, updatedApp)
|
state.apps.splice(updatedAppIndex, 1, updatedApp)
|
||||||
}
|
}
|
||||||
return state
|
return state
|
||||||
})
|
})
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
process.env.DISABLE_PINO_LOGGER = "1"
|
||||||
process.env.NO_JS = "1"
|
process.env.NO_JS = "1"
|
||||||
process.env.JS_BCRYPT = "1"
|
process.env.JS_BCRYPT = "1"
|
||||||
process.env.DISABLE_JWT_WARNING = "1"
|
process.env.DISABLE_JWT_WARNING = "1"
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
#!/usr/bin/env node
|
#!/usr/bin/env node
|
||||||
process.env.DISABLE_PINO_LOGGER = "1"
|
// have to import this before anything else
|
||||||
import "./environment"
|
import "./environment"
|
||||||
import { getCommands } from "./options"
|
import { getCommands } from "./options"
|
||||||
import { Command } from "commander"
|
import { Command } from "commander"
|
||||||
|
|
|
@ -1,3 +1,3 @@
|
||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
dir="$(dirname -- "$(readlink -f "${BASH_SOURCE}")")"
|
dir="$(dirname -- "$(readlink -f "${BASH_SOURCE}")")"
|
||||||
${dir}/node_modules/ts-node/dist/bin.js ${dir}/src/index.ts $@
|
${dir}/../../node_modules/ts-node/dist/bin.js ${dir}/src/index.ts $@
|
||||||
|
|
|
@ -2746,6 +2746,14 @@
|
||||||
"type": "plainText",
|
"type": "plainText",
|
||||||
"label": "Label",
|
"label": "Label",
|
||||||
"key": "label"
|
"key": "label"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "number",
|
||||||
|
"label": "Initial width",
|
||||||
|
"key": "width",
|
||||||
|
"placeholder": "Auto",
|
||||||
|
"min": 80,
|
||||||
|
"max": 9999
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
@ -7018,16 +7026,28 @@
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"context": {
|
"context": [
|
||||||
"type": "schema",
|
{
|
||||||
"scope": "local"
|
"type": "schema",
|
||||||
},
|
"scope": "local"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "static",
|
||||||
|
"values": [
|
||||||
|
{
|
||||||
|
"label": "Selected rows",
|
||||||
|
"key": "selectedRows",
|
||||||
|
"type": "array"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
"actions": ["RefreshDatasource"]
|
"actions": ["RefreshDatasource"]
|
||||||
},
|
},
|
||||||
"bbreferencefield": {
|
"bbreferencefield": {
|
||||||
"devComment": "As bb reference is only used for user subtype for now, we are using user for icon and labels",
|
"devComment": "As bb reference is only used for user subtype for now, we are using user for icon and labels",
|
||||||
"name": "User Field",
|
"name": "User List Field",
|
||||||
"icon": "User",
|
"icon": "UserGroup",
|
||||||
"styles": ["size"],
|
"styles": ["size"],
|
||||||
"requiredAncestors": ["form"],
|
"requiredAncestors": ["form"],
|
||||||
"editable": true,
|
"editable": true,
|
||||||
|
@ -7131,5 +7151,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>
|
<script>
|
||||||
// NOTE: this is not a block - it's just named as such to avoid confusing users,
|
// NOTE: this is not a block - it's just named as such to avoid confusing users,
|
||||||
// because it functions similarly to one
|
// because it functions similarly to one
|
||||||
import { getContext } from "svelte"
|
import { getContext, onMount } from "svelte"
|
||||||
import { get } from "svelte/store"
|
import { get, derived, readable } from "svelte/store"
|
||||||
import { Grid } from "@budibase/frontend-core"
|
import { Grid } from "@budibase/frontend-core"
|
||||||
|
|
||||||
// table is actually any datasource, but called table for legacy compatibility
|
// table is actually any datasource, but called table for legacy compatibility
|
||||||
|
@ -19,10 +19,10 @@
|
||||||
export let columns = null
|
export let columns = null
|
||||||
export let onRowClick = null
|
export let onRowClick = null
|
||||||
export let buttons = null
|
export let buttons = null
|
||||||
export let repeat = null
|
|
||||||
|
|
||||||
const context = getContext("context")
|
const context = getContext("context")
|
||||||
const component = getContext("component")
|
const component = getContext("component")
|
||||||
|
const { environmentStore } = getContext("sdk")
|
||||||
const {
|
const {
|
||||||
styleable,
|
styleable,
|
||||||
API,
|
API,
|
||||||
|
@ -36,22 +36,23 @@
|
||||||
} = getContext("sdk")
|
} = getContext("sdk")
|
||||||
|
|
||||||
let grid
|
let grid
|
||||||
|
let gridContext
|
||||||
|
let minHeight
|
||||||
|
|
||||||
$: columnWhitelist = parsedColumns
|
$: parsedColumns = getParsedColumns(columns)
|
||||||
?.filter(col => col.active)
|
$: columnWhitelist = parsedColumns.filter(x => x.active).map(x => x.field)
|
||||||
?.map(col => col.field)
|
|
||||||
$: schemaOverrides = getSchemaOverrides(parsedColumns)
|
$: schemaOverrides = getSchemaOverrides(parsedColumns)
|
||||||
$: enrichedButtons = enrichButtons(buttons)
|
$: enrichedButtons = enrichButtons(buttons)
|
||||||
$: parsedColumns = getParsedColumns(columns)
|
$: selectedRows = deriveSelectedRows(gridContext)
|
||||||
|
$: styles = patchStyles($component.styles, minHeight)
|
||||||
|
$: data = { selectedRows: $selectedRows }
|
||||||
$: actions = [
|
$: actions = [
|
||||||
{
|
{
|
||||||
type: ActionTypes.RefreshDatasource,
|
type: ActionTypes.RefreshDatasource,
|
||||||
callback: () => grid?.getContext()?.rows.actions.refreshData(),
|
callback: () => gridContext?.rows.actions.refreshData(),
|
||||||
metadata: { dataSource: table },
|
metadata: { dataSource: table },
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
$: height = $component.styles?.normal?.height || "408px"
|
|
||||||
$: styles = getSanitisedStyles($component.styles)
|
|
||||||
|
|
||||||
// Provide additional data context for live binding eval
|
// Provide additional data context for live binding eval
|
||||||
export const getAdditionalDataContext = () => {
|
export const getAdditionalDataContext = () => {
|
||||||
|
@ -68,12 +69,14 @@
|
||||||
|
|
||||||
// Parses columns to fix older formats
|
// Parses columns to fix older formats
|
||||||
const getParsedColumns = columns => {
|
const getParsedColumns = columns => {
|
||||||
|
if (!columns?.length) {
|
||||||
|
return []
|
||||||
|
}
|
||||||
// If the first element has an active key all elements should be in the new format
|
// 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
|
||||||
}
|
}
|
||||||
|
return columns.map(column => ({
|
||||||
return columns?.map(column => ({
|
|
||||||
label: column.displayName || column.name,
|
label: column.displayName || column.name,
|
||||||
field: column.name,
|
field: column.name,
|
||||||
active: true,
|
active: true,
|
||||||
|
@ -82,9 +85,11 @@
|
||||||
|
|
||||||
const getSchemaOverrides = columns => {
|
const getSchemaOverrides = columns => {
|
||||||
let overrides = {}
|
let overrides = {}
|
||||||
columns?.forEach(column => {
|
columns.forEach((column, idx) => {
|
||||||
overrides[column.field] = {
|
overrides[column.field] = {
|
||||||
displayName: column.label,
|
displayName: column.label,
|
||||||
|
width: column.width,
|
||||||
|
order: idx,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
return overrides
|
return overrides
|
||||||
|
@ -109,49 +114,70 @@
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
const getSanitisedStyles = styles => {
|
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 patchStyles = (styles, minHeight) => {
|
||||||
return {
|
return {
|
||||||
...styles,
|
...styles,
|
||||||
normal: {
|
normal: {
|
||||||
...styles?.normal,
|
...styles?.normal,
|
||||||
height: undefined,
|
"min-height": `${minHeight}px`,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
gridContext = grid.getContext()
|
||||||
|
gridContext.minHeight.subscribe($height => (minHeight = $height))
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div use:styleable={styles} class:in-builder={$builderStore.inBuilder}>
|
<div use:styleable={styles} class:in-builder={$builderStore.inBuilder}>
|
||||||
<span style="--height:{height};">
|
<Grid
|
||||||
<Provider {actions}>
|
bind:this={grid}
|
||||||
<Grid
|
datasource={table}
|
||||||
bind:this={grid}
|
{API}
|
||||||
datasource={table}
|
{stripeRows}
|
||||||
{API}
|
{quiet}
|
||||||
{stripeRows}
|
{initialFilter}
|
||||||
{quiet}
|
{initialSortColumn}
|
||||||
{initialFilter}
|
{initialSortOrder}
|
||||||
{initialSortColumn}
|
{fixedRowHeight}
|
||||||
{initialSortOrder}
|
{columnWhitelist}
|
||||||
{fixedRowHeight}
|
{schemaOverrides}
|
||||||
{columnWhitelist}
|
canAddRows={allowAddRows}
|
||||||
{schemaOverrides}
|
canEditRows={allowEditRows}
|
||||||
{repeat}
|
canDeleteRows={allowDeleteRows}
|
||||||
canAddRows={allowAddRows}
|
canEditColumns={false}
|
||||||
canEditRows={allowEditRows}
|
canExpandRows={false}
|
||||||
canDeleteRows={allowDeleteRows}
|
canSaveSchema={false}
|
||||||
canEditColumns={false}
|
canSelectRows={true}
|
||||||
canExpandRows={false}
|
showControls={false}
|
||||||
canSaveSchema={false}
|
notifySuccess={notificationStore.actions.success}
|
||||||
showControls={false}
|
notifyError={notificationStore.actions.error}
|
||||||
notifySuccess={notificationStore.actions.success}
|
buttons={enrichedButtons}
|
||||||
notifyError={notificationStore.actions.error}
|
isCloud={$environmentStore.cloud}
|
||||||
buttons={enrichedButtons}
|
on:rowclick={e => onRowClick?.({ row: e.detail })}
|
||||||
on:rowclick={e => onRowClick?.({ row: e.detail })}
|
/>
|
||||||
/>
|
|
||||||
</Provider>
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<Provider {data} {actions} />
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
div {
|
div {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
@ -160,14 +186,9 @@
|
||||||
border: 1px solid var(--spectrum-global-color-gray-300);
|
border: 1px solid var(--spectrum-global-color-gray-300);
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
height: 410px;
|
||||||
}
|
}
|
||||||
div.in-builder :global(*) {
|
div.in-builder :global(*) {
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
span {
|
|
||||||
display: contents;
|
|
||||||
}
|
|
||||||
span :global(.grid) {
|
|
||||||
height: var(--height);
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -21,6 +21,7 @@
|
||||||
[FieldType.JSON]: "jsonfield",
|
[FieldType.JSON]: "jsonfield",
|
||||||
[FieldType.BARCODEQR]: "codescanner",
|
[FieldType.BARCODEQR]: "codescanner",
|
||||||
[FieldType.BB_REFERENCE]: "bbreferencefield",
|
[FieldType.BB_REFERENCE]: "bbreferencefield",
|
||||||
|
[FieldType.BB_REFERENCE_SINGLE]: "bbreferencesinglefield",
|
||||||
}
|
}
|
||||||
|
|
||||||
const getFieldSchema = field => {
|
const getFieldSchema = field => {
|
||||||
|
|
|
@ -25,7 +25,7 @@
|
||||||
let fieldState
|
let fieldState
|
||||||
let fieldApi
|
let fieldApi
|
||||||
|
|
||||||
const { API, notificationStore } = getContext("sdk")
|
const { API, notificationStore, environmentStore } = getContext("sdk")
|
||||||
const formContext = getContext("form")
|
const formContext = getContext("form")
|
||||||
const BYTES_IN_MB = 1000000
|
const BYTES_IN_MB = 1000000
|
||||||
|
|
||||||
|
@ -87,7 +87,7 @@
|
||||||
error={fieldState.error}
|
error={fieldState.error}
|
||||||
on:change={handleChange}
|
on:change={handleChange}
|
||||||
{processFiles}
|
{processFiles}
|
||||||
{handleFileTooLarge}
|
handleFileTooLarge={$environmentStore.cloud ? handleFileTooLarge : null}
|
||||||
{handleTooManyFiles}
|
{handleTooManyFiles}
|
||||||
{maximum}
|
{maximum}
|
||||||
{extensions}
|
{extensions}
|
||||||
|
|
|
@ -1,8 +1,10 @@
|
||||||
<script>
|
<script>
|
||||||
import RelationshipField from "./RelationshipField.svelte"
|
|
||||||
import { sdk } from "@budibase/shared-core"
|
import { sdk } from "@budibase/shared-core"
|
||||||
|
import { FieldType } from "@budibase/types"
|
||||||
|
import RelationshipField from "./RelationshipField.svelte"
|
||||||
|
|
||||||
export let defaultValue
|
export let defaultValue
|
||||||
|
export let type = FieldType.BB_REFERENCE
|
||||||
|
|
||||||
function updateUserIDs(value) {
|
function updateUserIDs(value) {
|
||||||
if (Array.isArray(value)) {
|
if (Array.isArray(value)) {
|
||||||
|
@ -22,6 +24,7 @@
|
||||||
|
|
||||||
<RelationshipField
|
<RelationshipField
|
||||||
{...$$props}
|
{...$$props}
|
||||||
|
{type}
|
||||||
datasourceType={"user"}
|
datasourceType={"user"}
|
||||||
primaryDisplay={"email"}
|
primaryDisplay={"email"}
|
||||||
defaultValue={updateReferences(defaultValue)}
|
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>
|
<script>
|
||||||
import { CoreSelect, CoreMultiselect } from "@budibase/bbui"
|
import { CoreSelect, CoreMultiselect } from "@budibase/bbui"
|
||||||
|
import { FieldType } from "@budibase/types"
|
||||||
import { fetchData, Utils } from "@budibase/frontend-core"
|
import { fetchData, Utils } from "@budibase/frontend-core"
|
||||||
import { getContext, onMount } from "svelte"
|
import { getContext } from "svelte"
|
||||||
import Field from "./Field.svelte"
|
import Field from "./Field.svelte"
|
||||||
import { FieldTypes } from "../../../constants"
|
|
||||||
|
|
||||||
const { API } = getContext("sdk")
|
const { API } = getContext("sdk")
|
||||||
|
|
||||||
|
@ -21,6 +21,7 @@
|
||||||
export let primaryDisplay
|
export let primaryDisplay
|
||||||
export let span
|
export let span
|
||||||
export let helpText = null
|
export let helpText = null
|
||||||
|
export let type = FieldType.LINK
|
||||||
|
|
||||||
let fieldState
|
let fieldState
|
||||||
let fieldApi
|
let fieldApi
|
||||||
|
@ -28,12 +29,10 @@
|
||||||
let tableDefinition
|
let tableDefinition
|
||||||
let searchTerm
|
let searchTerm
|
||||||
let open
|
let open
|
||||||
let initialValue
|
|
||||||
|
|
||||||
$: type =
|
$: multiselect =
|
||||||
datasourceType === "table" ? FieldTypes.LINK : FieldTypes.BB_REFERENCE
|
[FieldType.LINK, FieldType.BB_REFERENCE].includes(type) &&
|
||||||
|
fieldSchema?.relationshipType !== "one-to-many"
|
||||||
$: multiselect = fieldSchema?.relationshipType !== "one-to-many"
|
|
||||||
$: linkedTableId = fieldSchema?.tableId
|
$: linkedTableId = fieldSchema?.tableId
|
||||||
$: fetch = fetchData({
|
$: fetch = fetchData({
|
||||||
API,
|
API,
|
||||||
|
@ -52,18 +51,19 @@
|
||||||
? flatten(fieldState?.value) ?? []
|
? flatten(fieldState?.value) ?? []
|
||||||
: flatten(fieldState?.value)?.[0]
|
: flatten(fieldState?.value)?.[0]
|
||||||
$: component = multiselect ? CoreMultiselect : CoreSelect
|
$: component = multiselect ? CoreMultiselect : CoreSelect
|
||||||
$: expandedDefaultValue = expand(defaultValue)
|
|
||||||
$: primaryDisplay = primaryDisplay || tableDefinition?.primaryDisplay
|
$: primaryDisplay = primaryDisplay || tableDefinition?.primaryDisplay
|
||||||
|
|
||||||
let optionsObj = {}
|
let optionsObj
|
||||||
let initialValuesProcessed
|
|
||||||
|
|
||||||
$: {
|
$: {
|
||||||
if (!initialValuesProcessed && primaryDisplay) {
|
if (primaryDisplay && fieldState && !optionsObj) {
|
||||||
// Persist the initial values as options, allowing them to be present in the dropdown,
|
// Persist the initial values as options, allowing them to be present in the dropdown,
|
||||||
// even if they are not in the inital fetch results
|
// even if they are not in the inital fetch results
|
||||||
initialValuesProcessed = true
|
let valueAsSafeArray = fieldState.value || []
|
||||||
optionsObj = (fieldState?.value || []).reduce((accumulator, 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
|
// fieldState has to be an array of strings to be valid for an update
|
||||||
// therefore we cannot guarantee value will be an object
|
// 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
|
// https://linear.app/budibase/issue/BUDI-7577/refactor-the-relationshipfield-component-to-have-better-support-for
|
||||||
|
@ -75,7 +75,7 @@
|
||||||
[primaryDisplay]: value.primaryDisplay,
|
[primaryDisplay]: value.primaryDisplay,
|
||||||
}
|
}
|
||||||
return accumulator
|
return accumulator
|
||||||
}, optionsObj)
|
}, {})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -86,7 +86,7 @@
|
||||||
accumulator[row._id] = row
|
accumulator[row._id] = row
|
||||||
}
|
}
|
||||||
return accumulator
|
return accumulator
|
||||||
}, optionsObj)
|
}, optionsObj || {})
|
||||||
|
|
||||||
return Object.values(result)
|
return Object.values(result)
|
||||||
}
|
}
|
||||||
|
@ -110,17 +110,10 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
$: forceFetchRows(filter)
|
$: forceFetchRows(filter)
|
||||||
$: debouncedFetchRows(
|
$: debouncedFetchRows(searchTerm, primaryDisplay, defaultValue)
|
||||||
searchTerm,
|
|
||||||
primaryDisplay,
|
|
||||||
initialValue || defaultValue
|
|
||||||
)
|
|
||||||
|
|
||||||
const forceFetchRows = async () => {
|
const forceFetchRows = async () => {
|
||||||
// if the filter has changed, then we need to reset the options, clear the selection, and re-fetch
|
|
||||||
optionsObj = {}
|
|
||||||
fieldApi?.setValue([])
|
fieldApi?.setValue([])
|
||||||
selectedValue = []
|
|
||||||
debouncedFetchRows(searchTerm, primaryDisplay, defaultValue)
|
debouncedFetchRows(searchTerm, primaryDisplay, defaultValue)
|
||||||
}
|
}
|
||||||
const fetchRows = async (searchTerm, primaryDisplay, defaultVal) => {
|
const fetchRows = async (searchTerm, primaryDisplay, defaultVal) => {
|
||||||
|
@ -136,7 +129,7 @@
|
||||||
if (defaultVal && !Array.isArray(defaultVal)) {
|
if (defaultVal && !Array.isArray(defaultVal)) {
|
||||||
defaultVal = defaultVal.split(",")
|
defaultVal = defaultVal.split(",")
|
||||||
}
|
}
|
||||||
if (defaultVal && defaultVal.some(val => !optionsObj[val])) {
|
if (defaultVal && optionsObj && defaultVal.some(val => !optionsObj[val])) {
|
||||||
await fetch.update({
|
await fetch.update({
|
||||||
query: { oneOf: { _id: defaultVal } },
|
query: { oneOf: { _id: defaultVal } },
|
||||||
})
|
})
|
||||||
|
@ -162,16 +155,13 @@
|
||||||
if (!values) {
|
if (!values) {
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!Array.isArray(values)) {
|
if (!Array.isArray(values)) {
|
||||||
values = [values]
|
values = [values]
|
||||||
}
|
}
|
||||||
values = values.map(value =>
|
values = values.map(value =>
|
||||||
typeof value === "object" ? value._id : value
|
typeof value === "object" ? value._id : value
|
||||||
)
|
)
|
||||||
// Make sure field state is valid
|
|
||||||
if (values?.length > 0) {
|
|
||||||
fieldApi.setValue(values)
|
|
||||||
}
|
|
||||||
return values
|
return values
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -179,25 +169,20 @@
|
||||||
return row?.[primaryDisplay] || "-"
|
return row?.[primaryDisplay] || "-"
|
||||||
}
|
}
|
||||||
|
|
||||||
const singleHandler = e => {
|
const handleChange = e => {
|
||||||
handleChange(e.detail == null ? [] : [e.detail])
|
let value = e.detail
|
||||||
}
|
if (!multiselect) {
|
||||||
|
value = value == null ? [] : [value]
|
||||||
const multiHandler = e => {
|
}
|
||||||
handleChange(e.detail)
|
|
||||||
}
|
if (
|
||||||
|
type === FieldType.BB_REFERENCE_SINGLE &&
|
||||||
const expand = values => {
|
value &&
|
||||||
if (!values) {
|
Array.isArray(value)
|
||||||
return []
|
) {
|
||||||
|
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)
|
const changed = fieldApi.setValue(value)
|
||||||
if (onChange && changed) {
|
if (onChange && changed) {
|
||||||
onChange({
|
onChange({
|
||||||
|
@ -211,16 +196,6 @@
|
||||||
fetch.nextPage()
|
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>
|
</script>
|
||||||
|
|
||||||
<Field
|
<Field
|
||||||
|
@ -229,7 +204,7 @@
|
||||||
{disabled}
|
{disabled}
|
||||||
{readonly}
|
{readonly}
|
||||||
{validation}
|
{validation}
|
||||||
defaultValue={expandedDefaultValue}
|
{defaultValue}
|
||||||
{type}
|
{type}
|
||||||
{span}
|
{span}
|
||||||
{helpText}
|
{helpText}
|
||||||
|
@ -243,7 +218,7 @@
|
||||||
options={enrichedOptions}
|
options={enrichedOptions}
|
||||||
{autocomplete}
|
{autocomplete}
|
||||||
value={selectedValue}
|
value={selectedValue}
|
||||||
on:change={multiselect ? multiHandler : singleHandler}
|
on:change={handleChange}
|
||||||
on:loadMore={loadMore}
|
on:loadMore={loadMore}
|
||||||
id={fieldState.fieldId}
|
id={fieldState.fieldId}
|
||||||
disabled={fieldState.disabled}
|
disabled={fieldState.disabled}
|
||||||
|
|
|
@ -17,3 +17,4 @@ export { default as jsonfield } from "./JSONField.svelte"
|
||||||
export { default as s3upload } from "./S3Upload.svelte"
|
export { default as s3upload } from "./S3Upload.svelte"
|
||||||
export { default as codescanner } from "./CodeScannerField.svelte"
|
export { default as codescanner } from "./CodeScannerField.svelte"
|
||||||
export { default as bbreferencefield } from "./BBReferenceField.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(
|
const componentId = Object.keys(selection).find(
|
||||||
componentId => componentId === tableComponentId
|
componentId => componentId === tableComponentId
|
||||||
)
|
)
|
||||||
return selection[componentId] || {}
|
return selection[componentId]
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
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 => {
|
const exportDataHandler = async action => {
|
||||||
let selection = rowSelectionStore.actions.getSelection(
|
let { tableComponentId, rows, type, columns, delimiter, customHeaders } =
|
||||||
action.parameters.tableComponentId
|
action.parameters
|
||||||
)
|
let tableId
|
||||||
if (selection.selectedRows && selection.selectedRows.length > 0) {
|
|
||||||
|
// 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 {
|
try {
|
||||||
|
// Flatten rows if required
|
||||||
|
if (typeof rows[0] !== "string") {
|
||||||
|
rows = rows.map(row => row._id)
|
||||||
|
}
|
||||||
const data = await API.exportRows({
|
const data = await API.exportRows({
|
||||||
tableId: selection.tableId,
|
tableId,
|
||||||
rows: selection.selectedRows,
|
rows,
|
||||||
format: action.parameters.type,
|
format: type,
|
||||||
columns: action.parameters.columns?.map(
|
columns: columns?.map(column => column.name || column),
|
||||||
column => column.name || column
|
delimiter,
|
||||||
),
|
customHeaders,
|
||||||
delimiter: action.parameters.delimiter,
|
|
||||||
customHeaders: action.parameters.customHeaders,
|
|
||||||
})
|
})
|
||||||
download(
|
download(new Blob([data], { type: "text/plain" }), `${tableId}.${type}`)
|
||||||
new Blob([data], { type: "text/plain" }),
|
|
||||||
`${selection.tableId}.${action.parameters.type}`
|
|
||||||
)
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
notificationStore.actions.error("There was an error exporting the data")
|
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.type = fieldSchema?.type
|
||||||
filter.subtype = fieldSchema?.subtype
|
filter.subtype = fieldSchema?.subtype
|
||||||
filter.formulaType = fieldSchema?.formulaType
|
filter.formulaType = fieldSchema?.formulaType
|
||||||
|
filter.constraints = fieldSchema?.constraints
|
||||||
|
|
||||||
// Update external type based on field
|
// Update external type based on field
|
||||||
filter.externalType = getSchema(filter)?.externalType
|
filter.externalType = getSchema(filter)?.externalType
|
||||||
|
@ -281,7 +282,7 @@
|
||||||
timeOnly={getSchema(filter)?.timeOnly}
|
timeOnly={getSchema(filter)?.timeOnly}
|
||||||
bind:value={filter.value}
|
bind:value={filter.value}
|
||||||
/>
|
/>
|
||||||
{:else if filter.type === FieldType.BB_REFERENCE}
|
{:else if [FieldType.BB_REFERENCE, FieldType.BB_REFERENCE_SINGLE].includes(filter.type)}
|
||||||
<FilterUsers
|
<FilterUsers
|
||||||
bind:value={filter.value}
|
bind:value={filter.value}
|
||||||
multiselect={[
|
multiselect={[
|
||||||
|
|
|
@ -12,7 +12,7 @@
|
||||||
export let schema
|
export let schema
|
||||||
export let maximum
|
export let maximum
|
||||||
|
|
||||||
const { API, notifications } = getContext("grid")
|
const { API, notifications, props } = getContext("grid")
|
||||||
const imageExtensions = ["png", "tiff", "gif", "raw", "jpg", "jpeg"]
|
const imageExtensions = ["png", "tiff", "gif", "raw", "jpg", "jpeg"]
|
||||||
|
|
||||||
let isOpen = false
|
let isOpen = false
|
||||||
|
@ -106,7 +106,7 @@
|
||||||
on:change={e => onChange(e.detail)}
|
on:change={e => onChange(e.detail)}
|
||||||
maximum={maximum || schema.constraints?.length?.maximum}
|
maximum={maximum || schema.constraints?.length?.maximum}
|
||||||
{processFiles}
|
{processFiles}
|
||||||
{handleFileTooLarge}
|
handleFileTooLarge={$props.isCloud ? handleFileTooLarge : null}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</GridPopover>
|
</GridPopover>
|
||||||
|
|
|
@ -1,19 +1,27 @@
|
||||||
<script>
|
<script>
|
||||||
import { getContext } from "svelte"
|
import { getContext } from "svelte"
|
||||||
|
import { helpers } from "@budibase/shared-core"
|
||||||
import RelationshipCell from "./RelationshipCell.svelte"
|
import RelationshipCell from "./RelationshipCell.svelte"
|
||||||
import { BBReferenceFieldSubType, RelationshipType } from "@budibase/types"
|
import {
|
||||||
|
BBReferenceFieldSubType,
|
||||||
|
FieldType,
|
||||||
|
RelationshipType,
|
||||||
|
} from "@budibase/types"
|
||||||
|
|
||||||
export let api
|
export let api
|
||||||
|
export let hideCounter = false
|
||||||
|
export let schema
|
||||||
|
|
||||||
const { API } = getContext("grid")
|
const { API } = getContext("grid")
|
||||||
const { subtype } = $$props.schema
|
const { type, subtype } = schema
|
||||||
|
|
||||||
const schema = {
|
$: schema = {
|
||||||
...$$props.schema,
|
...$$props.schema,
|
||||||
// This is not really used, just adding some content to be able to render the relationship cell
|
// This is not really used, just adding some content to be able to render the relationship cell
|
||||||
tableId: "external",
|
tableId: "external",
|
||||||
relationshipType:
|
relationshipType:
|
||||||
subtype === BBReferenceFieldSubType.USER
|
type === FieldType.BB_REFERENCE_SINGLE ||
|
||||||
|
helpers.schema.isDeprecatedSingleUserColumn(schema)
|
||||||
? RelationshipType.ONE_TO_MANY
|
? RelationshipType.ONE_TO_MANY
|
||||||
: RelationshipType.MANY_TO_MANY,
|
: RelationshipType.MANY_TO_MANY,
|
||||||
}
|
}
|
||||||
|
@ -44,8 +52,9 @@
|
||||||
|
|
||||||
<RelationshipCell
|
<RelationshipCell
|
||||||
bind:api
|
bind:api
|
||||||
{...$$props}
|
{...$$restProps}
|
||||||
{schema}
|
{schema}
|
||||||
{searchFunction}
|
{searchFunction}
|
||||||
primaryDisplay={"email"}
|
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 { config, dispatch, selectedRows } = getContext("grid")
|
||||||
const svelteDispatch = createEventDispatcher()
|
const svelteDispatch = createEventDispatcher()
|
||||||
|
|
||||||
|
$: selectionEnabled = $config.canSelectRows || $config.canDeleteRows
|
||||||
|
|
||||||
const select = e => {
|
const select = e => {
|
||||||
e.stopPropagation()
|
e.stopPropagation()
|
||||||
svelteDispatch("select")
|
svelteDispatch("select")
|
||||||
|
@ -52,7 +54,7 @@
|
||||||
<div
|
<div
|
||||||
on:click={select}
|
on:click={select}
|
||||||
class="checkbox"
|
class="checkbox"
|
||||||
class:visible={$config.canDeleteRows &&
|
class:visible={selectionEnabled &&
|
||||||
(disableNumber || rowSelected || rowHovered || rowFocused)}
|
(disableNumber || rowSelected || rowHovered || rowFocused)}
|
||||||
>
|
>
|
||||||
<Checkbox value={rowSelected} {disabled} />
|
<Checkbox value={rowSelected} {disabled} />
|
||||||
|
@ -60,7 +62,7 @@
|
||||||
{#if !disableNumber}
|
{#if !disableNumber}
|
||||||
<div
|
<div
|
||||||
class="number"
|
class="number"
|
||||||
class:visible={!$config.canDeleteRows ||
|
class:visible={!selectionEnabled ||
|
||||||
!(rowSelected || rowHovered || rowFocused)}
|
!(rowSelected || rowHovered || rowFocused)}
|
||||||
>
|
>
|
||||||
{row.__idx + 1}
|
{row.__idx + 1}
|
||||||
|
@ -117,19 +119,11 @@
|
||||||
.expand {
|
.expand {
|
||||||
margin-right: 4px;
|
margin-right: 4px;
|
||||||
}
|
}
|
||||||
.expand {
|
.expand:not(.visible),
|
||||||
|
.expand:not(.visible) :global(*) {
|
||||||
opacity: 0;
|
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 {
|
.delete:hover {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,7 +23,6 @@
|
||||||
subscribe,
|
subscribe,
|
||||||
config,
|
config,
|
||||||
ui,
|
ui,
|
||||||
columns,
|
|
||||||
definition,
|
definition,
|
||||||
datasource,
|
datasource,
|
||||||
schema,
|
schema,
|
||||||
|
@ -158,17 +157,13 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
const makeDisplayColumn = () => {
|
const makeDisplayColumn = () => {
|
||||||
columns.actions.changePrimaryDisplay(column.name)
|
datasource.actions.changePrimaryDisplay(column.name)
|
||||||
open = false
|
open = false
|
||||||
}
|
}
|
||||||
|
|
||||||
const hideColumn = () => {
|
const hideColumn = () => {
|
||||||
columns.update(state => {
|
datasource.actions.addSchemaMutation(column.name, { visible: false })
|
||||||
const index = state.findIndex(col => col.name === column.name)
|
datasource.actions.saveSchemaMutations()
|
||||||
state[index].visible = false
|
|
||||||
return state.slice()
|
|
||||||
})
|
|
||||||
columns.actions.saveChanges()
|
|
||||||
open = false
|
open = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -386,7 +381,7 @@
|
||||||
>
|
>
|
||||||
Hide column
|
Hide column
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
{#if $config.canEditColumns && column.schema.type === "link" && column.schema.tableId === TableNames.USERS}
|
{#if $config.canEditColumns && column.schema.type === "link" && column.schema.tableId === TableNames.USERS && !column.schema.autocolumn}
|
||||||
<MenuItem icon="User" on:click={openMigrationModal}>
|
<MenuItem icon="User" on:click={openMigrationModal}>
|
||||||
Migrate to user column
|
Migrate to user column
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
|
|
|
@ -17,6 +17,7 @@
|
||||||
export let contentLines = 1
|
export let contentLines = 1
|
||||||
export let searchFunction = API.searchTable
|
export let searchFunction = API.searchTable
|
||||||
export let primaryDisplay
|
export let primaryDisplay
|
||||||
|
export let hideCounter = false
|
||||||
|
|
||||||
const color = getColor(0)
|
const color = getColor(0)
|
||||||
|
|
||||||
|
@ -263,7 +264,7 @@
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
{#if value?.length}
|
{#if !hideCounter && value?.length}
|
||||||
<div class="count">
|
<div class="count">
|
||||||
{value?.length || 0}
|
{value?.length || 0}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
import { ActionButton, Popover, Toggle, Icon } from "@budibase/bbui"
|
import { ActionButton, Popover, Toggle, Icon } from "@budibase/bbui"
|
||||||
import { getColumnIcon } from "../lib/utils"
|
import { getColumnIcon } from "../lib/utils"
|
||||||
|
|
||||||
const { columns, stickyColumn, dispatch } = getContext("grid")
|
const { columns, datasource, stickyColumn, dispatch } = getContext("grid")
|
||||||
|
|
||||||
let open = false
|
let open = false
|
||||||
let anchor
|
let anchor
|
||||||
|
@ -11,36 +11,20 @@
|
||||||
$: anyHidden = $columns.some(col => !col.visible)
|
$: anyHidden = $columns.some(col => !col.visible)
|
||||||
$: text = getText($columns)
|
$: text = getText($columns)
|
||||||
|
|
||||||
const toggleVisibility = async (column, visible) => {
|
const toggleColumn = async (column, visible) => {
|
||||||
columns.update(state => {
|
datasource.actions.addSchemaMutation(column.name, { visible })
|
||||||
const index = state.findIndex(col => col.name === column.name)
|
await datasource.actions.saveSchemaMutations()
|
||||||
state[index].visible = visible
|
|
||||||
return state.slice()
|
|
||||||
})
|
|
||||||
await columns.actions.saveChanges()
|
|
||||||
dispatch(visible ? "show-column" : "hide-column")
|
dispatch(visible ? "show-column" : "hide-column")
|
||||||
}
|
}
|
||||||
|
|
||||||
const showAll = async () => {
|
const toggleAll = async visible => {
|
||||||
columns.update(state => {
|
let mutations = {}
|
||||||
return state.map(col => ({
|
$columns.forEach(column => {
|
||||||
...col,
|
mutations[column.name] = { visible }
|
||||||
visible: true,
|
|
||||||
}))
|
|
||||||
})
|
})
|
||||||
await columns.actions.saveChanges()
|
datasource.actions.addSchemaMutations(mutations)
|
||||||
dispatch("show-column")
|
await datasource.actions.saveSchemaMutations()
|
||||||
}
|
dispatch(visible ? "show-column" : "hide-column")
|
||||||
|
|
||||||
const hideAll = async () => {
|
|
||||||
columns.update(state => {
|
|
||||||
return state.map(col => ({
|
|
||||||
...col,
|
|
||||||
visible: false,
|
|
||||||
}))
|
|
||||||
})
|
|
||||||
await columns.actions.saveChanges()
|
|
||||||
dispatch("hide-column")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const getText = columns => {
|
const getText = columns => {
|
||||||
|
@ -80,14 +64,14 @@
|
||||||
<Toggle
|
<Toggle
|
||||||
size="S"
|
size="S"
|
||||||
value={column.visible}
|
value={column.visible}
|
||||||
on:change={e => toggleVisibility(column, e.detail)}
|
on:change={e => toggleColumn(column, e.detail)}
|
||||||
disabled={column.primaryDisplay}
|
disabled={column.primaryDisplay}
|
||||||
/>
|
/>
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
<div class="buttons">
|
<div class="buttons">
|
||||||
<ActionButton on:click={showAll}>Show all</ActionButton>
|
<ActionButton on:click={() => toggleAll(true)}>Show all</ActionButton>
|
||||||
<ActionButton on:click={hideAll}>Hide all</ActionButton>
|
<ActionButton on:click={() => toggleAll(false)}>Hide all</ActionButton>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Popover>
|
</Popover>
|
||||||
|
|
|
@ -7,11 +7,6 @@
|
||||||
} from "@budibase/bbui"
|
} from "@budibase/bbui"
|
||||||
import { getContext } from "svelte"
|
import { getContext } from "svelte"
|
||||||
import { ValidColumnNameRegex } from "@budibase/shared-core"
|
import { ValidColumnNameRegex } from "@budibase/shared-core"
|
||||||
import {
|
|
||||||
BBReferenceFieldSubType,
|
|
||||||
FieldType,
|
|
||||||
RelationshipType,
|
|
||||||
} from "@budibase/types"
|
|
||||||
|
|
||||||
const { API, definition, rows } = getContext("grid")
|
const { API, definition, rows } = getContext("grid")
|
||||||
|
|
||||||
|
@ -33,20 +28,11 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
const migrateUserColumn = async () => {
|
const migrateUserColumn = async () => {
|
||||||
let subtype = BBReferenceFieldSubType.USERS
|
|
||||||
if (column.schema.relationshipType === RelationshipType.ONE_TO_MANY) {
|
|
||||||
subtype = BBReferenceFieldSubType.USER
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await API.migrateColumn({
|
await API.migrateColumn({
|
||||||
tableId: $definition._id,
|
tableId: $definition._id,
|
||||||
oldColumn: column.schema,
|
oldColumn: column.schema.name,
|
||||||
newColumn: {
|
newColumn: newColumnName,
|
||||||
name: newColumnName,
|
|
||||||
type: FieldType.BB_REFERENCE,
|
|
||||||
subtype,
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
notifications.success("Column migrated")
|
notifications.success("Column migrated")
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
<script>
|
<script>
|
||||||
import { setContext, onMount } from "svelte"
|
import { setContext, onMount } from "svelte"
|
||||||
import { writable } from "svelte/store"
|
import { writable, derived } from "svelte/store"
|
||||||
import { fade } from "svelte/transition"
|
import { fade } from "svelte/transition"
|
||||||
import { clickOutside, ProgressCircle } from "@budibase/bbui"
|
import { clickOutside, ProgressCircle } from "@budibase/bbui"
|
||||||
import { createEventManagers } from "../lib/events"
|
import { createEventManagers } from "../lib/events"
|
||||||
|
@ -41,6 +41,7 @@
|
||||||
export let canDeleteRows = true
|
export let canDeleteRows = true
|
||||||
export let canEditColumns = true
|
export let canEditColumns = true
|
||||||
export let canSaveSchema = true
|
export let canSaveSchema = true
|
||||||
|
export let canSelectRows = false
|
||||||
export let stripeRows = false
|
export let stripeRows = false
|
||||||
export let quiet = false
|
export let quiet = false
|
||||||
export let collaboration = true
|
export let collaboration = true
|
||||||
|
@ -53,6 +54,7 @@
|
||||||
export let notifySuccess = null
|
export let notifySuccess = null
|
||||||
export let notifyError = null
|
export let notifyError = null
|
||||||
export let buttons = null
|
export let buttons = null
|
||||||
|
export let isCloud = null
|
||||||
|
|
||||||
// Unique identifier for DOM nodes inside this instance
|
// Unique identifier for DOM nodes inside this instance
|
||||||
const gridID = `grid-${Math.random().toString().slice(2)}`
|
const gridID = `grid-${Math.random().toString().slice(2)}`
|
||||||
|
@ -94,6 +96,7 @@
|
||||||
canDeleteRows,
|
canDeleteRows,
|
||||||
canEditColumns,
|
canEditColumns,
|
||||||
canSaveSchema,
|
canSaveSchema,
|
||||||
|
canSelectRows,
|
||||||
stripeRows,
|
stripeRows,
|
||||||
quiet,
|
quiet,
|
||||||
collaboration,
|
collaboration,
|
||||||
|
@ -106,9 +109,15 @@
|
||||||
notifySuccess,
|
notifySuccess,
|
||||||
notifyError,
|
notifyError,
|
||||||
buttons,
|
buttons,
|
||||||
|
isCloud,
|
||||||
})
|
})
|
||||||
$: minHeight =
|
|
||||||
Padding + SmallRowHeight + $rowHeight + (showControls ? ControlsHeight : 0)
|
// Derive min height and make available in context
|
||||||
|
const minHeight = derived(rowHeight, $height => {
|
||||||
|
const heightForControls = showControls ? ControlsHeight : 0
|
||||||
|
return Padding + SmallRowHeight + $height + heightForControls
|
||||||
|
})
|
||||||
|
context = { ...context, minHeight }
|
||||||
|
|
||||||
// Set context for children to consume
|
// Set context for children to consume
|
||||||
setContext("grid", context)
|
setContext("grid", context)
|
||||||
|
@ -134,7 +143,7 @@
|
||||||
class:quiet
|
class:quiet
|
||||||
on:mouseenter={() => gridFocused.set(true)}
|
on:mouseenter={() => gridFocused.set(true)}
|
||||||
on:mouseleave={() => gridFocused.set(false)}
|
on:mouseleave={() => gridFocused.set(false)}
|
||||||
style="--row-height:{$rowHeight}px; --default-row-height:{DefaultRowHeight}px; --gutter-width:{GutterWidth}px; --max-cell-render-overflow:{MaxCellRenderOverflow}px; --content-lines:{$contentLines}; --min-height:{minHeight}px; --controls-height:{ControlsHeight}px;"
|
style="--row-height:{$rowHeight}px; --default-row-height:{DefaultRowHeight}px; --gutter-width:{GutterWidth}px; --max-cell-render-overflow:{MaxCellRenderOverflow}px; --content-lines:{$contentLines}; --min-height:{$minHeight}px; --controls-height:{ControlsHeight}px;"
|
||||||
>
|
>
|
||||||
{#if showControls}
|
{#if showControls}
|
||||||
<div class="controls">
|
<div class="controls">
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
<script>
|
<script>
|
||||||
import { getContext } from "svelte"
|
import { getContext } from "svelte"
|
||||||
import DataCell from "../cells/DataCell.svelte"
|
import DataCell from "../cells/DataCell.svelte"
|
||||||
|
import { getCellID } from "../lib/utils"
|
||||||
|
|
||||||
export let row
|
export let row
|
||||||
export let top = false
|
export let top = false
|
||||||
|
@ -38,7 +39,7 @@
|
||||||
on:click={() => dispatch("rowclick", rows.actions.cleanRow(row))}
|
on:click={() => dispatch("rowclick", rows.actions.cleanRow(row))}
|
||||||
>
|
>
|
||||||
{#each $visibleColumns as column, columnIdx}
|
{#each $visibleColumns as column, columnIdx}
|
||||||
{@const cellId = `${row._id}-${column.name}`}
|
{@const cellId = getCellID(row._id, column.name)}
|
||||||
<DataCell
|
<DataCell
|
||||||
{cellId}
|
{cellId}
|
||||||
{column}
|
{column}
|
||||||
|
|
|
@ -7,6 +7,7 @@
|
||||||
import { GutterWidth, NewRowID } from "../lib/constants"
|
import { GutterWidth, NewRowID } from "../lib/constants"
|
||||||
import GutterCell from "../cells/GutterCell.svelte"
|
import GutterCell from "../cells/GutterCell.svelte"
|
||||||
import KeyboardShortcut from "./KeyboardShortcut.svelte"
|
import KeyboardShortcut from "./KeyboardShortcut.svelte"
|
||||||
|
import { getCellID } from "../lib/utils"
|
||||||
|
|
||||||
const {
|
const {
|
||||||
hoveredRowId,
|
hoveredRowId,
|
||||||
|
@ -70,7 +71,7 @@
|
||||||
|
|
||||||
// Select the first cell if possible
|
// Select the first cell if possible
|
||||||
if (firstColumn) {
|
if (firstColumn) {
|
||||||
$focusedCellId = `${savedRow._id}-${firstColumn.name}`
|
$focusedCellId = getCellID(savedRow._id, firstColumn.name)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
isAdding = false
|
isAdding = false
|
||||||
|
@ -118,7 +119,7 @@
|
||||||
visible = true
|
visible = true
|
||||||
$hoveredRowId = NewRowID
|
$hoveredRowId = NewRowID
|
||||||
if (firstColumn) {
|
if (firstColumn) {
|
||||||
$focusedCellId = `${NewRowID}-${firstColumn.name}`
|
$focusedCellId = getCellID(NewRowID, firstColumn.name)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Attach key listener
|
// Attach key listener
|
||||||
|
@ -194,7 +195,7 @@
|
||||||
{/if}
|
{/if}
|
||||||
</GutterCell>
|
</GutterCell>
|
||||||
{#if $stickyColumn}
|
{#if $stickyColumn}
|
||||||
{@const cellId = `${NewRowID}-${$stickyColumn.name}`}
|
{@const cellId = getCellID(NewRowID, $stickyColumn.name)}
|
||||||
<DataCell
|
<DataCell
|
||||||
{cellId}
|
{cellId}
|
||||||
rowFocused
|
rowFocused
|
||||||
|
|
|
@ -8,6 +8,7 @@
|
||||||
import { GutterWidth, BlankRowID } from "../lib/constants"
|
import { GutterWidth, BlankRowID } from "../lib/constants"
|
||||||
import GutterCell from "../cells/GutterCell.svelte"
|
import GutterCell from "../cells/GutterCell.svelte"
|
||||||
import KeyboardShortcut from "./KeyboardShortcut.svelte"
|
import KeyboardShortcut from "./KeyboardShortcut.svelte"
|
||||||
|
import { getCellID } from "../lib/utils"
|
||||||
|
|
||||||
const {
|
const {
|
||||||
rows,
|
rows,
|
||||||
|
@ -71,7 +72,7 @@
|
||||||
{@const rowSelected = !!$selectedRows[row._id]}
|
{@const rowSelected = !!$selectedRows[row._id]}
|
||||||
{@const rowHovered = $hoveredRowId === row._id}
|
{@const rowHovered = $hoveredRowId === row._id}
|
||||||
{@const rowFocused = $focusedRow?._id === row._id}
|
{@const rowFocused = $focusedRow?._id === row._id}
|
||||||
{@const cellId = `${row._id}-${$stickyColumn?.name}`}
|
{@const cellId = getCellID(row._id, $stickyColumn?.name)}
|
||||||
<div
|
<div
|
||||||
class="row"
|
class="row"
|
||||||
on:mouseenter={$isDragging ? null : () => ($hoveredRowId = row._id)}
|
on:mouseenter={$isDragging ? null : () => ($hoveredRowId = row._id)}
|
||||||
|
|
|
@ -13,6 +13,7 @@ import JSONCell from "../cells/JSONCell.svelte"
|
||||||
import AttachmentCell from "../cells/AttachmentCell.svelte"
|
import AttachmentCell from "../cells/AttachmentCell.svelte"
|
||||||
import AttachmentSingleCell from "../cells/AttachmentSingleCell.svelte"
|
import AttachmentSingleCell from "../cells/AttachmentSingleCell.svelte"
|
||||||
import BBReferenceCell from "../cells/BBReferenceCell.svelte"
|
import BBReferenceCell from "../cells/BBReferenceCell.svelte"
|
||||||
|
import BBReferenceSingleCell from "../cells/BBReferenceSingleCell.svelte"
|
||||||
|
|
||||||
const TypeComponentMap = {
|
const TypeComponentMap = {
|
||||||
[FieldType.STRING]: TextCell,
|
[FieldType.STRING]: TextCell,
|
||||||
|
@ -29,6 +30,7 @@ const TypeComponentMap = {
|
||||||
[FieldType.FORMULA]: FormulaCell,
|
[FieldType.FORMULA]: FormulaCell,
|
||||||
[FieldType.JSON]: JSONCell,
|
[FieldType.JSON]: JSONCell,
|
||||||
[FieldType.BB_REFERENCE]: BBReferenceCell,
|
[FieldType.BB_REFERENCE]: BBReferenceCell,
|
||||||
|
[FieldType.BB_REFERENCE_SINGLE]: BBReferenceSingleCell,
|
||||||
}
|
}
|
||||||
export const getCellRenderer = column => {
|
export const getCellRenderer = column => {
|
||||||
return TypeComponentMap[column?.schema?.type] || TextCell
|
return TypeComponentMap[column?.schema?.type] || TextCell
|
||||||
|
|
|
@ -1,5 +1,23 @@
|
||||||
|
import { helpers } from "@budibase/shared-core"
|
||||||
import { TypeIconMap } from "../../../constants"
|
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) => {
|
export const getColor = (idx, opacity = 0.3) => {
|
||||||
if (idx == null || idx === -1) {
|
if (idx == null || idx === -1) {
|
||||||
idx = 0
|
idx = 0
|
||||||
|
@ -11,8 +29,12 @@ export const getColumnIcon = column => {
|
||||||
if (column.schema.autocolumn) {
|
if (column.schema.autocolumn) {
|
||||||
return "MagicWand"
|
return "MagicWand"
|
||||||
}
|
}
|
||||||
const { type, subtype } = column.schema
|
|
||||||
|
|
||||||
|
if (helpers.schema.isDeprecatedSingleUserColumn(column.schema)) {
|
||||||
|
return "User"
|
||||||
|
}
|
||||||
|
|
||||||
|
const { type, subtype } = column.schema
|
||||||
const result =
|
const result =
|
||||||
typeof TypeIconMap[type] === "object" && subtype
|
typeof TypeIconMap[type] === "object" && subtype
|
||||||
? TypeIconMap[type][subtype]
|
? TypeIconMap[type][subtype]
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
import { getContext, onMount } from "svelte"
|
import { getContext, onMount } from "svelte"
|
||||||
import { debounce } from "../../../utils/utils"
|
import { debounce } from "../../../utils/utils"
|
||||||
import { NewRowID } from "../lib/constants"
|
import { NewRowID } from "../lib/constants"
|
||||||
|
import { getCellID, parseCellID } from "../lib/utils"
|
||||||
|
|
||||||
const {
|
const {
|
||||||
rows,
|
rows,
|
||||||
|
@ -154,7 +155,7 @@
|
||||||
if (!firstColumn) {
|
if (!firstColumn) {
|
||||||
return
|
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
|
// Changes the focused cell by moving it left or right to a different column
|
||||||
|
@ -163,8 +164,7 @@
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const cols = $visibleColumns
|
const cols = $visibleColumns
|
||||||
const split = $focusedCellId.split("-")
|
const { id, field: columnName } = parseCellID($focusedCellId)
|
||||||
const columnName = split[1]
|
|
||||||
let newColumnName
|
let newColumnName
|
||||||
if (columnName === $stickyColumn?.name) {
|
if (columnName === $stickyColumn?.name) {
|
||||||
const index = delta - 1
|
const index = delta - 1
|
||||||
|
@ -178,7 +178,7 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (newColumnName) {
|
if (newColumnName) {
|
||||||
$focusedCellId = `${split[0]}-${newColumnName}`
|
$focusedCellId = getCellID(id, newColumnName)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -189,8 +189,8 @@
|
||||||
}
|
}
|
||||||
const newRow = $rows[$focusedRow.__idx + delta]
|
const newRow = $rows[$focusedRow.__idx + delta]
|
||||||
if (newRow) {
|
if (newRow) {
|
||||||
const split = $focusedCellId.split("-")
|
const { field } = parseCellID($focusedCellId)
|
||||||
$focusedCellId = `${newRow._id}-${split[1]}`
|
$focusedCellId = getCellID(newRow._id, field)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -3,6 +3,7 @@
|
||||||
import { getContext } from "svelte"
|
import { getContext } from "svelte"
|
||||||
import { NewRowID } from "../lib/constants"
|
import { NewRowID } from "../lib/constants"
|
||||||
import GridPopover from "./GridPopover.svelte"
|
import GridPopover from "./GridPopover.svelte"
|
||||||
|
import { getCellID } from "../lib/utils"
|
||||||
|
|
||||||
const {
|
const {
|
||||||
focusedRow,
|
focusedRow,
|
||||||
|
@ -41,7 +42,7 @@
|
||||||
const newRow = await rows.actions.duplicateRow($focusedRow)
|
const newRow = await rows.actions.duplicateRow($focusedRow)
|
||||||
if (newRow) {
|
if (newRow) {
|
||||||
const column = $stickyColumn?.name || $columns[0].name
|
const column = $stickyColumn?.name || $columns[0].name
|
||||||
$focusedCellId = `${newRow._id}-${column}`
|
$focusedCellId = getCellID(newRow._id, column)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
import { derived, get, writable } from "svelte/store"
|
import { derived, get, writable } from "svelte/store"
|
||||||
import { cloneDeep } from "lodash/fp"
|
|
||||||
import { GutterWidth, DefaultColumnWidth } from "../lib/constants"
|
import { GutterWidth, DefaultColumnWidth } from "../lib/constants"
|
||||||
|
|
||||||
export const createStores = () => {
|
export const createStores = () => {
|
||||||
|
@ -75,72 +74,23 @@ export const deriveStores = context => {
|
||||||
}
|
}
|
||||||
|
|
||||||
export const createActions = context => {
|
export const createActions = context => {
|
||||||
const { columns, stickyColumn, datasource, definition, schema } = context
|
const { columns, datasource, schema } = context
|
||||||
|
|
||||||
// Updates the datasources primary display column
|
|
||||||
const changePrimaryDisplay = async column => {
|
|
||||||
return await datasource.actions.saveDefinition({
|
|
||||||
...get(definition),
|
|
||||||
primaryDisplay: column,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Updates the width of all columns
|
// Updates the width of all columns
|
||||||
const changeAllColumnWidths = async width => {
|
const changeAllColumnWidths = async width => {
|
||||||
columns.update(state => {
|
const $schema = get(schema)
|
||||||
return state.map(col => ({
|
let mutations = {}
|
||||||
...col,
|
Object.keys($schema).forEach(field => {
|
||||||
width,
|
mutations[field] = { width }
|
||||||
}))
|
|
||||||
})
|
|
||||||
if (get(stickyColumn)) {
|
|
||||||
stickyColumn.update(state => ({
|
|
||||||
...state,
|
|
||||||
width,
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
await saveChanges()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Persists column changes by saving metadata against datasource schema
|
|
||||||
const saveChanges = async () => {
|
|
||||||
const $columns = get(columns)
|
|
||||||
const $definition = get(definition)
|
|
||||||
const $stickyColumn = get(stickyColumn)
|
|
||||||
let newSchema = cloneDeep(get(schema)) || {}
|
|
||||||
|
|
||||||
// Build new updated datasource schema
|
|
||||||
Object.keys(newSchema).forEach(column => {
|
|
||||||
// Respect order specified by columns
|
|
||||||
const index = $columns.findIndex(x => x.name === column)
|
|
||||||
if (index !== -1) {
|
|
||||||
newSchema[column].order = index
|
|
||||||
} else {
|
|
||||||
delete newSchema[column].order
|
|
||||||
}
|
|
||||||
|
|
||||||
// Copy over metadata
|
|
||||||
if (column === $stickyColumn?.name) {
|
|
||||||
newSchema[column].visible = true
|
|
||||||
newSchema[column].width = $stickyColumn.width || DefaultColumnWidth
|
|
||||||
} else {
|
|
||||||
newSchema[column].visible = $columns[index]?.visible ?? true
|
|
||||||
newSchema[column].width = $columns[index]?.width || DefaultColumnWidth
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
await datasource.actions.saveDefinition({
|
|
||||||
...$definition,
|
|
||||||
schema: newSchema,
|
|
||||||
})
|
})
|
||||||
|
datasource.actions.addSchemaMutations(mutations)
|
||||||
|
await datasource.actions.saveSchemaMutations()
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
columns: {
|
columns: {
|
||||||
...columns,
|
...columns,
|
||||||
actions: {
|
actions: {
|
||||||
saveChanges,
|
|
||||||
changePrimaryDisplay,
|
|
||||||
changeAllColumnWidths,
|
changeAllColumnWidths,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
|
@ -4,15 +4,23 @@ import { memo } from "../../../utils"
|
||||||
|
|
||||||
export const createStores = () => {
|
export const createStores = () => {
|
||||||
const definition = memo(null)
|
const definition = memo(null)
|
||||||
|
const schemaMutations = memo({})
|
||||||
|
|
||||||
return {
|
return {
|
||||||
definition,
|
definition,
|
||||||
|
schemaMutations,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const deriveStores = context => {
|
export const deriveStores = context => {
|
||||||
const { API, definition, schemaOverrides, columnWhitelist, datasource } =
|
const {
|
||||||
context
|
API,
|
||||||
|
definition,
|
||||||
|
schemaOverrides,
|
||||||
|
columnWhitelist,
|
||||||
|
datasource,
|
||||||
|
schemaMutations,
|
||||||
|
} = context
|
||||||
|
|
||||||
const schema = derived(definition, $definition => {
|
const schema = derived(definition, $definition => {
|
||||||
let schema = getDatasourceSchema({
|
let schema = getDatasourceSchema({
|
||||||
|
@ -35,42 +43,26 @@ export const deriveStores = context => {
|
||||||
return schema
|
return schema
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Derives the total enriched schema, made up of the saved schema and any
|
||||||
|
// prop and user overrides
|
||||||
const enrichedSchema = derived(
|
const enrichedSchema = derived(
|
||||||
[schema, schemaOverrides, columnWhitelist],
|
[schema, schemaOverrides, schemaMutations, columnWhitelist],
|
||||||
([$schema, $schemaOverrides, $columnWhitelist]) => {
|
([$schema, $schemaOverrides, $schemaMutations, $columnWhitelist]) => {
|
||||||
if (!$schema) {
|
if (!$schema) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
let enrichedSchema = { ...$schema }
|
let enrichedSchema = {}
|
||||||
|
Object.keys($schema).forEach(field => {
|
||||||
// Apply schema overrides
|
// Apply whitelist if provided
|
||||||
Object.keys($schemaOverrides || {}).forEach(field => {
|
if ($columnWhitelist?.length && !$columnWhitelist.includes(field)) {
|
||||||
if (enrichedSchema[field]) {
|
return
|
||||||
enrichedSchema[field] = {
|
}
|
||||||
...enrichedSchema[field],
|
enrichedSchema[field] = {
|
||||||
...$schemaOverrides[field],
|
...$schema[field],
|
||||||
}
|
...$schemaOverrides?.[field],
|
||||||
|
...$schemaMutations[field],
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
// Apply whitelist if specified
|
|
||||||
if ($columnWhitelist?.length) {
|
|
||||||
const sortedColumns = {}
|
|
||||||
|
|
||||||
$columnWhitelist.forEach((columnKey, idx) => {
|
|
||||||
const enrichedColumn = enrichedSchema[columnKey]
|
|
||||||
if (enrichedColumn) {
|
|
||||||
sortedColumns[columnKey] = {
|
|
||||||
...enrichedColumn,
|
|
||||||
order: idx,
|
|
||||||
visible: true,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
return sortedColumns
|
|
||||||
}
|
|
||||||
|
|
||||||
return enrichedSchema
|
return enrichedSchema
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
@ -100,6 +92,8 @@ export const createActions = context => {
|
||||||
table,
|
table,
|
||||||
viewV2,
|
viewV2,
|
||||||
nonPlus,
|
nonPlus,
|
||||||
|
schemaMutations,
|
||||||
|
schema,
|
||||||
} = context
|
} = context
|
||||||
|
|
||||||
// Gets the appropriate API for the configured datasource type
|
// Gets the appropriate API for the configured datasource type
|
||||||
|
@ -136,11 +130,81 @@ export const createActions = context => {
|
||||||
// Update server
|
// Update server
|
||||||
if (get(config).canSaveSchema) {
|
if (get(config).canSaveSchema) {
|
||||||
await getAPI()?.actions.saveDefinition(newDefinition)
|
await getAPI()?.actions.saveDefinition(newDefinition)
|
||||||
}
|
|
||||||
|
|
||||||
// Broadcast change to external state can be updated, as this change
|
// Broadcast change so external state can be updated, as this change
|
||||||
// will not be received by the builder websocket because we caused it ourselves
|
// will not be received by the builder websocket because we caused it
|
||||||
dispatch("updatedatasource", newDefinition)
|
// ourselves
|
||||||
|
dispatch("updatedatasource", newDefinition)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Updates the datasources primary display column
|
||||||
|
const changePrimaryDisplay = async column => {
|
||||||
|
return await saveDefinition({
|
||||||
|
...get(definition),
|
||||||
|
primaryDisplay: column,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Adds a schema mutation for a single field
|
||||||
|
const addSchemaMutation = (field, mutation) => {
|
||||||
|
if (!field || !mutation) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
schemaMutations.update($schemaMutations => {
|
||||||
|
return {
|
||||||
|
...$schemaMutations,
|
||||||
|
[field]: {
|
||||||
|
...$schemaMutations[field],
|
||||||
|
...mutation,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Adds schema mutations for multiple fields at once
|
||||||
|
const addSchemaMutations = mutations => {
|
||||||
|
const fields = Object.keys(mutations || {})
|
||||||
|
if (!fields.length) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
schemaMutations.update($schemaMutations => {
|
||||||
|
let newSchemaMutations = { ...$schemaMutations }
|
||||||
|
fields.forEach(field => {
|
||||||
|
newSchemaMutations[field] = {
|
||||||
|
...newSchemaMutations[field],
|
||||||
|
...mutations[field],
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return newSchemaMutations
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Saves schema changes to the server, if possible
|
||||||
|
const saveSchemaMutations = async () => {
|
||||||
|
// If we can't save schema changes then we just want to keep this in memory
|
||||||
|
if (!get(config).canSaveSchema) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const $definition = get(definition)
|
||||||
|
const $schemaMutations = get(schemaMutations)
|
||||||
|
const $schema = get(schema)
|
||||||
|
let newSchema = {}
|
||||||
|
|
||||||
|
// Build new updated datasource schema
|
||||||
|
Object.keys($schema).forEach(column => {
|
||||||
|
newSchema[column] = {
|
||||||
|
...$schema[column],
|
||||||
|
...$schemaMutations[column],
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Save the changes, then reset our local mutations
|
||||||
|
await saveDefinition({
|
||||||
|
...$definition,
|
||||||
|
schema: newSchema,
|
||||||
|
})
|
||||||
|
schemaMutations.set({})
|
||||||
}
|
}
|
||||||
|
|
||||||
// Adds a row to the datasource
|
// Adds a row to the datasource
|
||||||
|
@ -185,6 +249,10 @@ export const createActions = context => {
|
||||||
getRow,
|
getRow,
|
||||||
isDatasourceValid,
|
isDatasourceValid,
|
||||||
canUseColumn,
|
canUseColumn,
|
||||||
|
changePrimaryDisplay,
|
||||||
|
addSchemaMutation,
|
||||||
|
addSchemaMutations,
|
||||||
|
saveSchemaMutations,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
|
@ -34,6 +34,7 @@ export const createActions = context => {
|
||||||
stickyColumn,
|
stickyColumn,
|
||||||
maxScrollLeft,
|
maxScrollLeft,
|
||||||
width,
|
width,
|
||||||
|
datasource,
|
||||||
} = context
|
} = context
|
||||||
|
|
||||||
let autoScrollInterval
|
let autoScrollInterval
|
||||||
|
@ -173,20 +174,17 @@ export const createActions = context => {
|
||||||
document.removeEventListener("touchend", stopReordering)
|
document.removeEventListener("touchend", stopReordering)
|
||||||
document.removeEventListener("touchcancel", stopReordering)
|
document.removeEventListener("touchcancel", stopReordering)
|
||||||
|
|
||||||
// Ensure there's actually a change
|
// Ensure there's actually a change before saving
|
||||||
let { sourceColumn, targetColumn } = get(reorder)
|
const { sourceColumn, targetColumn } = get(reorder)
|
||||||
if (sourceColumn !== targetColumn) {
|
|
||||||
moveColumn(sourceColumn, targetColumn)
|
|
||||||
await columns.actions.saveChanges()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Reset state
|
|
||||||
reorder.set(reorderInitialState)
|
reorder.set(reorderInitialState)
|
||||||
|
if (sourceColumn !== targetColumn) {
|
||||||
|
await moveColumn(sourceColumn, targetColumn)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Moves a column after another columns.
|
// Moves a column after another columns.
|
||||||
// An undefined target column will move the source to index 0.
|
// An undefined target column will move the source to index 0.
|
||||||
const moveColumn = (sourceColumn, targetColumn) => {
|
const moveColumn = async (sourceColumn, targetColumn) => {
|
||||||
let $columns = get(columns)
|
let $columns = get(columns)
|
||||||
let sourceIdx = $columns.findIndex(x => x.name === sourceColumn)
|
let sourceIdx = $columns.findIndex(x => x.name === sourceColumn)
|
||||||
let targetIdx = $columns.findIndex(x => x.name === targetColumn)
|
let targetIdx = $columns.findIndex(x => x.name === targetColumn)
|
||||||
|
@ -198,14 +196,21 @@ export const createActions = context => {
|
||||||
}
|
}
|
||||||
return state.toSpliced(targetIdx, 0, removed[0])
|
return state.toSpliced(targetIdx, 0, removed[0])
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Extract new orders as schema mutations
|
||||||
|
let mutations = {}
|
||||||
|
get(columns).forEach((column, idx) => {
|
||||||
|
mutations[column.name] = { order: idx }
|
||||||
|
})
|
||||||
|
datasource.actions.addSchemaMutations(mutations)
|
||||||
|
await datasource.actions.saveSchemaMutations()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Moves a column one place left (as appears visually)
|
// Moves a column one place left (as appears visually)
|
||||||
const moveColumnLeft = async column => {
|
const moveColumnLeft = async column => {
|
||||||
const $visibleColumns = get(visibleColumns)
|
const $visibleColumns = get(visibleColumns)
|
||||||
const sourceIdx = $visibleColumns.findIndex(x => x.name === column)
|
const sourceIdx = $visibleColumns.findIndex(x => x.name === column)
|
||||||
moveColumn(column, $visibleColumns[sourceIdx - 2]?.name)
|
await moveColumn(column, $visibleColumns[sourceIdx - 2]?.name)
|
||||||
await columns.actions.saveChanges()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Moves a column one place right (as appears visually)
|
// Moves a column one place right (as appears visually)
|
||||||
|
@ -215,8 +220,7 @@ export const createActions = context => {
|
||||||
if (sourceIdx === $visibleColumns.length - 1) {
|
if (sourceIdx === $visibleColumns.length - 1) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
moveColumn(column, $visibleColumns[sourceIdx + 1]?.name)
|
await moveColumn(column, $visibleColumns[sourceIdx + 1]?.name)
|
||||||
await columns.actions.saveChanges()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
|
@ -6,7 +6,6 @@ const initialState = {
|
||||||
initialMouseX: null,
|
initialMouseX: null,
|
||||||
initialWidth: null,
|
initialWidth: null,
|
||||||
column: null,
|
column: null,
|
||||||
columnIdx: null,
|
|
||||||
width: 0,
|
width: 0,
|
||||||
left: 0,
|
left: 0,
|
||||||
}
|
}
|
||||||
|
@ -21,7 +20,7 @@ export const createStores = () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
export const createActions = context => {
|
export const createActions = context => {
|
||||||
const { resize, columns, stickyColumn, ui } = context
|
const { resize, ui, datasource } = context
|
||||||
|
|
||||||
// Starts resizing a certain column
|
// Starts resizing a certain column
|
||||||
const startResizing = (column, e) => {
|
const startResizing = (column, e) => {
|
||||||
|
@ -32,12 +31,6 @@ export const createActions = context => {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
ui.actions.blur()
|
ui.actions.blur()
|
||||||
|
|
||||||
// Find and cache index
|
|
||||||
let columnIdx = get(columns).findIndex(col => col.name === column.name)
|
|
||||||
if (columnIdx === -1) {
|
|
||||||
columnIdx = "sticky"
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set initial store state
|
// Set initial store state
|
||||||
resize.set({
|
resize.set({
|
||||||
width: column.width,
|
width: column.width,
|
||||||
|
@ -45,7 +38,6 @@ export const createActions = context => {
|
||||||
initialWidth: column.width,
|
initialWidth: column.width,
|
||||||
initialMouseX: x,
|
initialMouseX: x,
|
||||||
column: column.name,
|
column: column.name,
|
||||||
columnIdx,
|
|
||||||
})
|
})
|
||||||
|
|
||||||
// Add mouse event listeners to handle resizing
|
// Add mouse event listeners to handle resizing
|
||||||
|
@ -58,7 +50,7 @@ export const createActions = context => {
|
||||||
|
|
||||||
// Handler for moving the mouse to resize columns
|
// Handler for moving the mouse to resize columns
|
||||||
const onResizeMouseMove = e => {
|
const onResizeMouseMove = e => {
|
||||||
const { initialMouseX, initialWidth, width, columnIdx } = get(resize)
|
const { initialMouseX, initialWidth, width, column } = get(resize)
|
||||||
const { x } = parseEventLocation(e)
|
const { x } = parseEventLocation(e)
|
||||||
const dx = x - initialMouseX
|
const dx = x - initialMouseX
|
||||||
const newWidth = Math.round(Math.max(MinColumnWidth, initialWidth + dx))
|
const newWidth = Math.round(Math.max(MinColumnWidth, initialWidth + dx))
|
||||||
|
@ -69,17 +61,7 @@ export const createActions = context => {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update column state
|
// Update column state
|
||||||
if (columnIdx === "sticky") {
|
datasource.actions.addSchemaMutation(column, { width })
|
||||||
stickyColumn.update(state => ({
|
|
||||||
...state,
|
|
||||||
width: newWidth,
|
|
||||||
}))
|
|
||||||
} else {
|
|
||||||
columns.update(state => {
|
|
||||||
state[columnIdx].width = newWidth
|
|
||||||
return [...state]
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update state
|
// Update state
|
||||||
resize.update(state => ({
|
resize.update(state => ({
|
||||||
|
@ -101,26 +83,16 @@ export const createActions = context => {
|
||||||
|
|
||||||
// Persist width if it changed
|
// Persist width if it changed
|
||||||
if ($resize.width !== $resize.initialWidth) {
|
if ($resize.width !== $resize.initialWidth) {
|
||||||
await columns.actions.saveChanges()
|
await datasource.actions.saveSchemaMutations()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Resets a column size back to default
|
// Resets a column size back to default
|
||||||
const resetSize = async column => {
|
const resetSize = async column => {
|
||||||
const $stickyColumn = get(stickyColumn)
|
datasource.actions.addSchemaMutation(column.name, {
|
||||||
if (column.name === $stickyColumn?.name) {
|
width: DefaultColumnWidth,
|
||||||
stickyColumn.update(state => ({
|
})
|
||||||
...state,
|
await datasource.actions.saveSchemaMutations()
|
||||||
width: DefaultColumnWidth,
|
|
||||||
}))
|
|
||||||
} else {
|
|
||||||
columns.update(state => {
|
|
||||||
const columnIdx = state.findIndex(x => x.name === column.name)
|
|
||||||
state[columnIdx].width = DefaultColumnWidth
|
|
||||||
return [...state]
|
|
||||||
})
|
|
||||||
}
|
|
||||||
await columns.actions.saveChanges()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import { writable, derived, get } from "svelte/store"
|
import { writable, derived, get } from "svelte/store"
|
||||||
import { fetchData } from "../../../fetch"
|
import { fetchData } from "../../../fetch"
|
||||||
import { NewRowID, RowPageSize } from "../lib/constants"
|
import { NewRowID, RowPageSize } from "../lib/constants"
|
||||||
|
import { getCellID, parseCellID } from "../lib/utils"
|
||||||
import { tick } from "svelte"
|
import { tick } from "svelte"
|
||||||
import { Helpers } from "@budibase/bbui"
|
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
|
// If the server doesn't reply with a valid error, assume that the source
|
||||||
// of the error is the focused cell's column
|
// of the error is the focused cell's column
|
||||||
if (!error?.json?.validationErrors && errorString) {
|
if (!error?.json?.validationErrors && errorString) {
|
||||||
const focusedColumn = get(focusedCellId)?.split("-")[1]
|
const { field: focusedColumn } = parseCellID(get(focusedCellId))
|
||||||
if (focusedColumn) {
|
if (focusedColumn) {
|
||||||
error = {
|
error = {
|
||||||
json: {
|
json: {
|
||||||
|
@ -245,7 +246,7 @@ export const createActions = context => {
|
||||||
}
|
}
|
||||||
// Set error against the cell
|
// Set error against the cell
|
||||||
validation.actions.setError(
|
validation.actions.setError(
|
||||||
`${rowId}-${column}`,
|
getCellID(rowId, column),
|
||||||
Helpers.capitalise(err)
|
Helpers.capitalise(err)
|
||||||
)
|
)
|
||||||
// Ensure the column is visible
|
// Ensure the column is visible
|
||||||
|
@ -265,7 +266,7 @@ export const createActions = context => {
|
||||||
|
|
||||||
// Focus the first cell with an error
|
// Focus the first cell with an error
|
||||||
if (erroredColumns.length) {
|
if (erroredColumns.length) {
|
||||||
focusedCellId.set(`${rowId}-${erroredColumns[0]}`)
|
focusedCellId.set(getCellID(rowId, erroredColumns[0]))
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
get(notifications).error(errorString || "An unknown error occurred")
|
get(notifications).error(errorString || "An unknown error occurred")
|
||||||
|
@ -571,9 +572,10 @@ export const initialise = context => {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// Stop if we changed row
|
// Stop if we changed row
|
||||||
const oldRowId = id.split("-")[0]
|
const split = parseCellID(id)
|
||||||
const oldColumn = id.split("-")[1]
|
const oldRowId = split.id
|
||||||
const newRowId = get(focusedCellId)?.split("-")[0]
|
const oldColumn = split.field
|
||||||
|
const { id: newRowId } = parseCellID(get(focusedCellId))
|
||||||
if (oldRowId !== newRowId) {
|
if (oldRowId !== newRowId) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import { writable, derived, get } from "svelte/store"
|
import { writable, derived, get } from "svelte/store"
|
||||||
import { tick } from "svelte"
|
import { tick } from "svelte"
|
||||||
import { Padding, GutterWidth, FocusedCellMinOffset } from "../lib/constants"
|
import { Padding, GutterWidth, FocusedCellMinOffset } from "../lib/constants"
|
||||||
|
import { parseCellID } from "../lib/utils"
|
||||||
|
|
||||||
export const createStores = () => {
|
export const createStores = () => {
|
||||||
const scroll = writable({
|
const scroll = writable({
|
||||||
|
@ -176,7 +177,7 @@ export const initialise = context => {
|
||||||
// Ensure horizontal position is viewable
|
// Ensure horizontal position is viewable
|
||||||
// Check horizontal position of columns next
|
// Check horizontal position of columns next
|
||||||
const $visibleColumns = get(visibleColumns)
|
const $visibleColumns = get(visibleColumns)
|
||||||
const columnName = $focusedCellId?.split("-")[1]
|
const { field: columnName } = parseCellID($focusedCellId)
|
||||||
const column = $visibleColumns.find(col => col.name === columnName)
|
const column = $visibleColumns.find(col => col.name === columnName)
|
||||||
if (!column) {
|
if (!column) {
|
||||||
return
|
return
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue