1
0
Fork 0
mirror of synced 2024-06-26 10:00:41 +12:00

merge with develop

This commit is contained in:
Martin McKeaveney 2022-02-10 11:39:15 +01:00
commit 9144fdef6e
389 changed files with 7129 additions and 5684 deletions

View file

@ -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 />

214
i18n/README.jp.md Normal file
View 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)仕様に準拠しています。どのような貢献でも歓迎します。

View file

@ -1,5 +1,5 @@
{
"version": "1.0.49-alpha.5",
"version": "1.0.50-alpha.6",
"npmClient": "yarn",
"packages": [
"packages/*"

View file

@ -0,0 +1,17 @@
const {
getAppDB,
getDevAppDB,
getProdAppDB,
getAppId,
updateAppId,
doInAppContext,
} = require("./src/context")
module.exports = {
getAppDB,
getDevAppDB,
getProdAppDB,
getAppId,
updateAppId,
doInAppContext,
}

View file

@ -1,5 +1,6 @@
module.exports = {
...require("./src/db/utils"),
...require("./src/db/constants"),
...require("./src/db"),
...require("./src/db/views"),
}

View file

@ -1 +1 @@
module.exports = require("./src/tenancy/deprovision")
module.exports = require("./src/context/deprovision")

View file

@ -1,6 +1,6 @@
{
"name": "@budibase/backend-core",
"version": "1.0.49-alpha.5",
"version": "1.0.50-alpha.6",
"description": "Budibase backend core libraries used in server and worker",
"main": "src/index.js",
"author": "Budibase",

View file

@ -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
}

View file

@ -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

View 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)
}

View file

@ -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

View 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
}

View file

@ -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 })

View file

@ -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)
})
}

View file

@ -8,7 +8,7 @@ const { newid } = require("../../hashing")
const { createASession } = require("../../security/sessions")
const { getTenantId } = require("../../tenancy")
const INVALID_ERR = "Invalid Credentials"
const INVALID_ERR = "Invalid credentials"
const SSO_NO_PASSWORD = "SSO user does not have a password set"
const EXPIRED = "This account has expired. Please reset your password"

View file

@ -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 = (

View file

@ -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
}

View file

@ -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
}

View file

@ -1,4 +1,4 @@
module.exports = {
...require("./context"),
...require("../context"),
...require("./tenancy"),
}

View file

@ -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

View file

@ -256,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) {
@ -265,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(

View file

@ -1,7 +1,7 @@
{
"name": "@budibase/bbui",
"description": "A UI solution used in the different Budibase projects.",
"version": "1.0.49-alpha.5",
"version": "1.0.50-alpha.6",
"license": "MPL-2.0",
"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"
},

View file

@ -1,5 +1,6 @@
<script>
import "@spectrum-css/button/dist/index-vars.css"
import Tooltip from "../Tooltip/Tooltip.svelte"
export let disabled = false
export let size = "M"
@ -11,36 +12,67 @@
export let quiet = false
export let icon = undefined
export let active = false
export let tooltip = undefined
let showTooltip = false
</script>
<button
class:spectrum-Button--cta={cta}
class:spectrum-Button--primary={primary}
class:spectrum-Button--secondary={secondary}
class:spectrum-Button--warning={warning}
class:spectrum-Button--overBackground={overBackground}
class:spectrum-Button--quiet={quiet}
class:active
class="spectrum-Button spectrum-Button--size{size.toUpperCase()}"
{disabled}
on:click|preventDefault
>
{#if icon}
<svg
class="spectrum-Icon spectrum-Icon--size{size.toUpperCase()}"
focusable="false"
aria-hidden="true"
aria-label={icon}
>
<use xlink:href="#spectrum-icon-18-{icon}" />
</svg>
<div class:container={!!tooltip}>
<button
class:spectrum-Button--cta={cta}
class:spectrum-Button--primary={primary}
class:spectrum-Button--secondary={secondary}
class:spectrum-Button--warning={warning}
class:spectrum-Button--overBackground={overBackground}
class:spectrum-Button--quiet={quiet}
class:active
class="spectrum-Button spectrum-Button--size{size.toUpperCase()}"
{disabled}
on:click|preventDefault
on:mouseover={() => (showTooltip = true)}
on:mouseleave={() => (showTooltip = false)}
>
{#if icon}
<svg
class="spectrum-Icon spectrum-Icon--size{size.toUpperCase()}"
focusable="false"
aria-hidden="true"
aria-label={icon}
>
<use xlink:href="#spectrum-icon-18-{icon}" />
</svg>
{/if}
{#if $$slots}
<span class="spectrum-Button-label"><slot /></span>
{/if}
{#if !disabled && tooltip}
<div class="tooltip-icon">
<svg
class="spectrum-Icon spectrum-Icon--size{size.toUpperCase()}"
focusable="false"
aria-hidden="true"
aria-label="Info"
>
<use xlink:href="#spectrum-icon-18-InfoOutline" />
</svg>
</div>
{/if}
</button>
{#if showTooltip && tooltip}
<div class="position">
<div class="tooltip">
<Tooltip textWrapping={true} direction={"bottom"} text={tooltip} />
</div>
</div>
{/if}
{#if $$slots}
<span class="spectrum-Button-label"><slot /></span>
{/if}
</button>
</div>
<style>
.container {
display: flex;
align-items: center;
flex-direction: column;
}
.spectrum-Button-label {
white-space: nowrap;
overflow: hidden;
@ -49,4 +81,23 @@
.active {
color: var(--spectrum-global-color-blue-600) !important;
}
.tooltip {
position: absolute;
display: flex;
justify-content: center;
z-index: 100;
width: 160px;
text-align: center;
transform: translateX(-50%);
top: -5px;
}
.position {
position: relative;
width: 0;
height: 0;
}
.tooltip-icon {
padding-left: var(--spacing-m);
line-height: 0;
}
</style>

View file

@ -5,7 +5,7 @@
import { fly } from "svelte/transition"
import Icon from "../Icon/Icon.svelte"
import Input from "../Form/Input.svelte"
import { capitalise } from "../utils/helpers"
import { capitalise } from "../helpers"
export let value
export let size = "M"

View file

@ -5,7 +5,7 @@
import "@spectrum-css/textfield/dist/index-vars.css"
import "@spectrum-css/picker/dist/index-vars.css"
import { createEventDispatcher } from "svelte"
import { generateID } from "../../utils/helpers"
import { uuid } from "../../helpers"
export let id = null
export let disabled = false
@ -14,16 +14,20 @@
export let value = null
export let placeholder = null
export let appendTo = undefined
export let timeOnly = false
const dispatch = createEventDispatcher()
const flatpickrId = `${generateID()}-wrapper`
const flatpickrId = `${uuid()}-wrapper`
let open = false
let flatpickr
let flatpickr, flatpickrOptions, isTimeOnly
$: isTimeOnly = !timeOnly && value ? !isNaN(new Date(`0-${value}`)) : timeOnly
$: flatpickrOptions = {
element: `#${flatpickrId}`,
enableTime: enableTime || false,
enableTime: isTimeOnly || enableTime || false,
noCalendar: isTimeOnly || false,
altInput: true,
altFormat: enableTime ? "F j Y, H:i" : "F j, Y",
altFormat: isTimeOnly ? "H:i" : enableTime ? "F j Y, H:i" : "F j, Y",
wrap: true,
appendTo,
disableMobile: "true",
@ -35,6 +39,11 @@
if (newValue) {
newValue = newValue.toISOString()
}
// if time only set date component to today
if (timeOnly) {
const todayDate = new Date().toISOString().split("T")[0]
newValue = `${todayDate}T${newValue.split("T")[1]}`
}
dispatch("change", newValue)
}
@ -67,7 +76,11 @@
return null
}
let date
if (val instanceof Date) {
let time = new Date(`0-${val}`)
// it is a string like 00:00:00, just time
if (timeOnly || (typeof val === "string" && !isNaN(time))) {
date = time
} else if (val instanceof Date) {
// Use real date obj if already parsed
date = val
} else if (isNaN(val)) {
@ -77,7 +90,7 @@
// Treat as numerical timestamp
date = new Date(parseInt(val))
}
const time = date.getTime()
time = date.getTime()
if (isNaN(time)) {
return null
}
@ -88,69 +101,71 @@
}
</script>
<Flatpickr
bind:flatpickr
value={parseDate(value)}
on:open={onOpen}
on:close={onClose}
options={flatpickrOptions}
on:change={handleChange}
element={`#${flatpickrId}`}
>
<div
id={flatpickrId}
class:is-disabled={disabled}
class:is-invalid={!!error}
class="flatpickr spectrum-InputGroup spectrum-Datepicker"
class:is-focused={open}
aria-readonly="false"
aria-required="false"
aria-haspopup="true"
{#key isTimeOnly}
<Flatpickr
bind:flatpickr
value={parseDate(value)}
on:open={onOpen}
on:close={onClose}
options={flatpickrOptions}
on:change={handleChange}
element={`#${flatpickrId}`}
>
<div
on:click={flatpickr?.open}
class="spectrum-Textfield spectrum-InputGroup-textfield"
id={flatpickrId}
class:is-disabled={disabled}
class:is-invalid={!!error}
class="flatpickr spectrum-InputGroup spectrum-Datepicker"
class:is-focused={open}
aria-readonly="false"
aria-required="false"
aria-haspopup="true"
>
{#if !!error}
<div
on:click={flatpickr?.open}
class="spectrum-Textfield spectrum-InputGroup-textfield"
class:is-disabled={disabled}
class:is-invalid={!!error}
>
{#if !!error}
<svg
class="spectrum-Icon spectrum-Icon--sizeM spectrum-Textfield-validationIcon"
focusable="false"
aria-hidden="true"
>
<use xlink:href="#spectrum-icon-18-Alert" />
</svg>
{/if}
<input
data-input
type="text"
{disabled}
class="spectrum-Textfield-input spectrum-InputGroup-input"
{placeholder}
{id}
{value}
/>
</div>
<button
type="button"
class="spectrum-Picker spectrum-Picker--sizeM spectrum-InputGroup-button"
tabindex="-1"
{disabled}
class:is-invalid={!!error}
on:click={flatpickr?.open}
>
<svg
class="spectrum-Icon spectrum-Icon--sizeM spectrum-Textfield-validationIcon"
class="spectrum-Icon spectrum-Icon--sizeM"
focusable="false"
aria-hidden="true"
aria-label="Calendar"
>
<use xlink:href="#spectrum-icon-18-Alert" />
<use xlink:href="#spectrum-icon-18-Calendar" />
</svg>
{/if}
<input
data-input
type="text"
{disabled}
class="spectrum-Textfield-input spectrum-InputGroup-input"
{placeholder}
{id}
{value}
/>
</button>
</div>
<button
type="button"
class="spectrum-Picker spectrum-Picker--sizeM spectrum-InputGroup-button"
tabindex="-1"
{disabled}
class:is-invalid={!!error}
on:click={flatpickr?.open}
>
<svg
class="spectrum-Icon spectrum-Icon--sizeM"
focusable="false"
aria-hidden="true"
aria-label="Calendar"
>
<use xlink:href="#spectrum-icon-18-Calendar" />
</svg>
</button>
</div>
</Flatpickr>
</Flatpickr>
{/key}
{#if open}
<div class="overlay" on:mousedown|self={flatpickr?.close} />
{/if}

View file

@ -3,7 +3,7 @@
import "@spectrum-css/typography/dist/index-vars.css"
import "@spectrum-css/illustratedmessage/dist/index-vars.css"
import { createEventDispatcher } from "svelte"
import { generateID } from "../../utils/helpers"
import { uuid } from "../../helpers"
import Icon from "../../Icon/Icon.svelte"
import Link from "../../Link/Link.svelte"
import Tag from "../../Tags/Tag.svelte"
@ -37,7 +37,7 @@
"jfif",
]
const fieldId = id || generateID()
const fieldId = id || uuid()
let selectedImageIdx = 0
let fileDragged = false
let selectedUrl

View 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>

View file

@ -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)}

View file

@ -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>

View file

@ -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"

View file

@ -9,6 +9,7 @@
export let disabled = false
export let error = null
export let enableTime = true
export let timeOnly = false
export let placeholder = null
export let appendTo = undefined
@ -27,6 +28,7 @@
{value}
{placeholder}
{enableTime}
{timeOnly}
{appendTo}
on:change={onChange}
/>

View 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>

View 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}

View 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>

View 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>

View file

@ -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>

View file

@ -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>

View file

@ -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,
}
}

View file

@ -2,9 +2,18 @@
import dayjs from "dayjs"
export let value
// adding the 0- will turn a string like 00:00:00 into a valid ISO
// date, but will make actual ISO dates invalid
$: time = new Date(`0-${value}`)
$: isTime = !isNaN(time)
</script>
<div>{dayjs(value).format("MMMM D YYYY, HH:mm")}</div>
<div>
{dayjs(isTime ? time : value).format(
isTime ? "HH:mm:ss" : "MMMM D YYYY, HH:mm"
)}
</div>
<style>
div {

View file

@ -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>

View file

@ -4,7 +4,7 @@
import CellRenderer from "./CellRenderer.svelte"
import SelectEditRenderer from "./SelectEditRenderer.svelte"
import { cloneDeep } from "lodash"
import { deepGet } from "../utils/helpers"
import { deepGet } from "../helpers"
/**
* The expected schema is our normal couch schemas for our tables.

View file

@ -17,6 +17,7 @@
class:icon-small={size === "M" || size === "S"}
on:mouseover={() => (showTooltip = true)}
on:mouseleave={() => (showTooltip = false)}
on:focus
>
<Icon name="InfoOutline" size="S" disabled={true} />
</div>
@ -47,7 +48,7 @@
display: flex;
justify-content: center;
top: 15px;
z-index: 1;
z-index: 100;
width: 160px;
}
.icon {

View file

@ -1,11 +1,45 @@
export const generateID = () => {
const rand = Math.random().toString(32).substring(2)
// Starts with a letter so that its a valid DOM ID
return `A${rand}`
/**
* Generates a DOM safe UUID.
* Starting with a letter is important to make it DOM safe.
* @return {string} a random DOM safe UUID
*/
export function uuid() {
return "cxxxxxxxxxxxx4xxxyxxxxxxxxxxxxxxx".replace(/[xy]/g, c => {
const r = (Math.random() * 16) | 0
const v = c === "x" ? r : (r & 0x3) | 0x8
return v.toString(16)
})
}
export const capitalise = s => s.substring(0, 1).toUpperCase() + s.substring(1)
/**
* Capitalises a string
* @param string the string to capitalise
* @return {string} the capitalised string
*/
export const capitalise = string => {
if (!string) {
return string
}
return string.substring(0, 1).toUpperCase() + string.substring(1)
}
/**
* Computes a short hash of a string
* @param string the string to compute a hash of
* @return {string} the hash string
*/
export const hashString = string => {
if (!string) {
return "0"
}
let hash = 0
for (let i = 0; i < string.length; i++) {
let char = string.charCodeAt(i)
hash = (hash << 5) - hash + char
hash = hash & hash // Convert to 32bit integer
}
return hash.toString()
}
/**
* Gets a key within an object. The key supports dot syntax for retrieving deep

View file

@ -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"
@ -82,5 +85,5 @@ export { default as clickOutside } from "./Actions/click_outside"
// Stores
export { notifications, createNotificationStore } from "./Stores/notifications"
// Utils
export * from "./utils/helpers"
// Helpers
export * as Helpers from "./helpers"

View file

@ -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"

View file

@ -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/dist/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()
}

View file

@ -0,0 +1,4 @@
// @ts-ignore
import { run } from "../setup"
run("../../server/src/index", "../../worker/src/index")

View file

@ -1,22 +1,21 @@
{
"name": "@budibase/builder",
"version": "1.0.49-alpha.5",
"version": "1.0.50-alpha.6",
"license": "GPL-3.0",
"private": true,
"scripts": {
"build": "routify -b && vite build --emptyOutDir",
"start": "routify -c rollup",
"test": "jest",
"test:watch": "jest --watchAll",
"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 +64,10 @@
}
},
"dependencies": {
"@budibase/bbui": "^1.0.49-alpha.5",
"@budibase/client": "^1.0.49-alpha.5",
"@budibase/colorpicker": "1.1.2",
"@budibase/string-templates": "^1.0.49-alpha.5",
"@budibase/bbui": "^1.0.50-alpha.6",
"@budibase/client": "^1.0.50-alpha.6",
"@budibase/frontend-core": "^1.0.50-alpha.6",
"@budibase/string-templates": "^1.0.50-alpha.6",
"@sentry/browser": "5.19.1",
"@spectrum-css/page": "^3.0.1",
"@spectrum-css/vars": "^3.0.1",
@ -106,6 +105,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"

View file

@ -1,4 +1,4 @@
import api from "builderStore/api"
import { API } from "api"
import PosthogClient from "./PosthogClient"
import IntercomClient from "./IntercomClient"
import SentryClient from "./SentryClient"
@ -17,13 +17,11 @@ class AnalyticsHub {
}
async activate() {
const analyticsStatus = await api.get("/api/analytics")
const json = await analyticsStatus.json()
// Analytics disabled
if (!json.enabled) return
this.clients.forEach(client => client.init())
// Check analytics are enabled
const analyticsStatus = await API.getAnalyticsStatus()
if (analyticsStatus.enabled) {
this.clients.forEach(client => client.init())
}
}
identify(id, metadata) {

View file

@ -0,0 +1,48 @@
import {
createAPIClient,
CookieUtils,
Constants,
} from "@budibase/frontend-core"
import { store } from "./builderStore"
import { get } from "svelte/store"
import { auth } from "./stores/portal"
export const API = createAPIClient({
attachHeaders: headers => {
// Attach app ID header from store
headers["x-budibase-app-id"] = get(store).appId
// Add csrf token if authenticated
const user = get(auth).user
if (user?.csrfToken) {
headers["x-csrf-token"] = user.csrfToken
}
},
onError: error => {
const { url, message, status, method, handled } = error || {}
// Log all API errors to Sentry
// analytics.captureException(error)
// Log any errors that we haven't manually handled
if (!handled) {
console.error("Unhandled error from API client", error)
return
}
// Log all errors to console
console.warn(`[Builder] HTTP ${status} on ${method}:${url}\n\t${message}`)
// Logout on 403's
if (status === 403) {
// Remove cookies
CookieUtils.removeCookie(Constants.Cookies.Auth)
// Reload after removing cookie, go to login
if (!url.includes("self") && !url.includes("login")) {
location.reload()
}
}
},
})

View file

@ -1,49 +0,0 @@
import { store } from "./index"
import { get as svelteGet } from "svelte/store"
import { removeCookie, Cookies } from "./cookies"
import { auth } from "stores/portal"
const apiCall =
method =>
async (url, body, headers = { "Content-Type": "application/json" }) => {
headers["x-budibase-app-id"] = svelteGet(store).appId
headers["x-budibase-api-version"] = "1"
// add csrf token if authenticated
const user = svelteGet(auth).user
if (user && user.csrfToken) {
headers["x-csrf-token"] = user.csrfToken
}
const json = headers["Content-Type"] === "application/json"
const resp = await fetch(url, {
method: method,
body: json ? JSON.stringify(body) : body,
headers,
})
if (resp.status === 403) {
if (url.includes("/api/templates")) {
return { json: () => [] }
}
removeCookie(Cookies.Auth)
// reload after removing cookie, go to login
if (!url.includes("self") && !url.includes("login")) {
location.reload()
}
}
return resp
}
export const post = apiCall("POST")
export const get = apiCall("GET")
export const patch = apiCall("PATCH")
export const del = apiCall("DELETE")
export const put = apiCall("PUT")
export default {
post: apiCall("POST"),
get: apiCall("GET"),
patch: apiCall("PATCH"),
delete: apiCall("DELETE"),
put: apiCall("PUT"),
}

View file

@ -15,10 +15,7 @@ import {
encodeJSBinding,
} from "@budibase/string-templates"
import { TableNames } from "../constants"
import {
convertJSONSchemaToTableSchema,
getJSONArrayDatasourceSchema,
} from "./jsonUtils"
import { JSONUtils } from "@budibase/frontend-core"
import ActionDefinitions from "components/design/PropertiesPanel/PropertyControls/ButtonActionEditor/manifest.json"
// Regex to match all instances of template strings
@ -439,7 +436,7 @@ export const getSchemaForDatasource = (asset, datasource, isForm = false) => {
else if (type === "jsonarray") {
table = tables.find(table => table._id === datasource.tableId)
let tableSchema = table?.schema
schema = getJSONArrayDatasourceSchema(tableSchema, datasource)
schema = JSONUtils.getJSONArrayDatasourceSchema(tableSchema, datasource)
}
// Otherwise we assume we're targeting an internal table or a plus
@ -471,9 +468,12 @@ export const getSchemaForDatasource = (asset, datasource, isForm = false) => {
Object.keys(schema).forEach(fieldKey => {
const fieldSchema = schema[fieldKey]
if (fieldSchema?.type === "json") {
const jsonSchema = convertJSONSchemaToTableSchema(fieldSchema, {
squashObjects: true,
})
const jsonSchema = JSONUtils.convertJSONSchemaToTableSchema(
fieldSchema,
{
squashObjects: true,
}
)
Object.keys(jsonSchema).forEach(jsonKey => {
jsonAdditions[`${fieldKey}.${jsonKey}`] = {
type: jsonSchema[jsonKey].type,

View file

@ -1,16 +0,0 @@
import { get } from "builderStore/api"
/**
* Fetches the definitions for component library components. This includes
* their props and other metadata from components.json.
* @param {string} appId - ID of the currently running app
*/
export const fetchComponentLibDefinitions = async appId => {
const LIB_DEFINITION_URL = `/api/${appId}/components/definitions`
try {
const libDefinitionResponse = await get(LIB_DEFINITION_URL)
return await libDefinitionResponse.json()
} catch (err) {
console.error(`Error fetching component definitions for ${appId}`, err)
}
}

View file

@ -1,26 +1,40 @@
import { writable } from "svelte/store"
import api from "../../api"
import { API } from "api"
import Automation from "./Automation"
import { cloneDeep } from "lodash/fp"
import analytics, { Events } from "analytics"
const initialAutomationState = {
automations: [],
blockDefinitions: {
TRIGGER: [],
ACTION: [],
},
selectedAutomation: null,
}
export const getAutomationStore = () => {
const store = writable(initialAutomationState)
store.actions = automationActions(store)
return store
}
const automationActions = store => ({
fetch: async () => {
const responses = await Promise.all([
api.get(`/api/automations`),
api.get(`/api/automations/definitions/list`),
API.getAutomations(),
API.getAutomationDefinitions(),
])
const jsonResponses = await Promise.all(responses.map(x => x.json()))
store.update(state => {
let selected = state.selectedAutomation?.automation
state.automations = jsonResponses[0]
state.automations = responses[0]
state.blockDefinitions = {
TRIGGER: jsonResponses[1].trigger,
ACTION: jsonResponses[1].action,
TRIGGER: responses[1].trigger,
ACTION: responses[1].action,
}
// if previously selected find the new obj and select it
// If previously selected find the new obj and select it
if (selected) {
selected = jsonResponses[0].filter(
selected = responses[0].filter(
automation => automation._id === selected._id
)
state.selectedAutomation = new Automation(selected[0])
@ -36,40 +50,36 @@ const automationActions = store => ({
steps: [],
},
}
const CREATE_AUTOMATION_URL = `/api/automations`
const response = await api.post(CREATE_AUTOMATION_URL, automation)
const json = await response.json()
const response = await API.createAutomation(automation)
store.update(state => {
state.automations = [...state.automations, json.automation]
store.actions.select(json.automation)
state.automations = [...state.automations, response.automation]
store.actions.select(response.automation)
return state
})
},
save: async automation => {
const UPDATE_AUTOMATION_URL = `/api/automations`
const response = await api.put(UPDATE_AUTOMATION_URL, automation)
const json = await response.json()
const response = await API.updateAutomation(automation)
store.update(state => {
const newAutomation = json.automation
const updatedAutomation = response.automation
const existingIdx = state.automations.findIndex(
existing => existing._id === automation._id
)
if (existingIdx !== -1) {
state.automations.splice(existingIdx, 1, newAutomation)
state.automations.splice(existingIdx, 1, updatedAutomation)
state.automations = [...state.automations]
store.actions.select(newAutomation)
store.actions.select(updatedAutomation)
return state
}
})
},
delete: async automation => {
const { _id, _rev } = automation
const DELETE_AUTOMATION_URL = `/api/automations/${_id}/${_rev}`
await api.delete(DELETE_AUTOMATION_URL)
await API.deleteAutomation({
automationId: automation?._id,
automationRev: automation?._rev,
})
store.update(state => {
const existingIdx = state.automations.findIndex(
existing => existing._id === _id
existing => existing._id === automation?._id
)
state.automations.splice(existingIdx, 1)
state.automations = [...state.automations]
@ -78,16 +88,17 @@ const automationActions = store => ({
return state
})
},
trigger: async automation => {
const { _id } = automation
return await api.post(`/api/automations/${_id}/trigger`)
},
test: async (automation, testData) => {
const { _id } = automation
const response = await api.post(`/api/automations/${_id}/test`, testData)
const json = await response.json()
store.update(state => {
state.selectedAutomation.testResults = json
state.selectedAutomation.testResults = null
return state
})
const result = await API.testAutomation({
automationId: automation?._id,
testData,
})
store.update(state => {
state.selectedAutomation.testResults = result
return state
})
},
@ -143,17 +154,3 @@ const automationActions = store => ({
})
},
})
export const getAutomationStore = () => {
const INITIAL_AUTOMATION_STATE = {
automations: [],
blockDefinitions: {
TRIGGER: [],
ACTION: [],
},
selectedAutomation: null,
}
const store = writable(INITIAL_AUTOMATION_STATE)
store.actions = automationActions(store)
return store
}

View file

@ -14,8 +14,7 @@ import {
database,
tables,
} from "stores/backend"
import { fetchComponentLibDefinitions } from "../loadComponentLibraries"
import api from "../api"
import { API } from "api"
import { FrontendTypes } from "constants"
import analytics, { Events } from "analytics"
import {
@ -26,7 +25,7 @@ import {
findComponent,
getComponentSettings,
} from "../componentUtils"
import { uuid } from "../uuid"
import { Helpers } from "@budibase/bbui"
import { removeBindings } from "../dataBinding"
const INITIAL_FRONTEND_STATE = {
@ -65,17 +64,17 @@ 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)
// make sure app isn't locked
if (
components &&
components.status === 400 &&
components.message?.includes("lock")
) {
throw { ok: false, reason: "locked" }
}
// Fetch component definitions.
// Allow errors to propagate.
let components = await API.fetchComponentLibDefinitions(application.appId)
// Reset store state
store.update(state => ({
...state,
libraries: application.componentLibraries,
@ -88,8 +87,8 @@ export const getFrontendStore = () => {
description: application.description,
appId: application.appId,
url: application.url,
layouts,
screens,
layouts: layouts || [],
screens: screens || [],
theme: application.theme || "spectrum--light",
customTheme: application.customTheme,
hasAppPackage: true,
@ -101,51 +100,43 @@ export const getFrontendStore = () => {
}))
// Initialise backend stores
const [_integrations] = await Promise.all([
api.get("/api/integrations").then(r => r.json()),
])
datasources.init()
integrations.set(_integrations)
queries.init()
database.set(application.instance)
tables.init()
await datasources.init()
await integrations.init()
await queries.init()
await tables.init()
},
theme: {
save: async theme => {
const appId = get(store).appId
const response = await api.put(`/api/applications/${appId}`, { theme })
if (response.status === 200) {
store.update(state => {
state.theme = theme
return state
})
} else {
throw new Error("Error updating theme")
}
await API.saveAppMetadata({
appId,
metadata: { theme },
})
store.update(state => {
state.theme = theme
return state
})
},
},
customTheme: {
save: async customTheme => {
const appId = get(store).appId
const response = await api.put(`/api/applications/${appId}`, {
customTheme,
await API.saveAppMetadata({
appId,
metadata: { customTheme },
})
store.update(state => {
state.customTheme = customTheme
return state
})
if (response.status === 200) {
store.update(state => {
state.customTheme = customTheme
return state
})
} else {
throw new Error("Error updating theme")
}
},
},
routing: {
fetch: async () => {
const response = await api.get("/api/routing")
const json = await response.json()
const response = await API.fetchAppRoutes()
store.update(state => {
state.routes = json.routes
state.routes = response.routes
return state
})
},
@ -169,82 +160,76 @@ export const getFrontendStore = () => {
return state
})
},
create: async screen => {
screen = await store.actions.screens.save(screen)
store.update(state => {
state.selectedScreenId = screen._id
state.selectedComponentId = screen.props._id
state.currentFrontEndType = FrontendTypes.SCREEN
selectedAccessRole.set(screen.routing.roleId)
return state
})
return screen
},
save: async screen => {
const creatingNewScreen = screen._id === undefined
const response = await api.post(`/api/screens`, screen)
if (response.status !== 200) {
return
}
screen = await response.json()
await store.actions.routing.fetch()
const savedScreen = await API.saveScreen(screen)
store.update(state => {
const foundScreen = state.screens.findIndex(
el => el._id === screen._id
)
if (foundScreen !== -1) {
state.screens.splice(foundScreen, 1)
const idx = state.screens.findIndex(x => x._id === savedScreen._id)
if (idx !== -1) {
state.screens.splice(idx, 1, savedScreen)
} else {
state.screens.push(savedScreen)
}
state.screens.push(screen)
return state
})
if (creatingNewScreen) {
store.actions.screens.select(screen._id)
}
// Refresh routes
await store.actions.routing.fetch()
return screen
// Select the new screen if creating a new one
if (creatingNewScreen) {
store.actions.screens.select(savedScreen._id)
}
return savedScreen
},
delete: async screens => {
const screensToDelete = Array.isArray(screens) ? screens : [screens]
const screenDeletePromises = []
// Build array of promises to speed up bulk deletions
const promises = []
screensToDelete.forEach(screen => {
// Delete the screen
promises.push(
API.deleteScreen({
screenId: screen._id,
screenRev: screen._rev,
})
)
// Remove links to this screen
promises.push(
store.actions.components.links.delete(
screen.routing.route,
screen.props._instanceName
)
)
})
await Promise.all(promises)
const deletedIds = screensToDelete.map(screen => screen._id)
store.update(state => {
for (let screenToDelete of screensToDelete) {
state.screens = state.screens.filter(
screen => screen._id !== screenToDelete._id
)
screenDeletePromises.push(
api.delete(
`/api/screens/${screenToDelete._id}/${screenToDelete._rev}`
)
)
if (screenToDelete._id === state.selectedScreenId) {
state.selectedScreenId = null
}
//remove the link for this screen
screenDeletePromises.push(
store.actions.components.links.delete(
screenToDelete.routing.route,
screenToDelete.props._instanceName
)
)
// Remove deleted screens from state
state.screens = state.screens.filter(screen => {
return !deletedIds.includes(screen._id)
})
// Deselect the current screen if it was deleted
if (deletedIds.includes(state.selectedScreenId)) {
state.selectedScreenId = null
}
return state
})
await Promise.all(screenDeletePromises)
// Refresh routes
await store.actions.routing.fetch()
},
},
preview: {
saveSelected: async () => {
const state = get(store)
const selectedAsset = get(currentAsset)
if (state.currentFrontEndType !== FrontendTypes.LAYOUT) {
await store.actions.screens.save(selectedAsset)
return await store.actions.screens.save(selectedAsset)
} else {
await store.actions.layouts.save(selectedAsset)
return await store.actions.layouts.save(selectedAsset)
}
},
setDevice: device => {
@ -268,25 +253,13 @@ export const getFrontendStore = () => {
})
},
save: async layout => {
const layoutToSave = cloneDeep(layout)
const creatingNewLayout = layoutToSave._id === undefined
const response = await api.post(`/api/layouts`, layoutToSave)
const savedLayout = await response.json()
// Abort if saving failed
if (response.status !== 200) {
return
}
const creatingNewLayout = layout._id === undefined
const savedLayout = await API.saveLayout(layout)
store.update(state => {
const layoutIdx = state.layouts.findIndex(
stateLayout => stateLayout._id === savedLayout._id
)
if (layoutIdx >= 0) {
// update existing layout
state.layouts.splice(layoutIdx, 1, savedLayout)
const idx = state.layouts.findIndex(x => x._id === savedLayout._id)
if (idx !== -1) {
state.layouts.splice(idx, 1, savedLayout)
} else {
// save new layout
state.layouts.push(savedLayout)
}
return state
@ -296,7 +269,6 @@ export const getFrontendStore = () => {
if (creatingNewLayout) {
store.actions.layouts.select(savedLayout._id)
}
return savedLayout
},
find: layoutId => {
@ -306,21 +278,20 @@ export const getFrontendStore = () => {
const storeContents = get(store)
return storeContents.layouts.find(layout => layout._id === layoutId)
},
delete: async layoutToDelete => {
const response = await api.delete(
`/api/layouts/${layoutToDelete._id}/${layoutToDelete._rev}`
)
if (response.status !== 200) {
const json = await response.json()
throw new Error(json.message)
delete: async layout => {
if (!layout?._id) {
return
}
await API.deleteLayout({
layoutId: layout._id,
layoutRev: layout._rev,
})
store.update(state => {
state.layouts = state.layouts.filter(
layout => layout._id !== layoutToDelete._id
)
if (layoutToDelete._id === state.selectedLayoutId) {
// Select main layout if we deleted the selected layout
if (layout._id === state.selectedLayoutId) {
state.selectedLayoutId = get(mainLayout)._id
}
state.layouts = state.layouts.filter(x => x._id !== layout._id)
return state
})
},
@ -395,7 +366,7 @@ export const getFrontendStore = () => {
}
return {
_id: uuid(),
_id: Helpers.uuid(),
_component: definition.component,
_styles: { normal: {}, hover: {}, active: {} },
_instanceName: `New ${definition.name}`,
@ -412,16 +383,12 @@ export const getFrontendStore = () => {
componentName,
presetProps
)
if (!componentInstance) {
if (!componentInstance || !asset) {
return
}
// Find parent node to attach this component to
let parentComponent
if (!asset) {
return
}
if (selected) {
// Use current screen or layout as parent if no component is selected
const definition = store.actions.components.getDefinition(
@ -549,7 +516,7 @@ export const getFrontendStore = () => {
if (!component) {
return
}
component._id = uuid()
component._id = Helpers.uuid()
component._children?.forEach(randomizeIds)
}
randomizeIds(componentToPaste)
@ -603,11 +570,6 @@ export const getFrontendStore = () => {
selected._styles.custom = style
await store.actions.preview.saveSelected()
},
resetStyles: async () => {
const selected = get(selectedComponent)
selected._styles = { normal: {}, hover: {}, active: {} }
await store.actions.preview.saveSelected()
},
updateConditions: async conditions => {
const selected = get(selectedComponent)
selected._conditions = conditions
@ -662,7 +624,7 @@ export const getFrontendStore = () => {
newLink = cloneDeep(nav._children[0])
// Set our new props
newLink._id = uuid()
newLink._id = Helpers.uuid()
newLink._instanceName = `${title} Link`
newLink.url = url
newLink.text = title

View file

@ -1,23 +0,0 @@
import { writable } from "svelte/store"
import { generate } from "shortid"
export const notificationStore = writable({
notifications: [],
})
export function send(message, type = "default") {
notificationStore.update(state => {
state.notifications = [
...state.notifications,
{ id: generate(), type, message },
]
return state
})
}
export const notifier = {
danger: msg => send(msg, "danger"),
warning: msg => send(msg, "warning"),
info: msg => send(msg, "info"),
success: msg => send(msg, "success"),
}

View file

@ -1,4 +1,4 @@
import { uuid } from "builderStore/uuid"
import { Helpers } from "@budibase/bbui"
import { BaseStructure } from "./BaseStructure"
export class Component extends BaseStructure {
@ -6,7 +6,7 @@ export class Component extends BaseStructure {
super(false)
this._children = []
this._json = {
_id: uuid(),
_id: Helpers.uuid(),
_component: name,
_styles: {
normal: {},

View file

@ -1,5 +1,5 @@
import { BaseStructure } from "./BaseStructure"
import { uuid } from "builderStore/uuid"
import { Helpers } from "@budibase/bbui"
export class Screen extends BaseStructure {
constructor() {
@ -7,7 +7,7 @@ export class Screen extends BaseStructure {
this._json = {
layoutId: "layout_private_master",
props: {
_id: uuid(),
_id: Helpers.uuid(),
_component: "@budibase/standard-components/container",
_styles: {
normal: {},

View file

@ -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",

View file

@ -1,4 +1,4 @@
import { localStorageStore } from "./localStorage"
import { createLocalStorageStore } from "@budibase/frontend-core"
export const getThemeStore = () => {
const themeElement = document.documentElement
@ -6,7 +6,7 @@ export const getThemeStore = () => {
theme: "darkest",
options: ["lightest", "light", "dark", "darkest"],
}
const store = localStorageStore("bb-theme", initialValue)
const store = createLocalStorageStore("bb-theme", initialValue)
// Update theme class when store changes
store.subscribe(state => {

View file

@ -1,9 +0,0 @@
export function uuid() {
// always want to make this start with a letter, as this makes it
// easier to use with template string bindings in the client
return "cxxxxxxxxxxxx4xxxyxxxxxxxxxxxxxxx".replace(/[xy]/g, c => {
const r = (Math.random() * 16) | 0,
v = c == "x" ? r : (r & 0x3) | 0x8
return v.toString(16)
})
}

View file

@ -6,6 +6,7 @@
Body,
Icon,
Tooltip,
notifications,
} from "@budibase/bbui"
import { automationStore } from "builderStore"
import { admin } from "stores/portal"
@ -47,15 +48,19 @@
}
async function addBlockToAutomation() {
const newBlock = $automationStore.selectedAutomation.constructBlock(
"ACTION",
actionVal.stepId,
actionVal
)
automationStore.actions.addBlockToAutomation(newBlock, blockIdx + 1)
await automationStore.actions.save(
$automationStore.selectedAutomation?.automation
)
try {
const newBlock = $automationStore.selectedAutomation.constructBlock(
"ACTION",
actionVal.stepId,
actionVal
)
automationStore.actions.addBlockToAutomation(newBlock, blockIdx + 1)
await automationStore.actions.save(
$automationStore.selectedAutomation?.automation
)
} catch (error) {
notifications.error("Error saving automation")
}
}
</script>

View file

@ -30,26 +30,13 @@
}
async function deleteAutomation() {
await automationStore.actions.delete(
$automationStore.selectedAutomation?.automation
)
notifications.success("Automation deleted.")
}
async function testAutomation() {
const result = await automationStore.actions.trigger(
$automationStore.selectedAutomation.automation
)
if (result.status === 200) {
notifications.success(
`Automation ${$automationStore.selectedAutomation.automation.name} triggered successfully.`
)
} else {
notifications.error(
`Failed to trigger automation ${$automationStore.selectedAutomation.automation.name}.`
try {
await automationStore.actions.delete(
$automationStore.selectedAutomation?.automation
)
} catch (error) {
notifications.error("Error deleting automation")
}
return result
}
</script>
@ -85,7 +72,7 @@
animate:flip={{ duration: 500 }}
in:fly|local={{ x: 500, duration: 1500 }}
>
<FlowItem {testDataModal} {testAutomation} {onSelect} {block} />
<FlowItem {testDataModal} {onSelect} {block} />
</div>
{/each}
</div>
@ -101,7 +88,7 @@
</ConfirmDialog>
<Modal bind:this={testDataModal} width="30%">
<TestDataModal {testAutomation} />
<TestDataModal />
</Modal>
</div>

View file

@ -10,6 +10,7 @@
Button,
StatusLight,
ActionButton,
notifications,
} from "@budibase/bbui"
import AutomationBlockSetup from "../../SetupPanel/AutomationBlockSetup.svelte"
import CreateWebhookModal from "components/automation/Shared/CreateWebhookModal.svelte"
@ -54,10 +55,14 @@
).every(x => block?.inputs[x])
async function deleteStep() {
automationStore.actions.deleteAutomationBlock(block)
await automationStore.actions.save(
$automationStore.selectedAutomation?.automation
)
try {
automationStore.actions.deleteAutomationBlock(block)
await automationStore.actions.save(
$automationStore.selectedAutomation?.automation
)
} catch (error) {
notifications.error("Error saving notification")
}
}
</script>

View file

@ -1,5 +1,12 @@
<script>
import { ModalContent, Tabs, Tab, TextArea, Label } from "@budibase/bbui"
import {
ModalContent,
Tabs,
Tab,
TextArea,
Label,
notifications,
} from "@budibase/bbui"
import { automationStore } from "builderStore"
import AutomationBlockSetup from "../../SetupPanel/AutomationBlockSetup.svelte"
import { cloneDeep } from "lodash/fp"
@ -37,6 +44,17 @@
failedParse = "Invalid JSON"
}
}
const testAutomation = async () => {
try {
await automationStore.actions.test(
$automationStore.selectedAutomation?.automation,
testData
)
} catch (error) {
notifications.error("Error testing notification")
}
}
</script>
<ModalContent
@ -44,12 +62,7 @@
confirmText="Test"
showConfirmButton={true}
disabled={isError}
onConfirm={() => {
automationStore.actions.test(
$automationStore.selectedAutomation?.automation,
testData
)
}}
onConfirm={testAutomation}
cancelText="Cancel"
>
<Tabs selected="Form" quiet

View file

@ -4,10 +4,16 @@
import { automationStore } from "builderStore"
import NavItem from "components/common/NavItem.svelte"
import EditAutomationPopover from "./EditAutomationPopover.svelte"
import { notifications } from "@budibase/bbui"
$: selectedAutomationId = $automationStore.selectedAutomation?.automation?._id
onMount(() => {
automationStore.actions.fetch()
onMount(async () => {
try {
await automationStore.actions.fetch()
} catch (error) {
notifications.error("Error getting automations list")
}
})
function selectAutomation(automation) {

View file

@ -24,29 +24,33 @@
nameTouched && !name ? "Please specify a name for the automation." : null
async function createAutomation() {
await automationStore.actions.create({
name,
instanceId,
})
const newBlock = $automationStore.selectedAutomation.constructBlock(
"TRIGGER",
triggerVal.stepId,
triggerVal
)
try {
await automationStore.actions.create({
name,
instanceId,
})
const newBlock = $automationStore.selectedAutomation.constructBlock(
"TRIGGER",
triggerVal.stepId,
triggerVal
)
automationStore.actions.addBlockToAutomation(newBlock)
if (triggerVal.stepId === "WEBHOOK") {
webhookModal.show
automationStore.actions.addBlockToAutomation(newBlock)
if (triggerVal.stepId === "WEBHOOK") {
webhookModal.show
}
await automationStore.actions.save(
$automationStore.selectedAutomation?.automation
)
notifications.success(`Automation ${name} created`)
$goto(`./${$automationStore.selectedAutomation.automation._id}`)
analytics.captureEvent(Events.AUTOMATION.CREATED, { name })
} catch (error) {
notifications.error("Error creating automation")
}
await automationStore.actions.save(
$automationStore.selectedAutomation?.automation
)
notifications.success(`Automation ${name} created.`)
$goto(`./${$automationStore.selectedAutomation.automation._id}`)
analytics.captureEvent(Events.AUTOMATION.CREATED, { name })
}
$: triggers = Object.entries($automationStore.blockDefinitions.TRIGGER)

View file

@ -11,9 +11,13 @@
let updateAutomationDialog
async function deleteAutomation() {
await automationStore.actions.delete(automation)
notifications.success("Automation deleted.")
$goto("../automate")
try {
await automationStore.actions.delete(automation)
notifications.success("Automation deleted successfully")
$goto("../automate")
} catch (error) {
notifications.error("Error deleting automation")
}
}
</script>

View file

@ -20,14 +20,18 @@
}
async function saveAutomation() {
const updatedAutomation = {
...automation,
name,
try {
const updatedAutomation = {
...automation,
name,
}
await automationStore.actions.save(updatedAutomation)
notifications.success(`Automation ${name} updated successfully`)
analytics.captureEvent(Events.AUTOMATION.SAVED, { name })
hide()
} catch (error) {
notifications.error("Error saving automation")
}
await automationStore.actions.save(updatedAutomation)
notifications.success(`Automation ${name} updated successfully.`)
analytics.captureEvent(Events.AUTOMATION.SAVED, { name })
hide()
}
function checkValid(evt) {

View file

@ -11,6 +11,7 @@
Drawer,
Modal,
Detail,
notifications,
} from "@budibase/bbui"
import CreateWebhookModal from "components/automation/Shared/CreateWebhookModal.svelte"
@ -28,7 +29,7 @@
import ModalBindableInput from "components/common/bindings/ModalBindableInput.svelte"
import FilterDrawer from "components/design/PropertiesPanel/PropertyControls/FilterEditor/FilterDrawer.svelte"
// need the client lucene builder to convert to the structure API expects
import { buildLuceneQuery } from "helpers/lucene"
import { LuceneUtils } from "@budibase/frontend-core"
export let block
export let testData
@ -54,28 +55,32 @@
$: schemaFields = table ? Object.values(table.schema) : []
const onChange = debounce(async function (e, key) {
if (isTestModal) {
// Special case for webhook, as it requires a body, but the schema already brings back the body's contents
if (stepId === "WEBHOOK") {
try {
if (isTestModal) {
// Special case for webhook, as it requires a body, but the schema already brings back the body's contents
if (stepId === "WEBHOOK") {
automationStore.actions.addTestDataToAutomation({
body: {
[key]: e.detail,
...$automationStore.selectedAutomation.automation.testData.body,
},
})
}
automationStore.actions.addTestDataToAutomation({
body: {
[key]: e.detail,
...$automationStore.selectedAutomation.automation.testData.body,
},
[key]: e.detail,
})
testData[key] = e.detail
await automationStore.actions.save(
$automationStore.selectedAutomation?.automation
)
} else {
block.inputs[key] = e.detail
await automationStore.actions.save(
$automationStore.selectedAutomation?.automation
)
}
automationStore.actions.addTestDataToAutomation({
[key]: e.detail,
})
testData[key] = e.detail
await automationStore.actions.save(
$automationStore.selectedAutomation?.automation
)
} else {
block.inputs[key] = e.detail
await automationStore.actions.save(
$automationStore.selectedAutomation?.automation
)
} catch (error) {
notifications.error("Error saving automation")
}
}, 800)
@ -131,7 +136,7 @@
}
function saveFilters(key) {
const filters = buildLuceneQuery(tempFilters)
const filters = LuceneUtils.buildLuceneQuery(tempFilters)
const defKey = `${key}-def`
inputData[key] = filters
inputData[defKey] = tempFilters

View file

@ -1,5 +1,5 @@
<script>
import { Icon } from "@budibase/bbui"
import { Icon, notifications } from "@budibase/bbui"
import { automationStore } from "builderStore"
import WebhookDisplay from "./WebhookDisplay.svelte"
import { ModalContent } from "@budibase/bbui"
@ -16,15 +16,24 @@
onMount(async () => {
if (!automation?.definition?.trigger?.inputs.schemaUrl) {
// save the automation initially
await automationStore.actions.save(automation)
try {
await automationStore.actions.save(automation)
} catch (error) {
notifications.error("Error saving automation")
}
}
interval = setInterval(async () => {
await automationStore.actions.fetch()
const outputs = automation?.definition?.trigger.schema.outputs?.properties
// always one prop for the "body"
if (Object.keys(outputs).length > 1) {
propCount = Object.keys(outputs).length - 1
finished = true
try {
await automationStore.actions.fetch()
const outputs =
automation?.definition?.trigger.schema.outputs?.properties
// always one prop for the "body"
if (Object.keys(outputs).length > 1) {
propCount = Object.keys(outputs).length - 1
finished = true
}
} catch (error) {
notifications.error("Error getting automations list")
}
}, POLL_RATE_MS)
schemaURL = automation?.definition?.trigger?.inputs.schemaUrl

View file

@ -14,18 +14,19 @@
import Table from "./Table.svelte"
import { TableNames } from "constants"
import CreateEditRow from "./modals/CreateEditRow.svelte"
import { fetchTableData } from "helpers/fetchTableData"
import { Pagination } from "@budibase/bbui"
import { fetchData } from "@budibase/frontend-core"
import { API } from "api"
let hideAutocolumns = true
$: isUsersTable = $tables.selected?._id === TableNames.USERS
$: type = $tables.selected?.type
$: isInternal = type !== "external"
$: schema = $tables.selected?.schema
$: enrichedSchema = enrichSchema($tables.selected?.schema)
$: id = $tables.selected?._id
$: search = searchTable(id)
$: columnOptions = Object.keys($search.schema || {})
$: fetch = createFetch(id)
const enrichSchema = schema => {
let tempSchema = { ...schema }
@ -47,18 +48,24 @@
return tempSchema
}
// Fetches new data whenever the table changes
const searchTable = tableId => {
return fetchTableData({
tableId,
schema,
limit: 10,
paginate: true,
const createFetch = tableId => {
return fetchData({
API,
datasource: {
tableId,
type: "table",
},
options: {
schema,
limit: 10,
paginate: true,
},
})
}
// Fetch data whenever sorting option changes
const onSort = e => {
search.update({
fetch.update({
sortColumn: e.detail.column,
sortOrder: e.detail.order,
})
@ -66,22 +73,20 @@
// Fetch data whenever filters change
const onFilter = e => {
search.update({
filters: e.detail,
fetch.update({
filter: e.detail,
})
}
// Fetch data whenever schema changes
const onUpdateColumns = () => {
search.update({
schema,
})
fetch.refresh()
}
// Fetch data whenever rows are modified. Unfortunately we have to lose
// our pagination place, as our bookmarks will have shifted.
const onUpdateRows = () => {
search.update()
fetch.refresh()
}
</script>
@ -91,9 +96,9 @@
schema={enrichedSchema}
{type}
tableId={id}
data={$search.rows}
data={$fetch.rows}
bind:hideAutocolumns
loading={$search.loading}
loading={$fetch.loading}
on:sort={onSort}
allowEditing
disableSorting
@ -138,11 +143,11 @@
<div in:fade={{ delay: 200, duration: 100 }}>
<div class="pagination">
<Pagination
page={$search.pageNumber + 1}
hasPrevPage={$search.hasPrevPage}
hasNextPage={$search.hasNextPage}
goToPrevPage={$search.loading ? null : search.prevPage}
goToNextPage={$search.loading ? null : search.nextPage}
page={$fetch.pageNumber + 1}
hasPrevPage={$fetch.hasPrevPage}
hasNextPage={$fetch.hasNextPage}
goToPrevPage={$fetch.loading ? null : fetch.prevPage}
goToNextPage={$fetch.loading ? null : fetch.nextPage}
/>
</div>
</div>

View file

@ -1,7 +1,8 @@
<script>
import api from "builderStore/api"
import { API } from "api"
import Table from "./Table.svelte"
import { tables } from "stores/backend"
import { notifications } from "@budibase/bbui"
export let tableId
export let rowId
@ -27,9 +28,15 @@
}
async function fetchData(tableId, rowId) {
const QUERY_VIEW_URL = `/api/${tableId}/${rowId}/enrich`
const response = await api.get(QUERY_VIEW_URL)
row = await response.json()
try {
row = await API.fetchRelationshipData({
tableId,
rowId,
})
} catch (error) {
row = null
notifications.error("Error fetching relationship data")
}
}
</script>

View file

@ -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

View file

@ -2,7 +2,7 @@
import { fade } from "svelte/transition"
import { goto, params } from "@roxi/routify"
import { Table, Modal, Heading, notifications, Layout } from "@budibase/bbui"
import api from "builderStore/api"
import { API } from "api"
import Spinner from "components/common/Spinner.svelte"
import DeleteRowsButton from "./buttons/DeleteRowsButton.svelte"
import CreateEditRow from "./modals/CreateEditRow.svelte"
@ -88,12 +88,17 @@
}
const deleteRows = async () => {
await api.delete(`/api/${tableId}/rows`, {
rows: selectedRows,
})
data = data.filter(row => !selectedRows.includes(row))
notifications.success(`Successfully deleted ${selectedRows.length} rows`)
selectedRows = []
try {
await API.deleteRows({
tableId,
rows: selectedRows,
})
data = data.filter(row => !selectedRows.includes(row))
notifications.success(`Successfully deleted ${selectedRows.length} rows`)
selectedRows = []
} catch (error) {
notifications.error("Error deleting rows")
}
}
const editRow = row => {

View file

@ -1,5 +1,5 @@
<script>
import api from "builderStore/api"
import { API } from "api"
import { tables } from "stores/backend"
import Table from "./Table.svelte"
@ -9,6 +9,7 @@
import ExportButton from "./buttons/ExportButton.svelte"
import ManageAccessButton from "./buttons/ManageAccessButton.svelte"
import HideAutocolumnButton from "./buttons/HideAutocolumnButton.svelte"
import { notifications } from "@budibase/bbui"
export let view = {}
@ -20,33 +21,31 @@
$: name = view.name
// Fetch rows for specified view
$: {
loading = true
fetchViewData(name, view.field, view.groupBy, view.calculation)
}
$: fetchViewData(name, view.field, view.groupBy, view.calculation)
async function fetchViewData(name, field, groupBy, calculation) {
loading = true
const _tables = $tables.list
const allTableViews = _tables.map(table => table.views)
const thisView = allTableViews.filter(
views => views != null && views[name] != null
)[0]
// don't fetch view data if the view no longer exists
// Don't fetch view data if the view no longer exists
if (!thisView) {
loading = false
return
}
const params = new URLSearchParams()
if (calculation) {
params.set("field", field)
params.set("calculation", calculation)
try {
data = await API.fetchViewData({
name,
calculation,
field,
groupBy,
})
} catch (error) {
notifications.error("Error fetching view data")
}
if (groupBy) {
params.set("group", groupBy)
}
const QUERY_VIEW_URL = `/api/views/${name}?${params}`
const response = await api.get(QUERY_VIEW_URL)
data = await response.json()
loading = false
}
</script>

View file

@ -1,34 +0,0 @@
import api from "builderStore/api"
export async function createUser(user) {
const CREATE_USER_URL = `/api/users/metadata`
const response = await api.post(CREATE_USER_URL, user)
return await response.json()
}
export async function saveRow(row, tableId) {
const SAVE_ROW_URL = `/api/${tableId}/rows`
const response = await api.post(SAVE_ROW_URL, row)
return await response.json()
}
export async function deleteRow(row) {
const DELETE_ROWS_URL = `/api/${row.tableId}/rows`
return api.delete(DELETE_ROWS_URL, {
_id: row._id,
_rev: row._rev,
})
}
export async function fetchDataForTable(tableId) {
const FETCH_ROWS_URL = `/api/${tableId}/rows`
const response = await api.get(FETCH_ROWS_URL)
const json = await response.json()
if (response.status !== 200) {
throw new Error(json.message)
}
return json
}

View file

@ -38,9 +38,13 @@
})
function saveView() {
views.save(view)
notifications.success(`View ${view.name} saved.`)
analytics.captureEvent(Events.VIEW.ADDED_CALCULATE, { field: view.field })
try {
views.save(view)
notifications.success(`View ${view.name} saved`)
analytics.captureEvent(Events.VIEW.ADDED_CALCULATE, { field: view.field })
} catch (error) {
notifications.error("Error saving view")
}
}
</script>

View file

@ -124,7 +124,7 @@
})
dispatch("updatecolumns")
} catch (err) {
notifications.error(err)
notifications.error("Error saving column")
}
}
@ -133,17 +133,21 @@
}
function deleteColumn() {
field.name = deleteColName
if (field.name === $tables.selected.primaryDisplay) {
notifications.error("You cannot delete the display column")
} else {
tables.deleteField(field)
notifications.success(`Column ${field.name} deleted.`)
confirmDeleteDialog.hide()
hide()
deletion = false
try {
field.name = deleteColName
if (field.name === $tables.selected.primaryDisplay) {
notifications.error("You cannot delete the display column")
} else {
tables.deleteField(field)
notifications.success(`Column ${field.name} deleted.`)
confirmDeleteDialog.hide()
hide()
deletion = false
dispatch("updatecolumns")
}
} catch (error) {
notifications.error("Error deleting column")
}
dispatch("updatecolumns")
}
function handleTypeChange(event) {
@ -367,7 +371,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}
@ -394,6 +398,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)"

View file

@ -3,7 +3,7 @@
import { tables, rows } from "stores/backend"
import { notifications } from "@budibase/bbui"
import RowFieldControl from "../RowFieldControl.svelte"
import * as api from "../api"
import { API } from "api"
import { ModalContent } from "@budibase/bbui"
import ErrorsBox from "components/common/ErrorsBox.svelte"
import { FIELDS } from "constants/backend"
@ -22,30 +22,30 @@
$: tableSchema = Object.entries(table?.schema ?? {})
async function saveRow() {
const rowResponse = await api.saveRow(
{ ...row, tableId: table._id },
table._id
)
if (rowResponse.errors) {
errors = Object.entries(rowResponse.errors)
.map(([key, error]) => ({ dataPath: key, message: error }))
.flat()
errors = []
try {
await API.saveRow({ ...row, tableId: table._id })
notifications.success("Row saved successfully")
rows.save()
dispatch("updaterows")
} catch (error) {
if (error.handled) {
const response = error.json
if (response?.errors) {
errors = Object.entries(response.errors)
.map(([key, error]) => ({ dataPath: key, message: error }))
.flat()
} else if (error.status === 400 && response?.validationErrors) {
errors = Object.keys(response.validationErrors).map(field => ({
message: `${field} ${response.validationErrors[field][0]}`,
}))
}
} else {
notifications.error("Failed to save row")
}
// Prevent modal closing if there were errors
return false
} else if (rowResponse.status === 400 && rowResponse.validationErrors) {
errors = Object.keys(rowResponse.validationErrors).map(field => ({
message: `${field} ${rowResponse.validationErrors[field][0]}`,
}))
return false
} else if (rowResponse.status >= 400) {
errors = [{ message: rowResponse.message }]
return false
}
notifications.success("Row saved successfully.")
rows.save(rowResponse)
dispatch("updaterows")
}
</script>

View file

@ -4,7 +4,7 @@
import { roles } from "stores/backend"
import { notifications } from "@budibase/bbui"
import RowFieldControl from "../RowFieldControl.svelte"
import * as backendApi from "../api"
import { API } from "api"
import { ModalContent, Select } from "@budibase/bbui"
import ErrorsBox from "components/common/ErrorsBox.svelte"
@ -53,27 +53,31 @@
return false
}
const rowResponse = await backendApi.saveRow(
{ ...row, tableId: table._id },
table._id
)
if (rowResponse.errors) {
if (Array.isArray(rowResponse.errors)) {
errors = rowResponse.errors.map(error => ({ message: error }))
try {
await API.saveRow({ ...row, tableId: table._id })
notifications.success("User saved successfully")
rows.save()
dispatch("updaterows")
} catch (error) {
if (error.handled) {
const response = error.json
if (response?.errors) {
if (Array.isArray(response.errors)) {
errors = response.errors.map(error => ({ message: error }))
} else {
errors = Object.entries(response.errors)
.map(([key, error]) => ({ dataPath: key, message: error }))
.flat()
}
} else if (error.status === 400) {
errors = [{ message: response?.message || "Unknown error" }]
}
} else {
errors = Object.entries(rowResponse.errors)
.map(([key, error]) => ({ dataPath: key, message: error }))
.flat()
notifications.error("Error saving user")
}
return false
} else if (rowResponse.status === 400 || rowResponse.status === 500) {
errors = [{ message: rowResponse.message }]
// Prevent closing the modal on errors
return false
}
notifications.success("User saved successfully")
rows.save(rowResponse)
dispatch("updaterows")
}
</script>

View file

@ -12,17 +12,21 @@
function saveView() {
if (views.includes(name)) {
notifications.error(`View exists with name ${name}.`)
notifications.error(`View exists with name ${name}`)
return
}
viewsStore.save({
name,
tableId: $tables.selected._id,
field,
})
notifications.success(`View ${name} created`)
analytics.captureEvent(Events.VIEW.CREATED, { name })
$goto(`../../view/${name}`)
try {
viewsStore.save({
name,
tableId: $tables.selected._id,
field,
})
notifications.success(`View ${name} created`)
analytics.captureEvent(Events.VIEW.CREATED, { name })
$goto(`../../view/${name}`)
} catch (error) {
notifications.error("Error creating view")
}
}
</script>

View file

@ -1,7 +1,7 @@
<script>
import { ModalContent, Select, Input, Button } from "@budibase/bbui"
import { onMount } from "svelte"
import api from "builderStore/api"
import { API } from "api"
import { notifications } from "@budibase/bbui"
import ErrorsBox from "components/common/ErrorsBox.svelte"
import { roles } from "stores/backend"
@ -24,8 +24,12 @@
!builtInRoles.includes(selectedRole.name)
const fetchBasePermissions = async () => {
const permissionsResponse = await api.get("/api/permission/builtin")
basePermissions = await permissionsResponse.json()
try {
basePermissions = await API.getBasePermissions()
} catch (error) {
notifications.error("Error fetching base permission options")
basePermissions = []
}
}
// Changes the selected role
@ -68,23 +72,23 @@
}
// Save/create the role
const response = await roles.save(selectedRole)
if (response.status === 200) {
notifications.success("Role saved successfully.")
} else {
notifications.error("Error saving role.")
try {
await roles.save(selectedRole)
notifications.success("Role saved successfully")
} catch (error) {
notifications.error("Error saving role")
return false
}
}
// Deletes the selected role
const deleteRole = async () => {
const response = await roles.delete(selectedRole)
if (response.status === 200) {
try {
await roles.delete(selectedRole)
changeRole()
notifications.success("Role deleted successfully.")
} else {
notifications.error("Error deleting role.")
notifications.success("Role deleted successfully")
} catch (error) {
notifications.error("Error deleting role")
}
}

View file

@ -1,7 +1,7 @@
<script>
import { Select, ModalContent, notifications } from "@budibase/bbui"
import download from "downloadjs"
import { get } from "builderStore/api"
import { API } from "api"
const FORMATS = [
{
@ -19,17 +19,14 @@
let exportFormat = FORMATS[0].key
async function exportView() {
const uri = encodeURIComponent(view)
const response = await get(
`/api/views/export?view=${uri}&format=${exportFormat}`
)
if (response.status === 200) {
const data = await response.text()
try {
const data = await API.exportView({
viewName: view,
format: exportFormat,
})
download(data, `export.${exportFormat}`)
} else {
notifications.error(
`Unable to export ${exportFormat.toUpperCase()} data.`
)
} catch (error) {
notifications.error(`Unable to export ${exportFormat.toUpperCase()} data`)
}
}
</script>

View file

@ -72,11 +72,15 @@
$: schema = viewTable && viewTable.schema ? viewTable.schema : {}
function saveView() {
views.save(view)
notifications.success(`View ${view.name} saved.`)
analytics.captureEvent(Events.VIEW.ADDED_FILTER, {
filters: JSON.stringify(view.filters),
})
try {
views.save(view)
notifications.success(`View ${view.name} saved`)
analytics.captureEvent(Events.VIEW.ADDED_FILTER, {
filters: JSON.stringify(view.filters),
})
} catch (error) {
notifications.error("Error saving view")
}
}
function removeFilter(idx) {
@ -158,7 +162,7 @@
<Select
bind:value={filter.value}
options={fieldOptions(filter.key)}
getOptionLabel={x => x.toString()}
getOptionLabel={x => x?.toString() || ""}
/>
{:else if filter.key && isDate(filter.key)}
<DatePicker

View file

@ -19,8 +19,12 @@
.map(([key]) => key)
function saveView() {
views.save(view)
notifications.success(`View ${view.name} saved.`)
try {
views.save(view)
notifications.success(`View ${view.name} saved`)
} catch (error) {
notifications.error("Error saving view")
}
}
</script>

View file

@ -1,7 +1,13 @@
<script>
import { ModalContent, Label, notifications, Body } from "@budibase/bbui"
import {
ModalContent,
Label,
notifications,
Body,
Layout,
} from "@budibase/bbui"
import TableDataImport from "../../TableNavigator/TableDataImport.svelte"
import api from "builderStore/api"
import { API } from "api"
import { createEventDispatcher } from "svelte"
const dispatch = createEventDispatcher()
@ -12,15 +18,17 @@
$: valid = dataImport?.csvString != null && dataImport?.valid
async function importData() {
const response = await api.post(`/api/tables/${tableId}/import`, {
dataImport,
})
if (response.status !== 200) {
const error = await response.text()
notifications.error(`Unable to import data - ${error}`)
} else {
notifications.success("Rows successfully imported.")
try {
await API.importTableData({
tableId,
data: dataImport,
})
notifications.success("Rows successfully imported")
} catch (error) {
notifications.error("Unable to import data")
}
// Always refresh rows just to be sure
dispatch("updaterows")
}
</script>
@ -31,12 +39,14 @@
onConfirm={importData}
disabled={!valid}
>
<Body
>Import rows to an existing table from a CSV. Only columns from the CSV
which exist in the table will be imported.</Body
>
<Label grey extraSmall>CSV to import</Label>
<TableDataImport bind:dataImport bind:existingTableId={tableId} />
<Body size="S">
Import rows to an existing table from a CSV. Only columns from the CSV which
exist in the table will be imported.
</Body>
<Layout gap="XS" noPadding>
<Label grey extraSmall>CSV to import</Label>
<TableDataImport bind:dataImport bind:existingTableId={tableId} />
</Layout>
</ModalContent>
<style>

View file

@ -14,15 +14,19 @@
export let permissions
async function changePermission(level, role) {
await permissionsStore.save({
level,
role,
resource: resourceId,
})
try {
await permissionsStore.save({
level,
role,
resource: resourceId,
})
// Show updated permissions in UI: REMOVE
permissions = await permissionsStore.forResource(resourceId)
notifications.success("Updated permissions.")
// Show updated permissions in UI: REMOVE
permissions = await permissionsStore.forResource(resourceId)
notifications.success("Updated permissions")
} catch (error) {
notifications.error("Error updating permissions")
}
}
</script>

View file

@ -10,6 +10,7 @@
import TableNavigator from "components/backend/TableNavigator/TableNavigator.svelte"
import { customQueryIconText, customQueryIconColor } from "helpers/data/utils"
import ICONS from "./icons"
import { notifications } from "@budibase/bbui"
let openDataSources = []
$: enrichedDataSources = Array.isArray($datasources.list)
@ -63,9 +64,13 @@
}
}
onMount(() => {
datasources.fetch()
queries.fetch()
onMount(async () => {
try {
await datasources.fetch()
await queries.fetch()
} catch (error) {
notifications.error("Error fetching datasources and queries")
}
})
const containsActiveEntity = datasource => {

View file

@ -1,5 +1,5 @@
<script>
import { ModalContent, Body, Input } from "@budibase/bbui"
import { ModalContent, Body, Input, notifications } from "@budibase/bbui"
import { tables, datasources } from "stores/backend"
import { goto } from "@roxi/routify"
@ -29,10 +29,14 @@
}
async function saveTable() {
submitted = true
const table = await tables.save(buildDefaultTable(name, datasource._id))
await datasources.fetch()
$goto(`../../table/${table._id}`)
try {
submitted = true
const table = await tables.save(buildDefaultTable(name, datasource._id))
await datasources.fetch()
$goto(`../../table/${table._id}`)
} catch (error) {
notifications.error("Error saving table")
}
}
</script>

View file

@ -90,8 +90,8 @@
await datasources.updateSchema(datasource)
notifications.success(`Datasource ${name} tables updated successfully.`)
await tables.fetch()
} catch (err) {
notifications.error(`Error updating datasource schema: ${err}`)
} catch (error) {
notifications.error("Error updating datasource schema")
}
}
@ -188,29 +188,27 @@
{:else}
<Body size="S"><i>No tables found.</i></Body>
{/if}
{#if plusTables?.length !== 0 && integration.relationships}
<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>

View file

@ -1,7 +1,7 @@
<script>
import { Body } from "@budibase/bbui"
import { Body, notifications } from "@budibase/bbui"
import { onMount } from "svelte"
import api from "builderStore/api"
import { API } from "api"
import ICONS from "../icons"
export let integration = {}
@ -9,14 +9,17 @@
const INTERNAL = "BUDIBASE"
async function fetchIntegrations() {
const response = await api.get("/api/integrations")
const json = await response.json()
let otherIntegrations
try {
otherIntegrations = await API.getIntegrations()
} catch (error) {
otherIntegrations = {}
notifications.error("Error getting integrations")
}
integrations = {
[INTERNAL]: { datasource: {}, name: "INTERNAL/CSV" },
...json,
...otherIntegrations,
}
return json
}
function selectIntegration(integrationType) {

View file

@ -2,7 +2,7 @@
import { Table, Modal, Layout, ActionButton } from "@budibase/bbui"
import AuthTypeRenderer from "./AuthTypeRenderer.svelte"
import RestAuthenticationModal from "./RestAuthenticationModal.svelte"
import { uuid } from "builderStore/uuid"
import { Helpers } from "@budibase/bbui"
export let configs = []
@ -29,7 +29,7 @@
return c
})
} else {
config._id = uuid()
config._id = Helpers.uuid()
configs = [...configs, config]
}
}

View file

@ -1,8 +1,15 @@
<script>
import { ModalContent, Modal, Body, Layout, Detail } from "@budibase/bbui"
import {
ModalContent,
Modal,
Body,
Layout,
Detail,
notifications,
} from "@budibase/bbui"
import { onMount } from "svelte"
import ICONS from "../icons"
import api from "builderStore/api"
import { API } from "api"
import { IntegrationNames, IntegrationTypes } from "constants/backend"
import CreateTableModal from "components/backend/TableNavigator/modals/CreateTableModal.svelte"
import DatasourceConfigModal from "components/backend/DatasourceNavigator/modals/DatasourceConfigModal.svelte"
@ -12,7 +19,7 @@
import ImportRestQueriesModal from "./ImportRestQueriesModal.svelte"
export let modal
let integrations = []
let integrations = {}
let integration = {}
let internalTableModal
let externalDatasourceModal
@ -57,22 +64,32 @@
externalDatasourceModal.hide()
internalTableModal.show()
} else if (integration.type === IntegrationTypes.REST) {
// skip modal for rest, create straight away
const resp = await createRestDatasource(integration)
$goto(`./datasource/${resp._id}`)
try {
// Skip modal for rest, create straight away
const resp = await createRestDatasource(integration)
$goto(`./datasource/${resp._id}`)
} catch (error) {
notifications.error("Error creating datasource")
}
} else {
externalDatasourceModal.show()
}
}
async function fetchIntegrations() {
const response = await api.get("/api/integrations")
const json = await response.json()
integrations = {
let newIntegrations = {
[IntegrationTypes.INTERNAL]: { datasource: {}, name: "INTERNAL/CSV" },
...json,
}
return json
try {
const integrationList = await API.getIntegrations()
newIntegrations = {
...newIntegrations,
...integrationList,
}
} catch (error) {
notifications.error("Error fetching integrations")
}
integrations = newIntegrations
}
</script>

View file

@ -20,7 +20,7 @@
$goto(`./datasource/${resp._id}`)
notifications.success(`Datasource updated successfully.`)
} catch (err) {
notifications.error(`Error saving datasource: ${err}`)
notifications.error("Error saving datasource")
}
}

View file

@ -79,8 +79,8 @@
})
return true
} catch (err) {
notifications.error(`Error importing: ${err}`)
} catch (error) {
notifications.error("Error importing queries")
return false
}
}

View file

@ -12,24 +12,28 @@
let updateDatasourceDialog
async function deleteDatasource() {
let wasSelectedSource = $datasources.selected
if (!wasSelectedSource && $queries.selected) {
const queryId = $queries.selected
wasSelectedSource = $datasources.list.find(ds =>
queryId.includes(ds._id)
)?._id
}
const wasSelectedTable = $tables.selected
await datasources.delete(datasource)
notifications.success("Datasource deleted")
// navigate to first index page if the source you are deleting is selected
const entities = Object.values(datasource?.entities || {})
if (
wasSelectedSource === datasource._id ||
(entities &&
entities.find(entity => entity._id === wasSelectedTable?._id))
) {
$goto("./datasource")
try {
let wasSelectedSource = $datasources.selected
if (!wasSelectedSource && $queries.selected) {
const queryId = $queries.selected
wasSelectedSource = $datasources.list.find(ds =>
queryId.includes(ds._id)
)?._id
}
const wasSelectedTable = $tables.selected
await datasources.delete(datasource)
notifications.success("Datasource deleted")
// Navigate to first index page if the source you are deleting is selected
const entities = Object.values(datasource?.entities || {})
if (
wasSelectedSource === datasource._id ||
(entities &&
entities.find(entity => entity._id === wasSelectedTable?._id))
) {
$goto("./datasource")
}
} catch (error) {
notifications.error("Error deleting datasource")
}
}
</script>

View file

@ -10,26 +10,30 @@
let confirmDeleteDialog
async function deleteQuery() {
const wasSelectedQuery = $queries.selected
// need to calculate this before the query is deleted
const navigateToDatasource = wasSelectedQuery === query._id
try {
const wasSelectedQuery = $queries.selected
// need to calculate this before the query is deleted
const navigateToDatasource = wasSelectedQuery === query._id
await queries.delete(query)
await datasources.fetch()
await queries.delete(query)
await datasources.fetch()
if (navigateToDatasource) {
await datasources.select(query.datasourceId)
$goto(`./datasource/${query.datasourceId}`)
if (navigateToDatasource) {
await datasources.select(query.datasourceId)
$goto(`./datasource/${query.datasourceId}`)
}
notifications.success("Query deleted")
} catch (error) {
notifications.error("Error deleting query")
}
notifications.success("Query deleted")
}
async function duplicateQuery() {
try {
const newQuery = await queries.duplicate(query)
onClickQuery(newQuery)
} catch (e) {
notifications.error(e.message)
} catch (error) {
notifications.error("Error duplicating query")
}
}
</script>

View file

@ -9,7 +9,7 @@
Body,
} from "@budibase/bbui"
import { tables } from "stores/backend"
import { uuid } from "builderStore/uuid"
import { Helpers } from "@budibase/bbui"
import { writable } from "svelte/store"
export let save
@ -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
@ -151,7 +140,7 @@
const manyToMany =
fromRelationship.relationshipType === RelationshipTypes.MANY_TO_MANY
// main is simply used to know this is the side the user configured it from
const id = uuid()
const id = Helpers.uuid()
if (!manyToMany) {
delete fromRelationship.through
delete toRelationship.through
@ -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>

View file

@ -1,7 +1,7 @@
<script>
import { Select, InlineAlert, notifications } from "@budibase/bbui"
import { FIELDS } from "constants/backend"
import api from "builderStore/api"
import { API } from "api"
const BYTES_IN_MB = 1000000
const FILE_SIZE_LIMIT = BYTES_IN_MB * 5
@ -50,28 +50,26 @@
}
async function validateCSV() {
const response = await api.post("/api/tables/csv/validate", {
csvString,
schema: schema || {},
tableId: existingTableId,
})
try {
const parseResult = await API.validateTableCSV({
csvString,
schema: schema || {},
tableId: existingTableId,
})
schema = parseResult?.schema
fields = Object.keys(schema || {}).filter(
key => schema[key].type !== "omit"
)
const parseResult = await response.json()
schema = parseResult && parseResult.schema
fields = Object.keys(schema || {}).filter(
key => schema[key].type !== "omit"
)
// Check primary display is valid
if (!primaryDisplay || fields.indexOf(primaryDisplay) === -1) {
primaryDisplay = fields[0]
}
// Check primary display is valid
if (!primaryDisplay || fields.indexOf(primaryDisplay) === -1) {
primaryDisplay = fields[0]
}
if (response.status !== 200) {
hasValidated = true
} catch (error) {
notifications.error("CSV Invalid, please try another CSV file")
return []
}
hasValidated = true
}
async function handleFile(evt) {

Some files were not shown because too many files have changed in this diff Show more