merge with develop
This commit is contained in:
commit
a495050871
|
@ -104,12 +104,14 @@ Budibase is made to scale. With Budibase, you can self-host on your own infrastr
|
||||||
|
|
||||||
## 🏁 Get started
|
## 🏁 Get started
|
||||||
|
|
||||||
<img src="https://res.cloudinary.com/daog6scxm/image/upload/v1634808888/logo/deploy_npl9za.png" />
|
<a href="https://docs.budibase.com/self-hosting/self-host"><img src="https://res.cloudinary.com/daog6scxm/image/upload/v1634808888/logo/deploy_npl9za.png" /></a>
|
||||||
|
|
||||||
Deploy Budibase self-Hosted in your existing infrastructure, using Docker, Kubernetes, and Digital Ocean.
|
Deploy Budibase self-hosted in your existing infrastructure, using Docker, Kubernetes, and Digital Ocean.
|
||||||
Or use Budibase Cloud if you don't need to self-host, and would like to get started quickly.
|
Or use Budibase Cloud if you don't need to self-host, and would like to get started quickly.
|
||||||
|
|
||||||
### [Get started with Budibase](https://budibase.com)
|
### [Get started with self-hosting Budibase](https://docs.budibase.com/self-hosting/self-host)
|
||||||
|
|
||||||
|
### [Get started with Budibase Cloud](https://budibase.com)
|
||||||
|
|
||||||
|
|
||||||
<br /><br />
|
<br /><br />
|
||||||
|
|
|
@ -0,0 +1,214 @@
|
||||||
|
<p align="center">
|
||||||
|
<a href="https://www.budibase.com">
|
||||||
|
<img alt="Budibase" src="https://d33wubrfki0l68.cloudfront.net/aac32159d7207b5085e74a7ef67afbb7027786c5/2b1fd/img/logo/bb-emblem.svg" width="60" />
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
<h1 align="center">
|
||||||
|
Budibase
|
||||||
|
</h1>
|
||||||
|
|
||||||
|
<h3 align="center">
|
||||||
|
使って楽しいローコードプラットフォーム
|
||||||
|
</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 all releases" src="https://img.shields.io/github/downloads/Budibase/budibase/total">
|
||||||
|
</a>
|
||||||
|
<a href="https://github.com/Budibase/budibase/releases">
|
||||||
|
<img alt="GitHub release (latest by date)" src="https://img.shields.io/github/v/release/Budibase/budibase">
|
||||||
|
</a>
|
||||||
|
<a href="https://twitter.com/intent/follow?screen_name=budibase">
|
||||||
|
<img src="https://img.shields.io/twitter/follow/budibase?style=social" alt="Follow @budibase" />
|
||||||
|
</a>
|
||||||
|
<img src="https://img.shields.io/badge/Contributor%20Covenant-v2.0%20adopted-ff69b4.svg" alt="Code of conduct" />
|
||||||
|
<a href="https://codecov.io/gh/Budibase/budibase">
|
||||||
|
<img src="https://codecov.io/gh/Budibase/budibase/graph/badge.svg?token=E8W2ZFXQOH"/>
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<h3 align="center">
|
||||||
|
<a href="https://docs.budibase.com/getting-started">はじめに</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には、美しくデザインされた強力なコンポーネントが付属しており、それら使用しUIを簡単に構築することができます。また、CSSによるスタイリングオプションも豊富に用意されているので、よりクリエイティブな表現もも可能です。
|
||||||
|
[Request new component](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 />
|
||||||
|
|
||||||
|
### プロセスを自動化し、ほかのツールと連携し、Webhookをでつながる!
|
||||||
|
定型化した作業を自動化して時間を節約しましょう。Webhookに接続、Eメールの自動送信など、すべて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 />
|
||||||
|
|
||||||
|
## 🏁 始めましょう
|
||||||
|
|
||||||
|
<a href="https://docs.budibase.com/self-hosting/self-host"><img src="https://res.cloudinary.com/daog6scxm/image/upload/v1634808888/logo/deploy_npl9za.png" /></a>
|
||||||
|
|
||||||
|
Docker、KubernetesもしくはDegital Oceanを使用しセルフホスティングするか、セルフホスティングに困難がある、もしくは今すぐ開始したい場合はBudibase Cloudを使用しすぐに始めましょう。
|
||||||
|
|
||||||
|
### [Budibaseをセルフホスティングする](https://docs.budibase.com/self-hosting/self-host)
|
||||||
|
|
||||||
|
### [Budibase Cloudを使用する](https://budibase.com)
|
||||||
|
|
||||||
|
|
||||||
|
<br /><br />
|
||||||
|
|
||||||
|
## 🎓 Budibaseを学ぶ
|
||||||
|
|
||||||
|
Budibaseのドキュメント[はここです](https://docs.budibase.com)。
|
||||||
|
<br />
|
||||||
|
|
||||||
|
|
||||||
|
<br /><br />
|
||||||
|
|
||||||
|
## 💬 コミュニティ
|
||||||
|
|
||||||
|
もし何か問題がある、もしくはBudibaseコミュニティのほかのユーザーと交流したいのであれば私たちの[Github discussions](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を作成してください。これであなたの貴重な考えは私たちにも伝わり、無駄とはなりません。
|
||||||
|
|
||||||
|
### どこから始めるか混乱していますか?
|
||||||
|
ここはコントリビュートをはじめるための最適な場所です! [First time issues project](https://github.com/Budibase/budibase/projects/22).
|
||||||
|
|
||||||
|
### リポジトリの構成
|
||||||
|
Budibaseは、lernaによってmonorepo方式で管理されています。budibase パッケージのビルドと公開はlernaによって管理されています。Budibaseを構成するパッケージは以下の通り:
|
||||||
|
|
||||||
|
- [packages/builder](https://github.com/Budibase/budibase/tree/HEAD/packages/builder) - budibase builder クライアントサイドのsvelteアプリケーションのコードが含まれています。
|
||||||
|
|
||||||
|
- [packages/client](https://github.com/Budibase/budibase/tree/HEAD/packages/client) - ブラウザ上で動作するモジュールで、JSONの定義を読み取り、そこから"生きている"Webアプリケーションを作成します。
|
||||||
|
|
||||||
|
- [packages/server](https://github.com/Budibase/budibase/tree/HEAD/packages/server) - budibaseのサーバーです。この Koa アプリは、builder アプリと budibase アプリの JS を提供し、データベースとファイル システムと対話するための 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 over time](https://starchart.cc/Budibase/budibase.svg)](https://starchart.cc/Budibase/budibase)
|
||||||
|
|
||||||
|
ビルダーのアップデートの間に問題が発生する場合は[ここ](https://github.com/Budibase/budibase/blob/HEAD/.github/CONTRIBUTING.md#troubleshooting)を参考に環境をクリアにしてください。
|
||||||
|
|
||||||
|
<br /><br />
|
||||||
|
|
||||||
|
## Contributors ✨
|
||||||
|
|
||||||
|
すばらしい皆さまに感謝しかありません。([emoji key](https://allcontributors.org/docs/en/emoji-key)):
|
||||||
|
|
||||||
|
<!-- ALL-CONTRIBUTORS-LIST:START - Do not remove or modify this section -->
|
||||||
|
<!-- prettier-ignore-start -->
|
||||||
|
<!-- markdownlint-disable -->
|
||||||
|
<table>
|
||||||
|
<tr>
|
||||||
|
<td align="center"><a href="http://martinmck.com"><img src="https://avatars1.githubusercontent.com/u/11256663?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Martin McKeaveney</b></sub></a><br /><a href="https://github.com/Budibase/budibase/commits?author=shogunpurple" title="Code">💻</a> <a href="https://github.com/Budibase/budibase/commits?author=shogunpurple" title="Documentation">📖</a> <a href="https://github.com/Budibase/budibase/commits?author=shogunpurple" title="Tests">⚠️</a> <a href="#infra-shogunpurple" title="Infrastructure (Hosting, Build-Tools, etc)">🚇</a></td>
|
||||||
|
<td align="center"><a href="http://www.michaeldrury.co.uk/"><img src="https://avatars2.githubusercontent.com/u/4407001?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Michael Drury</b></sub></a><br /><a href="https://github.com/Budibase/budibase/commits?author=mike12345567" title="Documentation">📖</a> <a href="https://github.com/Budibase/budibase/commits?author=mike12345567" title="Code">💻</a> <a href="https://github.com/Budibase/budibase/commits?author=mike12345567" title="Tests">⚠️</a> <a href="#infra-mike12345567" title="Infrastructure (Hosting, Build-Tools, etc)">🚇</a></td>
|
||||||
|
<td align="center"><a href="https://github.com/aptkingston"><img src="https://avatars3.githubusercontent.com/u/9075550?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Andrew Kingston</b></sub></a><br /><a href="https://github.com/Budibase/budibase/commits?author=aptkingston" title="Documentation">📖</a> <a href="https://github.com/Budibase/budibase/commits?author=aptkingston" title="Code">💻</a> <a href="https://github.com/Budibase/budibase/commits?author=aptkingston" title="Tests">⚠️</a> <a href="#design-aptkingston" title="Design">🎨</a></td>
|
||||||
|
<td align="center"><a href="https://budibase.com/"><img src="https://avatars3.githubusercontent.com/u/3524181?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Michael Shanks</b></sub></a><br /><a href="https://github.com/Budibase/budibase/commits?author=mjashanks" title="Documentation">📖</a> <a href="https://github.com/Budibase/budibase/commits?author=mjashanks" title="Code">💻</a> <a href="https://github.com/Budibase/budibase/commits?author=mjashanks" title="Tests">⚠️</a></td>
|
||||||
|
<td align="center"><a href="https://github.com/kevmodrome"><img src="https://avatars3.githubusercontent.com/u/534488?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Kevin Åberg Kultalahti</b></sub></a><br /><a href="https://github.com/Budibase/budibase/commits?author=kevmodrome" title="Documentation">📖</a> <a href="https://github.com/Budibase/budibase/commits?author=kevmodrome" title="Code">💻</a> <a href="https://github.com/Budibase/budibase/commits?author=kevmodrome" title="Tests">⚠️</a></td>
|
||||||
|
<td align="center"><a href="https://www.budibase.com/"><img src="https://avatars2.githubusercontent.com/u/49767913?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Joe</b></sub></a><br /><a href="https://github.com/Budibase/budibase/commits?author=joebudi" title="Documentation">📖</a> <a href="https://github.com/Budibase/budibase/commits?author=joebudi" title="Code">💻</a> <a href="#content-joebudi" title="Content">🖋</a> <a href="#design-joebudi" title="Design">🎨</a></td>
|
||||||
|
<td align="center"><a href="https://github.com/Rory-Powell"><img src="https://avatars.githubusercontent.com/u/8755148?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Rory Powell</b></sub></a><br /><a href="https://github.com/Budibase/budibase/commits?author=Rory-Powell" title="Code">💻</a> <a href="https://github.com/Budibase/budibase/commits?author=Rory-Powell" title="Documentation">📖</a> <a href="https://github.com/Budibase/budibase/commits?author=Rory-Powell" title="Tests">⚠️</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td align="center"><a href="https://github.com/PClmnt"><img src="https://avatars.githubusercontent.com/u/5665926?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Peter Clement</b></sub></a><br /><a href="https://github.com/Budibase/budibase/commits?author=PClmnt" title="Code">💻</a> <a href="https://github.com/Budibase/budibase/commits?author=PClmnt" title="Documentation">📖</a> <a href="https://github.com/Budibase/budibase/commits?author=PClmnt" title="Tests">⚠️</a></td>
|
||||||
|
<td align="center"><a href="https://github.com/Conor-Mack"><img src="https://avatars1.githubusercontent.com/u/36074859?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Conor_Mack</b></sub></a><br /><a href="https://github.com/Budibase/budibase/commits?author=Conor-Mack" title="Code">💻</a> <a href="https://github.com/Budibase/budibase/commits?author=Conor-Mack" title="Tests">⚠️</a></td>
|
||||||
|
<td align="center"><a href="https://github.com/pngwn"><img src="https://avatars1.githubusercontent.com/u/12937446?v=4?s=100" width="100px;" alt=""/><br /><sub><b>pngwn</b></sub></a><br /><a href="https://github.com/Budibase/budibase/commits?author=pngwn" title="Code">💻</a> <a href="https://github.com/Budibase/budibase/commits?author=pngwn" title="Tests">⚠️</a></td>
|
||||||
|
<td align="center"><a href="https://github.com/HugoLd"><img src="https://avatars0.githubusercontent.com/u/26521848?v=4?s=100" width="100px;" alt=""/><br /><sub><b>HugoLd</b></sub></a><br /><a href="https://github.com/Budibase/budibase/commits?author=HugoLd" title="Code">💻</a></td>
|
||||||
|
<td align="center"><a href="https://github.com/victoriasloan"><img src="https://avatars.githubusercontent.com/u/9913651?v=4?s=100" width="100px;" alt=""/><br /><sub><b>victoriasloan</b></sub></a><br /><a href="https://github.com/Budibase/budibase/commits?author=victoriasloan" title="Code">💻</a></td>
|
||||||
|
<td align="center"><a href="https://github.com/yashank09"><img src="https://avatars.githubusercontent.com/u/37672190?v=4?s=100" width="100px;" alt=""/><br /><sub><b>yashank09</b></sub></a><br /><a href="https://github.com/Budibase/budibase/commits?author=yashank09" title="Code">💻</a></td>
|
||||||
|
<td align="center"><a href="https://github.com/SOVLOOKUP"><img src="https://avatars.githubusercontent.com/u/53158137?v=4?s=100" width="100px;" alt=""/><br /><sub><b>SOVLOOKUP</b></sub></a><br /><a href="https://github.com/Budibase/budibase/commits?author=SOVLOOKUP" title="Code">💻</a></td>
|
||||||
|
<td align="center"><a href="https://github.com/seoulaja"><img src="https://avatars.githubusercontent.com/u/15101654?v=4?s=100" width="100px;" alt=""/><br /><sub><b>seoulaja</b></sub></a><br /><a href="#translation-seoulaja" title="Translation">🌍</a></td>
|
||||||
|
<td align="center"><a href="https://github.com/mslourens"><img src="https://avatars.githubusercontent.com/u/1907152?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Maurits Lourens</b></sub></a><br /><a href="https://github.com/Budibase/budibase/commits?author=mslourens" title="Tests">⚠️</a> <a href="https://github.com/Budibase/budibase/commits?author=mslourens" title="Code">💻</a></td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<!-- markdownlint-restore -->
|
||||||
|
<!-- prettier-ignore-end -->
|
||||||
|
|
||||||
|
<!-- ALL-CONTRIBUTORS-LIST:END -->
|
||||||
|
|
||||||
|
このプロジェクトは、[all-contributors](https://github.com/all-contributors/all-contributors)仕様に準拠しています。どのような貢献でも歓迎します。
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
{
|
{
|
||||||
"version": "1.0.49-alpha.5",
|
"version": "1.0.50-alpha.6",
|
||||||
"npmClient": "yarn",
|
"npmClient": "yarn",
|
||||||
"packages": [
|
"packages": [
|
||||||
"packages/*"
|
"packages/*"
|
||||||
|
|
|
@ -0,0 +1,17 @@
|
||||||
|
const {
|
||||||
|
getAppDB,
|
||||||
|
getDevAppDB,
|
||||||
|
getProdAppDB,
|
||||||
|
getAppId,
|
||||||
|
updateAppId,
|
||||||
|
doInAppContext,
|
||||||
|
} = require("./src/context")
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
getAppDB,
|
||||||
|
getDevAppDB,
|
||||||
|
getProdAppDB,
|
||||||
|
getAppId,
|
||||||
|
updateAppId,
|
||||||
|
doInAppContext,
|
||||||
|
}
|
|
@ -1,5 +1,6 @@
|
||||||
module.exports = {
|
module.exports = {
|
||||||
...require("./src/db/utils"),
|
...require("./src/db/utils"),
|
||||||
...require("./src/db/constants"),
|
...require("./src/db/constants"),
|
||||||
|
...require("./src/db"),
|
||||||
...require("./src/db/views"),
|
...require("./src/db/views"),
|
||||||
}
|
}
|
||||||
|
|
|
@ -1 +1 @@
|
||||||
module.exports = require("./src/tenancy/deprovision")
|
module.exports = require("./src/context/deprovision")
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@budibase/backend-core",
|
"name": "@budibase/backend-core",
|
||||||
"version": "1.0.49-alpha.5",
|
"version": "1.0.50-alpha.6",
|
||||||
"description": "Budibase backend core libraries used in server and worker",
|
"description": "Budibase backend core libraries used in server and worker",
|
||||||
"main": "src/index.js",
|
"main": "src/index.js",
|
||||||
"author": "Budibase",
|
"author": "Budibase",
|
||||||
|
|
|
@ -4,8 +4,8 @@ const { newid } = require("../hashing")
|
||||||
const REQUEST_ID_KEY = "requestId"
|
const REQUEST_ID_KEY = "requestId"
|
||||||
|
|
||||||
class FunctionContext {
|
class FunctionContext {
|
||||||
static getMiddleware(updateCtxFn = null) {
|
static getMiddleware(updateCtxFn = null, contextName = "session") {
|
||||||
const namespace = this.createNamespace()
|
const namespace = this.createNamespace(contextName)
|
||||||
|
|
||||||
return async function (ctx, next) {
|
return async function (ctx, next) {
|
||||||
await new Promise(
|
await new Promise(
|
||||||
|
@ -24,14 +24,14 @@ class FunctionContext {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static run(callback) {
|
static run(callback, contextName = "session") {
|
||||||
const namespace = this.createNamespace()
|
const namespace = this.createNamespace(contextName)
|
||||||
|
|
||||||
return namespace.runAndReturn(callback)
|
return namespace.runAndReturn(callback)
|
||||||
}
|
}
|
||||||
|
|
||||||
static setOnContext(key, value) {
|
static setOnContext(key, value, contextName = "session") {
|
||||||
const namespace = this.createNamespace()
|
const namespace = this.createNamespace(contextName)
|
||||||
namespace.set(key, value)
|
namespace.set(key, value)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -55,16 +55,16 @@ class FunctionContext {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static destroyNamespace() {
|
static destroyNamespace(name = "session") {
|
||||||
if (this._namespace) {
|
if (this._namespace) {
|
||||||
cls.destroyNamespace("session")
|
cls.destroyNamespace(name)
|
||||||
this._namespace = null
|
this._namespace = null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static createNamespace() {
|
static createNamespace(name = "session") {
|
||||||
if (!this._namespace) {
|
if (!this._namespace) {
|
||||||
this._namespace = cls.createNamespace("session")
|
this._namespace = cls.createNamespace(name)
|
||||||
}
|
}
|
||||||
return this._namespace
|
return this._namespace
|
||||||
}
|
}
|
|
@ -1,6 +1,6 @@
|
||||||
const { getGlobalUserParams, getAllApps } = require("../db/utils")
|
const { getGlobalUserParams, getAllApps } = require("../db/utils")
|
||||||
const { getDB, getCouch } = require("../db")
|
const { getDB, getCouch } = require("../db")
|
||||||
const { getGlobalDB } = require("./tenancy")
|
const { getGlobalDB } = require("../tenancy")
|
||||||
const { StaticDatabases } = require("../db/constants")
|
const { StaticDatabases } = require("../db/constants")
|
||||||
|
|
||||||
const TENANT_DOC = StaticDatabases.PLATFORM_INFO.docs.tenants
|
const TENANT_DOC = StaticDatabases.PLATFORM_INFO.docs.tenants
|
|
@ -0,0 +1,195 @@
|
||||||
|
const env = require("../environment")
|
||||||
|
const { Headers } = require("../../constants")
|
||||||
|
const cls = require("./FunctionContext")
|
||||||
|
const { getCouch } = require("../db")
|
||||||
|
const { getProdAppID, getDevelopmentAppID } = require("../db/conversions")
|
||||||
|
const { isEqual } = require("lodash")
|
||||||
|
|
||||||
|
// some test cases call functions directly, need to
|
||||||
|
// store an app ID to pretend there is a context
|
||||||
|
let TEST_APP_ID = null
|
||||||
|
|
||||||
|
const ContextKeys = {
|
||||||
|
TENANT_ID: "tenantId",
|
||||||
|
APP_ID: "appId",
|
||||||
|
// whatever the request app DB was
|
||||||
|
CURRENT_DB: "currentDb",
|
||||||
|
// get the prod app DB from the request
|
||||||
|
PROD_DB: "prodDb",
|
||||||
|
// get the dev app DB from the request
|
||||||
|
DEV_DB: "devDb",
|
||||||
|
DB_OPTS: "dbOpts",
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.DEFAULT_TENANT_ID = "default"
|
||||||
|
|
||||||
|
exports.isDefaultTenant = () => {
|
||||||
|
return exports.getTenantId() === exports.DEFAULT_TENANT_ID
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.isMultiTenant = () => {
|
||||||
|
return env.MULTI_TENANCY
|
||||||
|
}
|
||||||
|
|
||||||
|
// used for automations, API endpoints should always be in context already
|
||||||
|
exports.doInTenant = (tenantId, task) => {
|
||||||
|
return cls.run(() => {
|
||||||
|
// set the tenant id
|
||||||
|
cls.setOnContext(ContextKeys.TENANT_ID, tenantId)
|
||||||
|
|
||||||
|
// invoke the task
|
||||||
|
return task()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.doInAppContext = (appId, task) => {
|
||||||
|
return cls.run(() => {
|
||||||
|
// set the app ID
|
||||||
|
cls.setOnContext(ContextKeys.APP_ID, appId)
|
||||||
|
|
||||||
|
// invoke the task
|
||||||
|
return task()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.updateTenantId = tenantId => {
|
||||||
|
cls.setOnContext(ContextKeys.TENANT_ID, tenantId)
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.updateAppId = appId => {
|
||||||
|
try {
|
||||||
|
cls.setOnContext(ContextKeys.APP_ID, appId)
|
||||||
|
cls.setOnContext(ContextKeys.PROD_DB, null)
|
||||||
|
cls.setOnContext(ContextKeys.DEV_DB, null)
|
||||||
|
cls.setOnContext(ContextKeys.CURRENT_DB, null)
|
||||||
|
cls.setOnContext(ContextKeys.DB_OPTS, null)
|
||||||
|
} catch (err) {
|
||||||
|
if (env.isTest()) {
|
||||||
|
TEST_APP_ID = appId
|
||||||
|
} else {
|
||||||
|
throw err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.setTenantId = (
|
||||||
|
ctx,
|
||||||
|
opts = { allowQs: false, allowNoTenant: false }
|
||||||
|
) => {
|
||||||
|
let tenantId
|
||||||
|
// exit early if not multi-tenant
|
||||||
|
if (!exports.isMultiTenant()) {
|
||||||
|
cls.setOnContext(ContextKeys.TENANT_ID, this.DEFAULT_TENANT_ID)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const allowQs = opts && opts.allowQs
|
||||||
|
const allowNoTenant = opts && opts.allowNoTenant
|
||||||
|
const header = ctx.request.headers[Headers.TENANT_ID]
|
||||||
|
const user = ctx.user || {}
|
||||||
|
if (allowQs) {
|
||||||
|
const query = ctx.request.query || {}
|
||||||
|
tenantId = query.tenantId
|
||||||
|
}
|
||||||
|
// override query string (if allowed) by user, or header
|
||||||
|
// URL params cannot be used in a middleware, as they are
|
||||||
|
// processed later in the chain
|
||||||
|
tenantId = user.tenantId || header || tenantId
|
||||||
|
|
||||||
|
// Set the tenantId from the subdomain
|
||||||
|
if (!tenantId) {
|
||||||
|
tenantId = ctx.subdomains && ctx.subdomains[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!tenantId && !allowNoTenant) {
|
||||||
|
ctx.throw(403, "Tenant id not set")
|
||||||
|
}
|
||||||
|
// check tenant ID just incase no tenant was allowed
|
||||||
|
if (tenantId) {
|
||||||
|
cls.setOnContext(ContextKeys.TENANT_ID, tenantId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.isTenantIdSet = () => {
|
||||||
|
const tenantId = cls.getFromContext(ContextKeys.TENANT_ID)
|
||||||
|
return !!tenantId
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.getTenantId = () => {
|
||||||
|
if (!exports.isMultiTenant()) {
|
||||||
|
return exports.DEFAULT_TENANT_ID
|
||||||
|
}
|
||||||
|
const tenantId = cls.getFromContext(ContextKeys.TENANT_ID)
|
||||||
|
if (!tenantId) {
|
||||||
|
throw Error("Tenant id not found")
|
||||||
|
}
|
||||||
|
return tenantId
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.getAppId = () => {
|
||||||
|
const foundId = cls.getFromContext(ContextKeys.APP_ID)
|
||||||
|
if (!foundId && env.isTest() && TEST_APP_ID) {
|
||||||
|
return TEST_APP_ID
|
||||||
|
} else {
|
||||||
|
return foundId
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getDB(key, opts) {
|
||||||
|
const dbOptsKey = `${key}${ContextKeys.DB_OPTS}`
|
||||||
|
let storedOpts = cls.getFromContext(dbOptsKey)
|
||||||
|
let db = cls.getFromContext(key)
|
||||||
|
if (db && isEqual(opts, storedOpts)) {
|
||||||
|
return db
|
||||||
|
}
|
||||||
|
const appId = exports.getAppId()
|
||||||
|
const CouchDB = getCouch()
|
||||||
|
let toUseAppId
|
||||||
|
switch (key) {
|
||||||
|
case ContextKeys.CURRENT_DB:
|
||||||
|
toUseAppId = appId
|
||||||
|
break
|
||||||
|
case ContextKeys.PROD_DB:
|
||||||
|
toUseAppId = getProdAppID(appId)
|
||||||
|
break
|
||||||
|
case ContextKeys.DEV_DB:
|
||||||
|
toUseAppId = getDevelopmentAppID(appId)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
db = new CouchDB(toUseAppId, opts)
|
||||||
|
try {
|
||||||
|
cls.setOnContext(key, db)
|
||||||
|
if (opts) {
|
||||||
|
cls.setOnContext(dbOptsKey, opts)
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
if (!env.isTest()) {
|
||||||
|
throw err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return db
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Opens the app database based on whatever the request
|
||||||
|
* contained, dev or prod.
|
||||||
|
*/
|
||||||
|
exports.getAppDB = opts => {
|
||||||
|
return getDB(ContextKeys.CURRENT_DB, opts)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This specifically gets the prod app ID, if the request
|
||||||
|
* contained a development app ID, this will open the prod one.
|
||||||
|
*/
|
||||||
|
exports.getProdAppDB = opts => {
|
||||||
|
return getDB(ContextKeys.PROD_DB, opts)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This specifically gets the dev app ID, if the request
|
||||||
|
* contained a prod app ID, this will open the dev one.
|
||||||
|
*/
|
||||||
|
exports.getDevAppDB = opts => {
|
||||||
|
return getDB(ContextKeys.DEV_DB, opts)
|
||||||
|
}
|
|
@ -32,3 +32,7 @@ exports.StaticDatabases = {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
exports.APP_PREFIX = exports.DocumentTypes.APP + exports.SEPARATOR
|
||||||
|
exports.APP_DEV = exports.APP_DEV_PREFIX =
|
||||||
|
exports.DocumentTypes.APP_DEV + exports.SEPARATOR
|
||||||
|
|
|
@ -0,0 +1,46 @@
|
||||||
|
const NO_APP_ERROR = "No app provided"
|
||||||
|
const { APP_DEV_PREFIX, APP_PREFIX } = require("./constants")
|
||||||
|
|
||||||
|
exports.isDevAppID = appId => {
|
||||||
|
if (!appId) {
|
||||||
|
throw NO_APP_ERROR
|
||||||
|
}
|
||||||
|
return appId.startsWith(APP_DEV_PREFIX)
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.isProdAppID = appId => {
|
||||||
|
if (!appId) {
|
||||||
|
throw NO_APP_ERROR
|
||||||
|
}
|
||||||
|
return appId.startsWith(APP_PREFIX) && !exports.isDevAppID(appId)
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.isDevApp = app => {
|
||||||
|
if (!app) {
|
||||||
|
throw NO_APP_ERROR
|
||||||
|
}
|
||||||
|
return exports.isDevAppID(app.appId)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert a development app ID to a deployed app ID.
|
||||||
|
*/
|
||||||
|
exports.getProdAppID = appId => {
|
||||||
|
// if dev, convert it
|
||||||
|
if (appId.startsWith(APP_DEV_PREFIX)) {
|
||||||
|
const id = appId.split(APP_DEV_PREFIX)[1]
|
||||||
|
return `${APP_PREFIX}${id}`
|
||||||
|
}
|
||||||
|
return appId
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert a deployed app ID to a development app ID.
|
||||||
|
*/
|
||||||
|
exports.getDevelopmentAppID = appId => {
|
||||||
|
if (!appId.startsWith(APP_DEV_PREFIX)) {
|
||||||
|
const id = appId.split(APP_PREFIX)[1]
|
||||||
|
return `${APP_DEV_PREFIX}${id}`
|
||||||
|
}
|
||||||
|
return appId
|
||||||
|
}
|
|
@ -2,7 +2,13 @@ const { newid } = require("../hashing")
|
||||||
const Replication = require("./Replication")
|
const Replication = require("./Replication")
|
||||||
const { DEFAULT_TENANT_ID, Configs } = require("../constants")
|
const { DEFAULT_TENANT_ID, Configs } = require("../constants")
|
||||||
const env = require("../environment")
|
const env = require("../environment")
|
||||||
const { StaticDatabases, SEPARATOR, DocumentTypes } = require("./constants")
|
const {
|
||||||
|
StaticDatabases,
|
||||||
|
SEPARATOR,
|
||||||
|
DocumentTypes,
|
||||||
|
APP_PREFIX,
|
||||||
|
APP_DEV,
|
||||||
|
} = require("./constants")
|
||||||
const {
|
const {
|
||||||
getTenantId,
|
getTenantId,
|
||||||
getTenantIDFromAppID,
|
getTenantIDFromAppID,
|
||||||
|
@ -12,8 +18,13 @@ const fetch = require("node-fetch")
|
||||||
const { getCouch } = require("./index")
|
const { getCouch } = require("./index")
|
||||||
const { getAppMetadata } = require("../cache/appMetadata")
|
const { getAppMetadata } = require("../cache/appMetadata")
|
||||||
const { checkSlashesInUrl } = require("../helpers")
|
const { checkSlashesInUrl } = require("../helpers")
|
||||||
|
const {
|
||||||
const NO_APP_ERROR = "No app provided"
|
isDevApp,
|
||||||
|
isProdAppID,
|
||||||
|
isDevAppID,
|
||||||
|
getDevelopmentAppID,
|
||||||
|
getProdAppID,
|
||||||
|
} = require("./conversions")
|
||||||
|
|
||||||
const UNICODE_MAX = "\ufff0"
|
const UNICODE_MAX = "\ufff0"
|
||||||
|
|
||||||
|
@ -24,10 +35,15 @@ exports.ViewNames = {
|
||||||
exports.StaticDatabases = StaticDatabases
|
exports.StaticDatabases = StaticDatabases
|
||||||
|
|
||||||
exports.DocumentTypes = DocumentTypes
|
exports.DocumentTypes = DocumentTypes
|
||||||
exports.APP_PREFIX = DocumentTypes.APP + SEPARATOR
|
exports.APP_PREFIX = APP_PREFIX
|
||||||
exports.APP_DEV = exports.APP_DEV_PREFIX = DocumentTypes.APP_DEV + SEPARATOR
|
exports.APP_DEV = exports.APP_DEV_PREFIX = APP_DEV
|
||||||
exports.SEPARATOR = SEPARATOR
|
exports.SEPARATOR = SEPARATOR
|
||||||
exports.getTenantIDFromAppID = getTenantIDFromAppID
|
exports.getTenantIDFromAppID = getTenantIDFromAppID
|
||||||
|
exports.isDevApp = isDevApp
|
||||||
|
exports.isProdAppID = isProdAppID
|
||||||
|
exports.isDevAppID = isDevAppID
|
||||||
|
exports.getDevelopmentAppID = getDevelopmentAppID
|
||||||
|
exports.getProdAppID = getProdAppID
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* If creating DB allDocs/query params with only a single top level ID this can be used, this
|
* If creating DB allDocs/query params with only a single top level ID this can be used, this
|
||||||
|
@ -52,27 +68,6 @@ function getDocParams(docType, docId = null, otherProps = {}) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.isDevAppID = appId => {
|
|
||||||
if (!appId) {
|
|
||||||
throw NO_APP_ERROR
|
|
||||||
}
|
|
||||||
return appId.startsWith(exports.APP_DEV_PREFIX)
|
|
||||||
}
|
|
||||||
|
|
||||||
exports.isProdAppID = appId => {
|
|
||||||
if (!appId) {
|
|
||||||
throw NO_APP_ERROR
|
|
||||||
}
|
|
||||||
return appId.startsWith(exports.APP_PREFIX) && !exports.isDevAppID(appId)
|
|
||||||
}
|
|
||||||
|
|
||||||
function isDevApp(app) {
|
|
||||||
if (!app) {
|
|
||||||
throw NO_APP_ERROR
|
|
||||||
}
|
|
||||||
return exports.isDevAppID(app.appId)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generates a new workspace ID.
|
* Generates a new workspace ID.
|
||||||
* @returns {string} The new workspace ID which the workspace doc can be stored under.
|
* @returns {string} The new workspace ID which the workspace doc can be stored under.
|
||||||
|
@ -157,29 +152,6 @@ exports.getRoleParams = (roleId = null, otherProps = {}) => {
|
||||||
return getDocParams(DocumentTypes.ROLE, roleId, otherProps)
|
return getDocParams(DocumentTypes.ROLE, roleId, otherProps)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Convert a development app ID to a deployed app ID.
|
|
||||||
*/
|
|
||||||
exports.getDeployedAppID = appId => {
|
|
||||||
// if dev, convert it
|
|
||||||
if (appId.startsWith(exports.APP_DEV_PREFIX)) {
|
|
||||||
const id = appId.split(exports.APP_DEV_PREFIX)[1]
|
|
||||||
return `${exports.APP_PREFIX}${id}`
|
|
||||||
}
|
|
||||||
return appId
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Convert a deployed app ID to a development app ID.
|
|
||||||
*/
|
|
||||||
exports.getDevelopmentAppID = appId => {
|
|
||||||
if (!appId.startsWith(exports.APP_DEV_PREFIX)) {
|
|
||||||
const id = appId.split(exports.APP_PREFIX)[1]
|
|
||||||
return `${exports.APP_DEV_PREFIX}${id}`
|
|
||||||
}
|
|
||||||
return appId
|
|
||||||
}
|
|
||||||
|
|
||||||
exports.getCouchUrl = () => {
|
exports.getCouchUrl = () => {
|
||||||
if (!env.COUCH_DB_URL) return
|
if (!env.COUCH_DB_URL) return
|
||||||
|
|
||||||
|
@ -225,7 +197,7 @@ exports.getAllDbs = async () => {
|
||||||
}
|
}
|
||||||
let couchUrl = `${exports.getCouchUrl()}/_all_dbs`
|
let couchUrl = `${exports.getCouchUrl()}/_all_dbs`
|
||||||
let tenantId = getTenantId()
|
let tenantId = getTenantId()
|
||||||
if (!env.MULTI_TENANCY || tenantId == DEFAULT_TENANT_ID) {
|
if (!env.MULTI_TENANCY || tenantId === DEFAULT_TENANT_ID) {
|
||||||
// just get all DBs when:
|
// just get all DBs when:
|
||||||
// - single tenancy
|
// - single tenancy
|
||||||
// - default tenant
|
// - default tenant
|
||||||
|
@ -250,11 +222,11 @@ exports.getAllDbs = async () => {
|
||||||
/**
|
/**
|
||||||
* Lots of different points in the system need to find the full list of apps, this will
|
* Lots of different points in the system need to find the full list of apps, this will
|
||||||
* enumerate the entire CouchDB cluster and get the list of databases (every app).
|
* enumerate the entire CouchDB cluster and get the list of databases (every app).
|
||||||
* NOTE: this operation is fine in self hosting, but cannot be used when hosting many
|
*
|
||||||
* different users/companies apps as there is no security around it - all apps are returned.
|
|
||||||
* @return {Promise<object[]>} returns the app information document stored in each app database.
|
* @return {Promise<object[]>} returns the app information document stored in each app database.
|
||||||
*/
|
*/
|
||||||
exports.getAllApps = async (CouchDB, { dev, all, idsOnly } = {}) => {
|
exports.getAllApps = async ({ dev, all, idsOnly } = {}) => {
|
||||||
|
const CouchDB = getCouch()
|
||||||
let tenantId = getTenantId()
|
let tenantId = getTenantId()
|
||||||
if (!env.MULTI_TENANCY && !tenantId) {
|
if (!env.MULTI_TENANCY && !tenantId) {
|
||||||
tenantId = DEFAULT_TENANT_ID
|
tenantId = DEFAULT_TENANT_ID
|
||||||
|
@ -310,8 +282,8 @@ exports.getAllApps = async (CouchDB, { dev, all, idsOnly } = {}) => {
|
||||||
/**
|
/**
|
||||||
* Utility function for getAllApps but filters to production apps only.
|
* Utility function for getAllApps but filters to production apps only.
|
||||||
*/
|
*/
|
||||||
exports.getDeployedAppIDs = async CouchDB => {
|
exports.getProdAppIDs = async () => {
|
||||||
return (await exports.getAllApps(CouchDB, { idsOnly: true })).filter(
|
return (await exports.getAllApps({ idsOnly: true })).filter(
|
||||||
id => !exports.isDevAppID(id)
|
id => !exports.isDevAppID(id)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -319,13 +291,14 @@ exports.getDeployedAppIDs = async CouchDB => {
|
||||||
/**
|
/**
|
||||||
* Utility function for the inverse of above.
|
* Utility function for the inverse of above.
|
||||||
*/
|
*/
|
||||||
exports.getDevAppIDs = async CouchDB => {
|
exports.getDevAppIDs = async () => {
|
||||||
return (await exports.getAllApps(CouchDB, { idsOnly: true })).filter(id =>
|
return (await exports.getAllApps({ idsOnly: true })).filter(id =>
|
||||||
exports.isDevAppID(id)
|
exports.isDevAppID(id)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.dbExists = async (CouchDB, dbName) => {
|
exports.dbExists = async dbName => {
|
||||||
|
const CouchDB = getCouch()
|
||||||
let exists = false
|
let exists = false
|
||||||
try {
|
try {
|
||||||
const db = CouchDB(dbName, { skip_setup: true })
|
const db = CouchDB(dbName, { skip_setup: true })
|
||||||
|
|
|
@ -3,8 +3,9 @@ const {
|
||||||
updateTenantId,
|
updateTenantId,
|
||||||
isTenantIdSet,
|
isTenantIdSet,
|
||||||
DEFAULT_TENANT_ID,
|
DEFAULT_TENANT_ID,
|
||||||
|
updateAppId,
|
||||||
} = require("../tenancy")
|
} = require("../tenancy")
|
||||||
const ContextFactory = require("../tenancy/FunctionContext")
|
const ContextFactory = require("../context/FunctionContext")
|
||||||
const { getTenantIDFromAppID } = require("../db/utils")
|
const { getTenantIDFromAppID } = require("../db/utils")
|
||||||
|
|
||||||
module.exports = () => {
|
module.exports = () => {
|
||||||
|
@ -21,5 +22,6 @@ module.exports = () => {
|
||||||
const appId = ctx.appId ? ctx.appId : ctx.user ? ctx.user.appId : null
|
const appId = ctx.appId ? ctx.appId : ctx.user ? ctx.user.appId : null
|
||||||
const tenantId = getTenantIDFromAppID(appId) || DEFAULT_TENANT_ID
|
const tenantId = getTenantIDFromAppID(appId) || DEFAULT_TENANT_ID
|
||||||
updateTenantId(tenantId)
|
updateTenantId(tenantId)
|
||||||
|
updateAppId(appId)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,7 +8,7 @@ const { newid } = require("../../hashing")
|
||||||
const { createASession } = require("../../security/sessions")
|
const { createASession } = require("../../security/sessions")
|
||||||
const { getTenantId } = require("../../tenancy")
|
const { getTenantId } = require("../../tenancy")
|
||||||
|
|
||||||
const INVALID_ERR = "Invalid Credentials"
|
const INVALID_ERR = "Invalid credentials"
|
||||||
const SSO_NO_PASSWORD = "SSO user does not have a password set"
|
const SSO_NO_PASSWORD = "SSO user does not have a password set"
|
||||||
const EXPIRED = "This account has expired. Please reset your password"
|
const EXPIRED = "This account has expired. Please reset your password"
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
const { setTenantId } = require("../tenancy")
|
const { setTenantId } = require("../tenancy")
|
||||||
const ContextFactory = require("../tenancy/FunctionContext")
|
const ContextFactory = require("../context/FunctionContext")
|
||||||
const { buildMatcherRegex, matches } = require("./matchers")
|
const { buildMatcherRegex, matches } = require("./matchers")
|
||||||
|
|
||||||
module.exports = (
|
module.exports = (
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
const { getDB } = require("../db")
|
|
||||||
const { cloneDeep } = require("lodash/fp")
|
const { cloneDeep } = require("lodash/fp")
|
||||||
const { BUILTIN_PERMISSION_IDS } = require("./permissions")
|
const { BUILTIN_PERMISSION_IDS } = require("./permissions")
|
||||||
const {
|
const {
|
||||||
|
@ -7,6 +6,8 @@ const {
|
||||||
DocumentTypes,
|
DocumentTypes,
|
||||||
SEPARATOR,
|
SEPARATOR,
|
||||||
} = require("../db/utils")
|
} = require("../db/utils")
|
||||||
|
const { getAppDB } = require("../context")
|
||||||
|
const { getDB } = require("../db")
|
||||||
|
|
||||||
const BUILTIN_IDS = {
|
const BUILTIN_IDS = {
|
||||||
ADMIN: "ADMIN",
|
ADMIN: "ADMIN",
|
||||||
|
@ -111,11 +112,10 @@ exports.lowerBuiltinRoleID = (roleId1, roleId2) => {
|
||||||
/**
|
/**
|
||||||
* Gets the role object, this is mainly useful for two purposes, to check if the level exists and
|
* Gets the role object, this is mainly useful for two purposes, to check if the level exists and
|
||||||
* to check if the role inherits any others.
|
* to check if the role inherits any others.
|
||||||
* @param {string} appId The app in which to look for the role.
|
|
||||||
* @param {string|null} roleId The level ID to lookup.
|
* @param {string|null} roleId The level ID to lookup.
|
||||||
* @returns {Promise<Role|object|null>} The role object, which may contain an "inherits" property.
|
* @returns {Promise<Role|object|null>} The role object, which may contain an "inherits" property.
|
||||||
*/
|
*/
|
||||||
exports.getRole = async (appId, roleId) => {
|
exports.getRole = async roleId => {
|
||||||
if (!roleId) {
|
if (!roleId) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
@ -128,7 +128,7 @@ exports.getRole = async (appId, roleId) => {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
const db = getDB(appId)
|
const db = getAppDB()
|
||||||
const dbRole = await db.get(exports.getDBRoleID(roleId))
|
const dbRole = await db.get(exports.getDBRoleID(roleId))
|
||||||
role = Object.assign(role, dbRole)
|
role = Object.assign(role, dbRole)
|
||||||
// finalise the ID
|
// finalise the ID
|
||||||
|
@ -145,11 +145,12 @@ exports.getRole = async (appId, roleId) => {
|
||||||
/**
|
/**
|
||||||
* Simple function to get all the roles based on the top level user role ID.
|
* Simple function to get all the roles based on the top level user role ID.
|
||||||
*/
|
*/
|
||||||
async function getAllUserRoles(appId, userRoleId) {
|
async function getAllUserRoles(userRoleId) {
|
||||||
if (!userRoleId) {
|
// admins have access to all roles
|
||||||
return [BUILTIN_IDS.BASIC]
|
if (userRoleId === BUILTIN_IDS.ADMIN) {
|
||||||
|
return exports.getAllRoles()
|
||||||
}
|
}
|
||||||
let currentRole = await exports.getRole(appId, userRoleId)
|
let currentRole = await exports.getRole(userRoleId)
|
||||||
let roles = currentRole ? [currentRole] : []
|
let roles = currentRole ? [currentRole] : []
|
||||||
let roleIds = [userRoleId]
|
let roleIds = [userRoleId]
|
||||||
// get all the inherited roles
|
// get all the inherited roles
|
||||||
|
@ -159,7 +160,7 @@ async function getAllUserRoles(appId, userRoleId) {
|
||||||
roleIds.indexOf(currentRole.inherits) === -1
|
roleIds.indexOf(currentRole.inherits) === -1
|
||||||
) {
|
) {
|
||||||
roleIds.push(currentRole.inherits)
|
roleIds.push(currentRole.inherits)
|
||||||
currentRole = await exports.getRole(appId, currentRole.inherits)
|
currentRole = await exports.getRole(currentRole.inherits)
|
||||||
roles.push(currentRole)
|
roles.push(currentRole)
|
||||||
}
|
}
|
||||||
return roles
|
return roles
|
||||||
|
@ -168,29 +169,23 @@ async function getAllUserRoles(appId, userRoleId) {
|
||||||
/**
|
/**
|
||||||
* Returns an ordered array of the user's inherited role IDs, this can be used
|
* Returns an ordered array of the user's inherited role IDs, this can be used
|
||||||
* to determine if a user can access something that requires a specific role.
|
* to determine if a user can access something that requires a specific role.
|
||||||
* @param {string} appId The ID of the application from which roles should be obtained.
|
|
||||||
* @param {string} userRoleId The user's role ID, this can be found in their access token.
|
* @param {string} userRoleId The user's role ID, this can be found in their access token.
|
||||||
* @param {object} opts Various options, such as whether to only retrieve the IDs (default true).
|
* @param {object} opts Various options, such as whether to only retrieve the IDs (default true).
|
||||||
* @returns {Promise<string[]>} returns an ordered array of the roles, with the first being their
|
* @returns {Promise<string[]>} returns an ordered array of the roles, with the first being their
|
||||||
* highest level of access and the last being the lowest level.
|
* highest level of access and the last being the lowest level.
|
||||||
*/
|
*/
|
||||||
exports.getUserRoleHierarchy = async (
|
exports.getUserRoleHierarchy = async (userRoleId, opts = { idOnly: true }) => {
|
||||||
appId,
|
|
||||||
userRoleId,
|
|
||||||
opts = { idOnly: true }
|
|
||||||
) => {
|
|
||||||
// special case, if they don't have a role then they are a public user
|
// special case, if they don't have a role then they are a public user
|
||||||
const roles = await getAllUserRoles(appId, userRoleId)
|
const roles = await getAllUserRoles(userRoleId)
|
||||||
return opts.idOnly ? roles.map(role => role._id) : roles
|
return opts.idOnly ? roles.map(role => role._id) : roles
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Given an app ID this will retrieve all of the roles that are currently within that app.
|
* Given an app ID this will retrieve all of the roles that are currently within that app.
|
||||||
* @param {string} appId The ID of the app to retrieve the roles from.
|
|
||||||
* @return {Promise<object[]>} An array of the role objects that were found.
|
* @return {Promise<object[]>} An array of the role objects that were found.
|
||||||
*/
|
*/
|
||||||
exports.getAllRoles = async appId => {
|
exports.getAllRoles = async appId => {
|
||||||
const db = getDB(appId)
|
const db = appId ? getDB(appId) : getAppDB()
|
||||||
const body = await db.allDocs(
|
const body = await db.allDocs(
|
||||||
getRoleParams(null, {
|
getRoleParams(null, {
|
||||||
include_docs: true,
|
include_docs: true,
|
||||||
|
@ -218,19 +213,17 @@ exports.getAllRoles = async appId => {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This retrieves the required role/
|
* This retrieves the required role
|
||||||
* @param appId
|
|
||||||
* @param permLevel
|
* @param permLevel
|
||||||
* @param resourceId
|
* @param resourceId
|
||||||
* @param subResourceId
|
* @param subResourceId
|
||||||
* @return {Promise<{permissions}|Object>}
|
* @return {Promise<{permissions}|Object>}
|
||||||
*/
|
*/
|
||||||
exports.getRequiredResourceRole = async (
|
exports.getRequiredResourceRole = async (
|
||||||
appId,
|
|
||||||
permLevel,
|
permLevel,
|
||||||
{ resourceId, subResourceId }
|
{ resourceId, subResourceId }
|
||||||
) => {
|
) => {
|
||||||
const roles = await exports.getAllRoles(appId)
|
const roles = await exports.getAllRoles()
|
||||||
let main = [],
|
let main = [],
|
||||||
sub = []
|
sub = []
|
||||||
for (let role of roles) {
|
for (let role of roles) {
|
||||||
|
@ -251,8 +244,7 @@ exports.getRequiredResourceRole = async (
|
||||||
}
|
}
|
||||||
|
|
||||||
class AccessController {
|
class AccessController {
|
||||||
constructor(appId) {
|
constructor() {
|
||||||
this.appId = appId
|
|
||||||
this.userHierarchies = {}
|
this.userHierarchies = {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -270,7 +262,7 @@ class AccessController {
|
||||||
}
|
}
|
||||||
let roleIds = this.userHierarchies[userRoleId]
|
let roleIds = this.userHierarchies[userRoleId]
|
||||||
if (!roleIds) {
|
if (!roleIds) {
|
||||||
roleIds = await exports.getUserRoleHierarchy(this.appId, userRoleId)
|
roleIds = await exports.getUserRoleHierarchy(userRoleId)
|
||||||
this.userHierarchies[userRoleId] = roleIds
|
this.userHierarchies[userRoleId] = roleIds
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,84 +0,0 @@
|
||||||
const env = require("../environment")
|
|
||||||
const { Headers } = require("../../constants")
|
|
||||||
const cls = require("./FunctionContext")
|
|
||||||
|
|
||||||
exports.DEFAULT_TENANT_ID = "default"
|
|
||||||
|
|
||||||
exports.isDefaultTenant = () => {
|
|
||||||
return exports.getTenantId() === exports.DEFAULT_TENANT_ID
|
|
||||||
}
|
|
||||||
|
|
||||||
exports.isMultiTenant = () => {
|
|
||||||
return env.MULTI_TENANCY
|
|
||||||
}
|
|
||||||
|
|
||||||
const TENANT_ID = "tenantId"
|
|
||||||
|
|
||||||
// used for automations, API endpoints should always be in context already
|
|
||||||
exports.doInTenant = (tenantId, task) => {
|
|
||||||
return cls.run(() => {
|
|
||||||
// set the tenant id
|
|
||||||
cls.setOnContext(TENANT_ID, tenantId)
|
|
||||||
|
|
||||||
// invoke the task
|
|
||||||
return task()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
exports.updateTenantId = tenantId => {
|
|
||||||
cls.setOnContext(TENANT_ID, tenantId)
|
|
||||||
}
|
|
||||||
|
|
||||||
exports.setTenantId = (
|
|
||||||
ctx,
|
|
||||||
opts = { allowQs: false, allowNoTenant: false }
|
|
||||||
) => {
|
|
||||||
let tenantId
|
|
||||||
// exit early if not multi-tenant
|
|
||||||
if (!exports.isMultiTenant()) {
|
|
||||||
cls.setOnContext(TENANT_ID, this.DEFAULT_TENANT_ID)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const allowQs = opts && opts.allowQs
|
|
||||||
const allowNoTenant = opts && opts.allowNoTenant
|
|
||||||
const header = ctx.request.headers[Headers.TENANT_ID]
|
|
||||||
const user = ctx.user || {}
|
|
||||||
if (allowQs) {
|
|
||||||
const query = ctx.request.query || {}
|
|
||||||
tenantId = query.tenantId
|
|
||||||
}
|
|
||||||
// override query string (if allowed) by user, or header
|
|
||||||
// URL params cannot be used in a middleware, as they are
|
|
||||||
// processed later in the chain
|
|
||||||
tenantId = user.tenantId || header || tenantId
|
|
||||||
|
|
||||||
// Set the tenantId from the subdomain
|
|
||||||
if (!tenantId) {
|
|
||||||
tenantId = ctx.subdomains && ctx.subdomains[0]
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!tenantId && !allowNoTenant) {
|
|
||||||
ctx.throw(403, "Tenant id not set")
|
|
||||||
}
|
|
||||||
// check tenant ID just incase no tenant was allowed
|
|
||||||
if (tenantId) {
|
|
||||||
cls.setOnContext(TENANT_ID, tenantId)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
exports.isTenantIdSet = () => {
|
|
||||||
const tenantId = cls.getFromContext(TENANT_ID)
|
|
||||||
return !!tenantId
|
|
||||||
}
|
|
||||||
|
|
||||||
exports.getTenantId = () => {
|
|
||||||
if (!exports.isMultiTenant()) {
|
|
||||||
return exports.DEFAULT_TENANT_ID
|
|
||||||
}
|
|
||||||
const tenantId = cls.getFromContext(TENANT_ID)
|
|
||||||
if (!tenantId) {
|
|
||||||
throw Error("Tenant id not found")
|
|
||||||
}
|
|
||||||
return tenantId
|
|
||||||
}
|
|
|
@ -1,4 +1,4 @@
|
||||||
module.exports = {
|
module.exports = {
|
||||||
...require("./context"),
|
...require("../context"),
|
||||||
...require("./tenancy"),
|
...require("./tenancy"),
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
const { getDB } = require("../db")
|
const { getDB } = require("../db")
|
||||||
const { SEPARATOR, StaticDatabases, DocumentTypes } = require("../db/constants")
|
const { SEPARATOR, StaticDatabases, DocumentTypes } = require("../db/constants")
|
||||||
const { getTenantId, DEFAULT_TENANT_ID, isMultiTenant } = require("./context")
|
const { getTenantId, DEFAULT_TENANT_ID, isMultiTenant } = require("../context")
|
||||||
const env = require("../environment")
|
const env = require("../environment")
|
||||||
|
|
||||||
const TENANT_DOC = StaticDatabases.PLATFORM_INFO.docs.tenants
|
const TENANT_DOC = StaticDatabases.PLATFORM_INFO.docs.tenants
|
||||||
|
|
|
@ -256,7 +256,7 @@ exports.saveUser = async (
|
||||||
exports.platformLogout = async ({ ctx, userId, keepActiveSession }) => {
|
exports.platformLogout = async ({ ctx, userId, keepActiveSession }) => {
|
||||||
if (!ctx) throw new Error("Koa context must be supplied to logout.")
|
if (!ctx) throw new Error("Koa context must be supplied to logout.")
|
||||||
|
|
||||||
const currentSession = this.getCookie(ctx, Cookies.Auth)
|
const currentSession = exports.getCookie(ctx, Cookies.Auth)
|
||||||
let sessions = await getUserSessions(userId)
|
let sessions = await getUserSessions(userId)
|
||||||
|
|
||||||
if (keepActiveSession) {
|
if (keepActiveSession) {
|
||||||
|
@ -265,8 +265,8 @@ exports.platformLogout = async ({ ctx, userId, keepActiveSession }) => {
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
// clear cookies
|
// clear cookies
|
||||||
this.clearCookie(ctx, Cookies.Auth)
|
exports.clearCookie(ctx, Cookies.Auth)
|
||||||
this.clearCookie(ctx, Cookies.CurrentApp)
|
exports.clearCookie(ctx, Cookies.CurrentApp)
|
||||||
}
|
}
|
||||||
|
|
||||||
await invalidateSessions(
|
await invalidateSessions(
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"name": "@budibase/bbui",
|
"name": "@budibase/bbui",
|
||||||
"description": "A UI solution used in the different Budibase projects.",
|
"description": "A UI solution used in the different Budibase projects.",
|
||||||
"version": "1.0.49-alpha.5",
|
"version": "1.0.50-alpha.6",
|
||||||
"license": "MPL-2.0",
|
"license": "MPL-2.0",
|
||||||
"svelte": "src/index.js",
|
"svelte": "src/index.js",
|
||||||
"module": "dist/bbui.es.js",
|
"module": "dist/bbui.es.js",
|
||||||
|
@ -79,6 +79,7 @@
|
||||||
"@spectrum-css/underlay": "^2.0.9",
|
"@spectrum-css/underlay": "^2.0.9",
|
||||||
"@spectrum-css/vars": "^3.0.1",
|
"@spectrum-css/vars": "^3.0.1",
|
||||||
"dayjs": "^1.10.4",
|
"dayjs": "^1.10.4",
|
||||||
|
"easymde": "^2.16.1",
|
||||||
"svelte-flatpickr": "^3.2.3",
|
"svelte-flatpickr": "^3.2.3",
|
||||||
"svelte-portal": "^1.0.0"
|
"svelte-portal": "^1.0.0"
|
||||||
},
|
},
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
<script>
|
<script>
|
||||||
import "@spectrum-css/button/dist/index-vars.css"
|
import "@spectrum-css/button/dist/index-vars.css"
|
||||||
|
import Tooltip from "../Tooltip/Tooltip.svelte"
|
||||||
|
|
||||||
export let disabled = false
|
export let disabled = false
|
||||||
export let size = "M"
|
export let size = "M"
|
||||||
|
@ -11,36 +12,67 @@
|
||||||
export let quiet = false
|
export let quiet = false
|
||||||
export let icon = undefined
|
export let icon = undefined
|
||||||
export let active = false
|
export let active = false
|
||||||
|
export let tooltip = undefined
|
||||||
|
|
||||||
|
let showTooltip = false
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<button
|
<div class:container={!!tooltip}>
|
||||||
class:spectrum-Button--cta={cta}
|
<button
|
||||||
class:spectrum-Button--primary={primary}
|
class:spectrum-Button--cta={cta}
|
||||||
class:spectrum-Button--secondary={secondary}
|
class:spectrum-Button--primary={primary}
|
||||||
class:spectrum-Button--warning={warning}
|
class:spectrum-Button--secondary={secondary}
|
||||||
class:spectrum-Button--overBackground={overBackground}
|
class:spectrum-Button--warning={warning}
|
||||||
class:spectrum-Button--quiet={quiet}
|
class:spectrum-Button--overBackground={overBackground}
|
||||||
class:active
|
class:spectrum-Button--quiet={quiet}
|
||||||
class="spectrum-Button spectrum-Button--size{size.toUpperCase()}"
|
class:active
|
||||||
{disabled}
|
class="spectrum-Button spectrum-Button--size{size.toUpperCase()}"
|
||||||
on:click|preventDefault
|
{disabled}
|
||||||
>
|
on:click|preventDefault
|
||||||
{#if icon}
|
on:mouseover={() => (showTooltip = true)}
|
||||||
<svg
|
on:mouseleave={() => (showTooltip = false)}
|
||||||
class="spectrum-Icon spectrum-Icon--size{size.toUpperCase()}"
|
>
|
||||||
focusable="false"
|
{#if icon}
|
||||||
aria-hidden="true"
|
<svg
|
||||||
aria-label={icon}
|
class="spectrum-Icon spectrum-Icon--size{size.toUpperCase()}"
|
||||||
>
|
focusable="false"
|
||||||
<use xlink:href="#spectrum-icon-18-{icon}" />
|
aria-hidden="true"
|
||||||
</svg>
|
aria-label={icon}
|
||||||
|
>
|
||||||
|
<use xlink:href="#spectrum-icon-18-{icon}" />
|
||||||
|
</svg>
|
||||||
|
{/if}
|
||||||
|
{#if $$slots}
|
||||||
|
<span class="spectrum-Button-label"><slot /></span>
|
||||||
|
{/if}
|
||||||
|
{#if !disabled && tooltip}
|
||||||
|
<div class="tooltip-icon">
|
||||||
|
<svg
|
||||||
|
class="spectrum-Icon spectrum-Icon--size{size.toUpperCase()}"
|
||||||
|
focusable="false"
|
||||||
|
aria-hidden="true"
|
||||||
|
aria-label="Info"
|
||||||
|
>
|
||||||
|
<use xlink:href="#spectrum-icon-18-InfoOutline" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</button>
|
||||||
|
{#if showTooltip && tooltip}
|
||||||
|
<div class="position">
|
||||||
|
<div class="tooltip">
|
||||||
|
<Tooltip textWrapping={true} direction={"bottom"} text={tooltip} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
{#if $$slots}
|
</div>
|
||||||
<span class="spectrum-Button-label"><slot /></span>
|
|
||||||
{/if}
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
.container {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
.spectrum-Button-label {
|
.spectrum-Button-label {
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
@ -49,4 +81,23 @@
|
||||||
.active {
|
.active {
|
||||||
color: var(--spectrum-global-color-blue-600) !important;
|
color: var(--spectrum-global-color-blue-600) !important;
|
||||||
}
|
}
|
||||||
|
.tooltip {
|
||||||
|
position: absolute;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
z-index: 100;
|
||||||
|
width: 160px;
|
||||||
|
text-align: center;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
top: -5px;
|
||||||
|
}
|
||||||
|
.position {
|
||||||
|
position: relative;
|
||||||
|
width: 0;
|
||||||
|
height: 0;
|
||||||
|
}
|
||||||
|
.tooltip-icon {
|
||||||
|
padding-left: var(--spacing-m);
|
||||||
|
line-height: 0;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -5,7 +5,7 @@
|
||||||
import { fly } from "svelte/transition"
|
import { fly } from "svelte/transition"
|
||||||
import Icon from "../Icon/Icon.svelte"
|
import Icon from "../Icon/Icon.svelte"
|
||||||
import Input from "../Form/Input.svelte"
|
import Input from "../Form/Input.svelte"
|
||||||
import { capitalise } from "../utils/helpers"
|
import { capitalise } from "../helpers"
|
||||||
|
|
||||||
export let value
|
export let value
|
||||||
export let size = "M"
|
export let size = "M"
|
||||||
|
|
|
@ -5,7 +5,7 @@
|
||||||
import "@spectrum-css/textfield/dist/index-vars.css"
|
import "@spectrum-css/textfield/dist/index-vars.css"
|
||||||
import "@spectrum-css/picker/dist/index-vars.css"
|
import "@spectrum-css/picker/dist/index-vars.css"
|
||||||
import { createEventDispatcher } from "svelte"
|
import { createEventDispatcher } from "svelte"
|
||||||
import { generateID } from "../../utils/helpers"
|
import { uuid } from "../../helpers"
|
||||||
|
|
||||||
export let id = null
|
export let id = null
|
||||||
export let disabled = false
|
export let disabled = false
|
||||||
|
@ -14,16 +14,20 @@
|
||||||
export let value = null
|
export let value = null
|
||||||
export let placeholder = null
|
export let placeholder = null
|
||||||
export let appendTo = undefined
|
export let appendTo = undefined
|
||||||
|
export let timeOnly = false
|
||||||
|
|
||||||
const dispatch = createEventDispatcher()
|
const dispatch = createEventDispatcher()
|
||||||
const flatpickrId = `${generateID()}-wrapper`
|
const flatpickrId = `${uuid()}-wrapper`
|
||||||
let open = false
|
let open = false
|
||||||
let flatpickr
|
let flatpickr, flatpickrOptions, isTimeOnly
|
||||||
|
|
||||||
|
$: isTimeOnly = !timeOnly && value ? !isNaN(new Date(`0-${value}`)) : timeOnly
|
||||||
$: flatpickrOptions = {
|
$: flatpickrOptions = {
|
||||||
element: `#${flatpickrId}`,
|
element: `#${flatpickrId}`,
|
||||||
enableTime: enableTime || false,
|
enableTime: isTimeOnly || enableTime || false,
|
||||||
|
noCalendar: isTimeOnly || false,
|
||||||
altInput: true,
|
altInput: true,
|
||||||
altFormat: enableTime ? "F j Y, H:i" : "F j, Y",
|
altFormat: isTimeOnly ? "H:i" : enableTime ? "F j Y, H:i" : "F j, Y",
|
||||||
wrap: true,
|
wrap: true,
|
||||||
appendTo,
|
appendTo,
|
||||||
disableMobile: "true",
|
disableMobile: "true",
|
||||||
|
@ -35,6 +39,11 @@
|
||||||
if (newValue) {
|
if (newValue) {
|
||||||
newValue = newValue.toISOString()
|
newValue = newValue.toISOString()
|
||||||
}
|
}
|
||||||
|
// if time only set date component to today
|
||||||
|
if (timeOnly) {
|
||||||
|
const todayDate = new Date().toISOString().split("T")[0]
|
||||||
|
newValue = `${todayDate}T${newValue.split("T")[1]}`
|
||||||
|
}
|
||||||
dispatch("change", newValue)
|
dispatch("change", newValue)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -67,7 +76,11 @@
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
let date
|
let date
|
||||||
if (val instanceof Date) {
|
let time = new Date(`0-${val}`)
|
||||||
|
// it is a string like 00:00:00, just time
|
||||||
|
if (timeOnly || (typeof val === "string" && !isNaN(time))) {
|
||||||
|
date = time
|
||||||
|
} else if (val instanceof Date) {
|
||||||
// Use real date obj if already parsed
|
// Use real date obj if already parsed
|
||||||
date = val
|
date = val
|
||||||
} else if (isNaN(val)) {
|
} else if (isNaN(val)) {
|
||||||
|
@ -77,7 +90,7 @@
|
||||||
// Treat as numerical timestamp
|
// Treat as numerical timestamp
|
||||||
date = new Date(parseInt(val))
|
date = new Date(parseInt(val))
|
||||||
}
|
}
|
||||||
const time = date.getTime()
|
time = date.getTime()
|
||||||
if (isNaN(time)) {
|
if (isNaN(time)) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
@ -88,69 +101,71 @@
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Flatpickr
|
{#key isTimeOnly}
|
||||||
bind:flatpickr
|
<Flatpickr
|
||||||
value={parseDate(value)}
|
bind:flatpickr
|
||||||
on:open={onOpen}
|
value={parseDate(value)}
|
||||||
on:close={onClose}
|
on:open={onOpen}
|
||||||
options={flatpickrOptions}
|
on:close={onClose}
|
||||||
on:change={handleChange}
|
options={flatpickrOptions}
|
||||||
element={`#${flatpickrId}`}
|
on:change={handleChange}
|
||||||
>
|
element={`#${flatpickrId}`}
|
||||||
<div
|
|
||||||
id={flatpickrId}
|
|
||||||
class:is-disabled={disabled}
|
|
||||||
class:is-invalid={!!error}
|
|
||||||
class="flatpickr spectrum-InputGroup spectrum-Datepicker"
|
|
||||||
class:is-focused={open}
|
|
||||||
aria-readonly="false"
|
|
||||||
aria-required="false"
|
|
||||||
aria-haspopup="true"
|
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
on:click={flatpickr?.open}
|
id={flatpickrId}
|
||||||
class="spectrum-Textfield spectrum-InputGroup-textfield"
|
|
||||||
class:is-disabled={disabled}
|
class:is-disabled={disabled}
|
||||||
class:is-invalid={!!error}
|
class:is-invalid={!!error}
|
||||||
|
class="flatpickr spectrum-InputGroup spectrum-Datepicker"
|
||||||
|
class:is-focused={open}
|
||||||
|
aria-readonly="false"
|
||||||
|
aria-required="false"
|
||||||
|
aria-haspopup="true"
|
||||||
>
|
>
|
||||||
{#if !!error}
|
<div
|
||||||
|
on:click={flatpickr?.open}
|
||||||
|
class="spectrum-Textfield spectrum-InputGroup-textfield"
|
||||||
|
class:is-disabled={disabled}
|
||||||
|
class:is-invalid={!!error}
|
||||||
|
>
|
||||||
|
{#if !!error}
|
||||||
|
<svg
|
||||||
|
class="spectrum-Icon spectrum-Icon--sizeM spectrum-Textfield-validationIcon"
|
||||||
|
focusable="false"
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
<use xlink:href="#spectrum-icon-18-Alert" />
|
||||||
|
</svg>
|
||||||
|
{/if}
|
||||||
|
<input
|
||||||
|
data-input
|
||||||
|
type="text"
|
||||||
|
{disabled}
|
||||||
|
class="spectrum-Textfield-input spectrum-InputGroup-input"
|
||||||
|
{placeholder}
|
||||||
|
{id}
|
||||||
|
{value}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="spectrum-Picker spectrum-Picker--sizeM spectrum-InputGroup-button"
|
||||||
|
tabindex="-1"
|
||||||
|
{disabled}
|
||||||
|
class:is-invalid={!!error}
|
||||||
|
on:click={flatpickr?.open}
|
||||||
|
>
|
||||||
<svg
|
<svg
|
||||||
class="spectrum-Icon spectrum-Icon--sizeM spectrum-Textfield-validationIcon"
|
class="spectrum-Icon spectrum-Icon--sizeM"
|
||||||
focusable="false"
|
focusable="false"
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
|
aria-label="Calendar"
|
||||||
>
|
>
|
||||||
<use xlink:href="#spectrum-icon-18-Alert" />
|
<use xlink:href="#spectrum-icon-18-Calendar" />
|
||||||
</svg>
|
</svg>
|
||||||
{/if}
|
</button>
|
||||||
<input
|
|
||||||
data-input
|
|
||||||
type="text"
|
|
||||||
{disabled}
|
|
||||||
class="spectrum-Textfield-input spectrum-InputGroup-input"
|
|
||||||
{placeholder}
|
|
||||||
{id}
|
|
||||||
{value}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<button
|
</Flatpickr>
|
||||||
type="button"
|
{/key}
|
||||||
class="spectrum-Picker spectrum-Picker--sizeM spectrum-InputGroup-button"
|
|
||||||
tabindex="-1"
|
|
||||||
{disabled}
|
|
||||||
class:is-invalid={!!error}
|
|
||||||
on:click={flatpickr?.open}
|
|
||||||
>
|
|
||||||
<svg
|
|
||||||
class="spectrum-Icon spectrum-Icon--sizeM"
|
|
||||||
focusable="false"
|
|
||||||
aria-hidden="true"
|
|
||||||
aria-label="Calendar"
|
|
||||||
>
|
|
||||||
<use xlink:href="#spectrum-icon-18-Calendar" />
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</Flatpickr>
|
|
||||||
{#if open}
|
{#if open}
|
||||||
<div class="overlay" on:mousedown|self={flatpickr?.close} />
|
<div class="overlay" on:mousedown|self={flatpickr?.close} />
|
||||||
{/if}
|
{/if}
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
import "@spectrum-css/typography/dist/index-vars.css"
|
import "@spectrum-css/typography/dist/index-vars.css"
|
||||||
import "@spectrum-css/illustratedmessage/dist/index-vars.css"
|
import "@spectrum-css/illustratedmessage/dist/index-vars.css"
|
||||||
import { createEventDispatcher } from "svelte"
|
import { createEventDispatcher } from "svelte"
|
||||||
import { generateID } from "../../utils/helpers"
|
import { uuid } from "../../helpers"
|
||||||
import Icon from "../../Icon/Icon.svelte"
|
import Icon from "../../Icon/Icon.svelte"
|
||||||
import Link from "../../Link/Link.svelte"
|
import Link from "../../Link/Link.svelte"
|
||||||
import Tag from "../../Tags/Tag.svelte"
|
import Tag from "../../Tags/Tag.svelte"
|
||||||
|
@ -37,7 +37,7 @@
|
||||||
"jfif",
|
"jfif",
|
||||||
]
|
]
|
||||||
|
|
||||||
const fieldId = id || generateID()
|
const fieldId = id || uuid()
|
||||||
let selectedImageIdx = 0
|
let selectedImageIdx = 0
|
||||||
let fileDragged = false
|
let fileDragged = false
|
||||||
let selectedUrl
|
let selectedUrl
|
||||||
|
|
|
@ -0,0 +1,42 @@
|
||||||
|
<script>
|
||||||
|
import MarkdownEditor from "../../Markdown/MarkdownEditor.svelte"
|
||||||
|
|
||||||
|
export let value = ""
|
||||||
|
export let placeholder = null
|
||||||
|
export let disabled = false
|
||||||
|
export let error = null
|
||||||
|
export let height = null
|
||||||
|
export let id = null
|
||||||
|
export let fullScreenOffset = null
|
||||||
|
export let easyMDEOptions = null
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class:error>
|
||||||
|
<MarkdownEditor
|
||||||
|
{value}
|
||||||
|
{placeholder}
|
||||||
|
{height}
|
||||||
|
{id}
|
||||||
|
{fullScreenOffset}
|
||||||
|
{disabled}
|
||||||
|
{easyMDEOptions}
|
||||||
|
on:change
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.error :global(.EasyMDEContainer .editor-toolbar) {
|
||||||
|
border-top-color: var(--spectrum-semantic-negative-color-default);
|
||||||
|
border-left-color: var(--spectrum-semantic-negative-color-default);
|
||||||
|
border-right-color: var(--spectrum-semantic-negative-color-default);
|
||||||
|
}
|
||||||
|
.error :global(.EasyMDEContainer .CodeMirror) {
|
||||||
|
border-bottom-color: var(--spectrum-semantic-negative-color-default);
|
||||||
|
border-left-color: var(--spectrum-semantic-negative-color-default);
|
||||||
|
border-right-color: var(--spectrum-semantic-negative-color-default);
|
||||||
|
}
|
||||||
|
.error :global(.EasyMDEContainer .editor-preview-side) {
|
||||||
|
border-bottom-color: var(--spectrum-semantic-negative-color-default);
|
||||||
|
border-right-color: var(--spectrum-semantic-negative-color-default);
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -13,6 +13,7 @@
|
||||||
start: textarea.selectionStart,
|
start: textarea.selectionStart,
|
||||||
end: textarea.selectionEnd,
|
end: textarea.selectionEnd,
|
||||||
})
|
})
|
||||||
|
export let align = null
|
||||||
|
|
||||||
let focus = false
|
let focus = false
|
||||||
let textarea
|
let textarea
|
||||||
|
@ -21,11 +22,23 @@
|
||||||
dispatch("change", event.target.value)
|
dispatch("change", event.target.value)
|
||||||
focus = false
|
focus = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const getStyleString = (attribute, value) => {
|
||||||
|
if (!attribute || value == null) {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
if (isNaN(value)) {
|
||||||
|
return `${attribute}:${value};`
|
||||||
|
}
|
||||||
|
return `${attribute}:${value}px;`
|
||||||
|
}
|
||||||
|
|
||||||
|
$: heightString = getStyleString("height", height)
|
||||||
|
$: minHeightString = getStyleString("min-height", minHeight)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
style={(height ? `height: ${height}px;` : "") +
|
style={`${heightString}${minHeightString}`}
|
||||||
(minHeight ? `min-height: ${minHeight}px` : "")}
|
|
||||||
class="spectrum-Textfield spectrum-Textfield--multiline"
|
class="spectrum-Textfield spectrum-Textfield--multiline"
|
||||||
class:is-invalid={!!error}
|
class:is-invalid={!!error}
|
||||||
class:is-disabled={disabled}
|
class:is-disabled={disabled}
|
||||||
|
@ -46,6 +59,7 @@
|
||||||
bind:this={textarea}
|
bind:this={textarea}
|
||||||
placeholder={placeholder || ""}
|
placeholder={placeholder || ""}
|
||||||
class="spectrum-Textfield-input"
|
class="spectrum-Textfield-input"
|
||||||
|
style={align ? `text-align: ${align}` : ""}
|
||||||
{disabled}
|
{disabled}
|
||||||
{id}
|
{id}
|
||||||
on:focus={() => (focus = true)}
|
on:focus={() => (focus = true)}
|
||||||
|
|
|
@ -12,6 +12,7 @@
|
||||||
export let updateOnChange = true
|
export let updateOnChange = true
|
||||||
export let quiet = false
|
export let quiet = false
|
||||||
export let dataCy
|
export let dataCy
|
||||||
|
export let align
|
||||||
|
|
||||||
const dispatch = createEventDispatcher()
|
const dispatch = createEventDispatcher()
|
||||||
let focus = false
|
let focus = false
|
||||||
|
@ -92,8 +93,9 @@
|
||||||
on:input={onInput}
|
on:input={onInput}
|
||||||
on:keyup={updateValueOnEnter}
|
on:keyup={updateValueOnEnter}
|
||||||
{type}
|
{type}
|
||||||
inputmode={type === "number" ? "decimal" : "text"}
|
|
||||||
class="spectrum-Textfield-input"
|
class="spectrum-Textfield-input"
|
||||||
|
style={align ? `text-align: ${align};` : ""}
|
||||||
|
inputmode={type === "number" ? "decimal" : "text"}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
@ -10,3 +10,4 @@ export { default as CoreSearch } from "./Search.svelte"
|
||||||
export { default as CoreDatePicker } from "./DatePicker.svelte"
|
export { default as CoreDatePicker } from "./DatePicker.svelte"
|
||||||
export { default as CoreDropzone } from "./Dropzone.svelte"
|
export { default as CoreDropzone } from "./Dropzone.svelte"
|
||||||
export { default as CoreStepper } from "./Stepper.svelte"
|
export { default as CoreStepper } from "./Stepper.svelte"
|
||||||
|
export { default as CoreRichTextField } from "./RichTextField.svelte"
|
||||||
|
|
|
@ -9,6 +9,7 @@
|
||||||
export let disabled = false
|
export let disabled = false
|
||||||
export let error = null
|
export let error = null
|
||||||
export let enableTime = true
|
export let enableTime = true
|
||||||
|
export let timeOnly = false
|
||||||
export let placeholder = null
|
export let placeholder = null
|
||||||
export let appendTo = undefined
|
export let appendTo = undefined
|
||||||
|
|
||||||
|
@ -27,6 +28,7 @@
|
||||||
{value}
|
{value}
|
||||||
{placeholder}
|
{placeholder}
|
||||||
{enableTime}
|
{enableTime}
|
||||||
|
{timeOnly}
|
||||||
{appendTo}
|
{appendTo}
|
||||||
on:change={onChange}
|
on:change={onChange}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -0,0 +1,36 @@
|
||||||
|
<script>
|
||||||
|
import Field from "./Field.svelte"
|
||||||
|
import RichTextField from "./Core/RichTextField.svelte"
|
||||||
|
import { createEventDispatcher } from "svelte"
|
||||||
|
|
||||||
|
export let value = null
|
||||||
|
export let label = null
|
||||||
|
export let labelPosition = "above"
|
||||||
|
export let placeholder = null
|
||||||
|
export let disabled = false
|
||||||
|
export let error = null
|
||||||
|
export let height = null
|
||||||
|
export let id = null
|
||||||
|
export let fullScreenOffset = null
|
||||||
|
export let easyMDEOptions = null
|
||||||
|
|
||||||
|
const dispatch = createEventDispatcher()
|
||||||
|
const onChange = e => {
|
||||||
|
value = e.detail
|
||||||
|
dispatch("change", e.detail)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Field {label} {labelPosition} {error}>
|
||||||
|
<RichTextField
|
||||||
|
{error}
|
||||||
|
{disabled}
|
||||||
|
{value}
|
||||||
|
{placeholder}
|
||||||
|
{height}
|
||||||
|
{id}
|
||||||
|
{fullScreenOffset}
|
||||||
|
{easyMDEOptions}
|
||||||
|
on:change={onChange}
|
||||||
|
/>
|
||||||
|
</Field>
|
|
@ -0,0 +1,60 @@
|
||||||
|
<script>
|
||||||
|
import SpectrumMDE from "./SpectrumMDE.svelte"
|
||||||
|
import { createEventDispatcher } from "svelte"
|
||||||
|
|
||||||
|
export let value = null
|
||||||
|
export let height = null
|
||||||
|
export let placeholder = null
|
||||||
|
export let id = null
|
||||||
|
export let fullScreenOffset = 0
|
||||||
|
export let disabled = false
|
||||||
|
export let easyMDEOptions
|
||||||
|
|
||||||
|
const dispatch = createEventDispatcher()
|
||||||
|
|
||||||
|
let latestValue
|
||||||
|
let mde
|
||||||
|
|
||||||
|
// Ensure the value is updated if the value prop changes outside the editor's
|
||||||
|
// control
|
||||||
|
$: checkValue(value)
|
||||||
|
$: mde?.codemirror.on("change", debouncedUpdate)
|
||||||
|
|
||||||
|
const checkValue = val => {
|
||||||
|
if (mde && val !== latestValue) {
|
||||||
|
mde.value(val)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const debounce = (fn, interval) => {
|
||||||
|
let timeout
|
||||||
|
return () => {
|
||||||
|
clearTimeout(timeout)
|
||||||
|
timeout = setTimeout(fn, interval)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const update = () => {
|
||||||
|
latestValue = mde.value()
|
||||||
|
dispatch("change", latestValue)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Debounce the update function to avoid spamming it constantly
|
||||||
|
const debouncedUpdate = debounce(update, 250)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#key height}
|
||||||
|
<SpectrumMDE
|
||||||
|
bind:mde
|
||||||
|
scroll={true}
|
||||||
|
{height}
|
||||||
|
{id}
|
||||||
|
{fullScreenOffset}
|
||||||
|
{disabled}
|
||||||
|
easyMDEOptions={{
|
||||||
|
initialValue: value,
|
||||||
|
placeholder,
|
||||||
|
...easyMDEOptions,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{/key}
|
|
@ -0,0 +1,70 @@
|
||||||
|
<script>
|
||||||
|
import SpectrumMDE from "./SpectrumMDE.svelte"
|
||||||
|
|
||||||
|
export let value
|
||||||
|
export let height
|
||||||
|
|
||||||
|
let mde
|
||||||
|
|
||||||
|
// Keep the value up to date
|
||||||
|
$: mde && mde.value(value || "")
|
||||||
|
$: {
|
||||||
|
if (mde && !mde.isPreviewActive()) {
|
||||||
|
mde.togglePreview()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="markdown-viewer" style="height:{height};">
|
||||||
|
<SpectrumMDE
|
||||||
|
bind:mde
|
||||||
|
scroll={false}
|
||||||
|
easyMDEOptions={{
|
||||||
|
initialValue: value,
|
||||||
|
toolbar: false,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.markdown-viewer {
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
/* Remove padding, borders and background colors */
|
||||||
|
.markdown-viewer :global(.editor-preview) {
|
||||||
|
padding: 0;
|
||||||
|
border: none;
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
.markdown-viewer :global(.CodeMirror) {
|
||||||
|
border: none;
|
||||||
|
background: transparent;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
.markdown-viewer :global(.EasyMDEContainer) {
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
/* Hide the actual code editor */
|
||||||
|
.markdown-viewer :global(.CodeMirror-scroll) {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
/*Hide the scrollbar*/
|
||||||
|
.markdown-viewer :global(.CodeMirror-vscrollbar) {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
/*Position relatively so we only consume whatever space we need */
|
||||||
|
.markdown-viewer :global(.editor-preview-full) {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
/* Remove margin on the first and last components to fully trim the preview */
|
||||||
|
.markdown-viewer :global(.editor-preview-full > :first-child) {
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
.markdown-viewer :global(.editor-preview-full > :last-child) {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
/* Code blocks in preview */
|
||||||
|
.markdown-viewer :global(.editor-preview-full pre) {
|
||||||
|
background: var(--spectrum-global-color-gray-200);
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,184 @@
|
||||||
|
<script>
|
||||||
|
import EasyMDE from "easymde"
|
||||||
|
import "easymde/dist/easymde.min.css"
|
||||||
|
import { onMount } from "svelte"
|
||||||
|
|
||||||
|
export let height = null
|
||||||
|
export let scroll = true
|
||||||
|
export let easyMDEOptions = null
|
||||||
|
export let mde = null
|
||||||
|
export let id = null
|
||||||
|
export let fullScreenOffset = null
|
||||||
|
export let disabled = false
|
||||||
|
|
||||||
|
let element
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
height = height || "200px"
|
||||||
|
mde = new EasyMDE({
|
||||||
|
element,
|
||||||
|
spellChecker: false,
|
||||||
|
status: false,
|
||||||
|
unorderedListStyle: "-",
|
||||||
|
maxHeight: scroll ? height : undefined,
|
||||||
|
minHeight: scroll ? undefined : height,
|
||||||
|
...easyMDEOptions,
|
||||||
|
})
|
||||||
|
|
||||||
|
// Revert the editor when we unmount
|
||||||
|
return () => {
|
||||||
|
mde.toTextArea()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
$: styleString = getStyleString(fullScreenOffset)
|
||||||
|
|
||||||
|
const getStyleString = offset => {
|
||||||
|
let string = ""
|
||||||
|
string += `--fullscreen-offset-x:${offset?.x || "0px"};`
|
||||||
|
string += `--fullscreen-offset-y:${offset?.y || "0px"};`
|
||||||
|
return string
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class:disabled style={styleString}>
|
||||||
|
<textarea disabled {id} bind:this={element} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
/* Disabled styles */
|
||||||
|
.disabled :global(textarea) {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.disabled :global(.CodeMirror-cursor) {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.disabled :global(.EasyMDEContainer) {
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
.disabled :global(.editor-toolbar button i) {
|
||||||
|
color: var(--spectrum-global-color-gray-400);
|
||||||
|
}
|
||||||
|
.disabled :global(.CodeMirror) {
|
||||||
|
color: var(--spectrum-global-color-gray-600);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Toolbar container */
|
||||||
|
:global(.EasyMDEContainer .editor-toolbar) {
|
||||||
|
background: var(--spectrum-global-color-gray-50);
|
||||||
|
border-top: 1px solid var(--spectrum-alias-border-color);
|
||||||
|
border-left: 1px solid var(--spectrum-alias-border-color);
|
||||||
|
border-right: 1px solid var(--spectrum-alias-border-color);
|
||||||
|
}
|
||||||
|
/* Main code mirror instance and default color */
|
||||||
|
:global(.EasyMDEContainer .CodeMirror) {
|
||||||
|
border: 1px solid var(--spectrum-alias-border-color);
|
||||||
|
background: var(--spectrum-global-color-gray-50);
|
||||||
|
color: var(--spectrum-alias-text-color);
|
||||||
|
}
|
||||||
|
/* Toolbar button active state */
|
||||||
|
:global(.EasyMDEContainer .editor-toolbar button.active) {
|
||||||
|
background: var(--spectrum-global-color-gray-200);
|
||||||
|
border-color: var(--spectrum-global-color-gray-400);
|
||||||
|
}
|
||||||
|
/* Toolbar button hover state */
|
||||||
|
:global(.EasyMDEContainer .editor-toolbar button:hover) {
|
||||||
|
background: var(--spectrum-global-color-gray-200);
|
||||||
|
border-color: var(--spectrum-global-color-gray-400);
|
||||||
|
}
|
||||||
|
/* Toolbar button color */
|
||||||
|
:global(.EasyMDEContainer .editor-toolbar button i) {
|
||||||
|
color: var(--spectrum-global-color-gray-800);
|
||||||
|
}
|
||||||
|
/* Separator between toolbar buttons*/
|
||||||
|
:global(.EasyMDEContainer .editor-toolbar i.separator) {
|
||||||
|
border-color: var(--spectrum-global-color-gray-300);
|
||||||
|
}
|
||||||
|
/* Cursor */
|
||||||
|
:global(.EasyMDEContainer .CodeMirror-cursor) {
|
||||||
|
border-color: var(--spectrum-alias-text-color);
|
||||||
|
}
|
||||||
|
/* Text selections */
|
||||||
|
:global(.EasyMDEContainer .CodeMirror-selectedtext) {
|
||||||
|
background: var(--spectrum-global-color-gray-400) !important;
|
||||||
|
}
|
||||||
|
/* Background of lines containing selected text */
|
||||||
|
:global(.EasyMDEContainer .CodeMirror-selected) {
|
||||||
|
background: var(--spectrum-global-color-gray-400) !important;
|
||||||
|
}
|
||||||
|
/* Color of text for images and links */
|
||||||
|
:global(.EasyMDEContainer .cm-s-easymde .cm-link) {
|
||||||
|
color: var(--spectrum-global-color-gray-600);
|
||||||
|
}
|
||||||
|
/* Color of URL for images and links */
|
||||||
|
:global(.EasyMDEContainer .cm-s-easymde .cm-url) {
|
||||||
|
color: var(--spectrum-global-color-gray-500);
|
||||||
|
}
|
||||||
|
/* Full preview window */
|
||||||
|
:global(.EasyMDEContainer .editor-preview) {
|
||||||
|
background: var(--spectrum-global-color-gray-50);
|
||||||
|
}
|
||||||
|
/* Side by side preview window */
|
||||||
|
:global(.EasyMDEContainer .editor-preview) {
|
||||||
|
border: 1px solid var(--spectrum-alias-border-color);
|
||||||
|
}
|
||||||
|
/* Code blocks in editor */
|
||||||
|
:global(.EasyMDEContainer .cm-s-easymde .cm-comment) {
|
||||||
|
background: var(--spectrum-global-color-gray-100);
|
||||||
|
}
|
||||||
|
/* Code blocks in preview */
|
||||||
|
:global(.EasyMDEContainer pre) {
|
||||||
|
background: var(--spectrum-global-color-gray-100);
|
||||||
|
padding: 4px;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
:global(.EasyMDEContainer code) {
|
||||||
|
color: #e83e8c;
|
||||||
|
}
|
||||||
|
:global(.EasyMDEContainer pre code) {
|
||||||
|
color: var(--spectrum-alias-text-color);
|
||||||
|
}
|
||||||
|
/* Block quotes */
|
||||||
|
:global(.EasyMDEContainer blockquote) {
|
||||||
|
border-left: 4px solid var(--spectrum-global-color-gray-400);
|
||||||
|
color: var(--spectrum-global-color-gray-700);
|
||||||
|
margin-left: 0;
|
||||||
|
padding-left: 20px;
|
||||||
|
}
|
||||||
|
/* HR's */
|
||||||
|
:global(.EasyMDEContainer hr) {
|
||||||
|
background-color: var(--spectrum-global-color-gray-300);
|
||||||
|
border: none;
|
||||||
|
height: 2px;
|
||||||
|
}
|
||||||
|
/* Tables */
|
||||||
|
:global(.EasyMDEContainer td, .EasyMDEContainer th) {
|
||||||
|
border-color: var(--spectrum-alias-border-color) !important;
|
||||||
|
}
|
||||||
|
/* Links */
|
||||||
|
:global(.EasyMDEContainer a) {
|
||||||
|
color: var(--primaryColor);
|
||||||
|
}
|
||||||
|
:global(.EasyMDEContainer a:hover) {
|
||||||
|
color: var(--primaryColorHover);
|
||||||
|
}
|
||||||
|
/* Allow full screen offset */
|
||||||
|
:global(.EasyMDEContainer .editor-toolbar.fullscreen) {
|
||||||
|
left: var(--fullscreen-offset-x);
|
||||||
|
top: var(--fullscreen-offset-y);
|
||||||
|
}
|
||||||
|
:global(.EasyMDEContainer .CodeMirror-fullscreen) {
|
||||||
|
left: var(--fullscreen-offset-x);
|
||||||
|
top: calc(50px + var(--fullscreen-offset-y));
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.EasyMDEContainer .CodeMirror-fullscreen.CodeMirror-sided) {
|
||||||
|
width: calc((100% - var(--fullscreen-offset-x)) / 2) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.EasyMDEContainer .editor-preview-side) {
|
||||||
|
left: calc(50% + (var(--fullscreen-offset-x) / 2));
|
||||||
|
top: calc(50px + var(--fullscreen-offset-y));
|
||||||
|
width: calc((100% - var(--fullscreen-offset-x)) / 2) !important;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -1,7 +1,12 @@
|
||||||
<script>
|
<script>
|
||||||
|
import { createEventDispatcher } from "svelte"
|
||||||
|
|
||||||
export let type = "info"
|
export let type = "info"
|
||||||
export let icon = "Info"
|
export let icon = "Info"
|
||||||
export let message = ""
|
export let message = ""
|
||||||
|
export let dismissable = false
|
||||||
|
|
||||||
|
const dispatch = createEventDispatcher()
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="spectrum-Toast spectrum-Toast--{type}">
|
<div class="spectrum-Toast spectrum-Toast--{type}">
|
||||||
|
@ -17,4 +22,28 @@
|
||||||
<div class="spectrum-Toast-body">
|
<div class="spectrum-Toast-body">
|
||||||
<div class="spectrum-Toast-content">{message || ""}</div>
|
<div class="spectrum-Toast-content">{message || ""}</div>
|
||||||
</div>
|
</div>
|
||||||
|
{#if dismissable}
|
||||||
|
<div class="spectrum-Toast-buttons">
|
||||||
|
<button
|
||||||
|
class="spectrum-ClearButton spectrum-ClearButton--overBackground spectrum-ClearButton--sizeM"
|
||||||
|
on:click={() => dispatch("dismiss")}
|
||||||
|
>
|
||||||
|
<div class="spectrum-ClearButton-fill">
|
||||||
|
<svg
|
||||||
|
class="spectrum-ClearButton-icon spectrum-Icon spectrum-UIIcon-Cross100"
|
||||||
|
focusable="false"
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
<use xlink:href="#spectrum-css-icon-Cross100" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.spectrum-Toast {
|
||||||
|
pointer-events: all;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
<script>
|
<script>
|
||||||
import "@spectrum-css/toast/dist/index-vars.css"
|
import "@spectrum-css/toast/dist/index-vars.css"
|
||||||
import Portal from "svelte-portal"
|
import Portal from "svelte-portal"
|
||||||
import { flip } from "svelte/animate"
|
|
||||||
import { notifications } from "../Stores/notifications"
|
import { notifications } from "../Stores/notifications"
|
||||||
import Notification from "./Notification.svelte"
|
import Notification from "./Notification.svelte"
|
||||||
import { fly } from "svelte/transition"
|
import { fly } from "svelte/transition"
|
||||||
|
@ -9,9 +8,15 @@
|
||||||
|
|
||||||
<Portal target=".modal-container">
|
<Portal target=".modal-container">
|
||||||
<div class="notifications">
|
<div class="notifications">
|
||||||
{#each $notifications as { type, icon, message, id } (id)}
|
{#each $notifications as { type, icon, message, id, dismissable } (id)}
|
||||||
<div animate:flip transition:fly={{ y: -30 }}>
|
<div transition:fly={{ y: -30 }}>
|
||||||
<Notification {type} {icon} {message} />
|
<Notification
|
||||||
|
{type}
|
||||||
|
{icon}
|
||||||
|
{message}
|
||||||
|
{dismissable}
|
||||||
|
on:dismiss={() => notifications.dismiss(id)}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -20,20 +20,29 @@ export const createNotificationStore = () => {
|
||||||
setTimeout(() => (block = false), timeout)
|
setTimeout(() => (block = false), timeout)
|
||||||
}
|
}
|
||||||
|
|
||||||
const send = (message, type = "default", icon = "") => {
|
const send = (message, type = "default", icon = "", autoDismiss = true) => {
|
||||||
if (block) {
|
if (block) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
let _id = id()
|
let _id = id()
|
||||||
_notifications.update(state => {
|
_notifications.update(state => {
|
||||||
return [...state, { id: _id, type, message, icon }]
|
return [
|
||||||
|
...state,
|
||||||
|
{ id: _id, type, message, icon, dismissable: !autoDismiss },
|
||||||
|
]
|
||||||
|
})
|
||||||
|
if (autoDismiss) {
|
||||||
|
const timeoutId = setTimeout(() => {
|
||||||
|
dismissNotification(_id)
|
||||||
|
}, NOTIFICATION_TIMEOUT)
|
||||||
|
timeoutIds.add(timeoutId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const dismissNotification = id => {
|
||||||
|
_notifications.update(state => {
|
||||||
|
return state.filter(n => n.id !== id)
|
||||||
})
|
})
|
||||||
const timeoutId = setTimeout(() => {
|
|
||||||
_notifications.update(state => {
|
|
||||||
return state.filter(({ id }) => id !== _id)
|
|
||||||
})
|
|
||||||
}, NOTIFICATION_TIMEOUT)
|
|
||||||
timeoutIds.add(timeoutId)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const { subscribe } = _notifications
|
const { subscribe } = _notifications
|
||||||
|
@ -42,10 +51,11 @@ export const createNotificationStore = () => {
|
||||||
subscribe,
|
subscribe,
|
||||||
send,
|
send,
|
||||||
info: msg => send(msg, "info", "Info"),
|
info: msg => send(msg, "info", "Info"),
|
||||||
error: msg => send(msg, "error", "Alert"),
|
error: msg => send(msg, "error", "Alert", false),
|
||||||
warning: msg => send(msg, "warning", "Alert"),
|
warning: msg => send(msg, "warning", "Alert"),
|
||||||
success: msg => send(msg, "success", "CheckmarkCircle"),
|
success: msg => send(msg, "success", "CheckmarkCircle"),
|
||||||
blockNotifications,
|
blockNotifications,
|
||||||
|
dismiss: dismissNotification,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -2,9 +2,18 @@
|
||||||
import dayjs from "dayjs"
|
import dayjs from "dayjs"
|
||||||
|
|
||||||
export let value
|
export let value
|
||||||
|
|
||||||
|
// adding the 0- will turn a string like 00:00:00 into a valid ISO
|
||||||
|
// date, but will make actual ISO dates invalid
|
||||||
|
$: time = new Date(`0-${value}`)
|
||||||
|
$: isTime = !isNaN(time)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div>{dayjs(value).format("MMMM D YYYY, HH:mm")}</div>
|
<div>
|
||||||
|
{dayjs(isTime ? time : value).format(
|
||||||
|
isTime ? "HH:mm:ss" : "MMMM D YYYY, HH:mm"
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
div {
|
div {
|
||||||
|
|
|
@ -8,10 +8,35 @@
|
||||||
copyToClipboard(value)
|
copyToClipboard(value)
|
||||||
}
|
}
|
||||||
|
|
||||||
function copyToClipboard(value) {
|
const copyToClipboard = value => {
|
||||||
navigator.clipboard.writeText(value).then(() => {
|
return new Promise(res => {
|
||||||
notifications.success("Copied")
|
if (navigator.clipboard && window.isSecureContext) {
|
||||||
|
// Try using the clipboard API first
|
||||||
|
navigator.clipboard.writeText(value).then(res)
|
||||||
|
} else {
|
||||||
|
// Fall back to the textarea hack
|
||||||
|
let textArea = document.createElement("textarea")
|
||||||
|
textArea.value = value
|
||||||
|
textArea.style.position = "fixed"
|
||||||
|
textArea.style.left = "-9999px"
|
||||||
|
textArea.style.top = "-9999px"
|
||||||
|
document.body.appendChild(textArea)
|
||||||
|
textArea.focus()
|
||||||
|
textArea.select()
|
||||||
|
document.execCommand("copy")
|
||||||
|
textArea.remove()
|
||||||
|
res()
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
.then(() => {
|
||||||
|
notifications.success("Copied to clipboard")
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
notifications.error(
|
||||||
|
"Failed to copy to clipboard. Check the dev console for the value."
|
||||||
|
)
|
||||||
|
console.warn("Failed to copy the value", value)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
|
@ -4,7 +4,7 @@
|
||||||
import CellRenderer from "./CellRenderer.svelte"
|
import CellRenderer from "./CellRenderer.svelte"
|
||||||
import SelectEditRenderer from "./SelectEditRenderer.svelte"
|
import SelectEditRenderer from "./SelectEditRenderer.svelte"
|
||||||
import { cloneDeep } from "lodash"
|
import { cloneDeep } from "lodash"
|
||||||
import { deepGet } from "../utils/helpers"
|
import { deepGet } from "../helpers"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The expected schema is our normal couch schemas for our tables.
|
* The expected schema is our normal couch schemas for our tables.
|
||||||
|
|
|
@ -17,6 +17,7 @@
|
||||||
class:icon-small={size === "M" || size === "S"}
|
class:icon-small={size === "M" || size === "S"}
|
||||||
on:mouseover={() => (showTooltip = true)}
|
on:mouseover={() => (showTooltip = true)}
|
||||||
on:mouseleave={() => (showTooltip = false)}
|
on:mouseleave={() => (showTooltip = false)}
|
||||||
|
on:focus
|
||||||
>
|
>
|
||||||
<Icon name="InfoOutline" size="S" disabled={true} />
|
<Icon name="InfoOutline" size="S" disabled={true} />
|
||||||
</div>
|
</div>
|
||||||
|
@ -47,7 +48,7 @@
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
top: 15px;
|
top: 15px;
|
||||||
z-index: 1;
|
z-index: 100;
|
||||||
width: 160px;
|
width: 160px;
|
||||||
}
|
}
|
||||||
.icon {
|
.icon {
|
||||||
|
|
|
@ -1,11 +1,45 @@
|
||||||
export const generateID = () => {
|
/**
|
||||||
const rand = Math.random().toString(32).substring(2)
|
* Generates a DOM safe UUID.
|
||||||
|
* Starting with a letter is important to make it DOM safe.
|
||||||
// Starts with a letter so that its a valid DOM ID
|
* @return {string} a random DOM safe UUID
|
||||||
return `A${rand}`
|
*/
|
||||||
|
export function uuid() {
|
||||||
|
return "cxxxxxxxxxxxx4xxxyxxxxxxxxxxxxxxx".replace(/[xy]/g, c => {
|
||||||
|
const r = (Math.random() * 16) | 0
|
||||||
|
const v = c === "x" ? r : (r & 0x3) | 0x8
|
||||||
|
return v.toString(16)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export const capitalise = s => s.substring(0, 1).toUpperCase() + s.substring(1)
|
/**
|
||||||
|
* Capitalises a string
|
||||||
|
* @param string the string to capitalise
|
||||||
|
* @return {string} the capitalised string
|
||||||
|
*/
|
||||||
|
export const capitalise = string => {
|
||||||
|
if (!string) {
|
||||||
|
return string
|
||||||
|
}
|
||||||
|
return string.substring(0, 1).toUpperCase() + string.substring(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Computes a short hash of a string
|
||||||
|
* @param string the string to compute a hash of
|
||||||
|
* @return {string} the hash string
|
||||||
|
*/
|
||||||
|
export const hashString = string => {
|
||||||
|
if (!string) {
|
||||||
|
return "0"
|
||||||
|
}
|
||||||
|
let hash = 0
|
||||||
|
for (let i = 0; i < string.length; i++) {
|
||||||
|
let char = string.charCodeAt(i)
|
||||||
|
hash = (hash << 5) - hash + char
|
||||||
|
hash = hash & hash // Convert to 32bit integer
|
||||||
|
}
|
||||||
|
return hash.toString()
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets a key within an object. The key supports dot syntax for retrieving deep
|
* Gets a key within an object. The key supports dot syntax for retrieving deep
|
|
@ -60,6 +60,9 @@ export { default as StatusLight } from "./StatusLight/StatusLight.svelte"
|
||||||
export { default as ColorPicker } from "./ColorPicker/ColorPicker.svelte"
|
export { default as ColorPicker } from "./ColorPicker/ColorPicker.svelte"
|
||||||
export { default as InlineAlert } from "./InlineAlert/InlineAlert.svelte"
|
export { default as InlineAlert } from "./InlineAlert/InlineAlert.svelte"
|
||||||
export { default as Banner } from "./Banner/Banner.svelte"
|
export { default as Banner } from "./Banner/Banner.svelte"
|
||||||
|
export { default as MarkdownEditor } from "./Markdown/MarkdownEditor.svelte"
|
||||||
|
export { default as MarkdownViewer } from "./Markdown/MarkdownViewer.svelte"
|
||||||
|
export { default as RichTextField } from "./Form/RichTextField.svelte"
|
||||||
|
|
||||||
// Renderers
|
// Renderers
|
||||||
export { default as BoldRenderer } from "./Table/BoldRenderer.svelte"
|
export { default as BoldRenderer } from "./Table/BoldRenderer.svelte"
|
||||||
|
@ -82,5 +85,5 @@ export { default as clickOutside } from "./Actions/click_outside"
|
||||||
// Stores
|
// Stores
|
||||||
export { notifications, createNotificationStore } from "./Stores/notifications"
|
export { notifications, createNotificationStore } from "./Stores/notifications"
|
||||||
|
|
||||||
// Utils
|
// Helpers
|
||||||
export * from "./utils/helpers"
|
export * as Helpers from "./helpers"
|
||||||
|
|
|
@ -271,6 +271,13 @@
|
||||||
resolved "https://registry.yarnpkg.com/@spectrum-css/vars/-/vars-3.0.2.tgz#ea9062c3c98dfc6ba59e5df14a03025ad8969999"
|
resolved "https://registry.yarnpkg.com/@spectrum-css/vars/-/vars-3.0.2.tgz#ea9062c3c98dfc6ba59e5df14a03025ad8969999"
|
||||||
integrity sha512-vzS9KqYXot4J3AEER/u618MXWAS+IoMvYMNrOoscKiLLKYQWenaueakUWulFonToPd/9vIpqtdbwxznqrK5qDw==
|
integrity sha512-vzS9KqYXot4J3AEER/u618MXWAS+IoMvYMNrOoscKiLLKYQWenaueakUWulFonToPd/9vIpqtdbwxznqrK5qDw==
|
||||||
|
|
||||||
|
"@types/codemirror@^5.60.4":
|
||||||
|
version "5.60.5"
|
||||||
|
resolved "https://registry.yarnpkg.com/@types/codemirror/-/codemirror-5.60.5.tgz#5b989a3b4bbe657458cf372c92b6bfda6061a2b7"
|
||||||
|
integrity sha512-TiECZmm8St5YxjFUp64LK0c8WU5bxMDt9YaAek1UqUb9swrSCoJhh92fWu1p3mTEqlHjhB5sY7OFBhWroJXZVg==
|
||||||
|
dependencies:
|
||||||
|
"@types/tern" "*"
|
||||||
|
|
||||||
"@types/estree@*":
|
"@types/estree@*":
|
||||||
version "0.0.47"
|
version "0.0.47"
|
||||||
resolved "https://registry.yarnpkg.com/@types/estree/-/estree-0.0.47.tgz#d7a51db20f0650efec24cd04994f523d93172ed4"
|
resolved "https://registry.yarnpkg.com/@types/estree/-/estree-0.0.47.tgz#d7a51db20f0650efec24cd04994f523d93172ed4"
|
||||||
|
@ -281,6 +288,11 @@
|
||||||
resolved "https://registry.yarnpkg.com/@types/estree/-/estree-0.0.39.tgz#e177e699ee1b8c22d23174caaa7422644389509f"
|
resolved "https://registry.yarnpkg.com/@types/estree/-/estree-0.0.39.tgz#e177e699ee1b8c22d23174caaa7422644389509f"
|
||||||
integrity sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw==
|
integrity sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw==
|
||||||
|
|
||||||
|
"@types/marked@^4.0.1":
|
||||||
|
version "4.0.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/@types/marked/-/marked-4.0.2.tgz#cb2dbf10da2f41cf20bd91fb5f89b67540c282f7"
|
||||||
|
integrity sha512-auNrZ/c0w6wsM9DccwVxWHssrMDezHUAXNesdp2RQrCVCyrQbOiSq7yqdJKrUQQpw9VTm7CGYJH2A/YG7jjrjQ==
|
||||||
|
|
||||||
"@types/node@*":
|
"@types/node@*":
|
||||||
version "14.14.41"
|
version "14.14.41"
|
||||||
resolved "https://registry.yarnpkg.com/@types/node/-/node-14.14.41.tgz#d0b939d94c1d7bd53d04824af45f1139b8c45615"
|
resolved "https://registry.yarnpkg.com/@types/node/-/node-14.14.41.tgz#d0b939d94c1d7bd53d04824af45f1139b8c45615"
|
||||||
|
@ -303,6 +315,13 @@
|
||||||
dependencies:
|
dependencies:
|
||||||
"@types/node" "*"
|
"@types/node" "*"
|
||||||
|
|
||||||
|
"@types/tern@*":
|
||||||
|
version "0.23.4"
|
||||||
|
resolved "https://registry.yarnpkg.com/@types/tern/-/tern-0.23.4.tgz#03926eb13dbeaf3ae0d390caf706b2643a0127fb"
|
||||||
|
integrity sha512-JAUw1iXGO1qaWwEOzxTKJZ/5JxVeON9kvGZ/osgZaJImBnyjyn0cjovPsf6FNLmyGY8Vw9DoXZCMlfMkMwHRWg==
|
||||||
|
dependencies:
|
||||||
|
"@types/estree" "*"
|
||||||
|
|
||||||
accepts@~1.3.7:
|
accepts@~1.3.7:
|
||||||
version "1.3.7"
|
version "1.3.7"
|
||||||
resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.7.tgz#531bc726517a3b2b41f850021c6cc15eaab507cd"
|
resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.7.tgz#531bc726517a3b2b41f850021c6cc15eaab507cd"
|
||||||
|
@ -525,6 +544,18 @@ coa@^2.0.2:
|
||||||
chalk "^2.4.1"
|
chalk "^2.4.1"
|
||||||
q "^1.1.2"
|
q "^1.1.2"
|
||||||
|
|
||||||
|
codemirror-spell-checker@1.1.2:
|
||||||
|
version "1.1.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/codemirror-spell-checker/-/codemirror-spell-checker-1.1.2.tgz#1c660f9089483ccb5113b9ba9ca19c3f4993371e"
|
||||||
|
integrity sha1-HGYPkIlIPMtRE7m6nKGcP0mTNx4=
|
||||||
|
dependencies:
|
||||||
|
typo-js "*"
|
||||||
|
|
||||||
|
codemirror@^5.63.1:
|
||||||
|
version "5.65.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/codemirror/-/codemirror-5.65.1.tgz#5988a812c974c467f964bcc1a00c944e373de502"
|
||||||
|
integrity sha512-s6aac+DD+4O2u1aBmdxhB7yz2XU7tG3snOyQ05Kxifahz7hoxnfxIRHxiCSEv3TUC38dIVH8G+lZH9UWSfGQxA==
|
||||||
|
|
||||||
color-convert@^1.9.0, color-convert@^1.9.1:
|
color-convert@^1.9.0, color-convert@^1.9.1:
|
||||||
version "1.9.3"
|
version "1.9.3"
|
||||||
resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8"
|
resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8"
|
||||||
|
@ -861,6 +892,17 @@ dot-prop@^5.2.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
is-obj "^2.0.0"
|
is-obj "^2.0.0"
|
||||||
|
|
||||||
|
easymde@^2.16.1:
|
||||||
|
version "2.16.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/easymde/-/easymde-2.16.1.tgz#f4c2380312615cb33826f1a1fecfaa4022ff551a"
|
||||||
|
integrity sha512-FihYgjRsKfhGNk89SHSqxKLC4aJ1kfybPWW6iAmtb5GnXu+tnFPSzSaGBmk1RRlCuhFSjhF0SnIMGVPjEzkr6g==
|
||||||
|
dependencies:
|
||||||
|
"@types/codemirror" "^5.60.4"
|
||||||
|
"@types/marked" "^4.0.1"
|
||||||
|
codemirror "^5.63.1"
|
||||||
|
codemirror-spell-checker "1.1.2"
|
||||||
|
marked "^4.0.10"
|
||||||
|
|
||||||
ee-first@1.1.1:
|
ee-first@1.1.1:
|
||||||
version "1.1.1"
|
version "1.1.1"
|
||||||
resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d"
|
resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d"
|
||||||
|
@ -1472,6 +1514,11 @@ magic-string@^0.25.7:
|
||||||
dependencies:
|
dependencies:
|
||||||
sourcemap-codec "^1.4.4"
|
sourcemap-codec "^1.4.4"
|
||||||
|
|
||||||
|
marked@^4.0.10:
|
||||||
|
version "4.0.12"
|
||||||
|
resolved "https://registry.yarnpkg.com/marked/-/marked-4.0.12.tgz#2262a4e6fd1afd2f13557726238b69a48b982f7d"
|
||||||
|
integrity sha512-hgibXWrEDNBWgGiK18j/4lkS6ihTe9sxtV4Q1OQppb/0zzyPSzoFANBa5MfsG/zgsWklmNnhm0XACZOH/0HBiQ==
|
||||||
|
|
||||||
mdn-data@2.0.14:
|
mdn-data@2.0.14:
|
||||||
version "2.0.14"
|
version "2.0.14"
|
||||||
resolved "https://registry.yarnpkg.com/mdn-data/-/mdn-data-2.0.14.tgz#7113fc4281917d63ce29b43446f701e68c25ba50"
|
resolved "https://registry.yarnpkg.com/mdn-data/-/mdn-data-2.0.14.tgz#7113fc4281917d63ce29b43446f701e68c25ba50"
|
||||||
|
@ -2490,6 +2537,11 @@ type-is@~1.6.17, type-is@~1.6.18:
|
||||||
media-typer "0.3.0"
|
media-typer "0.3.0"
|
||||||
mime-types "~2.1.24"
|
mime-types "~2.1.24"
|
||||||
|
|
||||||
|
typo-js@*:
|
||||||
|
version "1.2.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/typo-js/-/typo-js-1.2.1.tgz#334a0d8c3f6c56f2f1e15fdf6c31677793cbbe9b"
|
||||||
|
integrity sha512-bTGLjbD3WqZDR3CgEFkyi9Q/SS2oM29ipXrWfDb4M74ea69QwKAECVceYpaBu0GfdnASMg9Qfl67ttB23nePHg==
|
||||||
|
|
||||||
unbox-primitive@^1.0.0:
|
unbox-primitive@^1.0.0:
|
||||||
version "1.0.1"
|
version "1.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/unbox-primitive/-/unbox-primitive-1.0.1.tgz#085e215625ec3162574dc8859abee78a59b14471"
|
resolved "https://registry.yarnpkg.com/unbox-primitive/-/unbox-primitive-1.0.1.tgz#085e215625ec3162574dc8859abee78a59b14471"
|
||||||
|
|
|
@ -3,9 +3,6 @@ const path = require("path")
|
||||||
|
|
||||||
const tmpdir = path.join(require("os").tmpdir(), ".budibase")
|
const tmpdir = path.join(require("os").tmpdir(), ".budibase")
|
||||||
|
|
||||||
// these run on ports we don't normally use so that they can run alongside the
|
|
||||||
const fs = require("fs")
|
|
||||||
|
|
||||||
// normal development system
|
// normal development system
|
||||||
const WORKER_PORT = "10002"
|
const WORKER_PORT = "10002"
|
||||||
const MAIN_PORT = cypressConfig.env.PORT
|
const MAIN_PORT = cypressConfig.env.PORT
|
||||||
|
@ -29,22 +26,20 @@ process.env.ALLOW_DEV_AUTOMATIONS = 1
|
||||||
// Stop info logs polluting test outputs
|
// Stop info logs polluting test outputs
|
||||||
process.env.LOG_LEVEL = "error"
|
process.env.LOG_LEVEL = "error"
|
||||||
|
|
||||||
async function run() {
|
exports.run = (
|
||||||
|
serverLoc = "../../server/dist",
|
||||||
|
workerLoc = "../../worker/dist"
|
||||||
|
) => {
|
||||||
// require("dotenv").config({ path: resolve(dir, ".env") })
|
// require("dotenv").config({ path: resolve(dir, ".env") })
|
||||||
if (!fs.existsSync("../server/dist")) {
|
|
||||||
console.error("Unable to run cypress, need to build server first")
|
|
||||||
process.exit(-1)
|
|
||||||
}
|
|
||||||
|
|
||||||
// don't make this a variable or top level require
|
// don't make this a variable or top level require
|
||||||
// it will cause environment module to be loaded prematurely
|
// it will cause environment module to be loaded prematurely
|
||||||
const server = require("../../server/dist/app")
|
require(serverLoc)
|
||||||
process.env.PORT = WORKER_PORT
|
process.env.PORT = WORKER_PORT
|
||||||
const worker = require("../../worker/dist/index")
|
require(workerLoc)
|
||||||
// reload main port for rest of system
|
// reload main port for rest of system
|
||||||
process.env.PORT = MAIN_PORT
|
process.env.PORT = MAIN_PORT
|
||||||
server.on("close", () => console.log("Server Closed"))
|
|
||||||
worker.on("close", () => console.log("Worker Closed"))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
run()
|
if (require.main === module) {
|
||||||
|
exports.run()
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,4 @@
|
||||||
|
// @ts-ignore
|
||||||
|
import { run } from "../setup"
|
||||||
|
|
||||||
|
run("../../server/src/index", "../../worker/src/index")
|
|
@ -1,22 +1,21 @@
|
||||||
{
|
{
|
||||||
"name": "@budibase/builder",
|
"name": "@budibase/builder",
|
||||||
"version": "1.0.49-alpha.5",
|
"version": "1.0.50-alpha.6",
|
||||||
"license": "GPL-3.0",
|
"license": "GPL-3.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "routify -b && vite build --emptyOutDir",
|
"build": "routify -b && vite build --emptyOutDir",
|
||||||
"start": "routify -c rollup",
|
"start": "routify -c rollup",
|
||||||
"test": "jest",
|
|
||||||
"test:watch": "jest --watchAll",
|
|
||||||
"dev:builder": "routify -c dev:vite",
|
"dev:builder": "routify -c dev:vite",
|
||||||
"dev:vite": "vite --host 0.0.0.0",
|
"dev:vite": "vite --host 0.0.0.0",
|
||||||
"rollup": "rollup -c -w",
|
"rollup": "rollup -c -w",
|
||||||
"cy:setup": "node ./cypress/setup.js",
|
"cy:setup": "ts-node ./cypress/ts/setup.ts",
|
||||||
|
"cy:setup:ci": "node ./cypress/setup.js",
|
||||||
"cy:run": "cypress run",
|
"cy:run": "cypress run",
|
||||||
"cy:open": "cypress open",
|
"cy:open": "cypress open",
|
||||||
"cy:run:ci": "cypress run --record",
|
"cy:run:ci": "cypress run --record",
|
||||||
"cy:test": "start-server-and-test cy:setup http://localhost:10001/builder cy:run",
|
"cy:test": "start-server-and-test cy:setup http://localhost:10001/builder cy:run",
|
||||||
"cy:ci": "start-server-and-test cy:setup http://localhost:10001/builder cy:run",
|
"cy:ci": "start-server-and-test cy:setup:ci http://localhost:10001/builder cy:run",
|
||||||
"cy:debug": "start-server-and-test cy:setup http://localhost:10001/builder cy:open"
|
"cy:debug": "start-server-and-test cy:setup http://localhost:10001/builder cy:open"
|
||||||
},
|
},
|
||||||
"jest": {
|
"jest": {
|
||||||
|
@ -65,10 +64,10 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@budibase/bbui": "^1.0.49-alpha.5",
|
"@budibase/bbui": "^1.0.50-alpha.6",
|
||||||
"@budibase/client": "^1.0.49-alpha.5",
|
"@budibase/client": "^1.0.50-alpha.6",
|
||||||
"@budibase/colorpicker": "1.1.2",
|
"@budibase/frontend-core": "^1.0.50-alpha.6",
|
||||||
"@budibase/string-templates": "^1.0.49-alpha.5",
|
"@budibase/string-templates": "^1.0.50-alpha.6",
|
||||||
"@sentry/browser": "5.19.1",
|
"@sentry/browser": "5.19.1",
|
||||||
"@spectrum-css/page": "^3.0.1",
|
"@spectrum-css/page": "^3.0.1",
|
||||||
"@spectrum-css/vars": "^3.0.1",
|
"@spectrum-css/vars": "^3.0.1",
|
||||||
|
@ -106,6 +105,8 @@
|
||||||
"start-server-and-test": "^1.12.1",
|
"start-server-and-test": "^1.12.1",
|
||||||
"svelte": "^3.38.2",
|
"svelte": "^3.38.2",
|
||||||
"svelte-jester": "^1.3.2",
|
"svelte-jester": "^1.3.2",
|
||||||
|
"ts-node": "^10.4.0",
|
||||||
|
"typescript": "^4.5.5",
|
||||||
"vite": "^2.1.5"
|
"vite": "^2.1.5"
|
||||||
},
|
},
|
||||||
"gitHead": "115189f72a850bfb52b65ec61d932531bf327072"
|
"gitHead": "115189f72a850bfb52b65ec61d932531bf327072"
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import api from "builderStore/api"
|
import { API } from "api"
|
||||||
import PosthogClient from "./PosthogClient"
|
import PosthogClient from "./PosthogClient"
|
||||||
import IntercomClient from "./IntercomClient"
|
import IntercomClient from "./IntercomClient"
|
||||||
import SentryClient from "./SentryClient"
|
import SentryClient from "./SentryClient"
|
||||||
|
@ -17,13 +17,11 @@ class AnalyticsHub {
|
||||||
}
|
}
|
||||||
|
|
||||||
async activate() {
|
async activate() {
|
||||||
const analyticsStatus = await api.get("/api/analytics")
|
// Check analytics are enabled
|
||||||
const json = await analyticsStatus.json()
|
const analyticsStatus = await API.getAnalyticsStatus()
|
||||||
|
if (analyticsStatus.enabled) {
|
||||||
// Analytics disabled
|
this.clients.forEach(client => client.init())
|
||||||
if (!json.enabled) return
|
}
|
||||||
|
|
||||||
this.clients.forEach(client => client.init())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
identify(id, metadata) {
|
identify(id, metadata) {
|
||||||
|
|
|
@ -0,0 +1,48 @@
|
||||||
|
import {
|
||||||
|
createAPIClient,
|
||||||
|
CookieUtils,
|
||||||
|
Constants,
|
||||||
|
} from "@budibase/frontend-core"
|
||||||
|
import { store } from "./builderStore"
|
||||||
|
import { get } from "svelte/store"
|
||||||
|
import { auth } from "./stores/portal"
|
||||||
|
|
||||||
|
export const API = createAPIClient({
|
||||||
|
attachHeaders: headers => {
|
||||||
|
// Attach app ID header from store
|
||||||
|
headers["x-budibase-app-id"] = get(store).appId
|
||||||
|
|
||||||
|
// Add csrf token if authenticated
|
||||||
|
const user = get(auth).user
|
||||||
|
if (user?.csrfToken) {
|
||||||
|
headers["x-csrf-token"] = user.csrfToken
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
onError: error => {
|
||||||
|
const { url, message, status, method, handled } = error || {}
|
||||||
|
|
||||||
|
// Log all API errors to Sentry
|
||||||
|
// analytics.captureException(error)
|
||||||
|
|
||||||
|
// Log any errors that we haven't manually handled
|
||||||
|
if (!handled) {
|
||||||
|
console.error("Unhandled error from API client", error)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Log all errors to console
|
||||||
|
console.warn(`[Builder] HTTP ${status} on ${method}:${url}\n\t${message}`)
|
||||||
|
|
||||||
|
// Logout on 403's
|
||||||
|
if (status === 403) {
|
||||||
|
// Remove cookies
|
||||||
|
CookieUtils.removeCookie(Constants.Cookies.Auth)
|
||||||
|
|
||||||
|
// Reload after removing cookie, go to login
|
||||||
|
if (!url.includes("self") && !url.includes("login")) {
|
||||||
|
location.reload()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
|
@ -1,49 +0,0 @@
|
||||||
import { store } from "./index"
|
|
||||||
import { get as svelteGet } from "svelte/store"
|
|
||||||
import { removeCookie, Cookies } from "./cookies"
|
|
||||||
import { auth } from "stores/portal"
|
|
||||||
|
|
||||||
const apiCall =
|
|
||||||
method =>
|
|
||||||
async (url, body, headers = { "Content-Type": "application/json" }) => {
|
|
||||||
headers["x-budibase-app-id"] = svelteGet(store).appId
|
|
||||||
headers["x-budibase-api-version"] = "1"
|
|
||||||
|
|
||||||
// add csrf token if authenticated
|
|
||||||
const user = svelteGet(auth).user
|
|
||||||
if (user && user.csrfToken) {
|
|
||||||
headers["x-csrf-token"] = user.csrfToken
|
|
||||||
}
|
|
||||||
|
|
||||||
const json = headers["Content-Type"] === "application/json"
|
|
||||||
const resp = await fetch(url, {
|
|
||||||
method: method,
|
|
||||||
body: json ? JSON.stringify(body) : body,
|
|
||||||
headers,
|
|
||||||
})
|
|
||||||
if (resp.status === 403) {
|
|
||||||
if (url.includes("/api/templates")) {
|
|
||||||
return { json: () => [] }
|
|
||||||
}
|
|
||||||
removeCookie(Cookies.Auth)
|
|
||||||
// reload after removing cookie, go to login
|
|
||||||
if (!url.includes("self") && !url.includes("login")) {
|
|
||||||
location.reload()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return resp
|
|
||||||
}
|
|
||||||
|
|
||||||
export const post = apiCall("POST")
|
|
||||||
export const get = apiCall("GET")
|
|
||||||
export const patch = apiCall("PATCH")
|
|
||||||
export const del = apiCall("DELETE")
|
|
||||||
export const put = apiCall("PUT")
|
|
||||||
|
|
||||||
export default {
|
|
||||||
post: apiCall("POST"),
|
|
||||||
get: apiCall("GET"),
|
|
||||||
patch: apiCall("PATCH"),
|
|
||||||
delete: apiCall("DELETE"),
|
|
||||||
put: apiCall("PUT"),
|
|
||||||
}
|
|
|
@ -15,10 +15,7 @@ import {
|
||||||
encodeJSBinding,
|
encodeJSBinding,
|
||||||
} from "@budibase/string-templates"
|
} from "@budibase/string-templates"
|
||||||
import { TableNames } from "../constants"
|
import { TableNames } from "../constants"
|
||||||
import {
|
import { JSONUtils } from "@budibase/frontend-core"
|
||||||
convertJSONSchemaToTableSchema,
|
|
||||||
getJSONArrayDatasourceSchema,
|
|
||||||
} from "./jsonUtils"
|
|
||||||
import ActionDefinitions from "components/design/PropertiesPanel/PropertyControls/ButtonActionEditor/manifest.json"
|
import ActionDefinitions from "components/design/PropertiesPanel/PropertyControls/ButtonActionEditor/manifest.json"
|
||||||
|
|
||||||
// Regex to match all instances of template strings
|
// Regex to match all instances of template strings
|
||||||
|
@ -439,7 +436,7 @@ export const getSchemaForDatasource = (asset, datasource, isForm = false) => {
|
||||||
else if (type === "jsonarray") {
|
else if (type === "jsonarray") {
|
||||||
table = tables.find(table => table._id === datasource.tableId)
|
table = tables.find(table => table._id === datasource.tableId)
|
||||||
let tableSchema = table?.schema
|
let tableSchema = table?.schema
|
||||||
schema = getJSONArrayDatasourceSchema(tableSchema, datasource)
|
schema = JSONUtils.getJSONArrayDatasourceSchema(tableSchema, datasource)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Otherwise we assume we're targeting an internal table or a plus
|
// Otherwise we assume we're targeting an internal table or a plus
|
||||||
|
@ -471,9 +468,12 @@ export const getSchemaForDatasource = (asset, datasource, isForm = false) => {
|
||||||
Object.keys(schema).forEach(fieldKey => {
|
Object.keys(schema).forEach(fieldKey => {
|
||||||
const fieldSchema = schema[fieldKey]
|
const fieldSchema = schema[fieldKey]
|
||||||
if (fieldSchema?.type === "json") {
|
if (fieldSchema?.type === "json") {
|
||||||
const jsonSchema = convertJSONSchemaToTableSchema(fieldSchema, {
|
const jsonSchema = JSONUtils.convertJSONSchemaToTableSchema(
|
||||||
squashObjects: true,
|
fieldSchema,
|
||||||
})
|
{
|
||||||
|
squashObjects: true,
|
||||||
|
}
|
||||||
|
)
|
||||||
Object.keys(jsonSchema).forEach(jsonKey => {
|
Object.keys(jsonSchema).forEach(jsonKey => {
|
||||||
jsonAdditions[`${fieldKey}.${jsonKey}`] = {
|
jsonAdditions[`${fieldKey}.${jsonKey}`] = {
|
||||||
type: jsonSchema[jsonKey].type,
|
type: jsonSchema[jsonKey].type,
|
||||||
|
|
|
@ -1,16 +0,0 @@
|
||||||
import { get } from "builderStore/api"
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Fetches the definitions for component library components. This includes
|
|
||||||
* their props and other metadata from components.json.
|
|
||||||
* @param {string} appId - ID of the currently running app
|
|
||||||
*/
|
|
||||||
export const fetchComponentLibDefinitions = async appId => {
|
|
||||||
const LIB_DEFINITION_URL = `/api/${appId}/components/definitions`
|
|
||||||
try {
|
|
||||||
const libDefinitionResponse = await get(LIB_DEFINITION_URL)
|
|
||||||
return await libDefinitionResponse.json()
|
|
||||||
} catch (err) {
|
|
||||||
console.error(`Error fetching component definitions for ${appId}`, err)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,26 +1,40 @@
|
||||||
import { writable } from "svelte/store"
|
import { writable } from "svelte/store"
|
||||||
import api from "../../api"
|
import { API } from "api"
|
||||||
import Automation from "./Automation"
|
import Automation from "./Automation"
|
||||||
import { cloneDeep } from "lodash/fp"
|
import { cloneDeep } from "lodash/fp"
|
||||||
import analytics, { Events } from "analytics"
|
import analytics, { Events } from "analytics"
|
||||||
|
|
||||||
|
const initialAutomationState = {
|
||||||
|
automations: [],
|
||||||
|
blockDefinitions: {
|
||||||
|
TRIGGER: [],
|
||||||
|
ACTION: [],
|
||||||
|
},
|
||||||
|
selectedAutomation: null,
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getAutomationStore = () => {
|
||||||
|
const store = writable(initialAutomationState)
|
||||||
|
store.actions = automationActions(store)
|
||||||
|
return store
|
||||||
|
}
|
||||||
|
|
||||||
const automationActions = store => ({
|
const automationActions = store => ({
|
||||||
fetch: async () => {
|
fetch: async () => {
|
||||||
const responses = await Promise.all([
|
const responses = await Promise.all([
|
||||||
api.get(`/api/automations`),
|
API.getAutomations(),
|
||||||
api.get(`/api/automations/definitions/list`),
|
API.getAutomationDefinitions(),
|
||||||
])
|
])
|
||||||
const jsonResponses = await Promise.all(responses.map(x => x.json()))
|
|
||||||
store.update(state => {
|
store.update(state => {
|
||||||
let selected = state.selectedAutomation?.automation
|
let selected = state.selectedAutomation?.automation
|
||||||
state.automations = jsonResponses[0]
|
state.automations = responses[0]
|
||||||
state.blockDefinitions = {
|
state.blockDefinitions = {
|
||||||
TRIGGER: jsonResponses[1].trigger,
|
TRIGGER: responses[1].trigger,
|
||||||
ACTION: jsonResponses[1].action,
|
ACTION: responses[1].action,
|
||||||
}
|
}
|
||||||
// if previously selected find the new obj and select it
|
// If previously selected find the new obj and select it
|
||||||
if (selected) {
|
if (selected) {
|
||||||
selected = jsonResponses[0].filter(
|
selected = responses[0].filter(
|
||||||
automation => automation._id === selected._id
|
automation => automation._id === selected._id
|
||||||
)
|
)
|
||||||
state.selectedAutomation = new Automation(selected[0])
|
state.selectedAutomation = new Automation(selected[0])
|
||||||
|
@ -36,40 +50,36 @@ const automationActions = store => ({
|
||||||
steps: [],
|
steps: [],
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
const CREATE_AUTOMATION_URL = `/api/automations`
|
const response = await API.createAutomation(automation)
|
||||||
const response = await api.post(CREATE_AUTOMATION_URL, automation)
|
|
||||||
const json = await response.json()
|
|
||||||
store.update(state => {
|
store.update(state => {
|
||||||
state.automations = [...state.automations, json.automation]
|
state.automations = [...state.automations, response.automation]
|
||||||
store.actions.select(json.automation)
|
store.actions.select(response.automation)
|
||||||
return state
|
return state
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
save: async automation => {
|
save: async automation => {
|
||||||
const UPDATE_AUTOMATION_URL = `/api/automations`
|
const response = await API.updateAutomation(automation)
|
||||||
const response = await api.put(UPDATE_AUTOMATION_URL, automation)
|
|
||||||
const json = await response.json()
|
|
||||||
store.update(state => {
|
store.update(state => {
|
||||||
const newAutomation = json.automation
|
const updatedAutomation = response.automation
|
||||||
const existingIdx = state.automations.findIndex(
|
const existingIdx = state.automations.findIndex(
|
||||||
existing => existing._id === automation._id
|
existing => existing._id === automation._id
|
||||||
)
|
)
|
||||||
if (existingIdx !== -1) {
|
if (existingIdx !== -1) {
|
||||||
state.automations.splice(existingIdx, 1, newAutomation)
|
state.automations.splice(existingIdx, 1, updatedAutomation)
|
||||||
state.automations = [...state.automations]
|
state.automations = [...state.automations]
|
||||||
store.actions.select(newAutomation)
|
store.actions.select(updatedAutomation)
|
||||||
return state
|
return state
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
delete: async automation => {
|
delete: async automation => {
|
||||||
const { _id, _rev } = automation
|
await API.deleteAutomation({
|
||||||
const DELETE_AUTOMATION_URL = `/api/automations/${_id}/${_rev}`
|
automationId: automation?._id,
|
||||||
await api.delete(DELETE_AUTOMATION_URL)
|
automationRev: automation?._rev,
|
||||||
|
})
|
||||||
store.update(state => {
|
store.update(state => {
|
||||||
const existingIdx = state.automations.findIndex(
|
const existingIdx = state.automations.findIndex(
|
||||||
existing => existing._id === _id
|
existing => existing._id === automation?._id
|
||||||
)
|
)
|
||||||
state.automations.splice(existingIdx, 1)
|
state.automations.splice(existingIdx, 1)
|
||||||
state.automations = [...state.automations]
|
state.automations = [...state.automations]
|
||||||
|
@ -78,16 +88,17 @@ const automationActions = store => ({
|
||||||
return state
|
return state
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
trigger: async automation => {
|
|
||||||
const { _id } = automation
|
|
||||||
return await api.post(`/api/automations/${_id}/trigger`)
|
|
||||||
},
|
|
||||||
test: async (automation, testData) => {
|
test: async (automation, testData) => {
|
||||||
const { _id } = automation
|
|
||||||
const response = await api.post(`/api/automations/${_id}/test`, testData)
|
|
||||||
const json = await response.json()
|
|
||||||
store.update(state => {
|
store.update(state => {
|
||||||
state.selectedAutomation.testResults = json
|
state.selectedAutomation.testResults = null
|
||||||
|
return state
|
||||||
|
})
|
||||||
|
const result = await API.testAutomation({
|
||||||
|
automationId: automation?._id,
|
||||||
|
testData,
|
||||||
|
})
|
||||||
|
store.update(state => {
|
||||||
|
state.selectedAutomation.testResults = result
|
||||||
return state
|
return state
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
@ -143,17 +154,3 @@ const automationActions = store => ({
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
export const getAutomationStore = () => {
|
|
||||||
const INITIAL_AUTOMATION_STATE = {
|
|
||||||
automations: [],
|
|
||||||
blockDefinitions: {
|
|
||||||
TRIGGER: [],
|
|
||||||
ACTION: [],
|
|
||||||
},
|
|
||||||
selectedAutomation: null,
|
|
||||||
}
|
|
||||||
const store = writable(INITIAL_AUTOMATION_STATE)
|
|
||||||
store.actions = automationActions(store)
|
|
||||||
return store
|
|
||||||
}
|
|
||||||
|
|
|
@ -14,8 +14,7 @@ import {
|
||||||
database,
|
database,
|
||||||
tables,
|
tables,
|
||||||
} from "stores/backend"
|
} from "stores/backend"
|
||||||
import { fetchComponentLibDefinitions } from "../loadComponentLibraries"
|
import { API } from "api"
|
||||||
import api from "../api"
|
|
||||||
import { FrontendTypes } from "constants"
|
import { FrontendTypes } from "constants"
|
||||||
import analytics, { Events } from "analytics"
|
import analytics, { Events } from "analytics"
|
||||||
import {
|
import {
|
||||||
|
@ -26,7 +25,7 @@ import {
|
||||||
findComponent,
|
findComponent,
|
||||||
getComponentSettings,
|
getComponentSettings,
|
||||||
} from "../componentUtils"
|
} from "../componentUtils"
|
||||||
import { uuid } from "../uuid"
|
import { Helpers } from "@budibase/bbui"
|
||||||
import { removeBindings } from "../dataBinding"
|
import { removeBindings } from "../dataBinding"
|
||||||
|
|
||||||
const INITIAL_FRONTEND_STATE = {
|
const INITIAL_FRONTEND_STATE = {
|
||||||
|
@ -65,17 +64,17 @@ export const getFrontendStore = () => {
|
||||||
const store = writable({ ...INITIAL_FRONTEND_STATE })
|
const store = writable({ ...INITIAL_FRONTEND_STATE })
|
||||||
|
|
||||||
store.actions = {
|
store.actions = {
|
||||||
|
reset: () => {
|
||||||
|
store.set({ ...INITIAL_FRONTEND_STATE })
|
||||||
|
},
|
||||||
initialise: async pkg => {
|
initialise: async pkg => {
|
||||||
const { layouts, screens, application, clientLibPath } = pkg
|
const { layouts, screens, application, clientLibPath } = pkg
|
||||||
const components = await fetchComponentLibDefinitions(application.appId)
|
|
||||||
// make sure app isn't locked
|
// Fetch component definitions.
|
||||||
if (
|
// Allow errors to propagate.
|
||||||
components &&
|
let components = await API.fetchComponentLibDefinitions(application.appId)
|
||||||
components.status === 400 &&
|
|
||||||
components.message?.includes("lock")
|
// Reset store state
|
||||||
) {
|
|
||||||
throw { ok: false, reason: "locked" }
|
|
||||||
}
|
|
||||||
store.update(state => ({
|
store.update(state => ({
|
||||||
...state,
|
...state,
|
||||||
libraries: application.componentLibraries,
|
libraries: application.componentLibraries,
|
||||||
|
@ -88,8 +87,8 @@ export const getFrontendStore = () => {
|
||||||
description: application.description,
|
description: application.description,
|
||||||
appId: application.appId,
|
appId: application.appId,
|
||||||
url: application.url,
|
url: application.url,
|
||||||
layouts,
|
layouts: layouts || [],
|
||||||
screens,
|
screens: screens || [],
|
||||||
theme: application.theme || "spectrum--light",
|
theme: application.theme || "spectrum--light",
|
||||||
customTheme: application.customTheme,
|
customTheme: application.customTheme,
|
||||||
hasAppPackage: true,
|
hasAppPackage: true,
|
||||||
|
@ -101,51 +100,43 @@ export const getFrontendStore = () => {
|
||||||
}))
|
}))
|
||||||
|
|
||||||
// Initialise backend stores
|
// Initialise backend stores
|
||||||
const [_integrations] = await Promise.all([
|
|
||||||
api.get("/api/integrations").then(r => r.json()),
|
|
||||||
])
|
|
||||||
datasources.init()
|
|
||||||
integrations.set(_integrations)
|
|
||||||
queries.init()
|
|
||||||
database.set(application.instance)
|
database.set(application.instance)
|
||||||
tables.init()
|
await datasources.init()
|
||||||
|
await integrations.init()
|
||||||
|
await queries.init()
|
||||||
|
await tables.init()
|
||||||
},
|
},
|
||||||
theme: {
|
theme: {
|
||||||
save: async theme => {
|
save: async theme => {
|
||||||
const appId = get(store).appId
|
const appId = get(store).appId
|
||||||
const response = await api.put(`/api/applications/${appId}`, { theme })
|
await API.saveAppMetadata({
|
||||||
if (response.status === 200) {
|
appId,
|
||||||
store.update(state => {
|
metadata: { theme },
|
||||||
state.theme = theme
|
})
|
||||||
return state
|
store.update(state => {
|
||||||
})
|
state.theme = theme
|
||||||
} else {
|
return state
|
||||||
throw new Error("Error updating theme")
|
})
|
||||||
}
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
customTheme: {
|
customTheme: {
|
||||||
save: async customTheme => {
|
save: async customTheme => {
|
||||||
const appId = get(store).appId
|
const appId = get(store).appId
|
||||||
const response = await api.put(`/api/applications/${appId}`, {
|
await API.saveAppMetadata({
|
||||||
customTheme,
|
appId,
|
||||||
|
metadata: { customTheme },
|
||||||
|
})
|
||||||
|
store.update(state => {
|
||||||
|
state.customTheme = customTheme
|
||||||
|
return state
|
||||||
})
|
})
|
||||||
if (response.status === 200) {
|
|
||||||
store.update(state => {
|
|
||||||
state.customTheme = customTheme
|
|
||||||
return state
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
throw new Error("Error updating theme")
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
routing: {
|
routing: {
|
||||||
fetch: async () => {
|
fetch: async () => {
|
||||||
const response = await api.get("/api/routing")
|
const response = await API.fetchAppRoutes()
|
||||||
const json = await response.json()
|
|
||||||
store.update(state => {
|
store.update(state => {
|
||||||
state.routes = json.routes
|
state.routes = response.routes
|
||||||
return state
|
return state
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
@ -169,82 +160,76 @@ export const getFrontendStore = () => {
|
||||||
return state
|
return state
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
create: async screen => {
|
|
||||||
screen = await store.actions.screens.save(screen)
|
|
||||||
store.update(state => {
|
|
||||||
state.selectedScreenId = screen._id
|
|
||||||
state.selectedComponentId = screen.props._id
|
|
||||||
state.currentFrontEndType = FrontendTypes.SCREEN
|
|
||||||
selectedAccessRole.set(screen.routing.roleId)
|
|
||||||
return state
|
|
||||||
})
|
|
||||||
return screen
|
|
||||||
},
|
|
||||||
save: async screen => {
|
save: async screen => {
|
||||||
const creatingNewScreen = screen._id === undefined
|
const creatingNewScreen = screen._id === undefined
|
||||||
const response = await api.post(`/api/screens`, screen)
|
const savedScreen = await API.saveScreen(screen)
|
||||||
if (response.status !== 200) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
screen = await response.json()
|
|
||||||
await store.actions.routing.fetch()
|
|
||||||
|
|
||||||
store.update(state => {
|
store.update(state => {
|
||||||
const foundScreen = state.screens.findIndex(
|
const idx = state.screens.findIndex(x => x._id === savedScreen._id)
|
||||||
el => el._id === screen._id
|
if (idx !== -1) {
|
||||||
)
|
state.screens.splice(idx, 1, savedScreen)
|
||||||
if (foundScreen !== -1) {
|
} else {
|
||||||
state.screens.splice(foundScreen, 1)
|
state.screens.push(savedScreen)
|
||||||
}
|
}
|
||||||
state.screens.push(screen)
|
|
||||||
return state
|
return state
|
||||||
})
|
})
|
||||||
|
|
||||||
if (creatingNewScreen) {
|
// Refresh routes
|
||||||
store.actions.screens.select(screen._id)
|
await store.actions.routing.fetch()
|
||||||
}
|
|
||||||
|
|
||||||
return screen
|
// Select the new screen if creating a new one
|
||||||
|
if (creatingNewScreen) {
|
||||||
|
store.actions.screens.select(savedScreen._id)
|
||||||
|
}
|
||||||
|
return savedScreen
|
||||||
},
|
},
|
||||||
delete: async screens => {
|
delete: async screens => {
|
||||||
const screensToDelete = Array.isArray(screens) ? screens : [screens]
|
const screensToDelete = Array.isArray(screens) ? screens : [screens]
|
||||||
|
|
||||||
const screenDeletePromises = []
|
// Build array of promises to speed up bulk deletions
|
||||||
|
const promises = []
|
||||||
|
screensToDelete.forEach(screen => {
|
||||||
|
// Delete the screen
|
||||||
|
promises.push(
|
||||||
|
API.deleteScreen({
|
||||||
|
screenId: screen._id,
|
||||||
|
screenRev: screen._rev,
|
||||||
|
})
|
||||||
|
)
|
||||||
|
// Remove links to this screen
|
||||||
|
promises.push(
|
||||||
|
store.actions.components.links.delete(
|
||||||
|
screen.routing.route,
|
||||||
|
screen.props._instanceName
|
||||||
|
)
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
await Promise.all(promises)
|
||||||
|
const deletedIds = screensToDelete.map(screen => screen._id)
|
||||||
store.update(state => {
|
store.update(state => {
|
||||||
for (let screenToDelete of screensToDelete) {
|
// Remove deleted screens from state
|
||||||
state.screens = state.screens.filter(
|
state.screens = state.screens.filter(screen => {
|
||||||
screen => screen._id !== screenToDelete._id
|
return !deletedIds.includes(screen._id)
|
||||||
)
|
})
|
||||||
screenDeletePromises.push(
|
// Deselect the current screen if it was deleted
|
||||||
api.delete(
|
if (deletedIds.includes(state.selectedScreenId)) {
|
||||||
`/api/screens/${screenToDelete._id}/${screenToDelete._rev}`
|
state.selectedScreenId = null
|
||||||
)
|
|
||||||
)
|
|
||||||
if (screenToDelete._id === state.selectedScreenId) {
|
|
||||||
state.selectedScreenId = null
|
|
||||||
}
|
|
||||||
//remove the link for this screen
|
|
||||||
screenDeletePromises.push(
|
|
||||||
store.actions.components.links.delete(
|
|
||||||
screenToDelete.routing.route,
|
|
||||||
screenToDelete.props._instanceName
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
return state
|
return state
|
||||||
})
|
})
|
||||||
await Promise.all(screenDeletePromises)
|
|
||||||
|
// Refresh routes
|
||||||
|
await store.actions.routing.fetch()
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
preview: {
|
preview: {
|
||||||
saveSelected: async () => {
|
saveSelected: async () => {
|
||||||
const state = get(store)
|
const state = get(store)
|
||||||
const selectedAsset = get(currentAsset)
|
const selectedAsset = get(currentAsset)
|
||||||
|
|
||||||
if (state.currentFrontEndType !== FrontendTypes.LAYOUT) {
|
if (state.currentFrontEndType !== FrontendTypes.LAYOUT) {
|
||||||
await store.actions.screens.save(selectedAsset)
|
return await store.actions.screens.save(selectedAsset)
|
||||||
} else {
|
} else {
|
||||||
await store.actions.layouts.save(selectedAsset)
|
return await store.actions.layouts.save(selectedAsset)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
setDevice: device => {
|
setDevice: device => {
|
||||||
|
@ -268,25 +253,13 @@ export const getFrontendStore = () => {
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
save: async layout => {
|
save: async layout => {
|
||||||
const layoutToSave = cloneDeep(layout)
|
const creatingNewLayout = layout._id === undefined
|
||||||
const creatingNewLayout = layoutToSave._id === undefined
|
const savedLayout = await API.saveLayout(layout)
|
||||||
const response = await api.post(`/api/layouts`, layoutToSave)
|
|
||||||
const savedLayout = await response.json()
|
|
||||||
|
|
||||||
// Abort if saving failed
|
|
||||||
if (response.status !== 200) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
store.update(state => {
|
store.update(state => {
|
||||||
const layoutIdx = state.layouts.findIndex(
|
const idx = state.layouts.findIndex(x => x._id === savedLayout._id)
|
||||||
stateLayout => stateLayout._id === savedLayout._id
|
if (idx !== -1) {
|
||||||
)
|
state.layouts.splice(idx, 1, savedLayout)
|
||||||
if (layoutIdx >= 0) {
|
|
||||||
// update existing layout
|
|
||||||
state.layouts.splice(layoutIdx, 1, savedLayout)
|
|
||||||
} else {
|
} else {
|
||||||
// save new layout
|
|
||||||
state.layouts.push(savedLayout)
|
state.layouts.push(savedLayout)
|
||||||
}
|
}
|
||||||
return state
|
return state
|
||||||
|
@ -296,7 +269,6 @@ export const getFrontendStore = () => {
|
||||||
if (creatingNewLayout) {
|
if (creatingNewLayout) {
|
||||||
store.actions.layouts.select(savedLayout._id)
|
store.actions.layouts.select(savedLayout._id)
|
||||||
}
|
}
|
||||||
|
|
||||||
return savedLayout
|
return savedLayout
|
||||||
},
|
},
|
||||||
find: layoutId => {
|
find: layoutId => {
|
||||||
|
@ -306,21 +278,20 @@ export const getFrontendStore = () => {
|
||||||
const storeContents = get(store)
|
const storeContents = get(store)
|
||||||
return storeContents.layouts.find(layout => layout._id === layoutId)
|
return storeContents.layouts.find(layout => layout._id === layoutId)
|
||||||
},
|
},
|
||||||
delete: async layoutToDelete => {
|
delete: async layout => {
|
||||||
const response = await api.delete(
|
if (!layout?._id) {
|
||||||
`/api/layouts/${layoutToDelete._id}/${layoutToDelete._rev}`
|
return
|
||||||
)
|
|
||||||
if (response.status !== 200) {
|
|
||||||
const json = await response.json()
|
|
||||||
throw new Error(json.message)
|
|
||||||
}
|
}
|
||||||
|
await API.deleteLayout({
|
||||||
|
layoutId: layout._id,
|
||||||
|
layoutRev: layout._rev,
|
||||||
|
})
|
||||||
store.update(state => {
|
store.update(state => {
|
||||||
state.layouts = state.layouts.filter(
|
// Select main layout if we deleted the selected layout
|
||||||
layout => layout._id !== layoutToDelete._id
|
if (layout._id === state.selectedLayoutId) {
|
||||||
)
|
|
||||||
if (layoutToDelete._id === state.selectedLayoutId) {
|
|
||||||
state.selectedLayoutId = get(mainLayout)._id
|
state.selectedLayoutId = get(mainLayout)._id
|
||||||
}
|
}
|
||||||
|
state.layouts = state.layouts.filter(x => x._id !== layout._id)
|
||||||
return state
|
return state
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
@ -395,7 +366,7 @@ export const getFrontendStore = () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
_id: uuid(),
|
_id: Helpers.uuid(),
|
||||||
_component: definition.component,
|
_component: definition.component,
|
||||||
_styles: { normal: {}, hover: {}, active: {} },
|
_styles: { normal: {}, hover: {}, active: {} },
|
||||||
_instanceName: `New ${definition.name}`,
|
_instanceName: `New ${definition.name}`,
|
||||||
|
@ -412,16 +383,12 @@ export const getFrontendStore = () => {
|
||||||
componentName,
|
componentName,
|
||||||
presetProps
|
presetProps
|
||||||
)
|
)
|
||||||
if (!componentInstance) {
|
if (!componentInstance || !asset) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Find parent node to attach this component to
|
// Find parent node to attach this component to
|
||||||
let parentComponent
|
let parentComponent
|
||||||
|
|
||||||
if (!asset) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (selected) {
|
if (selected) {
|
||||||
// Use current screen or layout as parent if no component is selected
|
// Use current screen or layout as parent if no component is selected
|
||||||
const definition = store.actions.components.getDefinition(
|
const definition = store.actions.components.getDefinition(
|
||||||
|
@ -549,7 +516,7 @@ export const getFrontendStore = () => {
|
||||||
if (!component) {
|
if (!component) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
component._id = uuid()
|
component._id = Helpers.uuid()
|
||||||
component._children?.forEach(randomizeIds)
|
component._children?.forEach(randomizeIds)
|
||||||
}
|
}
|
||||||
randomizeIds(componentToPaste)
|
randomizeIds(componentToPaste)
|
||||||
|
@ -603,11 +570,6 @@ export const getFrontendStore = () => {
|
||||||
selected._styles.custom = style
|
selected._styles.custom = style
|
||||||
await store.actions.preview.saveSelected()
|
await store.actions.preview.saveSelected()
|
||||||
},
|
},
|
||||||
resetStyles: async () => {
|
|
||||||
const selected = get(selectedComponent)
|
|
||||||
selected._styles = { normal: {}, hover: {}, active: {} }
|
|
||||||
await store.actions.preview.saveSelected()
|
|
||||||
},
|
|
||||||
updateConditions: async conditions => {
|
updateConditions: async conditions => {
|
||||||
const selected = get(selectedComponent)
|
const selected = get(selectedComponent)
|
||||||
selected._conditions = conditions
|
selected._conditions = conditions
|
||||||
|
@ -662,7 +624,7 @@ export const getFrontendStore = () => {
|
||||||
newLink = cloneDeep(nav._children[0])
|
newLink = cloneDeep(nav._children[0])
|
||||||
|
|
||||||
// Set our new props
|
// Set our new props
|
||||||
newLink._id = uuid()
|
newLink._id = Helpers.uuid()
|
||||||
newLink._instanceName = `${title} Link`
|
newLink._instanceName = `${title} Link`
|
||||||
newLink.url = url
|
newLink.url = url
|
||||||
newLink.text = title
|
newLink.text = title
|
||||||
|
|
|
@ -1,23 +0,0 @@
|
||||||
import { writable } from "svelte/store"
|
|
||||||
import { generate } from "shortid"
|
|
||||||
|
|
||||||
export const notificationStore = writable({
|
|
||||||
notifications: [],
|
|
||||||
})
|
|
||||||
|
|
||||||
export function send(message, type = "default") {
|
|
||||||
notificationStore.update(state => {
|
|
||||||
state.notifications = [
|
|
||||||
...state.notifications,
|
|
||||||
{ id: generate(), type, message },
|
|
||||||
]
|
|
||||||
return state
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
export const notifier = {
|
|
||||||
danger: msg => send(msg, "danger"),
|
|
||||||
warning: msg => send(msg, "warning"),
|
|
||||||
info: msg => send(msg, "info"),
|
|
||||||
success: msg => send(msg, "success"),
|
|
||||||
}
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { uuid } from "builderStore/uuid"
|
import { Helpers } from "@budibase/bbui"
|
||||||
import { BaseStructure } from "./BaseStructure"
|
import { BaseStructure } from "./BaseStructure"
|
||||||
|
|
||||||
export class Component extends BaseStructure {
|
export class Component extends BaseStructure {
|
||||||
|
@ -6,7 +6,7 @@ export class Component extends BaseStructure {
|
||||||
super(false)
|
super(false)
|
||||||
this._children = []
|
this._children = []
|
||||||
this._json = {
|
this._json = {
|
||||||
_id: uuid(),
|
_id: Helpers.uuid(),
|
||||||
_component: name,
|
_component: name,
|
||||||
_styles: {
|
_styles: {
|
||||||
normal: {},
|
normal: {},
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { BaseStructure } from "./BaseStructure"
|
import { BaseStructure } from "./BaseStructure"
|
||||||
import { uuid } from "builderStore/uuid"
|
import { Helpers } from "@budibase/bbui"
|
||||||
|
|
||||||
export class Screen extends BaseStructure {
|
export class Screen extends BaseStructure {
|
||||||
constructor() {
|
constructor() {
|
||||||
|
@ -7,7 +7,7 @@ export class Screen extends BaseStructure {
|
||||||
this._json = {
|
this._json = {
|
||||||
layoutId: "layout_private_master",
|
layoutId: "layout_private_master",
|
||||||
props: {
|
props: {
|
||||||
_id: uuid(),
|
_id: Helpers.uuid(),
|
||||||
_component: "@budibase/standard-components/container",
|
_component: "@budibase/standard-components/container",
|
||||||
_styles: {
|
_styles: {
|
||||||
normal: {},
|
normal: {},
|
||||||
|
|
|
@ -169,6 +169,11 @@ export function makeDatasourceFormComponents(datasource) {
|
||||||
optionsSource: "schema",
|
optionsSource: "schema",
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
if (fieldType === "longform") {
|
||||||
|
component.customProps({
|
||||||
|
format: "auto",
|
||||||
|
})
|
||||||
|
}
|
||||||
if (fieldType === "array") {
|
if (fieldType === "array") {
|
||||||
component.customProps({
|
component.customProps({
|
||||||
placeholder: "Choose an option",
|
placeholder: "Choose an option",
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { localStorageStore } from "./localStorage"
|
import { createLocalStorageStore } from "@budibase/frontend-core"
|
||||||
|
|
||||||
export const getThemeStore = () => {
|
export const getThemeStore = () => {
|
||||||
const themeElement = document.documentElement
|
const themeElement = document.documentElement
|
||||||
|
@ -6,7 +6,7 @@ export const getThemeStore = () => {
|
||||||
theme: "darkest",
|
theme: "darkest",
|
||||||
options: ["lightest", "light", "dark", "darkest"],
|
options: ["lightest", "light", "dark", "darkest"],
|
||||||
}
|
}
|
||||||
const store = localStorageStore("bb-theme", initialValue)
|
const store = createLocalStorageStore("bb-theme", initialValue)
|
||||||
|
|
||||||
// Update theme class when store changes
|
// Update theme class when store changes
|
||||||
store.subscribe(state => {
|
store.subscribe(state => {
|
||||||
|
|
|
@ -1,9 +0,0 @@
|
||||||
export function uuid() {
|
|
||||||
// always want to make this start with a letter, as this makes it
|
|
||||||
// easier to use with template string bindings in the client
|
|
||||||
return "cxxxxxxxxxxxx4xxxyxxxxxxxxxxxxxxx".replace(/[xy]/g, c => {
|
|
||||||
const r = (Math.random() * 16) | 0,
|
|
||||||
v = c == "x" ? r : (r & 0x3) | 0x8
|
|
||||||
return v.toString(16)
|
|
||||||
})
|
|
||||||
}
|
|
|
@ -6,6 +6,7 @@
|
||||||
Body,
|
Body,
|
||||||
Icon,
|
Icon,
|
||||||
Tooltip,
|
Tooltip,
|
||||||
|
notifications,
|
||||||
} from "@budibase/bbui"
|
} from "@budibase/bbui"
|
||||||
import { automationStore } from "builderStore"
|
import { automationStore } from "builderStore"
|
||||||
import { admin } from "stores/portal"
|
import { admin } from "stores/portal"
|
||||||
|
@ -47,15 +48,19 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
async function addBlockToAutomation() {
|
async function addBlockToAutomation() {
|
||||||
const newBlock = $automationStore.selectedAutomation.constructBlock(
|
try {
|
||||||
"ACTION",
|
const newBlock = $automationStore.selectedAutomation.constructBlock(
|
||||||
actionVal.stepId,
|
"ACTION",
|
||||||
actionVal
|
actionVal.stepId,
|
||||||
)
|
actionVal
|
||||||
automationStore.actions.addBlockToAutomation(newBlock, blockIdx + 1)
|
)
|
||||||
await automationStore.actions.save(
|
automationStore.actions.addBlockToAutomation(newBlock, blockIdx + 1)
|
||||||
$automationStore.selectedAutomation?.automation
|
await automationStore.actions.save(
|
||||||
)
|
$automationStore.selectedAutomation?.automation
|
||||||
|
)
|
||||||
|
} catch (error) {
|
||||||
|
notifications.error("Error saving automation")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
|
@ -30,26 +30,13 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
async function deleteAutomation() {
|
async function deleteAutomation() {
|
||||||
await automationStore.actions.delete(
|
try {
|
||||||
$automationStore.selectedAutomation?.automation
|
await automationStore.actions.delete(
|
||||||
)
|
$automationStore.selectedAutomation?.automation
|
||||||
notifications.success("Automation deleted.")
|
|
||||||
}
|
|
||||||
|
|
||||||
async function testAutomation() {
|
|
||||||
const result = await automationStore.actions.trigger(
|
|
||||||
$automationStore.selectedAutomation.automation
|
|
||||||
)
|
|
||||||
if (result.status === 200) {
|
|
||||||
notifications.success(
|
|
||||||
`Automation ${$automationStore.selectedAutomation.automation.name} triggered successfully.`
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
notifications.error(
|
|
||||||
`Failed to trigger automation ${$automationStore.selectedAutomation.automation.name}.`
|
|
||||||
)
|
)
|
||||||
|
} catch (error) {
|
||||||
|
notifications.error("Error deleting automation")
|
||||||
}
|
}
|
||||||
return result
|
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
@ -85,7 +72,7 @@
|
||||||
animate:flip={{ duration: 500 }}
|
animate:flip={{ duration: 500 }}
|
||||||
in:fly|local={{ x: 500, duration: 1500 }}
|
in:fly|local={{ x: 500, duration: 1500 }}
|
||||||
>
|
>
|
||||||
<FlowItem {testDataModal} {testAutomation} {onSelect} {block} />
|
<FlowItem {testDataModal} {onSelect} {block} />
|
||||||
</div>
|
</div>
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
|
@ -101,7 +88,7 @@
|
||||||
</ConfirmDialog>
|
</ConfirmDialog>
|
||||||
|
|
||||||
<Modal bind:this={testDataModal} width="30%">
|
<Modal bind:this={testDataModal} width="30%">
|
||||||
<TestDataModal {testAutomation} />
|
<TestDataModal />
|
||||||
</Modal>
|
</Modal>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
@ -10,6 +10,7 @@
|
||||||
Button,
|
Button,
|
||||||
StatusLight,
|
StatusLight,
|
||||||
ActionButton,
|
ActionButton,
|
||||||
|
notifications,
|
||||||
} from "@budibase/bbui"
|
} from "@budibase/bbui"
|
||||||
import AutomationBlockSetup from "../../SetupPanel/AutomationBlockSetup.svelte"
|
import AutomationBlockSetup from "../../SetupPanel/AutomationBlockSetup.svelte"
|
||||||
import CreateWebhookModal from "components/automation/Shared/CreateWebhookModal.svelte"
|
import CreateWebhookModal from "components/automation/Shared/CreateWebhookModal.svelte"
|
||||||
|
@ -54,10 +55,14 @@
|
||||||
).every(x => block?.inputs[x])
|
).every(x => block?.inputs[x])
|
||||||
|
|
||||||
async function deleteStep() {
|
async function deleteStep() {
|
||||||
automationStore.actions.deleteAutomationBlock(block)
|
try {
|
||||||
await automationStore.actions.save(
|
automationStore.actions.deleteAutomationBlock(block)
|
||||||
$automationStore.selectedAutomation?.automation
|
await automationStore.actions.save(
|
||||||
)
|
$automationStore.selectedAutomation?.automation
|
||||||
|
)
|
||||||
|
} catch (error) {
|
||||||
|
notifications.error("Error saving notification")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,12 @@
|
||||||
<script>
|
<script>
|
||||||
import { ModalContent, Tabs, Tab, TextArea, Label } from "@budibase/bbui"
|
import {
|
||||||
|
ModalContent,
|
||||||
|
Tabs,
|
||||||
|
Tab,
|
||||||
|
TextArea,
|
||||||
|
Label,
|
||||||
|
notifications,
|
||||||
|
} from "@budibase/bbui"
|
||||||
import { automationStore } from "builderStore"
|
import { automationStore } from "builderStore"
|
||||||
import AutomationBlockSetup from "../../SetupPanel/AutomationBlockSetup.svelte"
|
import AutomationBlockSetup from "../../SetupPanel/AutomationBlockSetup.svelte"
|
||||||
import { cloneDeep } from "lodash/fp"
|
import { cloneDeep } from "lodash/fp"
|
||||||
|
@ -37,6 +44,17 @@
|
||||||
failedParse = "Invalid JSON"
|
failedParse = "Invalid JSON"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const testAutomation = async () => {
|
||||||
|
try {
|
||||||
|
await automationStore.actions.test(
|
||||||
|
$automationStore.selectedAutomation?.automation,
|
||||||
|
testData
|
||||||
|
)
|
||||||
|
} catch (error) {
|
||||||
|
notifications.error("Error testing notification")
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<ModalContent
|
<ModalContent
|
||||||
|
@ -44,12 +62,7 @@
|
||||||
confirmText="Test"
|
confirmText="Test"
|
||||||
showConfirmButton={true}
|
showConfirmButton={true}
|
||||||
disabled={isError}
|
disabled={isError}
|
||||||
onConfirm={() => {
|
onConfirm={testAutomation}
|
||||||
automationStore.actions.test(
|
|
||||||
$automationStore.selectedAutomation?.automation,
|
|
||||||
testData
|
|
||||||
)
|
|
||||||
}}
|
|
||||||
cancelText="Cancel"
|
cancelText="Cancel"
|
||||||
>
|
>
|
||||||
<Tabs selected="Form" quiet
|
<Tabs selected="Form" quiet
|
||||||
|
|
|
@ -4,10 +4,16 @@
|
||||||
import { automationStore } from "builderStore"
|
import { automationStore } from "builderStore"
|
||||||
import NavItem from "components/common/NavItem.svelte"
|
import NavItem from "components/common/NavItem.svelte"
|
||||||
import EditAutomationPopover from "./EditAutomationPopover.svelte"
|
import EditAutomationPopover from "./EditAutomationPopover.svelte"
|
||||||
|
import { notifications } from "@budibase/bbui"
|
||||||
|
|
||||||
$: selectedAutomationId = $automationStore.selectedAutomation?.automation?._id
|
$: selectedAutomationId = $automationStore.selectedAutomation?.automation?._id
|
||||||
onMount(() => {
|
|
||||||
automationStore.actions.fetch()
|
onMount(async () => {
|
||||||
|
try {
|
||||||
|
await automationStore.actions.fetch()
|
||||||
|
} catch (error) {
|
||||||
|
notifications.error("Error getting automations list")
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
function selectAutomation(automation) {
|
function selectAutomation(automation) {
|
||||||
|
|
|
@ -24,29 +24,33 @@
|
||||||
nameTouched && !name ? "Please specify a name for the automation." : null
|
nameTouched && !name ? "Please specify a name for the automation." : null
|
||||||
|
|
||||||
async function createAutomation() {
|
async function createAutomation() {
|
||||||
await automationStore.actions.create({
|
try {
|
||||||
name,
|
await automationStore.actions.create({
|
||||||
instanceId,
|
name,
|
||||||
})
|
instanceId,
|
||||||
const newBlock = $automationStore.selectedAutomation.constructBlock(
|
})
|
||||||
"TRIGGER",
|
const newBlock = $automationStore.selectedAutomation.constructBlock(
|
||||||
triggerVal.stepId,
|
"TRIGGER",
|
||||||
triggerVal
|
triggerVal.stepId,
|
||||||
)
|
triggerVal
|
||||||
|
)
|
||||||
|
|
||||||
automationStore.actions.addBlockToAutomation(newBlock)
|
automationStore.actions.addBlockToAutomation(newBlock)
|
||||||
if (triggerVal.stepId === "WEBHOOK") {
|
if (triggerVal.stepId === "WEBHOOK") {
|
||||||
webhookModal.show
|
webhookModal.show
|
||||||
|
}
|
||||||
|
|
||||||
|
await automationStore.actions.save(
|
||||||
|
$automationStore.selectedAutomation?.automation
|
||||||
|
)
|
||||||
|
|
||||||
|
notifications.success(`Automation ${name} created`)
|
||||||
|
|
||||||
|
$goto(`./${$automationStore.selectedAutomation.automation._id}`)
|
||||||
|
analytics.captureEvent(Events.AUTOMATION.CREATED, { name })
|
||||||
|
} catch (error) {
|
||||||
|
notifications.error("Error creating automation")
|
||||||
}
|
}
|
||||||
|
|
||||||
await automationStore.actions.save(
|
|
||||||
$automationStore.selectedAutomation?.automation
|
|
||||||
)
|
|
||||||
|
|
||||||
notifications.success(`Automation ${name} created.`)
|
|
||||||
|
|
||||||
$goto(`./${$automationStore.selectedAutomation.automation._id}`)
|
|
||||||
analytics.captureEvent(Events.AUTOMATION.CREATED, { name })
|
|
||||||
}
|
}
|
||||||
$: triggers = Object.entries($automationStore.blockDefinitions.TRIGGER)
|
$: triggers = Object.entries($automationStore.blockDefinitions.TRIGGER)
|
||||||
|
|
||||||
|
|
|
@ -11,9 +11,13 @@
|
||||||
let updateAutomationDialog
|
let updateAutomationDialog
|
||||||
|
|
||||||
async function deleteAutomation() {
|
async function deleteAutomation() {
|
||||||
await automationStore.actions.delete(automation)
|
try {
|
||||||
notifications.success("Automation deleted.")
|
await automationStore.actions.delete(automation)
|
||||||
$goto("../automate")
|
notifications.success("Automation deleted successfully")
|
||||||
|
$goto("../automate")
|
||||||
|
} catch (error) {
|
||||||
|
notifications.error("Error deleting automation")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
|
@ -20,14 +20,18 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
async function saveAutomation() {
|
async function saveAutomation() {
|
||||||
const updatedAutomation = {
|
try {
|
||||||
...automation,
|
const updatedAutomation = {
|
||||||
name,
|
...automation,
|
||||||
|
name,
|
||||||
|
}
|
||||||
|
await automationStore.actions.save(updatedAutomation)
|
||||||
|
notifications.success(`Automation ${name} updated successfully`)
|
||||||
|
analytics.captureEvent(Events.AUTOMATION.SAVED, { name })
|
||||||
|
hide()
|
||||||
|
} catch (error) {
|
||||||
|
notifications.error("Error saving automation")
|
||||||
}
|
}
|
||||||
await automationStore.actions.save(updatedAutomation)
|
|
||||||
notifications.success(`Automation ${name} updated successfully.`)
|
|
||||||
analytics.captureEvent(Events.AUTOMATION.SAVED, { name })
|
|
||||||
hide()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function checkValid(evt) {
|
function checkValid(evt) {
|
||||||
|
|
|
@ -11,6 +11,7 @@
|
||||||
Drawer,
|
Drawer,
|
||||||
Modal,
|
Modal,
|
||||||
Detail,
|
Detail,
|
||||||
|
notifications,
|
||||||
} from "@budibase/bbui"
|
} from "@budibase/bbui"
|
||||||
import CreateWebhookModal from "components/automation/Shared/CreateWebhookModal.svelte"
|
import CreateWebhookModal from "components/automation/Shared/CreateWebhookModal.svelte"
|
||||||
|
|
||||||
|
@ -28,7 +29,7 @@
|
||||||
import ModalBindableInput from "components/common/bindings/ModalBindableInput.svelte"
|
import ModalBindableInput from "components/common/bindings/ModalBindableInput.svelte"
|
||||||
import FilterDrawer from "components/design/PropertiesPanel/PropertyControls/FilterEditor/FilterDrawer.svelte"
|
import FilterDrawer from "components/design/PropertiesPanel/PropertyControls/FilterEditor/FilterDrawer.svelte"
|
||||||
// need the client lucene builder to convert to the structure API expects
|
// need the client lucene builder to convert to the structure API expects
|
||||||
import { buildLuceneQuery } from "helpers/lucene"
|
import { LuceneUtils } from "@budibase/frontend-core"
|
||||||
|
|
||||||
export let block
|
export let block
|
||||||
export let testData
|
export let testData
|
||||||
|
@ -54,28 +55,32 @@
|
||||||
$: schemaFields = table ? Object.values(table.schema) : []
|
$: schemaFields = table ? Object.values(table.schema) : []
|
||||||
|
|
||||||
const onChange = debounce(async function (e, key) {
|
const onChange = debounce(async function (e, key) {
|
||||||
if (isTestModal) {
|
try {
|
||||||
// Special case for webhook, as it requires a body, but the schema already brings back the body's contents
|
if (isTestModal) {
|
||||||
if (stepId === "WEBHOOK") {
|
// Special case for webhook, as it requires a body, but the schema already brings back the body's contents
|
||||||
|
if (stepId === "WEBHOOK") {
|
||||||
|
automationStore.actions.addTestDataToAutomation({
|
||||||
|
body: {
|
||||||
|
[key]: e.detail,
|
||||||
|
...$automationStore.selectedAutomation.automation.testData.body,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
automationStore.actions.addTestDataToAutomation({
|
automationStore.actions.addTestDataToAutomation({
|
||||||
body: {
|
[key]: e.detail,
|
||||||
[key]: e.detail,
|
|
||||||
...$automationStore.selectedAutomation.automation.testData.body,
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
|
testData[key] = e.detail
|
||||||
|
await automationStore.actions.save(
|
||||||
|
$automationStore.selectedAutomation?.automation
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
block.inputs[key] = e.detail
|
||||||
|
await automationStore.actions.save(
|
||||||
|
$automationStore.selectedAutomation?.automation
|
||||||
|
)
|
||||||
}
|
}
|
||||||
automationStore.actions.addTestDataToAutomation({
|
} catch (error) {
|
||||||
[key]: e.detail,
|
notifications.error("Error saving automation")
|
||||||
})
|
|
||||||
testData[key] = e.detail
|
|
||||||
await automationStore.actions.save(
|
|
||||||
$automationStore.selectedAutomation?.automation
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
block.inputs[key] = e.detail
|
|
||||||
await automationStore.actions.save(
|
|
||||||
$automationStore.selectedAutomation?.automation
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}, 800)
|
}, 800)
|
||||||
|
|
||||||
|
@ -131,7 +136,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
function saveFilters(key) {
|
function saveFilters(key) {
|
||||||
const filters = buildLuceneQuery(tempFilters)
|
const filters = LuceneUtils.buildLuceneQuery(tempFilters)
|
||||||
const defKey = `${key}-def`
|
const defKey = `${key}-def`
|
||||||
inputData[key] = filters
|
inputData[key] = filters
|
||||||
inputData[defKey] = tempFilters
|
inputData[defKey] = tempFilters
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
<script>
|
<script>
|
||||||
import { Icon } from "@budibase/bbui"
|
import { Icon, notifications } from "@budibase/bbui"
|
||||||
import { automationStore } from "builderStore"
|
import { automationStore } from "builderStore"
|
||||||
import WebhookDisplay from "./WebhookDisplay.svelte"
|
import WebhookDisplay from "./WebhookDisplay.svelte"
|
||||||
import { ModalContent } from "@budibase/bbui"
|
import { ModalContent } from "@budibase/bbui"
|
||||||
|
@ -16,15 +16,24 @@
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
if (!automation?.definition?.trigger?.inputs.schemaUrl) {
|
if (!automation?.definition?.trigger?.inputs.schemaUrl) {
|
||||||
// save the automation initially
|
// save the automation initially
|
||||||
await automationStore.actions.save(automation)
|
try {
|
||||||
|
await automationStore.actions.save(automation)
|
||||||
|
} catch (error) {
|
||||||
|
notifications.error("Error saving automation")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
interval = setInterval(async () => {
|
interval = setInterval(async () => {
|
||||||
await automationStore.actions.fetch()
|
try {
|
||||||
const outputs = automation?.definition?.trigger.schema.outputs?.properties
|
await automationStore.actions.fetch()
|
||||||
// always one prop for the "body"
|
const outputs =
|
||||||
if (Object.keys(outputs).length > 1) {
|
automation?.definition?.trigger.schema.outputs?.properties
|
||||||
propCount = Object.keys(outputs).length - 1
|
// always one prop for the "body"
|
||||||
finished = true
|
if (Object.keys(outputs).length > 1) {
|
||||||
|
propCount = Object.keys(outputs).length - 1
|
||||||
|
finished = true
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
notifications.error("Error getting automations list")
|
||||||
}
|
}
|
||||||
}, POLL_RATE_MS)
|
}, POLL_RATE_MS)
|
||||||
schemaURL = automation?.definition?.trigger?.inputs.schemaUrl
|
schemaURL = automation?.definition?.trigger?.inputs.schemaUrl
|
||||||
|
|
|
@ -14,18 +14,19 @@
|
||||||
import Table from "./Table.svelte"
|
import Table from "./Table.svelte"
|
||||||
import { TableNames } from "constants"
|
import { TableNames } from "constants"
|
||||||
import CreateEditRow from "./modals/CreateEditRow.svelte"
|
import CreateEditRow from "./modals/CreateEditRow.svelte"
|
||||||
import { fetchTableData } from "helpers/fetchTableData"
|
|
||||||
import { Pagination } from "@budibase/bbui"
|
import { Pagination } from "@budibase/bbui"
|
||||||
|
import { fetchData } from "@budibase/frontend-core"
|
||||||
|
import { API } from "api"
|
||||||
|
|
||||||
let hideAutocolumns = true
|
let hideAutocolumns = true
|
||||||
|
|
||||||
$: isUsersTable = $tables.selected?._id === TableNames.USERS
|
$: isUsersTable = $tables.selected?._id === TableNames.USERS
|
||||||
$: type = $tables.selected?.type
|
$: type = $tables.selected?.type
|
||||||
$: isInternal = type !== "external"
|
$: isInternal = type !== "external"
|
||||||
$: schema = $tables.selected?.schema
|
$: schema = $tables.selected?.schema
|
||||||
$: enrichedSchema = enrichSchema($tables.selected?.schema)
|
$: enrichedSchema = enrichSchema($tables.selected?.schema)
|
||||||
$: id = $tables.selected?._id
|
$: id = $tables.selected?._id
|
||||||
$: search = searchTable(id)
|
$: fetch = createFetch(id)
|
||||||
$: columnOptions = Object.keys($search.schema || {})
|
|
||||||
|
|
||||||
const enrichSchema = schema => {
|
const enrichSchema = schema => {
|
||||||
let tempSchema = { ...schema }
|
let tempSchema = { ...schema }
|
||||||
|
@ -47,18 +48,24 @@
|
||||||
return tempSchema
|
return tempSchema
|
||||||
}
|
}
|
||||||
// Fetches new data whenever the table changes
|
// Fetches new data whenever the table changes
|
||||||
const searchTable = tableId => {
|
const createFetch = tableId => {
|
||||||
return fetchTableData({
|
return fetchData({
|
||||||
tableId,
|
API,
|
||||||
schema,
|
datasource: {
|
||||||
limit: 10,
|
tableId,
|
||||||
paginate: true,
|
type: "table",
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
schema,
|
||||||
|
limit: 10,
|
||||||
|
paginate: true,
|
||||||
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fetch data whenever sorting option changes
|
// Fetch data whenever sorting option changes
|
||||||
const onSort = e => {
|
const onSort = e => {
|
||||||
search.update({
|
fetch.update({
|
||||||
sortColumn: e.detail.column,
|
sortColumn: e.detail.column,
|
||||||
sortOrder: e.detail.order,
|
sortOrder: e.detail.order,
|
||||||
})
|
})
|
||||||
|
@ -66,22 +73,20 @@
|
||||||
|
|
||||||
// Fetch data whenever filters change
|
// Fetch data whenever filters change
|
||||||
const onFilter = e => {
|
const onFilter = e => {
|
||||||
search.update({
|
fetch.update({
|
||||||
filters: e.detail,
|
filter: e.detail,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fetch data whenever schema changes
|
// Fetch data whenever schema changes
|
||||||
const onUpdateColumns = () => {
|
const onUpdateColumns = () => {
|
||||||
search.update({
|
fetch.refresh()
|
||||||
schema,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fetch data whenever rows are modified. Unfortunately we have to lose
|
// Fetch data whenever rows are modified. Unfortunately we have to lose
|
||||||
// our pagination place, as our bookmarks will have shifted.
|
// our pagination place, as our bookmarks will have shifted.
|
||||||
const onUpdateRows = () => {
|
const onUpdateRows = () => {
|
||||||
search.update()
|
fetch.refresh()
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
@ -91,9 +96,9 @@
|
||||||
schema={enrichedSchema}
|
schema={enrichedSchema}
|
||||||
{type}
|
{type}
|
||||||
tableId={id}
|
tableId={id}
|
||||||
data={$search.rows}
|
data={$fetch.rows}
|
||||||
bind:hideAutocolumns
|
bind:hideAutocolumns
|
||||||
loading={$search.loading}
|
loading={$fetch.loading}
|
||||||
on:sort={onSort}
|
on:sort={onSort}
|
||||||
allowEditing
|
allowEditing
|
||||||
disableSorting
|
disableSorting
|
||||||
|
@ -138,11 +143,11 @@
|
||||||
<div in:fade={{ delay: 200, duration: 100 }}>
|
<div in:fade={{ delay: 200, duration: 100 }}>
|
||||||
<div class="pagination">
|
<div class="pagination">
|
||||||
<Pagination
|
<Pagination
|
||||||
page={$search.pageNumber + 1}
|
page={$fetch.pageNumber + 1}
|
||||||
hasPrevPage={$search.hasPrevPage}
|
hasPrevPage={$fetch.hasPrevPage}
|
||||||
hasNextPage={$search.hasNextPage}
|
hasNextPage={$fetch.hasNextPage}
|
||||||
goToPrevPage={$search.loading ? null : search.prevPage}
|
goToPrevPage={$fetch.loading ? null : fetch.prevPage}
|
||||||
goToNextPage={$search.loading ? null : search.nextPage}
|
goToNextPage={$fetch.loading ? null : fetch.nextPage}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,7 +1,8 @@
|
||||||
<script>
|
<script>
|
||||||
import api from "builderStore/api"
|
import { API } from "api"
|
||||||
import Table from "./Table.svelte"
|
import Table from "./Table.svelte"
|
||||||
import { tables } from "stores/backend"
|
import { tables } from "stores/backend"
|
||||||
|
import { notifications } from "@budibase/bbui"
|
||||||
|
|
||||||
export let tableId
|
export let tableId
|
||||||
export let rowId
|
export let rowId
|
||||||
|
@ -27,9 +28,15 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
async function fetchData(tableId, rowId) {
|
async function fetchData(tableId, rowId) {
|
||||||
const QUERY_VIEW_URL = `/api/${tableId}/${rowId}/enrich`
|
try {
|
||||||
const response = await api.get(QUERY_VIEW_URL)
|
row = await API.fetchRelationshipData({
|
||||||
row = await response.json()
|
tableId,
|
||||||
|
rowId,
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
row = null
|
||||||
|
notifications.error("Error fetching relationship data")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
|
@ -4,9 +4,10 @@
|
||||||
Select,
|
Select,
|
||||||
DatePicker,
|
DatePicker,
|
||||||
Toggle,
|
Toggle,
|
||||||
TextArea,
|
|
||||||
Multiselect,
|
Multiselect,
|
||||||
Label,
|
Label,
|
||||||
|
RichTextField,
|
||||||
|
TextArea,
|
||||||
} from "@budibase/bbui"
|
} from "@budibase/bbui"
|
||||||
import Dropzone from "components/common/Dropzone.svelte"
|
import Dropzone from "components/common/Dropzone.svelte"
|
||||||
import { capitalise } from "helpers"
|
import { capitalise } from "helpers"
|
||||||
|
@ -43,7 +44,11 @@
|
||||||
{:else if type === "link"}
|
{:else if type === "link"}
|
||||||
<LinkedRowSelector bind:linkedRows={value} schema={meta} />
|
<LinkedRowSelector bind:linkedRows={value} schema={meta} />
|
||||||
{:else if type === "longform"}
|
{:else if type === "longform"}
|
||||||
<TextArea {label} bind:value />
|
{#if meta.useRichText}
|
||||||
|
<RichTextField {label} height="150px" bind:value />
|
||||||
|
{:else}
|
||||||
|
<TextArea {label} height="150px" bind:value />
|
||||||
|
{/if}
|
||||||
{:else if type === "json"}
|
{:else if type === "json"}
|
||||||
<Label>{label}</Label>
|
<Label>{label}</Label>
|
||||||
<Editor
|
<Editor
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
import { fade } from "svelte/transition"
|
import { fade } from "svelte/transition"
|
||||||
import { goto, params } from "@roxi/routify"
|
import { goto, params } from "@roxi/routify"
|
||||||
import { Table, Modal, Heading, notifications, Layout } from "@budibase/bbui"
|
import { Table, Modal, Heading, notifications, Layout } from "@budibase/bbui"
|
||||||
import api from "builderStore/api"
|
import { API } from "api"
|
||||||
import Spinner from "components/common/Spinner.svelte"
|
import Spinner from "components/common/Spinner.svelte"
|
||||||
import DeleteRowsButton from "./buttons/DeleteRowsButton.svelte"
|
import DeleteRowsButton from "./buttons/DeleteRowsButton.svelte"
|
||||||
import CreateEditRow from "./modals/CreateEditRow.svelte"
|
import CreateEditRow from "./modals/CreateEditRow.svelte"
|
||||||
|
@ -88,12 +88,17 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
const deleteRows = async () => {
|
const deleteRows = async () => {
|
||||||
await api.delete(`/api/${tableId}/rows`, {
|
try {
|
||||||
rows: selectedRows,
|
await API.deleteRows({
|
||||||
})
|
tableId,
|
||||||
data = data.filter(row => !selectedRows.includes(row))
|
rows: selectedRows,
|
||||||
notifications.success(`Successfully deleted ${selectedRows.length} rows`)
|
})
|
||||||
selectedRows = []
|
data = data.filter(row => !selectedRows.includes(row))
|
||||||
|
notifications.success(`Successfully deleted ${selectedRows.length} rows`)
|
||||||
|
selectedRows = []
|
||||||
|
} catch (error) {
|
||||||
|
notifications.error("Error deleting rows")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const editRow = row => {
|
const editRow = row => {
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
<script>
|
<script>
|
||||||
import api from "builderStore/api"
|
import { API } from "api"
|
||||||
import { tables } from "stores/backend"
|
import { tables } from "stores/backend"
|
||||||
|
|
||||||
import Table from "./Table.svelte"
|
import Table from "./Table.svelte"
|
||||||
|
@ -9,6 +9,7 @@
|
||||||
import ExportButton from "./buttons/ExportButton.svelte"
|
import ExportButton from "./buttons/ExportButton.svelte"
|
||||||
import ManageAccessButton from "./buttons/ManageAccessButton.svelte"
|
import ManageAccessButton from "./buttons/ManageAccessButton.svelte"
|
||||||
import HideAutocolumnButton from "./buttons/HideAutocolumnButton.svelte"
|
import HideAutocolumnButton from "./buttons/HideAutocolumnButton.svelte"
|
||||||
|
import { notifications } from "@budibase/bbui"
|
||||||
|
|
||||||
export let view = {}
|
export let view = {}
|
||||||
|
|
||||||
|
@ -20,33 +21,31 @@
|
||||||
$: name = view.name
|
$: name = view.name
|
||||||
|
|
||||||
// Fetch rows for specified view
|
// Fetch rows for specified view
|
||||||
$: {
|
$: fetchViewData(name, view.field, view.groupBy, view.calculation)
|
||||||
loading = true
|
|
||||||
fetchViewData(name, view.field, view.groupBy, view.calculation)
|
|
||||||
}
|
|
||||||
|
|
||||||
async function fetchViewData(name, field, groupBy, calculation) {
|
async function fetchViewData(name, field, groupBy, calculation) {
|
||||||
|
loading = true
|
||||||
const _tables = $tables.list
|
const _tables = $tables.list
|
||||||
const allTableViews = _tables.map(table => table.views)
|
const allTableViews = _tables.map(table => table.views)
|
||||||
const thisView = allTableViews.filter(
|
const thisView = allTableViews.filter(
|
||||||
views => views != null && views[name] != null
|
views => views != null && views[name] != null
|
||||||
)[0]
|
)[0]
|
||||||
|
|
||||||
// don't fetch view data if the view no longer exists
|
// Don't fetch view data if the view no longer exists
|
||||||
if (!thisView) {
|
if (!thisView) {
|
||||||
|
loading = false
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const params = new URLSearchParams()
|
try {
|
||||||
if (calculation) {
|
data = await API.fetchViewData({
|
||||||
params.set("field", field)
|
name,
|
||||||
params.set("calculation", calculation)
|
calculation,
|
||||||
|
field,
|
||||||
|
groupBy,
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
notifications.error("Error fetching view data")
|
||||||
}
|
}
|
||||||
if (groupBy) {
|
|
||||||
params.set("group", groupBy)
|
|
||||||
}
|
|
||||||
const QUERY_VIEW_URL = `/api/views/${name}?${params}`
|
|
||||||
const response = await api.get(QUERY_VIEW_URL)
|
|
||||||
data = await response.json()
|
|
||||||
loading = false
|
loading = false
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -1,34 +0,0 @@
|
||||||
import api from "builderStore/api"
|
|
||||||
|
|
||||||
export async function createUser(user) {
|
|
||||||
const CREATE_USER_URL = `/api/users/metadata`
|
|
||||||
const response = await api.post(CREATE_USER_URL, user)
|
|
||||||
return await response.json()
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function saveRow(row, tableId) {
|
|
||||||
const SAVE_ROW_URL = `/api/${tableId}/rows`
|
|
||||||
const response = await api.post(SAVE_ROW_URL, row)
|
|
||||||
|
|
||||||
return await response.json()
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function deleteRow(row) {
|
|
||||||
const DELETE_ROWS_URL = `/api/${row.tableId}/rows`
|
|
||||||
return api.delete(DELETE_ROWS_URL, {
|
|
||||||
_id: row._id,
|
|
||||||
_rev: row._rev,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function fetchDataForTable(tableId) {
|
|
||||||
const FETCH_ROWS_URL = `/api/${tableId}/rows`
|
|
||||||
|
|
||||||
const response = await api.get(FETCH_ROWS_URL)
|
|
||||||
const json = await response.json()
|
|
||||||
|
|
||||||
if (response.status !== 200) {
|
|
||||||
throw new Error(json.message)
|
|
||||||
}
|
|
||||||
return json
|
|
||||||
}
|
|
|
@ -38,9 +38,13 @@
|
||||||
})
|
})
|
||||||
|
|
||||||
function saveView() {
|
function saveView() {
|
||||||
views.save(view)
|
try {
|
||||||
notifications.success(`View ${view.name} saved.`)
|
views.save(view)
|
||||||
analytics.captureEvent(Events.VIEW.ADDED_CALCULATE, { field: view.field })
|
notifications.success(`View ${view.name} saved`)
|
||||||
|
analytics.captureEvent(Events.VIEW.ADDED_CALCULATE, { field: view.field })
|
||||||
|
} catch (error) {
|
||||||
|
notifications.error("Error saving view")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
|
@ -124,7 +124,7 @@
|
||||||
})
|
})
|
||||||
dispatch("updatecolumns")
|
dispatch("updatecolumns")
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
notifications.error(err)
|
notifications.error("Error saving column")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -133,17 +133,21 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
function deleteColumn() {
|
function deleteColumn() {
|
||||||
field.name = deleteColName
|
try {
|
||||||
if (field.name === $tables.selected.primaryDisplay) {
|
field.name = deleteColName
|
||||||
notifications.error("You cannot delete the display column")
|
if (field.name === $tables.selected.primaryDisplay) {
|
||||||
} else {
|
notifications.error("You cannot delete the display column")
|
||||||
tables.deleteField(field)
|
} else {
|
||||||
notifications.success(`Column ${field.name} deleted.`)
|
tables.deleteField(field)
|
||||||
confirmDeleteDialog.hide()
|
notifications.success(`Column ${field.name} deleted.`)
|
||||||
hide()
|
confirmDeleteDialog.hide()
|
||||||
deletion = false
|
hide()
|
||||||
|
deletion = false
|
||||||
|
dispatch("updatecolumns")
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
notifications.error("Error deleting column")
|
||||||
}
|
}
|
||||||
dispatch("updatecolumns")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleTypeChange(event) {
|
function handleTypeChange(event) {
|
||||||
|
@ -367,7 +371,7 @@
|
||||||
|
|
||||||
{#if canBeSearched && !external}
|
{#if canBeSearched && !external}
|
||||||
<div>
|
<div>
|
||||||
<Label grey small>Search Indexes</Label>
|
<Label>Search Indexes</Label>
|
||||||
<Toggle
|
<Toggle
|
||||||
value={indexes[0] === field.name}
|
value={indexes[0] === field.name}
|
||||||
disabled={indexes[1] === field.name}
|
disabled={indexes[1] === field.name}
|
||||||
|
@ -394,6 +398,19 @@
|
||||||
label="Options (one per line)"
|
label="Options (one per line)"
|
||||||
bind:values={field.constraints.inclusion}
|
bind:values={field.constraints.inclusion}
|
||||||
/>
|
/>
|
||||||
|
{:else if field.type === "longform"}
|
||||||
|
<div>
|
||||||
|
<Label
|
||||||
|
size="M"
|
||||||
|
tooltip="Rich text includes support for images, links, tables, lists and more"
|
||||||
|
>
|
||||||
|
Formatting
|
||||||
|
</Label>
|
||||||
|
<Toggle
|
||||||
|
bind:value={field.useRichText}
|
||||||
|
text="Enable rich text support (markdown)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
{:else if field.type === "array"}
|
{:else if field.type === "array"}
|
||||||
<ValuesList
|
<ValuesList
|
||||||
label="Options (one per line)"
|
label="Options (one per line)"
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
import { tables, rows } from "stores/backend"
|
import { tables, rows } from "stores/backend"
|
||||||
import { notifications } from "@budibase/bbui"
|
import { notifications } from "@budibase/bbui"
|
||||||
import RowFieldControl from "../RowFieldControl.svelte"
|
import RowFieldControl from "../RowFieldControl.svelte"
|
||||||
import * as api from "../api"
|
import { API } from "api"
|
||||||
import { ModalContent } from "@budibase/bbui"
|
import { ModalContent } from "@budibase/bbui"
|
||||||
import ErrorsBox from "components/common/ErrorsBox.svelte"
|
import ErrorsBox from "components/common/ErrorsBox.svelte"
|
||||||
import { FIELDS } from "constants/backend"
|
import { FIELDS } from "constants/backend"
|
||||||
|
@ -22,30 +22,30 @@
|
||||||
$: tableSchema = Object.entries(table?.schema ?? {})
|
$: tableSchema = Object.entries(table?.schema ?? {})
|
||||||
|
|
||||||
async function saveRow() {
|
async function saveRow() {
|
||||||
const rowResponse = await api.saveRow(
|
errors = []
|
||||||
{ ...row, tableId: table._id },
|
try {
|
||||||
table._id
|
await API.saveRow({ ...row, tableId: table._id })
|
||||||
)
|
notifications.success("Row saved successfully")
|
||||||
|
rows.save()
|
||||||
if (rowResponse.errors) {
|
dispatch("updaterows")
|
||||||
errors = Object.entries(rowResponse.errors)
|
} catch (error) {
|
||||||
.map(([key, error]) => ({ dataPath: key, message: error }))
|
if (error.handled) {
|
||||||
.flat()
|
const response = error.json
|
||||||
|
if (response?.errors) {
|
||||||
|
errors = Object.entries(response.errors)
|
||||||
|
.map(([key, error]) => ({ dataPath: key, message: error }))
|
||||||
|
.flat()
|
||||||
|
} else if (error.status === 400 && response?.validationErrors) {
|
||||||
|
errors = Object.keys(response.validationErrors).map(field => ({
|
||||||
|
message: `${field} ${response.validationErrors[field][0]}`,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
notifications.error("Failed to save row")
|
||||||
|
}
|
||||||
// Prevent modal closing if there were errors
|
// Prevent modal closing if there were errors
|
||||||
return false
|
return false
|
||||||
} else if (rowResponse.status === 400 && rowResponse.validationErrors) {
|
|
||||||
errors = Object.keys(rowResponse.validationErrors).map(field => ({
|
|
||||||
message: `${field} ${rowResponse.validationErrors[field][0]}`,
|
|
||||||
}))
|
|
||||||
return false
|
|
||||||
} else if (rowResponse.status >= 400) {
|
|
||||||
errors = [{ message: rowResponse.message }]
|
|
||||||
return false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
notifications.success("Row saved successfully.")
|
|
||||||
rows.save(rowResponse)
|
|
||||||
dispatch("updaterows")
|
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
|
@ -4,7 +4,7 @@
|
||||||
import { roles } from "stores/backend"
|
import { roles } from "stores/backend"
|
||||||
import { notifications } from "@budibase/bbui"
|
import { notifications } from "@budibase/bbui"
|
||||||
import RowFieldControl from "../RowFieldControl.svelte"
|
import RowFieldControl from "../RowFieldControl.svelte"
|
||||||
import * as backendApi from "../api"
|
import { API } from "api"
|
||||||
import { ModalContent, Select } from "@budibase/bbui"
|
import { ModalContent, Select } from "@budibase/bbui"
|
||||||
import ErrorsBox from "components/common/ErrorsBox.svelte"
|
import ErrorsBox from "components/common/ErrorsBox.svelte"
|
||||||
|
|
||||||
|
@ -53,27 +53,31 @@
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
const rowResponse = await backendApi.saveRow(
|
try {
|
||||||
{ ...row, tableId: table._id },
|
await API.saveRow({ ...row, tableId: table._id })
|
||||||
table._id
|
notifications.success("User saved successfully")
|
||||||
)
|
rows.save()
|
||||||
if (rowResponse.errors) {
|
dispatch("updaterows")
|
||||||
if (Array.isArray(rowResponse.errors)) {
|
} catch (error) {
|
||||||
errors = rowResponse.errors.map(error => ({ message: error }))
|
if (error.handled) {
|
||||||
|
const response = error.json
|
||||||
|
if (response?.errors) {
|
||||||
|
if (Array.isArray(response.errors)) {
|
||||||
|
errors = response.errors.map(error => ({ message: error }))
|
||||||
|
} else {
|
||||||
|
errors = Object.entries(response.errors)
|
||||||
|
.map(([key, error]) => ({ dataPath: key, message: error }))
|
||||||
|
.flat()
|
||||||
|
}
|
||||||
|
} else if (error.status === 400) {
|
||||||
|
errors = [{ message: response?.message || "Unknown error" }]
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
errors = Object.entries(rowResponse.errors)
|
notifications.error("Error saving user")
|
||||||
.map(([key, error]) => ({ dataPath: key, message: error }))
|
|
||||||
.flat()
|
|
||||||
}
|
}
|
||||||
return false
|
// Prevent closing the modal on errors
|
||||||
} else if (rowResponse.status === 400 || rowResponse.status === 500) {
|
|
||||||
errors = [{ message: rowResponse.message }]
|
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
notifications.success("User saved successfully")
|
|
||||||
rows.save(rowResponse)
|
|
||||||
dispatch("updaterows")
|
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
|
@ -12,17 +12,21 @@
|
||||||
|
|
||||||
function saveView() {
|
function saveView() {
|
||||||
if (views.includes(name)) {
|
if (views.includes(name)) {
|
||||||
notifications.error(`View exists with name ${name}.`)
|
notifications.error(`View exists with name ${name}`)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
viewsStore.save({
|
try {
|
||||||
name,
|
viewsStore.save({
|
||||||
tableId: $tables.selected._id,
|
name,
|
||||||
field,
|
tableId: $tables.selected._id,
|
||||||
})
|
field,
|
||||||
notifications.success(`View ${name} created`)
|
})
|
||||||
analytics.captureEvent(Events.VIEW.CREATED, { name })
|
notifications.success(`View ${name} created`)
|
||||||
$goto(`../../view/${name}`)
|
analytics.captureEvent(Events.VIEW.CREATED, { name })
|
||||||
|
$goto(`../../view/${name}`)
|
||||||
|
} catch (error) {
|
||||||
|
notifications.error("Error creating view")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
<script>
|
<script>
|
||||||
import { ModalContent, Select, Input, Button } from "@budibase/bbui"
|
import { ModalContent, Select, Input, Button } from "@budibase/bbui"
|
||||||
import { onMount } from "svelte"
|
import { onMount } from "svelte"
|
||||||
import api from "builderStore/api"
|
import { API } from "api"
|
||||||
import { notifications } from "@budibase/bbui"
|
import { notifications } from "@budibase/bbui"
|
||||||
import ErrorsBox from "components/common/ErrorsBox.svelte"
|
import ErrorsBox from "components/common/ErrorsBox.svelte"
|
||||||
import { roles } from "stores/backend"
|
import { roles } from "stores/backend"
|
||||||
|
@ -24,8 +24,12 @@
|
||||||
!builtInRoles.includes(selectedRole.name)
|
!builtInRoles.includes(selectedRole.name)
|
||||||
|
|
||||||
const fetchBasePermissions = async () => {
|
const fetchBasePermissions = async () => {
|
||||||
const permissionsResponse = await api.get("/api/permission/builtin")
|
try {
|
||||||
basePermissions = await permissionsResponse.json()
|
basePermissions = await API.getBasePermissions()
|
||||||
|
} catch (error) {
|
||||||
|
notifications.error("Error fetching base permission options")
|
||||||
|
basePermissions = []
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Changes the selected role
|
// Changes the selected role
|
||||||
|
@ -68,23 +72,23 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
// Save/create the role
|
// Save/create the role
|
||||||
const response = await roles.save(selectedRole)
|
try {
|
||||||
if (response.status === 200) {
|
await roles.save(selectedRole)
|
||||||
notifications.success("Role saved successfully.")
|
notifications.success("Role saved successfully")
|
||||||
} else {
|
} catch (error) {
|
||||||
notifications.error("Error saving role.")
|
notifications.error("Error saving role")
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Deletes the selected role
|
// Deletes the selected role
|
||||||
const deleteRole = async () => {
|
const deleteRole = async () => {
|
||||||
const response = await roles.delete(selectedRole)
|
try {
|
||||||
if (response.status === 200) {
|
await roles.delete(selectedRole)
|
||||||
changeRole()
|
changeRole()
|
||||||
notifications.success("Role deleted successfully.")
|
notifications.success("Role deleted successfully")
|
||||||
} else {
|
} catch (error) {
|
||||||
notifications.error("Error deleting role.")
|
notifications.error("Error deleting role")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
<script>
|
<script>
|
||||||
import { Select, ModalContent, notifications } from "@budibase/bbui"
|
import { Select, ModalContent, notifications } from "@budibase/bbui"
|
||||||
import download from "downloadjs"
|
import download from "downloadjs"
|
||||||
import { get } from "builderStore/api"
|
import { API } from "api"
|
||||||
|
|
||||||
const FORMATS = [
|
const FORMATS = [
|
||||||
{
|
{
|
||||||
|
@ -19,17 +19,14 @@
|
||||||
let exportFormat = FORMATS[0].key
|
let exportFormat = FORMATS[0].key
|
||||||
|
|
||||||
async function exportView() {
|
async function exportView() {
|
||||||
const uri = encodeURIComponent(view)
|
try {
|
||||||
const response = await get(
|
const data = await API.exportView({
|
||||||
`/api/views/export?view=${uri}&format=${exportFormat}`
|
viewName: view,
|
||||||
)
|
format: exportFormat,
|
||||||
if (response.status === 200) {
|
})
|
||||||
const data = await response.text()
|
|
||||||
download(data, `export.${exportFormat}`)
|
download(data, `export.${exportFormat}`)
|
||||||
} else {
|
} catch (error) {
|
||||||
notifications.error(
|
notifications.error(`Unable to export ${exportFormat.toUpperCase()} data`)
|
||||||
`Unable to export ${exportFormat.toUpperCase()} data.`
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -72,11 +72,15 @@
|
||||||
$: schema = viewTable && viewTable.schema ? viewTable.schema : {}
|
$: schema = viewTable && viewTable.schema ? viewTable.schema : {}
|
||||||
|
|
||||||
function saveView() {
|
function saveView() {
|
||||||
views.save(view)
|
try {
|
||||||
notifications.success(`View ${view.name} saved.`)
|
views.save(view)
|
||||||
analytics.captureEvent(Events.VIEW.ADDED_FILTER, {
|
notifications.success(`View ${view.name} saved`)
|
||||||
filters: JSON.stringify(view.filters),
|
analytics.captureEvent(Events.VIEW.ADDED_FILTER, {
|
||||||
})
|
filters: JSON.stringify(view.filters),
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
notifications.error("Error saving view")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function removeFilter(idx) {
|
function removeFilter(idx) {
|
||||||
|
@ -158,7 +162,7 @@
|
||||||
<Select
|
<Select
|
||||||
bind:value={filter.value}
|
bind:value={filter.value}
|
||||||
options={fieldOptions(filter.key)}
|
options={fieldOptions(filter.key)}
|
||||||
getOptionLabel={x => x.toString()}
|
getOptionLabel={x => x?.toString() || ""}
|
||||||
/>
|
/>
|
||||||
{:else if filter.key && isDate(filter.key)}
|
{:else if filter.key && isDate(filter.key)}
|
||||||
<DatePicker
|
<DatePicker
|
||||||
|
|
|
@ -19,8 +19,12 @@
|
||||||
.map(([key]) => key)
|
.map(([key]) => key)
|
||||||
|
|
||||||
function saveView() {
|
function saveView() {
|
||||||
views.save(view)
|
try {
|
||||||
notifications.success(`View ${view.name} saved.`)
|
views.save(view)
|
||||||
|
notifications.success(`View ${view.name} saved`)
|
||||||
|
} catch (error) {
|
||||||
|
notifications.error("Error saving view")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,13 @@
|
||||||
<script>
|
<script>
|
||||||
import { ModalContent, Label, notifications, Body } from "@budibase/bbui"
|
import {
|
||||||
|
ModalContent,
|
||||||
|
Label,
|
||||||
|
notifications,
|
||||||
|
Body,
|
||||||
|
Layout,
|
||||||
|
} from "@budibase/bbui"
|
||||||
import TableDataImport from "../../TableNavigator/TableDataImport.svelte"
|
import TableDataImport from "../../TableNavigator/TableDataImport.svelte"
|
||||||
import api from "builderStore/api"
|
import { API } from "api"
|
||||||
import { createEventDispatcher } from "svelte"
|
import { createEventDispatcher } from "svelte"
|
||||||
|
|
||||||
const dispatch = createEventDispatcher()
|
const dispatch = createEventDispatcher()
|
||||||
|
@ -12,15 +18,17 @@
|
||||||
$: valid = dataImport?.csvString != null && dataImport?.valid
|
$: valid = dataImport?.csvString != null && dataImport?.valid
|
||||||
|
|
||||||
async function importData() {
|
async function importData() {
|
||||||
const response = await api.post(`/api/tables/${tableId}/import`, {
|
try {
|
||||||
dataImport,
|
await API.importTableData({
|
||||||
})
|
tableId,
|
||||||
if (response.status !== 200) {
|
data: dataImport,
|
||||||
const error = await response.text()
|
})
|
||||||
notifications.error(`Unable to import data - ${error}`)
|
notifications.success("Rows successfully imported")
|
||||||
} else {
|
} catch (error) {
|
||||||
notifications.success("Rows successfully imported.")
|
notifications.error("Unable to import data")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Always refresh rows just to be sure
|
||||||
dispatch("updaterows")
|
dispatch("updaterows")
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
@ -31,12 +39,14 @@
|
||||||
onConfirm={importData}
|
onConfirm={importData}
|
||||||
disabled={!valid}
|
disabled={!valid}
|
||||||
>
|
>
|
||||||
<Body
|
<Body size="S">
|
||||||
>Import rows to an existing table from a CSV. Only columns from the CSV
|
Import rows to an existing table from a CSV. Only columns from the CSV which
|
||||||
which exist in the table will be imported.</Body
|
exist in the table will be imported.
|
||||||
>
|
</Body>
|
||||||
<Label grey extraSmall>CSV to import</Label>
|
<Layout gap="XS" noPadding>
|
||||||
<TableDataImport bind:dataImport bind:existingTableId={tableId} />
|
<Label grey extraSmall>CSV to import</Label>
|
||||||
|
<TableDataImport bind:dataImport bind:existingTableId={tableId} />
|
||||||
|
</Layout>
|
||||||
</ModalContent>
|
</ModalContent>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
|
|
@ -14,15 +14,19 @@
|
||||||
export let permissions
|
export let permissions
|
||||||
|
|
||||||
async function changePermission(level, role) {
|
async function changePermission(level, role) {
|
||||||
await permissionsStore.save({
|
try {
|
||||||
level,
|
await permissionsStore.save({
|
||||||
role,
|
level,
|
||||||
resource: resourceId,
|
role,
|
||||||
})
|
resource: resourceId,
|
||||||
|
})
|
||||||
|
|
||||||
// Show updated permissions in UI: REMOVE
|
// Show updated permissions in UI: REMOVE
|
||||||
permissions = await permissionsStore.forResource(resourceId)
|
permissions = await permissionsStore.forResource(resourceId)
|
||||||
notifications.success("Updated permissions.")
|
notifications.success("Updated permissions")
|
||||||
|
} catch (error) {
|
||||||
|
notifications.error("Error updating permissions")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
|
@ -10,6 +10,7 @@
|
||||||
import TableNavigator from "components/backend/TableNavigator/TableNavigator.svelte"
|
import TableNavigator from "components/backend/TableNavigator/TableNavigator.svelte"
|
||||||
import { customQueryIconText, customQueryIconColor } from "helpers/data/utils"
|
import { customQueryIconText, customQueryIconColor } from "helpers/data/utils"
|
||||||
import ICONS from "./icons"
|
import ICONS from "./icons"
|
||||||
|
import { notifications } from "@budibase/bbui"
|
||||||
|
|
||||||
let openDataSources = []
|
let openDataSources = []
|
||||||
$: enrichedDataSources = Array.isArray($datasources.list)
|
$: enrichedDataSources = Array.isArray($datasources.list)
|
||||||
|
@ -63,9 +64,13 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onMount(() => {
|
onMount(async () => {
|
||||||
datasources.fetch()
|
try {
|
||||||
queries.fetch()
|
await datasources.fetch()
|
||||||
|
await queries.fetch()
|
||||||
|
} catch (error) {
|
||||||
|
notifications.error("Error fetching datasources and queries")
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
const containsActiveEntity = datasource => {
|
const containsActiveEntity = datasource => {
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
<script>
|
<script>
|
||||||
import { ModalContent, Body, Input } from "@budibase/bbui"
|
import { ModalContent, Body, Input, notifications } from "@budibase/bbui"
|
||||||
import { tables, datasources } from "stores/backend"
|
import { tables, datasources } from "stores/backend"
|
||||||
import { goto } from "@roxi/routify"
|
import { goto } from "@roxi/routify"
|
||||||
|
|
||||||
|
@ -29,10 +29,14 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
async function saveTable() {
|
async function saveTable() {
|
||||||
submitted = true
|
try {
|
||||||
const table = await tables.save(buildDefaultTable(name, datasource._id))
|
submitted = true
|
||||||
await datasources.fetch()
|
const table = await tables.save(buildDefaultTable(name, datasource._id))
|
||||||
$goto(`../../table/${table._id}`)
|
await datasources.fetch()
|
||||||
|
$goto(`../../table/${table._id}`)
|
||||||
|
} catch (error) {
|
||||||
|
notifications.error("Error saving table")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
|
@ -90,8 +90,8 @@
|
||||||
await datasources.updateSchema(datasource)
|
await datasources.updateSchema(datasource)
|
||||||
notifications.success(`Datasource ${name} tables updated successfully.`)
|
notifications.success(`Datasource ${name} tables updated successfully.`)
|
||||||
await tables.fetch()
|
await tables.fetch()
|
||||||
} catch (err) {
|
} catch (error) {
|
||||||
notifications.error(`Error updating datasource schema: ${err}`)
|
notifications.error("Error updating datasource schema")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -188,29 +188,27 @@
|
||||||
{:else}
|
{:else}
|
||||||
<Body size="S"><i>No tables found.</i></Body>
|
<Body size="S"><i>No tables found.</i></Body>
|
||||||
{/if}
|
{/if}
|
||||||
{#if plusTables?.length !== 0 && integration.relationships}
|
<Divider size="S" />
|
||||||
<Divider size="S" />
|
<div class="query-header">
|
||||||
<div class="query-header">
|
<Heading size="S">Relationships</Heading>
|
||||||
<Heading size="S">Relationships</Heading>
|
<Button primary on:click={() => openRelationshipModal()}>
|
||||||
<Button primary on:click={openRelationshipModal}>
|
Define relationship
|
||||||
Define relationship
|
</Button>
|
||||||
</Button>
|
</div>
|
||||||
</div>
|
<Body>
|
||||||
<Body>
|
Tell budibase how your tables are related to get even more smart features.
|
||||||
Tell budibase how your tables are related to get even more smart features.
|
</Body>
|
||||||
</Body>
|
{#if relationshipInfo && relationshipInfo.length > 0}
|
||||||
{#if relationshipInfo && relationshipInfo.length > 0}
|
<Table
|
||||||
<Table
|
on:click={({ detail }) => openRelationshipModal(detail.from, detail.to)}
|
||||||
on:click={({ detail }) => openRelationshipModal(detail.from, detail.to)}
|
schema={relationshipSchema}
|
||||||
schema={relationshipSchema}
|
data={relationshipInfo}
|
||||||
data={relationshipInfo}
|
allowEditColumns={false}
|
||||||
allowEditColumns={false}
|
allowEditRows={false}
|
||||||
allowEditRows={false}
|
allowSelectRows={false}
|
||||||
allowSelectRows={false}
|
/>
|
||||||
/>
|
{:else}
|
||||||
{:else}
|
<Body size="S"><i>No relationships configured.</i></Body>
|
||||||
<Body size="S"><i>No relationships configured.</i></Body>
|
|
||||||
{/if}
|
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
<script>
|
<script>
|
||||||
import { Body } from "@budibase/bbui"
|
import { Body, notifications } from "@budibase/bbui"
|
||||||
import { onMount } from "svelte"
|
import { onMount } from "svelte"
|
||||||
import api from "builderStore/api"
|
import { API } from "api"
|
||||||
import ICONS from "../icons"
|
import ICONS from "../icons"
|
||||||
|
|
||||||
export let integration = {}
|
export let integration = {}
|
||||||
|
@ -9,14 +9,17 @@
|
||||||
const INTERNAL = "BUDIBASE"
|
const INTERNAL = "BUDIBASE"
|
||||||
|
|
||||||
async function fetchIntegrations() {
|
async function fetchIntegrations() {
|
||||||
const response = await api.get("/api/integrations")
|
let otherIntegrations
|
||||||
const json = await response.json()
|
try {
|
||||||
|
otherIntegrations = await API.getIntegrations()
|
||||||
|
} catch (error) {
|
||||||
|
otherIntegrations = {}
|
||||||
|
notifications.error("Error getting integrations")
|
||||||
|
}
|
||||||
integrations = {
|
integrations = {
|
||||||
[INTERNAL]: { datasource: {}, name: "INTERNAL/CSV" },
|
[INTERNAL]: { datasource: {}, name: "INTERNAL/CSV" },
|
||||||
...json,
|
...otherIntegrations,
|
||||||
}
|
}
|
||||||
return json
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function selectIntegration(integrationType) {
|
function selectIntegration(integrationType) {
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
import { Table, Modal, Layout, ActionButton } from "@budibase/bbui"
|
import { Table, Modal, Layout, ActionButton } from "@budibase/bbui"
|
||||||
import AuthTypeRenderer from "./AuthTypeRenderer.svelte"
|
import AuthTypeRenderer from "./AuthTypeRenderer.svelte"
|
||||||
import RestAuthenticationModal from "./RestAuthenticationModal.svelte"
|
import RestAuthenticationModal from "./RestAuthenticationModal.svelte"
|
||||||
import { uuid } from "builderStore/uuid"
|
import { Helpers } from "@budibase/bbui"
|
||||||
|
|
||||||
export let configs = []
|
export let configs = []
|
||||||
|
|
||||||
|
@ -29,7 +29,7 @@
|
||||||
return c
|
return c
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
config._id = uuid()
|
config._id = Helpers.uuid()
|
||||||
configs = [...configs, config]
|
configs = [...configs, config]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,8 +1,15 @@
|
||||||
<script>
|
<script>
|
||||||
import { ModalContent, Modal, Body, Layout, Detail } from "@budibase/bbui"
|
import {
|
||||||
|
ModalContent,
|
||||||
|
Modal,
|
||||||
|
Body,
|
||||||
|
Layout,
|
||||||
|
Detail,
|
||||||
|
notifications,
|
||||||
|
} from "@budibase/bbui"
|
||||||
import { onMount } from "svelte"
|
import { onMount } from "svelte"
|
||||||
import ICONS from "../icons"
|
import ICONS from "../icons"
|
||||||
import api from "builderStore/api"
|
import { API } from "api"
|
||||||
import { IntegrationNames, IntegrationTypes } from "constants/backend"
|
import { IntegrationNames, IntegrationTypes } from "constants/backend"
|
||||||
import CreateTableModal from "components/backend/TableNavigator/modals/CreateTableModal.svelte"
|
import CreateTableModal from "components/backend/TableNavigator/modals/CreateTableModal.svelte"
|
||||||
import DatasourceConfigModal from "components/backend/DatasourceNavigator/modals/DatasourceConfigModal.svelte"
|
import DatasourceConfigModal from "components/backend/DatasourceNavigator/modals/DatasourceConfigModal.svelte"
|
||||||
|
@ -12,7 +19,7 @@
|
||||||
import ImportRestQueriesModal from "./ImportRestQueriesModal.svelte"
|
import ImportRestQueriesModal from "./ImportRestQueriesModal.svelte"
|
||||||
|
|
||||||
export let modal
|
export let modal
|
||||||
let integrations = []
|
let integrations = {}
|
||||||
let integration = {}
|
let integration = {}
|
||||||
let internalTableModal
|
let internalTableModal
|
||||||
let externalDatasourceModal
|
let externalDatasourceModal
|
||||||
|
@ -57,22 +64,32 @@
|
||||||
externalDatasourceModal.hide()
|
externalDatasourceModal.hide()
|
||||||
internalTableModal.show()
|
internalTableModal.show()
|
||||||
} else if (integration.type === IntegrationTypes.REST) {
|
} else if (integration.type === IntegrationTypes.REST) {
|
||||||
// skip modal for rest, create straight away
|
try {
|
||||||
const resp = await createRestDatasource(integration)
|
// Skip modal for rest, create straight away
|
||||||
$goto(`./datasource/${resp._id}`)
|
const resp = await createRestDatasource(integration)
|
||||||
|
$goto(`./datasource/${resp._id}`)
|
||||||
|
} catch (error) {
|
||||||
|
notifications.error("Error creating datasource")
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
externalDatasourceModal.show()
|
externalDatasourceModal.show()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function fetchIntegrations() {
|
async function fetchIntegrations() {
|
||||||
const response = await api.get("/api/integrations")
|
let newIntegrations = {
|
||||||
const json = await response.json()
|
|
||||||
integrations = {
|
|
||||||
[IntegrationTypes.INTERNAL]: { datasource: {}, name: "INTERNAL/CSV" },
|
[IntegrationTypes.INTERNAL]: { datasource: {}, name: "INTERNAL/CSV" },
|
||||||
...json,
|
|
||||||
}
|
}
|
||||||
return json
|
try {
|
||||||
|
const integrationList = await API.getIntegrations()
|
||||||
|
newIntegrations = {
|
||||||
|
...newIntegrations,
|
||||||
|
...integrationList,
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
notifications.error("Error fetching integrations")
|
||||||
|
}
|
||||||
|
integrations = newIntegrations
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
|
@ -20,7 +20,7 @@
|
||||||
$goto(`./datasource/${resp._id}`)
|
$goto(`./datasource/${resp._id}`)
|
||||||
notifications.success(`Datasource updated successfully.`)
|
notifications.success(`Datasource updated successfully.`)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
notifications.error(`Error saving datasource: ${err}`)
|
notifications.error("Error saving datasource")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -79,8 +79,8 @@
|
||||||
})
|
})
|
||||||
|
|
||||||
return true
|
return true
|
||||||
} catch (err) {
|
} catch (error) {
|
||||||
notifications.error(`Error importing: ${err}`)
|
notifications.error("Error importing queries")
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,24 +12,28 @@
|
||||||
let updateDatasourceDialog
|
let updateDatasourceDialog
|
||||||
|
|
||||||
async function deleteDatasource() {
|
async function deleteDatasource() {
|
||||||
let wasSelectedSource = $datasources.selected
|
try {
|
||||||
if (!wasSelectedSource && $queries.selected) {
|
let wasSelectedSource = $datasources.selected
|
||||||
const queryId = $queries.selected
|
if (!wasSelectedSource && $queries.selected) {
|
||||||
wasSelectedSource = $datasources.list.find(ds =>
|
const queryId = $queries.selected
|
||||||
queryId.includes(ds._id)
|
wasSelectedSource = $datasources.list.find(ds =>
|
||||||
)?._id
|
queryId.includes(ds._id)
|
||||||
}
|
)?._id
|
||||||
const wasSelectedTable = $tables.selected
|
}
|
||||||
await datasources.delete(datasource)
|
const wasSelectedTable = $tables.selected
|
||||||
notifications.success("Datasource deleted")
|
await datasources.delete(datasource)
|
||||||
// navigate to first index page if the source you are deleting is selected
|
notifications.success("Datasource deleted")
|
||||||
const entities = Object.values(datasource?.entities || {})
|
// Navigate to first index page if the source you are deleting is selected
|
||||||
if (
|
const entities = Object.values(datasource?.entities || {})
|
||||||
wasSelectedSource === datasource._id ||
|
if (
|
||||||
(entities &&
|
wasSelectedSource === datasource._id ||
|
||||||
entities.find(entity => entity._id === wasSelectedTable?._id))
|
(entities &&
|
||||||
) {
|
entities.find(entity => entity._id === wasSelectedTable?._id))
|
||||||
$goto("./datasource")
|
) {
|
||||||
|
$goto("./datasource")
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
notifications.error("Error deleting datasource")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -10,26 +10,30 @@
|
||||||
let confirmDeleteDialog
|
let confirmDeleteDialog
|
||||||
|
|
||||||
async function deleteQuery() {
|
async function deleteQuery() {
|
||||||
const wasSelectedQuery = $queries.selected
|
try {
|
||||||
// need to calculate this before the query is deleted
|
const wasSelectedQuery = $queries.selected
|
||||||
const navigateToDatasource = wasSelectedQuery === query._id
|
// need to calculate this before the query is deleted
|
||||||
|
const navigateToDatasource = wasSelectedQuery === query._id
|
||||||
|
|
||||||
await queries.delete(query)
|
await queries.delete(query)
|
||||||
await datasources.fetch()
|
await datasources.fetch()
|
||||||
|
|
||||||
if (navigateToDatasource) {
|
if (navigateToDatasource) {
|
||||||
await datasources.select(query.datasourceId)
|
await datasources.select(query.datasourceId)
|
||||||
$goto(`./datasource/${query.datasourceId}`)
|
$goto(`./datasource/${query.datasourceId}`)
|
||||||
|
}
|
||||||
|
notifications.success("Query deleted")
|
||||||
|
} catch (error) {
|
||||||
|
notifications.error("Error deleting query")
|
||||||
}
|
}
|
||||||
notifications.success("Query deleted")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function duplicateQuery() {
|
async function duplicateQuery() {
|
||||||
try {
|
try {
|
||||||
const newQuery = await queries.duplicate(query)
|
const newQuery = await queries.duplicate(query)
|
||||||
onClickQuery(newQuery)
|
onClickQuery(newQuery)
|
||||||
} catch (e) {
|
} catch (error) {
|
||||||
notifications.error(e.message)
|
notifications.error("Error duplicating query")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -9,7 +9,7 @@
|
||||||
Body,
|
Body,
|
||||||
} from "@budibase/bbui"
|
} from "@budibase/bbui"
|
||||||
import { tables } from "stores/backend"
|
import { tables } from "stores/backend"
|
||||||
import { uuid } from "builderStore/uuid"
|
import { Helpers } from "@budibase/bbui"
|
||||||
import { writable } from "svelte/store"
|
import { writable } from "svelte/store"
|
||||||
|
|
||||||
export let save
|
export let save
|
||||||
|
@ -22,6 +22,10 @@
|
||||||
|
|
||||||
let originalFromName = fromRelationship.name,
|
let originalFromName = fromRelationship.name,
|
||||||
originalToName = toRelationship.name
|
originalToName = toRelationship.name
|
||||||
|
let fromTable, toTable, through, linkTable, tableOptions
|
||||||
|
let isManyToMany, isManyToOne, relationshipTypes
|
||||||
|
let errors, valid
|
||||||
|
let currentTables = {}
|
||||||
|
|
||||||
if (fromRelationship && !fromRelationship.relationshipType) {
|
if (fromRelationship && !fromRelationship.relationshipType) {
|
||||||
fromRelationship.relationshipType = RelationshipTypes.MANY_TO_ONE
|
fromRelationship.relationshipType = RelationshipTypes.MANY_TO_ONE
|
||||||
|
@ -41,61 +45,52 @@
|
||||||
|
|
||||||
const touched = writable({})
|
const touched = writable({})
|
||||||
|
|
||||||
function checkForErrors(
|
function checkForErrors(fromRelate, toRelate) {
|
||||||
fromTable,
|
|
||||||
toTable,
|
|
||||||
throughTable,
|
|
||||||
fromRelate,
|
|
||||||
toRelate
|
|
||||||
) {
|
|
||||||
const isMany =
|
const isMany =
|
||||||
fromRelate.relationshipType === RelationshipTypes.MANY_TO_MANY
|
fromRelate.relationshipType === RelationshipTypes.MANY_TO_MANY
|
||||||
const tableNotSet = "Please specify a table"
|
const tableNotSet = "Please specify a table"
|
||||||
const errors = {}
|
const errObj = {}
|
||||||
if ($touched.from && !fromTable) {
|
if ($touched.from && !fromTable) {
|
||||||
errors.from = tableNotSet
|
errObj.from = tableNotSet
|
||||||
}
|
}
|
||||||
if ($touched.to && !toTable) {
|
if ($touched.to && !toTable) {
|
||||||
errors.to = tableNotSet
|
errObj.to = tableNotSet
|
||||||
}
|
}
|
||||||
if ($touched.through && isMany && !fromRelate.through) {
|
if ($touched.through && isMany && !fromRelate.through) {
|
||||||
errors.through = tableNotSet
|
errObj.through = tableNotSet
|
||||||
}
|
}
|
||||||
if ($touched.foreign && !isMany && !fromRelate.fieldName) {
|
if ($touched.foreign && !isMany && !fromRelate.fieldName) {
|
||||||
errors.foreign = "Please pick the foreign key"
|
errObj.foreign = "Please pick the foreign key"
|
||||||
}
|
}
|
||||||
const colNotSet = "Please specify a column name"
|
const colNotSet = "Please specify a column name"
|
||||||
if ($touched.fromCol && !fromRelate.name) {
|
if ($touched.fromCol && !fromRelate.name) {
|
||||||
errors.fromCol = colNotSet
|
errObj.fromCol = colNotSet
|
||||||
}
|
}
|
||||||
if ($touched.toCol && !toRelate.name) {
|
if ($touched.toCol && !toRelate.name) {
|
||||||
errors.toCol = colNotSet
|
errObj.toCol = colNotSet
|
||||||
}
|
}
|
||||||
if ($touched.primary && !fromPrimary) {
|
if ($touched.primary && !fromPrimary) {
|
||||||
errors.primary = "Please pick the primary key"
|
errObj.primary = "Please pick the primary key"
|
||||||
}
|
}
|
||||||
// currently don't support relationships back onto the table itself, needs to relate out
|
// currently don't support relationships back onto the table itself, needs to relate out
|
||||||
const tableError = "From/to/through tables must be different"
|
const tableError = "From/to/through tables must be different"
|
||||||
if (fromTable && (fromTable === toTable || fromTable === throughTable)) {
|
if (fromTable && (fromTable === toTable || fromTable === through)) {
|
||||||
errors.from = tableError
|
errObj.from = tableError
|
||||||
}
|
}
|
||||||
if (toTable && (toTable === fromTable || toTable === throughTable)) {
|
if (toTable && (toTable === fromTable || toTable === through)) {
|
||||||
errors.to = tableError
|
errObj.to = tableError
|
||||||
}
|
}
|
||||||
if (
|
if (through && (through === fromTable || through === toTable)) {
|
||||||
throughTable &&
|
errObj.through = tableError
|
||||||
(throughTable === fromTable || throughTable === toTable)
|
|
||||||
) {
|
|
||||||
errors.through = tableError
|
|
||||||
}
|
}
|
||||||
const colError = "Column name cannot be an existing column"
|
const colError = "Column name cannot be an existing column"
|
||||||
if (inSchema(fromTable, fromRelate.name, originalFromName)) {
|
if (inSchema(fromTable, fromRelate.name, originalFromName)) {
|
||||||
errors.fromCol = colError
|
errObj.fromCol = colError
|
||||||
}
|
}
|
||||||
if (inSchema(toTable, toRelate.name, originalToName)) {
|
if (inSchema(toTable, toRelate.name, originalToName)) {
|
||||||
errors.toCol = colError
|
errObj.toCol = colError
|
||||||
}
|
}
|
||||||
return errors
|
errors = errObj
|
||||||
}
|
}
|
||||||
|
|
||||||
let fromPrimary
|
let fromPrimary
|
||||||
|
@ -115,13 +110,7 @@
|
||||||
$: fromTable = plusTables.find(table => table._id === toRelationship?.tableId)
|
$: fromTable = plusTables.find(table => table._id === toRelationship?.tableId)
|
||||||
$: toTable = plusTables.find(table => table._id === fromRelationship?.tableId)
|
$: toTable = plusTables.find(table => table._id === fromRelationship?.tableId)
|
||||||
$: through = plusTables.find(table => table._id === fromRelationship?.through)
|
$: through = plusTables.find(table => table._id === fromRelationship?.through)
|
||||||
$: errors = checkForErrors(
|
$: checkForErrors(fromRelationship, toRelationship)
|
||||||
fromTable,
|
|
||||||
toTable,
|
|
||||||
through,
|
|
||||||
fromRelationship,
|
|
||||||
toRelationship
|
|
||||||
)
|
|
||||||
$: valid =
|
$: valid =
|
||||||
Object.keys(errors).length === 0 && Object.keys($touched).length !== 0
|
Object.keys(errors).length === 0 && Object.keys($touched).length !== 0
|
||||||
$: linkTable = through || toTable
|
$: linkTable = through || toTable
|
||||||
|
@ -151,7 +140,7 @@
|
||||||
const manyToMany =
|
const manyToMany =
|
||||||
fromRelationship.relationshipType === RelationshipTypes.MANY_TO_MANY
|
fromRelationship.relationshipType === RelationshipTypes.MANY_TO_MANY
|
||||||
// main is simply used to know this is the side the user configured it from
|
// main is simply used to know this is the side the user configured it from
|
||||||
const id = uuid()
|
const id = Helpers.uuid()
|
||||||
if (!manyToMany) {
|
if (!manyToMany) {
|
||||||
delete fromRelationship.through
|
delete fromRelationship.through
|
||||||
delete toRelationship.through
|
delete toRelationship.through
|
||||||
|
@ -239,19 +228,19 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
function tableChanged(fromTbl, toTbl) {
|
function tableChanged(fromTbl, toTbl) {
|
||||||
|
if (
|
||||||
|
(currentTables?.from?._id === fromTbl?._id &&
|
||||||
|
currentTables?.to?._id === toTbl?._id) ||
|
||||||
|
originalFromName ||
|
||||||
|
originalToName
|
||||||
|
) {
|
||||||
|
return
|
||||||
|
}
|
||||||
fromRelationship.name = toTbl?.name || ""
|
fromRelationship.name = toTbl?.name || ""
|
||||||
errors.fromCol = ""
|
errors.fromCol = ""
|
||||||
toRelationship.name = fromTbl?.name || ""
|
toRelationship.name = fromTbl?.name || ""
|
||||||
errors.toCol = ""
|
errors.toCol = ""
|
||||||
if (toTbl || fromTbl) {
|
currentTables = { from: fromTbl, to: toTbl }
|
||||||
checkForErrors(
|
|
||||||
fromTable,
|
|
||||||
toTable,
|
|
||||||
through,
|
|
||||||
fromRelationship,
|
|
||||||
toRelationship
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
<script>
|
<script>
|
||||||
import { Select, InlineAlert, notifications } from "@budibase/bbui"
|
import { Select, InlineAlert, notifications } from "@budibase/bbui"
|
||||||
import { FIELDS } from "constants/backend"
|
import { FIELDS } from "constants/backend"
|
||||||
import api from "builderStore/api"
|
import { API } from "api"
|
||||||
|
|
||||||
const BYTES_IN_MB = 1000000
|
const BYTES_IN_MB = 1000000
|
||||||
const FILE_SIZE_LIMIT = BYTES_IN_MB * 5
|
const FILE_SIZE_LIMIT = BYTES_IN_MB * 5
|
||||||
|
@ -50,28 +50,26 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
async function validateCSV() {
|
async function validateCSV() {
|
||||||
const response = await api.post("/api/tables/csv/validate", {
|
try {
|
||||||
csvString,
|
const parseResult = await API.validateTableCSV({
|
||||||
schema: schema || {},
|
csvString,
|
||||||
tableId: existingTableId,
|
schema: schema || {},
|
||||||
})
|
tableId: existingTableId,
|
||||||
|
})
|
||||||
|
schema = parseResult?.schema
|
||||||
|
fields = Object.keys(schema || {}).filter(
|
||||||
|
key => schema[key].type !== "omit"
|
||||||
|
)
|
||||||
|
|
||||||
const parseResult = await response.json()
|
// Check primary display is valid
|
||||||
schema = parseResult && parseResult.schema
|
if (!primaryDisplay || fields.indexOf(primaryDisplay) === -1) {
|
||||||
fields = Object.keys(schema || {}).filter(
|
primaryDisplay = fields[0]
|
||||||
key => schema[key].type !== "omit"
|
}
|
||||||
)
|
|
||||||
|
|
||||||
// Check primary display is valid
|
hasValidated = true
|
||||||
if (!primaryDisplay || fields.indexOf(primaryDisplay) === -1) {
|
} catch (error) {
|
||||||
primaryDisplay = fields[0]
|
|
||||||
}
|
|
||||||
|
|
||||||
if (response.status !== 200) {
|
|
||||||
notifications.error("CSV Invalid, please try another CSV file")
|
notifications.error("CSV Invalid, please try another CSV file")
|
||||||
return []
|
|
||||||
}
|
}
|
||||||
hasValidated = true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleFile(evt) {
|
async function handleFile(evt) {
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue