Merge branch 'cypress-testing' of https://github.com/Budibase/budibase into cypress-testing
This commit is contained in:
commit
c95f7da55e
280 changed files with 9469 additions and 5975 deletions
11
README.md
11
README.md
|
@ -104,12 +104,14 @@ Budibase is made to scale. With Budibase, you can self-host on your own infrastr
|
|||
|
||||
## 🏁 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.
|
||||
|
||||
### [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 />
|
||||
|
@ -201,9 +203,6 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d
|
|||
<td align="center"><a href="https://github.com/seoulaja"><img src="https://avatars.githubusercontent.com/u/15101654?v=4?s=100" width="100px;" alt=""/><br /><sub><b>seoulaja</b></sub></a><br /><a href="#translation-seoulaja" title="Translation">🌍</a></td>
|
||||
<td align="center"><a href="https://github.com/mslourens"><img src="https://avatars.githubusercontent.com/u/1907152?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Maurits Lourens</b></sub></a><br /><a href="https://github.com/Budibase/budibase/commits?author=mslourens" title="Tests">⚠️</a> <a href="https://github.com/Budibase/budibase/commits?author=mslourens" title="Code">💻</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center"><a href="https://github.com/Rory-Powell"><img src="https://avatars.githubusercontent.com/u/8755148?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Rory Powell</b></sub></a><br /><a href="#infra-Rory-Powell" title="Infrastructure (Hosting, Build-Tools, etc)">🚇</a> <a href="https://github.com/Budibase/budibase/commits?author=Rory-Powell" title="Tests">⚠️</a> <a href="https://github.com/Budibase/budibase/commits?author=Rory-Powell" title="Code">💻</a></td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<!-- markdownlint-restore -->
|
||||
|
|
|
@ -108,6 +108,8 @@ spec:
|
|||
value: {{ .Values.globals.accountPortalApiKey | quote }}
|
||||
- name: COOKIE_DOMAIN
|
||||
value: {{ .Values.globals.cookieDomain | quote }}
|
||||
- name: HTTP_MIGRATIONS
|
||||
value: {{ .Values.globals.httpMigrations | quote }}
|
||||
image: budibase/apps:{{ .Values.globals.appVersion }}
|
||||
imagePullPolicy: Always
|
||||
name: bbapps
|
||||
|
|
|
@ -99,6 +99,7 @@ globals:
|
|||
accountPortalApiKey: ""
|
||||
cookieDomain: ""
|
||||
platformUrl: ""
|
||||
httpMigrations: "0"
|
||||
|
||||
createSecrets: true # creates an internal API key, JWT secrets and redis password for you
|
||||
|
||||
|
|
214
i18n/README.jp.md
Normal file
214
i18n/README.jp.md
Normal file
|
@ -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.46-alpha.3",
|
||||
"version": "1.0.50-alpha.4",
|
||||
"npmClient": "yarn",
|
||||
"packages": [
|
||||
"packages/*"
|
||||
|
|
|
@ -36,10 +36,10 @@
|
|||
"dev:server": "lerna run --parallel dev:builder --concurrency 1 --scope @budibase/worker --scope @budibase/server",
|
||||
"test": "lerna run test",
|
||||
"lint:eslint": "eslint packages",
|
||||
"lint:prettier": "prettier --check \"packages/**/*.{js,svelte}\"",
|
||||
"lint:prettier": "prettier --check \"packages/**/*.{js,ts,svelte}\"",
|
||||
"lint": "yarn run lint:eslint && yarn run lint:prettier",
|
||||
"lint:fix:eslint": "eslint --fix packages",
|
||||
"lint:fix:prettier": "prettier --write \"packages/**/*.{js,svelte}\"",
|
||||
"lint:fix:prettier": "prettier --write \"packages/**/*.{js,ts,svelte}\"",
|
||||
"lint:fix:ts": "lerna run lint:fix",
|
||||
"lint:fix": "yarn run lint:fix:ts && yarn run lint:fix:prettier && yarn run lint:fix:eslint",
|
||||
"test:e2e": "lerna run cy:test",
|
||||
|
|
17
packages/backend-core/context.js
Normal file
17
packages/backend-core/context.js
Normal file
|
@ -0,0 +1,17 @@
|
|||
const {
|
||||
getAppDB,
|
||||
getDevAppDB,
|
||||
getProdAppDB,
|
||||
getAppId,
|
||||
updateAppId,
|
||||
doInAppContext,
|
||||
} = require("./src/context")
|
||||
|
||||
module.exports = {
|
||||
getAppDB,
|
||||
getDevAppDB,
|
||||
getProdAppDB,
|
||||
getAppId,
|
||||
updateAppId,
|
||||
doInAppContext,
|
||||
}
|
|
@ -1,4 +1,6 @@
|
|||
module.exports = {
|
||||
...require("./src/db/utils"),
|
||||
...require("./src/db/constants"),
|
||||
...require("./src/db"),
|
||||
...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",
|
||||
"version": "1.0.46-alpha.3",
|
||||
"version": "1.0.50-alpha.4",
|
||||
"description": "Budibase backend core libraries used in server and worker",
|
||||
"main": "src/index.js",
|
||||
"author": "Budibase",
|
||||
|
|
|
@ -12,6 +12,8 @@ const {
|
|||
tenancy,
|
||||
appTenancy,
|
||||
authError,
|
||||
csrf,
|
||||
internalApi,
|
||||
} = require("./middleware")
|
||||
|
||||
// Strategies
|
||||
|
@ -42,4 +44,6 @@ module.exports = {
|
|||
buildAppTenancyMiddleware: appTenancy,
|
||||
auditLog,
|
||||
authError,
|
||||
buildCsrfMiddleware: csrf,
|
||||
internalApi,
|
||||
}
|
||||
|
|
|
@ -7,8 +7,8 @@ exports.Cookies = {
|
|||
CurrentApp: "budibase:currentapp",
|
||||
Auth: "budibase:auth",
|
||||
Init: "budibase:init",
|
||||
DatasourceAuth: "budibase:datasourceauth",
|
||||
OIDC_CONFIG: "budibase:oidc:config",
|
||||
RETURN_URL: "budibase:returnurl",
|
||||
}
|
||||
|
||||
exports.Headers = {
|
||||
|
@ -18,6 +18,7 @@ exports.Headers = {
|
|||
TYPE: "x-budibase-type",
|
||||
TENANT_ID: "x-budibase-tenant-id",
|
||||
TOKEN: "x-budibase-token",
|
||||
CSRF_TOKEN: "x-csrf-token",
|
||||
}
|
||||
|
||||
exports.GlobalRoles = {
|
||||
|
|
|
@ -4,8 +4,8 @@ const { newid } = require("../hashing")
|
|||
const REQUEST_ID_KEY = "requestId"
|
||||
|
||||
class FunctionContext {
|
||||
static getMiddleware(updateCtxFn = null) {
|
||||
const namespace = this.createNamespace()
|
||||
static getMiddleware(updateCtxFn = null, contextName = "session") {
|
||||
const namespace = this.createNamespace(contextName)
|
||||
|
||||
return async function (ctx, next) {
|
||||
await new Promise(
|
||||
|
@ -24,14 +24,14 @@ class FunctionContext {
|
|||
}
|
||||
}
|
||||
|
||||
static run(callback) {
|
||||
const namespace = this.createNamespace()
|
||||
static run(callback, contextName = "session") {
|
||||
const namespace = this.createNamespace(contextName)
|
||||
|
||||
return namespace.runAndReturn(callback)
|
||||
}
|
||||
|
||||
static setOnContext(key, value) {
|
||||
const namespace = this.createNamespace()
|
||||
static setOnContext(key, value, contextName = "session") {
|
||||
const namespace = this.createNamespace(contextName)
|
||||
namespace.set(key, value)
|
||||
}
|
||||
|
||||
|
@ -55,16 +55,16 @@ class FunctionContext {
|
|||
}
|
||||
}
|
||||
|
||||
static destroyNamespace() {
|
||||
static destroyNamespace(name = "session") {
|
||||
if (this._namespace) {
|
||||
cls.destroyNamespace("session")
|
||||
cls.destroyNamespace(name)
|
||||
this._namespace = null
|
||||
}
|
||||
}
|
||||
|
||||
static createNamespace() {
|
||||
static createNamespace(name = "session") {
|
||||
if (!this._namespace) {
|
||||
this._namespace = cls.createNamespace("session")
|
||||
this._namespace = cls.createNamespace(name)
|
||||
}
|
||||
return this._namespace
|
||||
}
|
|
@ -1,6 +1,6 @@
|
|||
const { getGlobalUserParams, getAllApps } = require("../db/utils")
|
||||
const { getDB, getCouch } = require("../db")
|
||||
const { getGlobalDB } = require("./tenancy")
|
||||
const { getGlobalDB } = require("../tenancy")
|
||||
const { StaticDatabases } = require("../db/constants")
|
||||
|
||||
const TENANT_DOC = StaticDatabases.PLATFORM_INFO.docs.tenants
|
195
packages/backend-core/src/context/index.js
Normal file
195
packages/backend-core/src/context/index.js
Normal file
|
@ -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
|
||||
|
|
46
packages/backend-core/src/db/conversions.js
Normal file
46
packages/backend-core/src/db/conversions.js
Normal file
|
@ -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 { DEFAULT_TENANT_ID, Configs } = require("../constants")
|
||||
const env = require("../environment")
|
||||
const { StaticDatabases, SEPARATOR, DocumentTypes } = require("./constants")
|
||||
const {
|
||||
StaticDatabases,
|
||||
SEPARATOR,
|
||||
DocumentTypes,
|
||||
APP_PREFIX,
|
||||
APP_DEV,
|
||||
} = require("./constants")
|
||||
const {
|
||||
getTenantId,
|
||||
getTenantIDFromAppID,
|
||||
|
@ -12,8 +18,13 @@ const fetch = require("node-fetch")
|
|||
const { getCouch } = require("./index")
|
||||
const { getAppMetadata } = require("../cache/appMetadata")
|
||||
const { checkSlashesInUrl } = require("../helpers")
|
||||
|
||||
const NO_APP_ERROR = "No app provided"
|
||||
const {
|
||||
isDevApp,
|
||||
isProdAppID,
|
||||
isDevAppID,
|
||||
getDevelopmentAppID,
|
||||
getProdAppID,
|
||||
} = require("./conversions")
|
||||
|
||||
const UNICODE_MAX = "\ufff0"
|
||||
|
||||
|
@ -24,10 +35,15 @@ exports.ViewNames = {
|
|||
exports.StaticDatabases = StaticDatabases
|
||||
|
||||
exports.DocumentTypes = DocumentTypes
|
||||
exports.APP_PREFIX = DocumentTypes.APP + SEPARATOR
|
||||
exports.APP_DEV = exports.APP_DEV_PREFIX = DocumentTypes.APP_DEV + SEPARATOR
|
||||
exports.APP_PREFIX = APP_PREFIX
|
||||
exports.APP_DEV = exports.APP_DEV_PREFIX = APP_DEV
|
||||
exports.SEPARATOR = SEPARATOR
|
||||
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
|
||||
|
@ -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.
|
||||
* @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)
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 = () => {
|
||||
if (!env.COUCH_DB_URL) return
|
||||
|
||||
|
@ -225,7 +197,7 @@ exports.getAllDbs = async () => {
|
|||
}
|
||||
let couchUrl = `${exports.getCouchUrl()}/_all_dbs`
|
||||
let tenantId = getTenantId()
|
||||
if (!env.MULTI_TENANCY || tenantId == DEFAULT_TENANT_ID) {
|
||||
if (!env.MULTI_TENANCY || tenantId === DEFAULT_TENANT_ID) {
|
||||
// just get all DBs when:
|
||||
// - single tenancy
|
||||
// - 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
|
||||
* 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.
|
||||
*/
|
||||
exports.getAllApps = async (CouchDB, { dev, all, idsOnly } = {}) => {
|
||||
exports.getAllApps = async ({ dev, all, idsOnly } = {}) => {
|
||||
const CouchDB = getCouch()
|
||||
let tenantId = getTenantId()
|
||||
if (!env.MULTI_TENANCY && !tenantId) {
|
||||
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.
|
||||
*/
|
||||
exports.getDeployedAppIDs = async CouchDB => {
|
||||
return (await exports.getAllApps(CouchDB, { idsOnly: true })).filter(
|
||||
exports.getProdAppIDs = async () => {
|
||||
return (await exports.getAllApps({ idsOnly: true })).filter(
|
||||
id => !exports.isDevAppID(id)
|
||||
)
|
||||
}
|
||||
|
@ -319,13 +291,14 @@ exports.getDeployedAppIDs = async CouchDB => {
|
|||
/**
|
||||
* Utility function for the inverse of above.
|
||||
*/
|
||||
exports.getDevAppIDs = async CouchDB => {
|
||||
return (await exports.getAllApps(CouchDB, { idsOnly: true })).filter(id =>
|
||||
exports.getDevAppIDs = async () => {
|
||||
return (await exports.getAllApps({ idsOnly: true })).filter(id =>
|
||||
exports.isDevAppID(id)
|
||||
)
|
||||
}
|
||||
|
||||
exports.dbExists = async (CouchDB, dbName) => {
|
||||
exports.dbExists = async dbName => {
|
||||
const CouchDB = getCouch()
|
||||
let exists = false
|
||||
try {
|
||||
const db = CouchDB(dbName, { skip_setup: true })
|
||||
|
|
|
@ -3,8 +3,9 @@ const {
|
|||
updateTenantId,
|
||||
isTenantIdSet,
|
||||
DEFAULT_TENANT_ID,
|
||||
updateAppId,
|
||||
} = require("../tenancy")
|
||||
const ContextFactory = require("../tenancy/FunctionContext")
|
||||
const ContextFactory = require("../context/FunctionContext")
|
||||
const { getTenantIDFromAppID } = require("../db/utils")
|
||||
|
||||
module.exports = () => {
|
||||
|
@ -21,5 +22,6 @@ module.exports = () => {
|
|||
const appId = ctx.appId ? ctx.appId : ctx.user ? ctx.user.appId : null
|
||||
const tenantId = getTenantIDFromAppID(appId) || DEFAULT_TENANT_ID
|
||||
updateTenantId(tenantId)
|
||||
updateAppId(appId)
|
||||
})
|
||||
}
|
||||
|
|
|
@ -60,6 +60,7 @@ module.exports = (
|
|||
} else {
|
||||
user = await getUser(userId, session.tenantId)
|
||||
}
|
||||
user.csrfToken = session.csrfToken
|
||||
delete user.password
|
||||
authenticated = true
|
||||
} catch (err) {
|
||||
|
|
78
packages/backend-core/src/middleware/csrf.js
Normal file
78
packages/backend-core/src/middleware/csrf.js
Normal file
|
@ -0,0 +1,78 @@
|
|||
const { Headers } = require("../constants")
|
||||
const { buildMatcherRegex, matches } = require("./matchers")
|
||||
|
||||
/**
|
||||
* GET, HEAD and OPTIONS methods are considered safe operations
|
||||
*
|
||||
* POST, PUT, PATCH, and DELETE methods, being state changing verbs,
|
||||
* should have a CSRF token attached to the request
|
||||
*/
|
||||
const EXCLUDED_METHODS = ["GET", "HEAD", "OPTIONS"]
|
||||
|
||||
/**
|
||||
* There are only three content type values that can be used in cross domain requests.
|
||||
* If any other value is used, e.g. application/json, the browser will first make a OPTIONS
|
||||
* request which will be protected by CORS.
|
||||
*/
|
||||
const INCLUDED_CONTENT_TYPES = [
|
||||
"application/x-www-form-urlencoded",
|
||||
"multipart/form-data",
|
||||
"text/plain",
|
||||
]
|
||||
|
||||
/**
|
||||
* Validate the CSRF token generated aganst the user session.
|
||||
* Compare the token with the x-csrf-token header.
|
||||
*
|
||||
* If the token is not found within the request or the value provided
|
||||
* does not match the value within the user session, the request is rejected.
|
||||
*
|
||||
* CSRF protection provided using the 'Synchronizer Token Pattern'
|
||||
* https://cheatsheetseries.owasp.org/cheatsheets/Cross-Site_Request_Forgery_Prevention_Cheat_Sheet.html#synchronizer-token-pattern
|
||||
*
|
||||
*/
|
||||
module.exports = (opts = { noCsrfPatterns: [] }) => {
|
||||
const noCsrfOptions = buildMatcherRegex(opts.noCsrfPatterns)
|
||||
return async (ctx, next) => {
|
||||
// don't apply for excluded paths
|
||||
const found = matches(ctx, noCsrfOptions)
|
||||
if (found) {
|
||||
return next()
|
||||
}
|
||||
|
||||
// don't apply for the excluded http methods
|
||||
if (EXCLUDED_METHODS.indexOf(ctx.method) !== -1) {
|
||||
return next()
|
||||
}
|
||||
|
||||
// don't apply when the content type isn't supported
|
||||
let contentType = ctx.get("content-type")
|
||||
? ctx.get("content-type").toLowerCase()
|
||||
: ""
|
||||
if (
|
||||
!INCLUDED_CONTENT_TYPES.filter(type => contentType.includes(type)).length
|
||||
) {
|
||||
return next()
|
||||
}
|
||||
|
||||
// don't apply csrf when the internal api key has been used
|
||||
if (ctx.internal) {
|
||||
return next()
|
||||
}
|
||||
|
||||
// apply csrf when there is a token in the session (new logins)
|
||||
// in future there should be a hard requirement that the token is present
|
||||
const userToken = ctx.user.csrfToken
|
||||
if (!userToken) {
|
||||
return next()
|
||||
}
|
||||
|
||||
// reject if no token in request or mismatch
|
||||
const requestToken = ctx.get(Headers.CSRF_TOKEN)
|
||||
if (!requestToken || requestToken !== userToken) {
|
||||
ctx.throw(403, "Invalid CSRF token")
|
||||
}
|
||||
|
||||
return next()
|
||||
}
|
||||
}
|
|
@ -7,6 +7,9 @@ const authenticated = require("./authenticated")
|
|||
const auditLog = require("./auditLog")
|
||||
const tenancy = require("./tenancy")
|
||||
const appTenancy = require("./appTenancy")
|
||||
const internalApi = require("./internalApi")
|
||||
const datasourceGoogle = require("./passport/datasource/google")
|
||||
const csrf = require("./csrf")
|
||||
|
||||
module.exports = {
|
||||
google,
|
||||
|
@ -18,4 +21,9 @@ module.exports = {
|
|||
tenancy,
|
||||
appTenancy,
|
||||
authError,
|
||||
internalApi,
|
||||
datasource: {
|
||||
google: datasourceGoogle,
|
||||
},
|
||||
csrf,
|
||||
}
|
||||
|
|
14
packages/backend-core/src/middleware/internalApi.js
Normal file
14
packages/backend-core/src/middleware/internalApi.js
Normal file
|
@ -0,0 +1,14 @@
|
|||
const env = require("../environment")
|
||||
const { Headers } = require("../constants")
|
||||
|
||||
/**
|
||||
* API Key only endpoint.
|
||||
*/
|
||||
module.exports = async (ctx, next) => {
|
||||
const apiKey = ctx.request.headers[Headers.API_KEY]
|
||||
if (apiKey !== env.INTERNAL_API_KEY) {
|
||||
ctx.throw(403, "Unauthorized")
|
||||
}
|
||||
|
||||
return next()
|
||||
}
|
|
@ -0,0 +1,76 @@
|
|||
const { getScopedConfig } = require("../../../db/utils")
|
||||
const { getGlobalDB } = require("../../../tenancy")
|
||||
const google = require("../google")
|
||||
const { Configs, Cookies } = require("../../../constants")
|
||||
const { clearCookie, getCookie } = require("../../../utils")
|
||||
const { getDB } = require("../../../db")
|
||||
|
||||
async function preAuth(passport, ctx, next) {
|
||||
const db = getGlobalDB()
|
||||
// get the relevant config
|
||||
const config = await getScopedConfig(db, {
|
||||
type: Configs.GOOGLE,
|
||||
workspace: ctx.query.workspace,
|
||||
})
|
||||
const publicConfig = await getScopedConfig(db, {
|
||||
type: Configs.SETTINGS,
|
||||
})
|
||||
let callbackUrl = `${publicConfig.platformUrl}/api/global/auth/datasource/google/callback`
|
||||
const strategy = await google.strategyFactory(config, callbackUrl)
|
||||
|
||||
if (!ctx.query.appId || !ctx.query.datasourceId) {
|
||||
ctx.throw(400, "appId and datasourceId query params not present.")
|
||||
}
|
||||
|
||||
return passport.authenticate(strategy, {
|
||||
scope: ["profile", "email", "https://www.googleapis.com/auth/spreadsheets"],
|
||||
accessType: "offline",
|
||||
prompt: "consent",
|
||||
})(ctx, next)
|
||||
}
|
||||
|
||||
async function postAuth(passport, ctx, next) {
|
||||
const db = getGlobalDB()
|
||||
|
||||
const config = await getScopedConfig(db, {
|
||||
type: Configs.GOOGLE,
|
||||
workspace: ctx.query.workspace,
|
||||
})
|
||||
|
||||
const publicConfig = await getScopedConfig(db, {
|
||||
type: Configs.SETTINGS,
|
||||
})
|
||||
|
||||
let callbackUrl = `${publicConfig.platformUrl}/api/global/auth/datasource/google/callback`
|
||||
const strategy = await google.strategyFactory(
|
||||
config,
|
||||
callbackUrl,
|
||||
(accessToken, refreshToken, profile, done) => {
|
||||
clearCookie(ctx, Cookies.DatasourceAuth)
|
||||
done(null, { accessToken, refreshToken })
|
||||
}
|
||||
)
|
||||
|
||||
const authStateCookie = getCookie(ctx, Cookies.DatasourceAuth)
|
||||
|
||||
return passport.authenticate(
|
||||
strategy,
|
||||
{ successRedirect: "/", failureRedirect: "/error" },
|
||||
async (err, tokens) => {
|
||||
// update the DB for the datasource with all the user info
|
||||
const db = getDB(authStateCookie.appId)
|
||||
const datasource = await db.get(authStateCookie.datasourceId)
|
||||
if (!datasource.config) {
|
||||
datasource.config = {}
|
||||
}
|
||||
datasource.config.auth = { type: "google", ...tokens }
|
||||
await db.put(datasource)
|
||||
ctx.redirect(
|
||||
`/builder/app/${authStateCookie.appId}/data/datasource/${authStateCookie.datasourceId}`
|
||||
)
|
||||
}
|
||||
)(ctx, next)
|
||||
}
|
||||
|
||||
exports.preAuth = preAuth
|
||||
exports.postAuth = postAuth
|
|
@ -1,5 +1,5 @@
|
|||
const { setTenantId } = require("../tenancy")
|
||||
const ContextFactory = require("../tenancy/FunctionContext")
|
||||
const ContextFactory = require("../context/FunctionContext")
|
||||
const { buildMatcherRegex, matches } = require("./matchers")
|
||||
|
||||
module.exports = (
|
||||
|
|
|
@ -1,20 +1,17 @@
|
|||
const { DEFAULT_TENANT_ID } = require("../constants")
|
||||
const { DocumentTypes } = require("../db/constants")
|
||||
const { getGlobalDB, getTenantId } = require("../tenancy")
|
||||
const { getAllApps } = require("../db/utils")
|
||||
const environment = require("../environment")
|
||||
const {
|
||||
doInTenant,
|
||||
getTenantIds,
|
||||
getGlobalDBName,
|
||||
getTenantId,
|
||||
} = require("../tenancy")
|
||||
|
||||
exports.MIGRATION_DBS = {
|
||||
GLOBAL_DB: "GLOBAL_DB",
|
||||
}
|
||||
|
||||
exports.MIGRATIONS = {
|
||||
USER_EMAIL_VIEW_CASING: "user_email_view_casing",
|
||||
QUOTAS_1: "quotas_1",
|
||||
}
|
||||
|
||||
const DB_LOOKUP = {
|
||||
[exports.MIGRATION_DBS.GLOBAL_DB]: [
|
||||
exports.MIGRATIONS.USER_EMAIL_VIEW_CASING,
|
||||
exports.MIGRATIONS.QUOTAS_1,
|
||||
],
|
||||
exports.MIGRATION_TYPES = {
|
||||
GLOBAL: "global", // run once, recorded in global db, global db is provided as an argument
|
||||
APP: "app", // run per app, recorded in each app db, app db is provided as an argument
|
||||
}
|
||||
|
||||
exports.getMigrationsDoc = async db => {
|
||||
|
@ -28,40 +25,90 @@ exports.getMigrationsDoc = async db => {
|
|||
}
|
||||
}
|
||||
|
||||
exports.migrateIfRequired = async (migrationDb, migrationName, migrateFn) => {
|
||||
const runMigration = async (CouchDB, migration, options = {}) => {
|
||||
const tenantId = getTenantId()
|
||||
try {
|
||||
let db
|
||||
if (migrationDb === exports.MIGRATION_DBS.GLOBAL_DB) {
|
||||
db = getGlobalDB()
|
||||
} else {
|
||||
throw new Error(`Unrecognised migration db [${migrationDb}]`)
|
||||
}
|
||||
const migrationType = migration.type
|
||||
const migrationName = migration.name
|
||||
|
||||
if (!DB_LOOKUP[migrationDb].includes(migrationName)) {
|
||||
throw new Error(
|
||||
`Unrecognised migration name [${migrationName}] for db [${migrationDb}]`
|
||||
)
|
||||
}
|
||||
|
||||
const doc = await exports.getMigrationsDoc(db)
|
||||
// exit if the migration has been performed
|
||||
if (doc[migrationName]) {
|
||||
return
|
||||
}
|
||||
|
||||
console.log(`[Tenant: ${tenantId}] Performing migration: ${migrationName}`)
|
||||
await migrateFn()
|
||||
console.log(`[Tenant: ${tenantId}] Migration complete: ${migrationName}`)
|
||||
|
||||
// mark as complete
|
||||
doc[migrationName] = Date.now()
|
||||
await db.put(doc)
|
||||
} catch (err) {
|
||||
console.error(
|
||||
`[Tenant: ${tenantId}] Error performing migration: ${migrationName}: `,
|
||||
err
|
||||
// get the db to store the migration in
|
||||
let dbNames
|
||||
if (migrationType === exports.MIGRATION_TYPES.GLOBAL) {
|
||||
dbNames = [getGlobalDBName()]
|
||||
} else if (migrationType === exports.MIGRATION_TYPES.APP) {
|
||||
const apps = await getAllApps(CouchDB, migration.opts)
|
||||
dbNames = apps.map(app => app.appId)
|
||||
} else {
|
||||
throw new Error(
|
||||
`[Tenant: ${tenantId}] Unrecognised migration type [${migrationType}]`
|
||||
)
|
||||
throw err
|
||||
}
|
||||
|
||||
// run the migration against each db
|
||||
for (const dbName of dbNames) {
|
||||
const db = new CouchDB(dbName)
|
||||
try {
|
||||
const doc = await exports.getMigrationsDoc(db)
|
||||
|
||||
// exit if the migration has been performed already
|
||||
if (doc[migrationName]) {
|
||||
if (
|
||||
options.force &&
|
||||
options.force[migrationType] &&
|
||||
options.force[migrationType].includes(migrationName)
|
||||
) {
|
||||
console.log(
|
||||
`[Tenant: ${tenantId}] [Migration: ${migrationName}] [DB: ${dbName}] Forcing`
|
||||
)
|
||||
} else {
|
||||
// the migration has already been performed
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
console.log(
|
||||
`[Tenant: ${tenantId}] [Migration: ${migrationName}] [DB: ${dbName}] Running`
|
||||
)
|
||||
// run the migration with tenant context
|
||||
await migration.fn(db)
|
||||
console.log(
|
||||
`[Tenant: ${tenantId}] [Migration: ${migrationName}] [DB: ${dbName}] Complete`
|
||||
)
|
||||
|
||||
// mark as complete
|
||||
doc[migrationName] = Date.now()
|
||||
await db.put(doc)
|
||||
} catch (err) {
|
||||
console.error(
|
||||
`[Tenant: ${tenantId}] [Migration: ${migrationName}] [DB: ${dbName}] Error: `,
|
||||
err
|
||||
)
|
||||
throw err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
exports.runMigrations = async (CouchDB, migrations, options = {}) => {
|
||||
console.log("Running migrations")
|
||||
let tenantIds
|
||||
if (environment.MULTI_TENANCY) {
|
||||
if (!options.tenantIds || !options.tenantIds.length) {
|
||||
// run for all tenants
|
||||
tenantIds = await getTenantIds()
|
||||
}
|
||||
} else {
|
||||
// single tenancy
|
||||
tenantIds = [DEFAULT_TENANT_ID]
|
||||
}
|
||||
|
||||
// for all tenants
|
||||
for (const tenantId of tenantIds) {
|
||||
// for all migrations
|
||||
for (const migration of migrations) {
|
||||
// run the migration
|
||||
await doInTenant(tenantId, () =>
|
||||
runMigration(CouchDB, migration, options)
|
||||
)
|
||||
}
|
||||
}
|
||||
console.log("Migrations complete")
|
||||
}
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
exports[`migrations should match snapshot 1`] = `
|
||||
Object {
|
||||
"_id": "migrations",
|
||||
"_rev": "1-af6c272fe081efafecd2ea49a8fcbb40",
|
||||
"user_email_view_casing": 1487076708000,
|
||||
"_rev": "1-6277abc4e3db950221768e5a2618a059",
|
||||
"test": 1487076708000,
|
||||
}
|
||||
`;
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
require("../../tests/utilities/dbConfig")
|
||||
|
||||
const { migrateIfRequired, MIGRATION_DBS, MIGRATIONS, getMigrationsDoc } = require("../index")
|
||||
const database = require("../../db")
|
||||
const { runMigrations, getMigrationsDoc } = require("../index")
|
||||
const CouchDB = require("../../db").getCouch()
|
||||
const {
|
||||
StaticDatabases,
|
||||
} = require("../../db/utils")
|
||||
|
@ -13,8 +13,14 @@ describe("migrations", () => {
|
|||
|
||||
const migrationFunction = jest.fn()
|
||||
|
||||
const MIGRATIONS = [{
|
||||
type: "global",
|
||||
name: "test",
|
||||
fn: migrationFunction
|
||||
}]
|
||||
|
||||
beforeEach(() => {
|
||||
db = database.getDB(StaticDatabases.GLOBAL.name)
|
||||
db = new CouchDB(StaticDatabases.GLOBAL.name)
|
||||
})
|
||||
|
||||
afterEach(async () => {
|
||||
|
@ -22,39 +28,29 @@ describe("migrations", () => {
|
|||
await db.destroy()
|
||||
})
|
||||
|
||||
const validMigration = () => {
|
||||
return migrateIfRequired(MIGRATION_DBS.GLOBAL_DB, MIGRATIONS.USER_EMAIL_VIEW_CASING, migrationFunction)
|
||||
const migrate = () => {
|
||||
return runMigrations(CouchDB, MIGRATIONS)
|
||||
}
|
||||
|
||||
it("should run a new migration", async () => {
|
||||
await validMigration()
|
||||
await migrate()
|
||||
expect(migrationFunction).toHaveBeenCalled()
|
||||
const doc = await getMigrationsDoc(db)
|
||||
expect(doc.test).toBeDefined()
|
||||
})
|
||||
|
||||
it("should match snapshot", async () => {
|
||||
await validMigration()
|
||||
await migrate()
|
||||
const doc = await getMigrationsDoc(db)
|
||||
expect(doc).toMatchSnapshot()
|
||||
})
|
||||
|
||||
it("should skip a previously run migration", async () => {
|
||||
await validMigration()
|
||||
await validMigration()
|
||||
await migrate()
|
||||
const previousMigrationTime = await getMigrationsDoc(db).test
|
||||
await migrate()
|
||||
const currentMigrationTime = await getMigrationsDoc(db).test
|
||||
expect(migrationFunction).toHaveBeenCalledTimes(1)
|
||||
expect(currentMigrationTime).toBe(previousMigrationTime)
|
||||
})
|
||||
|
||||
it("should reject an unknown migration name", async () => {
|
||||
expect(async () => {
|
||||
await migrateIfRequired(MIGRATION_DBS.GLOBAL_DB, "bogus_name", migrationFunction)
|
||||
}).rejects.toThrow()
|
||||
expect(migrationFunction).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it("should reject an unknown database name", async () => {
|
||||
expect(async () => {
|
||||
await migrateIfRequired("bogus_db", MIGRATIONS.USER_EMAIL_VIEW_CASING, migrationFunction)
|
||||
}).rejects.toThrow()
|
||||
expect(migrationFunction).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
})
|
|
@ -1,4 +1,3 @@
|
|||
const { getDB } = require("../db")
|
||||
const { cloneDeep } = require("lodash/fp")
|
||||
const { BUILTIN_PERMISSION_IDS } = require("./permissions")
|
||||
const {
|
||||
|
@ -7,6 +6,8 @@ const {
|
|||
DocumentTypes,
|
||||
SEPARATOR,
|
||||
} = require("../db/utils")
|
||||
const { getAppDB } = require("../context")
|
||||
const { getDB } = require("../db")
|
||||
|
||||
const BUILTIN_IDS = {
|
||||
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
|
||||
* 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.
|
||||
* @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) {
|
||||
return null
|
||||
}
|
||||
|
@ -128,7 +128,7 @@ exports.getRole = async (appId, roleId) => {
|
|||
)
|
||||
}
|
||||
try {
|
||||
const db = getDB(appId)
|
||||
const db = getAppDB()
|
||||
const dbRole = await db.get(exports.getDBRoleID(roleId))
|
||||
role = Object.assign(role, dbRole)
|
||||
// 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.
|
||||
*/
|
||||
async function getAllUserRoles(appId, userRoleId) {
|
||||
if (!userRoleId) {
|
||||
return [BUILTIN_IDS.BASIC]
|
||||
async function getAllUserRoles(userRoleId) {
|
||||
// admins have access to all roles
|
||||
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 roleIds = [userRoleId]
|
||||
// get all the inherited roles
|
||||
|
@ -159,7 +160,7 @@ async function getAllUserRoles(appId, userRoleId) {
|
|||
roleIds.indexOf(currentRole.inherits) === -1
|
||||
) {
|
||||
roleIds.push(currentRole.inherits)
|
||||
currentRole = await exports.getRole(appId, currentRole.inherits)
|
||||
currentRole = await exports.getRole(currentRole.inherits)
|
||||
roles.push(currentRole)
|
||||
}
|
||||
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
|
||||
* 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 {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
|
||||
* highest level of access and the last being the lowest level.
|
||||
*/
|
||||
exports.getUserRoleHierarchy = async (
|
||||
appId,
|
||||
userRoleId,
|
||||
opts = { idOnly: true }
|
||||
) => {
|
||||
exports.getUserRoleHierarchy = async (userRoleId, opts = { idOnly: true }) => {
|
||||
// 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
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
exports.getAllRoles = async appId => {
|
||||
const db = getDB(appId)
|
||||
const db = appId ? getDB(appId) : getAppDB()
|
||||
const body = await db.allDocs(
|
||||
getRoleParams(null, {
|
||||
include_docs: true,
|
||||
|
@ -218,19 +213,17 @@ exports.getAllRoles = async appId => {
|
|||
}
|
||||
|
||||
/**
|
||||
* This retrieves the required role/
|
||||
* @param appId
|
||||
* This retrieves the required role
|
||||
* @param permLevel
|
||||
* @param resourceId
|
||||
* @param subResourceId
|
||||
* @return {Promise<{permissions}|Object>}
|
||||
*/
|
||||
exports.getRequiredResourceRole = async (
|
||||
appId,
|
||||
permLevel,
|
||||
{ resourceId, subResourceId }
|
||||
) => {
|
||||
const roles = await exports.getAllRoles(appId)
|
||||
const roles = await exports.getAllRoles()
|
||||
let main = [],
|
||||
sub = []
|
||||
for (let role of roles) {
|
||||
|
@ -251,8 +244,7 @@ exports.getRequiredResourceRole = async (
|
|||
}
|
||||
|
||||
class AccessController {
|
||||
constructor(appId) {
|
||||
this.appId = appId
|
||||
constructor() {
|
||||
this.userHierarchies = {}
|
||||
}
|
||||
|
||||
|
@ -270,7 +262,7 @@ class AccessController {
|
|||
}
|
||||
let roleIds = this.userHierarchies[userRoleId]
|
||||
if (!roleIds) {
|
||||
roleIds = await exports.getUserRoleHierarchy(this.appId, userRoleId)
|
||||
roleIds = await exports.getUserRoleHierarchy(userRoleId)
|
||||
this.userHierarchies[userRoleId] = roleIds
|
||||
}
|
||||
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
const redis = require("../redis/authRedis")
|
||||
const { v4: uuidv4 } = require("uuid")
|
||||
|
||||
// a week in seconds
|
||||
const EXPIRY_SECONDS = 86400 * 7
|
||||
|
@ -16,6 +17,9 @@ function makeSessionID(userId, sessionId) {
|
|||
exports.createASession = async (userId, session) => {
|
||||
const client = await redis.getSessionClient()
|
||||
const sessionId = session.sessionId
|
||||
if (!session.csrfToken) {
|
||||
session.csrfToken = uuidv4()
|
||||
}
|
||||
session = {
|
||||
createdAt: new Date().toISOString(),
|
||||
lastAccessedAt: new Date().toISOString(),
|
||||
|
|
|
@ -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 = {
|
||||
...require("./context"),
|
||||
...require("../context"),
|
||||
...require("./tenancy"),
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
const { getDB } = require("../db")
|
||||
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 TENANT_DOC = StaticDatabases.PLATFORM_INFO.docs.tenants
|
||||
|
@ -148,3 +148,15 @@ exports.isUserInAppTenant = (appId, user = null) => {
|
|||
const tenantId = exports.getTenantIDFromAppID(appId) || DEFAULT_TENANT_ID
|
||||
return tenantId === userTenantId
|
||||
}
|
||||
|
||||
exports.getTenantIds = async () => {
|
||||
const db = getDB(PLATFORM_INFO_DB)
|
||||
let tenants
|
||||
try {
|
||||
tenants = await db.get(TENANT_DOC)
|
||||
} catch (err) {
|
||||
// if theres an error the doc doesn't exist, no tenants exist
|
||||
return []
|
||||
}
|
||||
return (tenants && tenants.tenantIds) || []
|
||||
}
|
||||
|
|
|
@ -20,9 +20,6 @@ const { hash } = require("./hashing")
|
|||
const userCache = require("./cache/user")
|
||||
const env = require("./environment")
|
||||
const { getUserSessions, invalidateSessions } = require("./security/sessions")
|
||||
const { migrateIfRequired } = require("./migrations")
|
||||
const { USER_EMAIL_VIEW_CASING } = require("./migrations").MIGRATIONS
|
||||
const { GLOBAL_DB } = require("./migrations").MIGRATION_DBS
|
||||
|
||||
const APP_PREFIX = DocumentTypes.APP + SEPARATOR
|
||||
|
||||
|
@ -96,12 +93,7 @@ exports.getCookie = (ctx, name) => {
|
|||
* @param {string|object} value The value of cookie which will be set.
|
||||
* @param {object} opts options like whether to sign.
|
||||
*/
|
||||
exports.setCookie = (
|
||||
ctx,
|
||||
value,
|
||||
name = "builder",
|
||||
opts = { sign: true, requestDomain: false }
|
||||
) => {
|
||||
exports.setCookie = (ctx, value, name = "builder", opts = { sign: true }) => {
|
||||
if (value && opts && opts.sign) {
|
||||
value = jwt.sign(value, options.secretOrKey)
|
||||
}
|
||||
|
@ -113,7 +105,7 @@ exports.setCookie = (
|
|||
overwrite: true,
|
||||
}
|
||||
|
||||
if (environment.COOKIE_DOMAIN && !opts.requestDomain) {
|
||||
if (environment.COOKIE_DOMAIN) {
|
||||
config.domain = environment.COOKIE_DOMAIN
|
||||
}
|
||||
|
||||
|
@ -149,11 +141,6 @@ exports.getGlobalUserByEmail = async email => {
|
|||
}
|
||||
const db = getGlobalDB()
|
||||
|
||||
await migrateIfRequired(GLOBAL_DB, USER_EMAIL_VIEW_CASING, async () => {
|
||||
// re-create the view with latest changes
|
||||
await createUserEmailView(db)
|
||||
})
|
||||
|
||||
try {
|
||||
let users = (
|
||||
await db.query(`database/${ViewNames.USER_BY_EMAIL}`, {
|
||||
|
@ -269,7 +256,7 @@ exports.saveUser = async (
|
|||
exports.platformLogout = async ({ ctx, userId, keepActiveSession }) => {
|
||||
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)
|
||||
|
||||
if (keepActiveSession) {
|
||||
|
@ -278,8 +265,8 @@ exports.platformLogout = async ({ ctx, userId, keepActiveSession }) => {
|
|||
)
|
||||
} else {
|
||||
// clear cookies
|
||||
this.clearCookie(ctx, Cookies.Auth)
|
||||
this.clearCookie(ctx, Cookies.CurrentApp)
|
||||
exports.clearCookie(ctx, Cookies.Auth)
|
||||
exports.clearCookie(ctx, Cookies.CurrentApp)
|
||||
}
|
||||
|
||||
await invalidateSessions(
|
||||
|
|
|
@ -3410,9 +3410,9 @@ node-fetch@2.6.0:
|
|||
integrity sha512-8dG4H5ujfvFiqDmVu9fQ5bOHUC15JMjMY/Zumv26oOvvVJjM67KF8koCWIabKQ1GJIa9r2mMZscBq/TbdOcmNA==
|
||||
|
||||
node-fetch@^2.6.1:
|
||||
version "2.6.6"
|
||||
resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.6.tgz#1751a7c01834e8e1697758732e9efb6eeadfaf89"
|
||||
integrity sha512-Z8/6vRlTUChSdIgMa51jxQ4lrw/Jy5SOW10ObaA47/RElsAN2c5Pn8bTgFGWn/ibwzXTE8qwr1Yzx28vsecXEA==
|
||||
version "2.6.7"
|
||||
resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.7.tgz#24de9fba827e3b4ae44dc8b20256a379160052ad"
|
||||
integrity sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==
|
||||
dependencies:
|
||||
whatwg-url "^5.0.0"
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "@budibase/bbui",
|
||||
"description": "A UI solution used in the different Budibase projects.",
|
||||
"version": "1.0.46-alpha.3",
|
||||
"version": "1.0.50-alpha.4",
|
||||
"license": "MPL-2.0",
|
||||
"svelte": "src/index.js",
|
||||
"module": "dist/bbui.es.js",
|
||||
|
@ -79,6 +79,7 @@
|
|||
"@spectrum-css/underlay": "^2.0.9",
|
||||
"@spectrum-css/vars": "^3.0.1",
|
||||
"dayjs": "^1.10.4",
|
||||
"easymde": "^2.16.1",
|
||||
"svelte-flatpickr": "^3.2.3",
|
||||
"svelte-portal": "^1.0.0"
|
||||
},
|
||||
|
|
|
@ -147,7 +147,9 @@
|
|||
<img alt="preview" src={selectedUrl} />
|
||||
{:else}
|
||||
<div class="placeholder">
|
||||
<div class="extension">{selectedImage.extension}</div>
|
||||
<div class="extension">
|
||||
{selectedImage.name || "Unknown file"}
|
||||
</div>
|
||||
<div>Preview not supported</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
@ -359,18 +361,21 @@
|
|||
white-space: nowrap;
|
||||
width: 0;
|
||||
margin-right: 10px;
|
||||
user-select: all;
|
||||
}
|
||||
.placeholder {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
}
|
||||
.extension {
|
||||
color: var(--spectrum-global-color-gray-600);
|
||||
text-transform: uppercase;
|
||||
font-weight: 600;
|
||||
margin-bottom: 5px;
|
||||
user-select: all;
|
||||
}
|
||||
|
||||
.nav {
|
||||
|
|
42
packages/bbui/src/Form/Core/RichTextField.svelte
Normal file
42
packages/bbui/src/Form/Core/RichTextField.svelte
Normal file
|
@ -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,
|
||||
end: textarea.selectionEnd,
|
||||
})
|
||||
export let align = null
|
||||
|
||||
let focus = false
|
||||
let textarea
|
||||
|
@ -21,11 +22,23 @@
|
|||
dispatch("change", event.target.value)
|
||||
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>
|
||||
|
||||
<div
|
||||
style={(height ? `height: ${height}px;` : "") +
|
||||
(minHeight ? `min-height: ${minHeight}px` : "")}
|
||||
style={`${heightString}${minHeightString}`}
|
||||
class="spectrum-Textfield spectrum-Textfield--multiline"
|
||||
class:is-invalid={!!error}
|
||||
class:is-disabled={disabled}
|
||||
|
@ -46,6 +59,7 @@
|
|||
bind:this={textarea}
|
||||
placeholder={placeholder || ""}
|
||||
class="spectrum-Textfield-input"
|
||||
style={align ? `text-align: ${align}` : ""}
|
||||
{disabled}
|
||||
{id}
|
||||
on:focus={() => (focus = true)}
|
||||
|
|
|
@ -12,6 +12,7 @@
|
|||
export let updateOnChange = true
|
||||
export let quiet = false
|
||||
export let dataCy
|
||||
export let align
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
let focus = false
|
||||
|
@ -92,8 +93,9 @@
|
|||
on:input={onInput}
|
||||
on:keyup={updateValueOnEnter}
|
||||
{type}
|
||||
inputmode={type === "number" ? "decimal" : "text"}
|
||||
class="spectrum-Textfield-input"
|
||||
style={align ? `text-align: ${align};` : ""}
|
||||
inputmode={type === "number" ? "decimal" : "text"}
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
|
|
@ -10,3 +10,4 @@ export { default as CoreSearch } from "./Search.svelte"
|
|||
export { default as CoreDatePicker } from "./DatePicker.svelte"
|
||||
export { default as CoreDropzone } from "./Dropzone.svelte"
|
||||
export { default as CoreStepper } from "./Stepper.svelte"
|
||||
export { default as CoreRichTextField } from "./RichTextField.svelte"
|
||||
|
|
|
@ -6,11 +6,12 @@
|
|||
export let label = null
|
||||
export let labelPosition = "above"
|
||||
export let error = null
|
||||
export let tooltip = ""
|
||||
</script>
|
||||
|
||||
<div class="spectrum-Form-item" class:above={labelPosition === "above"}>
|
||||
{#if label}
|
||||
<FieldLabel forId={id} {label} position={labelPosition} />
|
||||
<FieldLabel forId={id} {label} position={labelPosition} {tooltip} />
|
||||
{/if}
|
||||
<div class="spectrum-Form-itemField">
|
||||
<slot />
|
||||
|
|
|
@ -1,19 +1,24 @@
|
|||
<script>
|
||||
import TooltipWrapper from "../Tooltip/TooltipWrapper.svelte"
|
||||
|
||||
import "@spectrum-css/fieldlabel/dist/index-vars.css"
|
||||
|
||||
export let forId
|
||||
export let label
|
||||
export let position = "above"
|
||||
export let tooltip = ""
|
||||
|
||||
$: className = position === "above" ? "" : `spectrum-FieldLabel--${position}`
|
||||
</script>
|
||||
|
||||
<label
|
||||
for={forId}
|
||||
class={`spectrum-FieldLabel spectrum-FieldLabel--sizeM spectrum-Form-itemLabel ${className}`}
|
||||
>
|
||||
{label || ""}
|
||||
</label>
|
||||
<TooltipWrapper {tooltip} size="S">
|
||||
<label
|
||||
for={forId}
|
||||
class={`spectrum-FieldLabel spectrum-FieldLabel--sizeM spectrum-Form-itemLabel ${className}`}
|
||||
>
|
||||
{label || ""}
|
||||
</label>
|
||||
</TooltipWrapper>
|
||||
|
||||
<style>
|
||||
label {
|
||||
|
|
36
packages/bbui/src/Form/RichTextField.svelte
Normal file
36
packages/bbui/src/Form/RichTextField.svelte
Normal file
|
@ -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>
|
|
@ -17,6 +17,7 @@
|
|||
export let quiet = false
|
||||
export let autoWidth = false
|
||||
export let sort = false
|
||||
export let tooltip = ""
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
const onChange = e => {
|
||||
|
@ -32,7 +33,7 @@
|
|||
}
|
||||
</script>
|
||||
|
||||
<Field {label} {labelPosition} {error}>
|
||||
<Field {label} {labelPosition} {error} {tooltip}>
|
||||
<Select
|
||||
{quiet}
|
||||
{error}
|
||||
|
|
|
@ -1,73 +1,20 @@
|
|||
<script>
|
||||
import "@spectrum-css/fieldlabel/dist/index-vars.css"
|
||||
import Tooltip from "../Tooltip/Tooltip.svelte"
|
||||
import Icon from "../Icon/Icon.svelte"
|
||||
import TooltipWrapper from "../Tooltip/TooltipWrapper.svelte"
|
||||
|
||||
export let size = "M"
|
||||
export let tooltip = ""
|
||||
export let showTooltip = false
|
||||
</script>
|
||||
|
||||
{#if tooltip}
|
||||
<div class="container">
|
||||
<label
|
||||
for=""
|
||||
class={`spectrum-FieldLabel spectrum-FieldLabel--size${size}`}
|
||||
>
|
||||
<slot />
|
||||
</label>
|
||||
<div class="icon-container">
|
||||
<div
|
||||
class="icon"
|
||||
class:icon-small={size === "M" || size === "S"}
|
||||
on:mouseover={() => (showTooltip = true)}
|
||||
on:mouseleave={() => (showTooltip = false)}
|
||||
>
|
||||
<Icon name="InfoOutline" size="S" disabled={true} />
|
||||
</div>
|
||||
{#if showTooltip}
|
||||
<div class="tooltip">
|
||||
<Tooltip textWrapping={true} direction={"bottom"} text={tooltip} />
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{:else}
|
||||
<TooltipWrapper {tooltip} {size}>
|
||||
<label for="" class={`spectrum-FieldLabel spectrum-FieldLabel--size${size}`}>
|
||||
<slot />
|
||||
</label>
|
||||
{/if}
|
||||
</TooltipWrapper>
|
||||
|
||||
<style>
|
||||
label {
|
||||
padding: 0;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
.icon-container {
|
||||
position: relative;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
margin-top: 1px;
|
||||
margin-left: 5px;
|
||||
margin-right: 5px;
|
||||
}
|
||||
.tooltip {
|
||||
position: absolute;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
top: 15px;
|
||||
z-index: 1;
|
||||
width: 160px;
|
||||
}
|
||||
.icon {
|
||||
transform: scale(0.75);
|
||||
}
|
||||
.icon-small {
|
||||
margin-top: -2px;
|
||||
margin-bottom: -5px;
|
||||
}
|
||||
</style>
|
||||
|
|
60
packages/bbui/src/Markdown/MarkdownEditor.svelte
Normal file
60
packages/bbui/src/Markdown/MarkdownEditor.svelte
Normal file
|
@ -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}
|
70
packages/bbui/src/Markdown/MarkdownViewer.svelte
Normal file
70
packages/bbui/src/Markdown/MarkdownViewer.svelte
Normal file
|
@ -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>
|
184
packages/bbui/src/Markdown/SpectrumMDE.svelte
Normal file
184
packages/bbui/src/Markdown/SpectrumMDE.svelte
Normal file
|
@ -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>
|
||||
import { createEventDispatcher } from "svelte"
|
||||
|
||||
export let type = "info"
|
||||
export let icon = "Info"
|
||||
export let message = ""
|
||||
export let dismissable = false
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
</script>
|
||||
|
||||
<div class="spectrum-Toast spectrum-Toast--{type}">
|
||||
|
@ -17,4 +22,28 @@
|
|||
<div class="spectrum-Toast-body">
|
||||
<div class="spectrum-Toast-content">{message || ""}</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>
|
||||
|
||||
<style>
|
||||
.spectrum-Toast {
|
||||
pointer-events: all;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
<script>
|
||||
import "@spectrum-css/toast/dist/index-vars.css"
|
||||
import Portal from "svelte-portal"
|
||||
import { flip } from "svelte/animate"
|
||||
import { notifications } from "../Stores/notifications"
|
||||
import Notification from "./Notification.svelte"
|
||||
import { fly } from "svelte/transition"
|
||||
|
@ -9,9 +8,15 @@
|
|||
|
||||
<Portal target=".modal-container">
|
||||
<div class="notifications">
|
||||
{#each $notifications as { type, icon, message, id } (id)}
|
||||
<div animate:flip transition:fly={{ y: -30 }}>
|
||||
<Notification {type} {icon} {message} />
|
||||
{#each $notifications as { type, icon, message, id, dismissable } (id)}
|
||||
<div transition:fly={{ y: -30 }}>
|
||||
<Notification
|
||||
{type}
|
||||
{icon}
|
||||
{message}
|
||||
{dismissable}
|
||||
on:dismiss={() => notifications.dismiss(id)}
|
||||
/>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
|
|
|
@ -20,20 +20,29 @@ export const createNotificationStore = () => {
|
|||
setTimeout(() => (block = false), timeout)
|
||||
}
|
||||
|
||||
const send = (message, type = "default", icon = "") => {
|
||||
const send = (message, type = "default", icon = "", autoDismiss = true) => {
|
||||
if (block) {
|
||||
return
|
||||
}
|
||||
let _id = id()
|
||||
_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
|
||||
|
@ -42,10 +51,11 @@ export const createNotificationStore = () => {
|
|||
subscribe,
|
||||
send,
|
||||
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"),
|
||||
success: msg => send(msg, "success", "CheckmarkCircle"),
|
||||
blockNotifications,
|
||||
dismiss: dismissNotification,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -8,10 +8,35 @@
|
|||
copyToClipboard(value)
|
||||
}
|
||||
|
||||
function copyToClipboard(value) {
|
||||
navigator.clipboard.writeText(value).then(() => {
|
||||
notifications.success("Copied")
|
||||
const copyToClipboard = value => {
|
||||
return new Promise(res => {
|
||||
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>
|
||||
|
||||
|
|
60
packages/bbui/src/Tooltip/TooltipWrapper.svelte
Normal file
60
packages/bbui/src/Tooltip/TooltipWrapper.svelte
Normal file
|
@ -0,0 +1,60 @@
|
|||
<script>
|
||||
import Tooltip from "./Tooltip.svelte"
|
||||
import Icon from "../Icon/Icon.svelte"
|
||||
|
||||
export let tooltip = ""
|
||||
export let size = "M"
|
||||
|
||||
let showTooltip = false
|
||||
</script>
|
||||
|
||||
<div class:container={!!tooltip}>
|
||||
<slot />
|
||||
{#if tooltip}
|
||||
<div class="icon-container">
|
||||
<div
|
||||
class="icon"
|
||||
class:icon-small={size === "M" || size === "S"}
|
||||
on:mouseover={() => (showTooltip = true)}
|
||||
on:mouseleave={() => (showTooltip = false)}
|
||||
>
|
||||
<Icon name="InfoOutline" size="S" disabled={true} />
|
||||
</div>
|
||||
{#if showTooltip}
|
||||
<div class="tooltip">
|
||||
<Tooltip textWrapping={true} direction={"bottom"} text={tooltip} />
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
.icon-container {
|
||||
position: relative;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
margin-top: 1px;
|
||||
margin-left: 5px;
|
||||
margin-right: 5px;
|
||||
}
|
||||
.tooltip {
|
||||
position: absolute;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
top: 15px;
|
||||
z-index: 1;
|
||||
width: 160px;
|
||||
}
|
||||
.icon {
|
||||
transform: scale(0.75);
|
||||
}
|
||||
.icon-small {
|
||||
margin-top: -2px;
|
||||
margin-bottom: -5px;
|
||||
}
|
||||
</style>
|
|
@ -60,6 +60,9 @@ export { default as StatusLight } from "./StatusLight/StatusLight.svelte"
|
|||
export { default as ColorPicker } from "./ColorPicker/ColorPicker.svelte"
|
||||
export { default as InlineAlert } from "./InlineAlert/InlineAlert.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
|
||||
export { default as BoldRenderer } from "./Table/BoldRenderer.svelte"
|
||||
|
|
|
@ -271,6 +271,13 @@
|
|||
resolved "https://registry.yarnpkg.com/@spectrum-css/vars/-/vars-3.0.2.tgz#ea9062c3c98dfc6ba59e5df14a03025ad8969999"
|
||||
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@*":
|
||||
version "0.0.47"
|
||||
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"
|
||||
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@*":
|
||||
version "14.14.41"
|
||||
resolved "https://registry.yarnpkg.com/@types/node/-/node-14.14.41.tgz#d0b939d94c1d7bd53d04824af45f1139b8c45615"
|
||||
|
@ -303,6 +315,13 @@
|
|||
dependencies:
|
||||
"@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:
|
||||
version "1.3.7"
|
||||
resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.7.tgz#531bc726517a3b2b41f850021c6cc15eaab507cd"
|
||||
|
@ -525,6 +544,18 @@ coa@^2.0.2:
|
|||
chalk "^2.4.1"
|
||||
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:
|
||||
version "1.9.3"
|
||||
resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8"
|
||||
|
@ -861,6 +892,17 @@ dot-prop@^5.2.0:
|
|||
dependencies:
|
||||
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:
|
||||
version "1.1.1"
|
||||
resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d"
|
||||
|
@ -1472,6 +1514,11 @@ magic-string@^0.25.7:
|
|||
dependencies:
|
||||
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:
|
||||
version "2.0.14"
|
||||
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"
|
||||
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:
|
||||
version "1.0.1"
|
||||
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")
|
||||
|
||||
// these run on ports we don't normally use so that they can run alongside the
|
||||
const fs = require("fs")
|
||||
|
||||
// normal development system
|
||||
const WORKER_PORT = "10002"
|
||||
const MAIN_PORT = cypressConfig.env.PORT
|
||||
|
@ -29,22 +26,20 @@ process.env.ALLOW_DEV_AUTOMATIONS = 1
|
|||
// Stop info logs polluting test outputs
|
||||
process.env.LOG_LEVEL = "error"
|
||||
|
||||
async function run() {
|
||||
exports.run = (
|
||||
serverLoc = "../../server/dist",
|
||||
workerLoc = "../../worker/dist"
|
||||
) => {
|
||||
// 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
|
||||
// it will cause environment module to be loaded prematurely
|
||||
const server = require("../../server/dist/app")
|
||||
require(serverLoc)
|
||||
process.env.PORT = WORKER_PORT
|
||||
const worker = require("../../worker/src/index")
|
||||
require(workerLoc)
|
||||
// reload main port for rest of system
|
||||
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()
|
||||
}
|
||||
|
|
4
packages/builder/cypress/ts/setup.ts
Normal file
4
packages/builder/cypress/ts/setup.ts
Normal file
|
@ -0,0 +1,4 @@
|
|||
// @ts-ignore
|
||||
import { run } from "../setup"
|
||||
|
||||
run("../../server/src/index", "../../worker/src/index")
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@budibase/builder",
|
||||
"version": "1.0.46-alpha.3",
|
||||
"version": "1.0.50-alpha.4",
|
||||
"license": "GPL-3.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
|
@ -11,12 +11,13 @@
|
|||
"dev:builder": "routify -c dev:vite",
|
||||
"dev:vite": "vite --host 0.0.0.0",
|
||||
"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:open": "cypress open",
|
||||
"cy:run:ci": "cypress run --record",
|
||||
"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"
|
||||
},
|
||||
"jest": {
|
||||
|
@ -65,10 +66,10 @@
|
|||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"@budibase/bbui": "^1.0.46-alpha.3",
|
||||
"@budibase/client": "^1.0.46-alpha.3",
|
||||
"@budibase/bbui": "^1.0.50-alpha.4",
|
||||
"@budibase/client": "^1.0.50-alpha.4",
|
||||
"@budibase/colorpicker": "1.1.2",
|
||||
"@budibase/string-templates": "^1.0.46-alpha.3",
|
||||
"@budibase/string-templates": "^1.0.50-alpha.4",
|
||||
"@sentry/browser": "5.19.1",
|
||||
"@spectrum-css/page": "^3.0.1",
|
||||
"@spectrum-css/vars": "^3.0.1",
|
||||
|
@ -106,6 +107,8 @@
|
|||
"start-server-and-test": "^1.12.1",
|
||||
"svelte": "^3.38.2",
|
||||
"svelte-jester": "^1.3.2",
|
||||
"ts-node": "^10.4.0",
|
||||
"typescript": "^4.5.5",
|
||||
"vite": "^2.1.5"
|
||||
},
|
||||
"gitHead": "115189f72a850bfb52b65ec61d932531bf327072"
|
||||
|
|
|
@ -1,12 +1,20 @@
|
|||
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,
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
import { getFrontendStore } from "./store/frontend"
|
||||
import { getAutomationStore } from "./store/automation"
|
||||
import { getHostingStore } from "./store/hosting"
|
||||
import { getThemeStore } from "./store/theme"
|
||||
import { derived, writable } from "svelte/store"
|
||||
import { FrontendTypes, LAYOUT_NAMES } from "../constants"
|
||||
|
@ -9,7 +8,6 @@ import { findComponent } from "./componentUtils"
|
|||
export const store = getFrontendStore()
|
||||
export const automationStore = getAutomationStore()
|
||||
export const themeStore = getThemeStore()
|
||||
export const hostingStore = getHostingStore()
|
||||
|
||||
export const currentAsset = derived(store, $store => {
|
||||
const type = $store.currentFrontEndType
|
||||
|
|
|
@ -2,7 +2,6 @@ import { get, writable } from "svelte/store"
|
|||
import { cloneDeep } from "lodash/fp"
|
||||
import {
|
||||
allScreens,
|
||||
hostingStore,
|
||||
currentAsset,
|
||||
mainLayout,
|
||||
selectedComponent,
|
||||
|
@ -66,6 +65,9 @@ export const getFrontendStore = () => {
|
|||
const store = writable({ ...INITIAL_FRONTEND_STATE })
|
||||
|
||||
store.actions = {
|
||||
reset: () => {
|
||||
store.set({ ...INITIAL_FRONTEND_STATE })
|
||||
},
|
||||
initialise: async pkg => {
|
||||
const { layouts, screens, application, clientLibPath } = pkg
|
||||
const components = await fetchComponentLibDefinitions(application.appId)
|
||||
|
@ -100,7 +102,6 @@ export const getFrontendStore = () => {
|
|||
version: application.version,
|
||||
revertableVersion: application.revertableVersion,
|
||||
}))
|
||||
await hostingStore.actions.fetch()
|
||||
|
||||
// Initialise backend stores
|
||||
const [_integrations] = await Promise.all([
|
||||
|
|
|
@ -1,34 +0,0 @@
|
|||
import { writable } from "svelte/store"
|
||||
import api, { get } from "../api"
|
||||
|
||||
const INITIAL_HOSTING_UI_STATE = {
|
||||
appUrl: "",
|
||||
deployedApps: {},
|
||||
deployedAppNames: [],
|
||||
deployedAppUrls: [],
|
||||
}
|
||||
|
||||
export const getHostingStore = () => {
|
||||
const store = writable({ ...INITIAL_HOSTING_UI_STATE })
|
||||
store.actions = {
|
||||
fetch: async () => {
|
||||
const response = await api.get("/api/hosting/urls")
|
||||
const urls = await response.json()
|
||||
store.update(state => {
|
||||
state.appUrl = urls.app
|
||||
return state
|
||||
})
|
||||
},
|
||||
fetchDeployedApps: async () => {
|
||||
let deployments = await (await get("/api/hosting/apps")).json()
|
||||
store.update(state => {
|
||||
state.deployedApps = deployments
|
||||
state.deployedAppNames = Object.values(deployments).map(app => app.name)
|
||||
state.deployedAppUrls = Object.values(deployments).map(app => app.url)
|
||||
return state
|
||||
})
|
||||
return deployments
|
||||
},
|
||||
}
|
||||
return store
|
||||
}
|
|
@ -169,6 +169,11 @@ export function makeDatasourceFormComponents(datasource) {
|
|||
optionsSource: "schema",
|
||||
})
|
||||
}
|
||||
if (fieldType === "longform") {
|
||||
component.customProps({
|
||||
format: "auto",
|
||||
})
|
||||
}
|
||||
if (fieldType === "array") {
|
||||
component.customProps({
|
||||
placeholder: "Choose an option",
|
||||
|
|
|
@ -4,9 +4,10 @@
|
|||
Select,
|
||||
DatePicker,
|
||||
Toggle,
|
||||
TextArea,
|
||||
Multiselect,
|
||||
Label,
|
||||
RichTextField,
|
||||
TextArea,
|
||||
} from "@budibase/bbui"
|
||||
import Dropzone from "components/common/Dropzone.svelte"
|
||||
import { capitalise } from "helpers"
|
||||
|
@ -43,7 +44,11 @@
|
|||
{:else if type === "link"}
|
||||
<LinkedRowSelector bind:linkedRows={value} schema={meta} />
|
||||
{: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"}
|
||||
<Label>{label}</Label>
|
||||
<Editor
|
||||
|
|
|
@ -22,8 +22,10 @@
|
|||
RelationshipTypes,
|
||||
ALLOWABLE_STRING_OPTIONS,
|
||||
ALLOWABLE_NUMBER_OPTIONS,
|
||||
ALLOWABLE_JSON_OPTIONS,
|
||||
ALLOWABLE_STRING_TYPES,
|
||||
ALLOWABLE_NUMBER_TYPES,
|
||||
ALLOWABLE_JSON_TYPES,
|
||||
SWITCHABLE_TYPES,
|
||||
} from "constants/backend"
|
||||
import { getAutoColumnInformation, buildAutoColumn } from "builderStore/utils"
|
||||
|
@ -150,6 +152,7 @@
|
|||
delete field.subtype
|
||||
delete field.tableId
|
||||
delete field.relationshipType
|
||||
delete field.formulaType
|
||||
|
||||
// Add in defaults and initial definition
|
||||
const definition = fieldDefinitions[event.detail?.toUpperCase()]
|
||||
|
@ -161,6 +164,9 @@
|
|||
if (field.type === LINK_TYPE) {
|
||||
field.relationshipType = RelationshipTypes.MANY_TO_MANY
|
||||
}
|
||||
if (field.type === FORMULA_TYPE) {
|
||||
field.formulaType = "dynamic"
|
||||
}
|
||||
}
|
||||
|
||||
function onChangeRequired(e) {
|
||||
|
@ -241,6 +247,11 @@
|
|||
ALLOWABLE_NUMBER_TYPES.indexOf(field.type) !== -1
|
||||
) {
|
||||
return ALLOWABLE_NUMBER_OPTIONS
|
||||
} else if (
|
||||
originalName &&
|
||||
ALLOWABLE_JSON_TYPES.indexOf(field.type) !== -1
|
||||
) {
|
||||
return ALLOWABLE_JSON_OPTIONS
|
||||
} else if (!external) {
|
||||
return [
|
||||
...Object.values(fieldDefinitions),
|
||||
|
@ -356,7 +367,7 @@
|
|||
|
||||
{#if canBeSearched && !external}
|
||||
<div>
|
||||
<Label grey small>Search Indexes</Label>
|
||||
<Label>Search Indexes</Label>
|
||||
<Toggle
|
||||
value={indexes[0] === field.name}
|
||||
disabled={indexes[1] === field.name}
|
||||
|
@ -383,6 +394,19 @@
|
|||
label="Options (one per line)"
|
||||
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"}
|
||||
<ValuesList
|
||||
label="Options (one per line)"
|
||||
|
@ -431,8 +455,22 @@
|
|||
error={errors.relatedName}
|
||||
/>
|
||||
{:else if field.type === FORMULA_TYPE}
|
||||
{#if !table.sql}
|
||||
<Select
|
||||
label="Formula type"
|
||||
bind:value={field.formulaType}
|
||||
options={[
|
||||
{ label: "Dynamic", value: "dynamic" },
|
||||
{ label: "Static", value: "static" },
|
||||
]}
|
||||
getOptionLabel={option => option.label}
|
||||
getOptionValue={option => option.value}
|
||||
tooltip="Dynamic formula are calculated when retrieved, but cannot be filtered,
|
||||
while static formula are calculated when the row is saved."
|
||||
/>
|
||||
{/if}
|
||||
<ModalBindableInput
|
||||
title="Handlebars Formula"
|
||||
title="Formula"
|
||||
label="Formula"
|
||||
value={field.formula}
|
||||
on:change={e => (field.formula = e.detail)}
|
||||
|
@ -441,7 +479,7 @@
|
|||
/>
|
||||
{:else if field.type === AUTO_TYPE}
|
||||
<Select
|
||||
label="Auto Column Type"
|
||||
label="Auto column type"
|
||||
value={field.subtype}
|
||||
on:change={e => (field.subtype = e.detail)}
|
||||
options={Object.entries(getAutoColumnInformation())}
|
||||
|
|
|
@ -188,29 +188,27 @@
|
|||
{:else}
|
||||
<Body size="S"><i>No tables found.</i></Body>
|
||||
{/if}
|
||||
{#if plusTables?.length !== 0}
|
||||
<Divider size="S" />
|
||||
<div class="query-header">
|
||||
<Heading size="S">Relationships</Heading>
|
||||
<Button primary on:click={openRelationshipModal}>
|
||||
Define relationship
|
||||
</Button>
|
||||
</div>
|
||||
<Body>
|
||||
Tell budibase how your tables are related to get even more smart features.
|
||||
</Body>
|
||||
{#if relationshipInfo && relationshipInfo.length > 0}
|
||||
<Table
|
||||
on:click={({ detail }) => openRelationshipModal(detail.from, detail.to)}
|
||||
schema={relationshipSchema}
|
||||
data={relationshipInfo}
|
||||
allowEditColumns={false}
|
||||
allowEditRows={false}
|
||||
allowSelectRows={false}
|
||||
/>
|
||||
{:else}
|
||||
<Body size="S"><i>No relationships configured.</i></Body>
|
||||
{/if}
|
||||
<Divider size="S" />
|
||||
<div class="query-header">
|
||||
<Heading size="S">Relationships</Heading>
|
||||
<Button primary on:click={() => openRelationshipModal()}>
|
||||
Define relationship
|
||||
</Button>
|
||||
</div>
|
||||
<Body>
|
||||
Tell budibase how your tables are related to get even more smart features.
|
||||
</Body>
|
||||
{#if relationshipInfo && relationshipInfo.length > 0}
|
||||
<Table
|
||||
on:click={({ detail }) => openRelationshipModal(detail.from, detail.to)}
|
||||
schema={relationshipSchema}
|
||||
data={relationshipInfo}
|
||||
allowEditColumns={false}
|
||||
allowEditRows={false}
|
||||
allowSelectRows={false}
|
||||
/>
|
||||
{:else}
|
||||
<Body size="S"><i>No relationships configured.</i></Body>
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
|
|
|
@ -0,0 +1,47 @@
|
|||
<script>
|
||||
import { ActionButton } from "@budibase/bbui"
|
||||
import GoogleLogo from "assets/google-logo.png"
|
||||
import { store } from "builderStore"
|
||||
import { auth } from "stores/portal"
|
||||
|
||||
export let preAuthStep
|
||||
export let datasource
|
||||
|
||||
$: tenantId = $auth.tenantId
|
||||
</script>
|
||||
|
||||
<ActionButton
|
||||
on:click={async () => {
|
||||
let ds = datasource
|
||||
if (!ds) {
|
||||
ds = await preAuthStep()
|
||||
}
|
||||
window.open(
|
||||
`/api/global/auth/${tenantId}/datasource/google?datasourceId=${datasource._id}&appId=${$store.appId}`,
|
||||
"_blank"
|
||||
)
|
||||
}}
|
||||
>
|
||||
<div class="inner">
|
||||
<img src={GoogleLogo} alt="google icon" />
|
||||
<p>Sign in with Google</p>
|
||||
</div>
|
||||
</ActionButton>
|
||||
|
||||
<style>
|
||||
.inner {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding-top: var(--spacing-xs);
|
||||
padding-bottom: var(--spacing-xs);
|
||||
}
|
||||
.inner img {
|
||||
width: 18px;
|
||||
margin: 3px 10px 3px 3px;
|
||||
}
|
||||
.inner p {
|
||||
margin: 0;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,184 @@
|
|||
<script>
|
||||
export let width = "100"
|
||||
export let height = "100"
|
||||
</script>
|
||||
|
||||
<svg
|
||||
{width}
|
||||
{height}
|
||||
version="1.0"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 50 80"
|
||||
preserveAspectRatio="xMidYMid meet"
|
||||
>
|
||||
<defs>
|
||||
<path
|
||||
d="M29.5833333,0 L4.4375,0 C1.996875,0 0,1.996875 0,4.4375 L0,60.6458333 C0,63.0864583 1.996875,65.0833333 4.4375,65.0833333 L42.8958333,65.0833333 C45.3364583,65.0833333 47.3333333,63.0864583 47.3333333,60.6458333 L47.3333333,17.75 L29.5833333,0 Z"
|
||||
id="path-1"
|
||||
/>
|
||||
<path
|
||||
d="M29.5833333,0 L4.4375,0 C1.996875,0 0,1.996875 0,4.4375 L0,60.6458333 C0,63.0864583 1.996875,65.0833333 4.4375,65.0833333 L42.8958333,65.0833333 C45.3364583,65.0833333 47.3333333,63.0864583 47.3333333,60.6458333 L47.3333333,17.75 L29.5833333,0 Z"
|
||||
id="path-3"
|
||||
/>
|
||||
<path
|
||||
d="M29.5833333,0 L4.4375,0 C1.996875,0 0,1.996875 0,4.4375 L0,60.6458333 C0,63.0864583 1.996875,65.0833333 4.4375,65.0833333 L42.8958333,65.0833333 C45.3364583,65.0833333 47.3333333,63.0864583 47.3333333,60.6458333 L47.3333333,17.75 L29.5833333,0 Z"
|
||||
id="path-5"
|
||||
/>
|
||||
<linearGradient
|
||||
x1="50.0053945%"
|
||||
y1="8.58610612%"
|
||||
x2="50.0053945%"
|
||||
y2="100.013939%"
|
||||
id="linearGradient-7"
|
||||
>
|
||||
<stop stop-color="#263238" stop-opacity="0.2" offset="0%" />
|
||||
<stop stop-color="#263238" stop-opacity="0.02" offset="100%" />
|
||||
</linearGradient>
|
||||
<path
|
||||
d="M29.5833333,0 L4.4375,0 C1.996875,0 0,1.996875 0,4.4375 L0,60.6458333 C0,63.0864583 1.996875,65.0833333 4.4375,65.0833333 L42.8958333,65.0833333 C45.3364583,65.0833333 47.3333333,63.0864583 47.3333333,60.6458333 L47.3333333,17.75 L29.5833333,0 Z"
|
||||
id="path-8"
|
||||
/>
|
||||
<path
|
||||
d="M29.5833333,0 L4.4375,0 C1.996875,0 0,1.996875 0,4.4375 L0,60.6458333 C0,63.0864583 1.996875,65.0833333 4.4375,65.0833333 L42.8958333,65.0833333 C45.3364583,65.0833333 47.3333333,63.0864583 47.3333333,60.6458333 L47.3333333,17.75 L29.5833333,0 Z"
|
||||
id="path-10"
|
||||
/>
|
||||
<path
|
||||
d="M29.5833333,0 L4.4375,0 C1.996875,0 0,1.996875 0,4.4375 L0,60.6458333 C0,63.0864583 1.996875,65.0833333 4.4375,65.0833333 L42.8958333,65.0833333 C45.3364583,65.0833333 47.3333333,63.0864583 47.3333333,60.6458333 L47.3333333,17.75 L29.5833333,0 Z"
|
||||
id="path-12"
|
||||
/>
|
||||
<path
|
||||
d="M29.5833333,0 L4.4375,0 C1.996875,0 0,1.996875 0,4.4375 L0,60.6458333 C0,63.0864583 1.996875,65.0833333 4.4375,65.0833333 L42.8958333,65.0833333 C45.3364583,65.0833333 47.3333333,63.0864583 47.3333333,60.6458333 L47.3333333,17.75 L29.5833333,0 Z"
|
||||
id="path-14"
|
||||
/>
|
||||
<radialGradient
|
||||
cx="3.16804688%"
|
||||
cy="2.71744318%"
|
||||
fx="3.16804688%"
|
||||
fy="2.71744318%"
|
||||
r="161.248516%"
|
||||
gradientTransform="translate(0.031680,0.027174),scale(1.000000,0.727273),translate(-0.031680,-0.027174)"
|
||||
id="radialGradient-16"
|
||||
>
|
||||
<stop stop-color="#FFFFFF" stop-opacity="0.1" offset="0%" />
|
||||
<stop stop-color="#FFFFFF" stop-opacity="0" offset="100%" />
|
||||
</radialGradient>
|
||||
</defs>
|
||||
<g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||
<g
|
||||
id="Consumer-Apps-Sheets-Large-VD-R8-"
|
||||
transform="translate(-451.000000, -451.000000)"
|
||||
>
|
||||
<g id="Hero" transform="translate(0.000000, 63.000000)">
|
||||
<g id="Personal" transform="translate(277.000000, 299.000000)">
|
||||
<g id="Sheets-icon" transform="translate(174.833333, 89.958333)">
|
||||
<g id="Group">
|
||||
<g id="Clipped">
|
||||
<mask id="mask-2" fill="white">
|
||||
<use xlink:href="#path-1" />
|
||||
</mask>
|
||||
<g id="SVGID_1_" />
|
||||
<path
|
||||
d="M29.5833333,0 L4.4375,0 C1.996875,0 0,1.996875 0,4.4375 L0,60.6458333 C0,63.0864583 1.996875,65.0833333 4.4375,65.0833333 L42.8958333,65.0833333 C45.3364583,65.0833333 47.3333333,63.0864583 47.3333333,60.6458333 L47.3333333,17.75 L36.9791667,10.3541667 L29.5833333,0 Z"
|
||||
id="Path"
|
||||
fill="#0F9D58"
|
||||
fill-rule="nonzero"
|
||||
mask="url(#mask-2)"
|
||||
/>
|
||||
</g>
|
||||
<g id="Clipped">
|
||||
<mask id="mask-4" fill="white">
|
||||
<use xlink:href="#path-3" />
|
||||
</mask>
|
||||
<g id="SVGID_1_" />
|
||||
<path
|
||||
d="M11.8333333,31.8020833 L11.8333333,53.25 L35.5,53.25 L35.5,31.8020833 L11.8333333,31.8020833 Z M22.1875,50.2916667 L14.7916667,50.2916667 L14.7916667,46.59375 L22.1875,46.59375 L22.1875,50.2916667 Z M22.1875,44.375 L14.7916667,44.375 L14.7916667,40.6770833 L22.1875,40.6770833 L22.1875,44.375 Z M22.1875,38.4583333 L14.7916667,38.4583333 L14.7916667,34.7604167 L22.1875,34.7604167 L22.1875,38.4583333 Z M32.5416667,50.2916667 L25.1458333,50.2916667 L25.1458333,46.59375 L32.5416667,46.59375 L32.5416667,50.2916667 Z M32.5416667,44.375 L25.1458333,44.375 L25.1458333,40.6770833 L32.5416667,40.6770833 L32.5416667,44.375 Z M32.5416667,38.4583333 L25.1458333,38.4583333 L25.1458333,34.7604167 L32.5416667,34.7604167 L32.5416667,38.4583333 Z"
|
||||
id="Shape"
|
||||
fill="#F1F1F1"
|
||||
fill-rule="nonzero"
|
||||
mask="url(#mask-4)"
|
||||
/>
|
||||
</g>
|
||||
<g id="Clipped">
|
||||
<mask id="mask-6" fill="white">
|
||||
<use xlink:href="#path-5" />
|
||||
</mask>
|
||||
<g id="SVGID_1_" />
|
||||
<polygon
|
||||
id="Path"
|
||||
fill="url(#linearGradient-7)"
|
||||
fill-rule="nonzero"
|
||||
mask="url(#mask-6)"
|
||||
points="30.8813021 16.4520313 47.3333333 32.9003646 47.3333333 17.75"
|
||||
/>
|
||||
</g>
|
||||
<g id="Clipped">
|
||||
<mask id="mask-9" fill="white">
|
||||
<use xlink:href="#path-8" />
|
||||
</mask>
|
||||
<g id="SVGID_1_" />
|
||||
<g id="Group" mask="url(#mask-9)">
|
||||
<g transform="translate(26.625000, -2.958333)">
|
||||
<path
|
||||
d="M2.95833333,2.95833333 L2.95833333,16.2708333 C2.95833333,18.7225521 4.94411458,20.7083333 7.39583333,20.7083333 L20.7083333,20.7083333 L2.95833333,2.95833333 Z"
|
||||
id="Path"
|
||||
fill="#87CEAC"
|
||||
fill-rule="nonzero"
|
||||
/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g id="Clipped">
|
||||
<mask id="mask-11" fill="white">
|
||||
<use xlink:href="#path-10" />
|
||||
</mask>
|
||||
<g id="SVGID_1_" />
|
||||
<path
|
||||
d="M4.4375,0 C1.996875,0 0,1.996875 0,4.4375 L0,4.80729167 C0,2.36666667 1.996875,0.369791667 4.4375,0.369791667 L29.5833333,0.369791667 L29.5833333,0 L4.4375,0 Z"
|
||||
id="Path"
|
||||
fill-opacity="0.2"
|
||||
fill="#FFFFFF"
|
||||
fill-rule="nonzero"
|
||||
mask="url(#mask-11)"
|
||||
/>
|
||||
</g>
|
||||
<g id="Clipped">
|
||||
<mask id="mask-13" fill="white">
|
||||
<use xlink:href="#path-12" />
|
||||
</mask>
|
||||
<g id="SVGID_1_" />
|
||||
<path
|
||||
d="M42.8958333,64.7135417 L4.4375,64.7135417 C1.996875,64.7135417 0,62.7166667 0,60.2760417 L0,60.6458333 C0,63.0864583 1.996875,65.0833333 4.4375,65.0833333 L42.8958333,65.0833333 C45.3364583,65.0833333 47.3333333,63.0864583 47.3333333,60.6458333 L47.3333333,60.2760417 C47.3333333,62.7166667 45.3364583,64.7135417 42.8958333,64.7135417 Z"
|
||||
id="Path"
|
||||
fill-opacity="0.2"
|
||||
fill="#263238"
|
||||
fill-rule="nonzero"
|
||||
mask="url(#mask-13)"
|
||||
/>
|
||||
</g>
|
||||
<g id="Clipped">
|
||||
<mask id="mask-15" fill="white">
|
||||
<use xlink:href="#path-14" />
|
||||
</mask>
|
||||
<g id="SVGID_1_" />
|
||||
<path
|
||||
d="M34.0208333,17.75 C31.5691146,17.75 29.5833333,15.7642188 29.5833333,13.3125 L29.5833333,13.6822917 C29.5833333,16.1340104 31.5691146,18.1197917 34.0208333,18.1197917 L47.3333333,18.1197917 L47.3333333,17.75 L34.0208333,17.75 Z"
|
||||
id="Path"
|
||||
fill-opacity="0.1"
|
||||
fill="#263238"
|
||||
fill-rule="nonzero"
|
||||
mask="url(#mask-15)"
|
||||
/>
|
||||
</g>
|
||||
</g>
|
||||
<path
|
||||
d="M29.5833333,0 L4.4375,0 C1.996875,0 0,1.996875 0,4.4375 L0,60.6458333 C0,63.0864583 1.996875,65.0833333 4.4375,65.0833333 L42.8958333,65.0833333 C45.3364583,65.0833333 47.3333333,63.0864583 47.3333333,60.6458333 L47.3333333,17.75 L29.5833333,0 Z"
|
||||
id="Path"
|
||||
fill="url(#radialGradient-16)"
|
||||
fill-rule="nonzero"
|
||||
/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
|
@ -11,6 +11,7 @@ import ArangoDB from "./ArangoDB.svelte"
|
|||
import Rest from "./Rest.svelte"
|
||||
import Budibase from "./Budibase.svelte"
|
||||
import Oracle from "./Oracle.svelte"
|
||||
import GoogleSheets from "./GoogleSheets.svelte"
|
||||
|
||||
export default {
|
||||
BUDIBASE: Budibase,
|
||||
|
@ -26,4 +27,5 @@ export default {
|
|||
ARANGODB: ArangoDB,
|
||||
REST: Rest,
|
||||
ORACLE: Oracle,
|
||||
GOOGLE_SHEETS: GoogleSheets,
|
||||
}
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
import { IntegrationNames, IntegrationTypes } from "constants/backend"
|
||||
import CreateTableModal from "components/backend/TableNavigator/modals/CreateTableModal.svelte"
|
||||
import DatasourceConfigModal from "components/backend/DatasourceNavigator/modals/DatasourceConfigModal.svelte"
|
||||
import GoogleDatasourceConfigModal from "components/backend/DatasourceNavigator/modals/GoogleDatasourceConfigModal.svelte"
|
||||
import { createRestDatasource } from "builderStore/datasource"
|
||||
import { goto } from "@roxi/routify"
|
||||
import ImportRestQueriesModal from "./ImportRestQueriesModal.svelte"
|
||||
|
@ -38,6 +39,7 @@
|
|||
plus: selected.plus,
|
||||
config,
|
||||
schema: selected.datasource,
|
||||
auth: selected.auth,
|
||||
}
|
||||
checkShowImport()
|
||||
}
|
||||
|
@ -79,7 +81,11 @@
|
|||
</Modal>
|
||||
|
||||
<Modal bind:this={externalDatasourceModal}>
|
||||
<DatasourceConfigModal {integration} {modal} />
|
||||
{#if integration?.auth?.type === "google"}
|
||||
<GoogleDatasourceConfigModal {integration} {modal} />
|
||||
{:else}
|
||||
<DatasourceConfigModal {integration} {modal} />
|
||||
{/if}
|
||||
</Modal>
|
||||
|
||||
<Modal bind:this={importModal}>
|
||||
|
|
|
@ -51,13 +51,9 @@
|
|||
>Connect your database to Budibase using the config below.
|
||||
</Body>
|
||||
</Layout>
|
||||
|
||||
<IntegrationConfigForm
|
||||
schema={datasource.schema}
|
||||
bind:datasource
|
||||
creating={true}
|
||||
/>
|
||||
</ModalContent>
|
||||
|
||||
<style>
|
||||
</style>
|
||||
|
|
|
@ -0,0 +1,29 @@
|
|||
<script>
|
||||
import { ModalContent, Body, Layout } from "@budibase/bbui"
|
||||
import { IntegrationNames } from "constants/backend"
|
||||
import cloneDeep from "lodash/cloneDeepWith"
|
||||
import GoogleButton from "../_components/GoogleButton.svelte"
|
||||
import { saveDatasource as save } from "builderStore/datasource"
|
||||
|
||||
export let integration
|
||||
export let modal
|
||||
|
||||
// kill the reference so the input isn't saved
|
||||
let datasource = cloneDeep(integration)
|
||||
</script>
|
||||
|
||||
<ModalContent
|
||||
title={`Connect to ${IntegrationNames[datasource.type]}`}
|
||||
onCancel={() => modal.show()}
|
||||
cancelText="Back"
|
||||
size="L"
|
||||
>
|
||||
<Layout noPadding>
|
||||
<Body size="XS"
|
||||
>Authenticate with your google account to use the {IntegrationNames[
|
||||
datasource.type
|
||||
]} integration.</Body
|
||||
>
|
||||
</Layout>
|
||||
<GoogleButton preAuthStep={() => save(datasource, true)} />
|
||||
</ModalContent>
|
|
@ -22,6 +22,10 @@
|
|||
|
||||
let originalFromName = fromRelationship.name,
|
||||
originalToName = toRelationship.name
|
||||
let fromTable, toTable, through, linkTable, tableOptions
|
||||
let isManyToMany, isManyToOne, relationshipTypes
|
||||
let errors, valid
|
||||
let currentTables = {}
|
||||
|
||||
if (fromRelationship && !fromRelationship.relationshipType) {
|
||||
fromRelationship.relationshipType = RelationshipTypes.MANY_TO_ONE
|
||||
|
@ -41,61 +45,52 @@
|
|||
|
||||
const touched = writable({})
|
||||
|
||||
function checkForErrors(
|
||||
fromTable,
|
||||
toTable,
|
||||
throughTable,
|
||||
fromRelate,
|
||||
toRelate
|
||||
) {
|
||||
function checkForErrors(fromRelate, toRelate) {
|
||||
const isMany =
|
||||
fromRelate.relationshipType === RelationshipTypes.MANY_TO_MANY
|
||||
const tableNotSet = "Please specify a table"
|
||||
const errors = {}
|
||||
const errObj = {}
|
||||
if ($touched.from && !fromTable) {
|
||||
errors.from = tableNotSet
|
||||
errObj.from = tableNotSet
|
||||
}
|
||||
if ($touched.to && !toTable) {
|
||||
errors.to = tableNotSet
|
||||
errObj.to = tableNotSet
|
||||
}
|
||||
if ($touched.through && isMany && !fromRelate.through) {
|
||||
errors.through = tableNotSet
|
||||
errObj.through = tableNotSet
|
||||
}
|
||||
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"
|
||||
if ($touched.fromCol && !fromRelate.name) {
|
||||
errors.fromCol = colNotSet
|
||||
errObj.fromCol = colNotSet
|
||||
}
|
||||
if ($touched.toCol && !toRelate.name) {
|
||||
errors.toCol = colNotSet
|
||||
errObj.toCol = colNotSet
|
||||
}
|
||||
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
|
||||
const tableError = "From/to/through tables must be different"
|
||||
if (fromTable && (fromTable === toTable || fromTable === throughTable)) {
|
||||
errors.from = tableError
|
||||
if (fromTable && (fromTable === toTable || fromTable === through)) {
|
||||
errObj.from = tableError
|
||||
}
|
||||
if (toTable && (toTable === fromTable || toTable === throughTable)) {
|
||||
errors.to = tableError
|
||||
if (toTable && (toTable === fromTable || toTable === through)) {
|
||||
errObj.to = tableError
|
||||
}
|
||||
if (
|
||||
throughTable &&
|
||||
(throughTable === fromTable || throughTable === toTable)
|
||||
) {
|
||||
errors.through = tableError
|
||||
if (through && (through === fromTable || through === toTable)) {
|
||||
errObj.through = tableError
|
||||
}
|
||||
const colError = "Column name cannot be an existing column"
|
||||
if (inSchema(fromTable, fromRelate.name, originalFromName)) {
|
||||
errors.fromCol = colError
|
||||
errObj.fromCol = colError
|
||||
}
|
||||
if (inSchema(toTable, toRelate.name, originalToName)) {
|
||||
errors.toCol = colError
|
||||
errObj.toCol = colError
|
||||
}
|
||||
return errors
|
||||
errors = errObj
|
||||
}
|
||||
|
||||
let fromPrimary
|
||||
|
@ -115,13 +110,7 @@
|
|||
$: fromTable = plusTables.find(table => table._id === toRelationship?.tableId)
|
||||
$: toTable = plusTables.find(table => table._id === fromRelationship?.tableId)
|
||||
$: through = plusTables.find(table => table._id === fromRelationship?.through)
|
||||
$: errors = checkForErrors(
|
||||
fromTable,
|
||||
toTable,
|
||||
through,
|
||||
fromRelationship,
|
||||
toRelationship
|
||||
)
|
||||
$: checkForErrors(fromRelationship, toRelationship)
|
||||
$: valid =
|
||||
Object.keys(errors).length === 0 && Object.keys($touched).length !== 0
|
||||
$: linkTable = through || toTable
|
||||
|
@ -239,19 +228,19 @@
|
|||
}
|
||||
|
||||
function tableChanged(fromTbl, toTbl) {
|
||||
if (
|
||||
(currentTables?.from?._id === fromTbl?._id &&
|
||||
currentTables?.to?._id === toTbl?._id) ||
|
||||
originalFromName ||
|
||||
originalToName
|
||||
) {
|
||||
return
|
||||
}
|
||||
fromRelationship.name = toTbl?.name || ""
|
||||
errors.fromCol = ""
|
||||
toRelationship.name = fromTbl?.name || ""
|
||||
errors.toCol = ""
|
||||
if (toTbl || fromTbl) {
|
||||
checkForErrors(
|
||||
fromTable,
|
||||
toTable,
|
||||
through,
|
||||
fromRelationship,
|
||||
toRelationship
|
||||
)
|
||||
}
|
||||
currentTables = { from: fromTbl, to: toTbl }
|
||||
}
|
||||
</script>
|
||||
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
import api from "builderStore/api"
|
||||
import { notifications } from "@budibase/bbui"
|
||||
import CreateWebhookDeploymentModal from "./CreateWebhookDeploymentModal.svelte"
|
||||
import { store, hostingStore } from "builderStore"
|
||||
import { store } from "builderStore"
|
||||
|
||||
const DeploymentStatus = {
|
||||
SUCCESS: "SUCCESS",
|
||||
|
@ -37,7 +37,7 @@
|
|||
let poll
|
||||
let deployments = []
|
||||
let urlComponent = $store.url || `/${appId}`
|
||||
let deploymentUrl = `${$hostingStore.appUrl}${urlComponent}`
|
||||
let deploymentUrl = `${urlComponent}`
|
||||
|
||||
const formatDate = (date, format) =>
|
||||
Intl.DateTimeFormat("en-GB", DATE_OPTIONS[format]).format(date)
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
<script>
|
||||
import { Select } from "@budibase/bbui"
|
||||
import { store } from "builderStore"
|
||||
import { get } from "svelte/store"
|
||||
|
||||
const themeOptions = [
|
||||
{
|
||||
|
@ -20,6 +21,17 @@
|
|||
value: "spectrum--darkest",
|
||||
},
|
||||
]
|
||||
|
||||
const onChangeTheme = async theme => {
|
||||
await store.actions.theme.save(theme)
|
||||
await store.actions.customTheme.save({
|
||||
...get(store).customTheme,
|
||||
navBackground:
|
||||
theme === "spectrum--light"
|
||||
? "var(--spectrum-global-color-gray-50)"
|
||||
: "var(--spectrum-global-color-gray-100)",
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<div>
|
||||
|
@ -27,7 +39,7 @@
|
|||
value={$store.theme}
|
||||
options={themeOptions}
|
||||
placeholder={null}
|
||||
on:change={e => store.actions.theme.save(e.detail)}
|
||||
on:change={e => onChangeTheme(e.detail)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
|
|
@ -19,7 +19,7 @@
|
|||
primaryColor: "var(--spectrum-global-color-blue-600)",
|
||||
primaryColorHover: "var(--spectrum-global-color-blue-500)",
|
||||
buttonBorderRadius: "16px",
|
||||
navBackground: "var(--spectrum-global-color-gray-100)",
|
||||
navBackground: "var(--spectrum-global-color-gray-50)",
|
||||
navTextColor: "var(--spectrum-global-color-gray-800)",
|
||||
}
|
||||
|
||||
|
@ -52,7 +52,14 @@
|
|||
}
|
||||
|
||||
const resetTheme = () => {
|
||||
store.actions.customTheme.save(null)
|
||||
const theme = get(store).theme
|
||||
store.actions.customTheme.save({
|
||||
...defaultTheme,
|
||||
navBackground:
|
||||
theme === "spectrum--light"
|
||||
? "var(--spectrum-global-color-gray-50)"
|
||||
: "var(--spectrum-global-color-gray-100)",
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
|
|
|
@ -44,7 +44,8 @@
|
|||
"relationshipfield",
|
||||
"daterangepicker",
|
||||
"multifieldselect",
|
||||
"jsonfield"
|
||||
"jsonfield",
|
||||
"s3upload"
|
||||
]
|
||||
},
|
||||
{
|
||||
|
@ -80,7 +81,8 @@
|
|||
"backgroundimage",
|
||||
"link",
|
||||
"icon",
|
||||
"embed"
|
||||
"embed",
|
||||
"markdownviewer"
|
||||
]
|
||||
}
|
||||
]
|
||||
|
|
|
@ -0,0 +1,33 @@
|
|||
<script>
|
||||
import { Select, Label } from "@budibase/bbui"
|
||||
import { currentAsset } from "builderStore"
|
||||
import { findAllMatchingComponents } from "builderStore/componentUtils"
|
||||
|
||||
export let parameters
|
||||
|
||||
$: components = findAllMatchingComponents($currentAsset.props, component =>
|
||||
component._component.endsWith("s3upload")
|
||||
)
|
||||
</script>
|
||||
|
||||
<div class="root">
|
||||
<Label small>S3 Upload Component</Label>
|
||||
<Select
|
||||
bind:value={parameters.componentId}
|
||||
options={components}
|
||||
getOptionLabel={x => x._instanceName}
|
||||
getOptionValue={x => x._id}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.root {
|
||||
display: grid;
|
||||
column-gap: var(--spacing-l);
|
||||
row-gap: var(--spacing-s);
|
||||
grid-template-columns: 120px 1fr;
|
||||
align-items: center;
|
||||
max-width: 400px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
</style>
|
|
@ -11,3 +11,4 @@ export { default as ChangeFormStep } from "./ChangeFormStep.svelte"
|
|||
export { default as UpdateState } from "./UpdateState.svelte"
|
||||
export { default as RefreshDataProvider } from "./RefreshDataProvider.svelte"
|
||||
export { default as DuplicateRow } from "./DuplicateRow.svelte"
|
||||
export { default as S3Upload } from "./S3Upload.svelte"
|
||||
|
|
|
@ -70,6 +70,16 @@
|
|||
"name": "Update State",
|
||||
"component": "UpdateState",
|
||||
"dependsOnFeature": "state"
|
||||
},
|
||||
{
|
||||
"name": "Upload File to S3",
|
||||
"component": "S3Upload",
|
||||
"context": [
|
||||
{
|
||||
"label": "File URL",
|
||||
"value": "publicUrl"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
|
@ -131,7 +131,7 @@
|
|||
{bindings}
|
||||
on:change={event => (filter.value = event.detail)}
|
||||
/>
|
||||
{:else if ["string", "longform", "number"].includes(filter.type)}
|
||||
{:else if ["string", "longform", "number", "formula"].includes(filter.type)}
|
||||
<Input disabled={filter.noValue} bind:value={filter.value} />
|
||||
{:else if ["options", "array"].includes(filter.type)}
|
||||
<Combobox
|
||||
|
|
|
@ -0,0 +1,15 @@
|
|||
<script>
|
||||
import { Select } from "@budibase/bbui"
|
||||
import { datasources } from "stores/backend"
|
||||
|
||||
export let value = null
|
||||
|
||||
$: dataSources = $datasources.list
|
||||
.filter(ds => ds.source === "S3" && !ds.config?.endpoint)
|
||||
.map(ds => ({
|
||||
label: ds.name,
|
||||
value: ds._id,
|
||||
}))
|
||||
</script>
|
||||
|
||||
<Select options={dataSources} {value} on:change />
|
|
@ -25,7 +25,7 @@
|
|||
return base
|
||||
}
|
||||
const currentTable = $tables.list.find(table => table._id === ds.tableId)
|
||||
return getFields(base, { allowLinks: currentTable.sql }).map(
|
||||
return getFields(base, { allowLinks: currentTable?.sql }).map(
|
||||
field => field.name
|
||||
)
|
||||
}
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import { Checkbox, Select, Stepper } from "@budibase/bbui"
|
||||
import DataSourceSelect from "./DataSourceSelect.svelte"
|
||||
import S3DataSourceSelect from "./S3DataSourceSelect.svelte"
|
||||
import DataProviderSelect from "./DataProviderSelect.svelte"
|
||||
import ButtonActionEditor from "./ButtonActionEditor/ButtonActionEditor.svelte"
|
||||
import TableSelect from "./TableSelect.svelte"
|
||||
|
@ -22,6 +23,7 @@ const componentMap = {
|
|||
text: DrawerBindableCombobox,
|
||||
select: Select,
|
||||
dataSource: DataSourceSelect,
|
||||
"dataSource/s3": S3DataSourceSelect,
|
||||
dataProvider: DataProviderSelect,
|
||||
boolean: Checkbox,
|
||||
number: Stepper,
|
||||
|
|
|
@ -1,16 +1,14 @@
|
|||
<script>
|
||||
import { Label, Select } from "@budibase/bbui"
|
||||
import { permissions, roles } from "stores/backend"
|
||||
import { onMount } from "svelte"
|
||||
import { Roles } from "constants/backend"
|
||||
|
||||
export let query
|
||||
export let saveId
|
||||
export let label
|
||||
|
||||
$: updateRole(roleId, saveId)
|
||||
$: getPermissions(query)
|
||||
|
||||
let roleId, loaded
|
||||
let roleId, loaded, fetched
|
||||
|
||||
async function updateRole(role, id) {
|
||||
roleId = role
|
||||
|
@ -26,19 +24,24 @@
|
|||
}
|
||||
}
|
||||
|
||||
onMount(async () => {
|
||||
if (!query || !query._id) {
|
||||
async function getPermissions(queryToFetch) {
|
||||
if (fetched?._id === queryToFetch?._id) {
|
||||
loaded = true
|
||||
return
|
||||
}
|
||||
fetched = queryToFetch
|
||||
if (!queryToFetch || !queryToFetch._id) {
|
||||
roleId = Roles.BASIC
|
||||
loaded = true
|
||||
return
|
||||
}
|
||||
try {
|
||||
roleId = (await permissions.forResource(query._id))["read"]
|
||||
roleId = (await permissions.forResource(queryToFetch._id))["read"]
|
||||
} catch (err) {
|
||||
roleId = Roles.BASIC
|
||||
}
|
||||
loaded = true
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if loaded}
|
||||
|
|
|
@ -15,8 +15,6 @@
|
|||
queryBindings = [...queryBindings, {}]
|
||||
}
|
||||
|
||||
$: console.log(bindings)
|
||||
|
||||
function deleteQueryBinding(idx) {
|
||||
queryBindings.splice(idx, 1)
|
||||
queryBindings = queryBindings
|
||||
|
|
|
@ -1,100 +1,46 @@
|
|||
<script>
|
||||
import { writable, get as svelteGet } from "svelte/store"
|
||||
|
||||
import { notifications, Input, ModalContent, Dropzone } from "@budibase/bbui"
|
||||
import { store, automationStore, hostingStore } from "builderStore"
|
||||
import { admin, auth } from "stores/portal"
|
||||
import { string, mixed, object } from "yup"
|
||||
import { store, automationStore } from "builderStore"
|
||||
import { apps, admin, auth } from "stores/portal"
|
||||
import api, { get, post } from "builderStore/api"
|
||||
import analytics, { Events } from "analytics"
|
||||
import { onMount } from "svelte"
|
||||
import { capitalise } from "helpers"
|
||||
import { goto } from "@roxi/routify"
|
||||
import { APP_NAME_REGEX } from "constants"
|
||||
import { createValidationStore } from "helpers/validation/yup"
|
||||
import * as appValidation from "helpers/validation/yup/app"
|
||||
|
||||
export let template
|
||||
export let inline
|
||||
|
||||
const values = writable({ name: null })
|
||||
const errors = writable({})
|
||||
const touched = writable({})
|
||||
const validator = {
|
||||
name: string()
|
||||
.trim()
|
||||
.required("Your application must have a name")
|
||||
.matches(
|
||||
APP_NAME_REGEX,
|
||||
"App name must be letters, numbers and spaces only"
|
||||
),
|
||||
file: template?.fromFile
|
||||
? mixed().required("Please choose a file to import")
|
||||
: null,
|
||||
}
|
||||
|
||||
let submitting = false
|
||||
let valid = false
|
||||
let initialTemplateInfo = template?.fromFile || template?.key
|
||||
|
||||
$: checkValidity($values, validator)
|
||||
$: showTemplateSelection = !template && !initialTemplateInfo
|
||||
const values = writable({ name: "", url: null })
|
||||
const validation = createValidationStore()
|
||||
$: validation.check($values)
|
||||
|
||||
onMount(async () => {
|
||||
await hostingStore.actions.fetchDeployedApps()
|
||||
const existingAppNames = svelteGet(hostingStore).deployedAppNames
|
||||
validator.name = string()
|
||||
.trim()
|
||||
.required("Your application must have a name")
|
||||
.matches(APP_NAME_REGEX, "App name must be letters and numbers only")
|
||||
.test(
|
||||
"non-existing-app-name",
|
||||
"Another app with the same name already exists",
|
||||
value => {
|
||||
return !existingAppNames.some(
|
||||
appName => appName.toLowerCase() === value.toLowerCase()
|
||||
)
|
||||
}
|
||||
)
|
||||
await setupValidation()
|
||||
})
|
||||
|
||||
const checkValidity = async (values, validator) => {
|
||||
const obj = object().shape(validator)
|
||||
Object.keys(validator).forEach(key => ($errors[key] = null))
|
||||
if (template?.fromFile && values.file == null) {
|
||||
valid = false
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
await obj.validate(values, { abortEarly: false })
|
||||
} catch (validationErrors) {
|
||||
validationErrors.inner.forEach(error => {
|
||||
$errors[error.path] = capitalise(error.message)
|
||||
})
|
||||
}
|
||||
|
||||
valid = await obj.isValid(values)
|
||||
const setupValidation = async () => {
|
||||
const applications = svelteGet(apps)
|
||||
appValidation.name(validation, { apps: applications })
|
||||
appValidation.url(validation, { apps: applications })
|
||||
appValidation.file(validation, { template })
|
||||
// init validation
|
||||
validation.check($values)
|
||||
}
|
||||
|
||||
async function createNewApp() {
|
||||
const templateToUse = Object.keys(template).length === 0 ? null : template
|
||||
submitting = true
|
||||
|
||||
// Check a template exists if we are important
|
||||
if (templateToUse?.fromFile && !$values.file) {
|
||||
$errors.file = "Please choose a file to import"
|
||||
valid = false
|
||||
submitting = false
|
||||
return false
|
||||
}
|
||||
|
||||
try {
|
||||
// Create form data to create app
|
||||
let data = new FormData()
|
||||
data.append("name", $values.name.trim())
|
||||
data.append("useTemplate", templateToUse != null)
|
||||
if (templateToUse) {
|
||||
data.append("templateName", templateToUse.name)
|
||||
data.append("templateKey", templateToUse.key)
|
||||
if ($values.url) {
|
||||
data.append("url", $values.url.trim())
|
||||
}
|
||||
data.append("useTemplate", template != null)
|
||||
if (template) {
|
||||
data.append("templateName", template.name)
|
||||
data.append("templateKey", template.key)
|
||||
data.append("templateFile", $values.file)
|
||||
}
|
||||
|
||||
|
@ -108,7 +54,7 @@
|
|||
analytics.captureEvent(Events.APP.CREATED, {
|
||||
name: $values.name,
|
||||
appId: appJson.instance._id,
|
||||
templateToUse,
|
||||
templateToUse: template,
|
||||
})
|
||||
|
||||
// Select Correct Application/DB in prep for creating user
|
||||
|
@ -136,44 +82,51 @@
|
|||
} catch (error) {
|
||||
console.error(error)
|
||||
notifications.error(error)
|
||||
submitting = false
|
||||
}
|
||||
}
|
||||
|
||||
async function onCancel() {
|
||||
template = null
|
||||
await auth.setInitInfo({})
|
||||
// auto add slash to url
|
||||
$: {
|
||||
if ($values.url && !$values.url.startsWith("/")) {
|
||||
$values.url = `/${$values.url}`
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<ModalContent
|
||||
title={"Name your app"}
|
||||
title={"Create your app"}
|
||||
confirmText={template?.fromFile ? "Import app" : "Create app"}
|
||||
onConfirm={createNewApp}
|
||||
onCancel={inline ? onCancel : null}
|
||||
cancelText={inline ? "Back" : undefined}
|
||||
showCloseIcon={!inline}
|
||||
disabled={!valid}
|
||||
disabled={!$validation.valid}
|
||||
>
|
||||
{#if template?.fromFile}
|
||||
<Dropzone
|
||||
error={$touched.file && $errors.file}
|
||||
error={$validation.touched.file && $validation.errors.file}
|
||||
gallery={false}
|
||||
label="File to import"
|
||||
value={[$values.file]}
|
||||
on:change={e => {
|
||||
$values.file = e.detail?.[0]
|
||||
$touched.file = true
|
||||
$validation.touched.file = true
|
||||
}}
|
||||
/>
|
||||
{/if}
|
||||
<Input
|
||||
bind:value={$values.name}
|
||||
error={$touched.name && $errors.name}
|
||||
on:blur={() => ($touched.name = true)}
|
||||
error={$validation.touched.name && $validation.errors.name}
|
||||
on:blur={() => ($validation.touched.name = true)}
|
||||
label="Name"
|
||||
placeholder={$auth.user.firstName
|
||||
? `${$auth.user.firstName}'s app`
|
||||
? `${$auth.user.firstName}s app`
|
||||
: "My app"}
|
||||
/>
|
||||
<Input
|
||||
bind:value={$values.url}
|
||||
error={$validation.touched.url && $validation.errors.url}
|
||||
on:blur={() => ($validation.touched.url = true)}
|
||||
label="URL"
|
||||
placeholder={$values.name
|
||||
? "/" + encodeURIComponent($values.name).toLowerCase()
|
||||
: "/"}
|
||||
/>
|
||||
</ModalContent>
|
||||
|
|
|
@ -1,120 +1,75 @@
|
|||
<script>
|
||||
import { writable, get as svelteGet } from "svelte/store"
|
||||
import {
|
||||
notifications,
|
||||
Input,
|
||||
Modal,
|
||||
ModalContent,
|
||||
Body,
|
||||
} from "@budibase/bbui"
|
||||
import { hostingStore } from "builderStore"
|
||||
import { notifications, Input, ModalContent, Body } from "@budibase/bbui"
|
||||
import { apps } from "stores/portal"
|
||||
import { string, object } from "yup"
|
||||
import { onMount } from "svelte"
|
||||
import { capitalise } from "helpers"
|
||||
import { APP_NAME_REGEX } from "constants"
|
||||
|
||||
const values = writable({ name: null })
|
||||
const errors = writable({})
|
||||
const touched = writable({})
|
||||
const validator = {
|
||||
name: string()
|
||||
.trim()
|
||||
.required("Your application must have a name")
|
||||
.matches(
|
||||
APP_NAME_REGEX,
|
||||
"App name must be letters, numbers and spaces only"
|
||||
),
|
||||
}
|
||||
import { createValidationStore } from "helpers/validation/yup"
|
||||
import * as appValidation from "helpers/validation/yup/app"
|
||||
|
||||
export let app
|
||||
|
||||
let modal
|
||||
let valid = false
|
||||
let dirty = false
|
||||
$: checkValidity($values, validator)
|
||||
$: {
|
||||
// prevent validation by setting name to undefined without an app
|
||||
if (app) {
|
||||
$values.name = app?.name
|
||||
}
|
||||
}
|
||||
const values = writable({ name: "", url: null })
|
||||
const validation = createValidationStore()
|
||||
$: validation.check($values)
|
||||
|
||||
onMount(async () => {
|
||||
await hostingStore.actions.fetchDeployedApps()
|
||||
const existingAppNames = svelteGet(hostingStore).deployedAppNames
|
||||
validator.name = string()
|
||||
.trim()
|
||||
.required("Your application must have a name")
|
||||
.matches(
|
||||
APP_NAME_REGEX,
|
||||
"App name must be letters, numbers and spaces only"
|
||||
)
|
||||
.test(
|
||||
"non-existing-app-name",
|
||||
"Another app with the same name already exists",
|
||||
value => {
|
||||
return !existingAppNames.some(
|
||||
appName => dirty && appName.toLowerCase() === value.toLowerCase()
|
||||
)
|
||||
}
|
||||
)
|
||||
$values.name = app.name
|
||||
$values.url = app.url
|
||||
setupValidation()
|
||||
})
|
||||
|
||||
const checkValidity = async (values, validator) => {
|
||||
const obj = object().shape(validator)
|
||||
Object.keys(validator).forEach(key => ($errors[key] = null))
|
||||
try {
|
||||
await obj.validate(values, { abortEarly: false })
|
||||
} catch (validationErrors) {
|
||||
validationErrors.inner.forEach(error => {
|
||||
$errors[error.path] = capitalise(error.message)
|
||||
})
|
||||
}
|
||||
valid = await obj.isValid(values)
|
||||
const setupValidation = async () => {
|
||||
const applications = svelteGet(apps)
|
||||
appValidation.name(validation, { apps: applications, currentApp: app })
|
||||
appValidation.url(validation, { apps: applications, currentApp: app })
|
||||
// init validation
|
||||
validation.check($values)
|
||||
}
|
||||
|
||||
async function updateApp() {
|
||||
try {
|
||||
// Update App
|
||||
await apps.update(app.instance._id, { name: $values.name.trim() })
|
||||
hide()
|
||||
const body = {
|
||||
name: $values.name.trim(),
|
||||
}
|
||||
if ($values.url) {
|
||||
body.url = $values.url.trim()
|
||||
}
|
||||
await apps.update(app.instance._id, body)
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
notifications.error(error)
|
||||
}
|
||||
}
|
||||
|
||||
export const show = () => {
|
||||
modal.show()
|
||||
}
|
||||
export const hide = () => {
|
||||
modal.hide()
|
||||
}
|
||||
|
||||
const onCancel = () => {
|
||||
hide()
|
||||
}
|
||||
|
||||
const onShow = () => {
|
||||
dirty = false
|
||||
// auto add slash to url
|
||||
$: {
|
||||
if ($values.url && !$values.url.startsWith("/")) {
|
||||
$values.url = `/${$values.url}`
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<Modal bind:this={modal} on:hide={onCancel} on:show={onShow}>
|
||||
<ModalContent
|
||||
title={"Edit app"}
|
||||
confirmText={"Save"}
|
||||
onConfirm={updateApp}
|
||||
disabled={!(valid && dirty)}
|
||||
>
|
||||
<Body size="S">Update the name of your app.</Body>
|
||||
<Input
|
||||
bind:value={$values.name}
|
||||
error={$touched.name && $errors.name}
|
||||
on:blur={() => ($touched.name = true)}
|
||||
on:change={() => (dirty = true)}
|
||||
label="Name"
|
||||
/>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
<ModalContent
|
||||
title={"Edit app"}
|
||||
confirmText={"Save"}
|
||||
onConfirm={updateApp}
|
||||
disabled={!$validation.valid}
|
||||
>
|
||||
<Body size="S">Update the name of your app.</Body>
|
||||
<Input
|
||||
bind:value={$values.name}
|
||||
error={$validation.touched.name && $validation.errors.name}
|
||||
on:blur={() => ($validation.touched.name = true)}
|
||||
label="Name"
|
||||
/>
|
||||
<Input
|
||||
bind:value={$values.url}
|
||||
error={$validation.touched.url && $validation.errors.url}
|
||||
on:blur={() => ($validation.touched.url = true)}
|
||||
label="URL"
|
||||
placeholder={$values.name
|
||||
? "/" + encodeURIComponent($values.name).toLowerCase()
|
||||
: "/"}
|
||||
/>
|
||||
</ModalContent>
|
||||
|
|
|
@ -148,20 +148,23 @@ export const RelationshipTypes = {
|
|||
}
|
||||
|
||||
export const ALLOWABLE_STRING_OPTIONS = [FIELDS.STRING, FIELDS.OPTIONS]
|
||||
|
||||
export const ALLOWABLE_STRING_TYPES = ALLOWABLE_STRING_OPTIONS.map(
|
||||
opt => opt.type
|
||||
)
|
||||
|
||||
export const ALLOWABLE_NUMBER_OPTIONS = [FIELDS.NUMBER, FIELDS.BOOLEAN]
|
||||
|
||||
export const ALLOWABLE_NUMBER_TYPES = ALLOWABLE_NUMBER_OPTIONS.map(
|
||||
opt => opt.type
|
||||
)
|
||||
|
||||
export const SWITCHABLE_TYPES = ALLOWABLE_NUMBER_TYPES.concat(
|
||||
ALLOWABLE_STRING_TYPES
|
||||
)
|
||||
export const ALLOWABLE_JSON_OPTIONS = [FIELDS.JSON, FIELDS.ARRAY]
|
||||
export const ALLOWABLE_JSON_TYPES = ALLOWABLE_JSON_OPTIONS.map(opt => opt.type)
|
||||
|
||||
export const SWITCHABLE_TYPES = [
|
||||
...ALLOWABLE_STRING_TYPES,
|
||||
...ALLOWABLE_NUMBER_TYPES,
|
||||
...ALLOWABLE_JSON_TYPES,
|
||||
]
|
||||
|
||||
export const IntegrationTypes = {
|
||||
POSTGRES: "POSTGRES",
|
||||
|
@ -177,6 +180,7 @@ export const IntegrationTypes = {
|
|||
ARANGODB: "ARANGODB",
|
||||
ORACLE: "ORACLE",
|
||||
INTERNAL: "INTERNAL",
|
||||
GOOGLE_SHEETS: "GOOGLE_SHEETS",
|
||||
}
|
||||
|
||||
export const IntegrationNames = {
|
||||
|
@ -193,6 +197,7 @@ export const IntegrationNames = {
|
|||
[IntegrationTypes.ARANGODB]: "ArangoDB",
|
||||
[IntegrationTypes.ORACLE]: "Oracle",
|
||||
[IntegrationTypes.INTERNAL]: "Internal",
|
||||
[IntegrationTypes.GOOGLE_SHEETS]: "Google Sheets",
|
||||
}
|
||||
|
||||
export const SchemaTypeOptions = [
|
||||
|
|
|
@ -15,6 +15,22 @@ export const AppStatus = {
|
|||
DEPLOYED: "published",
|
||||
}
|
||||
|
||||
export const IntegrationNames = {
|
||||
POSTGRES: "PostgreSQL",
|
||||
MONGODB: "MongoDB",
|
||||
COUCHDB: "CouchDB",
|
||||
S3: "S3",
|
||||
MYSQL: "MySQL",
|
||||
REST: "REST",
|
||||
DYNAMODB: "DynamoDB",
|
||||
ELASTICSEARCH: "ElasticSearch",
|
||||
SQL_SERVER: "SQL Server",
|
||||
AIRTABLE: "Airtable",
|
||||
ARANGODB: "ArangoDB",
|
||||
ORACLE: "Oracle",
|
||||
GOOGLE_SHEETS: "Google Sheets",
|
||||
}
|
||||
|
||||
// fields on the user table that cannot be edited
|
||||
export const UNEDITABLE_USER_FIELDS = [
|
||||
"email",
|
||||
|
@ -36,4 +52,7 @@ export const LAYOUT_NAMES = {
|
|||
|
||||
export const BUDIBASE_INTERNAL_DB = "bb_internal"
|
||||
|
||||
// one or more word characters and whitespace
|
||||
export const APP_NAME_REGEX = /^[\w\s]+$/
|
||||
// zero or more non-whitespace characters
|
||||
export const APP_URL_REGEX = /^\S*$/
|
||||
|
|
|
@ -59,24 +59,26 @@ export const NoEmptyFilterStrings = [
|
|||
*/
|
||||
export const getValidOperatorsForType = type => {
|
||||
const Op = OperatorOptions
|
||||
const stringOps = [
|
||||
Op.Equals,
|
||||
Op.NotEquals,
|
||||
Op.StartsWith,
|
||||
Op.Like,
|
||||
Op.Empty,
|
||||
Op.NotEmpty,
|
||||
]
|
||||
const numOps = [
|
||||
Op.Equals,
|
||||
Op.NotEquals,
|
||||
Op.MoreThan,
|
||||
Op.LessThan,
|
||||
Op.Empty,
|
||||
Op.NotEmpty,
|
||||
]
|
||||
if (type === "string") {
|
||||
return [
|
||||
Op.Equals,
|
||||
Op.NotEquals,
|
||||
Op.StartsWith,
|
||||
Op.Like,
|
||||
Op.Empty,
|
||||
Op.NotEmpty,
|
||||
]
|
||||
return stringOps
|
||||
} else if (type === "number") {
|
||||
return [
|
||||
Op.Equals,
|
||||
Op.NotEquals,
|
||||
Op.MoreThan,
|
||||
Op.LessThan,
|
||||
Op.Empty,
|
||||
Op.NotEmpty,
|
||||
]
|
||||
return numOps
|
||||
} else if (type === "options") {
|
||||
return [Op.Equals, Op.NotEquals, Op.Empty, Op.NotEmpty]
|
||||
} else if (type === "array") {
|
||||
|
@ -84,23 +86,11 @@ export const getValidOperatorsForType = type => {
|
|||
} else if (type === "boolean") {
|
||||
return [Op.Equals, Op.NotEquals, Op.Empty, Op.NotEmpty]
|
||||
} else if (type === "longform") {
|
||||
return [
|
||||
Op.Equals,
|
||||
Op.NotEquals,
|
||||
Op.StartsWith,
|
||||
Op.Like,
|
||||
Op.Empty,
|
||||
Op.NotEmpty,
|
||||
]
|
||||
return stringOps
|
||||
} else if (type === "datetime") {
|
||||
return [
|
||||
Op.Equals,
|
||||
Op.NotEquals,
|
||||
Op.MoreThan,
|
||||
Op.LessThan,
|
||||
Op.Empty,
|
||||
Op.NotEmpty,
|
||||
]
|
||||
return numOps
|
||||
} else if (type === "formula") {
|
||||
return stringOps.concat([Op.MoreThan, Op.LessThan])
|
||||
}
|
||||
return []
|
||||
}
|
||||
|
|
|
@ -27,5 +27,8 @@ export function getFields(fields, { allowLinks } = { allowLinks: true }) {
|
|||
filteredFields = filteredFields.concat(getTableFields(linkField))
|
||||
}
|
||||
}
|
||||
return filteredFields
|
||||
const staticFormulaFields = fields.filter(
|
||||
field => field.type === "formula" && field.formulaType === "static"
|
||||
)
|
||||
return filteredFields.concat(staticFormulaFields)
|
||||
}
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
import { writable, derived } from "svelte/store"
|
||||
|
||||
// DEPRECATED - Use the yup based validators for future validation
|
||||
|
||||
export function createValidationStore(initialValue, ...validators) {
|
||||
let touched = false
|
||||
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
// TODO: Convert to yup based validators
|
||||
|
||||
export function emailValidator(value) {
|
||||
return (
|
||||
(value &&
|
||||
|
|
83
packages/builder/src/helpers/validation/yup/app.js
Normal file
83
packages/builder/src/helpers/validation/yup/app.js
Normal file
|
@ -0,0 +1,83 @@
|
|||
import { string, mixed } from "yup"
|
||||
import { APP_NAME_REGEX, APP_URL_REGEX } from "constants"
|
||||
|
||||
export const name = (validation, { apps, currentApp } = { apps: [] }) => {
|
||||
validation.addValidator(
|
||||
"name",
|
||||
string()
|
||||
.trim()
|
||||
.required("Your application must have a name")
|
||||
.matches(
|
||||
APP_NAME_REGEX,
|
||||
"App name must be letters, numbers and spaces only"
|
||||
)
|
||||
.test(
|
||||
"non-existing-app-name",
|
||||
"Another app with the same name already exists",
|
||||
value => {
|
||||
if (!value) {
|
||||
// exit early, above validator will fail
|
||||
return true
|
||||
}
|
||||
if (currentApp) {
|
||||
// filter out the current app if present
|
||||
apps = apps.filter(app => app.appId !== currentApp.appId)
|
||||
}
|
||||
return !apps
|
||||
.map(app => app.name)
|
||||
.some(appName => appName.toLowerCase() === value.toLowerCase())
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
export const url = (validation, { apps, currentApp } = { apps: [] }) => {
|
||||
validation.addValidator(
|
||||
"url",
|
||||
string()
|
||||
.nullable()
|
||||
.matches(APP_URL_REGEX, "App URL must not contain spaces")
|
||||
.test(
|
||||
"non-existing-app-url",
|
||||
"Another app with the same URL already exists",
|
||||
value => {
|
||||
// url is nullable
|
||||
if (!value) {
|
||||
return true
|
||||
}
|
||||
if (currentApp) {
|
||||
// filter out the current app if present
|
||||
apps = apps.filter(app => app.appId !== currentApp.appId)
|
||||
}
|
||||
return !apps
|
||||
.map(app => app.url)
|
||||
.some(appUrl => appUrl?.toLowerCase() === value.toLowerCase())
|
||||
}
|
||||
)
|
||||
.test("valid-url", "Not a valid URL", value => {
|
||||
// url is nullable
|
||||
if (!value) {
|
||||
return true
|
||||
}
|
||||
// make it clear that this is a url path and cannot be a full url
|
||||
return (
|
||||
value.startsWith("/") &&
|
||||
!value.includes("http") &&
|
||||
!value.includes("www") &&
|
||||
!value.includes(".") &&
|
||||
value.length > 1 // just '/' is not valid
|
||||
)
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
export const file = (validation, { template } = {}) => {
|
||||
const templateToUse =
|
||||
template && Object.keys(template).length === 0 ? null : template
|
||||
validation.addValidator(
|
||||
"file",
|
||||
templateToUse?.fromFile
|
||||
? mixed().required("Please choose a file to import")
|
||||
: null
|
||||
)
|
||||
}
|
66
packages/builder/src/helpers/validation/yup/index.js
Normal file
66
packages/builder/src/helpers/validation/yup/index.js
Normal file
|
@ -0,0 +1,66 @@
|
|||
import { capitalise } from "helpers"
|
||||
import { object } from "yup"
|
||||
import { writable, get } from "svelte/store"
|
||||
import { notifications } from "@budibase/bbui"
|
||||
|
||||
export const createValidationStore = () => {
|
||||
const DEFAULT = {
|
||||
errors: {},
|
||||
touched: {},
|
||||
valid: false,
|
||||
}
|
||||
|
||||
const validator = {}
|
||||
const validation = writable(DEFAULT)
|
||||
|
||||
const addValidator = (propertyName, propertyValidator) => {
|
||||
if (!propertyValidator || !propertyName) {
|
||||
return
|
||||
}
|
||||
validator[propertyName] = propertyValidator
|
||||
}
|
||||
|
||||
const check = async values => {
|
||||
const obj = object().shape(validator)
|
||||
// clear the previous errors
|
||||
const properties = Object.keys(validator)
|
||||
properties.forEach(property => (get(validation).errors[property] = null))
|
||||
|
||||
let validationError = false
|
||||
try {
|
||||
await obj.validate(values, { abortEarly: false })
|
||||
} catch (error) {
|
||||
if (!error.inner) {
|
||||
notifications.error("Unexpected validation error", error)
|
||||
validationError = true
|
||||
} else {
|
||||
error.inner.forEach(err => {
|
||||
validation.update(store => {
|
||||
store.errors[err.path] = capitalise(err.message)
|
||||
return store
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
let valid
|
||||
if (properties.length && !validationError) {
|
||||
valid = await obj.isValid(values)
|
||||
} else {
|
||||
// don't say valid until validators have been loaded
|
||||
valid = false
|
||||
}
|
||||
|
||||
validation.update(store => {
|
||||
store.valid = valid
|
||||
return store
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
subscribe: validation.subscribe,
|
||||
set: validation.set,
|
||||
check,
|
||||
addValidator,
|
||||
}
|
||||
}
|
|
@ -61,7 +61,7 @@
|
|||
await auth.setInitInfo({ init_template: $params["?template"] })
|
||||
}
|
||||
|
||||
await auth.checkAuth()
|
||||
await auth.getSelf()
|
||||
await admin.init()
|
||||
|
||||
if (useAccountPortal && multiTenancyEnabled) {
|
||||
|
|
|
@ -12,7 +12,7 @@
|
|||
import Logo from "assets/bb-emblem.svg"
|
||||
import { capitalise } from "helpers"
|
||||
import UpgradeModal from "../../../../components/upgrade/UpgradeModal.svelte"
|
||||
import { onMount } from "svelte"
|
||||
import { onMount, onDestroy } from "svelte"
|
||||
|
||||
// Get Package and set store
|
||||
export let application
|
||||
|
@ -81,6 +81,10 @@
|
|||
hasSynced = true
|
||||
}
|
||||
})
|
||||
|
||||
onDestroy(() => {
|
||||
store.actions.reset()
|
||||
})
|
||||
</script>
|
||||
|
||||
{#await promise}
|
||||
|
|
|
@ -19,8 +19,8 @@
|
|||
import { IntegrationTypes } from "constants/backend"
|
||||
import { isEqual } from "lodash"
|
||||
import { cloneDeep } from "lodash/fp"
|
||||
|
||||
import ImportRestQueriesModal from "components/backend/DatasourceNavigator/modals/ImportRestQueriesModal.svelte"
|
||||
|
||||
let importQueriesModal
|
||||
|
||||
let changed
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue