Merge branch 'master' into BUDI-8064/doc-writethrough
This commit is contained in:
commit
8611fade5e
|
@ -0,0 +1,221 @@
|
|||
<p align="center">
|
||||
<a href="https://www.budibase.com">
|
||||
<img alt="Budibase" src="https://res.cloudinary.com/daog6scxm/image/upload/v1696515725/Branding/Assets/Symbol/RGB/Full%20Colour/Budibase_Symbol_RGB_FullColour_cbqvha_1_z5cwq2.svg" width="60" />
|
||||
</a>
|
||||
</p>
|
||||
<h1 align="center">
|
||||
Budibase
|
||||
</h1>
|
||||
<h3 align="center">
|
||||
자체 인프라에서 몇 분 만에 맞춤형 비즈니스 도구를 구축하세요.
|
||||
</h3>
|
||||
<p align="center">
|
||||
Budibase는 개발자와 IT 전문가가 몇 분 만에 맞춤형 애플리케이션을 구축하고 자동화할 수 있는 오픈 소스 로우코드 플랫폼입니다.
|
||||
</p>
|
||||
|
||||
<h3 align="center">
|
||||
🤖 🎨 🚀
|
||||
</h3>
|
||||
|
||||
<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를 사용하면 고성능 단일 페이지 애플리케이션을 구축할 수 있습니다. 또한 반응형 디자인으로 제작하여 사용자에게 멋진 경험을 제공할 수 있습니다.
|
||||
<br /><br />
|
||||
|
||||
### 오픈 소스 및 확장성
|
||||
Budibase는 오픈소스이며, GPL v3 라이선스에 따라 공개되어 있습니다. 이는 Budibase가 항상 당신 곁에 있다는 안도감을 줄 것입니다. 그리고 우리는 개발자 친화적인 환경을 제공하고 있기 때문에, 당신은 원하는 만큼 소스 코드를 포크하여 수정하거나 Budibase에 직접 기여할 수 있습니다.
|
||||
<br /><br />
|
||||
|
||||
### 기존 데이터 또는 처음부터 시작
|
||||
Budibase를 사용하면 다음과 같은 여러 소스에서 데이터를 가져올 수 있습니다: MondoDB, 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 />
|
||||
|
||||
### 프로세스를 자동화하고, 다른 도구와 연동하고, 웹훅으로 연결하세요!
|
||||
워크플로우와 수동 프로세스를 자동화하여 시간을 절약하세요. 웹훅 이벤트 연결부터 이메일 자동화까지, 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 />
|
||||
|
||||
## 🏁 시작
|
||||
|
||||
Docker, Kubernetes 또는 Digital Ocean을 사용하여 자체 인프라에서 Budibase를 호스팅하거나, 걱정 없이 빠르게 애플리케이션을 구축하려는 경우 클라우드에서 Budibase를 사용할 수 있습니다.
|
||||
|
||||
### [Budibase 셀프 호스팅으로 시작하기](https://docs.budibase.com/docs/hosting-methods)
|
||||
|
||||
- [Docker - single ARM compatible image](https://docs.budibase.com/docs/docker)
|
||||
- [Docker Compose](https://docs.budibase.com/docs/docker-compose)
|
||||
- [Kubernetes](https://docs.budibase.com/docs/kubernetes-k8s)
|
||||
- [Digital Ocean](https://docs.budibase.com/docs/digitalocean)
|
||||
- [Portainer](https://docs.budibase.com/docs/portainer)
|
||||
|
||||
|
||||
### [클라우드에서 Budibase 시작하기](https://budibase.com)
|
||||
|
||||
<br /><br />
|
||||
|
||||
## 🎓 Budibase 알아보기
|
||||
|
||||
문서 [documentacion de Budibase](https://docs.budibase.com/docs).
|
||||
<br />
|
||||
|
||||
|
||||
<br /><br />
|
||||
|
||||
## 💬 커뮤니티
|
||||
|
||||
질문하고, 다른 사람을 돕고, 다른 Budibase 사용자와 즐거운 대화를 나눌 수 있는 Budibase 커뮤니티에 여러분을 초대합니다.
|
||||
[깃허브 토론](https://github.com/Budibase/budibase/discussions)
|
||||
<br /><br /><br />
|
||||
|
||||
|
||||
## ❗ 행동강령
|
||||
|
||||
Budibase 는 모든 계층의 사람들을 환영하고 상호 존중하는 환경을 제공하는 데 특별한 주의를 기울이고 있습니다. 저희는 커뮤니티에도 같은 기대를 가지고 있습니다.
|
||||
[**행동 강령**](https://github.com/Budibase/budibase/blob/HEAD/.github/CODE_OF_CONDUCT.md).
|
||||
<br />
|
||||
|
||||
<br /><br />
|
||||
|
||||
|
||||
## 🙌 Contribuir en Budibase
|
||||
|
||||
버그 신고부터 코드의 버그 수정에 이르기까지 모든 기여를 감사하고 환영합니다. 새로운 기능을 구현하거나 API를 변경할 계획이 있다면 [여기에 새 메시지](https://github.com/Budibase/budibase/issues),
|
||||
이렇게 하면 여러분의 노력이 헛되지 않도록 보장할 수 있습니다.
|
||||
|
||||
여기에는 다음을 위해 Budibase 환경을 설정하는 방법에 대한 지침이 나와 있습니다. [여기를 클릭하세요](https://github.com/Budibase/budibase/tree/HEAD/docs/CONTRIBUTING.md).
|
||||
|
||||
### 어디서부터 시작해야 할지 혼란스러우신가요?
|
||||
이곳은 기여를 시작하기에 최적의 장소입니다! [First time issues project](https://github.com/Budibase/budibase/projects/22).
|
||||
|
||||
### 리포지토리 구성
|
||||
|
||||
Budibase는 Lerna에서 관리하는 단일 리포지토리입니다. Lerna는 변경 사항이 있을 때마다 이를 동기화하여 Budibase 패키지를 빌드하고 게시합니다. 크게 보면 이러한 패키지가 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) - budibase builder 클라이언트 측의 svelte 애플리케이션 코드가 포함되어 있습니다.
|
||||
|
||||
- [packages/server](https://github.com/Budibase/budibase/tree/HEAD/packages/server) - Budibase의 서버 부분입니다. 이 Koa 애플리케이션은 빌더에게 Budibase 애플리케이션을 생성하는 데 필요한 것을 제공하는 역할을 합니다. 또한 데이터베이스 및 파일 저장소와 상호 작용할 수 있는 API를 제공합니다.
|
||||
|
||||
자세한 내용은 다음 문서를 참조하세요. [CONTRIBUTING.md](https://github.com/Budibase/budibase/blob/HEAD/docs/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/docs/CONTRIBUTING.md#troubleshooting) 를 참고하여 환경을 정리해 주세요.
|
||||
|
||||
<br /><br />
|
||||
|
||||
## Contributors ✨
|
||||
|
||||
훌륭한 여러분께 감사할 따름입니다. ([emoji key](https://allcontributors.org/docs/en/emoji-key)):
|
||||
|
||||
<!-- ALL-CONTRIBUTORS-LIST:START - Do not remove or modify this section -->
|
||||
<!-- prettier-ignore-start -->
|
||||
<!-- markdownlint-disable -->
|
||||
<table>
|
||||
<tr>
|
||||
<td align="center"><a href="http://martinmck.com"><img src="https://avatars1.githubusercontent.com/u/11256663?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Martin McKeaveney</b></sub></a><br /><a href="https://github.com/Budibase/budibase/commits?author=shogunpurple" title="Code">💻</a> <a href="https://github.com/Budibase/budibase/commits?author=shogunpurple" title="Documentation">📖</a> <a href="https://github.com/Budibase/budibase/commits?author=shogunpurple" title="Tests">⚠️</a> <a href="#infra-shogunpurple" title="Infrastructure (Hosting, Build-Tools, etc)">🚇</a></td>
|
||||
<td align="center"><a href="http://www.michaeldrury.co.uk/"><img src="https://avatars2.githubusercontent.com/u/4407001?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Michael Drury</b></sub></a><br /><a href="https://github.com/Budibase/budibase/commits?author=mike12345567" title="Documentation">📖</a> <a href="https://github.com/Budibase/budibase/commits?author=mike12345567" title="Code">💻</a> <a href="https://github.com/Budibase/budibase/commits?author=mike12345567" title="Tests">⚠️</a> <a href="#infra-mike12345567" title="Infrastructure (Hosting, Build-Tools, etc)">🚇</a></td>
|
||||
<td align="center"><a href="https://github.com/aptkingston"><img src="https://avatars3.githubusercontent.com/u/9075550?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Andrew Kingston</b></sub></a><br /><a href="https://github.com/Budibase/budibase/commits?author=aptkingston" title="Documentation">📖</a> <a href="https://github.com/Budibase/budibase/commits?author=aptkingston" title="Code">💻</a> <a href="https://github.com/Budibase/budibase/commits?author=aptkingston" title="Tests">⚠️</a> <a href="#design-aptkingston" title="Design">🎨</a></td>
|
||||
<td align="center"><a href="https://budibase.com/"><img src="https://avatars3.githubusercontent.com/u/3524181?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Michael Shanks</b></sub></a><br /><a href="https://github.com/Budibase/budibase/commits?author=mjashanks" title="Documentation">📖</a> <a href="https://github.com/Budibase/budibase/commits?author=mjashanks" title="Code">💻</a> <a href="https://github.com/Budibase/budibase/commits?author=mjashanks" title="Tests">⚠️</a></td>
|
||||
<td align="center"><a href="https://github.com/kevmodrome"><img src="https://avatars3.githubusercontent.com/u/534488?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Kevin Åberg Kultalahti</b></sub></a><br /><a href="https://github.com/Budibase/budibase/commits?author=kevmodrome" title="Documentation">📖</a> <a href="https://github.com/Budibase/budibase/commits?author=kevmodrome" title="Code">💻</a> <a href="https://github.com/Budibase/budibase/commits?author=kevmodrome" title="Tests">⚠️</a></td>
|
||||
<td align="center"><a href="https://www.budibase.com/"><img src="https://avatars2.githubusercontent.com/u/49767913?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Joe</b></sub></a><br /><a href="https://github.com/Budibase/budibase/commits?author=joebudi" title="Documentation">📖</a> <a href="https://github.com/Budibase/budibase/commits?author=joebudi" title="Code">💻</a> <a href="#content-joebudi" title="Content">🖋</a> <a href="#design-joebudi" title="Design">🎨</a></td>
|
||||
<td align="center"><a href="https://github.com/Rory-Powell"><img src="https://avatars.githubusercontent.com/u/8755148?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Rory Powell</b></sub></a><br /><a href="https://github.com/Budibase/budibase/commits?author=Rory-Powell" title="Code">💻</a> <a href="https://github.com/Budibase/budibase/commits?author=Rory-Powell" title="Documentation">📖</a> <a href="https://github.com/Budibase/budibase/commits?author=Rory-Powell" title="Tests">⚠️</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center"><a href="https://github.com/PClmnt"><img src="https://avatars.githubusercontent.com/u/5665926?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Peter Clement</b></sub></a><br /><a href="https://github.com/Budibase/budibase/commits?author=PClmnt" title="Code">💻</a> <a href="https://github.com/Budibase/budibase/commits?author=PClmnt" title="Documentation">📖</a> <a href="https://github.com/Budibase/budibase/commits?author=PClmnt" title="Tests">⚠️</a></td>
|
||||
<td align="center"><a href="https://github.com/Conor-Mack"><img src="https://avatars1.githubusercontent.com/u/36074859?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Conor_Mack</b></sub></a><br /><a href="https://github.com/Budibase/budibase/commits?author=Conor-Mack" title="Code">💻</a> <a href="https://github.com/Budibase/budibase/commits?author=Conor-Mack" title="Tests">⚠️</a></td>
|
||||
<td align="center"><a href="https://github.com/pngwn"><img src="https://avatars1.githubusercontent.com/u/12937446?v=4?s=100" width="100px;" alt=""/><br /><sub><b>pngwn</b></sub></a><br /><a href="https://github.com/Budibase/budibase/commits?author=pngwn" title="Code">💻</a> <a href="https://github.com/Budibase/budibase/commits?author=pngwn" title="Tests">⚠️</a></td>
|
||||
<td align="center"><a href="https://github.com/HugoLd"><img src="https://avatars0.githubusercontent.com/u/26521848?v=4?s=100" width="100px;" alt=""/><br /><sub><b>HugoLd</b></sub></a><br /><a href="https://github.com/Budibase/budibase/commits?author=HugoLd" title="Code">💻</a></td>
|
||||
<td align="center"><a href="https://github.com/victoriasloan"><img src="https://avatars.githubusercontent.com/u/9913651?v=4?s=100" width="100px;" alt=""/><br /><sub><b>victoriasloan</b></sub></a><br /><a href="https://github.com/Budibase/budibase/commits?author=victoriasloan" title="Code">💻</a></td>
|
||||
<td align="center"><a href="https://github.com/yashank09"><img src="https://avatars.githubusercontent.com/u/37672190?v=4?s=100" width="100px;" alt=""/><br /><sub><b>yashank09</b></sub></a><br /><a href="https://github.com/Budibase/budibase/commits?author=yashank09" title="Code">💻</a></td>
|
||||
<td align="center"><a href="https://github.com/SOVLOOKUP"><img src="https://avatars.githubusercontent.com/u/53158137?v=4?s=100" width="100px;" alt=""/><br /><sub><b>SOVLOOKUP</b></sub></a><br /><a href="https://github.com/Budibase/budibase/commits?author=SOVLOOKUP" title="Code">💻</a></td>
|
||||
<td align="center"><a href="https://github.com/seoulaja"><img src="https://avatars.githubusercontent.com/u/15101654?v=4?s=100" width="100px;" alt=""/><br /><sub><b>seoulaja</b></sub></a><br /><a href="#translation-seoulaja" title="Translation">🌍</a></td>
|
||||
<td align="center"><a href="https://github.com/mslourens"><img src="https://avatars.githubusercontent.com/u/1907152?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Maurits Lourens</b></sub></a><br /><a href="https://github.com/Budibase/budibase/commits?author=mslourens" title="Tests">⚠️</a> <a href="https://github.com/Budibase/budibase/commits?author=mslourens" title="Code">💻</a></td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<!-- markdownlint-restore -->
|
||||
<!-- prettier-ignore-end -->
|
||||
|
||||
<!-- ALL-CONTRIBUTORS-LIST:END -->
|
||||
|
||||
이 프로젝트는 다음 사양을 따릅니다. [all-contributors](https://github.com/all-contributors/all-contributors).
|
||||
모든 종류의 기여를 환영합니다!
|
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"version": "2.20.12",
|
||||
"version": "2.20.14",
|
||||
"npmClient": "yarn",
|
||||
"packages": [
|
||||
"packages/*",
|
||||
|
|
|
@ -11,6 +11,7 @@ import {
|
|||
Document,
|
||||
isDocument,
|
||||
RowResponse,
|
||||
RowValue,
|
||||
} from "@budibase/types"
|
||||
import { getCouchInfo } from "./connections"
|
||||
import { directCouchUrlCall } from "./utils"
|
||||
|
@ -230,7 +231,7 @@ export class DatabaseImpl implements Database {
|
|||
})
|
||||
}
|
||||
|
||||
async allDocs<T extends Document>(
|
||||
async allDocs<T extends Document | RowValue>(
|
||||
params: DatabaseQueryOpts
|
||||
): Promise<AllDocsResponse<T>> {
|
||||
return this.performCall(db => {
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
import {
|
||||
DocumentScope,
|
||||
DocumentDestroyResponse,
|
||||
DocumentInsertResponse,
|
||||
DocumentBulkResponse,
|
||||
|
@ -13,6 +12,7 @@ import {
|
|||
DatabasePutOpts,
|
||||
DatabaseQueryOpts,
|
||||
Document,
|
||||
RowValue,
|
||||
} from "@budibase/types"
|
||||
import tracer from "dd-trace"
|
||||
import { Writable } from "stream"
|
||||
|
@ -86,7 +86,7 @@ export class DDInstrumentedDatabase implements Database {
|
|||
})
|
||||
}
|
||||
|
||||
allDocs<T extends Document>(
|
||||
allDocs<T extends Document | RowValue>(
|
||||
params: DatabaseQueryOpts
|
||||
): Promise<AllDocsResponse<T>> {
|
||||
return tracer.trace("db.allDocs", span => {
|
||||
|
|
|
@ -74,7 +74,7 @@ export function getGlobalIDFromUserMetadataID(id: string) {
|
|||
* Generates a template ID.
|
||||
* @param ownerId The owner/user of the template, this could be global or a workspace level.
|
||||
*/
|
||||
export function generateTemplateID(ownerId: any) {
|
||||
export function generateTemplateID(ownerId: string) {
|
||||
return `${DocumentType.TEMPLATE}${SEPARATOR}${ownerId}${SEPARATOR}${newid()}`
|
||||
}
|
||||
|
||||
|
@ -105,7 +105,7 @@ export function prefixRoleID(name: string) {
|
|||
* Generates a new dev info document ID - this is scoped to a user.
|
||||
* @returns The new dev info ID which info for dev (like api key) can be stored under.
|
||||
*/
|
||||
export const generateDevInfoID = (userId: any) => {
|
||||
export const generateDevInfoID = (userId: string) => {
|
||||
return `${DocumentType.DEV_INFO}${SEPARATOR}${userId}`
|
||||
}
|
||||
|
||||
|
|
|
@ -116,7 +116,6 @@
|
|||
$: pagerText = `Page ${currentPage} of ${totalPages}`
|
||||
</script>
|
||||
|
||||
a11y-click-events-have-key-events
|
||||
<div bind:this={buttonAnchor}>
|
||||
<ActionButton on:click={dropdown.show}>
|
||||
{displayValue}
|
||||
|
|
|
@ -69,11 +69,12 @@
|
|||
// brought back to the same screen.
|
||||
const topItemNavigate = path => () => {
|
||||
const activeTopNav = $layout.children.find(c => $isActive(c.path))
|
||||
if (!activeTopNav) return
|
||||
builderStore.setPreviousTopNavPath(
|
||||
activeTopNav.path,
|
||||
window.location.pathname
|
||||
)
|
||||
if (activeTopNav) {
|
||||
builderStore.setPreviousTopNavPath(
|
||||
activeTopNav.path,
|
||||
window.location.pathname
|
||||
)
|
||||
}
|
||||
$goto($builderStore.previousTopNavPath[path] || path)
|
||||
}
|
||||
|
||||
|
|
|
@ -12,11 +12,17 @@
|
|||
hoverStore,
|
||||
} from "stores/builder"
|
||||
import ConfirmDialog from "components/common/ConfirmDialog.svelte"
|
||||
import { Layout, Heading, Body, Icon, notifications } from "@budibase/bbui"
|
||||
import {
|
||||
ProgressCircle,
|
||||
Layout,
|
||||
Heading,
|
||||
Body,
|
||||
Icon,
|
||||
notifications,
|
||||
} from "@budibase/bbui"
|
||||
import ErrorSVG from "@budibase/frontend-core/assets/error.svg?raw"
|
||||
import { findComponent, findComponentPath } from "helpers/components"
|
||||
import { isActive, goto } from "@roxi/routify"
|
||||
import { ClientAppSkeleton } from "@budibase/frontend-core"
|
||||
|
||||
let iframe
|
||||
let layout
|
||||
|
@ -234,16 +240,8 @@
|
|||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<div class="component-container">
|
||||
{#if loading}
|
||||
<div
|
||||
class={`loading ${$builderStore.theme}`}
|
||||
class:tablet={$previewStore.previewDevice === "tablet"}
|
||||
class:mobile={$previewStore.previewDevice === "mobile"}
|
||||
>
|
||||
<ClientAppSkeleton
|
||||
sideNav={$builderStore.navigation?.navigation === "Left"}
|
||||
hideFooter
|
||||
hideDevTools
|
||||
/>
|
||||
<div class="center">
|
||||
<ProgressCircle />
|
||||
</div>
|
||||
{:else if error}
|
||||
<div class="center error">
|
||||
|
@ -260,6 +258,8 @@
|
|||
bind:this={iframe}
|
||||
src="/app/preview"
|
||||
class:hidden={loading || error}
|
||||
class:tablet={$previewStore.previewDevice === "tablet"}
|
||||
class:mobile={$previewStore.previewDevice === "mobile"}
|
||||
/>
|
||||
<div
|
||||
class="add-component"
|
||||
|
@ -279,25 +279,6 @@
|
|||
/>
|
||||
|
||||
<style>
|
||||
.loading {
|
||||
position: absolute;
|
||||
container-type: inline-size;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border: 2px solid transparent;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.loading.tablet {
|
||||
width: calc(1024px + 6px);
|
||||
max-height: calc(768px + 6px);
|
||||
}
|
||||
|
||||
.loading.mobile {
|
||||
width: calc(390px + 6px);
|
||||
max-height: calc(844px + 6px);
|
||||
}
|
||||
|
||||
.component-container {
|
||||
grid-row-start: middle;
|
||||
grid-column-start: middle;
|
||||
|
|
|
@ -1,22 +1,16 @@
|
|||
<script>
|
||||
import { onMount, onDestroy } from "svelte"
|
||||
import { params, goto } from "@roxi/routify"
|
||||
import { licensing, apps, auth, sideBarCollapsed } from "stores/portal"
|
||||
import { apps, auth, sideBarCollapsed } from "stores/portal"
|
||||
import { Link, Body, ActionButton } from "@budibase/bbui"
|
||||
import { sdk } from "@budibase/shared-core"
|
||||
import { API } from "api"
|
||||
import ErrorSVG from "./ErrorSVG.svelte"
|
||||
import { ClientAppSkeleton } from "@budibase/frontend-core"
|
||||
|
||||
$: app = $apps.find(app => app.appId === $params.appId)
|
||||
$: iframeUrl = getIframeURL(app)
|
||||
$: isBuilder = sdk.users.isBuilder($auth.user, app?.devId)
|
||||
|
||||
let loading = true
|
||||
|
||||
const getIframeURL = app => {
|
||||
loading = true
|
||||
|
||||
if (app.status === "published") {
|
||||
return `/app${app.url}`
|
||||
}
|
||||
|
@ -34,20 +28,6 @@
|
|||
}
|
||||
|
||||
$: fetchScreens(app?.devId)
|
||||
|
||||
const receiveMessage = async message => {
|
||||
if (message.data.type === "docLoaded") {
|
||||
loading = false
|
||||
}
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
window.addEventListener("message", receiveMessage)
|
||||
})
|
||||
|
||||
onDestroy(() => {
|
||||
window.removeEventListener("message", receiveMessage)
|
||||
})
|
||||
</script>
|
||||
|
||||
<div class="container">
|
||||
|
@ -98,17 +78,7 @@
|
|||
</Body>
|
||||
</div>
|
||||
{:else}
|
||||
<div class:hide={!loading} class="loading">
|
||||
<div class={`loadingThemeWrapper ${app.theme}`}>
|
||||
<ClientAppSkeleton
|
||||
noAnimation
|
||||
hideDevTools={app?.status === "published"}
|
||||
sideNav={app?.navigation.navigation === "Left"}
|
||||
hideFooter={$licensing.brandingEnabled}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<iframe class:hide={loading} src={iframeUrl} title={app.name} />
|
||||
<iframe src={iframeUrl} title={app.name} />
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
|
@ -130,23 +100,6 @@
|
|||
flex: 0 0 50px;
|
||||
}
|
||||
|
||||
.loading {
|
||||
height: 100%;
|
||||
border: 1px solid var(--spectrum-global-color-gray-300);
|
||||
border-radius: var(--spacing-s);
|
||||
overflow: hidden;
|
||||
}
|
||||
.loadingThemeWrapper {
|
||||
height: 100%;
|
||||
container-type: inline-size;
|
||||
}
|
||||
|
||||
.hide {
|
||||
visibility: hidden;
|
||||
height: 0;
|
||||
border: none;
|
||||
}
|
||||
|
||||
iframe {
|
||||
flex: 1 1 auto;
|
||||
border-radius: var(--spacing-s);
|
||||
|
|
|
@ -80,18 +80,11 @@
|
|||
}
|
||||
}
|
||||
|
||||
let fontsLoaded = false
|
||||
|
||||
// Load app config
|
||||
onMount(async () => {
|
||||
document.fonts.ready.then(() => {
|
||||
fontsLoaded = true
|
||||
})
|
||||
|
||||
await initialise()
|
||||
await authStore.actions.fetchUser()
|
||||
dataLoaded = true
|
||||
|
||||
if (get(builderStore).inBuilder) {
|
||||
builderStore.actions.notifyLoaded()
|
||||
} else {
|
||||
|
@ -100,12 +93,6 @@
|
|||
})
|
||||
}
|
||||
})
|
||||
|
||||
$: {
|
||||
if (dataLoaded && fontsLoaded) {
|
||||
document.getElementById("clientAppSkeletonLoader")?.remove()
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
|
@ -116,140 +103,140 @@
|
|||
{/if}
|
||||
</svelte:head>
|
||||
|
||||
<div
|
||||
id="spectrum-root"
|
||||
lang="en"
|
||||
dir="ltr"
|
||||
class="spectrum spectrum--medium {$themeStore.baseTheme} {$themeStore.theme}"
|
||||
class:builder={$builderStore.inBuilder}
|
||||
class:show={fontsLoaded && dataLoaded}
|
||||
>
|
||||
<DeviceBindingsProvider>
|
||||
<UserBindingsProvider>
|
||||
<StateBindingsProvider>
|
||||
<RowSelectionProvider>
|
||||
<QueryParamsProvider>
|
||||
<!-- Settings bar can be rendered outside of device preview -->
|
||||
<!-- Key block needs to be outside the if statement or it breaks -->
|
||||
{#key $builderStore.selectedComponentId}
|
||||
{#if $builderStore.inBuilder}
|
||||
<SettingsBar />
|
||||
{/if}
|
||||
{/key}
|
||||
|
||||
<!-- Clip boundary for selection indicators -->
|
||||
<div
|
||||
id="clip-root"
|
||||
class:preview={$builderStore.inBuilder}
|
||||
class:tablet-preview={$builderStore.previewDevice === "tablet"}
|
||||
class:mobile-preview={$builderStore.previewDevice === "mobile"}
|
||||
>
|
||||
<!-- Actual app -->
|
||||
<div id="app-root">
|
||||
{#if showDevTools}
|
||||
<DevToolsHeader />
|
||||
{#if dataLoaded}
|
||||
<div
|
||||
id="spectrum-root"
|
||||
lang="en"
|
||||
dir="ltr"
|
||||
class="spectrum spectrum--medium {$themeStore.baseTheme} {$themeStore.theme}"
|
||||
class:builder={$builderStore.inBuilder}
|
||||
>
|
||||
<DeviceBindingsProvider>
|
||||
<UserBindingsProvider>
|
||||
<StateBindingsProvider>
|
||||
<RowSelectionProvider>
|
||||
<QueryParamsProvider>
|
||||
<!-- Settings bar can be rendered outside of device preview -->
|
||||
<!-- Key block needs to be outside the if statement or it breaks -->
|
||||
{#key $builderStore.selectedComponentId}
|
||||
{#if $builderStore.inBuilder}
|
||||
<SettingsBar />
|
||||
{/if}
|
||||
{/key}
|
||||
|
||||
<div id="app-body">
|
||||
{#if permissionError}
|
||||
<div class="error">
|
||||
<Layout justifyItems="center" gap="S">
|
||||
<!-- eslint-disable-next-line svelte/no-at-html-tags -->
|
||||
{@html ErrorSVG}
|
||||
<Heading size="L">
|
||||
You don't have permission to use this app
|
||||
</Heading>
|
||||
<Body size="S">
|
||||
Ask your administrator to grant you access
|
||||
</Body>
|
||||
</Layout>
|
||||
</div>
|
||||
{:else if !$screenStore.activeLayout}
|
||||
<div class="error">
|
||||
<Layout justifyItems="center" gap="S">
|
||||
<!-- eslint-disable-next-line svelte/no-at-html-tags -->
|
||||
{@html ErrorSVG}
|
||||
<Heading size="L">
|
||||
Something went wrong rendering your app
|
||||
</Heading>
|
||||
<Body size="S">
|
||||
Get in touch with support if this issue persists
|
||||
</Body>
|
||||
</Layout>
|
||||
</div>
|
||||
{:else if embedNoScreens}
|
||||
<div class="error">
|
||||
<Layout justifyItems="center" gap="S">
|
||||
<!-- eslint-disable-next-line svelte/no-at-html-tags -->
|
||||
{@html ErrorSVG}
|
||||
<Heading size="L">
|
||||
This Budibase app is not publicly accessible
|
||||
</Heading>
|
||||
</Layout>
|
||||
</div>
|
||||
{:else}
|
||||
<CustomThemeWrapper>
|
||||
{#key $screenStore.activeLayout._id}
|
||||
<Component
|
||||
isLayout
|
||||
instance={$screenStore.activeLayout.props}
|
||||
/>
|
||||
{/key}
|
||||
|
||||
<!--
|
||||
Flatpickr needs to be inside the theme wrapper.
|
||||
It also needs its own container because otherwise it hijacks
|
||||
key events on the whole page. It is painful to work with.
|
||||
-->
|
||||
<div id="flatpickr-root" />
|
||||
|
||||
<!-- Modal container to ensure they sit on top -->
|
||||
<div class="modal-container" />
|
||||
|
||||
<!-- Layers on top of app -->
|
||||
<NotificationDisplay />
|
||||
<ConfirmationDisplay />
|
||||
<PeekScreenDisplay />
|
||||
</CustomThemeWrapper>
|
||||
<!-- Clip boundary for selection indicators -->
|
||||
<div
|
||||
id="clip-root"
|
||||
class:preview={$builderStore.inBuilder}
|
||||
class:tablet-preview={$builderStore.previewDevice === "tablet"}
|
||||
class:mobile-preview={$builderStore.previewDevice === "mobile"}
|
||||
>
|
||||
<!-- Actual app -->
|
||||
<div id="app-root">
|
||||
{#if showDevTools}
|
||||
<DevToolsHeader />
|
||||
{/if}
|
||||
|
||||
{#if showDevTools}
|
||||
<DevTools />
|
||||
<div id="app-body">
|
||||
{#if permissionError}
|
||||
<div class="error">
|
||||
<Layout justifyItems="center" gap="S">
|
||||
<!-- eslint-disable-next-line svelte/no-at-html-tags -->
|
||||
{@html ErrorSVG}
|
||||
<Heading size="L">
|
||||
You don't have permission to use this app
|
||||
</Heading>
|
||||
<Body size="S">
|
||||
Ask your administrator to grant you access
|
||||
</Body>
|
||||
</Layout>
|
||||
</div>
|
||||
{:else if !$screenStore.activeLayout}
|
||||
<div class="error">
|
||||
<Layout justifyItems="center" gap="S">
|
||||
<!-- eslint-disable-next-line svelte/no-at-html-tags -->
|
||||
{@html ErrorSVG}
|
||||
<Heading size="L">
|
||||
Something went wrong rendering your app
|
||||
</Heading>
|
||||
<Body size="S">
|
||||
Get in touch with support if this issue persists
|
||||
</Body>
|
||||
</Layout>
|
||||
</div>
|
||||
{:else if embedNoScreens}
|
||||
<div class="error">
|
||||
<Layout justifyItems="center" gap="S">
|
||||
<!-- eslint-disable-next-line svelte/no-at-html-tags -->
|
||||
{@html ErrorSVG}
|
||||
<Heading size="L">
|
||||
This Budibase app is not publicly accessible
|
||||
</Heading>
|
||||
</Layout>
|
||||
</div>
|
||||
{:else}
|
||||
<CustomThemeWrapper>
|
||||
{#key $screenStore.activeLayout._id}
|
||||
<Component
|
||||
isLayout
|
||||
instance={$screenStore.activeLayout.props}
|
||||
/>
|
||||
{/key}
|
||||
|
||||
<!--
|
||||
Flatpickr needs to be inside the theme wrapper.
|
||||
It also needs its own container because otherwise it hijacks
|
||||
key events on the whole page. It is painful to work with.
|
||||
-->
|
||||
<div id="flatpickr-root" />
|
||||
|
||||
<!-- Modal container to ensure they sit on top -->
|
||||
<div class="modal-container" />
|
||||
|
||||
<!-- Layers on top of app -->
|
||||
<NotificationDisplay />
|
||||
<ConfirmationDisplay />
|
||||
<PeekScreenDisplay />
|
||||
</CustomThemeWrapper>
|
||||
{/if}
|
||||
|
||||
{#if showDevTools}
|
||||
<DevTools />
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{#if !$builderStore.inBuilder && licensing.logoEnabled()}
|
||||
<FreeFooter />
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{#if !$builderStore.inBuilder && licensing.logoEnabled()}
|
||||
<FreeFooter />
|
||||
<!-- Preview and dev tools utilities -->
|
||||
{#if $appStore.isDevApp}
|
||||
<SelectionIndicator />
|
||||
{/if}
|
||||
{#if $builderStore.inBuilder || $devToolsStore.allowSelection}
|
||||
<HoverIndicator />
|
||||
{/if}
|
||||
{#if $builderStore.inBuilder}
|
||||
<DNDHandler />
|
||||
<GridDNDHandler />
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<!-- Preview and dev tools utilities -->
|
||||
{#if $appStore.isDevApp}
|
||||
<SelectionIndicator />
|
||||
{/if}
|
||||
{#if $builderStore.inBuilder || $devToolsStore.allowSelection}
|
||||
<HoverIndicator />
|
||||
{/if}
|
||||
{#if $builderStore.inBuilder}
|
||||
<DNDHandler />
|
||||
<GridDNDHandler />
|
||||
{/if}
|
||||
</div>
|
||||
</QueryParamsProvider>
|
||||
</RowSelectionProvider>
|
||||
</StateBindingsProvider>
|
||||
</UserBindingsProvider>
|
||||
</DeviceBindingsProvider>
|
||||
</div>
|
||||
<KeyboardManager />
|
||||
</QueryParamsProvider>
|
||||
</RowSelectionProvider>
|
||||
</StateBindingsProvider>
|
||||
</UserBindingsProvider>
|
||||
</DeviceBindingsProvider>
|
||||
</div>
|
||||
<KeyboardManager />
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
#spectrum-root {
|
||||
height: 0;
|
||||
visibility: hidden;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
overflow: hidden;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
|
@ -270,11 +257,6 @@
|
|||
background-color: transparent;
|
||||
}
|
||||
|
||||
#spectrum-root.show {
|
||||
height: 100%;
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
#app-root {
|
||||
overflow: hidden;
|
||||
height: 100%;
|
||||
|
|
|
@ -13,7 +13,6 @@
|
|||
|
||||
<style>
|
||||
.free-footer {
|
||||
min-height: 51px;
|
||||
flex: 0 0 auto;
|
||||
padding: 16px 20px;
|
||||
border-top: 1px solid var(--spectrum-global-color-gray-300);
|
||||
|
|
|
@ -1,244 +0,0 @@
|
|||
<script>
|
||||
export let sideNav = false
|
||||
export let hideDevTools = false
|
||||
export let hideFooter = false
|
||||
export let noAnimation = false
|
||||
</script>
|
||||
|
||||
<div class:sideNav id="clientAppSkeletonLoader" class="skeleton">
|
||||
<div class="animation" class:noAnimation />
|
||||
|
||||
{#if !hideDevTools}
|
||||
<div class="devTools" />
|
||||
{/if}
|
||||
<div class="main">
|
||||
<div class="nav" />
|
||||
<div class="body">
|
||||
<div class="bodyVerticalPadding" />
|
||||
<div class="bodyHorizontal">
|
||||
<div class="bodyHorizontalPadding" />
|
||||
<svg
|
||||
class="svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
width="240"
|
||||
height="256"
|
||||
>
|
||||
<mask id="mask">
|
||||
<rect x="0" y="0" width="240" height="256" fill="white" />
|
||||
<rect x="0" y="0" width="240" height="32" rx="6" fill="black" />
|
||||
<rect x="0" y="56" width="240" height="32" rx="6" fill="black" />
|
||||
<rect x="0" y="112" width="240" height="32" rx="6" fill="black" />
|
||||
<rect x="0" y="168" width="240" height="32" rx="6" fill="black" />
|
||||
<rect x="71" y="224" width="98" height="32" rx="6" fill="black" />
|
||||
</mask>
|
||||
|
||||
<rect
|
||||
x="0"
|
||||
y="0"
|
||||
width="240"
|
||||
height="256"
|
||||
fill="black"
|
||||
mask="url(#mask)"
|
||||
/>
|
||||
</svg>
|
||||
<div class="bodyHorizontalPadding" />
|
||||
</div>
|
||||
<div class="bodyVerticalPadding" />
|
||||
</div>
|
||||
</div>
|
||||
{#if !hideFooter}
|
||||
<div class="footer" />
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.skeleton {
|
||||
position: relative;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
background-color: var(--spectrum-global-color-gray-200);
|
||||
}
|
||||
|
||||
.animation {
|
||||
position: absolute;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
background: linear-gradient(
|
||||
to right,
|
||||
transparent 0%,
|
||||
var(--spectrum-global-color-gray-300) 20%,
|
||||
transparent 40%,
|
||||
transparent 100%
|
||||
);
|
||||
animation-duration: 1.3s;
|
||||
animation-fill-mode: forwards;
|
||||
animation-iteration-count: infinite;
|
||||
animation-name: shimmer;
|
||||
animation-timing-function: linear;
|
||||
}
|
||||
|
||||
.noAnimation {
|
||||
animation-name: none;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.devTools {
|
||||
display: flex;
|
||||
box-sizing: border-box;
|
||||
background-color: black;
|
||||
height: 60px;
|
||||
padding: 1px 24px 1px 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
z-index: 1;
|
||||
flex-shrink: 0;
|
||||
|
||||
color: white;
|
||||
mix-blend-mode: multiply;
|
||||
background: rgb(0 0 0);
|
||||
font-size: 30px;
|
||||
font-family: Source Sans Pro;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
|
||||
.main {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
@media (max-width: 720px) {
|
||||
#clientAppSkeletonLoader .main {
|
||||
flex-direction: column;
|
||||
width: initial;
|
||||
}
|
||||
}
|
||||
|
||||
@container (max-width: 720px) {
|
||||
#clientAppSkeletonLoader .main {
|
||||
flex-direction: column;
|
||||
width: initial;
|
||||
}
|
||||
}
|
||||
|
||||
.sideNav .main {
|
||||
flex-direction: row;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.nav {
|
||||
flex-shrink: 0;
|
||||
width: 100%;
|
||||
height: 141px;
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
@media (max-width: 720px) {
|
||||
#clientAppSkeletonLoader .nav {
|
||||
height: 61px;
|
||||
width: initial;
|
||||
}
|
||||
}
|
||||
|
||||
@container (max-width: 720px) {
|
||||
#clientAppSkeletonLoader .nav {
|
||||
height: 61px;
|
||||
width: initial;
|
||||
}
|
||||
}
|
||||
|
||||
.sideNav .nav {
|
||||
height: 100%;
|
||||
width: 251px;
|
||||
}
|
||||
|
||||
.body {
|
||||
z-index: 2;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
@media (max-width: 720px) {
|
||||
#clientAppSkeletonLoader .body {
|
||||
width: initial;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
@container (max-width: 720px) {
|
||||
#clientAppSkeletonLoader .body {
|
||||
width: initial;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.sideNav .body {
|
||||
width: 100%;
|
||||
height: initial;
|
||||
}
|
||||
|
||||
.body :global(svg > rect) {
|
||||
fill: var(--spectrum-alias-background-color-primary);
|
||||
}
|
||||
|
||||
.body :global(svg) {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.bodyHorizontal {
|
||||
display: flex;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.bodyHorizontalPadding {
|
||||
height: 100%;
|
||||
flex-grow: 1;
|
||||
background-color: var(--spectrum-alias-background-color-primary);
|
||||
}
|
||||
|
||||
.bodyVerticalPadding {
|
||||
width: 100%;
|
||||
flex-grow: 1;
|
||||
background-color: var(--spectrum-alias-background-color-primary);
|
||||
}
|
||||
|
||||
.footer {
|
||||
flex-shrink: 0;
|
||||
box-sizing: border-box;
|
||||
z-index: 1;
|
||||
height: 52px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
@media (max-width: 720px) {
|
||||
#clientAppSkeletonLoader .footer {
|
||||
border-top: none;
|
||||
}
|
||||
}
|
||||
|
||||
@container (max-width: 720px) {
|
||||
#clientAppSkeletonLoader .footer {
|
||||
border-top: none;
|
||||
}
|
||||
}
|
||||
|
||||
.sideNav .footer {
|
||||
border-top: 3px solid var(--spectrum-alias-background-color-primary);
|
||||
}
|
||||
|
||||
@keyframes shimmer {
|
||||
0% {
|
||||
left: -170%;
|
||||
}
|
||||
|
||||
100% {
|
||||
left: 170%;
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -5,4 +5,3 @@ export { default as UserAvatar } from "./UserAvatar.svelte"
|
|||
export { default as UserAvatars } from "./UserAvatars.svelte"
|
||||
export { default as Updating } from "./Updating.svelte"
|
||||
export { Grid } from "./grid"
|
||||
export { default as ClientAppSkeleton } from "./ClientAppSkeleton.svelte"
|
||||
|
|
|
@ -17,8 +17,5 @@
|
|||
--modal-background: var(--spectrum-global-color-gray-50);
|
||||
--drop-shadow: rgba(0, 0, 0, 0.25) !important;
|
||||
--spectrum-global-color-blue-100: rgba(35, 40, 50) !important;
|
||||
|
||||
--spectrum-alias-background-color-secondary: var(--spectrum-global-color-gray-75);
|
||||
--spectrum-alias-background-color-primary: var(--spectrum-global-color-gray-100);
|
||||
}
|
||||
|
||||
|
|
|
@ -50,7 +50,4 @@
|
|||
--modal-background: var(--spectrum-global-color-gray-50);
|
||||
--drop-shadow: rgba(0, 0, 0, 0.15) !important;
|
||||
--spectrum-global-color-blue-100: rgb(56, 65, 84) !important;
|
||||
|
||||
--spectrum-alias-background-color-secondary: var(--spectrum-global-color-gray-75);
|
||||
--spectrum-alias-background-color-primary: var(--spectrum-global-color-gray-100);
|
||||
}
|
||||
|
|
|
@ -52,7 +52,6 @@
|
|||
"@budibase/pro": "0.0.0",
|
||||
"@budibase/shared-core": "0.0.0",
|
||||
"@budibase/string-templates": "0.0.0",
|
||||
"@budibase/frontend-core": "0.0.0",
|
||||
"@budibase/types": "0.0.0",
|
||||
"@bull-board/api": "5.10.2",
|
||||
"@bull-board/koa": "5.10.2",
|
||||
|
|
|
@ -20,6 +20,7 @@ import {
|
|||
AutomationActionStepId,
|
||||
AutomationResults,
|
||||
UserCtx,
|
||||
DeleteAutomationResponse,
|
||||
} from "@budibase/types"
|
||||
import { getActionDefinitions as actionDefs } from "../../automations/actions"
|
||||
import sdk from "../../sdk"
|
||||
|
@ -72,7 +73,9 @@ function cleanAutomationInputs(automation: Automation) {
|
|||
return automation
|
||||
}
|
||||
|
||||
export async function create(ctx: UserCtx) {
|
||||
export async function create(
|
||||
ctx: UserCtx<Automation, { message: string; automation: Automation }>
|
||||
) {
|
||||
const db = context.getAppDB()
|
||||
let automation = ctx.request.body
|
||||
automation.appId = ctx.appId
|
||||
|
@ -207,7 +210,7 @@ export async function find(ctx: UserCtx) {
|
|||
ctx.body = await db.get(ctx.params.id)
|
||||
}
|
||||
|
||||
export async function destroy(ctx: UserCtx) {
|
||||
export async function destroy(ctx: UserCtx<void, DeleteAutomationResponse>) {
|
||||
const db = context.getAppDB()
|
||||
const automationId = ctx.params.id
|
||||
const oldAutomation = await db.get<Automation>(automationId)
|
||||
|
|
|
@ -15,10 +15,14 @@ import {
|
|||
FieldType,
|
||||
RelationshipFieldMetadata,
|
||||
SourceName,
|
||||
UpdateDatasourceRequest,
|
||||
UpdateDatasourceResponse,
|
||||
UserCtx,
|
||||
VerifyDatasourceRequest,
|
||||
VerifyDatasourceResponse,
|
||||
Table,
|
||||
RowValue,
|
||||
DynamicVariable,
|
||||
} from "@budibase/types"
|
||||
import sdk from "../../sdk"
|
||||
import { builderSocket } from "../../websockets"
|
||||
|
@ -90,8 +94,10 @@ async function invalidateVariables(
|
|||
existingDatasource: Datasource,
|
||||
updatedDatasource: Datasource
|
||||
) {
|
||||
const existingVariables: any = existingDatasource.config?.dynamicVariables
|
||||
const updatedVariables: any = updatedDatasource.config?.dynamicVariables
|
||||
const existingVariables: DynamicVariable[] =
|
||||
existingDatasource.config?.dynamicVariables || []
|
||||
const updatedVariables: DynamicVariable[] =
|
||||
updatedDatasource.config?.dynamicVariables || []
|
||||
const toInvalidate = []
|
||||
|
||||
if (!existingVariables) {
|
||||
|
@ -103,9 +109,9 @@ async function invalidateVariables(
|
|||
toInvalidate.push(...existingVariables)
|
||||
} else {
|
||||
// invaldate changed / removed
|
||||
existingVariables.forEach((existing: any) => {
|
||||
existingVariables.forEach(existing => {
|
||||
const unchanged = updatedVariables.find(
|
||||
(updated: any) =>
|
||||
updated =>
|
||||
existing.name === updated.name &&
|
||||
existing.queryId === updated.queryId &&
|
||||
existing.value === updated.value
|
||||
|
@ -118,24 +124,32 @@ async function invalidateVariables(
|
|||
await invalidateDynamicVariables(toInvalidate)
|
||||
}
|
||||
|
||||
export async function update(ctx: UserCtx<any, UpdateDatasourceResponse>) {
|
||||
export async function update(
|
||||
ctx: UserCtx<UpdateDatasourceRequest, UpdateDatasourceResponse>
|
||||
) {
|
||||
const db = context.getAppDB()
|
||||
const datasourceId = ctx.params.datasourceId
|
||||
const baseDatasource = await sdk.datasources.get(datasourceId)
|
||||
const auth = baseDatasource.config?.auth
|
||||
await invalidateVariables(baseDatasource, ctx.request.body)
|
||||
|
||||
const isBudibaseSource =
|
||||
baseDatasource.type === dbCore.BUDIBASE_DATASOURCE_TYPE
|
||||
|
||||
const dataSourceBody = isBudibaseSource
|
||||
? { name: ctx.request.body?.name }
|
||||
const dataSourceBody: Datasource = isBudibaseSource
|
||||
? {
|
||||
name: ctx.request.body?.name,
|
||||
type: dbCore.BUDIBASE_DATASOURCE_TYPE,
|
||||
source: SourceName.BUDIBASE,
|
||||
}
|
||||
: ctx.request.body
|
||||
|
||||
let datasource: Datasource = {
|
||||
...baseDatasource,
|
||||
...sdk.datasources.mergeConfigs(dataSourceBody, baseDatasource),
|
||||
}
|
||||
|
||||
// this block is specific to GSheets, if no auth set, set it back
|
||||
const auth = baseDatasource.config?.auth
|
||||
if (auth && !ctx.request.body.auth) {
|
||||
// don't strip auth config from DB
|
||||
datasource.config!.auth = auth
|
||||
|
@ -204,7 +218,7 @@ async function destroyInternalTablesBySourceId(datasourceId: string) {
|
|||
const db = context.getAppDB()
|
||||
|
||||
// Get all internal tables
|
||||
const internalTables = await db.allDocs(
|
||||
const internalTables = await db.allDocs<Table>(
|
||||
getTableParams(null, {
|
||||
include_docs: true,
|
||||
})
|
||||
|
@ -212,8 +226,8 @@ async function destroyInternalTablesBySourceId(datasourceId: string) {
|
|||
|
||||
// Filter by datasource and return the docs.
|
||||
const datasourceTableDocs = internalTables.rows.reduce(
|
||||
(acc: any, table: any) => {
|
||||
if (table.doc.sourceId == datasourceId) {
|
||||
(acc: Table[], table) => {
|
||||
if (table.doc?.sourceId == datasourceId) {
|
||||
acc.push(table.doc)
|
||||
}
|
||||
return acc
|
||||
|
@ -254,9 +268,9 @@ export async function destroy(ctx: UserCtx) {
|
|||
if (datasource.type === dbCore.BUDIBASE_DATASOURCE_TYPE) {
|
||||
await destroyInternalTablesBySourceId(datasourceId)
|
||||
} else {
|
||||
const queries = await db.allDocs(getQueryParams(datasourceId))
|
||||
const queries = await db.allDocs<RowValue>(getQueryParams(datasourceId))
|
||||
await db.bulkDocs(
|
||||
queries.rows.map((row: any) => ({
|
||||
queries.rows.map(row => ({
|
||||
_id: row.id,
|
||||
_rev: row.value.rev,
|
||||
_deleted: true,
|
||||
|
|
|
@ -1,7 +1,10 @@
|
|||
import { getDefinition, getDefinitions } from "../../integrations"
|
||||
import { SourceName, UserCtx } from "@budibase/types"
|
||||
|
||||
const DISABLED_EXTERNAL_INTEGRATIONS = [SourceName.AIRTABLE]
|
||||
const DISABLED_EXTERNAL_INTEGRATIONS = [
|
||||
SourceName.AIRTABLE,
|
||||
SourceName.BUDIBASE,
|
||||
]
|
||||
|
||||
export async function fetch(ctx: UserCtx) {
|
||||
const definitions = await getDefinitions()
|
||||
|
|
|
@ -1,9 +1,17 @@
|
|||
import { EMPTY_LAYOUT } from "../../constants/layouts"
|
||||
import { generateLayoutID, getScreenParams } from "../../db/utils"
|
||||
import { events, context } from "@budibase/backend-core"
|
||||
import { BBContext, Layout } from "@budibase/types"
|
||||
import {
|
||||
BBContext,
|
||||
Layout,
|
||||
SaveLayoutRequest,
|
||||
SaveLayoutResponse,
|
||||
UserCtx,
|
||||
} from "@budibase/types"
|
||||
|
||||
export async function save(ctx: BBContext) {
|
||||
export async function save(
|
||||
ctx: UserCtx<SaveLayoutRequest, SaveLayoutResponse>
|
||||
) {
|
||||
const db = context.getAppDB()
|
||||
let layout = ctx.request.body
|
||||
|
||||
|
|
|
@ -73,7 +73,7 @@ const _import = async (ctx: UserCtx) => {
|
|||
}
|
||||
export { _import as import }
|
||||
|
||||
export async function save(ctx: UserCtx) {
|
||||
export async function save(ctx: UserCtx<Query, Query>) {
|
||||
const db = context.getAppDB()
|
||||
const query: Query = ctx.request.body
|
||||
|
||||
|
|
|
@ -189,11 +189,12 @@ export async function fetchEnrichedRow(ctx: UserCtx) {
|
|||
const tableId = utils.getTableId(ctx)
|
||||
const rowId = ctx.params.rowId as string
|
||||
// need table to work out where links go in row, as well as the link docs
|
||||
const [table, row, links] = await Promise.all([
|
||||
const [table, links] = await Promise.all([
|
||||
sdk.tables.getTable(tableId),
|
||||
utils.findRow(ctx, tableId, rowId),
|
||||
linkRows.getLinkDocuments({ tableId, rowId, fieldName }),
|
||||
])
|
||||
let row = await utils.findRow(ctx, tableId, rowId)
|
||||
row = await outputProcessing(table, row)
|
||||
const linkVals = links as LinkDocumentValue[]
|
||||
|
||||
// look up the actual rows based on the ids
|
||||
|
|
|
@ -7,7 +7,13 @@ import {
|
|||
roles,
|
||||
} from "@budibase/backend-core"
|
||||
import { updateAppPackage } from "./application"
|
||||
import { Plugin, ScreenProps, BBContext, Screen } from "@budibase/types"
|
||||
import {
|
||||
Plugin,
|
||||
ScreenProps,
|
||||
BBContext,
|
||||
Screen,
|
||||
UserCtx,
|
||||
} from "@budibase/types"
|
||||
import { builderSocket } from "../../websockets"
|
||||
|
||||
export async function fetch(ctx: BBContext) {
|
||||
|
@ -31,7 +37,7 @@ export async function fetch(ctx: BBContext) {
|
|||
)
|
||||
}
|
||||
|
||||
export async function save(ctx: BBContext) {
|
||||
export async function save(ctx: UserCtx<Screen, Screen>) {
|
||||
const db = context.getAppDB()
|
||||
let screen = ctx.request.body
|
||||
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
import { InvalidFileExtensions } from "@budibase/shared-core"
|
||||
|
||||
import AppComponent from "./templates/BudibaseApp.svelte"
|
||||
|
||||
import { join } from "../../../utilities/centralPath"
|
||||
import * as uuid from "uuid"
|
||||
import { ObjectStoreBuckets } from "../../../constants"
|
||||
|
@ -22,13 +24,7 @@ import AWS from "aws-sdk"
|
|||
import fs from "fs"
|
||||
import sdk from "../../../sdk"
|
||||
import * as pro from "@budibase/pro"
|
||||
import {
|
||||
UserCtx,
|
||||
App,
|
||||
Ctx,
|
||||
ProcessAttachmentResponse,
|
||||
Feature,
|
||||
} from "@budibase/types"
|
||||
import { App, Ctx, ProcessAttachmentResponse } from "@budibase/types"
|
||||
import {
|
||||
getAppMigrationVersion,
|
||||
getLatestMigrationId,
|
||||
|
@ -36,61 +32,6 @@ import {
|
|||
|
||||
import send from "koa-send"
|
||||
|
||||
const getThemeVariables = (theme: string) => {
|
||||
if (theme === "spectrum--lightest") {
|
||||
return `
|
||||
--spectrum-global-color-gray-50: rgb(255, 255, 255);
|
||||
--spectrum-global-color-gray-200: rgb(244, 244, 244);
|
||||
--spectrum-global-color-gray-300: rgb(234, 234, 234);
|
||||
--spectrum-alias-background-color-primary: var(--spectrum-global-color-gray-50);
|
||||
`
|
||||
}
|
||||
if (theme === "spectrum--light") {
|
||||
return `
|
||||
--spectrum-global-color-gray-50: rgb(255, 255, 255);
|
||||
--spectrum-global-color-gray-200: rgb(234, 234, 234);
|
||||
--spectrum-global-color-gray-300: rgb(225, 225, 225);
|
||||
--spectrum-alias-background-color-primary: var(--spectrum-global-color-gray-50);
|
||||
|
||||
`
|
||||
}
|
||||
if (theme === "spectrum--dark") {
|
||||
return `
|
||||
--spectrum-global-color-gray-100: rgb(50, 50, 50);
|
||||
--spectrum-global-color-gray-200: rgb(62, 62, 62);
|
||||
--spectrum-global-color-gray-300: rgb(74, 74, 74);
|
||||
--spectrum-alias-background-color-primary: var(--spectrum-global-color-gray-100);
|
||||
`
|
||||
}
|
||||
if (theme === "spectrum--darkest") {
|
||||
return `
|
||||
--spectrum-global-color-gray-100: rgb(30, 30, 30);
|
||||
--spectrum-global-color-gray-200: rgb(44, 44, 44);
|
||||
--spectrum-global-color-gray-300: rgb(57, 57, 57);
|
||||
--spectrum-alias-background-color-primary: var(--spectrum-global-color-gray-100);
|
||||
`
|
||||
}
|
||||
if (theme === "spectrum--nord") {
|
||||
return `
|
||||
--spectrum-global-color-gray-100: #3b4252;
|
||||
|
||||
--spectrum-global-color-gray-200: #424a5c;
|
||||
--spectrum-global-color-gray-300: #4c566a;
|
||||
--spectrum-alias-background-color-primary: var(--spectrum-global-color-gray-100);
|
||||
`
|
||||
}
|
||||
if (theme === "spectrum--midnight") {
|
||||
return `
|
||||
--hue: 220;
|
||||
--sat: 10%;
|
||||
--spectrum-global-color-gray-100: hsl(var(--hue), var(--sat), 17%);
|
||||
--spectrum-global-color-gray-200: hsl(var(--hue), var(--sat), 20%);
|
||||
--spectrum-global-color-gray-300: hsl(var(--hue), var(--sat), 24%);
|
||||
--spectrum-alias-background-color-primary: var(--spectrum-global-color-gray-100);
|
||||
`
|
||||
}
|
||||
}
|
||||
|
||||
export const toggleBetaUiFeature = async function (ctx: Ctx) {
|
||||
const cookieName = `beta:${ctx.params.feature}`
|
||||
|
||||
|
@ -205,7 +146,7 @@ const requiresMigration = async (ctx: Ctx) => {
|
|||
return requiresMigrations
|
||||
}
|
||||
|
||||
export const serveApp = async function (ctx: UserCtx) {
|
||||
export const serveApp = async function (ctx: Ctx) {
|
||||
const needMigrations = await requiresMigration(ctx)
|
||||
|
||||
const bbHeaderEmbed =
|
||||
|
@ -226,19 +167,9 @@ export const serveApp = async function (ctx: UserCtx) {
|
|||
const appInfo = await db.get<any>(DocumentType.APP_METADATA)
|
||||
let appId = context.getAppId()
|
||||
|
||||
const hideDevTools = !!ctx.params.appUrl
|
||||
const sideNav = appInfo.navigation.navigation === "Left"
|
||||
const hideFooter =
|
||||
ctx?.user?.license?.features?.includes(Feature.BRANDING) || false
|
||||
const themeVariables = getThemeVariables(appInfo?.theme)
|
||||
|
||||
if (!env.isJest()) {
|
||||
const plugins = objectStore.enrichPluginURLs(appInfo.usedPlugins)
|
||||
|
||||
const { head, html, css } = AppComponent.render({
|
||||
hideDevTools,
|
||||
sideNav,
|
||||
hideFooter,
|
||||
metaImage:
|
||||
branding?.metaImageUrl ||
|
||||
"https://res.cloudinary.com/daog6scxm/image/upload/v1698759482/meta-images/plain-branded-meta-image-coral_ocxmgu.png",
|
||||
|
@ -263,7 +194,7 @@ export const serveApp = async function (ctx: UserCtx) {
|
|||
ctx.body = await processString(appHbs, {
|
||||
head,
|
||||
body: html,
|
||||
css: `:root{${themeVariables}} ${css.code}`,
|
||||
style: css.code,
|
||||
appId,
|
||||
embedded: bbHeaderEmbed,
|
||||
})
|
||||
|
|
|
@ -1,6 +1,4 @@
|
|||
<script>
|
||||
import ClientAppSkeleton from "@budibase/frontend-core/src/components/ClientAppSkeleton.svelte"
|
||||
|
||||
export let title = ""
|
||||
export let favicon = ""
|
||||
|
||||
|
@ -11,10 +9,6 @@
|
|||
export let clientLibPath
|
||||
export let usedPlugins
|
||||
export let appMigrating
|
||||
|
||||
export let hideDevTools
|
||||
export let sideNav
|
||||
export let hideFooter
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
|
@ -102,7 +96,6 @@
|
|||
</svelte:head>
|
||||
|
||||
<body id="app">
|
||||
<ClientAppSkeleton {hideDevTools} {sideNav} {hideFooter} />
|
||||
<div id="error">
|
||||
{#if clientLibPath}
|
||||
<h1>There was an error loading your app</h1>
|
||||
|
|
|
@ -1,12 +1,8 @@
|
|||
<html>
|
||||
<script>
|
||||
document.fonts.ready.then(() => {
|
||||
window.parent.postMessage({ type: "docLoaded" });
|
||||
})
|
||||
</script>
|
||||
|
||||
<head>
|
||||
{{{head}}}
|
||||
<style>{{{css}}}</style>
|
||||
<style>{{{style}}}</style>
|
||||
</head>
|
||||
|
||||
<script>
|
||||
|
|
|
@ -51,8 +51,8 @@ router
|
|||
controller.deleteObjects
|
||||
)
|
||||
.get("/app/preview", authorized(BUILDER), controller.serveBuilderPreview)
|
||||
.get("/app/:appUrl/:path*", controller.serveApp)
|
||||
.get("/:appId/:path*", controller.serveApp)
|
||||
.get("/app/:appUrl/:path*", controller.serveApp)
|
||||
.post(
|
||||
"/api/attachments/:datasourceId/url",
|
||||
authorized(PermissionType.TABLE, PermissionLevel.READ),
|
||||
|
|
|
@ -394,7 +394,7 @@ describe("/automations", () => {
|
|||
it("deletes a automation by its ID", async () => {
|
||||
const automation = await config.createAutomation()
|
||||
const res = await request
|
||||
.delete(`/api/automations/${automation.id}/${automation.rev}`)
|
||||
.delete(`/api/automations/${automation._id}/${automation._rev}`)
|
||||
.set(config.defaultHeaders())
|
||||
.expect("Content-Type", /json/)
|
||||
.expect(200)
|
||||
|
@ -408,7 +408,7 @@ describe("/automations", () => {
|
|||
await checkBuilderEndpoint({
|
||||
config,
|
||||
method: "DELETE",
|
||||
url: `/api/automations/${automation.id}/${automation._rev}`,
|
||||
url: `/api/automations/${automation._id}/${automation._rev}`,
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
@ -44,7 +44,7 @@ describe("/backups", () => {
|
|||
|
||||
expect(headers["content-disposition"]).toEqual(
|
||||
`attachment; filename="${
|
||||
config.getApp()!.name
|
||||
config.getApp().name
|
||||
}-export-${mocks.date.MOCK_DATE.getTime()}.tar.gz"`
|
||||
)
|
||||
})
|
||||
|
|
|
@ -86,7 +86,7 @@ describe("/datasources", () => {
|
|||
})
|
||||
// check variables in cache
|
||||
let contents = await checkCacheForDynamicVariable(
|
||||
query._id,
|
||||
query._id!,
|
||||
"variable3"
|
||||
)
|
||||
expect(contents.rows.length).toEqual(1)
|
||||
|
@ -102,7 +102,7 @@ describe("/datasources", () => {
|
|||
expect(res.body.errors).toBeUndefined()
|
||||
|
||||
// check variables no longer in cache
|
||||
contents = await checkCacheForDynamicVariable(query._id, "variable3")
|
||||
contents = await checkCacheForDynamicVariable(query._id!, "variable3")
|
||||
expect(contents).toBe(null)
|
||||
})
|
||||
})
|
||||
|
|
|
@ -467,7 +467,10 @@ describe("/queries", () => {
|
|||
queryString: "test={{ variable3 }}",
|
||||
})
|
||||
// check its in cache
|
||||
const contents = await checkCacheForDynamicVariable(base._id, "variable3")
|
||||
const contents = await checkCacheForDynamicVariable(
|
||||
base._id!,
|
||||
"variable3"
|
||||
)
|
||||
expect(contents.rows.length).toEqual(1)
|
||||
const responseBody = await preview(datasource, {
|
||||
path: "www.failonce.com",
|
||||
|
@ -490,7 +493,7 @@ describe("/queries", () => {
|
|||
queryString: "test={{ variable3 }}",
|
||||
})
|
||||
// check its in cache
|
||||
let contents = await checkCacheForDynamicVariable(base._id, "variable3")
|
||||
let contents = await checkCacheForDynamicVariable(base._id!, "variable3")
|
||||
expect(contents.rows.length).toEqual(1)
|
||||
|
||||
// delete the query
|
||||
|
@ -500,7 +503,7 @@ describe("/queries", () => {
|
|||
.expect(200)
|
||||
|
||||
// check variables no longer in cache
|
||||
contents = await checkCacheForDynamicVariable(base._id, "variable3")
|
||||
contents = await checkCacheForDynamicVariable(base._id!, "variable3")
|
||||
expect(contents).toBe(null)
|
||||
})
|
||||
})
|
||||
|
|
|
@ -110,7 +110,7 @@ describe.each([
|
|||
config.api.row.get(tbl_Id, id, { expectStatus: status })
|
||||
|
||||
const getRowUsage = async () => {
|
||||
const { total } = await config.doInContext(null, () =>
|
||||
const { total } = await config.doInContext(undefined, () =>
|
||||
quotas.getCurrentUsageValues(QuotaUsageType.STATIC, StaticQuotaName.ROWS)
|
||||
)
|
||||
return total
|
||||
|
|
|
@ -27,15 +27,17 @@ describe("/users", () => {
|
|||
|
||||
describe("fetch", () => {
|
||||
it("returns a list of users from an instance db", async () => {
|
||||
await config.createUser({ id: "uuidx" })
|
||||
await config.createUser({ id: "uuidy" })
|
||||
const id1 = `us_${utils.newid()}`
|
||||
const id2 = `us_${utils.newid()}`
|
||||
await config.createUser({ _id: id1 })
|
||||
await config.createUser({ _id: id2 })
|
||||
|
||||
const res = await config.api.user.fetch()
|
||||
expect(res.length).toBe(3)
|
||||
|
||||
const ids = res.map(u => u._id)
|
||||
expect(ids).toContain(`ro_ta_users_us_uuidx`)
|
||||
expect(ids).toContain(`ro_ta_users_us_uuidy`)
|
||||
expect(ids).toContain(`ro_ta_users_${id1}`)
|
||||
expect(ids).toContain(`ro_ta_users_${id2}`)
|
||||
})
|
||||
|
||||
it("should apply authorization to endpoint", async () => {
|
||||
|
@ -54,7 +56,7 @@ describe("/users", () => {
|
|||
describe("update", () => {
|
||||
it("should be able to update the user", async () => {
|
||||
const user: UserMetadata = await config.createUser({
|
||||
id: `us_update${utils.newid()}`,
|
||||
_id: `us_update${utils.newid()}`,
|
||||
})
|
||||
user.roleId = roles.BUILTIN_ROLE_IDS.BASIC
|
||||
delete user._rev
|
||||
|
|
|
@ -4,6 +4,7 @@ import { AppStatus } from "../../../../db/utils"
|
|||
import { roles, tenancy, context, db } from "@budibase/backend-core"
|
||||
import env from "../../../../environment"
|
||||
import Nano from "@budibase/nano"
|
||||
import TestConfiguration from "src/tests/utilities/TestConfiguration"
|
||||
|
||||
class Request {
|
||||
appId: any
|
||||
|
@ -52,10 +53,10 @@ export const clearAllApps = async (
|
|||
})
|
||||
}
|
||||
|
||||
export const clearAllAutomations = async (config: any) => {
|
||||
export const clearAllAutomations = async (config: TestConfiguration) => {
|
||||
const automations = await config.getAllAutomations()
|
||||
for (let auto of automations) {
|
||||
await context.doInAppContext(config.appId, async () => {
|
||||
await context.doInAppContext(config.getAppId(), async () => {
|
||||
await config.deleteAutomation(auto)
|
||||
})
|
||||
}
|
||||
|
@ -101,7 +102,12 @@ export const checkBuilderEndpoint = async ({
|
|||
method,
|
||||
url,
|
||||
body,
|
||||
}: any) => {
|
||||
}: {
|
||||
config: TestConfiguration
|
||||
method: string
|
||||
url: string
|
||||
body?: any
|
||||
}) => {
|
||||
const headers = await config.login({
|
||||
userId: "us_fail",
|
||||
builder: false,
|
||||
|
|
|
@ -36,7 +36,7 @@ describe("/webhooks", () => {
|
|||
const automation = await config.createAutomation()
|
||||
const res = await request
|
||||
.put(`/api/webhooks`)
|
||||
.send(basicWebhook(automation._id))
|
||||
.send(basicWebhook(automation._id!))
|
||||
.set(config.defaultHeaders())
|
||||
.expect("Content-Type", /json/)
|
||||
.expect(200)
|
||||
|
@ -145,7 +145,7 @@ describe("/webhooks", () => {
|
|||
let automation = collectAutomation()
|
||||
let newAutomation = await config.createAutomation(automation)
|
||||
let syncWebhook = await config.createWebhook(
|
||||
basicWebhook(newAutomation._id)
|
||||
basicWebhook(newAutomation._id!)
|
||||
)
|
||||
|
||||
// replicate changes before checking webhook
|
||||
|
|
|
@ -29,6 +29,6 @@ start().catch(err => {
|
|||
throw err
|
||||
})
|
||||
|
||||
export function getServer() {
|
||||
export function getServer(): Server {
|
||||
return server
|
||||
}
|
||||
|
|
|
@ -1,9 +1,11 @@
|
|||
import { Layout } from "@budibase/types"
|
||||
|
||||
export const BASE_LAYOUT_PROP_IDS = {
|
||||
PRIVATE: "layout_private_master",
|
||||
PUBLIC: "layout_public_master",
|
||||
}
|
||||
|
||||
export const EMPTY_LAYOUT = {
|
||||
export const EMPTY_LAYOUT: Layout = {
|
||||
componentLibraries: ["@budibase/standard-components"],
|
||||
title: "{{ name }}",
|
||||
favicon: "./_shared/favicon.png",
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import { roles } from "@budibase/backend-core"
|
||||
import { BASE_LAYOUT_PROP_IDS } from "./layouts"
|
||||
import { Screen } from "@budibase/types"
|
||||
|
||||
export function createHomeScreen(
|
||||
config: {
|
||||
|
@ -9,10 +10,8 @@ export function createHomeScreen(
|
|||
roleId: roles.BUILTIN_ROLE_IDS.BASIC,
|
||||
route: "/",
|
||||
}
|
||||
) {
|
||||
): Screen {
|
||||
return {
|
||||
description: "",
|
||||
url: "",
|
||||
layoutId: BASE_LAYOUT_PROP_IDS.PRIVATE,
|
||||
props: {
|
||||
_id: "d834fea2-1b3e-4320-ab34-f9009f5ecc59",
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
import {
|
||||
DEFAULT_BB_DATASOURCE_ID,
|
||||
DEFAULT_INVENTORY_TABLE_ID,
|
||||
DEFAULT_EMPLOYEE_TABLE_ID,
|
||||
DEFAULT_EXPENSES_TABLE_ID,
|
||||
DEFAULT_INVENTORY_TABLE_ID,
|
||||
DEFAULT_JOBS_TABLE_ID,
|
||||
} from "../../constants"
|
||||
import { importToRows } from "../../api/controllers/table/utils"
|
||||
|
@ -15,19 +15,21 @@ import { expensesImport } from "./expensesImport"
|
|||
import { db as dbCore } from "@budibase/backend-core"
|
||||
import {
|
||||
AutoFieldSubType,
|
||||
Datasource,
|
||||
FieldType,
|
||||
RelationshipType,
|
||||
Row,
|
||||
SourceName,
|
||||
Table,
|
||||
TableSchema,
|
||||
TableSourceType,
|
||||
} from "@budibase/types"
|
||||
|
||||
const defaultDatasource = {
|
||||
const defaultDatasource: Datasource = {
|
||||
_id: DEFAULT_BB_DATASOURCE_ID,
|
||||
type: dbCore.BUDIBASE_DATASOURCE_TYPE,
|
||||
name: "Sample Data",
|
||||
source: "BUDIBASE",
|
||||
source: SourceName.BUDIBASE,
|
||||
config: {},
|
||||
}
|
||||
|
||||
|
|
|
@ -1,13 +1,15 @@
|
|||
import newid from "./newid"
|
||||
import { db as dbCore } from "@budibase/backend-core"
|
||||
import {
|
||||
FieldType,
|
||||
DatabaseQueryOpts,
|
||||
Datasource,
|
||||
DocumentType,
|
||||
FieldSchema,
|
||||
RelationshipFieldMetadata,
|
||||
VirtualDocumentType,
|
||||
FieldType,
|
||||
INTERNAL_TABLE_SOURCE_ID,
|
||||
DatabaseQueryOpts,
|
||||
RelationshipFieldMetadata,
|
||||
SourceName,
|
||||
VirtualDocumentType,
|
||||
} from "@budibase/types"
|
||||
|
||||
export { DocumentType, VirtualDocumentType } from "@budibase/types"
|
||||
|
@ -20,11 +22,11 @@ export const enum AppStatus {
|
|||
DEPLOYED = "published",
|
||||
}
|
||||
|
||||
export const BudibaseInternalDB = {
|
||||
export const BudibaseInternalDB: Datasource = {
|
||||
_id: INTERNAL_TABLE_SOURCE_ID,
|
||||
type: dbCore.BUDIBASE_DATASOURCE_TYPE,
|
||||
name: "Budibase DB",
|
||||
source: "BUDIBASE",
|
||||
source: SourceName.BUDIBASE,
|
||||
config: {},
|
||||
}
|
||||
|
||||
|
|
|
@ -37,6 +37,7 @@ const DEFINITIONS: Record<SourceName, Integration | undefined> = {
|
|||
[SourceName.REDIS]: redis.schema,
|
||||
[SourceName.SNOWFLAKE]: snowflake.schema,
|
||||
[SourceName.ORACLE]: undefined,
|
||||
[SourceName.BUDIBASE]: undefined,
|
||||
}
|
||||
|
||||
const INTEGRATIONS: Record<SourceName, any> = {
|
||||
|
@ -56,6 +57,7 @@ const INTEGRATIONS: Record<SourceName, any> = {
|
|||
[SourceName.REDIS]: redis.integration,
|
||||
[SourceName.SNOWFLAKE]: snowflake.integration,
|
||||
[SourceName.ORACLE]: undefined,
|
||||
[SourceName.BUDIBASE]: undefined,
|
||||
}
|
||||
|
||||
// optionally add oracle integration if the oracle binary can be installed
|
||||
|
|
|
@ -13,7 +13,7 @@ describe("syncApps", () => {
|
|||
afterAll(config.end)
|
||||
|
||||
it("runs successfully", async () => {
|
||||
return config.doInContext(null, async () => {
|
||||
return config.doInContext(undefined, async () => {
|
||||
// create the usage quota doc and mock usages
|
||||
await quotas.getQuotaUsage()
|
||||
await quotas.setUsage(3, StaticQuotaName.APPS, QuotaUsageType.STATIC)
|
||||
|
|
|
@ -12,8 +12,8 @@ describe("syncCreators", () => {
|
|||
afterAll(config.end)
|
||||
|
||||
it("syncs creators", async () => {
|
||||
return config.doInContext(null, async () => {
|
||||
await config.createUser({ admin: true })
|
||||
return config.doInContext(undefined, async () => {
|
||||
await config.createUser({ admin: { global: true } })
|
||||
|
||||
await syncCreators.run()
|
||||
|
||||
|
|
|
@ -14,7 +14,7 @@ describe("syncRows", () => {
|
|||
afterAll(config.end)
|
||||
|
||||
it("runs successfully", async () => {
|
||||
return config.doInContext(null, async () => {
|
||||
return config.doInContext(undefined, async () => {
|
||||
// create the usage quota doc and mock usages
|
||||
await quotas.getQuotaUsage()
|
||||
await quotas.setUsage(300, StaticQuotaName.ROWS, QuotaUsageType.STATIC)
|
||||
|
|
|
@ -12,7 +12,7 @@ describe("syncUsers", () => {
|
|||
afterAll(config.end)
|
||||
|
||||
it("syncs users", async () => {
|
||||
return config.doInContext(null, async () => {
|
||||
return config.doInContext(undefined, async () => {
|
||||
await config.createUser()
|
||||
|
||||
await syncUsers.run()
|
||||
|
|
|
@ -40,7 +40,7 @@ describe("migrations", () => {
|
|||
|
||||
describe("backfill", () => {
|
||||
it("runs app db migration", async () => {
|
||||
await config.doInContext(null, async () => {
|
||||
await config.doInContext(undefined, async () => {
|
||||
await clearMigrations()
|
||||
await config.createAutomation()
|
||||
await config.createAutomation(structures.newAutomation())
|
||||
|
@ -93,18 +93,18 @@ describe("migrations", () => {
|
|||
})
|
||||
|
||||
it("runs global db migration", async () => {
|
||||
await config.doInContext(null, async () => {
|
||||
await config.doInContext(undefined, async () => {
|
||||
await clearMigrations()
|
||||
const appId = config.prodAppId
|
||||
const appId = config.getProdAppId()
|
||||
const roles = { [appId]: "role_12345" }
|
||||
await config.createUser({
|
||||
builder: false,
|
||||
admin: true,
|
||||
builder: { global: false },
|
||||
admin: { global: true },
|
||||
roles,
|
||||
}) // admin only
|
||||
await config.createUser({
|
||||
builder: false,
|
||||
admin: false,
|
||||
builder: { global: false },
|
||||
admin: { global: false },
|
||||
roles,
|
||||
}) // non admin non builder
|
||||
await config.createTable()
|
||||
|
|
|
@ -85,7 +85,9 @@ async function getImportableDocuments(db: Database) {
|
|||
const docPromises = []
|
||||
for (let docType of DocumentTypesToImport) {
|
||||
docPromises.push(
|
||||
db.allDocs(dbCore.getDocParams(docType, null, { include_docs: true }))
|
||||
db.allDocs<Document>(
|
||||
dbCore.getDocParams(docType, null, { include_docs: true })
|
||||
)
|
||||
)
|
||||
}
|
||||
// map the responses to the document itself
|
||||
|
|
|
@ -43,8 +43,8 @@ async function createUser(email: string, roles: UserRoles, builder?: boolean) {
|
|||
const user = await config.createUser({
|
||||
email,
|
||||
roles,
|
||||
builder: builder || false,
|
||||
admin: false,
|
||||
builder: { global: builder || false },
|
||||
admin: { global: false },
|
||||
})
|
||||
await context.doInContext(config.appId!, async () => {
|
||||
await events.user.created(user)
|
||||
|
@ -55,10 +55,10 @@ async function createUser(email: string, roles: UserRoles, builder?: boolean) {
|
|||
async function removeUserRole(user: User) {
|
||||
const final = await config.globalUser({
|
||||
...user,
|
||||
id: user._id,
|
||||
_id: user._id,
|
||||
roles: {},
|
||||
builder: false,
|
||||
admin: false,
|
||||
builder: { global: false },
|
||||
admin: { global: false },
|
||||
})
|
||||
await context.doInContext(config.appId!, async () => {
|
||||
await events.user.updated(final)
|
||||
|
@ -69,8 +69,8 @@ async function createGroupAndUser(email: string) {
|
|||
groupUser = await config.createUser({
|
||||
email,
|
||||
roles: {},
|
||||
builder: false,
|
||||
admin: false,
|
||||
builder: { global: false },
|
||||
admin: { global: false },
|
||||
})
|
||||
group = await config.createGroup()
|
||||
await config.addUserToGroup(group._id!, groupUser._id!)
|
||||
|
|
|
@ -229,7 +229,7 @@ export async function removeSecretSingle(datasource: Datasource) {
|
|||
}
|
||||
|
||||
export function mergeConfigs(update: Datasource, old: Datasource) {
|
||||
if (!update.config) {
|
||||
if (!update.config || !old.config) {
|
||||
return update
|
||||
}
|
||||
// specific to REST datasources, fix the auth configs again if required
|
||||
|
|
|
@ -81,7 +81,7 @@ describe("sdk >> rows >> internal", () => {
|
|||
const response = await internalSdk.save(
|
||||
table._id!,
|
||||
row,
|
||||
config.user._id
|
||||
config.getUser()._id
|
||||
)
|
||||
|
||||
expect(response).toEqual({
|
||||
|
@ -129,7 +129,7 @@ describe("sdk >> rows >> internal", () => {
|
|||
const response = await internalSdk.save(
|
||||
table._id!,
|
||||
row,
|
||||
config.user._id
|
||||
config.getUser()._id
|
||||
)
|
||||
|
||||
expect(response).toEqual({
|
||||
|
@ -190,15 +190,15 @@ describe("sdk >> rows >> internal", () => {
|
|||
|
||||
await config.doInContext(config.appId, async () => {
|
||||
for (const row of makeRows(5)) {
|
||||
await internalSdk.save(table._id!, row, config.user._id)
|
||||
await internalSdk.save(table._id!, row, config.getUser()._id)
|
||||
}
|
||||
await Promise.all(
|
||||
makeRows(10).map(row =>
|
||||
internalSdk.save(table._id!, row, config.user._id)
|
||||
internalSdk.save(table._id!, row, config.getUser()._id)
|
||||
)
|
||||
)
|
||||
for (const row of makeRows(5)) {
|
||||
await internalSdk.save(table._id!, row, config.user._id)
|
||||
await internalSdk.save(table._id!, row, config.getUser()._id)
|
||||
}
|
||||
})
|
||||
|
||||
|
|
|
@ -22,15 +22,18 @@ describe("syncGlobalUsers", () => {
|
|||
expect(metadata).toHaveLength(1)
|
||||
expect(metadata).toEqual([
|
||||
expect.objectContaining({
|
||||
_id: db.generateUserMetadataID(config.user._id),
|
||||
_id: db.generateUserMetadataID(config.getUser()._id!),
|
||||
}),
|
||||
])
|
||||
})
|
||||
})
|
||||
|
||||
it("admin and builders users are synced", async () => {
|
||||
const user1 = await config.createUser({ admin: true })
|
||||
const user2 = await config.createUser({ admin: false, builder: true })
|
||||
const user1 = await config.createUser({ admin: { global: true } })
|
||||
const user2 = await config.createUser({
|
||||
admin: { global: false },
|
||||
builder: { global: true },
|
||||
})
|
||||
await config.doInContext(config.appId, async () => {
|
||||
expect(await rawUserMetadata()).toHaveLength(1)
|
||||
await syncGlobalUsers()
|
||||
|
@ -51,7 +54,10 @@ describe("syncGlobalUsers", () => {
|
|||
})
|
||||
|
||||
it("app users are not synced if not specified", async () => {
|
||||
const user = await config.createUser({ admin: false, builder: false })
|
||||
const user = await config.createUser({
|
||||
admin: { global: false },
|
||||
builder: { global: false },
|
||||
})
|
||||
await config.doInContext(config.appId, async () => {
|
||||
await syncGlobalUsers()
|
||||
|
||||
|
@ -68,8 +74,14 @@ describe("syncGlobalUsers", () => {
|
|||
it("app users are added when group is assigned to app", async () => {
|
||||
await config.doInTenant(async () => {
|
||||
const group = await proSdk.groups.save(structures.userGroups.userGroup())
|
||||
const user1 = await config.createUser({ admin: false, builder: false })
|
||||
const user2 = await config.createUser({ admin: false, builder: false })
|
||||
const user1 = await config.createUser({
|
||||
admin: { global: false },
|
||||
builder: { global: false },
|
||||
})
|
||||
const user2 = await config.createUser({
|
||||
admin: { global: false },
|
||||
builder: { global: false },
|
||||
})
|
||||
await proSdk.groups.addUsers(group.id, [user1._id!, user2._id!])
|
||||
|
||||
await config.doInContext(config.appId, async () => {
|
||||
|
@ -103,8 +115,14 @@ describe("syncGlobalUsers", () => {
|
|||
it("app users are removed when app is removed from user group", async () => {
|
||||
await config.doInTenant(async () => {
|
||||
const group = await proSdk.groups.save(structures.userGroups.userGroup())
|
||||
const user1 = await config.createUser({ admin: false, builder: false })
|
||||
const user2 = await config.createUser({ admin: false, builder: false })
|
||||
const user1 = await config.createUser({
|
||||
admin: { global: false },
|
||||
builder: { global: false },
|
||||
})
|
||||
const user2 = await config.createUser({
|
||||
admin: { global: false },
|
||||
builder: { global: false },
|
||||
})
|
||||
await proSdk.groups.updateGroupApps(group.id, {
|
||||
appsToAdd: [
|
||||
{ appId: config.prodAppId!, roleId: roles.BUILTIN_ROLE_IDS.BASIC },
|
||||
|
|
|
@ -38,6 +38,7 @@ async function initRoutes(app: Koa) {
|
|||
|
||||
// api routes
|
||||
app.use(api.router.routes())
|
||||
app.use(api.router.allowedMethods())
|
||||
}
|
||||
|
||||
async function initPro() {
|
||||
|
|
|
@ -49,25 +49,31 @@ import {
|
|||
AuthToken,
|
||||
Automation,
|
||||
CreateViewRequest,
|
||||
Ctx,
|
||||
Datasource,
|
||||
FieldType,
|
||||
INTERNAL_TABLE_SOURCE_ID,
|
||||
Layout,
|
||||
Query,
|
||||
RelationshipFieldMetadata,
|
||||
RelationshipType,
|
||||
Row,
|
||||
Screen,
|
||||
SearchParams,
|
||||
SourceName,
|
||||
Table,
|
||||
TableSourceType,
|
||||
User,
|
||||
UserRoles,
|
||||
UserCtx,
|
||||
View,
|
||||
Webhook,
|
||||
WithRequired,
|
||||
} from "@budibase/types"
|
||||
|
||||
import API from "./api"
|
||||
import { cloneDeep } from "lodash"
|
||||
import jwt, { Secret } from "jsonwebtoken"
|
||||
import { Server } from "http"
|
||||
|
||||
mocks.licenses.init(pro)
|
||||
|
||||
|
@ -82,27 +88,23 @@ export interface TableToBuild extends Omit<Table, "sourceId" | "sourceType"> {
|
|||
}
|
||||
|
||||
export default class TestConfiguration {
|
||||
server: any
|
||||
request: supertest.SuperTest<supertest.Test> | undefined
|
||||
server?: Server
|
||||
request?: supertest.SuperTest<supertest.Test>
|
||||
started: boolean
|
||||
appId: string | null
|
||||
allApps: any[]
|
||||
appId?: string
|
||||
allApps: App[]
|
||||
app?: App
|
||||
prodApp: any
|
||||
prodAppId: any
|
||||
user: any
|
||||
userMetadataId: any
|
||||
prodApp?: App
|
||||
prodAppId?: string
|
||||
user?: User
|
||||
userMetadataId?: string
|
||||
table?: Table
|
||||
automation: any
|
||||
automation?: Automation
|
||||
datasource?: Datasource
|
||||
tenantId?: string
|
||||
api: API
|
||||
csrfToken?: string
|
||||
|
||||
private get globalUserId() {
|
||||
return this.user._id
|
||||
}
|
||||
|
||||
constructor(openServer = true) {
|
||||
if (openServer) {
|
||||
// use a random port because it doesn't matter
|
||||
|
@ -114,7 +116,7 @@ export default class TestConfiguration {
|
|||
} else {
|
||||
this.started = false
|
||||
}
|
||||
this.appId = null
|
||||
this.appId = undefined
|
||||
this.allApps = []
|
||||
|
||||
this.api = new API(this)
|
||||
|
@ -125,46 +127,86 @@ export default class TestConfiguration {
|
|||
}
|
||||
|
||||
getApp() {
|
||||
if (!this.app) {
|
||||
throw new Error("app has not been initialised, call config.init() first")
|
||||
}
|
||||
return this.app
|
||||
}
|
||||
|
||||
getProdApp() {
|
||||
if (!this.prodApp) {
|
||||
throw new Error(
|
||||
"prodApp has not been initialised, call config.init() first"
|
||||
)
|
||||
}
|
||||
return this.prodApp
|
||||
}
|
||||
|
||||
getAppId() {
|
||||
if (!this.appId) {
|
||||
throw "appId has not been initialised properly"
|
||||
throw new Error(
|
||||
"appId has not been initialised, call config.init() first"
|
||||
)
|
||||
}
|
||||
|
||||
return this.appId
|
||||
}
|
||||
|
||||
getProdAppId() {
|
||||
if (!this.prodAppId) {
|
||||
throw new Error(
|
||||
"prodAppId has not been initialised, call config.init() first"
|
||||
)
|
||||
}
|
||||
return this.prodAppId
|
||||
}
|
||||
|
||||
getUser(): User {
|
||||
if (!this.user) {
|
||||
throw new Error("User has not been initialised, call config.init() first")
|
||||
}
|
||||
return this.user
|
||||
}
|
||||
|
||||
getUserDetails() {
|
||||
const user = this.getUser()
|
||||
return {
|
||||
globalId: this.globalUserId,
|
||||
email: this.user.email,
|
||||
firstName: this.user.firstName,
|
||||
lastName: this.user.lastName,
|
||||
globalId: user._id!,
|
||||
email: user.email,
|
||||
firstName: user.firstName,
|
||||
lastName: user.lastName,
|
||||
}
|
||||
}
|
||||
|
||||
getAutomation() {
|
||||
if (!this.automation) {
|
||||
throw new Error(
|
||||
"automation has not been initialised, call config.init() first"
|
||||
)
|
||||
}
|
||||
return this.automation
|
||||
}
|
||||
|
||||
getDatasource() {
|
||||
if (!this.datasource) {
|
||||
throw new Error(
|
||||
"datasource has not been initialised, call config.init() first"
|
||||
)
|
||||
}
|
||||
return this.datasource
|
||||
}
|
||||
|
||||
async doInContext<T>(
|
||||
appId: string | null,
|
||||
appId: string | undefined,
|
||||
task: () => Promise<T>
|
||||
): Promise<T> {
|
||||
if (!appId) {
|
||||
appId = this.appId
|
||||
}
|
||||
|
||||
const tenant = this.getTenantId()
|
||||
return tenancy.doInTenant(tenant, () => {
|
||||
if (!appId) {
|
||||
appId = this.appId
|
||||
}
|
||||
|
||||
// check if already in a context
|
||||
if (context.getAppId() == null && appId !== null) {
|
||||
if (context.getAppId() == null && appId) {
|
||||
return context.doInAppContext(appId, async () => {
|
||||
return task()
|
||||
})
|
||||
|
@ -259,7 +301,11 @@ export default class TestConfiguration {
|
|||
|
||||
// UTILS
|
||||
|
||||
_req(body: any, params: any, controlFunc: any) {
|
||||
_req<Req extends Record<string, any> | void, Res>(
|
||||
handler: (ctx: UserCtx<Req, Res>) => Promise<void>,
|
||||
body?: Req,
|
||||
params?: Record<string, string | undefined>
|
||||
): Promise<Res> {
|
||||
// create a fake request ctx
|
||||
const request: any = {}
|
||||
const appId = this.appId
|
||||
|
@ -278,63 +324,48 @@ export default class TestConfiguration {
|
|||
throw new Error(`Error ${status} - ${message}`)
|
||||
}
|
||||
return this.doInContext(appId, async () => {
|
||||
await controlFunc(request)
|
||||
await handler(request)
|
||||
return request.body
|
||||
})
|
||||
}
|
||||
|
||||
// USER / AUTH
|
||||
async globalUser(
|
||||
config: {
|
||||
id?: string
|
||||
firstName?: string
|
||||
lastName?: string
|
||||
builder?: boolean
|
||||
admin?: boolean
|
||||
email?: string
|
||||
roles?: any
|
||||
} = {}
|
||||
): Promise<User> {
|
||||
async globalUser(config: Partial<User> = {}): Promise<User> {
|
||||
const {
|
||||
id = `us_${newid()}`,
|
||||
_id = `us_${newid()}`,
|
||||
firstName = generator.first(),
|
||||
lastName = generator.last(),
|
||||
builder = true,
|
||||
admin = false,
|
||||
builder = { global: true },
|
||||
admin = { global: false },
|
||||
email = generator.email(),
|
||||
roles,
|
||||
tenantId = this.getTenantId(),
|
||||
roles = {},
|
||||
} = config
|
||||
|
||||
const db = tenancy.getTenantDB(this.getTenantId())
|
||||
let existing
|
||||
let existing: Partial<User> = {}
|
||||
try {
|
||||
existing = await db.get<any>(id)
|
||||
existing = await db.get<User>(_id)
|
||||
} catch (err) {
|
||||
existing = { email }
|
||||
// ignore
|
||||
}
|
||||
const user: User = {
|
||||
_id: id,
|
||||
_id,
|
||||
...existing,
|
||||
roles: roles || {},
|
||||
tenantId: this.getTenantId(),
|
||||
...config,
|
||||
email,
|
||||
roles,
|
||||
tenantId,
|
||||
firstName,
|
||||
lastName,
|
||||
builder,
|
||||
admin,
|
||||
}
|
||||
await sessions.createASession(id, {
|
||||
await sessions.createASession(_id, {
|
||||
sessionId: "sessionid",
|
||||
tenantId: this.getTenantId(),
|
||||
csrfToken: this.csrfToken,
|
||||
})
|
||||
if (builder) {
|
||||
user.builder = { global: true }
|
||||
} else {
|
||||
user.builder = { global: false }
|
||||
}
|
||||
if (admin) {
|
||||
user.admin = { global: true }
|
||||
} else {
|
||||
user.admin = { global: false }
|
||||
}
|
||||
const resp = await db.put(user)
|
||||
return {
|
||||
_rev: resp.rev,
|
||||
|
@ -342,38 +373,9 @@ export default class TestConfiguration {
|
|||
}
|
||||
}
|
||||
|
||||
async createUser(
|
||||
user: {
|
||||
id?: string
|
||||
firstName?: string
|
||||
lastName?: string
|
||||
email?: string
|
||||
builder?: boolean
|
||||
admin?: boolean
|
||||
roles?: UserRoles
|
||||
} = {}
|
||||
): Promise<User> {
|
||||
const {
|
||||
id,
|
||||
firstName = generator.first(),
|
||||
lastName = generator.last(),
|
||||
email = generator.email(),
|
||||
builder = true,
|
||||
admin,
|
||||
roles,
|
||||
} = user
|
||||
|
||||
const globalId = !id ? `us_${Math.random()}` : `us_${id}`
|
||||
const resp = await this.globalUser({
|
||||
id: globalId,
|
||||
firstName,
|
||||
lastName,
|
||||
email,
|
||||
builder,
|
||||
admin,
|
||||
roles: roles || {},
|
||||
})
|
||||
await cache.user.invalidateUser(globalId)
|
||||
async createUser(user: Partial<User> = {}): Promise<User> {
|
||||
const resp = await this.globalUser(user)
|
||||
await cache.user.invalidateUser(resp._id!)
|
||||
return resp
|
||||
}
|
||||
|
||||
|
@ -381,7 +383,7 @@ export default class TestConfiguration {
|
|||
return context.doInTenant(this.tenantId!, async () => {
|
||||
const baseGroup = structures.userGroups.userGroup()
|
||||
baseGroup.roles = {
|
||||
[this.prodAppId]: roleId,
|
||||
[this.getProdAppId()]: roleId,
|
||||
}
|
||||
const { id, rev } = await pro.sdk.groups.save(baseGroup)
|
||||
return {
|
||||
|
@ -404,8 +406,18 @@ export default class TestConfiguration {
|
|||
})
|
||||
}
|
||||
|
||||
async login({ roleId, userId, builder, prodApp = false }: any = {}) {
|
||||
const appId = prodApp ? this.prodAppId : this.appId
|
||||
async login({
|
||||
roleId,
|
||||
userId,
|
||||
builder,
|
||||
prodApp,
|
||||
}: {
|
||||
roleId?: string
|
||||
userId: string
|
||||
builder: boolean
|
||||
prodApp: boolean
|
||||
}) {
|
||||
const appId = prodApp ? this.getProdAppId() : this.getAppId()
|
||||
return context.doInAppContext(appId, async () => {
|
||||
userId = !userId ? `us_uuid1` : userId
|
||||
if (!this.request) {
|
||||
|
@ -414,9 +426,9 @@ export default class TestConfiguration {
|
|||
// make sure the user exists in the global DB
|
||||
if (roleId !== roles.BUILTIN_ROLE_IDS.PUBLIC) {
|
||||
await this.globalUser({
|
||||
id: userId,
|
||||
builder,
|
||||
roles: { [this.prodAppId]: roleId },
|
||||
_id: userId,
|
||||
builder: { global: builder },
|
||||
roles: { [appId]: roleId || roles.BUILTIN_ROLE_IDS.BASIC },
|
||||
})
|
||||
}
|
||||
await sessions.createASession(userId, {
|
||||
|
@ -445,8 +457,9 @@ export default class TestConfiguration {
|
|||
|
||||
defaultHeaders(extras = {}, prodApp = false) {
|
||||
const tenantId = this.getTenantId()
|
||||
const user = this.getUser()
|
||||
const authObj: AuthToken = {
|
||||
userId: this.globalUserId,
|
||||
userId: user._id!,
|
||||
sessionId: "sessionid",
|
||||
tenantId,
|
||||
}
|
||||
|
@ -498,7 +511,7 @@ export default class TestConfiguration {
|
|||
builder = false,
|
||||
prodApp = true,
|
||||
} = {}) {
|
||||
return this.login({ email, roleId, builder, prodApp })
|
||||
return this.login({ userId: email, roleId, builder, prodApp })
|
||||
}
|
||||
|
||||
// TENANCY
|
||||
|
@ -521,18 +534,22 @@ export default class TestConfiguration {
|
|||
|
||||
this.tenantId = structures.tenant.id()
|
||||
this.user = await this.globalUser()
|
||||
this.userMetadataId = generateUserMetadataID(this.user._id)
|
||||
this.userMetadataId = generateUserMetadataID(this.user._id!)
|
||||
|
||||
return this.createApp(appName)
|
||||
}
|
||||
|
||||
doInTenant(task: any) {
|
||||
doInTenant<T>(task: () => T) {
|
||||
return context.doInTenant(this.getTenantId(), task)
|
||||
}
|
||||
|
||||
// API
|
||||
|
||||
async generateApiKey(userId = this.user._id) {
|
||||
async generateApiKey(userId?: string) {
|
||||
const user = this.getUser()
|
||||
if (!userId) {
|
||||
userId = user._id!
|
||||
}
|
||||
const db = tenancy.getTenantDB(this.getTenantId())
|
||||
const id = dbCore.generateDevInfoID(userId)
|
||||
let devInfo: any
|
||||
|
@ -552,25 +569,28 @@ export default class TestConfiguration {
|
|||
async createApp(appName: string): Promise<App> {
|
||||
// create dev app
|
||||
// clear any old app
|
||||
this.appId = null
|
||||
this.app = await context.doInTenant(this.tenantId!, async () => {
|
||||
const app = await this._req({ name: appName }, null, appController.create)
|
||||
this.appId = app.appId!
|
||||
return app
|
||||
})
|
||||
return await context.doInAppContext(this.getAppId(), async () => {
|
||||
this.appId = undefined
|
||||
this.app = await context.doInTenant(
|
||||
this.tenantId!,
|
||||
async () =>
|
||||
(await this._req(appController.create, {
|
||||
name: appName,
|
||||
})) as App
|
||||
)
|
||||
this.appId = this.app.appId
|
||||
return await context.doInAppContext(this.app.appId!, async () => {
|
||||
// create production app
|
||||
this.prodApp = await this.publish()
|
||||
|
||||
this.allApps.push(this.prodApp)
|
||||
this.allApps.push(this.app)
|
||||
this.allApps.push(this.app!)
|
||||
|
||||
return this.app!
|
||||
})
|
||||
}
|
||||
|
||||
async publish() {
|
||||
await this._req(null, null, deployController.publishApp)
|
||||
await this._req(deployController.publishApp)
|
||||
// @ts-ignore
|
||||
const prodAppId = this.getAppId().replace("_dev", "")
|
||||
this.prodAppId = prodAppId
|
||||
|
@ -582,13 +602,11 @@ export default class TestConfiguration {
|
|||
}
|
||||
|
||||
async unpublish() {
|
||||
const response = await this._req(
|
||||
null,
|
||||
{ appId: this.appId },
|
||||
appController.unpublish
|
||||
)
|
||||
this.prodAppId = null
|
||||
this.prodApp = null
|
||||
const response = await this._req(appController.unpublish, {
|
||||
appId: this.appId,
|
||||
})
|
||||
this.prodAppId = undefined
|
||||
this.prodApp = undefined
|
||||
return response
|
||||
}
|
||||
|
||||
|
@ -716,8 +734,7 @@ export default class TestConfiguration {
|
|||
// ROLE
|
||||
|
||||
async createRole(config?: any) {
|
||||
config = config || basicRole()
|
||||
return this._req(config, null, roleController.save)
|
||||
return this._req(roleController.save, config || basicRole())
|
||||
}
|
||||
|
||||
// VIEW
|
||||
|
@ -730,7 +747,7 @@ export default class TestConfiguration {
|
|||
tableId: this.table!._id,
|
||||
name: generator.guid(),
|
||||
}
|
||||
return this._req(view, null, viewController.v1.save)
|
||||
return this._req(viewController.v1.save, view)
|
||||
}
|
||||
|
||||
async createView(
|
||||
|
@ -754,40 +771,38 @@ export default class TestConfiguration {
|
|||
|
||||
// AUTOMATION
|
||||
|
||||
async createAutomation(config?: any) {
|
||||
async createAutomation(config?: Automation) {
|
||||
config = config || basicAutomation()
|
||||
if (config._rev) {
|
||||
delete config._rev
|
||||
}
|
||||
this.automation = (
|
||||
await this._req(config, null, automationController.create)
|
||||
).automation
|
||||
const res = await this._req(automationController.create, config)
|
||||
this.automation = res.automation
|
||||
return this.automation
|
||||
}
|
||||
|
||||
async getAllAutomations() {
|
||||
return this._req(null, null, automationController.fetch)
|
||||
return this._req(automationController.fetch)
|
||||
}
|
||||
|
||||
async deleteAutomation(automation?: any) {
|
||||
async deleteAutomation(automation?: Automation) {
|
||||
automation = automation || this.automation
|
||||
if (!automation) {
|
||||
return
|
||||
}
|
||||
return this._req(
|
||||
null,
|
||||
{ id: automation._id, rev: automation._rev },
|
||||
automationController.destroy
|
||||
)
|
||||
return this._req(automationController.destroy, undefined, {
|
||||
id: automation._id,
|
||||
rev: automation._rev,
|
||||
})
|
||||
}
|
||||
|
||||
async createWebhook(config?: any) {
|
||||
async createWebhook(config?: Webhook) {
|
||||
if (!this.automation) {
|
||||
throw "Must create an automation before creating webhook."
|
||||
}
|
||||
config = config || basicWebhook(this.automation._id)
|
||||
config = config || basicWebhook(this.automation._id!)
|
||||
|
||||
return (await this._req(config, null, webhookController.save)).webhook
|
||||
return (await this._req(webhookController.save, config)).webhook
|
||||
}
|
||||
|
||||
// DATASOURCE
|
||||
|
@ -809,7 +824,7 @@ export default class TestConfiguration {
|
|||
return { ...this.datasource, _id: this.datasource!._id! }
|
||||
}
|
||||
|
||||
async restDatasource(cfg?: any) {
|
||||
async restDatasource(cfg?: Record<string, any>) {
|
||||
return this.createDatasource({
|
||||
datasource: {
|
||||
...basicDatasource().datasource,
|
||||
|
@ -866,26 +881,25 @@ export default class TestConfiguration {
|
|||
|
||||
// QUERY
|
||||
|
||||
async createQuery(config?: any) {
|
||||
if (!this.datasource && !config) {
|
||||
throw "No datasource created for query."
|
||||
}
|
||||
config = config || basicQuery(this.datasource!._id!)
|
||||
return this._req(config, null, queryController.save)
|
||||
async createQuery(config?: Query) {
|
||||
return this._req(
|
||||
queryController.save,
|
||||
config || basicQuery(this.getDatasource()._id!)
|
||||
)
|
||||
}
|
||||
|
||||
// SCREEN
|
||||
|
||||
async createScreen(config?: any) {
|
||||
async createScreen(config?: Screen) {
|
||||
config = config || basicScreen()
|
||||
return this._req(config, null, screenController.save)
|
||||
return this._req(screenController.save, config)
|
||||
}
|
||||
|
||||
// LAYOUT
|
||||
|
||||
async createLayout(config?: any) {
|
||||
async createLayout(config?: Layout) {
|
||||
config = config || basicLayout()
|
||||
return await this._req(config, null, layoutController.save)
|
||||
return await this._req(layoutController.save, config)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -22,6 +22,8 @@ import {
|
|||
INTERNAL_TABLE_SOURCE_ID,
|
||||
TableSourceType,
|
||||
Query,
|
||||
Webhook,
|
||||
WebhookActionType,
|
||||
} from "@budibase/types"
|
||||
import { LoopInput, LoopStepType } from "../../definitions/automations"
|
||||
|
||||
|
@ -407,12 +409,12 @@ export function basicLayout() {
|
|||
return cloneDeep(EMPTY_LAYOUT)
|
||||
}
|
||||
|
||||
export function basicWebhook(automationId: string) {
|
||||
export function basicWebhook(automationId: string): Webhook {
|
||||
return {
|
||||
live: true,
|
||||
name: "webhook",
|
||||
action: {
|
||||
type: "automation",
|
||||
type: WebhookActionType.AUTOMATION,
|
||||
target: automationId,
|
||||
},
|
||||
}
|
||||
|
|
|
@ -32,9 +32,7 @@ export interface FetchDatasourceInfoResponse {
|
|||
tableNames: string[]
|
||||
}
|
||||
|
||||
export interface UpdateDatasourceRequest extends Datasource {
|
||||
datasource: Datasource
|
||||
}
|
||||
export interface UpdateDatasourceRequest extends Datasource {}
|
||||
|
||||
export interface BuildSchemaFromSourceRequest {
|
||||
tablesFilter?: string[]
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
import { DocumentDestroyResponse } from "@budibase/nano"
|
||||
|
||||
export interface DeleteAutomationResponse extends DocumentDestroyResponse {}
|
|
@ -11,3 +11,5 @@ export * from "./global"
|
|||
export * from "./pagination"
|
||||
export * from "./searchFilter"
|
||||
export * from "./cookies"
|
||||
export * from "./automation"
|
||||
export * from "./layout"
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
import { Layout } from "../../documents"
|
||||
|
||||
export interface SaveLayoutRequest extends Layout {}
|
||||
|
||||
export interface SaveLayoutResponse extends Layout {}
|
|
@ -6,6 +6,9 @@ export interface Datasource extends Document {
|
|||
type: string
|
||||
name?: string
|
||||
source: SourceName
|
||||
// this is a googlesheets specific property which
|
||||
// can be found in the GSheets schema - pertains to SSO creds
|
||||
auth?: { type: string }
|
||||
// the config is defined by the schema
|
||||
config?: Record<string, any>
|
||||
plus?: boolean
|
||||
|
@ -36,6 +39,12 @@ export interface RestAuthConfig {
|
|||
config: RestBasicAuthConfig | RestBearerAuthConfig
|
||||
}
|
||||
|
||||
export interface DynamicVariable {
|
||||
name: string
|
||||
queryId: string
|
||||
value: string
|
||||
}
|
||||
|
||||
export interface RestConfig {
|
||||
url: string
|
||||
rejectUnauthorized: boolean
|
||||
|
@ -47,11 +56,5 @@ export interface RestConfig {
|
|||
staticVariables: {
|
||||
[key: string]: string
|
||||
}
|
||||
dynamicVariables: [
|
||||
{
|
||||
name: string
|
||||
queryId: string
|
||||
value: string
|
||||
}
|
||||
]
|
||||
dynamicVariables: DynamicVariable[]
|
||||
}
|
||||
|
|
|
@ -1,6 +1,11 @@
|
|||
import { Document } from "../document"
|
||||
|
||||
export interface Layout extends Document {
|
||||
componentLibraries: string[]
|
||||
title: string
|
||||
favicon: string
|
||||
stylesheets: string[]
|
||||
props: any
|
||||
layoutId?: string
|
||||
name?: string
|
||||
}
|
||||
|
|
|
@ -22,4 +22,5 @@ export interface Screen extends Document {
|
|||
routing: ScreenRouting
|
||||
props: ScreenProps
|
||||
name?: string
|
||||
pluginAdded?: boolean
|
||||
}
|
||||
|
|
|
@ -5,15 +5,15 @@ export interface RowValue {
|
|||
deleted: boolean
|
||||
}
|
||||
|
||||
export interface RowResponse<T extends Document> {
|
||||
export interface RowResponse<T extends Document | RowValue> {
|
||||
id: string
|
||||
key: string
|
||||
error: string
|
||||
value: T | RowValue
|
||||
value: T
|
||||
doc?: T
|
||||
}
|
||||
|
||||
export interface AllDocsResponse<T extends Document> {
|
||||
export interface AllDocsResponse<T extends Document | RowValue> {
|
||||
offset: number
|
||||
total_rows: number
|
||||
rows: RowResponse<T>[]
|
||||
|
|
|
@ -56,6 +56,7 @@ export enum SourceName {
|
|||
FIRESTORE = "FIRESTORE",
|
||||
REDIS = "REDIS",
|
||||
SNOWFLAKE = "SNOWFLAKE",
|
||||
BUDIBASE = "BUDIBASE",
|
||||
}
|
||||
|
||||
export enum IncludeRelationship {
|
||||
|
|
|
@ -1,5 +1,11 @@
|
|||
import type Nano from "@budibase/nano"
|
||||
import { AllDocsResponse, AnyDocument, Document, ViewTemplateOpts } from "../"
|
||||
import {
|
||||
AllDocsResponse,
|
||||
AnyDocument,
|
||||
Document,
|
||||
RowValue,
|
||||
ViewTemplateOpts,
|
||||
} from "../"
|
||||
import { Writable } from "stream"
|
||||
|
||||
export enum SearchIndex {
|
||||
|
@ -136,7 +142,7 @@ export interface Database {
|
|||
opts?: DatabasePutOpts
|
||||
): Promise<Nano.DocumentInsertResponse>
|
||||
bulkDocs(documents: AnyDocument[]): Promise<Nano.DocumentBulkResponse[]>
|
||||
allDocs<T extends Document>(
|
||||
allDocs<T extends Document | RowValue>(
|
||||
params: DatabaseQueryOpts
|
||||
): Promise<AllDocsResponse<T>>
|
||||
query<T extends Document>(
|
||||
|
|
|
@ -280,7 +280,7 @@ class TestConfiguration {
|
|||
|
||||
const db = context.getGlobalDB()
|
||||
|
||||
const id = dbCore.generateDevInfoID(this.user!._id)
|
||||
const id = dbCore.generateDevInfoID(this.user!._id!)
|
||||
// TODO: dry
|
||||
this.apiKey = encryption.encrypt(
|
||||
`${this.tenantId}${dbCore.SEPARATOR}${utils.newid()}`
|
||||
|
|
|
@ -17,6 +17,12 @@ const { nodeExternalsPlugin } = require("esbuild-node-externals")
|
|||
const svelteCompilePlugin = {
|
||||
name: 'svelteCompile',
|
||||
setup(build) {
|
||||
// This resolve handler is necessary to bundle the Svelte runtime into the the final output,
|
||||
// otherwise the bundled script will attempt to resolve it at runtime
|
||||
build.onResolve({ filter: /svelte\/internal/ }, async () => {
|
||||
return { path: `${process.cwd()}/../../node_modules/svelte/src/runtime/internal/ssr.js` }
|
||||
})
|
||||
|
||||
// Compiles `.svelte` files into JS classes so that they can be directly imported into our
|
||||
// Typescript packages
|
||||
build.onLoad({ filter: /\.svelte$/ }, async (args) => {
|
||||
|
@ -31,7 +37,7 @@ const svelteCompilePlugin = {
|
|||
contents: js.code,
|
||||
// The loader this is passed to, basically how the above provided content is "treated",
|
||||
// the contents provided above will be transpiled and bundled like any other JS file.
|
||||
loader: 'js',
|
||||
loader: 'js',
|
||||
// Where to resolve any imports present in the loaded file
|
||||
resolveDir: dir
|
||||
}
|
||||
|
@ -74,11 +80,11 @@ async function runBuild(entry, outfile) {
|
|||
plugins: [
|
||||
svelteCompilePlugin,
|
||||
TsconfigPathsPlugin({ tsconfig: tsconfigPathPluginContent }),
|
||||
nodeExternalsPlugin({
|
||||
allowList: ["@budibase/frontend-core", "svelte"]
|
||||
}),
|
||||
nodeExternalsPlugin(),
|
||||
],
|
||||
preserveSymlinks: true,
|
||||
loader: {
|
||||
},
|
||||
metafile: true,
|
||||
external: [
|
||||
"deasync",
|
||||
|
|
Loading…
Reference in New Issue