Merge branch 'next' of github.com:Budibase/budibase into feature/draft-apps
This commit is contained in:
commit
13b1e78ee2
|
@ -8,10 +8,10 @@
|
||||||
</h1>
|
</h1>
|
||||||
|
|
||||||
<h3 align="center">
|
<h3 align="center">
|
||||||
Build internal tools 50x faster on your own infrastructure
|
Build custom business tools in minutes and on your own infrastructure
|
||||||
</h3>
|
</h3>
|
||||||
<p align="center">
|
<p align="center">
|
||||||
Budibase is an open-source low-code platform, helping developers and IT professionals build, automate, and ship internal tools 50x faster on their own infrastructure.
|
Budibase is an open-source low-code platform, helping developers and IT professionals build, automate, and ship custom business apps in minutes and on their own infrastructure.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<h3 align="center">
|
<h3 align="center">
|
||||||
|
|
|
@ -16,7 +16,7 @@ services:
|
||||||
MINIO_URL: http://minio-service:9000
|
MINIO_URL: http://minio-service:9000
|
||||||
MINIO_ACCESS_KEY: ${MINIO_ACCESS_KEY}
|
MINIO_ACCESS_KEY: ${MINIO_ACCESS_KEY}
|
||||||
MINIO_SECRET_KEY: ${MINIO_SECRET_KEY}
|
MINIO_SECRET_KEY: ${MINIO_SECRET_KEY}
|
||||||
HOSTING_KEY: ${HOSTING_KEY}
|
INTERNAL_API_KEY: ${INTERNAL_API_KEY}
|
||||||
BUDIBASE_ENVIRONMENT: ${BUDIBASE_ENVIRONMENT}
|
BUDIBASE_ENVIRONMENT: ${BUDIBASE_ENVIRONMENT}
|
||||||
PORT: 4002
|
PORT: 4002
|
||||||
JWT_SECRET: ${JWT_SECRET}
|
JWT_SECRET: ${JWT_SECRET}
|
||||||
|
@ -44,7 +44,7 @@ services:
|
||||||
COUCH_DB_USERNAME: ${COUCH_DB_USER}
|
COUCH_DB_USERNAME: ${COUCH_DB_USER}
|
||||||
COUCH_DB_PASSWORD: ${COUCH_DB_PASSWORD}
|
COUCH_DB_PASSWORD: ${COUCH_DB_PASSWORD}
|
||||||
COUCH_DB_URL: http://${COUCH_DB_USER}:${COUCH_DB_PASSWORD}@couchdb-service:5984
|
COUCH_DB_URL: http://${COUCH_DB_USER}:${COUCH_DB_PASSWORD}@couchdb-service:5984
|
||||||
SELF_HOST_KEY: ${HOSTING_KEY}
|
INTERNAL_API_KEY: ${INTERNAL_API_KEY}
|
||||||
REDIS_URL: redis-service:6379
|
REDIS_URL: redis-service:6379
|
||||||
REDIS_PASSWORD: ${REDIS_PASSWORD}
|
REDIS_PASSWORD: ${REDIS_PASSWORD}
|
||||||
depends_on:
|
depends_on:
|
||||||
|
|
|
@ -1,10 +1,6 @@
|
||||||
# Use the main port in the builder for your self hosting URL, e.g. localhost:10000
|
# Use the main port in the builder for your self hosting URL, e.g. localhost:10000
|
||||||
MAIN_PORT=10000
|
MAIN_PORT=10000
|
||||||
|
|
||||||
# Use this password when configuring your self hosting settings
|
|
||||||
# This should be updated
|
|
||||||
HOSTING_KEY=budibase
|
|
||||||
|
|
||||||
# This section contains all secrets pertaining to the system
|
# This section contains all secrets pertaining to the system
|
||||||
# These should be updated
|
# These should be updated
|
||||||
JWT_SECRET=testsecret
|
JWT_SECRET=testsecret
|
||||||
|
@ -13,6 +9,7 @@ MINIO_SECRET_KEY=budibase
|
||||||
COUCH_DB_PASSWORD=budibase
|
COUCH_DB_PASSWORD=budibase
|
||||||
COUCH_DB_USER=budibase
|
COUCH_DB_USER=budibase
|
||||||
REDIS_PASSWORD=budibase
|
REDIS_PASSWORD=budibase
|
||||||
|
INTERNAL_API_KEY=budibase
|
||||||
|
|
||||||
# This section contains variables that do not need to be altered under normal circumstances
|
# This section contains variables that do not need to be altered under normal circumstances
|
||||||
APP_PORT=4002
|
APP_PORT=4002
|
||||||
|
|
|
@ -0,0 +1,198 @@
|
||||||
|
<p align="center">
|
||||||
|
<a href="https://www.budibase.com">
|
||||||
|
<img alt="Budibase" src="https://d33wubrfki0l68.cloudfront.net/aac32159d7207b5085e74a7ef67afbb7027786c5/2b1fd/img/logo/bb-emblem.svg" width="60" />
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
<h1 align="center">
|
||||||
|
Budibase
|
||||||
|
</h1>
|
||||||
|
|
||||||
|
<h3 align="center">
|
||||||
|
Construye herramientas empresariales personalizadas en cuestión de minutos y en su propia infraestructura.
|
||||||
|
</h3>
|
||||||
|
<p align="center">
|
||||||
|
Budibase es una plataforma de código bajo de código abierto, que ayuda a desarrolladores y profesionales de TI a crear, automatizar y enviar aplicaciones empresariales personalizadas en cuestión de minutos y en su propia infraestructura
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<h3 align="center">
|
||||||
|
🤖 🎨 🚀
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
|
||||||
|
<p align="center">
|
||||||
|
<img src="https://i.imgur.com/tPQHruf.png">
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p align="center">
|
||||||
|
<a href="https://github.com/Budibase/budibase/releases">
|
||||||
|
<img alt="GitHub all releases" src="https://img.shields.io/github/downloads/Budibase/budibase/total">
|
||||||
|
</a>
|
||||||
|
<a href="https://github.com/Budibase/budibase/releases">
|
||||||
|
<img alt="GitHub release (latest by date)" src="https://img.shields.io/github/v/release/Budibase/budibase">
|
||||||
|
</a>
|
||||||
|
<a href="https://discord.gg/rCYayfe">
|
||||||
|
<img alt="Discord" src="https://img.shields.io/discord/733030666647765003">
|
||||||
|
</a>
|
||||||
|
<a href="https://twitter.com/intent/follow?screen_name=budibase">
|
||||||
|
<img src="https://img.shields.io/twitter/follow/budibase?style=social" alt="Follow @budibase" />
|
||||||
|
</a>
|
||||||
|
<img src="https://img.shields.io/badge/Contributor%20Covenant-v2.0%20adopted-ff69b4.svg" alt="Code of conduct" />
|
||||||
|
<a href="https://codecov.io/gh/Budibase/budibase">
|
||||||
|
<img src="https://codecov.io/gh/Budibase/budibase/graph/badge.svg?token=E8W2ZFXQOH"/>
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<h3 align="center">
|
||||||
|
<a href="https://portal.budi.live/signup">Sign-up</a>
|
||||||
|
<span> · </span>
|
||||||
|
<a href="https://docs.budibase.com">Docs</a>
|
||||||
|
<span> · </span>
|
||||||
|
<a href="https://github.com/Budibase/budibase/discussions?discussions_q=category%3AIdeas">Feature request</a>
|
||||||
|
<span> · </span>
|
||||||
|
<a href="https://github.com/Budibase/budibase/issues">Report a bug</a>
|
||||||
|
<span> · </span>
|
||||||
|
Support: <a href="https://github.com/Budibase/budibase/discussions">Discussions</a>
|
||||||
|
<span> & </span>
|
||||||
|
<a href="https://discord.gg/rCYayfe">Discord</a>
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
|
||||||
|
## ✨ Features
|
||||||
|
When other platforms chose the closed source route, we decided to go open source. When other platforms chose cloud builders, we decided a local builder offered the better developer experience. We like to do things differently at Budibase.
|
||||||
|
|
||||||
|
- **Build and ship real software.** Unlike other platforms, with Budibase you build and ship single page applications. Budibase applications have performance baked in and can be designed responsively, providing your users with a great experience.
|
||||||
|
|
||||||
|
- **Open source and extensable.** Budibase is open-source. The builder is licensed AGPL v3, the server is GPL v3, and the client is MPL. This should fill you with confidence that Budibase will always be around. You can also code against Budibase or fork it and make changes as you please, providing a developer-friendly experience.
|
||||||
|
|
||||||
|
- **Load data or start from scratch.** Budibase pulls in data from multiple sources, including MongoDB, CouchDB, PostgreSQL, mySQL, Airtable, Google Sheets, S3, DyanmoDB, or a REST API. And unlike other platforms, with Budibase you can start from scratch and create business apps with no data sources. [Request new data sources](https://github.com/Budibase/budibase/discussions?discussions_q=category%3AIdeas).
|
||||||
|
|
||||||
|
- **Design and build apps with powerful pre-made components.** Budibase comes out of the box with beautifully designed, powerful components which you can use like building blocks to build your UI. We also expose a lot of your favourite CSS styling options so you can go that extra creative mile. [Request new components](https://github.com/Budibase/budibase/discussions?discussions_q=category%3AIdeas).
|
||||||
|
|
||||||
|
- **Automate processes, integrate with other tools, and connect to webhooks.** Save time by automating manual processes and workflows. From connecting to webhooks, to automating emails, simply tell Budibase what to do and let it work for you. You can easily [create new automations for Budibase here](https://github.com/Budibase/automations) or [request new integrations here](https://github.com/Budibase/budibase/discussions?discussions_q=category%3AIdeas).
|
||||||
|
|
||||||
|
- **Cloud hosting and self-hosting.** Users can self-host (see below), or host their apps with Budibase. Currently, our cloud hosting offering is limited to the free tier but we aim to change this in the future. For heavy usage, we advise users to self-host.
|
||||||
|
|
||||||
|
<p align="center">
|
||||||
|
<img alt="Budibase design ui" src="https://imgur.com/v8m6v3q.png">
|
||||||
|
</p>
|
||||||
|
|
||||||
|
|
||||||
|
## ⌛ Status
|
||||||
|
- [x] Alpha: We are demoing Budibase to users and receiving feedback
|
||||||
|
- [x] Private Beta: We are testing Budibase with a closed set of customers
|
||||||
|
- [x] Public Beta: Anyone can [sign-up and use Budibase](https://portal.budi.live/signup).
|
||||||
|
- [ ] Official Launch
|
||||||
|
|
||||||
|
Watch "releases" of this repo to get notified of major updates, and give the star button a click whilst you're there.
|
||||||
|
|
||||||
|
<p align="center">
|
||||||
|
<img src="https://i.imgur.com/cJpgqm8.png">
|
||||||
|
</p>
|
||||||
|
|
||||||
|
### Stargazers over time
|
||||||
|
|
||||||
|
[![Stargazers over time](https://starchart.cc/Budibase/budibase.svg)](https://starchart.cc/Budibase/budibase)
|
||||||
|
|
||||||
|
If you are having issues between updates of the builder, please use the guide [here](https://github.com/Budibase/budibase/blob/HEAD/.github/CONTRIBUTING.md#troubleshooting) to clear down your environment.
|
||||||
|
|
||||||
|
|
||||||
|
## 🏁 Getting Started with Budibase
|
||||||
|
|
||||||
|
The Budibase builder runs in Electron, on Mac, PC and Linux. Follow the steps below to get started:
|
||||||
|
- [ ] [Sign-up to Budibase](https://portal.budi.live/signup)
|
||||||
|
- [ ] Create a username and password
|
||||||
|
- [ ] Copy your API key
|
||||||
|
- [ ] Download Budibase
|
||||||
|
- [ ] Open Budibase and enter your API key
|
||||||
|
|
||||||
|
[Here is a guided tutorial](https://docs.budibase.com/tutorial/tutorial-signing-up) if you need extra help.
|
||||||
|
|
||||||
|
|
||||||
|
## 🤖 Self-hosting
|
||||||
|
|
||||||
|
Budibase wants to make sure anyone can use the tools we develop and we know a lot of people need to be able to host the apps they make on their own systems - that is why we've decided to try and make self hosting as easy as possible!
|
||||||
|
|
||||||
|
Currently, you can host your apps using Docker or Digital Ocean. The documentation for self-hosting can be found [here](https://docs.budibase.com/self-hosting/introduction-to-self-hosting).
|
||||||
|
|
||||||
|
[![Deploy to DO](https://www.deploytodo.com/do-btn-blue.svg)](https://cloud.digitalocean.com/droplets/new?onboarding_origin=marketplace&i=09038e&fleetUuid=bb04f9c8-1de8-4687-b2ae-1d5177a0535b&appId=77729671&type=applications&size=s-4vcpu-8gb®ion=nyc1&refcode=0caaa6085a82&image=budibase-20-04)
|
||||||
|
|
||||||
|
|
||||||
|
## 🎓 Learning Budibase
|
||||||
|
|
||||||
|
The Budibase [documentation lives here](https://docs.budibase.com).
|
||||||
|
|
||||||
|
You can also follow a quick tutorial on [how to build a CRM with Budibase](https://docs.budibase.com/tutorial/tutorial-introduction)
|
||||||
|
|
||||||
|
|
||||||
|
## Roadmap
|
||||||
|
|
||||||
|
Checkout our [Public Roadmap](https://github.com/Budibase/budibase/projects/10). If you would like to discuss some of the items on the roadmap, please feel to reach out on [Discord](https://discord.gg/rCYayfe), or via [Github discussions](https://github.com/Budibase/budibase/discussions)
|
||||||
|
|
||||||
|
|
||||||
|
## ❗ Code of Conduct
|
||||||
|
|
||||||
|
Budibase is dedicated to providing a welcoming, diverse, and harrassment-free experience for everyone. We expect everyone in the Budibase community to abide by our [**Code of Conduct**](https://github.com/Budibase/budibase/blob/HEAD/.github/CODE_OF_CONDUCT.md). Please read it.
|
||||||
|
|
||||||
|
## 🙌 Contributing to Budibase
|
||||||
|
|
||||||
|
From opening a bug report to creating a pull request: every contribution is appreciated and welcomed. If you're planning to implement a new feature or change the API please create an issue first. This way we can ensure your work is not in vain.
|
||||||
|
|
||||||
|
### Not Sure Where to Start?
|
||||||
|
A good place to start contributing, is the [First time issues project](https://github.com/Budibase/budibase/projects/22).
|
||||||
|
|
||||||
|
### How the repository is organized
|
||||||
|
Budibase is a monorepo managed by lerna. Lerna manages the building and publishing of the budibase packages. At a high level, here are the packages that make up Budibase.
|
||||||
|
|
||||||
|
- [packages/builder](https://github.com/Budibase/budibase/tree/HEAD/packages/builder) - contains code for the budibase builder client side svelte application.
|
||||||
|
|
||||||
|
- [packages/client](https://github.com/Budibase/budibase/tree/HEAD/packages/client) - A module that runs in the browser responsible for reading JSON definition and creating living, breathing web apps from it.
|
||||||
|
|
||||||
|
- [packages/server](https://github.com/Budibase/budibase/tree/HEAD/packages/server) - The budibase server. This Koa app is responsible for serving the JS for the builder and budibase apps, as well as providing the API for interaction with the database and file system.
|
||||||
|
|
||||||
|
For more information, see [CONTRIBUTING.md](https://github.com/Budibase/budibase/blob/HEAD/.github/CONTRIBUTING.md)
|
||||||
|
|
||||||
|
## 📝 License
|
||||||
|
|
||||||
|
Budibase is open-source. The builder is licensed [AGPL v3](https://www.gnu.org/licenses/agpl-3.0.en.html), the server is licensed [GPL v3](https://www.gnu.org/licenses/gpl-3.0.en.html), and the client is licensed [MPL](https://directory.fsf.org/wiki/License:MPL-2.0).
|
||||||
|
|
||||||
|
## 💬 Get in touch
|
||||||
|
|
||||||
|
If you have a question or would like to talk with other Budibase users, please hop over to [Github discussions](https://github.com/Budibase/budibase/discussions) or join our Discord server:
|
||||||
|
|
||||||
|
[Discord chatroom](https://discord.gg/rCYayfe)
|
||||||
|
|
||||||
|
![Discord Shield](https://discordapp.com/api/guilds/733030666647765003/widget.png?style=shield)
|
||||||
|
|
||||||
|
|
||||||
|
## Contributors ✨
|
||||||
|
|
||||||
|
Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/docs/en/emoji-key)):
|
||||||
|
|
||||||
|
<!-- ALL-CONTRIBUTORS-LIST:START - Do not remove or modify this section -->
|
||||||
|
<!-- prettier-ignore-start -->
|
||||||
|
<!-- markdownlint-disable -->
|
||||||
|
<table>
|
||||||
|
<tr>
|
||||||
|
<td align="center"><a href="http://martinmck.com"><img src="https://avatars1.githubusercontent.com/u/11256663?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Martin McKeaveney</b></sub></a><br /><a href="https://github.com/Budibase/budibase/commits?author=shogunpurple" title="Code">💻</a> <a href="https://github.com/Budibase/budibase/commits?author=shogunpurple" title="Documentation">📖</a> <a href="https://github.com/Budibase/budibase/commits?author=shogunpurple" title="Tests">⚠️</a> <a href="#infra-shogunpurple" title="Infrastructure (Hosting, Build-Tools, etc)">🚇</a></td>
|
||||||
|
<td align="center"><a href="http://www.michaeldrury.co.uk/"><img src="https://avatars2.githubusercontent.com/u/4407001?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Michael Drury</b></sub></a><br /><a href="https://github.com/Budibase/budibase/commits?author=mike12345567" title="Documentation">📖</a> <a href="https://github.com/Budibase/budibase/commits?author=mike12345567" title="Code">💻</a> <a href="https://github.com/Budibase/budibase/commits?author=mike12345567" title="Tests">⚠️</a> <a href="#infra-mike12345567" title="Infrastructure (Hosting, Build-Tools, etc)">🚇</a></td>
|
||||||
|
<td align="center"><a href="https://github.com/aptkingston"><img src="https://avatars3.githubusercontent.com/u/9075550?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Andrew Kingston</b></sub></a><br /><a href="https://github.com/Budibase/budibase/commits?author=aptkingston" title="Documentation">📖</a> <a href="https://github.com/Budibase/budibase/commits?author=aptkingston" title="Code">💻</a> <a href="https://github.com/Budibase/budibase/commits?author=aptkingston" title="Tests">⚠️</a> <a href="#design-aptkingston" title="Design">🎨</a></td>
|
||||||
|
<td align="center"><a href="https://budibase.com/"><img src="https://avatars3.githubusercontent.com/u/3524181?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Michael Shanks</b></sub></a><br /><a href="https://github.com/Budibase/budibase/commits?author=mjashanks" title="Documentation">📖</a> <a href="https://github.com/Budibase/budibase/commits?author=mjashanks" title="Code">💻</a> <a href="https://github.com/Budibase/budibase/commits?author=mjashanks" title="Tests">⚠️</a></td>
|
||||||
|
<td align="center"><a href="https://github.com/kevmodrome"><img src="https://avatars3.githubusercontent.com/u/534488?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Kevin Åberg Kultalahti</b></sub></a><br /><a href="https://github.com/Budibase/budibase/commits?author=kevmodrome" title="Documentation">📖</a> <a href="https://github.com/Budibase/budibase/commits?author=kevmodrome" title="Code">💻</a> <a href="https://github.com/Budibase/budibase/commits?author=kevmodrome" title="Tests">⚠️</a></td>
|
||||||
|
<td align="center"><a href="https://www.budibase.com/"><img src="https://avatars2.githubusercontent.com/u/49767913?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Joe</b></sub></a><br /><a href="https://github.com/Budibase/budibase/commits?author=joebudi" title="Documentation">📖</a> <a href="https://github.com/Budibase/budibase/commits?author=joebudi" title="Code">💻</a> <a href="#content-joebudi" title="Content">🖋</a> <a href="#design-joebudi" title="Design">🎨</a></td>
|
||||||
|
<td align="center"><a href="https://github.com/Conor-Mack"><img src="https://avatars1.githubusercontent.com/u/36074859?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Conor_Mack</b></sub></a><br /><a href="https://github.com/Budibase/budibase/commits?author=Conor-Mack" title="Code">💻</a> <a href="https://github.com/Budibase/budibase/commits?author=Conor-Mack" title="Tests">⚠️</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td align="center"><a href="https://github.com/pngwn"><img src="https://avatars1.githubusercontent.com/u/12937446?v=4?s=100" width="100px;" alt=""/><br /><sub><b>pngwn</b></sub></a><br /><a href="https://github.com/Budibase/budibase/commits?author=pngwn" title="Code">💻</a> <a href="https://github.com/Budibase/budibase/commits?author=pngwn" title="Tests">⚠️</a></td>
|
||||||
|
<td align="center"><a href="https://github.com/HugoLd"><img src="https://avatars0.githubusercontent.com/u/26521848?v=4?s=100" width="100px;" alt=""/><br /><sub><b>HugoLd</b></sub></a><br /><a href="https://github.com/Budibase/budibase/commits?author=HugoLd" title="Code">💻</a></td>
|
||||||
|
<td align="center"><a href="https://github.com/victoriasloan"><img src="https://avatars.githubusercontent.com/u/9913651?v=4?s=100" width="100px;" alt=""/><br /><sub><b>victoriasloan</b></sub></a><br /><a href="https://github.com/Budibase/budibase/commits?author=victoriasloan" title="Code">💻</a></td>
|
||||||
|
<td align="center"><a href="https://github.com/yashank09"><img src="https://avatars.githubusercontent.com/u/37672190?v=4?s=100" width="100px;" alt=""/><br /><sub><b>yashank09</b></sub></a><br /><a href="https://github.com/Budibase/budibase/commits?author=yashank09" title="Code">💻</a></td>
|
||||||
|
<td align="center"><a href="https://github.com/SOVLOOKUP"><img src="https://avatars.githubusercontent.com/u/53158137?v=4?s=100" width="100px;" alt=""/><br /><sub><b>SOVLOOKUP</b></sub></a><br /><a href="https://github.com/Budibase/budibase/commits?author=SOVLOOKUP" title="Code">💻</a></td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<!-- markdownlint-restore -->
|
||||||
|
<!-- prettier-ignore-end -->
|
||||||
|
|
||||||
|
<!-- ALL-CONTRIBUTORS-LIST:END -->
|
||||||
|
|
||||||
|
This project follows the [all-contributors](https://github.com/all-contributors/all-contributors) specification. Contributions of any kind welcome!
|
|
@ -14,7 +14,7 @@
|
||||||
"prettier-plugin-svelte": "^2.2.0",
|
"prettier-plugin-svelte": "^2.2.0",
|
||||||
"rimraf": "^3.0.2",
|
"rimraf": "^3.0.2",
|
||||||
"rollup-plugin-replace": "^2.2.0",
|
"rollup-plugin-replace": "^2.2.0",
|
||||||
"svelte": "^3.37.0"
|
"svelte": "^3.38.2"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"setup": "./hosting/scripts/setup.js && yarn && yarn bootstrap && yarn build && yarn dev",
|
"setup": "./hosting/scripts/setup.js && yarn && yarn bootstrap && yarn build && yarn dev",
|
||||||
|
|
|
@ -71,7 +71,7 @@ exports.getGlobalUserParams = (globalId, otherProps = {}) => {
|
||||||
* @param ownerId The owner/user of the template, this could be global or a group level.
|
* @param ownerId The owner/user of the template, this could be global or a group level.
|
||||||
*/
|
*/
|
||||||
exports.generateTemplateID = ownerId => {
|
exports.generateTemplateID = ownerId => {
|
||||||
return `${DocumentTypes.TEMPLATE}${SEPARATOR}${ownerId}${newid()}`
|
return `${DocumentTypes.TEMPLATE}${SEPARATOR}${ownerId}${SEPARATOR}${newid()}`
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -15,5 +15,6 @@ module.exports = {
|
||||||
MINIO_ACCESS_KEY: process.env.MINIO_ACCESS_KEY,
|
MINIO_ACCESS_KEY: process.env.MINIO_ACCESS_KEY,
|
||||||
MINIO_SECRET_KEY: process.env.MINIO_SECRET_KEY,
|
MINIO_SECRET_KEY: process.env.MINIO_SECRET_KEY,
|
||||||
MINIO_URL: process.env.MINIO_URL,
|
MINIO_URL: process.env.MINIO_URL,
|
||||||
|
INTERNAL_API_KEY: process.env.INTERNAL_API_KEY,
|
||||||
isTest,
|
isTest,
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,6 +2,7 @@ const { Cookies } = require("../constants")
|
||||||
const database = require("../db")
|
const database = require("../db")
|
||||||
const { getCookie, clearCookie } = require("../utils")
|
const { getCookie, clearCookie } = require("../utils")
|
||||||
const { StaticDatabases } = require("../db/utils")
|
const { StaticDatabases } = require("../db/utils")
|
||||||
|
const env = require("../environment")
|
||||||
|
|
||||||
const PARAM_REGEX = /\/:(.*?)\//g
|
const PARAM_REGEX = /\/:(.*?)\//g
|
||||||
|
|
||||||
|
@ -35,10 +36,14 @@ module.exports = (noAuthPatterns = [], opts) => {
|
||||||
return next()
|
return next()
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
|
const apiKey = ctx.request.headers["x-budibase-api-key"]
|
||||||
// check the actual user is authenticated first
|
// check the actual user is authenticated first
|
||||||
const authCookie = getCookie(ctx, Cookies.Auth)
|
const authCookie = getCookie(ctx, Cookies.Auth)
|
||||||
|
|
||||||
if (authCookie) {
|
// this is an internal request, no user made it
|
||||||
|
if (apiKey && apiKey === env.INTERNAL_API_KEY) {
|
||||||
|
ctx.isAuthenticated = true
|
||||||
|
} else if (authCookie) {
|
||||||
try {
|
try {
|
||||||
const db = database.getDB(StaticDatabases.GLOBAL.name)
|
const db = database.getDB(StaticDatabases.GLOBAL.name)
|
||||||
const user = await db.get(authCookie.userId)
|
const user = await db.get(authCookie.userId)
|
||||||
|
|
|
@ -27,7 +27,7 @@
|
||||||
"rollup-plugin-postcss": "^4.0.0",
|
"rollup-plugin-postcss": "^4.0.0",
|
||||||
"rollup-plugin-svelte": "^7.1.0",
|
"rollup-plugin-svelte": "^7.1.0",
|
||||||
"rollup-plugin-terser": "^7.0.2",
|
"rollup-plugin-terser": "^7.0.2",
|
||||||
"svelte": "^3.37.0"
|
"svelte": "^3.38.2"
|
||||||
},
|
},
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"svelte"
|
"svelte"
|
||||||
|
|
|
@ -15,6 +15,8 @@
|
||||||
export let fileSizeLimit = BYTES_IN_MB * 20
|
export let fileSizeLimit = BYTES_IN_MB * 20
|
||||||
export let processFiles = null
|
export let processFiles = null
|
||||||
export let handleFileTooLarge = null
|
export let handleFileTooLarge = null
|
||||||
|
export let gallery = true
|
||||||
|
export let error = null
|
||||||
|
|
||||||
const dispatch = createEventDispatcher()
|
const dispatch = createEventDispatcher()
|
||||||
const imageExtensions = [
|
const imageExtensions = [
|
||||||
|
@ -52,6 +54,8 @@
|
||||||
const newValue = [...value, ...processedFiles]
|
const newValue = [...value, ...processedFiles]
|
||||||
dispatch("change", newValue)
|
dispatch("change", newValue)
|
||||||
selectedImageIdx = newValue.length - 1
|
selectedImageIdx = newValue.length - 1
|
||||||
|
} else {
|
||||||
|
dispatch("change", fileList)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -94,47 +98,68 @@
|
||||||
|
|
||||||
<div class="container">
|
<div class="container">
|
||||||
{#if selectedImage}
|
{#if selectedImage}
|
||||||
<div class="gallery">
|
{#if gallery}
|
||||||
<div class="title">
|
<div class="gallery">
|
||||||
<div class="filename">{selectedImage.name}</div>
|
<div class="title">
|
||||||
<div class="filesize">
|
<div class="filename">{selectedImage.name}</div>
|
||||||
{#if selectedImage.size <= BYTES_IN_MB}
|
<div class="filesize">
|
||||||
{`${selectedImage.size / BYTES_IN_KB} KB`}
|
{#if selectedImage.size <= BYTES_IN_MB}
|
||||||
{:else}{`${selectedImage.size / BYTES_IN_MB} MB`}{/if}
|
{`${selectedImage.size / BYTES_IN_KB} KB`}
|
||||||
|
{:else}{`${selectedImage.size / BYTES_IN_MB} MB`}{/if}
|
||||||
|
</div>
|
||||||
|
{#if !disabled}
|
||||||
|
<div class="delete-button" on:click={removeFile}>
|
||||||
|
<Icon name="Close" />
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
{#if !disabled}
|
{#if isImage}
|
||||||
<div class="delete-button" on:click={removeFile}>
|
<img alt="preview" src={selectedImage.url} />
|
||||||
<Icon name="Close" />
|
{:else}
|
||||||
|
<div class="placeholder">
|
||||||
|
<div class="extension">{selectedImage.extension}</div>
|
||||||
|
<div>Preview not supported</div>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
<div
|
||||||
{#if isImage}
|
class="nav left"
|
||||||
<img alt="preview" src={selectedImage.url} />
|
class:visible={selectedImageIdx > 0}
|
||||||
{:else}
|
on:click={navigateLeft}
|
||||||
<div class="placeholder">
|
>
|
||||||
<div class="extension">{selectedImage.extension}</div>
|
<Icon name="ChevronLeft" />
|
||||||
<div>Preview not supported</div>
|
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
<div
|
||||||
<div
|
class="nav right"
|
||||||
class="nav left"
|
class:visible={selectedImageIdx < fileCount - 1}
|
||||||
class:visible={selectedImageIdx > 0}
|
on:click={navigateRight}
|
||||||
on:click={navigateLeft}
|
>
|
||||||
>
|
<Icon name="ChevronRight" />
|
||||||
<Icon name="ChevronLeft" />
|
</div>
|
||||||
|
<div class="footer">File {selectedImageIdx + 1} of {fileCount}</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
{:else if value?.length}
|
||||||
class="nav right"
|
{#each value as file}
|
||||||
class:visible={selectedImageIdx < fileCount - 1}
|
<div class="gallery">
|
||||||
on:click={navigateRight}
|
<div class="title">
|
||||||
>
|
<div class="filename">{file.name}</div>
|
||||||
<Icon name="ChevronRight" />
|
<div class="filesize">
|
||||||
</div>
|
{#if file.size <= BYTES_IN_MB}
|
||||||
<div class="footer">File {selectedImageIdx + 1} of {fileCount}</div>
|
{`${file.size / BYTES_IN_KB} KB`}
|
||||||
</div>
|
{:else}{`${file.size / BYTES_IN_MB} MB`}{/if}
|
||||||
|
</div>
|
||||||
|
{#if !disabled}
|
||||||
|
<div class="delete-button" on:click={removeFile}>
|
||||||
|
<Icon name="Close" />
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
{/if}
|
||||||
{/if}
|
{/if}
|
||||||
<div
|
<div
|
||||||
class="spectrum-Dropzone"
|
class="spectrum-Dropzone"
|
||||||
|
class:is-invalid={!!error}
|
||||||
class:disabled
|
class:disabled
|
||||||
role="region"
|
role="region"
|
||||||
tabindex="0"
|
tabindex="0"
|
||||||
|
@ -245,6 +270,9 @@
|
||||||
.spectrum-Dropzone {
|
.spectrum-Dropzone {
|
||||||
user-select: none;
|
user-select: none;
|
||||||
}
|
}
|
||||||
|
.spectrum-Dropzone.is-invalid {
|
||||||
|
border-color: var(--spectrum-global-color-red-400);
|
||||||
|
}
|
||||||
input[type="file"] {
|
input[type="file"] {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
@ -276,7 +304,7 @@
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
justify-content: flex-start;
|
justify-content: flex-start;
|
||||||
align-items: stretch;
|
align-items: center;
|
||||||
}
|
}
|
||||||
.filename {
|
.filename {
|
||||||
flex: 1 1 auto;
|
flex: 1 1 auto;
|
||||||
|
@ -331,6 +359,7 @@
|
||||||
.delete-button {
|
.delete-button {
|
||||||
transition: all 0.3s;
|
transition: all 0.3s;
|
||||||
margin-left: 10px;
|
margin-left: 10px;
|
||||||
|
display: flex;
|
||||||
}
|
}
|
||||||
.delete-button i {
|
.delete-button i {
|
||||||
font-size: 2em;
|
font-size: 2em;
|
||||||
|
|
|
@ -37,6 +37,7 @@
|
||||||
}
|
}
|
||||||
focus = false
|
focus = false
|
||||||
updateValue(event.target.value)
|
updateValue(event.target.value)
|
||||||
|
dispatch("blur")
|
||||||
}
|
}
|
||||||
|
|
||||||
const updateValueOnEnter = event => {
|
const updateValueOnEnter = event => {
|
||||||
|
|
|
@ -11,6 +11,7 @@
|
||||||
export let fileSizeLimit = undefined
|
export let fileSizeLimit = undefined
|
||||||
export let processFiles = undefined
|
export let processFiles = undefined
|
||||||
export let handleFileTooLarge = undefined
|
export let handleFileTooLarge = undefined
|
||||||
|
export let gallery = true
|
||||||
|
|
||||||
const dispatch = createEventDispatcher()
|
const dispatch = createEventDispatcher()
|
||||||
const onChange = e => {
|
const onChange = e => {
|
||||||
|
@ -27,6 +28,7 @@
|
||||||
{fileSizeLimit}
|
{fileSizeLimit}
|
||||||
{processFiles}
|
{processFiles}
|
||||||
{handleFileTooLarge}
|
{handleFileTooLarge}
|
||||||
|
{gallery}
|
||||||
on:change={onChange}
|
on:change={onChange}
|
||||||
/>
|
/>
|
||||||
</Field>
|
</Field>
|
||||||
|
|
|
@ -30,5 +30,6 @@
|
||||||
on:change={onChange}
|
on:change={onChange}
|
||||||
on:click
|
on:click
|
||||||
on:input
|
on:input
|
||||||
|
on:blur
|
||||||
/>
|
/>
|
||||||
</Field>
|
</Field>
|
||||||
|
|
|
@ -5,9 +5,11 @@
|
||||||
export let noPadding = false
|
export let noPadding = false
|
||||||
export let gap = "M"
|
export let gap = "M"
|
||||||
export let noGap = false
|
export let noGap = false
|
||||||
|
export let alignContent = "normal"
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
|
style="align-content:{alignContent};"
|
||||||
class:horizontal
|
class:horizontal
|
||||||
class="container paddingX-{!noPadding && paddingX} paddingY-{!noPadding &&
|
class="container paddingX-{!noPadding && paddingX} paddingY-{!noPadding &&
|
||||||
paddingY} gap-{!noGap && gap}"
|
paddingY} gap-{!noGap && gap}"
|
||||||
|
|
|
@ -8,8 +8,10 @@
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
div {
|
div {
|
||||||
display: grid;
|
display: flex;
|
||||||
grid-template-columns: 1fr;
|
flex-direction: column;
|
||||||
|
justify-content: flex-start;
|
||||||
|
align-items: stretch;
|
||||||
max-width: 80ch;
|
max-width: 80ch;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
padding: calc(var(--spacing-xl) * 2);
|
padding: calc(var(--spacing-xl) * 2);
|
||||||
|
@ -18,6 +20,7 @@
|
||||||
.wide {
|
.wide {
|
||||||
max-width: none;
|
max-width: none;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: var(--spacing-xl) calc(var(--spacing-xl) * 2);
|
padding: var(--spacing-xl) calc(var(--spacing-xl) * 2)
|
||||||
|
calc(var(--spacing-xl) * 2) calc(var(--spacing-xl) * 2);
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -7,9 +7,10 @@
|
||||||
import Context from "../context"
|
import Context from "../context"
|
||||||
|
|
||||||
export let fixed = false
|
export let fixed = false
|
||||||
|
export let inline = false
|
||||||
|
|
||||||
const dispatch = createEventDispatcher()
|
const dispatch = createEventDispatcher()
|
||||||
let visible = !!fixed
|
let visible = fixed || inline
|
||||||
$: dispatch(visible ? "show" : "hide")
|
$: dispatch(visible ? "show" : "hide")
|
||||||
|
|
||||||
export function show() {
|
export function show() {
|
||||||
|
@ -20,7 +21,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
export function hide() {
|
export function hide() {
|
||||||
if (!visible || fixed) {
|
if (!visible || fixed || inline) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
visible = false
|
visible = false
|
||||||
|
@ -45,11 +46,17 @@
|
||||||
|
|
||||||
<svelte:window on:keydown={handleKey} />
|
<svelte:window on:keydown={handleKey} />
|
||||||
|
|
||||||
{#if visible}
|
<!-- These svelte if statements need to be defined like this. -->
|
||||||
|
<!-- The modal transitions do not work if nested inside more than one "if" -->
|
||||||
|
{#if visible && inline}
|
||||||
|
<div use:focusFirstInput class="spectrum-Modal inline is-open">
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
|
{:else if visible}
|
||||||
<Portal target=".modal-container">
|
<Portal target=".modal-container">
|
||||||
<div
|
<div
|
||||||
class="spectrum-Underlay is-open"
|
class="spectrum-Underlay is-open"
|
||||||
transition:fade={{ duration: 200 }}
|
transition:fade|local={{ duration: 200 }}
|
||||||
on:mousedown|self={hide}
|
on:mousedown|self={hide}
|
||||||
>
|
>
|
||||||
<div class="modal-wrapper" on:mousedown|self={hide}>
|
<div class="modal-wrapper" on:mousedown|self={hide}>
|
||||||
|
@ -57,7 +64,7 @@
|
||||||
<div
|
<div
|
||||||
use:focusFirstInput
|
use:focusFirstInput
|
||||||
class="spectrum-Modal is-open"
|
class="spectrum-Modal is-open"
|
||||||
transition:fly={{ y: 30, duration: 200 }}
|
transition:fly|local={{ y: 30, duration: 200 }}
|
||||||
>
|
>
|
||||||
<slot />
|
<slot />
|
||||||
</div>
|
</div>
|
||||||
|
@ -98,6 +105,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.spectrum-Modal {
|
.spectrum-Modal {
|
||||||
|
background: var(--background);
|
||||||
overflow: visible;
|
overflow: visible;
|
||||||
max-height: none;
|
max-height: none;
|
||||||
margin: 40px 0;
|
margin: 40px 0;
|
||||||
|
@ -106,4 +114,7 @@
|
||||||
--spectrum-global-dimension-size-100
|
--spectrum-global-dimension-size-100
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
:global(.spectrum--lightest .spectrum-Modal.inline) {
|
||||||
|
border: var(--border-light);
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -214,7 +214,7 @@
|
||||||
>
|
>
|
||||||
<div style={contentStyle}>
|
<div style={contentStyle}>
|
||||||
<table class="spectrum-Table" class:spectrum-Table--quiet={quiet}>
|
<table class="spectrum-Table" class:spectrum-Table--quiet={quiet}>
|
||||||
{#if sortedRows?.length}
|
{#if fields.length}
|
||||||
<thead class="spectrum-Table-head">
|
<thead class="spectrum-Table-head">
|
||||||
<tr>
|
<tr>
|
||||||
{#if showEditColumn}
|
{#if showEditColumn}
|
||||||
|
@ -269,7 +269,7 @@
|
||||||
</thead>
|
</thead>
|
||||||
{/if}
|
{/if}
|
||||||
<tbody class="spectrum-Table-body">
|
<tbody class="spectrum-Table-body">
|
||||||
{#if sortedRows?.length}
|
{#if sortedRows?.length && fields.length}
|
||||||
{#each sortedRows as row, idx}
|
{#each sortedRows as row, idx}
|
||||||
<tr
|
<tr
|
||||||
on:click={() => toggleSelectRow(row)}
|
on:click={() => toggleSelectRow(row)}
|
||||||
|
@ -316,15 +316,25 @@
|
||||||
</tr>
|
</tr>
|
||||||
{/each}
|
{/each}
|
||||||
{:else}
|
{:else}
|
||||||
<div class="placeholder">
|
<tr class="placeholder-row">
|
||||||
<svg
|
{#if showEditColumn}
|
||||||
class="spectrum-Icon spectrum-Icon--sizeXXL"
|
<td class="placeholder-offset" />
|
||||||
focusable="false"
|
{/if}
|
||||||
>
|
{#each fields as field}
|
||||||
<use xlink:href="#spectrum-icon-18-Table" />
|
<td />
|
||||||
</svg>
|
{/each}
|
||||||
<div>No rows found</div>
|
<div class="placeholder" class:has-fields={fields.length > 0}>
|
||||||
</div>
|
<div class="placeholder-content">
|
||||||
|
<svg
|
||||||
|
class="spectrum-Icon spectrum-Icon--sizeXXL"
|
||||||
|
focusable="false"
|
||||||
|
>
|
||||||
|
<use xlink:href="#spectrum-icon-18-Table" />
|
||||||
|
</svg>
|
||||||
|
<div>No rows found</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</tr>
|
||||||
{/if}
|
{/if}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
@ -347,7 +357,7 @@
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
}
|
}
|
||||||
.container.quiet {
|
.container.quiet {
|
||||||
border: none !important;
|
border: none;
|
||||||
}
|
}
|
||||||
table {
|
table {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
@ -381,7 +391,7 @@
|
||||||
z-index: 2;
|
z-index: 2;
|
||||||
background-color: var(--spectrum-alias-background-color-secondary);
|
background-color: var(--spectrum-alias-background-color-secondary);
|
||||||
border-bottom: 1px solid
|
border-bottom: 1px solid
|
||||||
var(--spectrum-table-border-color, var(--spectrum-alias-border-color-mid)) !important;
|
var(--spectrum-table-border-color, var(--spectrum-alias-border-color-mid));
|
||||||
}
|
}
|
||||||
.spectrum-Table-headCell-content {
|
.spectrum-Table-headCell-content {
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
|
@ -396,7 +406,34 @@
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.placeholder-row {
|
||||||
|
position: relative;
|
||||||
|
height: 150px;
|
||||||
|
}
|
||||||
|
.placeholder-row td {
|
||||||
|
border-top: none !important;
|
||||||
|
border-bottom: none !important;
|
||||||
|
}
|
||||||
|
.placeholder-offset {
|
||||||
|
width: 1px;
|
||||||
|
}
|
||||||
.placeholder {
|
.placeholder {
|
||||||
|
top: 0;
|
||||||
|
height: 100%;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
position: absolute;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
.placeholder.has-fields {
|
||||||
|
top: var(--header-height);
|
||||||
|
height: calc(100% - var(--header-height));
|
||||||
|
}
|
||||||
|
|
||||||
|
.placeholder-content {
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
@ -407,12 +444,13 @@
|
||||||
var(--spectrum-alias-text-color)
|
var(--spectrum-alias-text-color)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
.placeholder div {
|
.placeholder-content div {
|
||||||
margin-top: 10px;
|
margin-top: 10px;
|
||||||
font-size: var(
|
font-size: var(
|
||||||
--spectrum-table-cell-text-size,
|
--spectrum-table-cell-text-size,
|
||||||
var(--spectrum-alias-font-size-default)
|
var(--spectrum-alias-font-size-default)
|
||||||
);
|
);
|
||||||
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
tbody {
|
tbody {
|
||||||
|
@ -431,17 +469,17 @@
|
||||||
td {
|
td {
|
||||||
padding-top: 0;
|
padding-top: 0;
|
||||||
padding-bottom: 0;
|
padding-bottom: 0;
|
||||||
border-bottom: none !important;
|
border-bottom: none;
|
||||||
border-top: 1px solid
|
border-top: 1px solid
|
||||||
var(--spectrum-table-border-color, var(--spectrum-alias-border-color-mid)) !important;
|
var(--spectrum-table-border-color, var(--spectrum-alias-border-color-mid));
|
||||||
border-radius: 0 !important;
|
border-radius: 0;
|
||||||
}
|
}
|
||||||
tr:first-child td {
|
tr:first-child td {
|
||||||
border-top: none !important;
|
border-top: none;
|
||||||
}
|
}
|
||||||
tr:last-child td {
|
tr:last-child td {
|
||||||
border-bottom: 1px solid
|
border-bottom: 1px solid
|
||||||
var(--spectrum-table-border-color, var(--spectrum-alias-border-color-mid)) !important;
|
var(--spectrum-table-border-color, var(--spectrum-alias-border-color-mid));
|
||||||
}
|
}
|
||||||
td.spectrum-Table-cell--divider {
|
td.spectrum-Table-cell--divider {
|
||||||
width: 1px;
|
width: 1px;
|
||||||
|
|
|
@ -15,6 +15,11 @@
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
p {
|
||||||
|
margin-top: 0.75em;
|
||||||
|
margin-bottom: 0.75em;
|
||||||
|
}
|
||||||
|
|
||||||
.noPadding {
|
.noPadding {
|
||||||
padding: 0;
|
padding: 0;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
|
|
|
@ -2407,10 +2407,10 @@ svelte-portal@^1.0.0:
|
||||||
resolved "https://registry.yarnpkg.com/svelte-portal/-/svelte-portal-1.0.0.tgz#36a47c5578b1a4d9b4dc60fa32a904640ec4cdd3"
|
resolved "https://registry.yarnpkg.com/svelte-portal/-/svelte-portal-1.0.0.tgz#36a47c5578b1a4d9b4dc60fa32a904640ec4cdd3"
|
||||||
integrity sha512-nHf+DS/jZ6jjnZSleBMSaZua9JlG5rZv9lOGKgJuaZStfevtjIlUJrkLc3vbV8QdBvPPVmvcjTlazAzfKu0v3Q==
|
integrity sha512-nHf+DS/jZ6jjnZSleBMSaZua9JlG5rZv9lOGKgJuaZStfevtjIlUJrkLc3vbV8QdBvPPVmvcjTlazAzfKu0v3Q==
|
||||||
|
|
||||||
svelte@^3.37.0:
|
svelte@^3.38.2:
|
||||||
version "3.37.0"
|
version "3.38.2"
|
||||||
resolved "https://registry.yarnpkg.com/svelte/-/svelte-3.37.0.tgz#dc7cd24bcc275cdb3f8c684ada89e50489144ccd"
|
resolved "https://registry.yarnpkg.com/svelte/-/svelte-3.38.2.tgz#55e5c681f793ae349b5cc2fe58e5782af4275ef5"
|
||||||
integrity sha512-TRF30F4W4+d+Jr2KzUUL1j8Mrpns/WM/WacxYlo5MMb2E5Qy2Pk1Guj6GylxsW9OnKQl1tnF8q3hG/hQ3h6VUA==
|
integrity sha512-q5Dq0/QHh4BLJyEVWGe7Cej5NWs040LWjMbicBGZ+3qpFWJ1YObRmUDZKbbovddLC9WW7THTj3kYbTOFmU9fbg==
|
||||||
|
|
||||||
svgo@^1.0.0:
|
svgo@^1.0.0:
|
||||||
version "1.3.2"
|
version "1.3.2"
|
||||||
|
|
|
@ -90,7 +90,7 @@
|
||||||
"@babel/preset-env": "^7.13.12",
|
"@babel/preset-env": "^7.13.12",
|
||||||
"@babel/runtime": "^7.13.10",
|
"@babel/runtime": "^7.13.10",
|
||||||
"@rollup/plugin-replace": "^2.4.2",
|
"@rollup/plugin-replace": "^2.4.2",
|
||||||
"@roxi/routify": "2.15.1",
|
"@roxi/routify": "2.18.0",
|
||||||
"@sveltejs/vite-plugin-svelte": "^1.0.0-next.5",
|
"@sveltejs/vite-plugin-svelte": "^1.0.0-next.5",
|
||||||
"@testing-library/jest-dom": "^5.11.10",
|
"@testing-library/jest-dom": "^5.11.10",
|
||||||
"@testing-library/svelte": "^3.0.0",
|
"@testing-library/svelte": "^3.0.0",
|
||||||
|
@ -106,7 +106,7 @@
|
||||||
"rollup": "^2.44.0",
|
"rollup": "^2.44.0",
|
||||||
"rollup-plugin-copy": "^3.4.0",
|
"rollup-plugin-copy": "^3.4.0",
|
||||||
"start-server-and-test": "^1.12.1",
|
"start-server-and-test": "^1.12.1",
|
||||||
"svelte": "^3.37.0",
|
"svelte": "^3.38.2",
|
||||||
"svelte-jester": "^1.3.2",
|
"svelte-jester": "^1.3.2",
|
||||||
"vite": "^2.1.5"
|
"vite": "^2.1.5"
|
||||||
},
|
},
|
||||||
|
|
|
@ -1,9 +1,10 @@
|
||||||
export const gradient = (node, config = {}) => {
|
export const gradient = (node, config = {}) => {
|
||||||
const defaultConfig = {
|
const defaultConfig = {
|
||||||
points: 10,
|
points: 12,
|
||||||
saturation: 0.8,
|
saturation: 0.85,
|
||||||
lightness: 0.75,
|
lightness: 0.7,
|
||||||
softness: 0.8,
|
softness: 0.9,
|
||||||
|
seed: null,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Applies a gradient background
|
// Applies a gradient background
|
||||||
|
@ -13,42 +14,67 @@ export const gradient = (node, config = {}) => {
|
||||||
...config,
|
...config,
|
||||||
}
|
}
|
||||||
const { saturation, lightness, softness, points } = config
|
const { saturation, lightness, softness, points } = config
|
||||||
|
const seed = config.seed || Math.random().toString(32).substring(2)
|
||||||
|
|
||||||
// Generates a random number between min and max
|
// Hash function which returns a fixed hash between specified limits
|
||||||
const rand = (min, max) => {
|
// for a given seed and a given version
|
||||||
return Math.round(min + Math.random() * (max - min))
|
const rangeHash = (seed, min = 0, max = 100, version = 0) => {
|
||||||
|
const range = max - min
|
||||||
|
let hash = range + version
|
||||||
|
for (let i = 0; i < seed.length * 2 + version; i++) {
|
||||||
|
hash = (hash << 5) - hash + seed.charCodeAt(i % seed.length)
|
||||||
|
hash = ((hash & hash) % range) + version
|
||||||
|
}
|
||||||
|
return min + (hash % range)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generates a random HSL colour using the options specified
|
// Generates a random HSL colour using the options specified
|
||||||
const randomHSL = () => {
|
const randomHSL = (seed, version, alpha = 1) => {
|
||||||
const lowerSaturation = Math.min(100, saturation * 100)
|
const lowerSaturation = Math.min(100, saturation * 100)
|
||||||
const upperSaturation = Math.min(100, (saturation + 0.2) * 100)
|
const upperSaturation = Math.min(100, (saturation + 0.2) * 100)
|
||||||
const lowerLightness = Math.min(100, lightness * 100)
|
const lowerLightness = Math.min(100, lightness * 100)
|
||||||
const upperLightness = Math.min(100, (lightness + 0.2) * 100)
|
const upperLightness = Math.min(100, (lightness + 0.2) * 100)
|
||||||
const hue = rand(0, 360)
|
const hue = rangeHash(seed, 0, 360, version)
|
||||||
const sat = `${rand(lowerSaturation, upperSaturation)}%`
|
const sat = `${rangeHash(
|
||||||
const light = `${rand(lowerLightness, upperLightness)}%`
|
seed,
|
||||||
return `hsl(${hue},${sat},${light})`
|
lowerSaturation,
|
||||||
|
upperSaturation,
|
||||||
|
version
|
||||||
|
)}%`
|
||||||
|
const light = `${rangeHash(
|
||||||
|
seed,
|
||||||
|
lowerLightness,
|
||||||
|
upperLightness,
|
||||||
|
version
|
||||||
|
)}%`
|
||||||
|
return `hsla(${hue},${sat},${light},${alpha})`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generates a radial gradient stop point
|
// Generates a radial gradient stop point
|
||||||
const randomGradientPoint = () => {
|
const randomGradientPoint = (seed, version) => {
|
||||||
const lowerTransparency = Math.min(100, softness * 100)
|
const lowerTransparency = Math.min(100, softness * 100)
|
||||||
const upperTransparency = Math.min(100, (softness + 0.2) * 100)
|
const upperTransparency = Math.min(100, (softness + 0.2) * 100)
|
||||||
const transparency = rand(lowerTransparency, upperTransparency)
|
const transparency = rangeHash(
|
||||||
|
seed,
|
||||||
|
lowerTransparency,
|
||||||
|
upperTransparency,
|
||||||
|
version
|
||||||
|
)
|
||||||
return (
|
return (
|
||||||
`radial-gradient(` +
|
`radial-gradient(at ` +
|
||||||
`at ${rand(10, 90)}% ${rand(10, 90)}%,` +
|
`${rangeHash(seed, 0, 100, version)}% ` +
|
||||||
`${randomHSL()} 0,` +
|
`${rangeHash(seed, 0, 100, version + 1)}%,` +
|
||||||
|
`${randomHSL(seed, version, saturation)} 0,` +
|
||||||
`transparent ${transparency}%)`
|
`transparent ${transparency}%)`
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
let css = `opacity:0.9;background-color:${randomHSL()};background-image:`
|
let css = `opacity:0.9;background:${randomHSL(seed, 0, 0.7)};`
|
||||||
|
css += "background-image:"
|
||||||
for (let i = 0; i < points - 1; i++) {
|
for (let i = 0; i < points - 1; i++) {
|
||||||
css += `${randomGradientPoint()},`
|
css += `${randomGradientPoint(seed, i)},`
|
||||||
}
|
}
|
||||||
css += `${randomGradientPoint()};`
|
css += `${randomGradientPoint(seed, points)};`
|
||||||
node.style = css
|
node.style = css
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -7,6 +7,10 @@
|
||||||
import WebhookDisplay from "../Shared/WebhookDisplay.svelte"
|
import WebhookDisplay from "../Shared/WebhookDisplay.svelte"
|
||||||
import DrawerBindableInput from "../../common/bindings/DrawerBindableInput.svelte"
|
import DrawerBindableInput from "../../common/bindings/DrawerBindableInput.svelte"
|
||||||
import AutomationBindingPanel from "../../common/bindings/ServerBindingPanel.svelte"
|
import AutomationBindingPanel from "../../common/bindings/ServerBindingPanel.svelte"
|
||||||
|
import CodeEditorModal from "./CodeEditorModal.svelte"
|
||||||
|
import QuerySelector from "./QuerySelector.svelte"
|
||||||
|
import QueryParamSelector from "./QueryParamSelector.svelte"
|
||||||
|
import Editor from "components/integration/QueryEditor.svelte"
|
||||||
|
|
||||||
export let block
|
export let block
|
||||||
export let webhookModal
|
export let webhookModal
|
||||||
|
@ -70,6 +74,10 @@
|
||||||
on:change={e => (block.inputs[key] = e.detail)}
|
on:change={e => (block.inputs[key] = e.detail)}
|
||||||
{bindings}
|
{bindings}
|
||||||
/>
|
/>
|
||||||
|
{:else if value.customType === "query"}
|
||||||
|
<QuerySelector bind:value={block.inputs[key]} />
|
||||||
|
{:else if value.customType === "queryParams"}
|
||||||
|
<QueryParamSelector bind:value={block.inputs[key]} {bindings} />
|
||||||
{:else if value.customType === "table"}
|
{:else if value.customType === "table"}
|
||||||
<TableSelector bind:value={block.inputs[key]} />
|
<TableSelector bind:value={block.inputs[key]} />
|
||||||
{:else if value.customType === "row"}
|
{:else if value.customType === "row"}
|
||||||
|
@ -78,6 +86,17 @@
|
||||||
<WebhookDisplay value={block.inputs[key]} />
|
<WebhookDisplay value={block.inputs[key]} />
|
||||||
{:else if value.customType === "triggerSchema"}
|
{:else if value.customType === "triggerSchema"}
|
||||||
<SchemaSetup bind:value={block.inputs[key]} />
|
<SchemaSetup bind:value={block.inputs[key]} />
|
||||||
|
{:else if value.customType === "code"}
|
||||||
|
<CodeEditorModal>
|
||||||
|
<pre>{JSON.stringify(bindings, null, 2)}</pre>
|
||||||
|
<Editor
|
||||||
|
mode="javascript"
|
||||||
|
on:change={e => {
|
||||||
|
block.inputs[key] = e.detail.value
|
||||||
|
}}
|
||||||
|
value={block.inputs[key]}
|
||||||
|
/>
|
||||||
|
</CodeEditorModal>
|
||||||
{:else if value.type === "string" || value.type === "number"}
|
{:else if value.type === "string" || value.type === "number"}
|
||||||
<DrawerBindableInput
|
<DrawerBindableInput
|
||||||
panel={AutomationBindingPanel}
|
panel={AutomationBindingPanel}
|
||||||
|
|
|
@ -11,8 +11,9 @@
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Modal bind:this={modal} width="60%">
|
<Modal bind:this={modal}>
|
||||||
<ModalContent
|
<ModalContent
|
||||||
|
size="XL"
|
||||||
title="Edit Code"
|
title="Edit Code"
|
||||||
showConfirmButton={false}
|
showConfirmButton={false}
|
||||||
showCancelButton={false}
|
showCancelButton={false}
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
<script>
|
<script>
|
||||||
import { queries } from "stores/backend"
|
import { queries } from "stores/backend"
|
||||||
import { Select } from "@budibase/bbui"
|
import { Select } from "@budibase/bbui"
|
||||||
import DrawerBindableInput from "../../common/DrawerBindableInput.svelte"
|
import DrawerBindableInput from "../../common/bindings/DrawerBindableInput.svelte"
|
||||||
import AutomationBindingPanel from "./AutomationBindingPanel.svelte"
|
import AutomationBindingPanel from "../../common/bindings/ServerBindingPanel.svelte"
|
||||||
|
|
||||||
export let value
|
export let value
|
||||||
export let bindings
|
export let bindings
|
||||||
|
@ -13,7 +13,6 @@
|
||||||
// Ensure any nullish queryId values get set to empty string so
|
// Ensure any nullish queryId values get set to empty string so
|
||||||
// that the select works
|
// that the select works
|
||||||
$: if (value?.queryId == null) value = { queryId: "" }
|
$: if (value?.queryId == null) value = { queryId: "" }
|
||||||
$: console.log("daValuz", value)
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="block-field">
|
<div class="block-field">
|
||||||
|
|
|
@ -7,6 +7,7 @@
|
||||||
export let cancelText = "Cancel"
|
export let cancelText = "Cancel"
|
||||||
export let onOk = undefined
|
export let onOk = undefined
|
||||||
export let onCancel = undefined
|
export let onCancel = undefined
|
||||||
|
export let warning = true
|
||||||
|
|
||||||
let modal
|
let modal
|
||||||
|
|
||||||
|
@ -19,7 +20,13 @@
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Modal bind:this={modal} on:hide={onCancel}>
|
<Modal bind:this={modal} on:hide={onCancel}>
|
||||||
<ModalContent onConfirm={onOk} {title} confirmText={okText} {cancelText} red>
|
<ModalContent
|
||||||
|
onConfirm={onOk}
|
||||||
|
{title}
|
||||||
|
confirmText={okText}
|
||||||
|
{cancelText}
|
||||||
|
{warning}
|
||||||
|
>
|
||||||
<Body size="S">
|
<Body size="S">
|
||||||
{body}
|
{body}
|
||||||
<slot />
|
<slot />
|
||||||
|
|
|
@ -117,7 +117,7 @@
|
||||||
readOnly,
|
readOnly,
|
||||||
autoCloseBrackets: true,
|
autoCloseBrackets: true,
|
||||||
autoCloseTags: true,
|
autoCloseTags: true,
|
||||||
theme: $themeStore.darkMode ? THEMES.DARK : THEMES.LIGHT,
|
theme: THEMES.DARK,
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!tab)
|
if (!tab)
|
||||||
|
|
|
@ -7,56 +7,49 @@
|
||||||
ActionMenu,
|
ActionMenu,
|
||||||
MenuItem,
|
MenuItem,
|
||||||
Link,
|
Link,
|
||||||
notifications,
|
|
||||||
} from "@budibase/bbui"
|
} from "@budibase/bbui"
|
||||||
import download from "downloadjs"
|
|
||||||
import { gradient } from "actions"
|
import { gradient } from "actions"
|
||||||
|
import { url } from "@roxi/routify"
|
||||||
|
|
||||||
export let name
|
export let app
|
||||||
export let _id
|
export let exportApp
|
||||||
|
export let deleteApp
|
||||||
let appExportLoading = false
|
|
||||||
|
|
||||||
async function exportApp() {
|
|
||||||
appExportLoading = true
|
|
||||||
try {
|
|
||||||
download(
|
|
||||||
`/api/backups/export?appId=${_id}&appname=${encodeURIComponent(name)}`
|
|
||||||
)
|
|
||||||
notifications.success("App export complete")
|
|
||||||
} catch (err) {
|
|
||||||
console.error(err)
|
|
||||||
notifications.error("App export failed")
|
|
||||||
} finally {
|
|
||||||
appExportLoading = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Layout noPadding gap="XS">
|
<div class="wrapper">
|
||||||
<div class="preview" use:gradient />
|
<Layout noPadding gap="XS" alignContent="start">
|
||||||
<div class="title">
|
<div class="preview" use:gradient={{ seed: app.name }} />
|
||||||
<Link href={`/builder/app/${_id}`}>
|
<div class="title">
|
||||||
<Heading size="XS">
|
<Link href={$url(`../../app/${app._id}`)}>
|
||||||
{name}
|
<Heading size="XS">
|
||||||
</Heading>
|
{app.name}
|
||||||
</Link>
|
</Heading>
|
||||||
<ActionMenu>
|
</Link>
|
||||||
<Icon slot="control" name="More" hoverable />
|
<ActionMenu align="right">
|
||||||
<MenuItem on:click={exportApp} icon="Download">Export</MenuItem>
|
<Icon slot="control" name="More" hoverable />
|
||||||
</ActionMenu>
|
<MenuItem on:click={() => exportApp(app)} icon="Download">
|
||||||
</div>
|
Export
|
||||||
<div class="status">
|
</MenuItem>
|
||||||
<Body noPadding size="S">
|
<MenuItem on:click={() => deleteApp(app)} icon="Delete">
|
||||||
Edited {Math.floor(1 + Math.random() * 10)} months ago
|
Delete
|
||||||
</Body>
|
</MenuItem>
|
||||||
{#if Math.random() > 0.5}
|
</ActionMenu>
|
||||||
<Icon name="LockClosed" />
|
</div>
|
||||||
{/if}
|
<div class="status">
|
||||||
</div>
|
<Body noPadding size="S">
|
||||||
</Layout>
|
Edited {Math.floor(1 + Math.random() * 10)} months ago
|
||||||
|
</Body>
|
||||||
|
{#if Math.random() > 0.5}
|
||||||
|
<Icon name="LockClosed" />
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</Layout>
|
||||||
|
</div>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
.wrapper {
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
.preview {
|
.preview {
|
||||||
height: 135px;
|
height: 135px;
|
||||||
border-radius: var(--border-radius-s);
|
border-radius: var(--border-radius-s);
|
||||||
|
@ -73,6 +66,15 @@
|
||||||
|
|
||||||
.title :global(a) {
|
.title :global(a) {
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
|
flex: 1 1 auto;
|
||||||
|
width: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
margin-right: var(--spacing-m);
|
||||||
|
}
|
||||||
|
.title :global(h1) {
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
}
|
}
|
||||||
.title :global(h1:hover) {
|
.title :global(h1:hover) {
|
||||||
color: var(--spectrum-global-color-blue-600);
|
color: var(--spectrum-global-color-blue-600);
|
||||||
|
|
|
@ -1,25 +0,0 @@
|
||||||
<script>
|
|
||||||
import { onMount } from "svelte"
|
|
||||||
import AppCard from "./AppCard.svelte"
|
|
||||||
import { apps } from "stores/portal"
|
|
||||||
|
|
||||||
onMount(apps.load)
|
|
||||||
</script>
|
|
||||||
|
|
||||||
{#if $apps.length}
|
|
||||||
<div class="appList">
|
|
||||||
{#each $apps as app}
|
|
||||||
<AppCard {...app} />
|
|
||||||
{/each}
|
|
||||||
</div>
|
|
||||||
{:else}
|
|
||||||
<div>No apps found.</div>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
<style>
|
|
||||||
.appList {
|
|
||||||
display: grid;
|
|
||||||
grid-gap: 50px;
|
|
||||||
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
|
|
||||||
}
|
|
||||||
</style>
|
|
|
@ -0,0 +1,80 @@
|
||||||
|
<script>
|
||||||
|
import { gradient } from "actions"
|
||||||
|
import {
|
||||||
|
Heading,
|
||||||
|
Button,
|
||||||
|
Icon,
|
||||||
|
ActionMenu,
|
||||||
|
MenuItem,
|
||||||
|
Link,
|
||||||
|
} from "@budibase/bbui"
|
||||||
|
import { url } from "@roxi/routify"
|
||||||
|
|
||||||
|
export let app
|
||||||
|
export let openApp
|
||||||
|
export let exportApp
|
||||||
|
export let deleteApp
|
||||||
|
export let last
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="title" class:last>
|
||||||
|
<div class="preview" use:gradient={{ seed: app.name }} />
|
||||||
|
<Link href={$url(`../../app/${app._id}`)}>
|
||||||
|
<Heading size="XS">
|
||||||
|
{app.name}
|
||||||
|
</Heading>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
<div class:last>
|
||||||
|
Edited {Math.round(Math.random() * 10 + 1)} months ago
|
||||||
|
</div>
|
||||||
|
<div class:last>
|
||||||
|
{#if Math.random() < 0.33}
|
||||||
|
<div class="status status--open" />
|
||||||
|
Open
|
||||||
|
{:else if Math.random() < 0.33}
|
||||||
|
<div class="status status--locked-other" />
|
||||||
|
Locked by Will Wheaton
|
||||||
|
{:else}
|
||||||
|
<div class="status status--locked-you" />
|
||||||
|
Locked by you
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
<div class:last>
|
||||||
|
<Button on:click={() => openApp(app)} size="S" secondary>Open</Button>
|
||||||
|
<ActionMenu align="right">
|
||||||
|
<Icon hoverable slot="control" name="More" />
|
||||||
|
<MenuItem on:click={() => exportApp(app)} icon="Download">Export</MenuItem>
|
||||||
|
<MenuItem on:click={() => deleteApp(app)} icon="Delete">Delete</MenuItem>
|
||||||
|
</ActionMenu>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.preview {
|
||||||
|
height: 40px;
|
||||||
|
width: 40px;
|
||||||
|
border-radius: var(--border-radius-s);
|
||||||
|
}
|
||||||
|
.title :global(a) {
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
.title :global(h1:hover) {
|
||||||
|
color: var(--spectrum-global-color-blue-600);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: color 130ms ease;
|
||||||
|
}
|
||||||
|
.status {
|
||||||
|
height: 10px;
|
||||||
|
width: 10px;
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
.status--locked-you {
|
||||||
|
background-color: var(--spectrum-global-color-orange-600);
|
||||||
|
}
|
||||||
|
.status--locked-other {
|
||||||
|
background-color: var(--spectrum-global-color-red-600);
|
||||||
|
}
|
||||||
|
.status--open {
|
||||||
|
background-color: var(--spectrum-global-color-green-600);
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -1,52 +1,48 @@
|
||||||
<script>
|
<script>
|
||||||
import { writable, get as svelteGet } from "svelte/store"
|
import { writable, get as svelteGet } from "svelte/store"
|
||||||
import { notifications, Heading, Button } from "@budibase/bbui"
|
import {
|
||||||
|
notifications,
|
||||||
|
Input,
|
||||||
|
ModalContent,
|
||||||
|
Dropzone,
|
||||||
|
Body,
|
||||||
|
Checkbox,
|
||||||
|
} from "@budibase/bbui"
|
||||||
import { store, automationStore, hostingStore } from "builderStore"
|
import { store, automationStore, hostingStore } from "builderStore"
|
||||||
import { string, object } from "yup"
|
import { string, mixed, object } from "yup"
|
||||||
import api, { get } from "builderStore/api"
|
import api, { get, post } from "builderStore/api"
|
||||||
import Spinner from "components/common/Spinner.svelte"
|
|
||||||
import { Info, User } from "./Steps"
|
|
||||||
import Indicator from "./Indicator.svelte"
|
|
||||||
import { post } from "builderStore/api"
|
|
||||||
import analytics from "analytics"
|
import analytics from "analytics"
|
||||||
import { onMount } from "svelte"
|
import { onMount } from "svelte"
|
||||||
import Logo from "/assets/bb-logo.svg"
|
import { capitalise } from "helpers"
|
||||||
import { capitalise } from "../../helpers"
|
import { goto } from "@roxi/routify"
|
||||||
|
|
||||||
export let template
|
export let template
|
||||||
|
|
||||||
const currentStep = writable(0)
|
const values = writable({ name: null })
|
||||||
const values = writable({ roleId: "ADMIN" })
|
|
||||||
const errors = writable({})
|
const errors = writable({})
|
||||||
const touched = writable({})
|
const touched = writable({})
|
||||||
const steps = [Info, User]
|
const validator = {
|
||||||
let validators = [
|
name: string().required("Your application must have a name"),
|
||||||
{
|
file: template ? mixed().required("Please choose a file to import") : null,
|
||||||
applicationName: string().required("Your application must have a name"),
|
}
|
||||||
},
|
|
||||||
{
|
|
||||||
roleId: string()
|
|
||||||
.nullable()
|
|
||||||
.required("You need to select a role for this app"),
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
let submitting = false
|
let submitting = false
|
||||||
let valid = false
|
let valid = false
|
||||||
$: checkValidity($values, validators[$currentStep])
|
$: checkValidity($values, validator)
|
||||||
|
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
await hostingStore.actions.fetchDeployedApps()
|
await hostingStore.actions.fetchDeployedApps()
|
||||||
const existingAppNames = svelteGet(hostingStore).deployedAppNames
|
const existingAppNames = svelteGet(hostingStore).deployedAppNames
|
||||||
validators[0].applicationName = string()
|
validator.name = string()
|
||||||
.required("Your application must have a name.")
|
.required("Your application must have a name")
|
||||||
.test(
|
.test(
|
||||||
"non-existing-app-name",
|
"non-existing-app-name",
|
||||||
"App with same name already exists. Please try another app name.",
|
"Another app with the same name already exists",
|
||||||
value =>
|
value => {
|
||||||
!existingAppNames.some(
|
return !existingAppNames.some(
|
||||||
appName => appName.toLowerCase() === value.toLowerCase()
|
appName => appName.toLowerCase() === value.toLowerCase()
|
||||||
)
|
)
|
||||||
|
}
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -65,15 +61,24 @@
|
||||||
|
|
||||||
async function createNewApp() {
|
async function createNewApp() {
|
||||||
submitting = true
|
submitting = true
|
||||||
|
|
||||||
|
// Check a template exists if we are important
|
||||||
|
if (template && !$values.file) {
|
||||||
|
$errors.file = "Please choose a file to import"
|
||||||
|
valid = false
|
||||||
|
submitting = false
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Create form data to create app
|
// Create form data to create app
|
||||||
let data = new FormData()
|
let data = new FormData()
|
||||||
data.append("name", $values.applicationName)
|
data.append("name", $values.name)
|
||||||
data.append("useTemplate", template != null)
|
data.append("useTemplate", template != null)
|
||||||
if (template) {
|
if (template) {
|
||||||
data.append("templateName", template.name)
|
data.append("templateName", template.name)
|
||||||
data.append("templateKey", template.key)
|
data.append("templateKey", template.key)
|
||||||
data.append("templateFile", template.file)
|
data.append("templateFile", $values.file)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create App
|
// Create App
|
||||||
|
@ -84,7 +89,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
analytics.captureEvent("App Created", {
|
analytics.captureEvent("App Created", {
|
||||||
name: $values.applicationName,
|
name: $values.name,
|
||||||
appId: appJson._id,
|
appId: appJson._id,
|
||||||
template,
|
template,
|
||||||
})
|
})
|
||||||
|
@ -107,7 +112,7 @@
|
||||||
}
|
}
|
||||||
const userResp = await api.post(`/api/users/metadata/self`, user)
|
const userResp = await api.post(`/api/users/metadata/self`, user)
|
||||||
await userResp.json()
|
await userResp.json()
|
||||||
window.location = `/builder/app/${appJson._id}`
|
$goto(`/builder/app/${appJson._id}`)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error)
|
console.error(error)
|
||||||
notifications.error(error)
|
notifications.error(error)
|
||||||
|
@ -116,129 +121,33 @@
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="container">
|
<ModalContent
|
||||||
<div class="sidebar">
|
title={template ? "Import app" : "Create new app"}
|
||||||
<img src={Logo} alt="budibase icon" />
|
confirmText={template ? "Import app" : "Create app"}
|
||||||
<div class="steps">
|
onConfirm={createNewApp}
|
||||||
{#each steps as component, i}
|
disabled={!valid}
|
||||||
<Indicator
|
>
|
||||||
active={$currentStep === i}
|
{#if template}
|
||||||
done={i < $currentStep}
|
<Dropzone
|
||||||
step={i + 1}
|
error={$touched.file && $errors.file}
|
||||||
/>
|
gallery={false}
|
||||||
{/each}
|
label="File to import"
|
||||||
</div>
|
value={[$values.file]}
|
||||||
</div>
|
on:change={e => {
|
||||||
<div class="body">
|
$values.file = e.detail?.[0]
|
||||||
<div class="heading">
|
$touched.file = true
|
||||||
<Heading size="L">Get started with Budibase</Heading>
|
}}
|
||||||
</div>
|
/>
|
||||||
<div class="step">
|
|
||||||
{#each steps as component, i (i)}
|
|
||||||
<div class:hidden={$currentStep !== i}>
|
|
||||||
<svelte:component
|
|
||||||
this={component}
|
|
||||||
{template}
|
|
||||||
{values}
|
|
||||||
{errors}
|
|
||||||
{touched}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
{/each}
|
|
||||||
</div>
|
|
||||||
<div class="footer">
|
|
||||||
{#if $currentStep > 0}
|
|
||||||
<Button medium secondary on:click={() => $currentStep--}>Back</Button>
|
|
||||||
{/if}
|
|
||||||
{#if $currentStep < steps.length - 1}
|
|
||||||
<Button medium cta on:click={() => $currentStep++} disabled={!valid}>
|
|
||||||
Next
|
|
||||||
</Button>
|
|
||||||
{/if}
|
|
||||||
{#if $currentStep === steps.length - 1}
|
|
||||||
<Button
|
|
||||||
medium
|
|
||||||
cta
|
|
||||||
on:click={createNewApp}
|
|
||||||
disabled={!valid || submitting}
|
|
||||||
>
|
|
||||||
{submitting ? "Loading..." : "Submit"}
|
|
||||||
</Button>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{#if submitting}
|
|
||||||
<div in:fade class="spinner-container">
|
|
||||||
<Spinner />
|
|
||||||
<span class="spinner-text">Creating your app...</span>
|
|
||||||
</div>
|
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
<Body size="S">
|
||||||
|
Give your new app a name, and choose which groups have access (paid plans
|
||||||
<style>
|
only).
|
||||||
.container {
|
</Body>
|
||||||
min-height: 600px;
|
<Input
|
||||||
display: grid;
|
bind:value={$values.name}
|
||||||
grid-template-columns: 80px 1fr;
|
error={$touched.name && $errors.name}
|
||||||
position: relative;
|
on:blur={() => ($touched.name = true)}
|
||||||
}
|
label="Name"
|
||||||
.sidebar {
|
/>
|
||||||
display: flex;
|
<Checkbox label="Group access" disabled value={true} text="All users" />
|
||||||
flex-direction: column;
|
</ModalContent>
|
||||||
justify-content: flex-start;
|
|
||||||
align-items: stretch;
|
|
||||||
padding: 40px 0;
|
|
||||||
background: var(--grey-1);
|
|
||||||
}
|
|
||||||
.steps {
|
|
||||||
flex: 1 1 auto;
|
|
||||||
display: grid;
|
|
||||||
border-bottom-left-radius: 0.5rem;
|
|
||||||
border-top-left-radius: 0.5rem;
|
|
||||||
grid-gap: 30px;
|
|
||||||
align-content: center;
|
|
||||||
}
|
|
||||||
.heading {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
align-items: center;
|
|
||||||
margin-bottom: 20px;
|
|
||||||
}
|
|
||||||
.body {
|
|
||||||
padding: 40px 60px 40px 60px;
|
|
||||||
display: grid;
|
|
||||||
align-items: center;
|
|
||||||
grid-template-rows: auto 1fr auto;
|
|
||||||
}
|
|
||||||
.footer {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
justify-content: flex-end;
|
|
||||||
align-items: center;
|
|
||||||
gap: 15px;
|
|
||||||
}
|
|
||||||
.spinner-container {
|
|
||||||
background: var(--background);
|
|
||||||
position: absolute;
|
|
||||||
border-radius: 5px;
|
|
||||||
left: 0;
|
|
||||||
top: 0;
|
|
||||||
right: 0;
|
|
||||||
bottom: 0;
|
|
||||||
display: grid;
|
|
||||||
justify-items: center;
|
|
||||||
align-content: center;
|
|
||||||
grid-gap: 50px;
|
|
||||||
}
|
|
||||||
.spinner-text {
|
|
||||||
font-size: 2em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hidden {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
img {
|
|
||||||
height: 40px;
|
|
||||||
margin-bottom: 20px;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
|
@ -1,83 +0,0 @@
|
||||||
<script>
|
|
||||||
export let step, done, active
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<div class="container" class:active class:done>
|
|
||||||
<div class="circle" class:active class:done>
|
|
||||||
{#if done}
|
|
||||||
<svg
|
|
||||||
width="12"
|
|
||||||
height="10"
|
|
||||||
viewBox="0 0 12 10"
|
|
||||||
fill="none"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
fill-rule="evenodd"
|
|
||||||
clip-rule="evenodd"
|
|
||||||
d="M10.1212 0.319527C10.327 0.115582 10.6047 0.000803464 10.8944
|
|
||||||
4.20219e-06C11.1841 -0.00079506 11.4624 0.11245 11.6693
|
|
||||||
0.315256C11.8762 0.518062 11.9949 0.794134 11.9998 1.08379C12.0048
|
|
||||||
1.37344 11.8955 1.65339 11.6957 1.86313L5.82705 9.19893C5.72619
|
|
||||||
9.30757 5.60445 9.39475 5.46913 9.45527C5.3338 9.51578 5.18766 9.54839
|
|
||||||
5.03944 9.55113C4.89123 9.55388 4.74398 9.52671 4.60651
|
|
||||||
9.47124C4.46903 9.41578 4.34416 9.33316 4.23934 9.22833L0.350925
|
|
||||||
5.33845C0.242598 5.23751 0.155712 5.11578 0.0954499 4.98054C0.0351876
|
|
||||||
4.84529 0.00278364 4.69929 0.00017159 4.55124C-0.00244046 4.4032
|
|
||||||
0.024793 4.25615 0.0802466 4.11886C0.1357 3.98157 0.218238 3.85685
|
|
||||||
0.322937 3.75215C0.427636 3.64746 0.55235 3.56492 0.68964
|
|
||||||
3.50946C0.82693 3.45401 0.973983 3.42678 1.12203 3.42939C1.27007 3.432
|
|
||||||
1.41607 3.46441 1.55132 3.52467C1.68657 3.58493 1.80829 3.67182
|
|
||||||
1.90923 3.78014L4.98762 6.85706L10.0933 0.35187C10.1024 0.340482
|
|
||||||
10.1122 0.329679 10.1227 0.319527H10.1212Z"
|
|
||||||
fill="var(--background)"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
{:else}{step}{/if}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
.container::before {
|
|
||||||
content: "";
|
|
||||||
position: absolute;
|
|
||||||
top: -30px;
|
|
||||||
width: 1px;
|
|
||||||
height: 30px;
|
|
||||||
background: var(--grey-5);
|
|
||||||
}
|
|
||||||
.container:first-child::before {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
.container {
|
|
||||||
position: relative;
|
|
||||||
height: 45px;
|
|
||||||
display: grid;
|
|
||||||
place-items: center;
|
|
||||||
}
|
|
||||||
.container.active {
|
|
||||||
box-shadow: inset 3px 0 0 0 var(--blue);
|
|
||||||
}
|
|
||||||
.circle.active {
|
|
||||||
background: var(--blue);
|
|
||||||
color: white;
|
|
||||||
border: none;
|
|
||||||
}
|
|
||||||
.circle.done {
|
|
||||||
background: var(--grey-5);
|
|
||||||
color: white;
|
|
||||||
border: none;
|
|
||||||
}
|
|
||||||
.circle {
|
|
||||||
color: var(--grey-5);
|
|
||||||
font-size: 14px;
|
|
||||||
font-weight: 500;
|
|
||||||
display: grid;
|
|
||||||
place-items: center;
|
|
||||||
width: 30px;
|
|
||||||
height: 30px;
|
|
||||||
border-radius: 50%;
|
|
||||||
border: 1px solid var(--grey-5);
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
</style>
|
|
|
@ -1,121 +0,0 @@
|
||||||
<script>
|
|
||||||
import { Label, Heading, Input, notifications } from "@budibase/bbui"
|
|
||||||
|
|
||||||
const BYTES_IN_MB = 1000000
|
|
||||||
const FILE_SIZE_LIMIT = BYTES_IN_MB * 5
|
|
||||||
|
|
||||||
export let template
|
|
||||||
export let values
|
|
||||||
export let errors
|
|
||||||
export let touched
|
|
||||||
|
|
||||||
let blurred = { appName: false }
|
|
||||||
let file
|
|
||||||
|
|
||||||
function handleFile(evt) {
|
|
||||||
const fileArray = Array.from(evt.target.files)
|
|
||||||
if (fileArray.some(file => file.size >= FILE_SIZE_LIMIT)) {
|
|
||||||
notifications.error(
|
|
||||||
`Files cannot exceed ${
|
|
||||||
FILE_SIZE_LIMIT / BYTES_IN_MB
|
|
||||||
}MB. Please try again with smaller files.`
|
|
||||||
)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
file = evt.target.files[0]
|
|
||||||
template.file = file
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<div class="container">
|
|
||||||
{#if template?.fromFile}
|
|
||||||
<Heading size="L">Import your Web App</Heading>
|
|
||||||
{:else}
|
|
||||||
<Heading size="L">Create your Web App</Heading>
|
|
||||||
{/if}
|
|
||||||
{#if template?.fromFile}
|
|
||||||
<div class="template">
|
|
||||||
<Label extraSmall grey>Import File</Label>
|
|
||||||
<div class="dropzone">
|
|
||||||
<input
|
|
||||||
id="file-upload"
|
|
||||||
accept=".txt"
|
|
||||||
type="file"
|
|
||||||
on:change={handleFile}
|
|
||||||
/>
|
|
||||||
<label for="file-upload" class:uploaded={file}>
|
|
||||||
{#if file}{file.name}{:else}Import{/if}
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{:else if template}
|
|
||||||
<div class="template">
|
|
||||||
<Label extraSmall grey>Selected Template</Label>
|
|
||||||
<Heading size="S">{template.name}</Heading>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
<Input
|
|
||||||
on:change={() => ($touched.applicationName = true)}
|
|
||||||
bind:value={$values.applicationName}
|
|
||||||
label="Web App Name"
|
|
||||||
placeholder="Enter name of your web application"
|
|
||||||
error={$touched.applicationName && $errors.applicationName}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
.container {
|
|
||||||
display: grid;
|
|
||||||
grid-gap: var(--spacing-xl);
|
|
||||||
margin-top: var(--spacing-xl);
|
|
||||||
}
|
|
||||||
|
|
||||||
.template :global(label) {
|
|
||||||
/* Fix layout due to LH 0 on heading */
|
|
||||||
margin-bottom: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dropzone {
|
|
||||||
text-align: center;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
flex-direction: column;
|
|
||||||
border-radius: 10px;
|
|
||||||
transition: all 0.3s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.uploaded {
|
|
||||||
color: var(--blue);
|
|
||||||
}
|
|
||||||
|
|
||||||
input[type="file"] {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
label {
|
|
||||||
font-family: var(--font-sans);
|
|
||||||
cursor: pointer;
|
|
||||||
font-weight: 500;
|
|
||||||
box-sizing: border-box;
|
|
||||||
overflow: hidden;
|
|
||||||
border-radius: var(--border-radius-s);
|
|
||||||
color: var(--ink);
|
|
||||||
padding: var(--spacing-m) var(--spacing-l);
|
|
||||||
transition: all 0.2s ease 0s;
|
|
||||||
display: inline-flex;
|
|
||||||
text-rendering: optimizeLegibility;
|
|
||||||
min-width: auto;
|
|
||||||
outline: none;
|
|
||||||
font-feature-settings: "case" 1, "rlig" 1, "calt" 0;
|
|
||||||
-webkit-box-align: center;
|
|
||||||
user-select: none;
|
|
||||||
flex-shrink: 0;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
width: 100%;
|
|
||||||
background-color: var(--grey-2);
|
|
||||||
font-size: var(--font-size-xs);
|
|
||||||
line-height: normal;
|
|
||||||
border: var(--border-transparent);
|
|
||||||
}
|
|
||||||
</style>
|
|
|
@ -1,29 +0,0 @@
|
||||||
<script>
|
|
||||||
import { Select, Heading } from "@budibase/bbui"
|
|
||||||
|
|
||||||
export let values
|
|
||||||
export let errors
|
|
||||||
export let touched
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<div class="container">
|
|
||||||
<Heading size="L">What's your role for this app?</Heading>
|
|
||||||
<Select
|
|
||||||
bind:value={$values.roleId}
|
|
||||||
label="Role"
|
|
||||||
options={[
|
|
||||||
{ label: "Admin", value: "ADMIN" },
|
|
||||||
{ label: "Power User", value: "POWER_USER" },
|
|
||||||
]}
|
|
||||||
getOptionLabel={option => option.label}
|
|
||||||
getOptionValue={option => option.value}
|
|
||||||
error={$errors.roleId}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
.container {
|
|
||||||
display: grid;
|
|
||||||
grid-gap: var(--spacing-xl);
|
|
||||||
}
|
|
||||||
</style>
|
|
|
@ -1,2 +0,0 @@
|
||||||
export { default as Info } from "./Info.svelte"
|
|
||||||
export { default as User } from "./User.svelte"
|
|
|
@ -17,9 +17,6 @@
|
||||||
import { auth } from "stores/backend"
|
import { auth } from "stores/backend"
|
||||||
import BuilderSettingsModal from "components/start/BuilderSettingsModal.svelte"
|
import BuilderSettingsModal from "components/start/BuilderSettingsModal.svelte"
|
||||||
|
|
||||||
organisation.init()
|
|
||||||
apps.load()
|
|
||||||
|
|
||||||
let orgName
|
let orgName
|
||||||
let orgLogo
|
let orgLogo
|
||||||
let user
|
let user
|
||||||
|
@ -32,7 +29,10 @@
|
||||||
user = { name: "John Doe" }
|
user = { name: "John Doe" }
|
||||||
}
|
}
|
||||||
|
|
||||||
onMount(getInfo)
|
onMount(() => {
|
||||||
|
organisation.init()
|
||||||
|
getInfo()
|
||||||
|
})
|
||||||
|
|
||||||
let menu = [
|
let menu = [
|
||||||
{ title: "Apps", href: "/builder/portal/apps" },
|
{ title: "Apps", href: "/builder/portal/apps" },
|
||||||
|
|
|
@ -1,7 +0,0 @@
|
||||||
<script>
|
|
||||||
import { Page } from "@budibase/bbui"
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<Page wide>
|
|
||||||
<slot />
|
|
||||||
</Page>
|
|
|
@ -8,18 +8,31 @@
|
||||||
ButtonGroup,
|
ButtonGroup,
|
||||||
Select,
|
Select,
|
||||||
Modal,
|
Modal,
|
||||||
|
ModalContent,
|
||||||
|
Page,
|
||||||
|
notifications,
|
||||||
|
Body,
|
||||||
} from "@budibase/bbui"
|
} from "@budibase/bbui"
|
||||||
import AppList from "components/start/AppList.svelte"
|
|
||||||
import CreateAppModal from "components/start/CreateAppModal.svelte"
|
import CreateAppModal from "components/start/CreateAppModal.svelte"
|
||||||
import api from "builderStore/api"
|
import api, { del } from "builderStore/api"
|
||||||
import analytics from "analytics"
|
import analytics from "analytics"
|
||||||
import { onMount } from "svelte"
|
import { onMount } from "svelte"
|
||||||
|
import { apps } from "stores/portal"
|
||||||
|
import download from "downloadjs"
|
||||||
|
import { goto } from "@roxi/routify"
|
||||||
|
import ConfirmDialog from "components/common/ConfirmDialog.svelte"
|
||||||
|
import AppCard from "components/start/AppCard.svelte"
|
||||||
|
import AppRow from "components/start/AppRow.svelte"
|
||||||
|
|
||||||
let layout = "grid"
|
let layout = "grid"
|
||||||
let modal
|
|
||||||
let template
|
let template
|
||||||
|
let appToDelete
|
||||||
|
let creationModal
|
||||||
|
let deletionModal
|
||||||
|
let creatingApp = false
|
||||||
|
let loaded = false
|
||||||
|
|
||||||
async function checkKeys() {
|
const checkKeys = async () => {
|
||||||
const response = await api.get(`/api/keys/`)
|
const response = await api.get(`/api/keys/`)
|
||||||
const keys = await response.json()
|
const keys = await response.json()
|
||||||
if (keys.userId) {
|
if (keys.userId) {
|
||||||
|
@ -27,55 +40,146 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function initiateAppImport() {
|
const initiateAppCreation = () => {
|
||||||
template = { fromFile: true }
|
creationModal.show()
|
||||||
modal.show()
|
creatingApp = true
|
||||||
}
|
}
|
||||||
|
|
||||||
onMount(checkKeys)
|
const initiateAppImport = () => {
|
||||||
|
template = { fromFile: true }
|
||||||
|
creationModal.show()
|
||||||
|
creatingApp = true
|
||||||
|
}
|
||||||
|
|
||||||
|
const stopAppCreation = () => {
|
||||||
|
template = null
|
||||||
|
creatingApp = false
|
||||||
|
}
|
||||||
|
|
||||||
|
const openApp = app => {
|
||||||
|
$goto(`../../app/${app._id}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const exportApp = app => {
|
||||||
|
try {
|
||||||
|
download(
|
||||||
|
`/api/backups/export?appId=${app._id}&appname=${encodeURIComponent(
|
||||||
|
app.name
|
||||||
|
)}`
|
||||||
|
)
|
||||||
|
notifications.success("App export complete")
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err)
|
||||||
|
notifications.error("App export failed")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const deleteApp = app => {
|
||||||
|
appToDelete = app
|
||||||
|
deletionModal.show()
|
||||||
|
}
|
||||||
|
|
||||||
|
const confirmDeleteApp = async () => {
|
||||||
|
if (!appToDelete) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
await del(`/api/applications/${appToDelete?._id}`)
|
||||||
|
await apps.load()
|
||||||
|
appToDelete = null
|
||||||
|
}
|
||||||
|
|
||||||
|
onMount(async () => {
|
||||||
|
checkKeys()
|
||||||
|
await apps.load()
|
||||||
|
loaded = true
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Layout noPadding>
|
<Page wide>
|
||||||
<div class="title">
|
{#if $apps.length}
|
||||||
<Heading>Apps</Heading>
|
<Layout noPadding>
|
||||||
<ButtonGroup>
|
<div class="title">
|
||||||
<Button secondary on:click={initiateAppImport}>Import app</Button>
|
<Heading>Apps</Heading>
|
||||||
<Button cta on:click={modal.show}>Create new app</Button>
|
<ButtonGroup>
|
||||||
</ButtonGroup>
|
<Button secondary on:click={initiateAppImport}>Import app</Button>
|
||||||
</div>
|
<Button cta on:click={initiateAppCreation}>Create new app</Button>
|
||||||
<div class="filter">
|
</ButtonGroup>
|
||||||
<div class="select">
|
</div>
|
||||||
<Select quiet placeholder="Filter by groups" />
|
<div class="filter">
|
||||||
</div>
|
<div class="select">
|
||||||
<ActionGroup>
|
<Select quiet placeholder="Filter by groups" />
|
||||||
<ActionButton
|
</div>
|
||||||
on:click={() => (layout = "grid")}
|
<ActionGroup>
|
||||||
selected={layout === "grid"}
|
<ActionButton
|
||||||
quiet
|
on:click={() => (layout = "grid")}
|
||||||
icon="ClassicGridView"
|
selected={layout === "grid"}
|
||||||
/>
|
quiet
|
||||||
<ActionButton
|
icon="ClassicGridView"
|
||||||
on:click={() => (layout = "table")}
|
/>
|
||||||
selected={layout === "table"}
|
<ActionButton
|
||||||
quiet
|
on:click={() => (layout = "table")}
|
||||||
icon="ViewRow"
|
selected={layout === "table"}
|
||||||
/>
|
quiet
|
||||||
</ActionGroup>
|
icon="ViewRow"
|
||||||
</div>
|
/>
|
||||||
{#if layout === "grid"}
|
</ActionGroup>
|
||||||
<AppList />
|
</div>
|
||||||
{:else}
|
<div
|
||||||
Table view.
|
class:appGrid={layout === "grid"}
|
||||||
|
class:appTable={layout === "table"}
|
||||||
|
>
|
||||||
|
{#each $apps as app, idx (app._id)}
|
||||||
|
<svelte:component
|
||||||
|
this={layout === "grid" ? AppCard : AppRow}
|
||||||
|
{app}
|
||||||
|
{openApp}
|
||||||
|
{exportApp}
|
||||||
|
{deleteApp}
|
||||||
|
last={idx === $apps.length - 1}
|
||||||
|
/>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</Layout>
|
||||||
{/if}
|
{/if}
|
||||||
</Layout>
|
{#if !$apps.length && !creatingApp && loaded}
|
||||||
|
<div class="empty-wrapper">
|
||||||
|
<Modal inline>
|
||||||
|
<ModalContent
|
||||||
|
title="Create your first app"
|
||||||
|
confirmText="Create app"
|
||||||
|
showCancelButton={false}
|
||||||
|
showCloseIcon={false}
|
||||||
|
onConfirm={initiateAppCreation}
|
||||||
|
size="M"
|
||||||
|
>
|
||||||
|
<div slot="footer">
|
||||||
|
<Button on:click={initiateAppImport} secondary>Import app</Button>
|
||||||
|
</div>
|
||||||
|
<Body size="S">
|
||||||
|
The purpose of the Budibase builder is to help you build beautiful,
|
||||||
|
powerful applications quickly and easily.
|
||||||
|
</Body>
|
||||||
|
</ModalContent>
|
||||||
|
</Modal>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</Page>
|
||||||
<Modal
|
<Modal
|
||||||
bind:this={modal}
|
bind:this={creationModal}
|
||||||
padding={false}
|
padding={false}
|
||||||
width="600px"
|
width="600px"
|
||||||
on:hide={() => (template = null)}
|
on:hide={stopAppCreation}
|
||||||
>
|
>
|
||||||
<CreateAppModal {template} />
|
<CreateAppModal {template} />
|
||||||
</Modal>
|
</Modal>
|
||||||
|
<ConfirmDialog
|
||||||
|
bind:this={deletionModal}
|
||||||
|
title="Confirm deletion"
|
||||||
|
okText="Delete app"
|
||||||
|
onOk={confirmDeleteApp}
|
||||||
|
>
|
||||||
|
Are you sure you want to delete the app <b>{appToDelete?.name}</b>?
|
||||||
|
</ConfirmDialog>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.title,
|
.title,
|
||||||
|
@ -87,6 +191,41 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.select {
|
.select {
|
||||||
width: 110px;
|
width: 120px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.appGrid {
|
||||||
|
display: grid;
|
||||||
|
grid-gap: 50px;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
|
||||||
|
}
|
||||||
|
.appTable {
|
||||||
|
display: grid;
|
||||||
|
grid-template-rows: auto;
|
||||||
|
grid-template-columns: 1fr 1fr 1fr auto;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
.appTable :global(> div) {
|
||||||
|
height: 70px;
|
||||||
|
display: grid;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--spacing-xl);
|
||||||
|
grid-template-columns: auto 1fr;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
padding: 0 var(--spacing-s);
|
||||||
|
}
|
||||||
|
.appTable :global(> div:not(.last)) {
|
||||||
|
border-bottom: var(--border-light);
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-wrapper {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -0,0 +1,22 @@
|
||||||
|
<script>
|
||||||
|
import {
|
||||||
|
Body,
|
||||||
|
Menu,
|
||||||
|
MenuItem,
|
||||||
|
Detail,
|
||||||
|
MenuSection,
|
||||||
|
DetailSummary,
|
||||||
|
} from "@budibase/bbui"
|
||||||
|
|
||||||
|
export let bindings
|
||||||
|
export let onBindingClick = () => {}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Menu>
|
||||||
|
{#each bindings as binding}
|
||||||
|
<MenuItem on:click={() => onBindingClick(binding)}>
|
||||||
|
<Detail size="M">{binding.name}</Detail>
|
||||||
|
<Body size="XS" noPadding>{binding.description}</Body>
|
||||||
|
</MenuItem>
|
||||||
|
{/each}
|
||||||
|
</Menu>
|
|
@ -0,0 +1,13 @@
|
||||||
|
<script>
|
||||||
|
import { goto } from "@roxi/routify"
|
||||||
|
|
||||||
|
export let value
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<span on:click={() => $goto(`./${value}`)}>{value}</span>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
span {
|
||||||
|
text-transform: capitalize;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,142 @@
|
||||||
|
<script>
|
||||||
|
import {
|
||||||
|
Menu,
|
||||||
|
MenuItem,
|
||||||
|
Button,
|
||||||
|
Detail,
|
||||||
|
Heading,
|
||||||
|
Divider,
|
||||||
|
Label,
|
||||||
|
Modal,
|
||||||
|
ModalContent,
|
||||||
|
notifications,
|
||||||
|
Layout,
|
||||||
|
Icon,
|
||||||
|
Body,
|
||||||
|
Page,
|
||||||
|
Select,
|
||||||
|
Tabs,
|
||||||
|
Tab,
|
||||||
|
MenuSection,
|
||||||
|
MenuSeparator,
|
||||||
|
} from "@budibase/bbui"
|
||||||
|
import { goto } from "@roxi/routify"
|
||||||
|
import { onMount } from "svelte"
|
||||||
|
import { fade } from "svelte/transition"
|
||||||
|
import { email } from "stores/portal"
|
||||||
|
import Editor from "components/integration/QueryEditor.svelte"
|
||||||
|
import TemplateBindings from "./TemplateBindings.svelte"
|
||||||
|
import api from "builderStore/api"
|
||||||
|
|
||||||
|
const ConfigTypes = {
|
||||||
|
SMTP: "smtp",
|
||||||
|
}
|
||||||
|
|
||||||
|
export let template
|
||||||
|
|
||||||
|
let selected = "Edit"
|
||||||
|
let selectedBindingTab = "Template"
|
||||||
|
let htmlEditor
|
||||||
|
|
||||||
|
$: selectedTemplate = $email.templates.find(
|
||||||
|
({ purpose }) => purpose === template
|
||||||
|
)
|
||||||
|
$: templateBindings =
|
||||||
|
$email.definitions?.bindings[selectedTemplate.purpose] || []
|
||||||
|
|
||||||
|
async function saveTemplate() {
|
||||||
|
try {
|
||||||
|
// Save your template config
|
||||||
|
await email.templates.save(selectedTemplate)
|
||||||
|
notifications.success(`Template saved.`)
|
||||||
|
} catch (err) {
|
||||||
|
notifications.error(`Failed to update template settings. ${err}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function setTemplateBinding(binding) {
|
||||||
|
htmlEditor.update((selectedTemplate.contents += `{{ ${binding.name} }}`))
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Page wide gap="L">
|
||||||
|
<div class="backbutton" on:click={() => $goto("./")}>
|
||||||
|
<Icon name="BackAndroid" />
|
||||||
|
<span>Back</span>
|
||||||
|
</div>
|
||||||
|
<header>
|
||||||
|
<Heading>
|
||||||
|
Email Template: {template}
|
||||||
|
</Heading>
|
||||||
|
<Button cta on:click={saveTemplate}>Save</Button>
|
||||||
|
</header>
|
||||||
|
<Tabs {selected}>
|
||||||
|
<Tab title="Edit">
|
||||||
|
<div class="template-editor">
|
||||||
|
<Editor
|
||||||
|
editorHeight={800}
|
||||||
|
bind:this={htmlEditor}
|
||||||
|
mode="handlebars"
|
||||||
|
on:change={e => {
|
||||||
|
selectedTemplate.contents = e.detail.value
|
||||||
|
}}
|
||||||
|
value={selectedTemplate.contents}
|
||||||
|
/>
|
||||||
|
<div class="bindings-editor">
|
||||||
|
<Detail size="L">Bindings</Detail>
|
||||||
|
<Tabs selected={selectedBindingTab}>
|
||||||
|
<Tab title="Template">
|
||||||
|
<TemplateBindings
|
||||||
|
title="Template Bindings"
|
||||||
|
bindings={templateBindings}
|
||||||
|
onBindingClick={setTemplateBinding}
|
||||||
|
/>
|
||||||
|
</Tab>
|
||||||
|
<Tab title="Common">
|
||||||
|
<TemplateBindings
|
||||||
|
title="Common Bindings"
|
||||||
|
bindings={$email.definitions.bindings.common}
|
||||||
|
onBindingClick={setTemplateBinding}
|
||||||
|
/>
|
||||||
|
</Tab>
|
||||||
|
</Tabs>
|
||||||
|
</div>
|
||||||
|
</div></Tab
|
||||||
|
>
|
||||||
|
<Tab title="Preview">
|
||||||
|
<div class="preview" transition:fade>
|
||||||
|
{@html selectedTemplate.contents}
|
||||||
|
</div>
|
||||||
|
</Tab>
|
||||||
|
</Tabs>
|
||||||
|
</Page>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.template-editor {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 20%;
|
||||||
|
grid-gap: var(--spacing-xl);
|
||||||
|
margin-top: var(--spacing-xl);
|
||||||
|
}
|
||||||
|
|
||||||
|
header {
|
||||||
|
display: flex;
|
||||||
|
width: 100%;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-bottom: var(--spacing-l);
|
||||||
|
margin-top: var(--spacing-l);
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview {
|
||||||
|
background: white;
|
||||||
|
height: 800px;
|
||||||
|
padding: var(--spacing-xl);
|
||||||
|
}
|
||||||
|
|
||||||
|
.backbutton {
|
||||||
|
display: flex;
|
||||||
|
gap: var(--spacing-m);
|
||||||
|
margin-bottom: var(--spacing-xl);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,191 @@
|
||||||
|
<script>
|
||||||
|
import { goto } from "@roxi/routify"
|
||||||
|
import {
|
||||||
|
Menu,
|
||||||
|
MenuItem,
|
||||||
|
Button,
|
||||||
|
Heading,
|
||||||
|
Divider,
|
||||||
|
Label,
|
||||||
|
Modal,
|
||||||
|
ModalContent,
|
||||||
|
notifications,
|
||||||
|
Layout,
|
||||||
|
Input,
|
||||||
|
TextArea,
|
||||||
|
Body,
|
||||||
|
Page,
|
||||||
|
Select,
|
||||||
|
MenuSection,
|
||||||
|
MenuSeparator,
|
||||||
|
Table,
|
||||||
|
} from "@budibase/bbui"
|
||||||
|
import { onMount } from "svelte"
|
||||||
|
import { email } from "stores/portal"
|
||||||
|
import Editor from "components/integration/QueryEditor.svelte"
|
||||||
|
import TemplateBindings from "./TemplateBindings.svelte"
|
||||||
|
import TemplateLink from "./TemplateLink.svelte"
|
||||||
|
import api from "builderStore/api"
|
||||||
|
|
||||||
|
const ConfigTypes = {
|
||||||
|
SMTP: "smtp",
|
||||||
|
}
|
||||||
|
|
||||||
|
const templateSchema = {
|
||||||
|
purpose: {
|
||||||
|
displayName: "Email",
|
||||||
|
editable: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
const customRenderers = [
|
||||||
|
{
|
||||||
|
column: "purpose",
|
||||||
|
component: TemplateLink,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
let smtpConfig
|
||||||
|
let bindingsOpen = false
|
||||||
|
let htmlModal
|
||||||
|
let htmlEditor
|
||||||
|
let loading
|
||||||
|
|
||||||
|
async function saveSmtp() {
|
||||||
|
try {
|
||||||
|
// Save your SMTP config
|
||||||
|
const response = await api.post(`/api/admin/configs`, smtpConfig)
|
||||||
|
const json = await response.json()
|
||||||
|
if (response.status !== 200) throw new Error(json.message)
|
||||||
|
smtpConfig._rev = json._rev
|
||||||
|
smtpConfig._id = json._id
|
||||||
|
|
||||||
|
notifications.success(`Settings saved.`)
|
||||||
|
} catch (err) {
|
||||||
|
notifications.error(`Failed to save email settings. ${err}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function saveTemplate() {
|
||||||
|
try {
|
||||||
|
await email.templates.save(selectedTemplate)
|
||||||
|
notifications.success(`Template saved.`)
|
||||||
|
} catch (err) {
|
||||||
|
notifications.error(`Failed to update template settings. ${err}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchSmtp() {
|
||||||
|
// fetch the configs for smtp
|
||||||
|
const smtpResponse = await api.get(`/api/admin/configs/${ConfigTypes.SMTP}`)
|
||||||
|
const smtpDoc = await smtpResponse.json()
|
||||||
|
|
||||||
|
if (!smtpDoc._id) {
|
||||||
|
smtpConfig = {
|
||||||
|
type: ConfigTypes.SMTP,
|
||||||
|
config: {
|
||||||
|
auth: {
|
||||||
|
type: "login",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
smtpConfig = smtpDoc
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMount(async () => {
|
||||||
|
loading = true
|
||||||
|
await fetchSmtp()
|
||||||
|
await email.templates.fetch()
|
||||||
|
loading = false
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Page>
|
||||||
|
<header>
|
||||||
|
<Heading size="M">Email</Heading>
|
||||||
|
<Body size="S">
|
||||||
|
Sending email is not required, but highly recommended for processes such
|
||||||
|
as password recovery. To setup automated auth emails, simply add the
|
||||||
|
values below and click activate.
|
||||||
|
</Body>
|
||||||
|
</header>
|
||||||
|
<Divider />
|
||||||
|
{#if smtpConfig}
|
||||||
|
<div class="config-form">
|
||||||
|
<Heading size="S">SMTP</Heading>
|
||||||
|
<Body size="S">
|
||||||
|
To allow your app to benefit from automated auth emails, add your SMTP
|
||||||
|
details below.
|
||||||
|
</Body>
|
||||||
|
<Layout gap="S">
|
||||||
|
<Heading size="S">
|
||||||
|
<span />
|
||||||
|
</Heading>
|
||||||
|
<div class="form-row">
|
||||||
|
<Label>Host</Label>
|
||||||
|
<Input bind:value={smtpConfig.config.host} />
|
||||||
|
</div>
|
||||||
|
<div class="form-row">
|
||||||
|
<Label>Port</Label>
|
||||||
|
<Input type="number" bind:value={smtpConfig.config.port} />
|
||||||
|
</div>
|
||||||
|
<div class="form-row">
|
||||||
|
<Label>User</Label>
|
||||||
|
<Input bind:value={smtpConfig.config.auth.user} />
|
||||||
|
</div>
|
||||||
|
<div class="form-row">
|
||||||
|
<Label>Password</Label>
|
||||||
|
<Input type="password" bind:value={smtpConfig.config.auth.pass} />
|
||||||
|
</div>
|
||||||
|
<div class="form-row">
|
||||||
|
<Label>From email address</Label>
|
||||||
|
<Input type="email" bind:value={smtpConfig.config.from} />
|
||||||
|
</div>
|
||||||
|
</Layout>
|
||||||
|
<Button cta on:click={saveSmtp}>Save</Button>
|
||||||
|
</div>
|
||||||
|
<Divider />
|
||||||
|
|
||||||
|
<div class="config-form">
|
||||||
|
<Heading size="S">Templates</Heading>
|
||||||
|
<Body size="S">
|
||||||
|
Budibase comes out of the box with ready-made email templates to help
|
||||||
|
with user onboarding. Please refrain from changing the links.
|
||||||
|
</Body>
|
||||||
|
</div>
|
||||||
|
<Table
|
||||||
|
{customRenderers}
|
||||||
|
data={$email.templates}
|
||||||
|
schema={templateSchema}
|
||||||
|
{loading}
|
||||||
|
allowEditRows={false}
|
||||||
|
allowSelectRows={false}
|
||||||
|
allowEditColumns={false}
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
|
</Page>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.config-form {
|
||||||
|
margin-top: 42px;
|
||||||
|
margin-bottom: 42px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-row {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 20% 1fr;
|
||||||
|
grid-gap: var(--spacing-l);
|
||||||
|
}
|
||||||
|
|
||||||
|
span {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--spacing-s);
|
||||||
|
}
|
||||||
|
|
||||||
|
header {
|
||||||
|
margin-bottom: 42px;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -91,7 +91,7 @@
|
||||||
</div>
|
</div>
|
||||||
{/each}
|
{/each}
|
||||||
<div>
|
<div>
|
||||||
<Button primary on:click={() => save(google)}>Save</Button>
|
<Button cta on:click={() => save(google)}>Save</Button>
|
||||||
</div>
|
</div>
|
||||||
<Divider />
|
<Divider />
|
||||||
{/if}
|
{/if}
|
||||||
|
|
|
@ -0,0 +1,44 @@
|
||||||
|
import { writable } from "svelte/store"
|
||||||
|
import api from "builderStore/api"
|
||||||
|
|
||||||
|
export function createEmailStore() {
|
||||||
|
const store = writable([])
|
||||||
|
|
||||||
|
return {
|
||||||
|
subscribe: store.subscribe,
|
||||||
|
templates: {
|
||||||
|
fetch: async () => {
|
||||||
|
// fetch the email template definitions
|
||||||
|
const response = await api.get(`/api/admin/template/definitions`)
|
||||||
|
const definitions = await response.json()
|
||||||
|
|
||||||
|
// fetch the email templates themselves
|
||||||
|
const templatesResponse = await api.get(`/api/admin/template/email`)
|
||||||
|
const templates = await templatesResponse.json()
|
||||||
|
|
||||||
|
store.set({
|
||||||
|
definitions,
|
||||||
|
templates,
|
||||||
|
})
|
||||||
|
},
|
||||||
|
save: async template => {
|
||||||
|
// Save your template config
|
||||||
|
const response = await api.post(`/api/admin/template`, template)
|
||||||
|
const json = await response.json()
|
||||||
|
if (response.status !== 200) throw new Error(json.message)
|
||||||
|
template._rev = json._rev
|
||||||
|
template._id = json._id
|
||||||
|
|
||||||
|
store.update(state => {
|
||||||
|
const currentIdx = state.templates.findIndex(
|
||||||
|
template => template.purpose === json.purpose
|
||||||
|
)
|
||||||
|
state.templates.splice(currentIdx, 1, template)
|
||||||
|
return state
|
||||||
|
})
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const email = createEmailStore()
|
|
@ -1,3 +1,4 @@
|
||||||
export { organisation } from "./organisation"
|
export { organisation } from "./organisation"
|
||||||
export { admin } from "./admin"
|
export { admin } from "./admin"
|
||||||
export { apps } from "./apps"
|
export { apps } from "./apps"
|
||||||
|
export { email } from "./email"
|
||||||
|
|
|
@ -1194,10 +1194,10 @@
|
||||||
estree-walker "^2.0.1"
|
estree-walker "^2.0.1"
|
||||||
picomatch "^2.2.2"
|
picomatch "^2.2.2"
|
||||||
|
|
||||||
"@roxi/routify@2.15.1":
|
"@roxi/routify@2.18.0":
|
||||||
version "2.15.1"
|
version "2.18.0"
|
||||||
resolved "https://registry.yarnpkg.com/@roxi/routify/-/routify-2.15.1.tgz#cbd5eafedfee7f04b154173dccd7474c177acb4f"
|
resolved "https://registry.yarnpkg.com/@roxi/routify/-/routify-2.18.0.tgz#8f88bedd936312d0dbe44cbc11ab179b1f938ec2"
|
||||||
integrity sha512-IRdoaPSfP09EwWtB+tpbHgH6ejYtowale24rgfpxRQhFNyTUK4jYXclvx3XkUD1NSupSgl1kDAsWSiRSG0WvkQ==
|
integrity sha512-MVB50HN+VQWLzfjLplcBjsSBvwOiExKOmht2DuWR3WQ60JxQi9pSejkB06tFVkFKNXz2X5iYtKDqKBTdae/gRg==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@roxi/ssr" "^0.2.1"
|
"@roxi/ssr" "^0.2.1"
|
||||||
"@types/node" ">=4.2.0 < 13"
|
"@types/node" ">=4.2.0 < 13"
|
||||||
|
@ -5821,7 +5821,7 @@ svelte-portal@0.1.0:
|
||||||
resolved "https://registry.yarnpkg.com/svelte-portal/-/svelte-portal-0.1.0.tgz#cc2821cc84b05ed5814e0218dcdfcbebc53c1742"
|
resolved "https://registry.yarnpkg.com/svelte-portal/-/svelte-portal-0.1.0.tgz#cc2821cc84b05ed5814e0218dcdfcbebc53c1742"
|
||||||
integrity sha512-kef+ksXVKun224mRxat+DdO4C+cGHla+fEcZfnBAvoZocwiaceOfhf5azHYOPXSSB1igWVFTEOF3CDENPnuWxg==
|
integrity sha512-kef+ksXVKun224mRxat+DdO4C+cGHla+fEcZfnBAvoZocwiaceOfhf5azHYOPXSSB1igWVFTEOF3CDENPnuWxg==
|
||||||
|
|
||||||
svelte@^3.37.0:
|
svelte@^3.38.2:
|
||||||
version "3.38.2"
|
version "3.38.2"
|
||||||
resolved "https://registry.yarnpkg.com/svelte/-/svelte-3.38.2.tgz#55e5c681f793ae349b5cc2fe58e5782af4275ef5"
|
resolved "https://registry.yarnpkg.com/svelte/-/svelte-3.38.2.tgz#55e5c681f793ae349b5cc2fe58e5782af4275ef5"
|
||||||
integrity sha512-q5Dq0/QHh4BLJyEVWGe7Cej5NWs040LWjMbicBGZ+3qpFWJ1YObRmUDZKbbovddLC9WW7THTj3kYbTOFmU9fbg==
|
integrity sha512-q5Dq0/QHh4BLJyEVWGe7Cej5NWs040LWjMbicBGZ+3qpFWJ1YObRmUDZKbbovddLC9WW7THTj3kYbTOFmU9fbg==
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
const { string, number } = require("../questions")
|
const { number } = require("../questions")
|
||||||
const { success } = require("../utils")
|
const { success } = require("../utils")
|
||||||
const fs = require("fs")
|
const fs = require("fs")
|
||||||
const path = require("path")
|
const path = require("path")
|
||||||
|
@ -6,14 +6,11 @@ const randomString = require("randomstring")
|
||||||
|
|
||||||
const FILE_PATH = path.resolve("./.env")
|
const FILE_PATH = path.resolve("./.env")
|
||||||
|
|
||||||
function getContents(port, hostingKey) {
|
function getContents(port) {
|
||||||
return `
|
return `
|
||||||
# Use the main port in the builder for your self hosting URL, e.g. localhost:10000
|
# Use the main port in the builder for your self hosting URL, e.g. localhost:10000
|
||||||
MAIN_PORT=${port}
|
MAIN_PORT=${port}
|
||||||
|
|
||||||
# Use this password when configuring your self hosting settings
|
|
||||||
HOSTING_KEY=${hostingKey}
|
|
||||||
|
|
||||||
# This section contains all secrets pertaining to the system
|
# This section contains all secrets pertaining to the system
|
||||||
JWT_SECRET=${randomString.generate()}
|
JWT_SECRET=${randomString.generate()}
|
||||||
MINIO_ACCESS_KEY=${randomString.generate()}
|
MINIO_ACCESS_KEY=${randomString.generate()}
|
||||||
|
@ -21,6 +18,7 @@ MINIO_SECRET_KEY=${randomString.generate()}
|
||||||
COUCH_DB_PASSWORD=${randomString.generate()}
|
COUCH_DB_PASSWORD=${randomString.generate()}
|
||||||
COUCH_DB_USER=${randomString.generate()}
|
COUCH_DB_USER=${randomString.generate()}
|
||||||
REDIS_PASSWORD=${randomString.generate()}
|
REDIS_PASSWORD=${randomString.generate()}
|
||||||
|
INTERNAL_API_KEY=${randomString.generate()}
|
||||||
|
|
||||||
# This section contains variables that do not need to be altered under normal circumstances
|
# This section contains variables that do not need to be altered under normal circumstances
|
||||||
APP_PORT=4002
|
APP_PORT=4002
|
||||||
|
@ -33,7 +31,6 @@ BUDIBASE_ENVIRONMENT=PRODUCTION`
|
||||||
|
|
||||||
module.exports.filePath = FILE_PATH
|
module.exports.filePath = FILE_PATH
|
||||||
module.exports.ConfigMap = {
|
module.exports.ConfigMap = {
|
||||||
HOSTING_KEY: "key",
|
|
||||||
MAIN_PORT: "port",
|
MAIN_PORT: "port",
|
||||||
}
|
}
|
||||||
module.exports.QUICK_CONFIG = {
|
module.exports.QUICK_CONFIG = {
|
||||||
|
@ -42,18 +39,13 @@ module.exports.QUICK_CONFIG = {
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports.make = async (inputs = {}) => {
|
module.exports.make = async (inputs = {}) => {
|
||||||
const hostingKey =
|
|
||||||
inputs.key ||
|
|
||||||
(await string(
|
|
||||||
"Please input the password you'd like to use as your hosting key: "
|
|
||||||
))
|
|
||||||
const hostingPort =
|
const hostingPort =
|
||||||
inputs.port ||
|
inputs.port ||
|
||||||
(await number(
|
(await number(
|
||||||
"Please enter the port on which you want your installation to run: ",
|
"Please enter the port on which you want your installation to run: ",
|
||||||
10000
|
10000
|
||||||
))
|
))
|
||||||
const fileContents = getContents(hostingPort, hostingKey)
|
const fileContents = getContents(hostingPort)
|
||||||
fs.writeFileSync(FILE_PATH, fileContents)
|
fs.writeFileSync(FILE_PATH, fileContents)
|
||||||
console.log(
|
console.log(
|
||||||
success(
|
success(
|
||||||
|
|
|
@ -38,7 +38,7 @@
|
||||||
"rollup-plugin-svelte": "^7.1.0",
|
"rollup-plugin-svelte": "^7.1.0",
|
||||||
"rollup-plugin-svg": "^2.0.0",
|
"rollup-plugin-svg": "^2.0.0",
|
||||||
"rollup-plugin-terser": "^7.0.2",
|
"rollup-plugin-terser": "^7.0.2",
|
||||||
"svelte": "^3.37.0"
|
"svelte": "^3.38.2"
|
||||||
},
|
},
|
||||||
"gitHead": "4b6efc42ed3273595c7a129411f4d883733d3321"
|
"gitHead": "4b6efc42ed3273595c7a129411f4d883733d3321"
|
||||||
}
|
}
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -122,7 +122,7 @@
|
||||||
"pouchdb-find": "^7.2.2",
|
"pouchdb-find": "^7.2.2",
|
||||||
"pouchdb-replication-stream": "1.2.9",
|
"pouchdb-replication-stream": "1.2.9",
|
||||||
"server-destroy": "1.0.1",
|
"server-destroy": "1.0.1",
|
||||||
"svelte": "3.30.0",
|
"svelte": "^3.38.2",
|
||||||
"to-json-schema": "0.2.5",
|
"to-json-schema": "0.2.5",
|
||||||
"uuid": "3.3.2",
|
"uuid": "3.3.2",
|
||||||
"validate.js": "0.13.1",
|
"validate.js": "0.13.1",
|
||||||
|
|
|
@ -39,6 +39,7 @@ async function init() {
|
||||||
COUCH_DB_URL: "http://budibase:budibase@localhost:10000/db/",
|
COUCH_DB_URL: "http://budibase:budibase@localhost:10000/db/",
|
||||||
REDIS_URL: "localhost:6379",
|
REDIS_URL: "localhost:6379",
|
||||||
WORKER_URL: "http://localhost:4002",
|
WORKER_URL: "http://localhost:4002",
|
||||||
|
INTERNAL_API_KEY: "budibase",
|
||||||
JWT_SECRET: "testsecret",
|
JWT_SECRET: "testsecret",
|
||||||
REDIS_PASSWORD: "budibase",
|
REDIS_PASSWORD: "budibase",
|
||||||
MINIO_ACCESS_KEY: "budibase",
|
MINIO_ACCESS_KEY: "budibase",
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
const sendEmail = require("./steps/sendEmail")
|
const sendgridEmail = require("./steps/sendgridEmail")
|
||||||
|
const sendSmtpEmail = require("./steps/sendSmtpEmail")
|
||||||
const createRow = require("./steps/createRow")
|
const createRow = require("./steps/createRow")
|
||||||
const updateRow = require("./steps/updateRow")
|
const updateRow = require("./steps/updateRow")
|
||||||
const deleteRow = require("./steps/deleteRow")
|
const deleteRow = require("./steps/deleteRow")
|
||||||
|
@ -14,7 +15,8 @@ const {
|
||||||
} = require("../utilities/fileSystem")
|
} = require("../utilities/fileSystem")
|
||||||
|
|
||||||
const BUILTIN_ACTIONS = {
|
const BUILTIN_ACTIONS = {
|
||||||
SEND_EMAIL: sendEmail.run,
|
SEND_EMAIL: sendgridEmail.run,
|
||||||
|
SEND_EMAIL_SMTP: sendSmtpEmail.run,
|
||||||
CREATE_ROW: createRow.run,
|
CREATE_ROW: createRow.run,
|
||||||
UPDATE_ROW: updateRow.run,
|
UPDATE_ROW: updateRow.run,
|
||||||
DELETE_ROW: deleteRow.run,
|
DELETE_ROW: deleteRow.run,
|
||||||
|
@ -24,7 +26,8 @@ const BUILTIN_ACTIONS = {
|
||||||
EXECUTE_QUERY: executeQuery.run,
|
EXECUTE_QUERY: executeQuery.run,
|
||||||
}
|
}
|
||||||
const BUILTIN_DEFINITIONS = {
|
const BUILTIN_DEFINITIONS = {
|
||||||
SEND_EMAIL: sendEmail.definition,
|
SEND_EMAIL: sendgridEmail.definition,
|
||||||
|
SEND_EMAIL_SMTP: sendSmtpEmail.definition,
|
||||||
CREATE_ROW: createRow.definition,
|
CREATE_ROW: createRow.definition,
|
||||||
UPDATE_ROW: updateRow.definition,
|
UPDATE_ROW: updateRow.definition,
|
||||||
DELETE_ROW: deleteRow.definition,
|
DELETE_ROW: deleteRow.definition,
|
||||||
|
|
|
@ -0,0 +1,67 @@
|
||||||
|
const { sendSmtpEmail } = require("../../utilities/workerRequests")
|
||||||
|
|
||||||
|
module.exports.definition = {
|
||||||
|
description: "Send an email using SMTP",
|
||||||
|
tagline: "Send SMTP email to {{inputs.to}}",
|
||||||
|
icon: "ri-mail-open-line",
|
||||||
|
name: "Send Email (SMTP)",
|
||||||
|
type: "ACTION",
|
||||||
|
stepId: "SEND_EMAIL_SMTP",
|
||||||
|
inputs: {},
|
||||||
|
schema: {
|
||||||
|
inputs: {
|
||||||
|
properties: {
|
||||||
|
to: {
|
||||||
|
type: "string",
|
||||||
|
title: "Send To",
|
||||||
|
},
|
||||||
|
from: {
|
||||||
|
type: "string",
|
||||||
|
title: "Send From",
|
||||||
|
},
|
||||||
|
subject: {
|
||||||
|
type: "string",
|
||||||
|
title: "Email Subject",
|
||||||
|
},
|
||||||
|
contents: {
|
||||||
|
type: "string",
|
||||||
|
title: "HTML Contents",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
required: ["to", "from", "subject", "contents"],
|
||||||
|
},
|
||||||
|
outputs: {
|
||||||
|
properties: {
|
||||||
|
success: {
|
||||||
|
type: "boolean",
|
||||||
|
description: "Whether the email was sent",
|
||||||
|
},
|
||||||
|
response: {
|
||||||
|
type: "object",
|
||||||
|
description: "A response from the email client, this may be an error",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
required: ["success"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports.run = async function ({ inputs }) {
|
||||||
|
let { to, from, subject, contents } = inputs
|
||||||
|
if (!contents) {
|
||||||
|
contents = "<h1>No content</h1>"
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
let response = await sendSmtpEmail(to, from, subject, contents)
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
response,
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err)
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
response: err,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,8 +1,8 @@
|
||||||
module.exports.definition = {
|
module.exports.definition = {
|
||||||
description: "Send an email",
|
description: "Send an email using SendGrid",
|
||||||
tagline: "Send email to {{inputs.to}}",
|
tagline: "Send email to {{inputs.to}}",
|
||||||
icon: "ri-mail-open-line",
|
icon: "ri-mail-open-line",
|
||||||
name: "Send Email",
|
name: "Send Email (SendGrid)",
|
||||||
type: "ACTION",
|
type: "ACTION",
|
||||||
stepId: "SEND_EMAIL",
|
stepId: "SEND_EMAIL",
|
||||||
inputs: {},
|
inputs: {},
|
|
@ -34,6 +34,7 @@ module.exports = {
|
||||||
USE_QUOTAS: process.env.USE_QUOTAS,
|
USE_QUOTAS: process.env.USE_QUOTAS,
|
||||||
REDIS_URL: process.env.REDIS_URL,
|
REDIS_URL: process.env.REDIS_URL,
|
||||||
REDIS_PASSWORD: process.env.REDIS_PASSWORD,
|
REDIS_PASSWORD: process.env.REDIS_PASSWORD,
|
||||||
|
INTERNAL_API_KEY: process.env.INTERNAL_API_KEY,
|
||||||
// environment
|
// environment
|
||||||
NODE_ENV: process.env.NODE_ENV,
|
NODE_ENV: process.env.NODE_ENV,
|
||||||
JEST_WORKER_ID: process.env.JEST_WORKER_ID,
|
JEST_WORKER_ID: process.env.JEST_WORKER_ID,
|
||||||
|
@ -53,7 +54,6 @@ module.exports = {
|
||||||
BUDIBASE_API_KEY: process.env.BUDIBASE_API_KEY,
|
BUDIBASE_API_KEY: process.env.BUDIBASE_API_KEY,
|
||||||
USERID_API_KEY: process.env.USERID_API_KEY,
|
USERID_API_KEY: process.env.USERID_API_KEY,
|
||||||
DEPLOYMENT_CREDENTIALS_URL: process.env.DEPLOYMENT_CREDENTIALS_URL,
|
DEPLOYMENT_CREDENTIALS_URL: process.env.DEPLOYMENT_CREDENTIALS_URL,
|
||||||
HOSTING_KEY: process.env.HOSTING_KEY,
|
|
||||||
_set(key, value) {
|
_set(key, value) {
|
||||||
process.env[key] = value
|
process.env[key] = value
|
||||||
module.exports[key] = value
|
module.exports[key] = value
|
||||||
|
|
|
@ -28,7 +28,7 @@ function request(ctx, request) {
|
||||||
} else {
|
} else {
|
||||||
delete request.body
|
delete request.body
|
||||||
}
|
}
|
||||||
if (ctx.headers) {
|
if (ctx && ctx.headers) {
|
||||||
request.headers.cookie = ctx.headers.cookie
|
request.headers.cookie = ctx.headers.cookie
|
||||||
}
|
}
|
||||||
return request
|
return request
|
||||||
|
@ -36,6 +36,31 @@ function request(ctx, request) {
|
||||||
|
|
||||||
exports.request = request
|
exports.request = request
|
||||||
|
|
||||||
|
exports.sendSmtpEmail = async (to, from, subject, contents) => {
|
||||||
|
const response = await fetch(
|
||||||
|
checkSlashesInUrl(env.WORKER_URL + `/api/admin/email/send`),
|
||||||
|
request(null, {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"x-budibase-api-key": env.INTERNAL_API_KEY,
|
||||||
|
},
|
||||||
|
body: {
|
||||||
|
email: to,
|
||||||
|
from,
|
||||||
|
contents,
|
||||||
|
subject,
|
||||||
|
purpose: "custom",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
const json = await response.json()
|
||||||
|
if (json.status !== 200 && response.status !== 200) {
|
||||||
|
throw "Unable to send email."
|
||||||
|
}
|
||||||
|
return json
|
||||||
|
}
|
||||||
|
|
||||||
exports.getDeployedApps = async ctx => {
|
exports.getDeployedApps = async ctx => {
|
||||||
try {
|
try {
|
||||||
const response = await fetch(
|
const response = await fetch(
|
||||||
|
|
|
@ -1696,9 +1696,9 @@ anymatch@~3.1.1:
|
||||||
picomatch "^2.0.4"
|
picomatch "^2.0.4"
|
||||||
|
|
||||||
apexcharts@^3.19.2, apexcharts@^3.22.1:
|
apexcharts@^3.19.2, apexcharts@^3.22.1:
|
||||||
version "3.26.1"
|
version "3.26.2"
|
||||||
resolved "https://registry.yarnpkg.com/apexcharts/-/apexcharts-3.26.1.tgz#2094ec0cfd00ed8129d93e73e75018716d55ef5b"
|
resolved "https://registry.yarnpkg.com/apexcharts/-/apexcharts-3.26.2.tgz#5ed0d88c9db8fe881ae6ec6b60f086468727577b"
|
||||||
integrity sha512-Z/pfGTsL4YUm1tHd6a0d0G2hX4XUmhEI0b/5BxVK69dEB/XXvVMX9hKCkcIx+kPyVqVFCuo91ZjCdqjMcmSVBA==
|
integrity sha512-CD7bad4ygwc9rs9vOQDDagUcoJ1mcc9BwNSiQB14l6jiZBCQKrXxnG4I1ZjJ2MIel/Y5GmsJFs8HTcZBqpe/Ew==
|
||||||
dependencies:
|
dependencies:
|
||||||
svg.draggable.js "^2.2.2"
|
svg.draggable.js "^2.2.2"
|
||||||
svg.easing.js "^2.0.0"
|
svg.easing.js "^2.0.0"
|
||||||
|
@ -4840,7 +4840,15 @@ is-plain-object@^2.0.3, is-plain-object@^2.0.4:
|
||||||
dependencies:
|
dependencies:
|
||||||
isobject "^3.0.1"
|
isobject "^3.0.1"
|
||||||
|
|
||||||
is-regex@^1.0.4, is-regex@^1.1.2:
|
is-regex@^1.0.4:
|
||||||
|
version "1.1.3"
|
||||||
|
resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.1.3.tgz#d029f9aff6448b93ebbe3f33dac71511fdcbef9f"
|
||||||
|
integrity sha512-qSVXFz28HM7y+IWX6vLCsexdlvzT1PJNFSBuaQLQ5o0IEw8UDYW6/2+eCMVyIsbM8CNLX2a/QWmSpyxYEHY7CQ==
|
||||||
|
dependencies:
|
||||||
|
call-bind "^1.0.2"
|
||||||
|
has-symbols "^1.0.2"
|
||||||
|
|
||||||
|
is-regex@^1.1.2:
|
||||||
version "1.1.2"
|
version "1.1.2"
|
||||||
resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.1.2.tgz#81c8ebde4db142f2cf1c53fc86d6a45788266251"
|
resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.1.2.tgz#81c8ebde4db142f2cf1c53fc86d6a45788266251"
|
||||||
integrity sha512-axvdhb5pdhEVThqJzYXwMlVuZwC+FF2DpcOhTS+y/8jVq4trxyPgfcwIxIKiyeuLlSQYKkmUaPQJ8ZE4yNKXDg==
|
integrity sha512-axvdhb5pdhEVThqJzYXwMlVuZwC+FF2DpcOhTS+y/8jVq4trxyPgfcwIxIKiyeuLlSQYKkmUaPQJ8ZE4yNKXDg==
|
||||||
|
@ -8868,10 +8876,10 @@ svelte-spa-router@^3.0.5:
|
||||||
dependencies:
|
dependencies:
|
||||||
regexparam "1.3.0"
|
regexparam "1.3.0"
|
||||||
|
|
||||||
svelte@3.30.0:
|
svelte@^3.38.2:
|
||||||
version "3.30.0"
|
version "3.38.2"
|
||||||
resolved "https://registry.yarnpkg.com/svelte/-/svelte-3.30.0.tgz#cbde341e96bf34f4ac73c8f14f8a014e03bfb7d6"
|
resolved "https://registry.yarnpkg.com/svelte/-/svelte-3.38.2.tgz#55e5c681f793ae349b5cc2fe58e5782af4275ef5"
|
||||||
integrity sha512-z+hdIACb9TROGvJBQWcItMtlr4s0DBUgJss6qWrtFkOoIInkG+iAMo/FJZQFyDBQZc+dul2+TzYSi/tpTT5/Ag==
|
integrity sha512-q5Dq0/QHh4BLJyEVWGe7Cej5NWs040LWjMbicBGZ+3qpFWJ1YObRmUDZKbbovddLC9WW7THTj3kYbTOFmU9fbg==
|
||||||
|
|
||||||
svg.draggable.js@^2.2.2:
|
svg.draggable.js@^2.2.2:
|
||||||
version "2.2.2"
|
version "2.2.2"
|
||||||
|
@ -9281,9 +9289,9 @@ uc.micro@^1.0.1, uc.micro@^1.0.5:
|
||||||
integrity sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA==
|
integrity sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA==
|
||||||
|
|
||||||
uglify-js@^3.1.4:
|
uglify-js@^3.1.4:
|
||||||
version "3.13.5"
|
version "3.13.6"
|
||||||
resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.13.5.tgz#5d71d6dbba64cf441f32929b1efce7365bb4f113"
|
resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.13.6.tgz#6815ac7fdd155d03c83e2362bb717e5b39b74013"
|
||||||
integrity sha512-xtB8yEqIkn7zmOyS2zUNBsYCBRhDkvlNxMMY2smuJ/qA8NCHeQvKCF3i9Z4k8FJH4+PJvZRtMrPynfZ75+CSZw==
|
integrity sha512-rRprLwl8RVaS+Qvx3Wh5hPfPBn9++G6xkGlUupya0s5aDmNjI7z3lnRLB3u7sN4OmbB0pWgzhM9BEJyiWAwtAA==
|
||||||
|
|
||||||
uid2@0.0.x:
|
uid2@0.0.x:
|
||||||
version "0.0.3"
|
version "0.0.3"
|
||||||
|
|
|
@ -23,7 +23,7 @@
|
||||||
],
|
],
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@sveltejs/vite-plugin-svelte": "^1.0.0-next.5",
|
"@sveltejs/vite-plugin-svelte": "^1.0.0-next.5",
|
||||||
"svelte": "^3.37.0",
|
"svelte": "^3.38.2",
|
||||||
"vite": "^2.1.5"
|
"vite": "^2.1.5"
|
||||||
},
|
},
|
||||||
"keywords": [
|
"keywords": [
|
||||||
|
|
|
@ -8,6 +8,7 @@ async function init() {
|
||||||
SELF_HOSTED: 1,
|
SELF_HOSTED: 1,
|
||||||
PORT: 4002,
|
PORT: 4002,
|
||||||
JWT_SECRET: "testsecret",
|
JWT_SECRET: "testsecret",
|
||||||
|
INTERNAL_API_KEY: "budibase",
|
||||||
MINIO_ACCESS_KEY: "budibase",
|
MINIO_ACCESS_KEY: "budibase",
|
||||||
MINIO_SECRET_KEY: "budibase",
|
MINIO_SECRET_KEY: "budibase",
|
||||||
COUCH_DB_USER: "budibase",
|
COUCH_DB_USER: "budibase",
|
||||||
|
|
|
@ -54,7 +54,10 @@ exports.reset = async ctx => {
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
const user = await getGlobalUserByEmail(email)
|
const user = await getGlobalUserByEmail(email)
|
||||||
await sendEmail(email, EmailTemplatePurpose.PASSWORD_RECOVERY, { user })
|
await sendEmail(email, EmailTemplatePurpose.PASSWORD_RECOVERY, {
|
||||||
|
user,
|
||||||
|
subject: "{{ company }} platform password reset",
|
||||||
|
})
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
// don't throw any kind of error to the user, this might give away something
|
// don't throw any kind of error to the user, this might give away something
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,13 +5,27 @@ const authPkg = require("@budibase/auth")
|
||||||
const GLOBAL_DB = authPkg.StaticDatabases.GLOBAL.name
|
const GLOBAL_DB = authPkg.StaticDatabases.GLOBAL.name
|
||||||
|
|
||||||
exports.sendEmail = async ctx => {
|
exports.sendEmail = async ctx => {
|
||||||
const { groupId, email, userId, purpose } = ctx.request.body
|
const {
|
||||||
|
groupId,
|
||||||
|
email,
|
||||||
|
userId,
|
||||||
|
purpose,
|
||||||
|
contents,
|
||||||
|
from,
|
||||||
|
subject,
|
||||||
|
} = ctx.request.body
|
||||||
let user
|
let user
|
||||||
if (userId) {
|
if (userId) {
|
||||||
const db = new CouchDB(GLOBAL_DB)
|
const db = new CouchDB(GLOBAL_DB)
|
||||||
user = await db.get(userId)
|
user = await db.get(userId)
|
||||||
}
|
}
|
||||||
const response = await sendEmail(email, purpose, { groupId, user })
|
const response = await sendEmail(email, purpose, {
|
||||||
|
groupId,
|
||||||
|
user,
|
||||||
|
contents,
|
||||||
|
from,
|
||||||
|
subject,
|
||||||
|
})
|
||||||
ctx.body = {
|
ctx.body = {
|
||||||
...response,
|
...response,
|
||||||
message: `Email sent to ${email}.`,
|
message: `Email sent to ${email}.`,
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
const { generateTemplateID, StaticDatabases } = require("@budibase/auth").db
|
const { generateTemplateID, StaticDatabases } = require("@budibase/auth").db
|
||||||
const { CouchDB } = require("../../../db")
|
const CouchDB = require("../../../db")
|
||||||
const {
|
const {
|
||||||
TemplateMetadata,
|
TemplateMetadata,
|
||||||
TemplateBindings,
|
TemplateBindings,
|
||||||
|
@ -11,7 +11,6 @@ const GLOBAL_DB = StaticDatabases.GLOBAL.name
|
||||||
|
|
||||||
exports.save = async ctx => {
|
exports.save = async ctx => {
|
||||||
const db = new CouchDB(GLOBAL_DB)
|
const db = new CouchDB(GLOBAL_DB)
|
||||||
const type = ctx.params.type
|
|
||||||
let template = ctx.request.body
|
let template = ctx.request.body
|
||||||
if (!template.ownerId) {
|
if (!template.ownerId) {
|
||||||
template.ownerId = GLOBAL_OWNER
|
template.ownerId = GLOBAL_OWNER
|
||||||
|
@ -20,10 +19,7 @@ exports.save = async ctx => {
|
||||||
template._id = generateTemplateID(template.ownerId)
|
template._id = generateTemplateID(template.ownerId)
|
||||||
}
|
}
|
||||||
|
|
||||||
const response = await db.put({
|
const response = await db.put(template)
|
||||||
...template,
|
|
||||||
type,
|
|
||||||
})
|
|
||||||
ctx.body = {
|
ctx.body = {
|
||||||
...template,
|
...template,
|
||||||
_rev: response.rev,
|
_rev: response.rev,
|
||||||
|
@ -31,9 +27,17 @@ exports.save = async ctx => {
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.definitions = async ctx => {
|
exports.definitions = async ctx => {
|
||||||
|
const bindings = {}
|
||||||
|
|
||||||
|
for (let template of TemplateMetadata.email) {
|
||||||
|
bindings[template.purpose] = template.bindings
|
||||||
|
}
|
||||||
|
|
||||||
ctx.body = {
|
ctx.body = {
|
||||||
purpose: TemplateMetadata,
|
bindings: {
|
||||||
bindings: Object.values(TemplateBindings),
|
...bindings,
|
||||||
|
common: Object.values(TemplateBindings),
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -136,7 +136,9 @@ exports.invite = async ctx => {
|
||||||
if (existing) {
|
if (existing) {
|
||||||
ctx.throw(400, "Email address already in use.")
|
ctx.throw(400, "Email address already in use.")
|
||||||
}
|
}
|
||||||
await sendEmail(email, EmailTemplatePurpose.INVITATION)
|
await sendEmail(email, EmailTemplatePurpose.INVITATION, {
|
||||||
|
subject: "{{ company }} platform invitation",
|
||||||
|
})
|
||||||
ctx.body = {
|
ctx.body = {
|
||||||
message: "Invitation has been sent.",
|
message: "Invitation has been sent.",
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,93 @@
|
||||||
|
const authPkg = require("@budibase/auth")
|
||||||
|
const { google } = require("@budibase/auth/src/middleware")
|
||||||
|
const { Configs } = require("../../constants")
|
||||||
|
const CouchDB = require("../../db")
|
||||||
|
const { clearCookie } = authPkg.utils
|
||||||
|
const { Cookies } = authPkg.constants
|
||||||
|
const { passport } = authPkg.auth
|
||||||
|
|
||||||
|
const GLOBAL_DB = authPkg.StaticDatabases.GLOBAL.name
|
||||||
|
|
||||||
|
exports.authenticate = async (ctx, next) => {
|
||||||
|
return passport.authenticate("local", async (err, user) => {
|
||||||
|
if (err) {
|
||||||
|
return ctx.throw(403, "Unauthorized")
|
||||||
|
}
|
||||||
|
|
||||||
|
const expires = new Date()
|
||||||
|
expires.setDate(expires.getDate() + 1)
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
return ctx.throw(403, "Unauthorized")
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.cookies.set(Cookies.Auth, user.token, {
|
||||||
|
expires,
|
||||||
|
path: "/",
|
||||||
|
httpOnly: false,
|
||||||
|
overwrite: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
delete user.token
|
||||||
|
|
||||||
|
ctx.body = { user }
|
||||||
|
})(ctx, next)
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.logout = async ctx => {
|
||||||
|
clearCookie(ctx, Cookies.Auth)
|
||||||
|
ctx.body = { message: "User logged out" }
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The initial call that google authentication makes to take you to the google login screen.
|
||||||
|
* On a successful login, you will be redirected to the googleAuth callback route.
|
||||||
|
*/
|
||||||
|
exports.googlePreAuth = async (ctx, next) => {
|
||||||
|
const db = new CouchDB(GLOBAL_DB)
|
||||||
|
const config = await authPkg.db.getScopedFullConfig(db, {
|
||||||
|
type: Configs.GOOGLE,
|
||||||
|
group: ctx.query.group,
|
||||||
|
})
|
||||||
|
const strategy = await google.strategyFactory(config)
|
||||||
|
|
||||||
|
return passport.authenticate(strategy, {
|
||||||
|
scope: ["profile", "email"],
|
||||||
|
})(ctx, next)
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.googleAuth = async (ctx, next) => {
|
||||||
|
const db = new CouchDB(GLOBAL_DB)
|
||||||
|
|
||||||
|
const config = await authPkg.db.getScopedFullConfig(db, {
|
||||||
|
type: Configs.GOOGLE,
|
||||||
|
group: ctx.query.group,
|
||||||
|
})
|
||||||
|
const strategy = await google.strategyFactory(config)
|
||||||
|
|
||||||
|
return passport.authenticate(
|
||||||
|
strategy,
|
||||||
|
{ successRedirect: "/", failureRedirect: "/error" },
|
||||||
|
async (err, user) => {
|
||||||
|
if (err) {
|
||||||
|
return ctx.throw(403, "Unauthorized")
|
||||||
|
}
|
||||||
|
|
||||||
|
const expires = new Date()
|
||||||
|
expires.setDate(expires.getDate() + 1)
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
return ctx.throw(403, "Unauthorized")
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.cookies.set(Cookies.Auth, user.token, {
|
||||||
|
expires,
|
||||||
|
path: "/",
|
||||||
|
httpOnly: false,
|
||||||
|
overwrite: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
ctx.redirect("/")
|
||||||
|
}
|
||||||
|
)(ctx, next)
|
||||||
|
}
|
|
@ -53,9 +53,9 @@ router
|
||||||
.use(buildAuthMiddleware(PUBLIC_ENDPOINTS))
|
.use(buildAuthMiddleware(PUBLIC_ENDPOINTS))
|
||||||
// for now no public access is allowed to worker (bar health check)
|
// for now no public access is allowed to worker (bar health check)
|
||||||
.use((ctx, next) => {
|
.use((ctx, next) => {
|
||||||
if (!ctx.isAuthenticated) {
|
// if (!ctx.isAuthenticated) {
|
||||||
ctx.throw(403, "Unauthorized - no public worker access")
|
// ctx.throw(403, "Unauthorized - no public worker access")
|
||||||
}
|
// }
|
||||||
return next()
|
return next()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
@ -17,7 +17,7 @@ function smtpValidation() {
|
||||||
auth: Joi.object({
|
auth: Joi.object({
|
||||||
type: Joi.string().valid("login", "oauth2", null),
|
type: Joi.string().valid("login", "oauth2", null),
|
||||||
user: Joi.string().required(),
|
user: Joi.string().required(),
|
||||||
pass: Joi.string().valid("", null),
|
pass: Joi.string().allow("", null),
|
||||||
}).optional(),
|
}).optional(),
|
||||||
}).unknown(true)
|
}).unknown(true)
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,8 +10,11 @@ function buildEmailSendValidation() {
|
||||||
// prettier-ignore
|
// prettier-ignore
|
||||||
return joiValidator.body(Joi.object({
|
return joiValidator.body(Joi.object({
|
||||||
email: Joi.string().email(),
|
email: Joi.string().email(),
|
||||||
|
purpose: Joi.string().valid(...Object.values(EmailTemplatePurpose)),
|
||||||
groupId: Joi.string().allow("", null),
|
groupId: Joi.string().allow("", null),
|
||||||
purpose: Joi.string().allow(...Object.values(EmailTemplatePurpose)),
|
fromt: Joi.string().allow("", null),
|
||||||
|
contents: Joi.string().allow("", null),
|
||||||
|
subject: Joi.string().allow("", null),
|
||||||
}).required().unknown(true))
|
}).required().unknown(true))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -2,8 +2,13 @@ const setup = require("./utilities")
|
||||||
const { EmailTemplatePurpose } = require("../../../constants")
|
const { EmailTemplatePurpose } = require("../../../constants")
|
||||||
|
|
||||||
// mock the email system
|
// mock the email system
|
||||||
|
const sendMailMock = jest.fn()
|
||||||
jest.mock("nodemailer")
|
jest.mock("nodemailer")
|
||||||
const sendMailMock = setup.emailMock()
|
const nodemailer = require("nodemailer")
|
||||||
|
nodemailer.createTransport.mockReturnValue({
|
||||||
|
sendMail: sendMailMock,
|
||||||
|
verify: jest.fn()
|
||||||
|
})
|
||||||
|
|
||||||
describe("/api/admin/email", () => {
|
describe("/api/admin/email", () => {
|
||||||
let request = setup.getRequest()
|
let request = setup.getRequest()
|
||||||
|
|
|
@ -29,6 +29,7 @@ describe("/api/admin/email", () => {
|
||||||
.expect(200)
|
.expect(200)
|
||||||
expect(res.body.message).toBeDefined()
|
expect(res.body.message).toBeDefined()
|
||||||
const testUrl = nodemailer.getTestMessageUrl(res.body)
|
const testUrl = nodemailer.getTestMessageUrl(res.body)
|
||||||
|
console.log(`${purpose} URL: ${testUrl}`)
|
||||||
expect(testUrl).toBeDefined()
|
expect(testUrl).toBeDefined()
|
||||||
const response = await fetch(testUrl)
|
const response = await fetch(testUrl)
|
||||||
const text = await response.text()
|
const text = await response.text()
|
||||||
|
|
|
@ -24,7 +24,6 @@ const TemplateTypes = {
|
||||||
|
|
||||||
const EmailTemplatePurpose = {
|
const EmailTemplatePurpose = {
|
||||||
BASE: "base",
|
BASE: "base",
|
||||||
STYLES: "styles",
|
|
||||||
PASSWORD_RECOVERY: "password_recovery",
|
PASSWORD_RECOVERY: "password_recovery",
|
||||||
INVITATION: "invitation",
|
INVITATION: "invitation",
|
||||||
WELCOME: "welcome",
|
WELCOME: "welcome",
|
||||||
|
@ -32,47 +31,105 @@ const EmailTemplatePurpose = {
|
||||||
}
|
}
|
||||||
|
|
||||||
const TemplateBindings = {
|
const TemplateBindings = {
|
||||||
PLATFORM_URL: "platformUrl",
|
PLATFORM_URL: {
|
||||||
COMPANY: "company",
|
name: "platformUrl",
|
||||||
LOGO_URL: "logoUrl",
|
description: "The URL used to access the budibase platform",
|
||||||
STYLES: "styles",
|
},
|
||||||
BODY: "body",
|
COMPANY: {
|
||||||
REGISTRATION_URL: "registrationUrl",
|
name: "company",
|
||||||
EMAIL: "email",
|
description: "The name of your organization",
|
||||||
RESET_URL: "resetUrl",
|
},
|
||||||
USER: "user",
|
LOGO_URL: {
|
||||||
REQUEST: "request",
|
name: "logoUrl",
|
||||||
DOCS_URL: "docsUrl",
|
description: "The URL of your organizations logo.",
|
||||||
LOGIN_URL: "loginUrl",
|
},
|
||||||
CURRENT_YEAR: "currentYear",
|
EMAIL: {
|
||||||
CURRENT_DATE: "currentDate",
|
name: "email",
|
||||||
RESET_CODE: "resetCode",
|
description: "The recipients email address.",
|
||||||
INVITE_CODE: "inviteCode",
|
},
|
||||||
|
USER: {
|
||||||
|
name: "user",
|
||||||
|
description: "The recipients user object.",
|
||||||
|
},
|
||||||
|
REQUEST: {
|
||||||
|
name: "request",
|
||||||
|
description: "Additional request metadata.",
|
||||||
|
},
|
||||||
|
DOCS_URL: {
|
||||||
|
name: "docsUrl",
|
||||||
|
description: "Organization documentation URL.",
|
||||||
|
},
|
||||||
|
LOGIN_URL: {
|
||||||
|
name: "loginUrl",
|
||||||
|
description: "The URL used to log into the organization budibase instance.",
|
||||||
|
},
|
||||||
|
CURRENT_YEAR: {
|
||||||
|
name: "currentYear",
|
||||||
|
description: "The current year.",
|
||||||
|
},
|
||||||
|
CURRENT_DATE: {
|
||||||
|
name: "currentDate",
|
||||||
|
description: "The current date.",
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
const TemplateMetadata = {
|
const TemplateMetadata = {
|
||||||
[TemplateTypes.EMAIL]: [
|
[TemplateTypes.EMAIL]: [
|
||||||
{
|
|
||||||
name: "Styling",
|
|
||||||
purpose: EmailTemplatePurpose.STYLES,
|
|
||||||
bindings: ["url", "company", "companyUrl", "styles", "body"],
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
name: "Base Format",
|
name: "Base Format",
|
||||||
purpose: EmailTemplatePurpose.BASE,
|
purpose: EmailTemplatePurpose.BASE,
|
||||||
bindings: ["company", "registrationUrl"],
|
bindings: [
|
||||||
|
{
|
||||||
|
name: "body",
|
||||||
|
description: "The main body of another email template.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "styles",
|
||||||
|
description: "The contents of the Styling email template.",
|
||||||
|
},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Password Recovery",
|
name: "Password Recovery",
|
||||||
purpose: EmailTemplatePurpose.PASSWORD_RECOVERY,
|
purpose: EmailTemplatePurpose.PASSWORD_RECOVERY,
|
||||||
|
bindings: [
|
||||||
|
{
|
||||||
|
name: "resetUrl",
|
||||||
|
description:
|
||||||
|
"The URL the recipient must click to reset their password.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "resetCode",
|
||||||
|
description:
|
||||||
|
"The temporary password reset code used in the recipients password reset URL.",
|
||||||
|
},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "New User Invitation",
|
name: "New User Invitation",
|
||||||
purpose: EmailTemplatePurpose.INVITATION,
|
purpose: EmailTemplatePurpose.INVITATION,
|
||||||
|
bindings: [
|
||||||
|
{
|
||||||
|
name: "inviteUrl",
|
||||||
|
description:
|
||||||
|
"The URL the recipient must click to accept the invitation and activate their account.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "inviteCode",
|
||||||
|
description:
|
||||||
|
"The temporary invite code used in the recipients invitation URL.",
|
||||||
|
},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Custom",
|
name: "Custom",
|
||||||
purpose: EmailTemplatePurpose.CUSTOM,
|
purpose: EmailTemplatePurpose.CUSTOM,
|
||||||
|
bindings: [
|
||||||
|
{
|
||||||
|
name: "contents",
|
||||||
|
description: "Custom content body.",
|
||||||
|
},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,7 +9,426 @@
|
||||||
<meta name="supported-color-schemes" content="light dark" />
|
<meta name="supported-color-schemes" content="light dark" />
|
||||||
<title></title>
|
<title></title>
|
||||||
<style type="text/css" rel="stylesheet" media="all">
|
<style type="text/css" rel="stylesheet" media="all">
|
||||||
{{ styles }}
|
@import url('https://fonts.googleapis.com/css2?family=Source+Sans+Pro&display=swap');
|
||||||
|
|
||||||
|
body {
|
||||||
|
width: 100% !important;
|
||||||
|
height: 100%;
|
||||||
|
margin: 0;
|
||||||
|
-webkit-text-size-adjust: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: #3869D4;
|
||||||
|
}
|
||||||
|
|
||||||
|
a img {
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
td {
|
||||||
|
word-break: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preheader {
|
||||||
|
display: none !important;
|
||||||
|
visibility: hidden;
|
||||||
|
mso-hide: all;
|
||||||
|
font-size: 1px;
|
||||||
|
line-height: 1px;
|
||||||
|
max-height: 0;
|
||||||
|
max-width: 0;
|
||||||
|
opacity: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Type ------------------------------ */
|
||||||
|
|
||||||
|
body,
|
||||||
|
td,
|
||||||
|
th {
|
||||||
|
font-family: "Source Sans Pro", Helvetica, Arial, sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
margin-top: 0;
|
||||||
|
color: #333333;
|
||||||
|
font-size: 22px;
|
||||||
|
font-weight: bold;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
margin-top: 0;
|
||||||
|
color: #333333;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: bold;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
margin-top: 0;
|
||||||
|
color: #333333;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: bold;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
td,
|
||||||
|
th {
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
p,
|
||||||
|
ul,
|
||||||
|
ol,
|
||||||
|
blockquote {
|
||||||
|
margin: .4em 0 1.1875em;
|
||||||
|
font-size: 16px;
|
||||||
|
line-height: 1.625;
|
||||||
|
}
|
||||||
|
|
||||||
|
p.sub {
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Utilities ------------------------------ */
|
||||||
|
|
||||||
|
.align-right {
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.align-left {
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.align-center {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Buttons ------------------------------ */
|
||||||
|
|
||||||
|
.button {
|
||||||
|
background-color: #3869D4;
|
||||||
|
border-top: 10px solid #3869D4;
|
||||||
|
border-right: 18px solid #3869D4;
|
||||||
|
border-bottom: 10px solid #3869D4;
|
||||||
|
border-left: 18px solid #3869D4;
|
||||||
|
display: inline-block;
|
||||||
|
color: #FFF;
|
||||||
|
text-decoration: none;
|
||||||
|
border-radius: 3px;
|
||||||
|
box-shadow: 0 2px 3px rgba(0, 0, 0, 0.16);
|
||||||
|
-webkit-text-size-adjust: none;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button--green {
|
||||||
|
background-color: #22BC66;
|
||||||
|
border-top: 10px solid #22BC66;
|
||||||
|
border-right: 18px solid #22BC66;
|
||||||
|
border-bottom: 10px solid #22BC66;
|
||||||
|
border-left: 18px solid #22BC66;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button--red {
|
||||||
|
background-color: #FF6136;
|
||||||
|
border-top: 10px solid #FF6136;
|
||||||
|
border-right: 18px solid #FF6136;
|
||||||
|
border-bottom: 10px solid #FF6136;
|
||||||
|
border-left: 18px solid #FF6136;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media only screen and (max-width: 500px) {
|
||||||
|
.button {
|
||||||
|
width: 100% !important;
|
||||||
|
text-align: center !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Attribute list ------------------------------ */
|
||||||
|
|
||||||
|
.attributes {
|
||||||
|
margin: 0 0 21px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.attributes_content {
|
||||||
|
background-color: #F4F4F7;
|
||||||
|
padding: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.attributes_item {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Related Items ------------------------------ */
|
||||||
|
|
||||||
|
.related {
|
||||||
|
width: 100%;
|
||||||
|
margin: 0;
|
||||||
|
padding: 25px 0 0 0;
|
||||||
|
-premailer-width: 100%;
|
||||||
|
-premailer-cellpadding: 0;
|
||||||
|
-premailer-cellspacing: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.related_item {
|
||||||
|
padding: 10px 0;
|
||||||
|
color: #CBCCCF;
|
||||||
|
font-size: 15px;
|
||||||
|
line-height: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.related_item-title {
|
||||||
|
display: block;
|
||||||
|
margin: .5em 0 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.related_item-thumb {
|
||||||
|
display: block;
|
||||||
|
padding-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.related_heading {
|
||||||
|
border-top: 1px solid #CBCCCF;
|
||||||
|
text-align: center;
|
||||||
|
padding: 25px 0 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Discount Code ------------------------------ */
|
||||||
|
|
||||||
|
.discount {
|
||||||
|
width: 100%;
|
||||||
|
margin: 0;
|
||||||
|
padding: 24px;
|
||||||
|
-premailer-width: 100%;
|
||||||
|
-premailer-cellpadding: 0;
|
||||||
|
-premailer-cellspacing: 0;
|
||||||
|
background-color: #F4F4F7;
|
||||||
|
border: 2px dashed #CBCCCF;
|
||||||
|
}
|
||||||
|
|
||||||
|
.discount_heading {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.discount_body {
|
||||||
|
text-align: center;
|
||||||
|
font-size: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Social Icons ------------------------------ */
|
||||||
|
|
||||||
|
.social {
|
||||||
|
width: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.social td {
|
||||||
|
padding: 0;
|
||||||
|
width: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.social_icon {
|
||||||
|
height: 20px;
|
||||||
|
margin: 0 8px 10px 8px;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Data table ------------------------------ */
|
||||||
|
|
||||||
|
.purchase {
|
||||||
|
width: 100%;
|
||||||
|
margin: 0;
|
||||||
|
padding: 35px 0;
|
||||||
|
-premailer-width: 100%;
|
||||||
|
-premailer-cellpadding: 0;
|
||||||
|
-premailer-cellspacing: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.purchase_content {
|
||||||
|
width: 100%;
|
||||||
|
margin: 0;
|
||||||
|
padding: 25px 0 0 0;
|
||||||
|
-premailer-width: 100%;
|
||||||
|
-premailer-cellpadding: 0;
|
||||||
|
-premailer-cellspacing: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.purchase_item {
|
||||||
|
padding: 10px 0;
|
||||||
|
color: #51545E;
|
||||||
|
font-size: 15px;
|
||||||
|
line-height: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.purchase_heading {
|
||||||
|
padding-bottom: 8px;
|
||||||
|
border-bottom: 1px solid #EAEAEC;
|
||||||
|
}
|
||||||
|
|
||||||
|
.purchase_heading p {
|
||||||
|
margin: 0;
|
||||||
|
color: #85878E;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.purchase_footer {
|
||||||
|
padding-top: 15px;
|
||||||
|
border-top: 1px solid #EAEAEC;
|
||||||
|
}
|
||||||
|
|
||||||
|
.purchase_total {
|
||||||
|
margin: 0;
|
||||||
|
text-align: right;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #333333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.purchase_total--label {
|
||||||
|
padding: 0 15px 0 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
background-color: #FFF;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.email-wrapper {
|
||||||
|
width: 100%;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
-premailer-width: 100%;
|
||||||
|
-premailer-cellpadding: 0;
|
||||||
|
-premailer-cellspacing: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.email-content {
|
||||||
|
width: 100%;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
-premailer-width: 100%;
|
||||||
|
-premailer-cellpadding: 0;
|
||||||
|
-premailer-cellspacing: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Masthead ----------------------- */
|
||||||
|
|
||||||
|
.email-masthead {
|
||||||
|
padding: 25px 0;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.email-masthead_logo {
|
||||||
|
width: 94px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.email-masthead_name {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #A8AAAF;
|
||||||
|
text-decoration: none;
|
||||||
|
text-shadow: 0 1px 0 white;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Body ------------------------------ */
|
||||||
|
|
||||||
|
.email-body {
|
||||||
|
width: 100%;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
-premailer-width: 100%;
|
||||||
|
-premailer-cellpadding: 0;
|
||||||
|
-premailer-cellspacing: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.email-body_inner {
|
||||||
|
width: 570px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 0;
|
||||||
|
-premailer-width: 570px;
|
||||||
|
-premailer-cellpadding: 0;
|
||||||
|
-premailer-cellspacing: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.email-footer {
|
||||||
|
width: 570px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 0;
|
||||||
|
-premailer-width: 570px;
|
||||||
|
-premailer-cellpadding: 0;
|
||||||
|
-premailer-cellspacing: 0;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.email-footer p {
|
||||||
|
color: #A8AAAF;
|
||||||
|
}
|
||||||
|
|
||||||
|
.body-action {
|
||||||
|
width: 100%;
|
||||||
|
margin: 30px auto;
|
||||||
|
padding: 0;
|
||||||
|
-premailer-width: 100%;
|
||||||
|
-premailer-cellpadding: 0;
|
||||||
|
-premailer-cellspacing: 0;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.body-sub {
|
||||||
|
margin-top: 25px;
|
||||||
|
padding-top: 25px;
|
||||||
|
border-top: 1px solid #EAEAEC;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content-cell {
|
||||||
|
padding: 35px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*Media Queries ------------------------------ */
|
||||||
|
|
||||||
|
@media only screen and (max-width: 600px) {
|
||||||
|
.email-body_inner,
|
||||||
|
.email-footer {
|
||||||
|
width: 100% !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
body {
|
||||||
|
background-color: #333333 !important;
|
||||||
|
color: #FFF !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
p,
|
||||||
|
ul,
|
||||||
|
ol,
|
||||||
|
blockquote,
|
||||||
|
h1,
|
||||||
|
h2,
|
||||||
|
h3,
|
||||||
|
span,
|
||||||
|
.purchase_item {
|
||||||
|
color: #FFF !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.attributes_content,
|
||||||
|
.discount {
|
||||||
|
background-color: #222 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.email-masthead_name {
|
||||||
|
text-shadow: none !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
:root {
|
||||||
|
color-scheme: light dark;
|
||||||
|
supported-color-schemes: light dark;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
<!--[if mso]>
|
<!--[if mso]>
|
||||||
<style type="text/css">
|
<style type="text/css">
|
||||||
|
|
|
@ -0,0 +1,14 @@
|
||||||
|
<tr>
|
||||||
|
<td class="email-body" width="570" cellpadding="0" cellspacing="0">
|
||||||
|
<table class="email-body_inner" align="center" width="570" cellpadding="0" cellspacing="0" role="presentation">
|
||||||
|
<!-- Body content -->
|
||||||
|
<tr>
|
||||||
|
<td class="content-cell">
|
||||||
|
<div class="f-fallback">
|
||||||
|
{{ contents }}
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</td>
|
||||||
|
</tr>
|
|
@ -17,10 +17,10 @@ exports.EmailTemplates = {
|
||||||
join(__dirname, "invitation.hbs")
|
join(__dirname, "invitation.hbs")
|
||||||
),
|
),
|
||||||
[EmailTemplatePurpose.BASE]: readStaticFile(join(__dirname, "base.hbs")),
|
[EmailTemplatePurpose.BASE]: readStaticFile(join(__dirname, "base.hbs")),
|
||||||
[EmailTemplatePurpose.STYLES]: readStaticFile(join(__dirname, "style.hbs")),
|
|
||||||
[EmailTemplatePurpose.WELCOME]: readStaticFile(
|
[EmailTemplatePurpose.WELCOME]: readStaticFile(
|
||||||
join(__dirname, "welcome.hbs")
|
join(__dirname, "welcome.hbs")
|
||||||
),
|
),
|
||||||
|
[EmailTemplatePurpose.CUSTOM]: readStaticFile(join(__dirname, "custom.hbs")),
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.addBaseTemplates = (templates, type = null) => {
|
exports.addBaseTemplates = (templates, type = null) => {
|
||||||
|
|
|
@ -22,7 +22,7 @@
|
||||||
<table width="100%" border="0" cellspacing="0" cellpadding="0" role="presentation">
|
<table width="100%" border="0" cellspacing="0" cellpadding="0" role="presentation">
|
||||||
<tr>
|
<tr>
|
||||||
<td align="center">
|
<td align="center">
|
||||||
<a href="{{ registrationUrl }}" class="f-fallback button" target="_blank">Set up account</a>
|
<a href="{{ inviteUrl }}" class="f-fallback button" target="_blank">Set up account</a>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
|
@ -38,7 +38,7 @@
|
||||||
<tr>
|
<tr>
|
||||||
<td>
|
<td>
|
||||||
<p class="f-fallback sub">If you’re having trouble with the button above, copy and paste the URL below into your web browser.</p>
|
<p class="f-fallback sub">If you’re having trouble with the button above, copy and paste the URL below into your web browser.</p>
|
||||||
<p class="f-fallback sub">{{ registrationUrl }}</p>
|
<p class="f-fallback sub">{{ inviteUrl }}</p>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
|
|
|
@ -1,408 +0,0 @@
|
||||||
/* Based on templates: https://github.com/wildbit/postmark-templates/blob/master/templates/plain */
|
|
||||||
/* Base ------------------------------ */
|
|
||||||
|
|
||||||
@import url('https://fonts.googleapis.com/css2?family=Source+Sans+Pro&display=swap');
|
|
||||||
body {
|
|
||||||
width: 100% !important;
|
|
||||||
height: 100%;
|
|
||||||
margin: 0;
|
|
||||||
-webkit-text-size-adjust: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
a {
|
|
||||||
color: #3869D4;
|
|
||||||
}
|
|
||||||
|
|
||||||
a img {
|
|
||||||
border: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
td {
|
|
||||||
word-break: break-word;
|
|
||||||
}
|
|
||||||
|
|
||||||
.preheader {
|
|
||||||
display: none !important;
|
|
||||||
visibility: hidden;
|
|
||||||
mso-hide: all;
|
|
||||||
font-size: 1px;
|
|
||||||
line-height: 1px;
|
|
||||||
max-height: 0;
|
|
||||||
max-width: 0;
|
|
||||||
opacity: 0;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
/* Type ------------------------------ */
|
|
||||||
|
|
||||||
body,
|
|
||||||
td,
|
|
||||||
th {
|
|
||||||
font-family: "Source Sans Pro", Helvetica, Arial, sans-serif;
|
|
||||||
}
|
|
||||||
|
|
||||||
h1 {
|
|
||||||
margin-top: 0;
|
|
||||||
color: #333333;
|
|
||||||
font-size: 22px;
|
|
||||||
font-weight: bold;
|
|
||||||
text-align: left;
|
|
||||||
}
|
|
||||||
|
|
||||||
h2 {
|
|
||||||
margin-top: 0;
|
|
||||||
color: #333333;
|
|
||||||
font-size: 16px;
|
|
||||||
font-weight: bold;
|
|
||||||
text-align: left;
|
|
||||||
}
|
|
||||||
|
|
||||||
h3 {
|
|
||||||
margin-top: 0;
|
|
||||||
color: #333333;
|
|
||||||
font-size: 14px;
|
|
||||||
font-weight: bold;
|
|
||||||
text-align: left;
|
|
||||||
}
|
|
||||||
|
|
||||||
td,
|
|
||||||
th {
|
|
||||||
font-size: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
p,
|
|
||||||
ul,
|
|
||||||
ol,
|
|
||||||
blockquote {
|
|
||||||
margin: .4em 0 1.1875em;
|
|
||||||
font-size: 16px;
|
|
||||||
line-height: 1.625;
|
|
||||||
}
|
|
||||||
|
|
||||||
p.sub {
|
|
||||||
font-size: 13px;
|
|
||||||
}
|
|
||||||
/* Utilities ------------------------------ */
|
|
||||||
|
|
||||||
.align-right {
|
|
||||||
text-align: right;
|
|
||||||
}
|
|
||||||
|
|
||||||
.align-left {
|
|
||||||
text-align: left;
|
|
||||||
}
|
|
||||||
|
|
||||||
.align-center {
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
/* Buttons ------------------------------ */
|
|
||||||
|
|
||||||
.button {
|
|
||||||
background-color: #3869D4;
|
|
||||||
border-top: 10px solid #3869D4;
|
|
||||||
border-right: 18px solid #3869D4;
|
|
||||||
border-bottom: 10px solid #3869D4;
|
|
||||||
border-left: 18px solid #3869D4;
|
|
||||||
display: inline-block;
|
|
||||||
color: #FFF;
|
|
||||||
text-decoration: none;
|
|
||||||
border-radius: 3px;
|
|
||||||
box-shadow: 0 2px 3px rgba(0, 0, 0, 0.16);
|
|
||||||
-webkit-text-size-adjust: none;
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
|
|
||||||
.button--green {
|
|
||||||
background-color: #22BC66;
|
|
||||||
border-top: 10px solid #22BC66;
|
|
||||||
border-right: 18px solid #22BC66;
|
|
||||||
border-bottom: 10px solid #22BC66;
|
|
||||||
border-left: 18px solid #22BC66;
|
|
||||||
}
|
|
||||||
|
|
||||||
.button--red {
|
|
||||||
background-color: #FF6136;
|
|
||||||
border-top: 10px solid #FF6136;
|
|
||||||
border-right: 18px solid #FF6136;
|
|
||||||
border-bottom: 10px solid #FF6136;
|
|
||||||
border-left: 18px solid #FF6136;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media only screen and (max-width: 500px) {
|
|
||||||
.button {
|
|
||||||
width: 100% !important;
|
|
||||||
text-align: center !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
/* Attribute list ------------------------------ */
|
|
||||||
|
|
||||||
.attributes {
|
|
||||||
margin: 0 0 21px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.attributes_content {
|
|
||||||
background-color: #F4F4F7;
|
|
||||||
padding: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.attributes_item {
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
/* Related Items ------------------------------ */
|
|
||||||
|
|
||||||
.related {
|
|
||||||
width: 100%;
|
|
||||||
margin: 0;
|
|
||||||
padding: 25px 0 0 0;
|
|
||||||
-premailer-width: 100%;
|
|
||||||
-premailer-cellpadding: 0;
|
|
||||||
-premailer-cellspacing: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.related_item {
|
|
||||||
padding: 10px 0;
|
|
||||||
color: #CBCCCF;
|
|
||||||
font-size: 15px;
|
|
||||||
line-height: 18px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.related_item-title {
|
|
||||||
display: block;
|
|
||||||
margin: .5em 0 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.related_item-thumb {
|
|
||||||
display: block;
|
|
||||||
padding-bottom: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.related_heading {
|
|
||||||
border-top: 1px solid #CBCCCF;
|
|
||||||
text-align: center;
|
|
||||||
padding: 25px 0 10px;
|
|
||||||
}
|
|
||||||
/* Discount Code ------------------------------ */
|
|
||||||
|
|
||||||
.discount {
|
|
||||||
width: 100%;
|
|
||||||
margin: 0;
|
|
||||||
padding: 24px;
|
|
||||||
-premailer-width: 100%;
|
|
||||||
-premailer-cellpadding: 0;
|
|
||||||
-premailer-cellspacing: 0;
|
|
||||||
background-color: #F4F4F7;
|
|
||||||
border: 2px dashed #CBCCCF;
|
|
||||||
}
|
|
||||||
|
|
||||||
.discount_heading {
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.discount_body {
|
|
||||||
text-align: center;
|
|
||||||
font-size: 15px;
|
|
||||||
}
|
|
||||||
/* Social Icons ------------------------------ */
|
|
||||||
|
|
||||||
.social {
|
|
||||||
width: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.social td {
|
|
||||||
padding: 0;
|
|
||||||
width: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.social_icon {
|
|
||||||
height: 20px;
|
|
||||||
margin: 0 8px 10px 8px;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
/* Data table ------------------------------ */
|
|
||||||
|
|
||||||
.purchase {
|
|
||||||
width: 100%;
|
|
||||||
margin: 0;
|
|
||||||
padding: 35px 0;
|
|
||||||
-premailer-width: 100%;
|
|
||||||
-premailer-cellpadding: 0;
|
|
||||||
-premailer-cellspacing: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.purchase_content {
|
|
||||||
width: 100%;
|
|
||||||
margin: 0;
|
|
||||||
padding: 25px 0 0 0;
|
|
||||||
-premailer-width: 100%;
|
|
||||||
-premailer-cellpadding: 0;
|
|
||||||
-premailer-cellspacing: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.purchase_item {
|
|
||||||
padding: 10px 0;
|
|
||||||
color: #51545E;
|
|
||||||
font-size: 15px;
|
|
||||||
line-height: 18px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.purchase_heading {
|
|
||||||
padding-bottom: 8px;
|
|
||||||
border-bottom: 1px solid #EAEAEC;
|
|
||||||
}
|
|
||||||
|
|
||||||
.purchase_heading p {
|
|
||||||
margin: 0;
|
|
||||||
color: #85878E;
|
|
||||||
font-size: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.purchase_footer {
|
|
||||||
padding-top: 15px;
|
|
||||||
border-top: 1px solid #EAEAEC;
|
|
||||||
}
|
|
||||||
|
|
||||||
.purchase_total {
|
|
||||||
margin: 0;
|
|
||||||
text-align: right;
|
|
||||||
font-weight: bold;
|
|
||||||
color: #333333;
|
|
||||||
}
|
|
||||||
|
|
||||||
.purchase_total--label {
|
|
||||||
padding: 0 15px 0 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
|
||||||
background-color: #FFF;
|
|
||||||
color: #333;
|
|
||||||
}
|
|
||||||
|
|
||||||
p {
|
|
||||||
color: #333;
|
|
||||||
}
|
|
||||||
|
|
||||||
.email-wrapper {
|
|
||||||
width: 100%;
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
-premailer-width: 100%;
|
|
||||||
-premailer-cellpadding: 0;
|
|
||||||
-premailer-cellspacing: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.email-content {
|
|
||||||
width: 100%;
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
-premailer-width: 100%;
|
|
||||||
-premailer-cellpadding: 0;
|
|
||||||
-premailer-cellspacing: 0;
|
|
||||||
}
|
|
||||||
/* Masthead ----------------------- */
|
|
||||||
|
|
||||||
.email-masthead {
|
|
||||||
padding: 25px 0;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.email-masthead_logo {
|
|
||||||
width: 94px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.email-masthead_name {
|
|
||||||
font-size: 16px;
|
|
||||||
font-weight: bold;
|
|
||||||
color: #A8AAAF;
|
|
||||||
text-decoration: none;
|
|
||||||
text-shadow: 0 1px 0 white;
|
|
||||||
}
|
|
||||||
/* Body ------------------------------ */
|
|
||||||
|
|
||||||
.email-body {
|
|
||||||
width: 100%;
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
-premailer-width: 100%;
|
|
||||||
-premailer-cellpadding: 0;
|
|
||||||
-premailer-cellspacing: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.email-body_inner {
|
|
||||||
width: 570px;
|
|
||||||
margin: 0 auto;
|
|
||||||
padding: 0;
|
|
||||||
-premailer-width: 570px;
|
|
||||||
-premailer-cellpadding: 0;
|
|
||||||
-premailer-cellspacing: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.email-footer {
|
|
||||||
width: 570px;
|
|
||||||
margin: 0 auto;
|
|
||||||
padding: 0;
|
|
||||||
-premailer-width: 570px;
|
|
||||||
-premailer-cellpadding: 0;
|
|
||||||
-premailer-cellspacing: 0;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.email-footer p {
|
|
||||||
color: #A8AAAF;
|
|
||||||
}
|
|
||||||
|
|
||||||
.body-action {
|
|
||||||
width: 100%;
|
|
||||||
margin: 30px auto;
|
|
||||||
padding: 0;
|
|
||||||
-premailer-width: 100%;
|
|
||||||
-premailer-cellpadding: 0;
|
|
||||||
-premailer-cellspacing: 0;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.body-sub {
|
|
||||||
margin-top: 25px;
|
|
||||||
padding-top: 25px;
|
|
||||||
border-top: 1px solid #EAEAEC;
|
|
||||||
}
|
|
||||||
|
|
||||||
.content-cell {
|
|
||||||
padding: 35px;
|
|
||||||
}
|
|
||||||
/*Media Queries ------------------------------ */
|
|
||||||
|
|
||||||
@media only screen and (max-width: 600px) {
|
|
||||||
.email-body_inner,
|
|
||||||
.email-footer {
|
|
||||||
width: 100% !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (prefers-color-scheme: dark) {
|
|
||||||
body {
|
|
||||||
background-color: #333333 !important;
|
|
||||||
color: #FFF !important;
|
|
||||||
}
|
|
||||||
p,
|
|
||||||
ul,
|
|
||||||
ol,
|
|
||||||
blockquote,
|
|
||||||
h1,
|
|
||||||
h2,
|
|
||||||
h3,
|
|
||||||
span,
|
|
||||||
.purchase_item {
|
|
||||||
color: #FFF !important;
|
|
||||||
}
|
|
||||||
.attributes_content,
|
|
||||||
.discount {
|
|
||||||
background-color: #222 !important;
|
|
||||||
}
|
|
||||||
.email-masthead_name {
|
|
||||||
text-shadow: none !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
:root {
|
|
||||||
color-scheme: light dark;
|
|
||||||
supported-color-schemes: light dark;
|
|
||||||
}
|
|
|
@ -28,8 +28,8 @@ module.exports = {
|
||||||
SALT_ROUNDS: process.env.SALT_ROUNDS,
|
SALT_ROUNDS: process.env.SALT_ROUNDS,
|
||||||
REDIS_URL: process.env.REDIS_URL,
|
REDIS_URL: process.env.REDIS_URL,
|
||||||
REDIS_PASSWORD: process.env.REDIS_PASSWORD,
|
REDIS_PASSWORD: process.env.REDIS_PASSWORD,
|
||||||
|
INTERNAL_API_KEY: process.env.INTERNAL_API_KEY,
|
||||||
/* TODO: to remove - once deployment removed */
|
/* TODO: to remove - once deployment removed */
|
||||||
SELF_HOST_KEY: process.env.SELF_HOST_KEY,
|
|
||||||
COUCH_DB_USERNAME: process.env.COUCH_DB_USERNAME,
|
COUCH_DB_USERNAME: process.env.COUCH_DB_USERNAME,
|
||||||
COUCH_DB_PASSWORD: process.env.COUCH_DB_PASSWORD,
|
COUCH_DB_PASSWORD: process.env.COUCH_DB_PASSWORD,
|
||||||
_set(key, value) {
|
_set(key, value) {
|
||||||
|
|
|
@ -7,6 +7,7 @@ const { getSettingsTemplateContext } = require("./templates")
|
||||||
const { processString } = require("@budibase/string-templates")
|
const { processString } = require("@budibase/string-templates")
|
||||||
const { getResetPasswordCode, getInviteCode } = require("../utilities/redis")
|
const { getResetPasswordCode, getInviteCode } = require("../utilities/redis")
|
||||||
|
|
||||||
|
const TEST_MODE = false
|
||||||
const GLOBAL_DB = StaticDatabases.GLOBAL.name
|
const GLOBAL_DB = StaticDatabases.GLOBAL.name
|
||||||
const TYPE = TemplateTypes.EMAIL
|
const TYPE = TemplateTypes.EMAIL
|
||||||
|
|
||||||
|
@ -14,18 +15,32 @@ const FULL_EMAIL_PURPOSES = [
|
||||||
EmailTemplatePurpose.INVITATION,
|
EmailTemplatePurpose.INVITATION,
|
||||||
EmailTemplatePurpose.PASSWORD_RECOVERY,
|
EmailTemplatePurpose.PASSWORD_RECOVERY,
|
||||||
EmailTemplatePurpose.WELCOME,
|
EmailTemplatePurpose.WELCOME,
|
||||||
|
EmailTemplatePurpose.CUSTOM,
|
||||||
]
|
]
|
||||||
|
|
||||||
function createSMTPTransport(config) {
|
function createSMTPTransport(config) {
|
||||||
const options = {
|
let options
|
||||||
port: config.port,
|
if (!TEST_MODE) {
|
||||||
host: config.host,
|
options = {
|
||||||
secure: config.secure || false,
|
port: config.port,
|
||||||
auth: config.auth,
|
host: config.host,
|
||||||
}
|
secure: config.secure || false,
|
||||||
if (config.selfSigned) {
|
auth: config.auth,
|
||||||
options.tls = {
|
}
|
||||||
rejectUnauthorized: false,
|
if (config.selfSigned) {
|
||||||
|
options.tls = {
|
||||||
|
rejectUnauthorized: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
options = {
|
||||||
|
port: 587,
|
||||||
|
host: "smtp.ethereal.email",
|
||||||
|
secure: false,
|
||||||
|
auth: {
|
||||||
|
user: "don.bahringer@ethereal.email",
|
||||||
|
pass: "yCKSH8rWyUPbnhGYk9",
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return nodemailer.createTransport(options)
|
return nodemailer.createTransport(options)
|
||||||
|
@ -46,40 +61,36 @@ async function getLinkCode(purpose, email, user) {
|
||||||
* Builds an email using handlebars and the templates found in the system (default or otherwise).
|
* Builds an email using handlebars and the templates found in the system (default or otherwise).
|
||||||
* @param {string} purpose the purpose of the email being built, e.g. invitation, password reset.
|
* @param {string} purpose the purpose of the email being built, e.g. invitation, password reset.
|
||||||
* @param {string} email the address which it is being sent to for contextual purposes.
|
* @param {string} email the address which it is being sent to for contextual purposes.
|
||||||
* @param {object|null} user If being sent to an existing user then the object can be provided for context.
|
* @param {object} context the context which is being used for building the email (hbs context).
|
||||||
|
* @param {object|null} user if being sent to an existing user then the object can be provided for context.
|
||||||
|
* @param {string|null} contents if using a custom template can supply contents for context.
|
||||||
* @return {Promise<string>} returns the built email HTML if all provided parameters were valid.
|
* @return {Promise<string>} returns the built email HTML if all provided parameters were valid.
|
||||||
*/
|
*/
|
||||||
async function buildEmail(purpose, email, user) {
|
async function buildEmail(purpose, email, context, { user, contents } = {}) {
|
||||||
// this isn't a full email
|
// this isn't a full email
|
||||||
if (FULL_EMAIL_PURPOSES.indexOf(purpose) === -1) {
|
if (FULL_EMAIL_PURPOSES.indexOf(purpose) === -1) {
|
||||||
throw `Unable to build an email of type ${purpose}`
|
throw `Unable to build an email of type ${purpose}`
|
||||||
}
|
}
|
||||||
let [base, styles, body] = await Promise.all([
|
let [base, body] = await Promise.all([
|
||||||
getTemplateByPurpose(TYPE, EmailTemplatePurpose.BASE),
|
getTemplateByPurpose(TYPE, EmailTemplatePurpose.BASE),
|
||||||
getTemplateByPurpose(TYPE, EmailTemplatePurpose.STYLES),
|
|
||||||
getTemplateByPurpose(TYPE, purpose),
|
getTemplateByPurpose(TYPE, purpose),
|
||||||
])
|
])
|
||||||
if (!base || !styles || !body) {
|
if (!base || !body) {
|
||||||
throw "Unable to build email, missing base components"
|
throw "Unable to build email, missing base components"
|
||||||
}
|
}
|
||||||
base = base.contents
|
base = base.contents
|
||||||
styles = styles.contents
|
|
||||||
body = body.contents
|
body = body.contents
|
||||||
|
context = {
|
||||||
// if there is a link code needed this will retrieve it
|
...context,
|
||||||
const code = await getLinkCode(purpose, email, user)
|
contents,
|
||||||
const context = {
|
|
||||||
...(await getSettingsTemplateContext(purpose, code)),
|
|
||||||
email,
|
email,
|
||||||
user: user || {},
|
user: user || {},
|
||||||
}
|
}
|
||||||
|
|
||||||
body = await processString(body, context)
|
body = await processString(body, context)
|
||||||
styles = await processString(styles, context)
|
|
||||||
// this should now be the complete email HTML
|
// this should now be the complete email HTML
|
||||||
return processString(base, {
|
return processString(base, {
|
||||||
...context,
|
...context,
|
||||||
styles,
|
|
||||||
body,
|
body,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -117,24 +128,38 @@ exports.isEmailConfigured = async (groupId = null) => {
|
||||||
* @param {string} email The email address to send to.
|
* @param {string} email The email address to send to.
|
||||||
* @param {string} purpose The purpose of the email being sent (e.g. reset password).
|
* @param {string} purpose The purpose of the email being sent (e.g. reset password).
|
||||||
* @param {string|undefined} groupId If finer grain controls being used then this will lookup config for group.
|
* @param {string|undefined} groupId If finer grain controls being used then this will lookup config for group.
|
||||||
* @param {object|undefined} user if sending to an existing user the object can be provided, this is used in the context.
|
* @param {object|undefined} user If sending to an existing user the object can be provided, this is used in the context.
|
||||||
|
* @param {string|undefined} from If sending from an address that is not what is configured in the SMTP config.
|
||||||
|
* @param {string|undefined} contents If sending a custom email then can supply contents which will be added to it.
|
||||||
|
* @param {string|undefined} subject A custom subject can be specified if the config one is not desired.
|
||||||
* @return {Promise<object>} returns details about the attempt to send email, e.g. if it is successful; based on
|
* @return {Promise<object>} returns details about the attempt to send email, e.g. if it is successful; based on
|
||||||
* nodemailer response.
|
* nodemailer response.
|
||||||
*/
|
*/
|
||||||
exports.sendEmail = async (email, purpose, { groupId, user } = {}) => {
|
exports.sendEmail = async (
|
||||||
|
email,
|
||||||
|
purpose,
|
||||||
|
{ groupId, user, from, contents, subject } = {}
|
||||||
|
) => {
|
||||||
const db = new CouchDB(GLOBAL_DB)
|
const db = new CouchDB(GLOBAL_DB)
|
||||||
const config = await getSmtpConfiguration(db, groupId)
|
let config = (await getSmtpConfiguration(db, groupId)) || {}
|
||||||
if (!config) {
|
if (Object.keys(config).length === 0 && !TEST_MODE) {
|
||||||
throw "Unable to find SMTP configuration."
|
throw "Unable to find SMTP configuration."
|
||||||
}
|
}
|
||||||
const transport = createSMTPTransport(config)
|
const transport = createSMTPTransport(config)
|
||||||
|
// if there is a link code needed this will retrieve it
|
||||||
|
const code = await getLinkCode(purpose, email, user)
|
||||||
|
const context = await getSettingsTemplateContext(purpose, code)
|
||||||
const message = {
|
const message = {
|
||||||
from: config.from,
|
from: from || config.from,
|
||||||
subject: config.subject,
|
subject: await processString(subject || config.subject, context),
|
||||||
to: email,
|
to: email,
|
||||||
html: await buildEmail(purpose, email, user),
|
html: await buildEmail(purpose, email, context, { user, contents }),
|
||||||
}
|
}
|
||||||
return transport.sendMail(message)
|
const response = await transport.sendMail(message)
|
||||||
|
if (TEST_MODE) {
|
||||||
|
console.log("Test email URL: " + nodemailer.getTestMessageUrl(response))
|
||||||
|
}
|
||||||
|
return response
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -15,8 +15,8 @@ const BASE_COMPANY = "Budibase"
|
||||||
exports.getSettingsTemplateContext = async (purpose, code = null) => {
|
exports.getSettingsTemplateContext = async (purpose, code = null) => {
|
||||||
const db = new CouchDB(StaticDatabases.GLOBAL.name)
|
const db = new CouchDB(StaticDatabases.GLOBAL.name)
|
||||||
// TODO: use more granular settings in the future if required
|
// TODO: use more granular settings in the future if required
|
||||||
const settings = await getScopedConfig(db, { type: Configs.SETTINGS })
|
let settings = (await getScopedConfig(db, { type: Configs.SETTINGS })) || {}
|
||||||
if (!settings.platformUrl) {
|
if (!settings || !settings.platformUrl) {
|
||||||
settings.platformUrl = LOCAL_URL
|
settings.platformUrl = LOCAL_URL
|
||||||
}
|
}
|
||||||
const URL = settings.platformUrl
|
const URL = settings.platformUrl
|
||||||
|
@ -41,7 +41,7 @@ exports.getSettingsTemplateContext = async (purpose, code = null) => {
|
||||||
break
|
break
|
||||||
case EmailTemplatePurpose.INVITATION:
|
case EmailTemplatePurpose.INVITATION:
|
||||||
context[TemplateBindings.INVITE_CODE] = code
|
context[TemplateBindings.INVITE_CODE] = code
|
||||||
context[TemplateBindings.REGISTRATION_URL] = checkSlashesInUrl(
|
context[TemplateBindings.INVITE_URL] = checkSlashesInUrl(
|
||||||
`${URL}/invite?code=${code}`
|
`${URL}/invite?code=${code}`
|
||||||
)
|
)
|
||||||
break
|
break
|
||||||
|
|
Loading…
Reference in New Issue