mirror of https://github.com/Budibase/budibase.git
100 changed files with 2416 additions and 665 deletions
@ -0,0 +1,193 @@ |
|||
<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是一个开放源代码的低代码平台,可帮助开发人员和IT专业人员在几分钟内在自己的设施上构建、自动化和交付部署定制业务应用。 |
|||
</p> |
|||
|
|||
|
|||
<h3 align="center"> |
|||
🤖 🎨 🚀 |
|||
</h3> |
|||
|
|||
|
|||
<p align="center"> |
|||
<img src="https://i.imgur.com/tPQHruf.png"> |
|||
</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://discord.gg/rCYayfe"> |
|||
<img alt="Discord" src="https://img.shields.io/discord/733030666647765003"> |
|||
</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://portal.budi.live/signup">注册</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">报告Bug</a> |
|||
<span> · </span> |
|||
支持: <a href="https://github.com/Budibase/budibase/discussions">讨论区</a> |
|||
<span> & </span> |
|||
<a href="https://discord.gg/rCYayfe">Discord</a> |
|||
</h3> |
|||
|
|||
|
|||
|
|||
## ✨ 功能 |
|||
当其他平台选择闭源路线时,我们决定采用开放源代码。当其他平台选择云服务时,我们决定提供本地构建器提供更好的开发体验。我们喜欢在Budibase上做不同的事情。 |
|||
|
|||
- **构建和发布真实的软件** 与其他平台不同,使用Budibase可以构建和交付单页应用程序。Budibase应用程序具有增强的性能,并且可以进行响应式设计,从而为您的用户提供出色的体验。 |
|||
- **开源且可扩展** Budibase是开源的。Builder使用AGPLv3许可,Server使用GPLv3,Client使用MPL。这会让您充满信心,Budibase将永远存在。您还可以针对Budibase进行修改或对其进行分叉,并根据需要进行更改,以提供开发人员友好的体验。 |
|||
- **连接数据库或直接开始** Budibase从多个来源(包括MongoDB,CouchDB,PostgreSQL,mySQL,Airtable,Google Sheets,S3,DyanmoDB或REST API)提取数据。与其他平台不同,使用Budibase可以从头开始,创建没有数据源的业务应用程序。[新的数据源需求](https://github.com/Budibase/budibase/discussions?discussions_q=category%3AIdeas)。 |
|||
- **使用强大的预制组件设计和构建应用程序** Budibase开箱即用,具有精美设计,功能强大的组件,您可以使用它们像构建基块来构建UI。我们还提供了许多您最喜欢的CSS样式选项,因此您可以发挥更多的创意。[新的组件需求](https://github.com/Budibase/budibase/discussions?discussions_q=category%3AIdeas)。 |
|||
- **自动化流程,与其他工具集成,并连接到Webhooks** 通过自动化手动流程和工作流来节省时间。从连接到Webhook,到自动发送电子邮件,只需告诉Budibase要做些什么,然后让它为您工作。您可以[在此处](https://github.com/Budibase/automations)轻松地[为Budibase创建新的自动化,](https://github.com/Budibase/automations)或[在此处](https://github.com/Budibase/automations)[新的集成需求](https://github.com/Budibase/budibase/discussions?discussions_q=category%3AIdeas)。 |
|||
- **云托管和自我托管** 用户可以自行托管(请参见下文),或使用Budibase托管其应用。目前,我们的云托管产品仅限于免费套餐,但我们会在将来进行更改。对于大量使用,我们建议用户进行自我托管。 |
|||
|
|||
<p align="center"> |
|||
<img alt="Budibase design ui" src="https://imgur.com/v8m6v3q.png"> |
|||
</p> |
|||
|
|||
|
|||
## ⌛ 状态 |
|||
- [x] Alpha:我们正在将Budibase演示给用户并收到反馈 |
|||
- [x] 内部测试:我们正在与一组封闭的客户一起测试Budibase |
|||
- [x] 公开测试:任何人都可以[注册并使用Budibase](https://portal.budi.live/signup) |
|||
- [ ] 正式发布 |
|||
|
|||
关注此存储库的“release”以获取主要更新的通知,并给我们一个star~。 |
|||
|
|||
<p align="center"> |
|||
<img src="https://i.imgur.com/cJpgqm8.png"> |
|||
</p> |
|||
|
|||
### Star时序图 |
|||
|
|||
[](https://starchart.cc/Budibase/budibase) |
|||
|
|||
如果您在两个版本之间遇到问题,请使用[此处](https://github.com/Budibase/budibase/blob/HEAD/.github/CONTRIBUTING.md#troubleshooting)的指南清理您的环境。 |
|||
|
|||
|
|||
## 🏁 Budibase入门 |
|||
|
|||
Budibase构建器在Mac,PC和Linux上的Electron中运行。请按照以下步骤开始: |
|||
|
|||
- [ ] [注册Budibase](https://portal.budi.live/signup) |
|||
- [ ] 创建用户名和密码 |
|||
- [ ] 复制您的API密钥 |
|||
- [ ] 下载Budibase |
|||
- [ ] 打开Budibase并输入您的API密钥 |
|||
|
|||
如果您需要其他帮助,请[参阅以下指导教程](https://docs.budibase.com/tutorial/tutorial-signing-up)。 |
|||
|
|||
|
|||
## 🤖 自托管 |
|||
|
|||
Budibase希望确保任何人都可以使用我们开发的工具,并且我们知道很多人需要能够在自己的系统上托管他们制作的应用程序——这就是为什么我们决定尝试使自托管服务变得如此简单的原因! |
|||
|
|||
当前,您可以使用Docker或Digital Ocean托管您的应用程序。可以在[此处](https://docs.budibase.com/self-hosting/introduction-to-self-hosting)找到有关自我托管的文档。 |
|||
|
|||
[](https://cloud.digitalocean.com/droplets/new?onboarding_origin=marketplace&i=09038e&fleetUuid=bb04f9c8-1de8-4687-b2ae-1d5177a0535b&appId=77729671&type=applications&size=s-4vcpu-8gb®ion=nyc1&refcode=0caaa6085a82&image=budibase-20-04) |
|||
|
|||
|
|||
## 🎓 学习Budibase |
|||
|
|||
Budibase[文档](https://docs.budibase.com/)位于[此处](https://docs.budibase.com/)。 |
|||
|
|||
您还可以按照有关[如何使用Budibase构建CRM](https://docs.budibase.com/tutorial/tutorial-introduction)的快速教程进行[操作](https://docs.budibase.com/tutorial/tutorial-introduction) |
|||
|
|||
|
|||
## 路线图 |
|||
查看我们的[公共路线图](https://github.com/Budibase/budibase/projects/10)。如果您想讨论路线图上的某些项目,请与[Discord](https://discord.gg/rCYayfe)或通过Github进行[讨论](https://github.com/Budibase/budibase/discussions) |
|||
|
|||
|
|||
## ❗ 行为守则 |
|||
|
|||
Budibase致力于为每个人提供热情,多样且无烦恼的体验。我们希望Budibase社区中的每个人都遵守我们的[**行为准则**](https://github.com/Budibase/budibase/blob/HEAD/.github/CODE_OF_CONDUCT.md)。 |
|||
|
|||
## 🙌 为Budibase贡献 |
|||
|
|||
从错误报告到PR请求:每一个贡献都将受到赞赏和欢迎。如果您打算实施一项新功能或更改API,请先创建一个Issue。这样我们可以确保您的工作没有白费。 |
|||
|
|||
### 不知道从哪里开始? |
|||
|
|||
[第一次提出Issue](https://github.com/Budibase/budibase/projects/22)是一个开始做出贡献的好地方。 |
|||
|
|||
### 存储库的组织方式 |
|||
|
|||
Budibase是由lerna管理的项目。Lerna管理budibase软件包的构建和发布。在较高的层次上,这里是构成Budibase的软件包。 |
|||
|
|||
- [packages/builder](https://github.com/Budibase/budibase/tree/HEAD/packages/builder)-包含budibase构建器客户端苗条应用程序的代码。 |
|||
- [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应用程序负责为构建器和budibase应用程序提供JS服务,并提供与数据库和文件系统交互的API。 |
|||
|
|||
有关更多信息,请参见[CONTRIBUTING.md](https://github.com/Budibase/budibase/blob/HEAD/.github/CONTRIBUTING.md) |
|||
|
|||
## 📝 开源协议 |
|||
|
|||
Budibase是开源的。该构建器的许可证为[AGPL v3](https://www.gnu.org/licenses/agpl-3.0.en.html),服务器的许可证为[GPL v3](https://www.gnu.org/licenses/gpl-3.0.en.html),客户端的许可证为[MPL](https://directory.fsf.org/wiki/License:MPL-2.0)。 |
|||
|
|||
## 💬 保持联系 |
|||
|
|||
如果您有任何疑问或想与其他Budibase用户交谈,请跳至[Github讨论](https://github.com/Budibase/budibase/discussions)或加入我们的Discord服务器: |
|||
|
|||
[Discord 聊天室](https://discord.gg/rCYayfe) |
|||
|
|||
 |
|||
|
|||
|
|||
## 贡献者 ✨ |
|||
|
|||
感谢这些出色的人 ([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/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> |
|||
</tr> |
|||
<tr> |
|||
<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> |
|||
</tr> |
|||
</table> |
|||
|
|||
<!-- markdownlint-restore --> |
|||
<!-- prettier-ignore-end --> |
|||
|
|||
<!-- ALL-CONTRIBUTORS-LIST:END --> |
|||
|
|||
该项目遵循[所有参与者的](https://github.com/all-contributors/all-contributors)规范。欢迎任何形式的捐助! |
|||
@ -0,0 +1,57 @@ |
|||
<script> |
|||
import "@spectrum-css/pagination/dist/index-vars.css" |
|||
import "@spectrum-css/actionbutton/dist/index-vars.css" |
|||
import "@spectrum-css/typography/dist/index-vars.css" |
|||
|
|||
export let page |
|||
export let goToPrevPage |
|||
export let goToNextPage |
|||
export let hasPrevPage = true |
|||
export let hasNextPage = true |
|||
</script> |
|||
|
|||
<nav class="spectrum-Pagination spectrum-Pagination--explicit"> |
|||
<div |
|||
href="#" |
|||
class="spectrum-ActionButton spectrum-ActionButton--sizeM spectrum-ActionButton--quiet spectrum-Pagination-prevButton" |
|||
on:click={hasPrevPage ? goToPrevPage : null} |
|||
class:is-disabled={!hasPrevPage} |
|||
> |
|||
<svg |
|||
class="spectrum-Icon spectrum-UIIcon-ChevronLeft100" |
|||
focusable="false" |
|||
aria-hidden="true" |
|||
aria-label="ChevronLeft" |
|||
> |
|||
<use xlink:href="#spectrum-css-icon-Chevron100" /> |
|||
</svg> |
|||
</div> |
|||
<span class="spectrum-Body--secondary spectrum-Pagination-counter"> |
|||
Page {page} |
|||
</span> |
|||
<div |
|||
href="#" |
|||
class="spectrum-ActionButton spectrum-ActionButton--sizeM spectrum-ActionButton--quiet spectrum-Pagination-nextButton" |
|||
on:click={hasNextPage ? goToNextPage : null} |
|||
class:is-disabled={!hasNextPage} |
|||
> |
|||
<svg |
|||
class="spectrum-Icon spectrum-UIIcon-ChevronRight100" |
|||
focusable="false" |
|||
aria-hidden="true" |
|||
aria-label="ChevronLeft" |
|||
> |
|||
<use xlink:href="#spectrum-css-icon-Chevron100" /> |
|||
</svg> |
|||
</div> |
|||
</nav> |
|||
|
|||
<style> |
|||
.spectrum-Pagination-counter { |
|||
margin-left: 0; |
|||
user-select: none; |
|||
} |
|||
.is-disabled:hover { |
|||
cursor: initial; |
|||
} |
|||
</style> |
|||
@ -1,71 +0,0 @@ |
|||
<script> |
|||
import { Button, Drawer, Body, DrawerContent, Layout } from "@budibase/bbui" |
|||
import { createEventDispatcher } from "svelte" |
|||
import { notifications } from "@budibase/bbui" |
|||
import { |
|||
getDatasourceForProvider, |
|||
getSchemaForDatasource, |
|||
} from "builderStore/dataBinding" |
|||
import SaveFields from "./EventsEditor/actions/SaveFields.svelte" |
|||
import { currentAsset } from "builderStore" |
|||
|
|||
const dispatch = createEventDispatcher() |
|||
|
|||
export let value = {} |
|||
export let componentInstance |
|||
let drawer |
|||
let tempValue = value |
|||
|
|||
$: schemaFields = getSchemaFields(componentInstance) |
|||
|
|||
const getSchemaFields = component => { |
|||
const datasource = getDatasourceForProvider($currentAsset, component) |
|||
const { schema } = getSchemaForDatasource(datasource) |
|||
return Object.values(schema || {}) |
|||
} |
|||
|
|||
const saveFilter = async () => { |
|||
dispatch("change", tempValue) |
|||
notifications.success("Filters saved.") |
|||
drawer.hide() |
|||
} |
|||
|
|||
const onFieldsChanged = event => { |
|||
tempValue = event.detail |
|||
} |
|||
</script> |
|||
|
|||
<Button secondary on:click={drawer.show}>Define Filters</Button> |
|||
<Drawer bind:this={drawer} title="Filtering"> |
|||
<Button cta slot="buttons" on:click={saveFilter}>Save</Button> |
|||
<DrawerContent slot="body"> |
|||
<Layout> |
|||
<Body size="S"> |
|||
{#if !Object.keys(tempValue || {}).length} |
|||
Add your first filter column. |
|||
{:else} |
|||
Results are filtered to only those which match all of the following |
|||
constaints. |
|||
{/if} |
|||
</Body> |
|||
<div class="fields"> |
|||
<SaveFields |
|||
parameterFields={value} |
|||
{schemaFields} |
|||
valueLabel="Equals" |
|||
on:change={onFieldsChanged} |
|||
/> |
|||
</div> |
|||
</Layout> |
|||
</DrawerContent> |
|||
</Drawer> |
|||
|
|||
<style> |
|||
.fields { |
|||
display: grid; |
|||
column-gap: var(--spacing-l); |
|||
row-gap: var(--spacing-s); |
|||
align-items: center; |
|||
grid-template-columns: auto 1fr auto 1fr auto; |
|||
} |
|||
</style> |
|||
@ -0,0 +1,90 @@ |
|||
<script> |
|||
import { |
|||
notifications, |
|||
Button, |
|||
Drawer, |
|||
Body, |
|||
DrawerContent, |
|||
Layout, |
|||
} from "@budibase/bbui" |
|||
import { createEventDispatcher } from "svelte" |
|||
import { |
|||
getDatasourceForProvider, |
|||
getSchemaForDatasource, |
|||
} from "builderStore/dataBinding" |
|||
import LuceneFilterBuilder from "./LuceneFilterBuilder.svelte" |
|||
import { currentAsset } from "builderStore" |
|||
import SaveFields from "../EventsEditor/actions/SaveFields.svelte" |
|||
|
|||
const dispatch = createEventDispatcher() |
|||
|
|||
export let value = [] |
|||
export let componentInstance |
|||
let drawer |
|||
let tempValue = value |
|||
|
|||
$: numFilters = Array.isArray(tempValue) |
|||
? tempValue.length |
|||
: Object.keys(tempValue || {}).length |
|||
$: dataSource = getDatasourceForProvider($currentAsset, componentInstance) |
|||
$: schema = getSchemaForDatasource(dataSource)?.schema |
|||
$: schemaFields = Object.values(schema || {}) |
|||
$: internalTable = dataSource?.type === "table" |
|||
|
|||
// Reset value if value is wrong type for the datasource. |
|||
// Lucene editor needs an array, and simple editor needs an object. |
|||
$: { |
|||
if (internalTable && !Array.isArray(value)) { |
|||
tempValue = [] |
|||
dispatch("change", []) |
|||
} else if (!internalTable && Array.isArray(value)) { |
|||
tempValue = {} |
|||
dispatch("change", {}) |
|||
} |
|||
} |
|||
|
|||
const saveFilter = async () => { |
|||
dispatch("change", tempValue) |
|||
notifications.success("Filters saved.") |
|||
drawer.hide() |
|||
} |
|||
</script> |
|||
|
|||
<Button secondary on:click={drawer.show}>Define Filters</Button> |
|||
<Drawer bind:this={drawer} title="Filtering"> |
|||
<Button cta slot="buttons" on:click={saveFilter}>Save</Button> |
|||
<DrawerContent slot="body"> |
|||
<Layout> |
|||
<Body size="S"> |
|||
{#if !numFilters} |
|||
Add your first filter column. |
|||
{:else} |
|||
Results are filtered to only those which match all of the following |
|||
constaints. |
|||
{/if} |
|||
</Body> |
|||
{#if internalTable} |
|||
<LuceneFilterBuilder bind:value={tempValue} {schemaFields} /> |
|||
{:else} |
|||
<div class="fields"> |
|||
<SaveFields |
|||
parameterFields={Array.isArray(value) ? {} : value} |
|||
{schemaFields} |
|||
valueLabel="Equals" |
|||
on:change={e => (tempValue = e.detail)} |
|||
/> |
|||
</div> |
|||
{/if} |
|||
</Layout> |
|||
</DrawerContent> |
|||
</Drawer> |
|||
|
|||
<style> |
|||
.fields { |
|||
display: grid; |
|||
column-gap: var(--spacing-l); |
|||
row-gap: var(--spacing-s); |
|||
align-items: center; |
|||
grid-template-columns: auto 1fr auto 1fr auto; |
|||
} |
|||
</style> |
|||
@ -0,0 +1,237 @@ |
|||
<script> |
|||
import { |
|||
DatePicker, |
|||
ActionButton, |
|||
Button, |
|||
Select, |
|||
Combobox, |
|||
Input, |
|||
} from "@budibase/bbui" |
|||
import { store, currentAsset } from "builderStore" |
|||
import { getBindableProperties } from "builderStore/dataBinding" |
|||
import { createEventDispatcher } from "svelte" |
|||
import DrawerBindableInput from "components/common/bindings/DrawerBindableInput.svelte" |
|||
import { generate } from "shortid" |
|||
|
|||
const dispatch = createEventDispatcher() |
|||
|
|||
export let schemaFields |
|||
export let value |
|||
|
|||
const OperatorOptions = { |
|||
Equals: { |
|||
value: "equal", |
|||
label: "Equals", |
|||
}, |
|||
NotEquals: { |
|||
value: "notEqual", |
|||
label: "Not equals", |
|||
}, |
|||
Empty: { |
|||
value: "empty", |
|||
label: "Is empty", |
|||
}, |
|||
NotEmpty: { |
|||
value: "notEmpty", |
|||
label: "Is not empty", |
|||
}, |
|||
StartsWith: { |
|||
value: "string", |
|||
label: "Starts with", |
|||
}, |
|||
Like: { |
|||
value: "fuzzy", |
|||
label: "Like", |
|||
}, |
|||
MoreThan: { |
|||
value: "rangeLow", |
|||
label: "More than", |
|||
}, |
|||
LessThan: { |
|||
value: "rangeHigh", |
|||
label: "Less than", |
|||
}, |
|||
} |
|||
const BannedTypes = ["link", "attachment"] |
|||
$: bindableProperties = getBindableProperties( |
|||
$currentAsset, |
|||
$store.selectedComponentId |
|||
) |
|||
$: fieldOptions = (schemaFields ?? []) |
|||
.filter(field => !BannedTypes.includes(field.type)) |
|||
.map(field => field.name) |
|||
|
|||
const addField = () => { |
|||
value = [ |
|||
...value, |
|||
{ |
|||
id: generate(), |
|||
field: null, |
|||
operator: OperatorOptions.Equals.value, |
|||
value: null, |
|||
valueType: "Value", |
|||
}, |
|||
] |
|||
} |
|||
|
|||
const removeField = id => { |
|||
value = value.filter(field => field.id !== id) |
|||
} |
|||
|
|||
const getValidOperatorsForType = type => { |
|||
const Op = OperatorOptions |
|||
if (type === "string") { |
|||
return [ |
|||
Op.Equals, |
|||
Op.NotEquals, |
|||
Op.StartsWith, |
|||
Op.Like, |
|||
Op.Empty, |
|||
Op.NotEmpty, |
|||
] |
|||
} else if (type === "number") { |
|||
return [ |
|||
Op.Equals, |
|||
Op.NotEquals, |
|||
Op.MoreThan, |
|||
Op.LessThan, |
|||
Op.Empty, |
|||
Op.NotEmpty, |
|||
] |
|||
} else if (type === "options") { |
|||
return [Op.Equals, Op.NotEquals, Op.Empty, Op.NotEmpty] |
|||
} else if (type === "boolean") { |
|||
return [Op.Equals, Op.NotEquals, Op.Empty, Op.NotEmpty] |
|||
} else if (type === "longform") { |
|||
return [ |
|||
Op.Equals, |
|||
Op.NotEquals, |
|||
Op.StartsWith, |
|||
Op.Like, |
|||
Op.Empty, |
|||
Op.NotEmpty, |
|||
] |
|||
} else if (type === "datetime") { |
|||
return [ |
|||
Op.Equals, |
|||
Op.NotEquals, |
|||
Op.MoreThan, |
|||
Op.LessThan, |
|||
Op.Empty, |
|||
Op.NotEmpty, |
|||
] |
|||
} |
|||
return [] |
|||
} |
|||
|
|||
const onFieldChange = (expression, field) => { |
|||
// Update the field type |
|||
expression.type = schemaFields.find(x => x.name === field)?.type |
|||
|
|||
// Ensure a valid operator is set |
|||
const validOperators = getValidOperatorsForType(expression.type) |
|||
if (!validOperators.includes(expression.operator)) { |
|||
expression.operator = |
|||
validOperators[0]?.value ?? OperatorOptions.Equals.value |
|||
onOperatorChange(expression, expression.operator) |
|||
} |
|||
} |
|||
|
|||
const onOperatorChange = (expression, operator) => { |
|||
const noValueOptions = [ |
|||
OperatorOptions.Empty.value, |
|||
OperatorOptions.NotEmpty.value, |
|||
] |
|||
expression.noValue = noValueOptions.includes(operator) |
|||
if (expression.noValue) { |
|||
expression.value = null |
|||
} |
|||
} |
|||
|
|||
const getFieldOptions = field => { |
|||
const schema = schemaFields.find(x => x.name === field) |
|||
return schema?.constraints?.inclusion || [] |
|||
} |
|||
</script> |
|||
|
|||
{#if value?.length} |
|||
<div class="fields"> |
|||
{#each value as expression, idx} |
|||
<Select |
|||
bind:value={expression.field} |
|||
options={fieldOptions} |
|||
on:change={e => onFieldChange(expression, e.detail)} |
|||
placeholder="Column" |
|||
/> |
|||
<Select |
|||
disabled={!expression.field} |
|||
options={getValidOperatorsForType(expression.type)} |
|||
bind:value={expression.operator} |
|||
on:change={e => onOperatorChange(expression, e.detail)} |
|||
placeholder={null} |
|||
/> |
|||
<Select |
|||
disabled={expression.noValue || !expression.field} |
|||
options={["Value", "Binding"]} |
|||
bind:value={expression.valueType} |
|||
placeholder={null} |
|||
/> |
|||
{#if expression.valueType === "Binding"} |
|||
<DrawerBindableInput |
|||
disabled={expression.noValue} |
|||
title={`Value for "${expression.field}"`} |
|||
value={expression.value} |
|||
placeholder="Value" |
|||
bindings={bindableProperties} |
|||
on:change={event => (expression.value = event.detail)} |
|||
/> |
|||
{:else if ["string", "longform", "number"].includes(expression.type)} |
|||
<Input disabled={expression.noValue} bind:value={expression.value} /> |
|||
{:else if expression.type === "options"} |
|||
<Combobox |
|||
disabled={expression.noValue} |
|||
options={getFieldOptions(expression.field)} |
|||
bind:value={expression.value} |
|||
/> |
|||
{:else if expression.type === "boolean"} |
|||
<Combobox |
|||
disabled |
|||
options={[ |
|||
{ label: "True", value: true }, |
|||
{ label: "False", value: false }, |
|||
]} |
|||
bind:value={expression.value} |
|||
/> |
|||
{:else if expression.type === "datetime"} |
|||
<DatePicker |
|||
disabled={expression.noValue} |
|||
bind:value={expression.value} |
|||
/> |
|||
{:else} |
|||
<DrawerBindableInput disabled /> |
|||
{/if} |
|||
|
|||
<ActionButton |
|||
size="S" |
|||
quiet |
|||
icon="Close" |
|||
on:click={() => removeField(expression.id)} |
|||
/> |
|||
{/each} |
|||
</div> |
|||
{/if} |
|||
<div> |
|||
<Button icon="AddCircle" size="M" secondary on:click={addField}> |
|||
Add expression |
|||
</Button> |
|||
</div> |
|||
|
|||
<style> |
|||
.fields { |
|||
display: grid; |
|||
column-gap: var(--spacing-l); |
|||
row-gap: var(--spacing-s); |
|||
align-items: center; |
|||
grid-template-columns: 1fr 120px 120px 1fr auto; |
|||
} |
|||
</style> |
|||
@ -0,0 +1,20 @@ |
|||
import { writable } from "svelte/store" |
|||
import api from "builderStore/api" |
|||
|
|||
export default function (url) { |
|||
const store = writable({ status: "LOADING", data: {}, error: {} }) |
|||
|
|||
async function get() { |
|||
store.update(u => ({ ...u, status: "LOADING" })) |
|||
try { |
|||
const response = await api.get(url) |
|||
store.set({ data: await response.json(), status: "SUCCESS" }) |
|||
} catch (e) { |
|||
store.set({ data: {}, error: e, status: "ERROR" }) |
|||
} |
|||
} |
|||
|
|||
get() |
|||
|
|||
return { subscribe: store.subscribe, refresh: get } |
|||
} |
|||
@ -0,0 +1,9 @@ |
|||
export { default as fetchData } from "./fetchData" |
|||
export { |
|||
buildStyle, |
|||
convertCamel, |
|||
pipe, |
|||
capitalise, |
|||
get_name, |
|||
get_capitalised_name, |
|||
} from "./helpers" |
|||
@ -0,0 +1,2 @@ |
|||
export { emailValidator, requiredValidator } from "./validators" |
|||
export { createValidationStore } from "./validation" |
|||
@ -0,0 +1,23 @@ |
|||
import { writable, derived } from "svelte/store" |
|||
|
|||
export function createValidationStore(initialValue, ...validators) { |
|||
let touched = false |
|||
|
|||
const value = writable(initialValue || "") |
|||
const error = derived(value, $v => validate($v, validators)) |
|||
const touchedStore = derived(value, () => { |
|||
if (!touched) { |
|||
touched = true |
|||
return false |
|||
} |
|||
return touched |
|||
}) |
|||
|
|||
return [value, error, touchedStore] |
|||
} |
|||
|
|||
function validate(value, validators) { |
|||
const failing = validators.find(v => v(value) !== true) |
|||
|
|||
return failing && failing(value) |
|||
} |
|||
@ -0,0 +1,16 @@ |
|||
export function emailValidator(value) { |
|||
return ( |
|||
(value && |
|||
!!value.match( |
|||
/^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/ |
|||
)) || |
|||
"Please enter a valid email" |
|||
) |
|||
} |
|||
|
|||
export function requiredValidator(value) { |
|||
return ( |
|||
(value !== undefined && value !== null && value !== "") || |
|||
"This field is required" |
|||
) |
|||
} |
|||
@ -0,0 +1,97 @@ |
|||
<script> |
|||
import { |
|||
Layout, |
|||
Heading, |
|||
Body, |
|||
Input, |
|||
Button, |
|||
notifications, |
|||
} from "@budibase/bbui" |
|||
import { goto, params } from "@roxi/routify" |
|||
import { createValidationStore, requiredValidator } from "helpers/validation" |
|||
import { users } from "stores/portal" |
|||
|
|||
const [password, passwordError, passwordTouched] = createValidationStore( |
|||
"", |
|||
requiredValidator |
|||
) |
|||
const [repeat, _, repeatTouched] = createValidationStore( |
|||
"", |
|||
requiredValidator |
|||
) |
|||
const inviteCode = $params["?code"] |
|||
|
|||
async function acceptInvite() { |
|||
try { |
|||
const res = await users.acceptInvite(inviteCode, $password) |
|||
if (!res) { |
|||
throw new Error(res.message) |
|||
} |
|||
notifications.success(`User created.`) |
|||
$goto("../auth/login") |
|||
} catch (err) { |
|||
notifications.error(err) |
|||
} |
|||
} |
|||
</script> |
|||
|
|||
<section> |
|||
<div class="container"> |
|||
<Layout gap="XS"> |
|||
<img src="https://i.imgur.com/ZKyklgF.png" /> |
|||
</Layout> |
|||
<div class="center"> |
|||
<Layout gap="XS"> |
|||
<Heading size="M">Accept Invitation</Heading> |
|||
<Body size="M">Please enter a password to setup your user.</Body> |
|||
</Layout> |
|||
</div> |
|||
<Layout gap="XS"> |
|||
<Input |
|||
label="Password" |
|||
type="password" |
|||
error={$passwordTouched && $passwordError} |
|||
bind:value={$password} |
|||
/> |
|||
<Input |
|||
label="Repeat Password" |
|||
type="password" |
|||
error={$repeatTouched && |
|||
$password !== $repeat && |
|||
"Passwords must match"} |
|||
bind:value={$repeat} |
|||
/> |
|||
</Layout> |
|||
<Layout gap="S"> |
|||
<Button |
|||
disabled={!$passwordTouched || !$repeatTouched || $password !== $repeat} |
|||
cta |
|||
on:click={acceptInvite}>Accept invite</Button |
|||
> |
|||
</Layout> |
|||
</div> |
|||
</section> |
|||
|
|||
<style> |
|||
section { |
|||
display: flex; |
|||
align-items: center; |
|||
justify-content: center; |
|||
height: 100%; |
|||
} |
|||
.container { |
|||
margin: 0 auto; |
|||
width: 260px; |
|||
display: flex; |
|||
flex-direction: column; |
|||
justify-content: flex-start; |
|||
align-items: stretch; |
|||
} |
|||
.center { |
|||
text-align: center; |
|||
} |
|||
img { |
|||
width: 40px; |
|||
margin: 0 auto; |
|||
} |
|||
</style> |
|||
@ -0,0 +1,7 @@ |
|||
<script> |
|||
import { Page } from "@budibase/bbui" |
|||
</script> |
|||
|
|||
<Page> |
|||
<slot /> |
|||
</Page> |
|||
|
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 1.5 KiB |
@ -0,0 +1,114 @@ |
|||
<script> |
|||
import GoogleLogo from "./_logos/Google.svelte" |
|||
import { |
|||
Button, |
|||
Heading, |
|||
Divider, |
|||
Page, |
|||
Label, |
|||
notifications, |
|||
Layout, |
|||
Input, |
|||
Body, |
|||
} from "@budibase/bbui" |
|||
import { onMount } from "svelte" |
|||
import api from "builderStore/api" |
|||
|
|||
const ConfigTypes = { |
|||
Google: "google", |
|||
// Github: "github", |
|||
// AzureAD: "ad", |
|||
} |
|||
|
|||
const ConfigFields = { |
|||
Google: ["clientID", "clientSecret", "callbackURL"], |
|||
} |
|||
|
|||
let google |
|||
|
|||
async function save(doc) { |
|||
try { |
|||
// Save an oauth config |
|||
const response = await api.post(`/api/admin/configs`, doc) |
|||
const json = await response.json() |
|||
if (response.status !== 200) throw new Error(json.message) |
|||
google._rev = json._rev |
|||
google._id = json._id |
|||
|
|||
notifications.success(`Settings saved.`) |
|||
} catch (err) { |
|||
notifications.error(`Failed to update OAuth settings. ${err}`) |
|||
} |
|||
} |
|||
|
|||
onMount(async () => { |
|||
// fetch the configs for oauth |
|||
const googleResponse = await api.get( |
|||
`/api/admin/configs/${ConfigTypes.Google}` |
|||
) |
|||
const googleDoc = await googleResponse.json() |
|||
|
|||
if (!googleDoc._id) { |
|||
google = { |
|||
type: ConfigTypes.Google, |
|||
config: {}, |
|||
} |
|||
} else { |
|||
google = googleDoc |
|||
} |
|||
}) |
|||
</script> |
|||
|
|||
<Page> |
|||
<Layout noPadding> |
|||
<div> |
|||
<Heading size="M">OAuth</Heading> |
|||
<Body> |
|||
Every budibase app comes with basic authentication (email/password) |
|||
included. You can add additional authentication methods from the options |
|||
below. |
|||
</Body> |
|||
</div> |
|||
<Divider /> |
|||
{#if google} |
|||
<div> |
|||
<Heading size="S"> |
|||
<span> |
|||
<GoogleLogo /> |
|||
Google |
|||
</span> |
|||
</Heading> |
|||
<Body> |
|||
To allow users to authenticate using their Google accounts, fill out |
|||
the fields below. |
|||
</Body> |
|||
</div> |
|||
|
|||
{#each ConfigFields.Google as field} |
|||
<div class="form-row"> |
|||
<Label size="L">{field}</Label> |
|||
<Input bind:value={google.config[field]} /> |
|||
</div> |
|||
{/each} |
|||
<div> |
|||
<Button primary on:click={() => save(google)}>Save</Button> |
|||
</div> |
|||
<Divider /> |
|||
{/if} |
|||
</Layout> |
|||
</Page> |
|||
|
|||
<style> |
|||
.form-row { |
|||
display: grid; |
|||
grid-template-columns: 20% 1fr; |
|||
grid-gap: var(--spacing-l); |
|||
align-items: center; |
|||
} |
|||
|
|||
span { |
|||
display: flex; |
|||
align-items: center; |
|||
gap: var(--spacing-s); |
|||
} |
|||
</style> |
|||
@ -1,32 +1,19 @@ |
|||
<script> |
|||
import { |
|||
Menu, |
|||
MenuItem, |
|||
Button, |
|||
Detail, |
|||
Heading, |
|||
Divider, |
|||
Label, |
|||
Modal, |
|||
ModalContent, |
|||
notifications, |
|||
Layout, |
|||
Icon, |
|||
Body, |
|||
Page, |
|||
Select, |
|||
Tabs, |
|||
Tab, |
|||
MenuSection, |
|||
MenuSeparator, |
|||
} from "@budibase/bbui" |
|||
import { goto } from "@roxi/routify" |
|||
import { onMount } from "svelte" |
|||
import { fade } from "svelte/transition" |
|||
import { email } from "stores/portal" |
|||
import Editor from "components/integration/QueryEditor.svelte" |
|||
import TemplateBindings from "./TemplateBindings.svelte" |
|||
import api from "builderStore/api" |
|||
import TemplateBindings from "./_components/TemplateBindings.svelte" |
|||
|
|||
const ConfigTypes = { |
|||
SMTP: "smtp", |
|||
|
After Width: | Height: | Size: 1.5 KiB |
@ -0,0 +1,115 @@ |
|||
<script> |
|||
import GoogleLogo from "./_logos/Google.svelte" |
|||
import { |
|||
Button, |
|||
Heading, |
|||
Divider, |
|||
Label, |
|||
notifications, |
|||
Layout, |
|||
Input, |
|||
Body, |
|||
Page, |
|||
} from "@budibase/bbui" |
|||
import { onMount } from "svelte" |
|||
import api from "builderStore/api" |
|||
|
|||
const ConfigTypes = { |
|||
Google: "google", |
|||
// Github: "github", |
|||
// AzureAD: "ad", |
|||
} |
|||
|
|||
const ConfigFields = { |
|||
Google: ["clientID", "clientSecret", "callbackURL"], |
|||
} |
|||
|
|||
let google |
|||
|
|||
async function save(doc) { |
|||
try { |
|||
// Save an oauth config |
|||
const response = await api.post(`/api/admin/configs`, doc) |
|||
const json = await response.json() |
|||
if (response.status !== 200) throw new Error(json.message) |
|||
google._rev = json._rev |
|||
google._id = json._id |
|||
|
|||
notifications.success(`Settings saved.`) |
|||
} catch (err) { |
|||
notifications.error(`Failed to update OAuth settings. ${err}`) |
|||
} |
|||
} |
|||
|
|||
onMount(async () => { |
|||
// fetch the configs for oauth |
|||
const googleResponse = await api.get( |
|||
`/api/admin/configs/${ConfigTypes.Google}` |
|||
) |
|||
const googleDoc = await googleResponse.json() |
|||
|
|||
if (!googleDoc._id) { |
|||
google = { |
|||
type: ConfigTypes.Google, |
|||
config: {}, |
|||
} |
|||
} else { |
|||
google = googleDoc |
|||
} |
|||
}) |
|||
</script> |
|||
|
|||
<Page> |
|||
<header> |
|||
<Heading size="M">OAuth</Heading> |
|||
<Body size="S"> |
|||
Every budibase app comes with basic authentication (email/password) |
|||
included. You can add additional authentication methods from the options |
|||
below. |
|||
</Body> |
|||
</header> |
|||
<Divider /> |
|||
{#if google} |
|||
<div class="config-form"> |
|||
<Layout gap="S"> |
|||
<Heading size="S"> |
|||
<span> |
|||
<GoogleLogo /> |
|||
Google |
|||
</span> |
|||
</Heading> |
|||
{#each ConfigFields.Google as field} |
|||
<div class="form-row"> |
|||
<Label>{field}</Label> |
|||
<Input bind:value={google.config[field]} /> |
|||
</div> |
|||
{/each} |
|||
</Layout> |
|||
<Button primary on:click={() => save(google)}>Save</Button> |
|||
</div> |
|||
<Divider /> |
|||
{/if} |
|||
</Page> |
|||
|
|||
<style> |
|||
.config-form { |
|||
margin-top: 42px; |
|||
margin-bottom: 42px; |
|||
} |
|||
|
|||
.form-row { |
|||
display: grid; |
|||
grid-template-columns: 20% 1fr; |
|||
grid-gap: var(--spacing-l); |
|||
} |
|||
|
|||
span { |
|||
display: flex; |
|||
align-items: center; |
|||
gap: var(--spacing-s); |
|||
} |
|||
|
|||
header { |
|||
margin-bottom: 42px; |
|||
} |
|||
</style> |
|||
@ -0,0 +1,168 @@ |
|||
<script> |
|||
import { goto } from "@roxi/routify" |
|||
import { |
|||
ActionButton, |
|||
Button, |
|||
Layout, |
|||
Heading, |
|||
Body, |
|||
Divider, |
|||
Label, |
|||
Input, |
|||
Modal, |
|||
Table, |
|||
ModalContent, |
|||
notifications, |
|||
} from "@budibase/bbui" |
|||
import { fetchData } from "helpers" |
|||
import { users } from "stores/portal" |
|||
|
|||
import TagsRenderer from "./_components/TagsTableRenderer.svelte" |
|||
import UpdateRolesModal from "./_components/UpdateRolesModal.svelte" |
|||
|
|||
export let userId |
|||
let deleteUserModal |
|||
let editRolesModal |
|||
|
|||
const roleSchema = { |
|||
name: { displayName: "App" }, |
|||
role: {}, |
|||
} |
|||
|
|||
// Merge the Apps list and the roles response to get something that makes sense for the table |
|||
$: appList = Object.keys($apps?.data).map(id => ({ |
|||
...$apps?.data?.[id], |
|||
_id: id, |
|||
role: [$roleFetch?.data?.roles?.[id]], |
|||
})) |
|||
let selectedApp |
|||
|
|||
const roleFetch = fetchData(`/api/admin/users/${userId}`) |
|||
const apps = fetchData(`/api/admin/roles`) |
|||
|
|||
async function deleteUser() { |
|||
const res = await users.del(userId) |
|||
if (res.message) { |
|||
notifications.success(`User ${$roleFetch?.data?.email} deleted.`) |
|||
$goto("./") |
|||
} else { |
|||
notifications.error("Failed to delete user.") |
|||
} |
|||
} |
|||
|
|||
async function openUpdateRolesModal({ detail }) { |
|||
console.log(detail) |
|||
selectedApp = detail |
|||
editRolesModal.show() |
|||
} |
|||
</script> |
|||
|
|||
<Layout noPadding gap="XS"> |
|||
<div class="back"> |
|||
<ActionButton on:click={() => $goto("./")} quiet size="S" icon="BackAndroid" |
|||
>Back to users</ActionButton |
|||
> |
|||
</div> |
|||
<div class="heading"> |
|||
<Layout noPadding gap="XS"> |
|||
<Heading>User: {$roleFetch?.data?.email}</Heading> |
|||
<Body |
|||
>Lorem ipsum dolor sit amet consectetur adipisicing elit. Debitis porro |
|||
ut nesciunt ipsam perspiciatis aliquam et hic minus alias beatae. Odit |
|||
veritatis quos quas laborum magnam tenetur perspiciatis ex hic. |
|||
</Body> |
|||
</Layout> |
|||
</div> |
|||
<Divider size="S" /> |
|||
<div class="general"> |
|||
<Heading size="S">General</Heading> |
|||
<div class="fields"> |
|||
<div class="field"> |
|||
<Label size="L">Email</Label> |
|||
<Input disabled thin value={$roleFetch?.data?.email} /> |
|||
</div> |
|||
</div> |
|||
<div class="regenerate"> |
|||
<ActionButton size="S" icon="Refresh" quiet |
|||
>Regenerate password</ActionButton |
|||
> |
|||
</div> |
|||
</div> |
|||
<Divider size="S" /> |
|||
<div class="roles"> |
|||
<Heading size="S">Configure roles</Heading> |
|||
<Table |
|||
on:click={openUpdateRolesModal} |
|||
schema={roleSchema} |
|||
data={appList} |
|||
allowEditColumns={false} |
|||
allowEditRows={false} |
|||
allowSelectRows={false} |
|||
customRenderers={[{ column: "role", component: TagsRenderer }]} |
|||
/> |
|||
</div> |
|||
<Divider size="S" /> |
|||
<div class="delete"> |
|||
<Layout gap="S" noPadding |
|||
><Heading size="S">Delete user</Heading> |
|||
<Body>Deleting a user completely removes them from your account.</Body> |
|||
<div class="delete-button"> |
|||
<Button warning on:click={deleteUserModal.show}>Delete user</Button> |
|||
</div></Layout |
|||
> |
|||
</div> |
|||
</Layout> |
|||
|
|||
<Modal bind:this={deleteUserModal}> |
|||
<ModalContent |
|||
warning |
|||
onConfirm={deleteUser} |
|||
title="Delete User" |
|||
confirmText="Delete user" |
|||
cancelText="Cancel" |
|||
showCloseIcon={false} |
|||
> |
|||
<Body |
|||
>Are you sure you want to delete <strong>{$roleFetch?.data?.email}</strong |
|||
></Body |
|||
> |
|||
</ModalContent> |
|||
</Modal> |
|||
<Modal bind:this={editRolesModal}> |
|||
<UpdateRolesModal |
|||
app={selectedApp} |
|||
user={$roleFetch.data} |
|||
on:update={roleFetch.refresh} |
|||
/> |
|||
</Modal> |
|||
|
|||
<style> |
|||
.fields { |
|||
display: grid; |
|||
grid-gap: var(--spacing-m); |
|||
margin-top: var(--spacing-xl); |
|||
} |
|||
.field { |
|||
display: grid; |
|||
grid-template-columns: 32% 1fr; |
|||
align-items: center; |
|||
} |
|||
.heading { |
|||
margin-bottom: var(--spacing-xl); |
|||
} |
|||
.general { |
|||
position: relative; |
|||
margin: var(--spacing-xl) 0; |
|||
} |
|||
.roles { |
|||
margin: var(--spacing-xl) 0; |
|||
} |
|||
.delete { |
|||
margin-top: var(--spacing-xl); |
|||
} |
|||
.regenerate { |
|||
position: absolute; |
|||
top: 0; |
|||
right: 0; |
|||
} |
|||
</style> |
|||
@ -0,0 +1,59 @@ |
|||
<script> |
|||
import { |
|||
Body, |
|||
Input, |
|||
Select, |
|||
ModalContent, |
|||
notifications, |
|||
} from "@budibase/bbui" |
|||
import { createValidationStore, emailValidator } from "helpers/validation" |
|||
import { users } from "stores/portal" |
|||
|
|||
export let disabled |
|||
|
|||
const options = ["Email onboarding", "Basic onboarding"] |
|||
let selected = options[0] |
|||
|
|||
const [email, error, touched] = createValidationStore("", emailValidator) |
|||
|
|||
async function createUserFlow() { |
|||
const res = await users.invite($email) |
|||
console.log(res) |
|||
if (res.status) { |
|||
notifications.error(res.message) |
|||
} else { |
|||
notifications.success(res.message) |
|||
} |
|||
} |
|||
</script> |
|||
|
|||
<ModalContent |
|||
onConfirm={createUserFlow} |
|||
size="M" |
|||
title="Add new user options" |
|||
confirmText="Add user" |
|||
confirmDisabled={disabled} |
|||
cancelText="Cancel" |
|||
disabled={$error} |
|||
showCloseIcon={false} |
|||
> |
|||
<Body noPadding |
|||
>If you have SMTP configured and an email for the new user, you can use the |
|||
automated email onboarding flow. Otherwise, use our basic onboarding process |
|||
with autogenerated passwords.</Body |
|||
> |
|||
<Select |
|||
placeholder={null} |
|||
bind:value={selected} |
|||
on:change |
|||
{options} |
|||
label="Add new user via:" |
|||
/> |
|||
<Input |
|||
type="email" |
|||
bind:value={$email} |
|||
error={$touched && $error} |
|||
placeholder="john@doe.com" |
|||
label="Email" |
|||
/> |
|||
</ModalContent> |
|||
@ -0,0 +1,40 @@ |
|||
<script> |
|||
import { ModalContent, Body, Input, notifications } from "@budibase/bbui" |
|||
import { createValidationStore, emailValidator } from "helpers/validation" |
|||
import { users } from "stores/portal" |
|||
|
|||
const [email, error, touched] = createValidationStore("", emailValidator) |
|||
const password = Math.random().toString(36).substr(2, 20) |
|||
|
|||
async function createUser() { |
|||
const res = await users.create({ email: $email, password }) |
|||
if (res.status) { |
|||
notifications.error(res.message) |
|||
} else { |
|||
notifications.success("Succesfully created user") |
|||
} |
|||
} |
|||
</script> |
|||
|
|||
<ModalContent |
|||
onConfirm={createUser} |
|||
size="M" |
|||
title="Basic user onboarding" |
|||
confirmText="Continue" |
|||
cancelText="Cancel" |
|||
disabled={$error} |
|||
error={$touched && $error} |
|||
showCloseIcon={false} |
|||
> |
|||
<Body noPadding |
|||
>Below you will find the user’s username and password. The password will not |
|||
be accessible from this point. Please download the credentials.</Body |
|||
> |
|||
<Input |
|||
type="email" |
|||
label="Username" |
|||
bind:value={$email} |
|||
error={$touched && $error} |
|||
/> |
|||
<Input disabled label="Password" value={password} /> |
|||
</ModalContent> |
|||
@ -0,0 +1,20 @@ |
|||
<script> |
|||
import { Tag, Tags } from "@budibase/bbui" |
|||
export let value |
|||
|
|||
const displayLimit = 5 |
|||
|
|||
$: tags = value?.slice(0, displayLimit) ?? [] |
|||
$: leftover = (value?.length ?? 0) - tags.length |
|||
</script> |
|||
|
|||
<Tags> |
|||
{#each tags as tag} |
|||
<Tag> |
|||
{tag} |
|||
</Tag> |
|||
{/each} |
|||
{#if leftover} |
|||
<Tag>+{leftover} more</Tag> |
|||
{/if} |
|||
</Tags> |
|||
@ -0,0 +1,51 @@ |
|||
<script> |
|||
import { createEventDispatcher } from "svelte" |
|||
import { Body, Select, ModalContent, notifications } from "@budibase/bbui" |
|||
import { fetchData } from "helpers" |
|||
import { users } from "stores/portal" |
|||
|
|||
export let app |
|||
export let user |
|||
|
|||
const dispatch = createEventDispatcher() |
|||
|
|||
const roles = app.roles |
|||
let options = roles.map(role => role._id) |
|||
let selectedRole |
|||
|
|||
async function updateUserRoles() { |
|||
const res = await users.updateRoles({ |
|||
...user, |
|||
roles: { |
|||
...user.roles, |
|||
[app._id]: selectedRole, |
|||
}, |
|||
}) |
|||
if (res.status === 400) { |
|||
notifications.error("Failed to update role") |
|||
} else { |
|||
notifications.success("Roles updated") |
|||
dispatch("update") |
|||
} |
|||
} |
|||
</script> |
|||
|
|||
<ModalContent |
|||
onConfirm={updateUserRoles} |
|||
title="Update App Roles" |
|||
confirmText="Update roles" |
|||
cancelText="Cancel" |
|||
size="M" |
|||
showCloseIcon={false} |
|||
> |
|||
<Body noPadding |
|||
>Update {user.email}'s roles for <strong>{app.name}</strong>.</Body |
|||
> |
|||
<Select |
|||
placeholder={null} |
|||
bind:value={selectedRole} |
|||
on:change |
|||
{options} |
|||
label="Select roles:" |
|||
/> |
|||
</ModalContent> |
|||
@ -0,0 +1,105 @@ |
|||
<script> |
|||
import { goto } from "@roxi/routify" |
|||
import { |
|||
Heading, |
|||
Body, |
|||
Divider, |
|||
Button, |
|||
ButtonGroup, |
|||
Search, |
|||
Table, |
|||
Label, |
|||
Layout, |
|||
Modal, |
|||
} from "@budibase/bbui" |
|||
import TagsRenderer from "./_components/TagsTableRenderer.svelte" |
|||
import AddUserModal from "./_components/AddUserModal.svelte" |
|||
import BasicOnboardingModal from "./_components/BasicOnboardingModal.svelte" |
|||
import { users } from "stores/portal" |
|||
|
|||
users.init() |
|||
|
|||
const schema = { |
|||
email: {}, |
|||
status: { displayName: "Development Access", type: "boolean" }, |
|||
// role: { type: "options" }, |
|||
group: {}, |
|||
// access: {}, |
|||
// group: {} |
|||
} |
|||
|
|||
let search |
|||
let email |
|||
$: filteredUsers = $users |
|||
.filter(user => user.email.includes(search || "")) |
|||
.map(user => ({ ...user, group: ["All"] })) |
|||
|
|||
let createUserModal |
|||
let basicOnboardingModal |
|||
|
|||
function openBasicOnoboardingModal() { |
|||
createUserModal.hide() |
|||
basicOnboardingModal.show() |
|||
} |
|||
</script> |
|||
|
|||
<Layout> |
|||
<div class="heading"> |
|||
<Heading>Users</Heading> |
|||
<Body |
|||
>Users are the common denominator in Budibase. Each user is assigned to a |
|||
group that contains apps and permissions. In this section, you can add |
|||
users, or edit and delete an existing user.</Body |
|||
> |
|||
</div> |
|||
<Divider size="S" /> |
|||
|
|||
<div class="users"> |
|||
<Heading size="S">Users</Heading> |
|||
<div class="field"> |
|||
<Label size="L">Search / filter</Label> |
|||
<Search bind:value={search} placeholder="" /> |
|||
</div> |
|||
<div class="buttons"> |
|||
<ButtonGroup> |
|||
<Button disabled secondary>Import users</Button> |
|||
<Button overBackground on:click={createUserModal.show}>Add user</Button> |
|||
</ButtonGroup> |
|||
</div> |
|||
<Table |
|||
on:click={({ detail }) => $goto(`./${detail._id}`)} |
|||
{schema} |
|||
data={filteredUsers || $users} |
|||
allowEditColumns={false} |
|||
allowEditRows={false} |
|||
allowSelectRows={false} |
|||
customRenderers={[{ column: "group", component: TagsRenderer }]} |
|||
/> |
|||
</div> |
|||
</Layout> |
|||
|
|||
<Modal bind:this={createUserModal} |
|||
><AddUserModal on:change={openBasicOnoboardingModal} /></Modal |
|||
> |
|||
<Modal bind:this={basicOnboardingModal}><BasicOnboardingModal {email} /></Modal> |
|||
|
|||
<style> |
|||
.users { |
|||
position: relative; |
|||
} |
|||
.field { |
|||
display: flex; |
|||
align-items: center; |
|||
flex-direction: row; |
|||
grid-gap: var(--spacing-m); |
|||
margin: var(--spacing-xl) 0; |
|||
} |
|||
.field > :global(*) + :global(*) { |
|||
margin-left: var(--spacing-m); |
|||
} |
|||
.buttons { |
|||
position: absolute; |
|||
top: 0; |
|||
right: 0; |
|||
} |
|||
</style> |
|||
|
After Width: | Height: | Size: 1.5 KiB |
@ -1,4 +1,5 @@ |
|||
export { organisation } from "./organisation" |
|||
export { users } from "./users" |
|||
export { admin } from "./admin" |
|||
export { apps } from "./apps" |
|||
export { email } from "./email" |
|||
export { email } from "./email" |
|||
|
|||
@ -0,0 +1,65 @@ |
|||
import { writable } from "svelte/store" |
|||
import api, { post } from "builderStore/api" |
|||
import { update } from "lodash" |
|||
|
|||
export function createUsersStore() { |
|||
const { subscribe, set } = writable([]) |
|||
|
|||
async function init() { |
|||
const response = await api.get(`/api/admin/users`) |
|||
const json = await response.json() |
|||
set(json) |
|||
} |
|||
|
|||
async function invite(email) { |
|||
const response = await api.post(`/api/admin/users/invite`, { email }) |
|||
return await response.json() |
|||
} |
|||
async function acceptInvite(inviteCode, password) { |
|||
const response = await api.post("/api/admin/users/invite/accept", { |
|||
inviteCode, |
|||
password, |
|||
}) |
|||
return await response.json() |
|||
} |
|||
|
|||
async function create({ email, password }) { |
|||
const response = await api.post("/api/admin/users", { |
|||
email, |
|||
password, |
|||
builder: { global: true }, |
|||
roles: {}, |
|||
}) |
|||
init() |
|||
return await response.json() |
|||
} |
|||
|
|||
async function del(id) { |
|||
const response = await api.delete(`/api/admin/users/${id}`) |
|||
update(users => users.filter(user => user._id !== id)) |
|||
return await response.json() |
|||
} |
|||
|
|||
async function updateRoles(data) { |
|||
try { |
|||
const res = await post(`/api/admin/users`, data) |
|||
const json = await res.json() |
|||
return json |
|||
} catch (error) { |
|||
console.log(error) |
|||
return error |
|||
} |
|||
} |
|||
|
|||
return { |
|||
subscribe, |
|||
init, |
|||
invite, |
|||
acceptInvite, |
|||
create, |
|||
updateRoles, |
|||
del, |
|||
} |
|||
} |
|||
|
|||
export const users = createUsersStore() |
|||
@ -1,18 +1,26 @@ |
|||
const { QueryBuilder, buildSearchUrl, search } = require("./utils") |
|||
const { fullSearch, paginatedSearch } = require("./utils") |
|||
const CouchDB = require("../../../db") |
|||
const { outputProcessing } = require("../../../utilities/rowProcessor") |
|||
|
|||
exports.rowSearch = async ctx => { |
|||
const appId = ctx.appId |
|||
const { tableId } = ctx.params |
|||
const { bookmark, query, raw } = ctx.request.body |
|||
let url |
|||
if (query) { |
|||
url = new QueryBuilder(appId, query, bookmark).addTable(tableId).complete() |
|||
} else if (raw) { |
|||
url = buildSearchUrl({ |
|||
appId, |
|||
query: raw, |
|||
bookmark, |
|||
}) |
|||
const db = new CouchDB(appId) |
|||
const { paginate, query, ...params } = ctx.request.body |
|||
params.tableId = tableId |
|||
|
|||
let response |
|||
if (paginate) { |
|||
response = await paginatedSearch(appId, query, params) |
|||
} else { |
|||
response = await fullSearch(appId, query, params) |
|||
} |
|||
|
|||
// Enrich search results with relationships
|
|||
if (response.rows && response.rows.length) { |
|||
const table = await db.get(tableId) |
|||
response.rows = await outputProcessing(appId, table, response.rows) |
|||
} |
|||
ctx.body = await search(url) |
|||
|
|||
ctx.body = response |
|||
} |
|||
|
|||
@ -1,195 +0,0 @@ |
|||
<script> |
|||
import { getContext } from "svelte" |
|||
import { |
|||
Button, |
|||
DatePicker, |
|||
Label, |
|||
Select, |
|||
Toggle, |
|||
Input, |
|||
} from "@budibase/bbui" |
|||
|
|||
const { API, styleable, Provider, builderStore, ActionTypes } = getContext( |
|||
"sdk" |
|||
) |
|||
const component = getContext("component") |
|||
|
|||
export let table |
|||
export let columns = [] |
|||
export let pageSize |
|||
export let noRowsMessage |
|||
|
|||
let rows = [] |
|||
let loaded = false |
|||
let search = {} |
|||
let tableDefinition |
|||
let schema |
|||
|
|||
let nextBookmark = null |
|||
let bookmark = null |
|||
let lastBookmark = null |
|||
|
|||
$: fetchData(table, bookmark) |
|||
// omit empty strings |
|||
$: parsedSearch = Object.keys(search).reduce( |
|||
(acc, next) => |
|||
search[next] === "" ? acc : { ...acc, [next]: search[next] }, |
|||
{} |
|||
) |
|||
$: actions = [ |
|||
{ |
|||
type: ActionTypes.RefreshDatasource, |
|||
callback: () => fetchData(table, bookmark), |
|||
metadata: { datasource: { type: "table", tableId: table } }, |
|||
}, |
|||
] |
|||
|
|||
async function fetchData(table, mark) { |
|||
if (table) { |
|||
const tableDef = await API.fetchTableDefinition(table) |
|||
schema = tableDef.schema |
|||
const output = await API.searchTableData({ |
|||
tableId: table, |
|||
search: parsedSearch, |
|||
pagination: { |
|||
pageSize, |
|||
bookmark: mark, |
|||
}, |
|||
}) |
|||
rows = output.rows |
|||
nextBookmark = output.bookmark |
|||
} |
|||
loaded = true |
|||
} |
|||
|
|||
function nextPage() { |
|||
lastBookmark = bookmark |
|||
bookmark = nextBookmark |
|||
} |
|||
|
|||
function previousPage() { |
|||
nextBookmark = bookmark |
|||
if (lastBookmark !== bookmark) { |
|||
bookmark = lastBookmark |
|||
} else { |
|||
// special case for going back to beginning |
|||
bookmark = null |
|||
lastBookmark = null |
|||
} |
|||
} |
|||
</script> |
|||
|
|||
<Provider {actions}> |
|||
<div use:styleable={$component.styles}> |
|||
<div class="query-builder"> |
|||
{#if schema} |
|||
{#each columns as field} |
|||
<div class="form-field"> |
|||
<Label extraSmall grey>{schema[field].name}</Label> |
|||
{#if schema[field].type === "options"} |
|||
<Select secondary bind:value={search[field]}> |
|||
<option value="">Choose an option</option> |
|||
{#each schema[field].constraints.inclusion as opt} |
|||
<option>{opt}</option> |
|||
{/each} |
|||
</Select> |
|||
{:else if schema[field].type === "datetime"} |
|||
<DatePicker bind:value={search[field]} /> |
|||
{:else if schema[field].type === "boolean"} |
|||
<Toggle text={schema[field].name} bind:checked={search[field]} /> |
|||
{:else if schema[field].type === "number"} |
|||
<Input type="number" bind:value={search[field]} /> |
|||
{:else if schema[field].type === "string"} |
|||
<Input bind:value={search[field]} /> |
|||
{/if} |
|||
</div> |
|||
{/each} |
|||
{/if} |
|||
<div class="actions"> |
|||
<Button |
|||
secondary |
|||
on:click={() => { |
|||
search = {} |
|||
bookmark = null |
|||
}} |
|||
> |
|||
Reset |
|||
</Button> |
|||
<Button |
|||
primary |
|||
on:click={() => { |
|||
bookmark = null |
|||
fetchData(table, bookmark) |
|||
}} |
|||
> |
|||
Search |
|||
</Button> |
|||
</div> |
|||
</div> |
|||
{#if loaded} |
|||
{#if rows.length > 0} |
|||
{#if $component.children === 0 && $builderStore.inBuilder} |
|||
<p><i class="ri-image-line" />Add some components to display.</p> |
|||
{:else} |
|||
{#each rows as row} |
|||
<Provider data={row}> |
|||
<slot /> |
|||
</Provider> |
|||
{/each} |
|||
{/if} |
|||
{:else if noRowsMessage} |
|||
<p><i class="ri-search-2-line" />{noRowsMessage}</p> |
|||
{/if} |
|||
{/if} |
|||
<div class="pagination"> |
|||
{#if lastBookmark != null || bookmark != null} |
|||
<Button primary on:click={previousPage}>Back</Button> |
|||
{/if} |
|||
{#if nextBookmark != null && rows.length !== 0} |
|||
<Button primary on:click={nextPage}>Next</Button> |
|||
{/if} |
|||
</div> |
|||
</div> |
|||
</Provider> |
|||
|
|||
<style> |
|||
p { |
|||
margin: 0 var(--spacing-m); |
|||
background-color: var(--grey-2); |
|||
color: var(--grey-6); |
|||
font-size: var(--font-size-s); |
|||
padding: var(--spacing-l); |
|||
border-radius: var(--border-radius-s); |
|||
display: grid; |
|||
place-items: center; |
|||
} |
|||
p i { |
|||
margin-bottom: var(--spacing-m); |
|||
font-size: 1.5rem; |
|||
color: var(--grey-5); |
|||
} |
|||
|
|||
.query-builder { |
|||
padding: var(--spacing-m); |
|||
border-radius: var(--border-radius-s); |
|||
} |
|||
|
|||
.actions { |
|||
display: grid; |
|||
grid-gap: var(--spacing-s); |
|||
justify-content: flex-end; |
|||
grid-auto-flow: column; |
|||
} |
|||
|
|||
.form-field { |
|||
margin-bottom: var(--spacing-m); |
|||
} |
|||
|
|||
.pagination { |
|||
display: grid; |
|||
grid-gap: var(--spacing-s); |
|||
justify-content: flex-end; |
|||
margin-top: var(--spacing-m); |
|||
grid-auto-flow: column; |
|||
} |
|||
</style> |
|||
Loading…
Reference in new issue