Merge branch 'develop' into bug/sev2/mongodb-fixes
This commit is contained in:
commit
4d217bfc04
|
@ -119,6 +119,8 @@ This job is responsible for deploying to our production, cloud kubernetes enviro
|
||||||
|
|
||||||
## Pro
|
## Pro
|
||||||
|
|
||||||
|
| **NOTE**: When developing for both pro / budibase repositories, your branch names need to match, or else the correct pro doesn't get run within your CI job.
|
||||||
|
|
||||||
### Installing Pro
|
### Installing Pro
|
||||||
|
|
||||||
The pro package is always installed from source in our CI jobs.
|
The pro package is always installed from source in our CI jobs.
|
||||||
|
@ -132,7 +134,7 @@ This is done to prevent pro needing to be published prior to CI runs in budiabse
|
||||||
- backend-core lives in the monorepo, so it can't be released independently to be used in pro
|
- backend-core lives in the monorepo, so it can't be released independently to be used in pro
|
||||||
- therefore the only option is to pull pro from source and release it as a part of the monorepo release, as if it were a mono package
|
- therefore the only option is to pull pro from source and release it as a part of the monorepo release, as if it were a mono package
|
||||||
|
|
||||||
The install is performed using the same steps as local development, via the `yarn bootstrap` command, see the [Contributing Guide#Pro](../CONTRIBUTING.md#pro)
|
The install is performed using the same steps as local development, via the `yarn bootstrap` command, see the [Contributing Guide#Pro](../../docs/CONTRIBUTING.md#pro)
|
||||||
|
|
||||||
The branch to install pro from can vary depending on ref of the commit that triggered the budibase CI job. This is done to enable branches which have changes in both the monorepo and the pro repo to have their CI pass successfully.
|
The branch to install pro from can vary depending on ref of the commit that triggered the budibase CI job. This is done to enable branches which have changes in both the monorepo and the pro repo to have their CI pass successfully.
|
||||||
|
|
||||||
|
|
|
@ -18,8 +18,9 @@ on:
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
env:
|
env:
|
||||||
# Posthog token used by ui at build time
|
# Posthog token used by ui at build time
|
||||||
POSTHOG_TOKEN: phc_uDYOfnFt6wAbBAXkC6STjcrTpAFiWIhqgFcsC1UVO5F
|
# disable unless needed for testing
|
||||||
|
# POSTHOG_TOKEN: phc_uDYOfnFt6wAbBAXkC6STjcrTpAFiWIhqgFcsC1UVO5F
|
||||||
INTERCOM_TOKEN: ${{ secrets.INTERCOM_TOKEN }}
|
INTERCOM_TOKEN: ${{ secrets.INTERCOM_TOKEN }}
|
||||||
PERSONAL_ACCESS_TOKEN: ${{ secrets.PERSONAL_ACCESS_TOKEN }}
|
PERSONAL_ACCESS_TOKEN: ${{ secrets.PERSONAL_ACCESS_TOKEN }}
|
||||||
FEATURE_PREVIEW_URL: https://budirelease.live
|
FEATURE_PREVIEW_URL: https://budirelease.live
|
||||||
|
|
|
@ -169,7 +169,7 @@ If you have a question or would like to talk with other Budibase users and join
|
||||||
|
|
||||||
## ❗ Code of conduct
|
## ❗ 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.
|
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/docs/CODE_OF_CONDUCT.md). Please read it.
|
||||||
<br />
|
<br />
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -4,10 +4,10 @@ From opening a bug report to creating a pull request: every contribution is appr
|
||||||
|
|
||||||
## Table of contents
|
## Table of contents
|
||||||
|
|
||||||
- [Quick start](#quick-start)
|
- [Where to start](#not-sure-where-to-start)
|
||||||
- [Status](#status)
|
- [Contributor Licence Agreement](#contributor-license-agreement-cla)
|
||||||
- [What's included](#whats-included)
|
- [Glossary of Terms](#glossary-of-terms)
|
||||||
- [Bugs and feature requests](#bugs-and-feature-requests)
|
- [Contributing to Budibase](#contributing-to-budibase)
|
||||||
|
|
||||||
|
|
||||||
## Not Sure Where to Start?
|
## Not Sure Where to Start?
|
||||||
|
@ -32,6 +32,9 @@ All contributors must sign an [Individual Contributor License Agreement](https:/
|
||||||
|
|
||||||
If contributing on behalf of your company, your company must sign a [Corporate Contributor License Agreement](https://github.com/budibase/budibase/blob/next/.github/cla/corporate-cla.md). If so, please contact us via community@budibase.com.
|
If contributing on behalf of your company, your company must sign a [Corporate Contributor License Agreement](https://github.com/budibase/budibase/blob/next/.github/cla/corporate-cla.md). If so, please contact us via community@budibase.com.
|
||||||
|
|
||||||
|
If for any reason, your first contribution is in a PR created by other contributor, please just add a comment to the PR
|
||||||
|
with the following text to agree our CLA: "I have read the CLA Document and I hereby sign the CLA".
|
||||||
|
|
||||||
## Glossary of Terms
|
## Glossary of Terms
|
||||||
|
|
||||||
To understand the budibase API, it can be helpful to understand the top level entities that make up Budibase.
|
To understand the budibase API, it can be helpful to understand the top level entities that make up Budibase.
|
||||||
|
@ -162,7 +165,10 @@ When you are running locally, budibase stores data on disk using docker volumes.
|
||||||
|
|
||||||
### Development Modes
|
### Development Modes
|
||||||
|
|
||||||
A combination of environment variables controls the mode budibase runs in.
|
A combination of environment variables controls the mode budibase runs in.
|
||||||
|
|
||||||
|
| **NOTE**: You need to clean your browser cookies when you change between different modes.
|
||||||
|
|
||||||
Yarn commands can be used to mimic the different modes as described in the sections below:
|
Yarn commands can be used to mimic the different modes as described in the sections below:
|
||||||
|
|
||||||
#### Self Hosted
|
#### Self Hosted
|
||||||
|
@ -189,7 +195,7 @@ To enable this mode, use:
|
||||||
yarn mode:account
|
yarn mode:account
|
||||||
```
|
```
|
||||||
### CI
|
### CI
|
||||||
An overview of the CI pipelines can be found [here](./workflows/README.md)
|
An overview of the CI pipelines can be found [here](../.github/workflows/README.md)
|
||||||
|
|
||||||
### Pro
|
### Pro
|
||||||
|
|
||||||
|
|
|
@ -4,6 +4,11 @@
|
||||||
|
|
||||||
Install instructions [here](https://brew.sh/)
|
Install instructions [here](https://brew.sh/)
|
||||||
|
|
||||||
|
| **NOTE**: If you are working on a M1 Apple Silicon which is running Z shell, you could need to add
|
||||||
|
`eval $(/opt/homebrew/bin/brew shellenv)` line to your `.zshrc`. This will make your zsh to find the apps you install
|
||||||
|
through brew.
|
||||||
|
|
||||||
|
|
||||||
### Install Node
|
### Install Node
|
||||||
|
|
||||||
Budibase requires a recent version of node (14+):
|
Budibase requires a recent version of node (14+):
|
||||||
|
@ -51,4 +56,7 @@ So this command will actually run the application in dev mode. It creates .env f
|
||||||
|
|
||||||
The dev version will be available on port 10000 i.e.
|
The dev version will be available on port 10000 i.e.
|
||||||
|
|
||||||
http://127.0.0.1:10000/builder/admin
|
http://127.0.0.1:10000/builder/admin
|
||||||
|
|
||||||
|
| **NOTE**: If you are working on a M1 Apple Silicon, you will need to uncomment `# platform: linux/amd64` line in
|
||||||
|
[hosting/docker-compose-dev.yaml](../hosting/docker-compose.dev.yaml)
|
|
@ -8,10 +8,11 @@
|
||||||
</h1>
|
</h1>
|
||||||
|
|
||||||
<h3 align="center">
|
<h3 align="center">
|
||||||
Construye herramientas empresariales personalizadas en cuestión de minutos y en su propia infraestructura.
|
Construye herramientas empresariales personalizadas en cuestión de minutos y en tu propia infraestructura.
|
||||||
</h3>
|
</h3>
|
||||||
<p align="center">
|
<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
|
Budibase es una plataforma low code de código abierto, que ayuda a desarrolladores y profesionales de TI a crear y
|
||||||
|
automatizar aplicaciones personalizadas en cuestión de minutos
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<h3 align="center">
|
<h3 align="center">
|
||||||
|
@ -20,7 +21,7 @@
|
||||||
|
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<img src="https://i.imgur.com/tPQHruf.png">
|
<img alt="Budibase design ui" src="https://res.cloudinary.com/daog6scxm/image/upload/v1633524049/ui/design-ui-wide-mobile_gdaveq.jpg">
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
|
@ -30,9 +31,6 @@
|
||||||
<a href="https://github.com/Budibase/budibase/releases">
|
<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">
|
<img alt="GitHub release (latest by date)" src="https://img.shields.io/github/v/release/Budibase/budibase">
|
||||||
</a>
|
</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">
|
<a href="https://twitter.com/intent/follow?screen_name=budibase">
|
||||||
<img src="https://img.shields.io/twitter/follow/budibase?style=social" alt="Follow @budibase" />
|
<img src="https://img.shields.io/twitter/follow/budibase?style=social" alt="Follow @budibase" />
|
||||||
</a>
|
</a>
|
||||||
|
@ -43,130 +41,213 @@
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<h3 align="center">
|
<h3 align="center">
|
||||||
<a href="https://portal.budi.live/signup">Sign-up</a>
|
<a href="https://account.budibase.app/register">Comenzar con Budibase en la nube</a>
|
||||||
<span> · </span>
|
<span> · </span>
|
||||||
<a href="https://docs.budibase.com">Docs</a>
|
<a href="https://docs.budibase.com/docs/hosting-methods">Comenzar con Docker, K8s, DO</a>
|
||||||
<span> · </span>
|
<span> · </span>
|
||||||
<a href="https://github.com/Budibase/budibase/discussions?discussions_q=category%3AIdeas">Feature request</a>
|
<a href="https://docs.budibase.com/docs">Documentaciones</a>
|
||||||
<span> · </span>
|
<span> · </span>
|
||||||
<a href="https://github.com/Budibase/budibase/issues">Report a bug</a>
|
<a href="https://github.com/Budibase/budibase/discussions?discussions_q=category%3AIdeas">Pedir una funcionalidad</a>
|
||||||
<span> · </span>
|
<span> · </span>
|
||||||
Support: <a href="https://github.com/Budibase/budibase/discussions">Discussions</a>
|
<a href="https://github.com/Budibase/budibase/issues">Reportar un error</a>
|
||||||
<span> & </span>
|
<span> · </span>
|
||||||
<a href="https://discord.gg/rCYayfe">Discord</a>
|
Support: <a href="https://github.com/Budibase/budibase/discussions">Comunidad</a>
|
||||||
</h3>
|
</h3>
|
||||||
|
|
||||||
|
<br /><br />
|
||||||
|
## ✨ Caracteristicas
|
||||||
|
|
||||||
## ✨ Features
|
### Construir aplicaciones reales
|
||||||
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.
|
Con Budibase podras construir aplicaciones de pagina unica de gran rendimiento. Ademas, puedes hacerlas con un diseño
|
||||||
|
adaptativo para darles a tus usuarios una gran experiencia.
|
||||||
|
<br /><br />
|
||||||
|
|
||||||
- **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.
|
### Codigo abierto y ampliable
|
||||||
|
Budibase es de codigo abierto con licencia GPL v3. Puedes ampliarlo o modificarlo para adaptarlo a tus necesidades y preferencias.
|
||||||
|
|
||||||
- **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.
|
De esta manera proveemos una buena experiencia para el desarrollador asi como establecemos la confianza de que Budibase siempre estara funcional.
|
||||||
|
<br /><br />
|
||||||
|
|
||||||
- **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).
|
### Cargar informacion o empezar desde cero
|
||||||
|
Budibase permite importar datos desde multiples fuentes, entre las que estan incluidas: MondoDB, CouchDB, PostgreSQL, MySQL,
|
||||||
|
Airtable, S3, DynamoDB o API REST.
|
||||||
|
|
||||||
- **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).
|
O si lo prefieres, con Budibase puedes empezar desde cero y construir tus propias aplicaciones
|
||||||
|
sin necesidad de herramientas externas.
|
||||||
- **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).
|
[Sugerir fuente de datos](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">
|
<p align="center">
|
||||||
<img alt="Budibase design ui" src="https://imgur.com/v8m6v3q.png">
|
<img alt="Budibase data" src="https://res.cloudinary.com/daog6scxm/image/upload/v1636970242/Out%20of%20beta%20launch/data_n1tlhf.png">
|
||||||
</p>
|
</p>
|
||||||
|
<br /><br />
|
||||||
|
|
||||||
|
### Diseña y construye aplicaciones con componentes profesionales prediseñados
|
||||||
|
|
||||||
## ⌛ Status
|
Budibase incorpora componentes profesionales prediseñados que podras usar de manera facil e intuitiva
|
||||||
- [x] Alpha: We are demoing Budibase to users and receiving feedback
|
como bloques de construccion para la interfaz de tu aplicacion.
|
||||||
- [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.
|
Tambien mostramos gran parte del CSS para que puedas adaptar los componentes a tus diseños.
|
||||||
|
[Sugerir componente](https://github.com/Budibase/budibase/discussions?discussions_q=category%3AIdeas).
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<img src="https://i.imgur.com/cJpgqm8.png">
|
<img alt="Budibase design" src="https://res.cloudinary.com/daog6scxm/image/upload/v1636970243/Out%20of%20beta%20launch/design-like-a-pro_qhlfeu.gif">
|
||||||
</p>
|
</p>
|
||||||
|
<br /><br />
|
||||||
|
|
||||||
### Stargazers over time
|
### Procesos automatizados, integra tu aplicacion con otras herramientas y conectala a eventos webhook
|
||||||
|
|
||||||
|
Ahorra tiempo automatizando flujos de trabajo y procesos manuales. Podras desde conectar eventos webhook hasta automatizar emails,
|
||||||
|
simplemente dile a Budibase que hacer y deja que el haga el trabajo por ti.
|
||||||
|
[Crear nuevos procesos automatizados](https://github.com/Budibase/automations) o [Sugerir proceso automatizado](https://github.com/Budibase/budibase/discussions?discussions_q=category%3AIdeas).
|
||||||
|
|
||||||
|
<p align="center">
|
||||||
|
<img alt="Budibase automations" src="https://res.cloudinary.com/daog6scxm/image/upload/v1636970486/Out%20of%20beta%20launch/automation_riro7u.png">
|
||||||
|
</p>
|
||||||
|
<br /><br />
|
||||||
|
|
||||||
|
### Tus herramientas favoritas
|
||||||
|
|
||||||
|
Budibase integra un gran numero de herramientas que te permitiran construir tus aplicaciones ajustandose a tus preferencias.
|
||||||
|
|
||||||
|
<p align="center">
|
||||||
|
<img alt="Budibase integrations" src="https://res.cloudinary.com/daog6scxm/image/upload/v1636970242/Out%20of%20beta%20launch/integrations_kc7dqt.png">
|
||||||
|
</p>
|
||||||
|
<br /><br />
|
||||||
|
|
||||||
|
### Un paraiso para administradores
|
||||||
|
|
||||||
|
Puedes albergar Budibase en tu propia infraestructura y gestionar globalmente usuarios, incorporaciones, SMTP, aplicaciones,
|
||||||
|
grupos, diseños de temas, etc.
|
||||||
|
|
||||||
|
Tambien puedes gestionar los usuarios y grupos, o delegar en personas asignadas para ello, desde nuestra aplicacion sin
|
||||||
|
mucho esfuerzo.
|
||||||
|
|
||||||
|
Budibase is made to scale. With Budibase, you can self-host on your own infrastructure and globally manage users, onboarding, SMTP, apps, groups, theming and more. You can also provide users/groups with an app portal and disseminate user-management to the group manager.
|
||||||
|
|
||||||
|
- Video Promocional: https://youtu.be/xoljVpty_Kw
|
||||||
|
|
||||||
|
<br />
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<br />
|
||||||
|
|
||||||
|
|
||||||
|
## Budibase API Publica
|
||||||
|
|
||||||
|
Como todo lo que construimos en Budibase, nuestra nueva API publica es facil de usar, flexible e introduce nueva ampliacion
|
||||||
|
del sistema. Budibase API ofrece:
|
||||||
|
- Uso de Budibase como backend
|
||||||
|
- Interoperabilidad
|
||||||
|
|
||||||
|
#### Documentacion
|
||||||
|
|
||||||
|
Puedes aprender mas acerca de Budibase API en los siguientes documentos:
|
||||||
|
- [Documentacion general](https://docs.budibase.com/docs/public-api) : Como optener tu clave para la API, usar Insomnia y Postman
|
||||||
|
- [API Interactiva](https://docs.budibase.com/reference/post_applications) : Aprende como trabajar con la API
|
||||||
|
|
||||||
|
#### Guias
|
||||||
|
|
||||||
|
- [Construye una aplicacion con Budibase y Next.js](https://budibase.com/blog/building-a-crud-app-with-budibase-and-next.js/)
|
||||||
|
|
||||||
|
<p align="center">
|
||||||
|
<img alt="Budibase data" src="https://res.cloudinary.com/daog6scxm/image/upload/v1647858558/Feb%20release/Start_building_with_Budibase_s_API_3_rhlzhv.png">
|
||||||
|
</p>
|
||||||
|
<br /><br />
|
||||||
|
|
||||||
|
<br /><br /><br />
|
||||||
|
|
||||||
|
## 🏁 Comenzar con Budibase
|
||||||
|
|
||||||
|
Puedes alojar Budibase en tu propia infraestructura con Docker, Kubernetes o Digital Ocean; o usa Budibase en la nube si
|
||||||
|
quieres empezar a crear tus aplicaciones rapidamente y sin ningun tipo de preocupacion.
|
||||||
|
|
||||||
|
### [Comenzar con Budibase self-hosting](https://docs.budibase.com/docs/hosting-methods)
|
||||||
|
|
||||||
|
- [Docker - single ARM compatible image](https://docs.budibase.com/docs/docker)
|
||||||
|
- [Docker Compose](https://docs.budibase.com/docs/docker-compose)
|
||||||
|
- [Kubernetes](https://docs.budibase.com/docs/kubernetes-k8s)
|
||||||
|
- [Digital Ocean](https://docs.budibase.com/docs/digitalocean)
|
||||||
|
- [Portainer](https://docs.budibase.com/docs/portainer)
|
||||||
|
|
||||||
|
|
||||||
|
### [Comenzar con Budibase en la nube](https://budibase.com)
|
||||||
|
|
||||||
|
<br /><br />
|
||||||
|
|
||||||
|
## 🎓 Aprende a usar Budibase
|
||||||
|
|
||||||
|
Aqui tienes la [documentacion de Budibase](https://docs.budibase.com/docs).
|
||||||
|
<br />
|
||||||
|
|
||||||
|
|
||||||
|
<br /><br />
|
||||||
|
|
||||||
|
## 💬 Comunidad
|
||||||
|
|
||||||
|
Te invitamos a que te unas a nuestra comunidad de Budibase, alli podras hacer las preguntas que quieras, ayudar a otras
|
||||||
|
personas o tener una charla entretenida con otros usuarios de Budibase.
|
||||||
|
[Acceder a la comunidad de Budibase](https://github.com/Budibase/budibase/discussions)
|
||||||
|
<br /><br /><br />
|
||||||
|
|
||||||
|
|
||||||
|
## ❗ Codigo de conducta
|
||||||
|
|
||||||
|
Budibase presta especial atencion en acoger a personas de toda diversidad y ofrecer un entorno de respeto mutuo. Asi mismo
|
||||||
|
esperamos lo mismo de nuestra comunidad, por favor lee el
|
||||||
|
[**Codigo de conducta**](https://github.com/Budibase/budibase/blob/HEAD/.github/CODE_OF_CONDUCT.md).
|
||||||
|
<br />
|
||||||
|
|
||||||
|
<br /><br />
|
||||||
|
|
||||||
|
|
||||||
|
## 🙌 Contribuir en Budibase
|
||||||
|
|
||||||
|
Desde comunicar un bug a solventar un error en el codigo, toda contribucion es apreciada y bienvenida. Si estas planeando
|
||||||
|
implementar una nueva funcionalidad o un realizar un cambio en la API, por favor crea un [nuevo mensaje aqui](https://github.com/Budibase/budibase/issues),
|
||||||
|
de esta manera nos encargaremos que tu trabajo no sea en vano.
|
||||||
|
|
||||||
|
Aqui tienes instrucciones de como configurar tu entorno Budibase para [Debian](https://github.com/Budibase/budibase/tree/HEAD/docs/DEV-SETUP-DEBIAN.md)
|
||||||
|
y [MacOSX](https://github.com/Budibase/budibase/tree/HEAD/docs/DEV-SETUP-MACOSX.md)
|
||||||
|
|
||||||
|
### No estas seguro por donde empezar?
|
||||||
|
Un buen lugar para empezar a contribuir con nosotros es [aqui](https://github.com/Budibase/budibase/projects/22).
|
||||||
|
|
||||||
|
### Organizacion del repositorio
|
||||||
|
|
||||||
|
Budibase es un repositorio unico gestionado por Lerna. Lerna construye y publica los paquetes de Budibase sincronizandolos
|
||||||
|
cada ves que se realiza un cambio. A rasgos generales, estos son los paquetes que conforman Budibase:
|
||||||
|
|
||||||
|
- [packages/builder](https://github.com/Budibase/budibase/tree/HEAD/packages/builder) - contiene el codigo del builder de la parte cliente, esta es una aplicacion svelte.
|
||||||
|
|
||||||
|
- [packages/client](https://github.com/Budibase/budibase/tree/HEAD/packages/client) - Este modulo se ejecuta en el browser y es el responsable de leer definiciones JSON y crear aplicaciones web en el momento.
|
||||||
|
|
||||||
|
- [packages/server](https://github.com/Budibase/budibase/tree/HEAD/packages/server) - La parte servidor de Budibase. Esta aplicacion Koa es responsable de suministrar lo necesario al builder para asi generar las aplicaciones Budibase. Tambien provee una API para interaccionar con la base de datos y el almacenamiento de ficheros.
|
||||||
|
|
||||||
|
Para mas informacion, por favor lee el siguiente documento [CONTRIBUTING.md](https://github.com/Budibase/budibase/blob/HEAD/docs/CONTRIBUTING.md)
|
||||||
|
|
||||||
|
<br /><br />
|
||||||
|
|
||||||
|
|
||||||
|
## 📝 Licencia
|
||||||
|
|
||||||
|
Budibase es open-source, licenciado como [GPL v3](https://www.gnu.org/licenses/gpl-3.0.en.html). El cliente y las librerias
|
||||||
|
de componentes estan licenciadas como [MPL](https://directory.fsf.org/wiki/License:MPL-2.0) - de esta manera, puedes licenciar
|
||||||
|
como tu quieras las aplicaciones que construyas.
|
||||||
|
|
||||||
|
<br /><br />
|
||||||
|
|
||||||
|
## ⭐ Historia de nuestros Stargazers
|
||||||
|
|
||||||
[![Stargazers over time](https://starchart.cc/Budibase/budibase.svg)](https://starchart.cc/Budibase/budibase)
|
[![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.
|
Si estas teniendo problemas con el builder despues de actualizar, por favor [lee esta guia](https://github.com/Budibase/budibase/blob/HEAD/docs/CONTRIBUTING.md#troubleshooting) to clear down your environment.
|
||||||
|
|
||||||
|
<br /><br />
|
||||||
|
|
||||||
## 🏁 Getting Started with Budibase
|
## Contribuidores ✨
|
||||||
|
|
||||||
The Budibase builder runs in Electron, on Mac, PC and Linux. Follow the steps below to get started:
|
Queremos prestar un especial agradecimiento a nuestra maravillosa gente ([emoji key](https://allcontributors.org/docs/en/emoji-key)):
|
||||||
- [ ] [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/docs/hosting-methods).
|
|
||||||
|
|
||||||
[![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 -->
|
<!-- ALL-CONTRIBUTORS-LIST:START - Do not remove or modify this section -->
|
||||||
<!-- prettier-ignore-start -->
|
<!-- prettier-ignore-start -->
|
||||||
|
@ -179,14 +260,18 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d
|
||||||
<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://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://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://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>
|
<td align="center"><a href="https://github.com/Rory-Powell"><img src="https://avatars.githubusercontent.com/u/8755148?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Rory Powell</b></sub></a><br /><a href="https://github.com/Budibase/budibase/commits?author=Rory-Powell" title="Code">💻</a> <a href="https://github.com/Budibase/budibase/commits?author=Rory-Powell" title="Documentation">📖</a> <a href="https://github.com/Budibase/budibase/commits?author=Rory-Powell" title="Tests">⚠️</a></td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
|
<td align="center"><a href="https://github.com/PClmnt"><img src="https://avatars.githubusercontent.com/u/5665926?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Peter Clement</b></sub></a><br /><a href="https://github.com/Budibase/budibase/commits?author=PClmnt" title="Code">💻</a> <a href="https://github.com/Budibase/budibase/commits?author=PClmnt" title="Documentation">📖</a> <a href="https://github.com/Budibase/budibase/commits?author=PClmnt" title="Tests">⚠️</a></td>
|
||||||
|
<td align="center"><a href="https://github.com/Conor-Mack"><img src="https://avatars1.githubusercontent.com/u/36074859?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Conor_Mack</b></sub></a><br /><a href="https://github.com/Budibase/budibase/commits?author=Conor-Mack" title="Code">💻</a> <a href="https://github.com/Budibase/budibase/commits?author=Conor-Mack" title="Tests">⚠️</a></td>
|
||||||
<td align="center"><a href="https://github.com/pngwn"><img src="https://avatars1.githubusercontent.com/u/12937446?v=4?s=100" width="100px;" alt=""/><br /><sub><b>pngwn</b></sub></a><br /><a href="https://github.com/Budibase/budibase/commits?author=pngwn" title="Code">💻</a> <a href="https://github.com/Budibase/budibase/commits?author=pngwn" title="Tests">⚠️</a></td>
|
<td align="center"><a href="https://github.com/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/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/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/yashank09"><img src="https://avatars.githubusercontent.com/u/37672190?v=4?s=100" width="100px;" alt=""/><br /><sub><b>yashank09</b></sub></a><br /><a href="https://github.com/Budibase/budibase/commits?author=yashank09" title="Code">💻</a></td>
|
||||||
<td align="center"><a href="https://github.com/SOVLOOKUP"><img src="https://avatars.githubusercontent.com/u/53158137?v=4?s=100" width="100px;" alt=""/><br /><sub><b>SOVLOOKUP</b></sub></a><br /><a href="https://github.com/Budibase/budibase/commits?author=SOVLOOKUP" title="Code">💻</a></td>
|
<td align="center"><a href="https://github.com/SOVLOOKUP"><img src="https://avatars.githubusercontent.com/u/53158137?v=4?s=100" width="100px;" alt=""/><br /><sub><b>SOVLOOKUP</b></sub></a><br /><a href="https://github.com/Budibase/budibase/commits?author=SOVLOOKUP" title="Code">💻</a></td>
|
||||||
|
<td align="center"><a href="https://github.com/seoulaja"><img src="https://avatars.githubusercontent.com/u/15101654?v=4?s=100" width="100px;" alt=""/><br /><sub><b>seoulaja</b></sub></a><br /><a href="#translation-seoulaja" title="Translation">🌍</a></td>
|
||||||
|
<td align="center"><a href="https://github.com/mslourens"><img src="https://avatars.githubusercontent.com/u/1907152?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Maurits Lourens</b></sub></a><br /><a href="https://github.com/Budibase/budibase/commits?author=mslourens" title="Tests">⚠️</a> <a href="https://github.com/Budibase/budibase/commits?author=mslourens" title="Code">💻</a></td>
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
|
@ -195,4 +280,5 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d
|
||||||
|
|
||||||
<!-- ALL-CONTRIBUTORS-LIST:END -->
|
<!-- ALL-CONTRIBUTORS-LIST:END -->
|
||||||
|
|
||||||
This project follows the [all-contributors](https://github.com/all-contributors/all-contributors) specification. Contributions of any kind welcome!
|
Este proyecto sigue las especificaciones de [all-contributors](https://github.com/all-contributors/all-contributors).
|
||||||
|
Todo tipo de contribuciones son agradecidas!
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
{
|
{
|
||||||
"version": "1.1.22-alpha.0",
|
"version": "1.2.20-alpha.1",
|
||||||
"npmClient": "yarn",
|
"npmClient": "yarn",
|
||||||
"packages": [
|
"packages": [
|
||||||
"packages/*"
|
"packages/*"
|
||||||
|
|
|
@ -26,7 +26,7 @@
|
||||||
"build": "lerna run build",
|
"build": "lerna run build",
|
||||||
"build:dev": "lerna run prebuild && tsc --build --watch --preserveWatchOutput",
|
"build:dev": "lerna run prebuild && tsc --build --watch --preserveWatchOutput",
|
||||||
"release": "lerna publish ${RELEASE_VERSION_TYPE:-patch} --yes --force-publish && yarn release:pro",
|
"release": "lerna publish ${RELEASE_VERSION_TYPE:-patch} --yes --force-publish && yarn release:pro",
|
||||||
"release:develop": "lerna publish prerelease --yes --force-publish --dist-tag develop && yarn release:pro:develop",
|
"release:develop": "lerna publish prerelease --yes --force-publish --dist-tag develop --exact && yarn release:pro:develop",
|
||||||
"release:pro": "bash scripts/pro/release.sh",
|
"release:pro": "bash scripts/pro/release.sh",
|
||||||
"release:pro:develop": "bash scripts/pro/release.sh develop",
|
"release:pro:develop": "bash scripts/pro/release.sh develop",
|
||||||
"restore": "yarn run clean && yarn run bootstrap && yarn run build",
|
"restore": "yarn run clean && yarn run bootstrap && yarn run build",
|
||||||
|
@ -85,4 +85,4 @@
|
||||||
"install:pro": "bash scripts/pro/install.sh",
|
"install:pro": "bash scripts/pro/install.sh",
|
||||||
"dep:clean": "yarn clean && yarn bootstrap"
|
"dep:clean": "yarn clean && yarn bootstrap"
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@budibase/backend-core",
|
"name": "@budibase/backend-core",
|
||||||
"version": "1.1.22-alpha.0",
|
"version": "1.2.20-alpha.1",
|
||||||
"description": "Budibase backend core libraries used in server and worker",
|
"description": "Budibase backend core libraries used in server and worker",
|
||||||
"main": "dist/src/index.js",
|
"main": "dist/src/index.js",
|
||||||
"types": "dist/src/index.d.ts",
|
"types": "dist/src/index.d.ts",
|
||||||
|
@ -20,13 +20,14 @@
|
||||||
"test:watch": "jest --watchAll"
|
"test:watch": "jest --watchAll"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@budibase/types": "^1.1.22-alpha.0",
|
"@budibase/types": "1.2.20-alpha.1",
|
||||||
"@techpass/passport-openidconnect": "0.3.2",
|
"@techpass/passport-openidconnect": "0.3.2",
|
||||||
"aws-sdk": "2.1030.0",
|
"aws-sdk": "2.1030.0",
|
||||||
"bcrypt": "5.0.1",
|
"bcrypt": "5.0.1",
|
||||||
"dotenv": "16.0.1",
|
"dotenv": "16.0.1",
|
||||||
"emitter-listener": "1.1.2",
|
"emitter-listener": "1.1.2",
|
||||||
"ioredis": "4.28.0",
|
"ioredis": "4.28.0",
|
||||||
|
"joi": "17.6.0",
|
||||||
"jsonwebtoken": "8.5.1",
|
"jsonwebtoken": "8.5.1",
|
||||||
"koa-passport": "4.1.4",
|
"koa-passport": "4.1.4",
|
||||||
"lodash": "4.17.21",
|
"lodash": "4.17.21",
|
||||||
|
|
|
@ -18,6 +18,10 @@ const {
|
||||||
ssoCallbackUrl,
|
ssoCallbackUrl,
|
||||||
csrf,
|
csrf,
|
||||||
internalApi,
|
internalApi,
|
||||||
|
adminOnly,
|
||||||
|
builderOnly,
|
||||||
|
builderOrAdmin,
|
||||||
|
joiValidator,
|
||||||
} = require("./middleware")
|
} = require("./middleware")
|
||||||
|
|
||||||
const { invalidateUser } = require("./cache/user")
|
const { invalidateUser } = require("./cache/user")
|
||||||
|
@ -173,4 +177,8 @@ module.exports = {
|
||||||
refreshOAuthToken,
|
refreshOAuthToken,
|
||||||
updateUserOAuth,
|
updateUserOAuth,
|
||||||
ssoCallbackUrl,
|
ssoCallbackUrl,
|
||||||
|
adminOnly,
|
||||||
|
builderOnly,
|
||||||
|
builderOrAdmin,
|
||||||
|
joiValidator,
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import BaseCache from "./base"
|
import BaseCache from "./base"
|
||||||
import { getWritethroughClient } from "../redis/init"
|
import { getWritethroughClient } from "../redis/init"
|
||||||
|
import { logWarn } from "../logging"
|
||||||
|
|
||||||
const DEFAULT_WRITE_RATE_MS = 10000
|
const DEFAULT_WRITE_RATE_MS = 10000
|
||||||
let CACHE: BaseCache | null = null
|
let CACHE: BaseCache | null = null
|
||||||
|
@ -51,10 +52,8 @@ export async function put(
|
||||||
if (err.status !== 409) {
|
if (err.status !== 409) {
|
||||||
throw err
|
throw err
|
||||||
} else {
|
} else {
|
||||||
// get the rev, update over it - this is risky, may change in future
|
// Swallow 409s but log them
|
||||||
const readDoc = await db.get(doc._id)
|
logWarn(`Ignoring conflict in write-through cache`)
|
||||||
doc._rev = readDoc._rev
|
|
||||||
await writeDb(doc)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,6 +11,7 @@ export enum AutomationViewModes {
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum ViewNames {
|
export enum ViewNames {
|
||||||
|
USER_BY_APP = "by_app",
|
||||||
USER_BY_EMAIL = "by_email2",
|
USER_BY_EMAIL = "by_email2",
|
||||||
BY_API_KEY = "by_api_key",
|
BY_API_KEY = "by_api_key",
|
||||||
USER_BY_BUILDERS = "by_builders",
|
USER_BY_BUILDERS = "by_builders",
|
||||||
|
@ -28,6 +29,7 @@ export const DeprecatedViews = {
|
||||||
|
|
||||||
export enum DocumentTypes {
|
export enum DocumentTypes {
|
||||||
USER = "us",
|
USER = "us",
|
||||||
|
GROUP = "gr",
|
||||||
WORKSPACE = "workspace",
|
WORKSPACE = "workspace",
|
||||||
CONFIG = "config",
|
CONFIG = "config",
|
||||||
TEMPLATE = "template",
|
TEMPLATE = "template",
|
||||||
|
|
|
@ -50,3 +50,8 @@ exports.getProdAppID = appId => {
|
||||||
const rest = split.join(APP_DEV_PREFIX)
|
const rest = split.join(APP_DEV_PREFIX)
|
||||||
return `${APP_PREFIX}${rest}`
|
return `${APP_PREFIX}${rest}`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
exports.extractAppUUID = id => {
|
||||||
|
const split = id?.split("_") || []
|
||||||
|
return split.length ? split[split.length - 1] : null
|
||||||
|
}
|
||||||
|
|
|
@ -8,7 +8,7 @@ import { doWithDB, allDbs } from "./index"
|
||||||
import { getCouchInfo } from "./pouch"
|
import { getCouchInfo } from "./pouch"
|
||||||
import { getAppMetadata } from "../cache/appMetadata"
|
import { getAppMetadata } from "../cache/appMetadata"
|
||||||
import { checkSlashesInUrl } from "../helpers"
|
import { checkSlashesInUrl } from "../helpers"
|
||||||
import { isDevApp, isDevAppID } from "./conversions"
|
import { isDevApp, isDevAppID, getProdAppID } from "./conversions"
|
||||||
import { APP_PREFIX } from "./constants"
|
import { APP_PREFIX } from "./constants"
|
||||||
import * as events from "../events"
|
import * as events from "../events"
|
||||||
|
|
||||||
|
@ -107,6 +107,15 @@ export function getGlobalUserParams(globalId: any, otherProps: any = {}) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getUsersByAppParams(appId: any, otherProps: any = {}) {
|
||||||
|
const prodAppId = getProdAppID(appId)
|
||||||
|
return {
|
||||||
|
...otherProps,
|
||||||
|
startkey: prodAppId,
|
||||||
|
endkey: `${prodAppId}${UNICODE_MAX}`,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generates a template ID.
|
* Generates a template ID.
|
||||||
* @param ownerId The owner/user of the template, this could be global or a workspace level.
|
* @param ownerId The owner/user of the template, this could be global or a workspace level.
|
||||||
|
@ -115,6 +124,10 @@ export function generateTemplateID(ownerId: any) {
|
||||||
return `${DocumentTypes.TEMPLATE}${SEPARATOR}${ownerId}${SEPARATOR}${newid()}`
|
return `${DocumentTypes.TEMPLATE}${SEPARATOR}${ownerId}${SEPARATOR}${newid()}`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function generateAppUserID(prodAppId: string, userId: string) {
|
||||||
|
return `${prodAppId}${SEPARATOR}${userId}`
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets parameters for retrieving templates. Owner ID must be specified, either global or a workspace level.
|
* Gets parameters for retrieving templates. Owner ID must be specified, either global or a workspace level.
|
||||||
*/
|
*/
|
||||||
|
@ -442,15 +455,29 @@ export const getPlatformUrl = async (opts = { tenantAware: true }) => {
|
||||||
export function pagination(
|
export function pagination(
|
||||||
data: any[],
|
data: any[],
|
||||||
pageSize: number,
|
pageSize: number,
|
||||||
{ paginate, property } = { paginate: true, property: "_id" }
|
{
|
||||||
|
paginate,
|
||||||
|
property,
|
||||||
|
getKey,
|
||||||
|
}: {
|
||||||
|
paginate: boolean
|
||||||
|
property: string
|
||||||
|
getKey?: (doc: any) => string | undefined
|
||||||
|
} = {
|
||||||
|
paginate: true,
|
||||||
|
property: "_id",
|
||||||
|
}
|
||||||
) {
|
) {
|
||||||
if (!paginate) {
|
if (!paginate) {
|
||||||
return { data, hasNextPage: false }
|
return { data, hasNextPage: false }
|
||||||
}
|
}
|
||||||
const hasNextPage = data.length > pageSize
|
const hasNextPage = data.length > pageSize
|
||||||
let nextPage = undefined
|
let nextPage = undefined
|
||||||
|
if (!getKey) {
|
||||||
|
getKey = (doc: any) => (property ? doc?.[property] : doc?._id)
|
||||||
|
}
|
||||||
if (hasNextPage) {
|
if (hasNextPage) {
|
||||||
nextPage = property ? data[pageSize]?.[property] : data[pageSize]?._id
|
nextPage = getKey(data[pageSize])
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
data: data.slice(0, pageSize),
|
data: data.slice(0, pageSize),
|
||||||
|
|
|
@ -56,6 +56,33 @@ exports.createNewUserEmailView = async () => {
|
||||||
await db.put(designDoc)
|
await db.put(designDoc)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
exports.createUserAppView = async () => {
|
||||||
|
const db = getGlobalDB()
|
||||||
|
let designDoc
|
||||||
|
try {
|
||||||
|
designDoc = await db.get("_design/database")
|
||||||
|
} catch (err) {
|
||||||
|
// no design doc, make one
|
||||||
|
designDoc = DesignDoc()
|
||||||
|
}
|
||||||
|
const view = {
|
||||||
|
// if using variables in a map function need to inject them before use
|
||||||
|
map: `function(doc) {
|
||||||
|
if (doc._id.startsWith("${DocumentTypes.USER}${SEPARATOR}") && doc.roles) {
|
||||||
|
for (let prodAppId of Object.keys(doc.roles)) {
|
||||||
|
let emitted = prodAppId + "${SEPARATOR}" + doc._id
|
||||||
|
emit(emitted, null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}`,
|
||||||
|
}
|
||||||
|
designDoc.views = {
|
||||||
|
...designDoc.views,
|
||||||
|
[ViewNames.USER_BY_APP]: view,
|
||||||
|
}
|
||||||
|
await db.put(designDoc)
|
||||||
|
}
|
||||||
|
|
||||||
exports.createApiKeyView = async () => {
|
exports.createApiKeyView = async () => {
|
||||||
const db = getGlobalDB()
|
const db = getGlobalDB()
|
||||||
let designDoc
|
let designDoc
|
||||||
|
@ -106,6 +133,7 @@ exports.queryGlobalView = async (viewName, params, db = null) => {
|
||||||
[ViewNames.USER_BY_EMAIL]: exports.createNewUserEmailView,
|
[ViewNames.USER_BY_EMAIL]: exports.createNewUserEmailView,
|
||||||
[ViewNames.BY_API_KEY]: exports.createApiKeyView,
|
[ViewNames.BY_API_KEY]: exports.createApiKeyView,
|
||||||
[ViewNames.USER_BY_BUILDERS]: exports.createUserBuildersView,
|
[ViewNames.USER_BY_BUILDERS]: exports.createUserBuildersView,
|
||||||
|
[ViewNames.USER_BY_APP]: exports.createUserAppView,
|
||||||
}
|
}
|
||||||
// can pass DB in if working with something specific
|
// can pass DB in if working with something specific
|
||||||
if (!db) {
|
if (!db) {
|
||||||
|
|
|
@ -55,6 +55,8 @@ const env = {
|
||||||
DEFAULT_LICENSE: process.env.DEFAULT_LICENSE,
|
DEFAULT_LICENSE: process.env.DEFAULT_LICENSE,
|
||||||
SERVICE: process.env.SERVICE || "budibase",
|
SERVICE: process.env.SERVICE || "budibase",
|
||||||
MEMORY_LEAK_CHECK: process.env.MEMORY_LEAK_CHECK || false,
|
MEMORY_LEAK_CHECK: process.env.MEMORY_LEAK_CHECK || false,
|
||||||
|
LOG_LEVEL: process.env.LOG_LEVEL,
|
||||||
|
SESSION_UPDATE_PERIOD: process.env.SESSION_UPDATE_PERIOD,
|
||||||
DEPLOYMENT_ENVIRONMENT:
|
DEPLOYMENT_ENVIRONMENT:
|
||||||
process.env.DEPLOYMENT_ENVIRONMENT || "docker-compose",
|
process.env.DEPLOYMENT_ENVIRONMENT || "docker-compose",
|
||||||
_set(key: any, value: any) {
|
_set(key: any, value: any) {
|
||||||
|
|
|
@ -37,6 +37,7 @@ module.exports = {
|
||||||
types,
|
types,
|
||||||
errors: {
|
errors: {
|
||||||
UsageLimitError: licensing.UsageLimitError,
|
UsageLimitError: licensing.UsageLimitError,
|
||||||
|
FeatureDisabledError: licensing.FeatureDisabledError,
|
||||||
HTTPError: http.HTTPError,
|
HTTPError: http.HTTPError,
|
||||||
},
|
},
|
||||||
getPublicError,
|
getPublicError,
|
||||||
|
|
|
@ -4,6 +4,7 @@ const type = "license_error"
|
||||||
|
|
||||||
const codes = {
|
const codes = {
|
||||||
USAGE_LIMIT_EXCEEDED: "usage_limit_exceeded",
|
USAGE_LIMIT_EXCEEDED: "usage_limit_exceeded",
|
||||||
|
FEATURE_DISABLED: "feature_disabled",
|
||||||
}
|
}
|
||||||
|
|
||||||
const context = {
|
const context = {
|
||||||
|
@ -12,6 +13,11 @@ const context = {
|
||||||
limitName: err.limitName,
|
limitName: err.limitName,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
[codes.FEATURE_DISABLED]: err => {
|
||||||
|
return {
|
||||||
|
featureName: err.featureName,
|
||||||
|
}
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
class UsageLimitError extends HTTPError {
|
class UsageLimitError extends HTTPError {
|
||||||
|
@ -21,9 +27,17 @@ class UsageLimitError extends HTTPError {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class FeatureDisabledError extends HTTPError {
|
||||||
|
constructor(message, featureName) {
|
||||||
|
super(message, 400, codes.FEATURE_DISABLED, type)
|
||||||
|
this.featureName = featureName
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
type,
|
type,
|
||||||
codes,
|
codes,
|
||||||
context,
|
context,
|
||||||
UsageLimitError,
|
UsageLimitError,
|
||||||
|
FeatureDisabledError,
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,6 +5,22 @@ import env from "../../environment"
|
||||||
import * as context from "../../context"
|
import * as context from "../../context"
|
||||||
const pkg = require("../../../package.json")
|
const pkg = require("../../../package.json")
|
||||||
|
|
||||||
|
const EXCLUDED_EVENTS: Event[] = [
|
||||||
|
Event.USER_UPDATED,
|
||||||
|
Event.EMAIL_SMTP_UPDATED,
|
||||||
|
Event.AUTH_SSO_UPDATED,
|
||||||
|
Event.APP_UPDATED,
|
||||||
|
Event.ROLE_UPDATED,
|
||||||
|
Event.DATASOURCE_UPDATED,
|
||||||
|
Event.QUERY_UPDATED,
|
||||||
|
Event.TABLE_UPDATED,
|
||||||
|
Event.VIEW_UPDATED,
|
||||||
|
Event.VIEW_FILTER_UPDATED,
|
||||||
|
Event.VIEW_CALCULATION_UPDATED,
|
||||||
|
Event.AUTOMATION_TRIGGER_UPDATED,
|
||||||
|
Event.USER_GROUP_UPDATED,
|
||||||
|
]
|
||||||
|
|
||||||
export default class PosthogProcessor implements EventProcessor {
|
export default class PosthogProcessor implements EventProcessor {
|
||||||
posthog: PostHog
|
posthog: PostHog
|
||||||
|
|
||||||
|
@ -21,6 +37,11 @@ export default class PosthogProcessor implements EventProcessor {
|
||||||
properties: BaseEvent,
|
properties: BaseEvent,
|
||||||
timestamp?: string | number
|
timestamp?: string | number
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
|
// don't send excluded events
|
||||||
|
if (EXCLUDED_EVENTS.includes(event)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
properties.version = pkg.version
|
properties.version = pkg.version
|
||||||
properties.service = env.SERVICE
|
properties.service = env.SERVICE
|
||||||
properties.environment = identity.environment
|
properties.environment = identity.environment
|
||||||
|
|
|
@ -0,0 +1,40 @@
|
||||||
|
import PosthogProcessor from "../PosthogProcessor"
|
||||||
|
import { Event, IdentityType, Hosting } from "@budibase/types"
|
||||||
|
|
||||||
|
const newIdentity = () => {
|
||||||
|
return {
|
||||||
|
id: "test",
|
||||||
|
type: IdentityType.USER,
|
||||||
|
hosting: Hosting.SELF,
|
||||||
|
environment: "test",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("PosthogProcessor", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.clearAllMocks()
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("processEvent", () => {
|
||||||
|
it("processes event", () => {
|
||||||
|
const processor = new PosthogProcessor("test")
|
||||||
|
|
||||||
|
const identity = newIdentity()
|
||||||
|
const properties = {}
|
||||||
|
|
||||||
|
processor.processEvent(Event.APP_CREATED, identity, properties)
|
||||||
|
|
||||||
|
expect(processor.posthog.capture).toHaveBeenCalledTimes(1)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("honours exclusions", () => {
|
||||||
|
const processor = new PosthogProcessor("test")
|
||||||
|
|
||||||
|
const identity = newIdentity()
|
||||||
|
const properties = {}
|
||||||
|
|
||||||
|
processor.processEvent(Event.AUTH_SSO_UPDATED, identity, properties)
|
||||||
|
expect(processor.posthog.capture).toHaveBeenCalledTimes(0)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
|
@ -0,0 +1,64 @@
|
||||||
|
import { publishEvent } from "../events"
|
||||||
|
import {
|
||||||
|
Event,
|
||||||
|
UserGroup,
|
||||||
|
GroupCreatedEvent,
|
||||||
|
GroupDeletedEvent,
|
||||||
|
GroupUpdatedEvent,
|
||||||
|
GroupUsersAddedEvent,
|
||||||
|
GroupUsersDeletedEvent,
|
||||||
|
GroupAddedOnboardingEvent,
|
||||||
|
UserGroupRoles,
|
||||||
|
} from "@budibase/types"
|
||||||
|
|
||||||
|
export async function created(group: UserGroup, timestamp?: number) {
|
||||||
|
const properties: GroupCreatedEvent = {
|
||||||
|
groupId: group._id as string,
|
||||||
|
}
|
||||||
|
await publishEvent(Event.USER_GROUP_CREATED, properties, timestamp)
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function updated(group: UserGroup) {
|
||||||
|
const properties: GroupUpdatedEvent = {
|
||||||
|
groupId: group._id as string,
|
||||||
|
}
|
||||||
|
await publishEvent(Event.USER_GROUP_UPDATED, properties)
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function deleted(group: UserGroup) {
|
||||||
|
const properties: GroupDeletedEvent = {
|
||||||
|
groupId: group._id as string,
|
||||||
|
}
|
||||||
|
await publishEvent(Event.USER_GROUP_DELETED, properties)
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function usersAdded(count: number, group: UserGroup) {
|
||||||
|
const properties: GroupUsersAddedEvent = {
|
||||||
|
count,
|
||||||
|
groupId: group._id as string,
|
||||||
|
}
|
||||||
|
await publishEvent(Event.USER_GROUP_USERS_ADDED, properties)
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function usersDeleted(emails: string[], group: UserGroup) {
|
||||||
|
const properties: GroupUsersDeletedEvent = {
|
||||||
|
count: emails.length,
|
||||||
|
groupId: group._id as string,
|
||||||
|
}
|
||||||
|
await publishEvent(Event.USER_GROUP_USERS_REMOVED, properties)
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function createdOnboarding(groupId: string) {
|
||||||
|
const properties: GroupAddedOnboardingEvent = {
|
||||||
|
groupId: groupId,
|
||||||
|
onboarding: true,
|
||||||
|
}
|
||||||
|
await publishEvent(Event.USER_GROUP_ONBOARDING, properties)
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function permissionsEdited(roles: UserGroupRoles) {
|
||||||
|
const properties: UserGroupRoles = {
|
||||||
|
...roles,
|
||||||
|
}
|
||||||
|
await publishEvent(Event.USER_GROUP_PERMISSIONS_EDITED, properties)
|
||||||
|
}
|
|
@ -17,3 +17,4 @@ export * as user from "./user"
|
||||||
export * as view from "./view"
|
export * as view from "./view"
|
||||||
export * as installation from "./installation"
|
export * as installation from "./installation"
|
||||||
export * as backfill from "./backfill"
|
export * as backfill from "./backfill"
|
||||||
|
export * as group from "./group"
|
||||||
|
|
|
@ -20,12 +20,6 @@ export async function downgraded(license: License) {
|
||||||
await publishEvent(Event.LICENSE_DOWNGRADED, properties)
|
await publishEvent(Event.LICENSE_DOWNGRADED, properties)
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO
|
|
||||||
export async function updated(license: License) {
|
|
||||||
const properties: LicenseUpdatedEvent = {}
|
|
||||||
await publishEvent(Event.LICENSE_UPDATED, properties)
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO
|
// TODO
|
||||||
export async function activated(license: License) {
|
export async function activated(license: License) {
|
||||||
const properties: LicenseActivatedEvent = {}
|
const properties: LicenseActivatedEvent = {}
|
||||||
|
|
|
@ -50,4 +50,5 @@ exports.getTenantFeatureFlags = tenantId => {
|
||||||
exports.FeatureFlag = {
|
exports.FeatureFlag = {
|
||||||
LICENSING: "LICENSING",
|
LICENSING: "LICENSING",
|
||||||
GOOGLE_SHEETS: "GOOGLE_SHEETS",
|
GOOGLE_SHEETS: "GOOGLE_SHEETS",
|
||||||
|
USER_GROUPS: "USER_GROUPS",
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,17 +3,19 @@ const errorClasses = errors.errors
|
||||||
import * as events from "./events"
|
import * as events from "./events"
|
||||||
import * as migrations from "./migrations"
|
import * as migrations from "./migrations"
|
||||||
import * as users from "./users"
|
import * as users from "./users"
|
||||||
|
import * as roles from "./security/roles"
|
||||||
import * as accounts from "./cloud/accounts"
|
import * as accounts from "./cloud/accounts"
|
||||||
import * as installation from "./installation"
|
import * as installation from "./installation"
|
||||||
import env from "./environment"
|
import env from "./environment"
|
||||||
import tenancy from "./tenancy"
|
import tenancy from "./tenancy"
|
||||||
import featureFlags from "./featureFlags"
|
import featureFlags from "./featureFlags"
|
||||||
import sessions from "./security/sessions"
|
import * as sessions from "./security/sessions"
|
||||||
import deprovisioning from "./context/deprovision"
|
import deprovisioning from "./context/deprovision"
|
||||||
import auth from "./auth"
|
import auth from "./auth"
|
||||||
import constants from "./constants"
|
import constants from "./constants"
|
||||||
import * as dbConstants from "./db/constants"
|
import * as dbConstants from "./db/constants"
|
||||||
import logging from "./logging"
|
import logging from "./logging"
|
||||||
|
import pino from "./pino"
|
||||||
|
|
||||||
// mimic the outer package exports
|
// mimic the outer package exports
|
||||||
import * as db from "./pkg/db"
|
import * as db from "./pkg/db"
|
||||||
|
@ -51,6 +53,8 @@ const core = {
|
||||||
installation,
|
installation,
|
||||||
errors,
|
errors,
|
||||||
logging,
|
logging,
|
||||||
|
roles,
|
||||||
|
...pino,
|
||||||
...errorClasses,
|
...errorClasses,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -15,6 +15,22 @@ export function logAlert(message: string, e?: any) {
|
||||||
console.error(`bb-alert: ${message} ${errorJson}`)
|
console.error(`bb-alert: ${message} ${errorJson}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function logAlertWithInfo(
|
||||||
|
message: string,
|
||||||
|
db: string,
|
||||||
|
id: string,
|
||||||
|
error: any
|
||||||
|
) {
|
||||||
|
message = `${message} - db: ${db} - doc: ${id} - error: `
|
||||||
|
logAlert(message, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function logWarn(message: string) {
|
||||||
|
console.warn(`bb-warn: ${message}`)
|
||||||
|
}
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
logAlert,
|
logAlert,
|
||||||
|
logAlertWithInfo,
|
||||||
|
logWarn,
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,28 +1,39 @@
|
||||||
const { Cookies, Headers } = require("../constants")
|
import { Cookies, Headers } from "../constants"
|
||||||
const { getCookie, clearCookie, openJwt } = require("../utils")
|
import { getCookie, clearCookie, openJwt } from "../utils"
|
||||||
const { getUser } = require("../cache/user")
|
import { getUser } from "../cache/user"
|
||||||
const { getSession, updateSessionTTL } = require("../security/sessions")
|
import { getSession, updateSessionTTL } from "../security/sessions"
|
||||||
const { buildMatcherRegex, matches } = require("./matchers")
|
import { buildMatcherRegex, matches } from "./matchers"
|
||||||
const env = require("../environment")
|
import { SEPARATOR } from "../db/constants"
|
||||||
const { SEPARATOR } = require("../db/constants")
|
import { ViewNames } from "../db/utils"
|
||||||
const { ViewNames } = require("../db/utils")
|
import { queryGlobalView } from "../db/views"
|
||||||
const { queryGlobalView } = require("../db/views")
|
import { getGlobalDB, doInTenant } from "../tenancy"
|
||||||
const { getGlobalDB, doInTenant } = require("../tenancy")
|
import { decrypt } from "../security/encryption"
|
||||||
const { decrypt } = require("../security/encryption")
|
|
||||||
const identity = require("../context/identity")
|
const identity = require("../context/identity")
|
||||||
|
const env = require("../environment")
|
||||||
|
|
||||||
function finalise(
|
const ONE_MINUTE = env.SESSION_UPDATE_PERIOD || 60 * 1000
|
||||||
ctx,
|
|
||||||
{ authenticated, user, internal, version, publicEndpoint } = {}
|
interface FinaliseOpts {
|
||||||
) {
|
authenticated?: boolean
|
||||||
ctx.publicEndpoint = publicEndpoint || false
|
internal?: boolean
|
||||||
ctx.isAuthenticated = authenticated || false
|
publicEndpoint?: boolean
|
||||||
ctx.user = user
|
version?: string
|
||||||
ctx.internal = internal || false
|
user?: any
|
||||||
ctx.version = version
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function checkApiKey(apiKey, populateUser) {
|
function timeMinusOneMinute() {
|
||||||
|
return new Date(Date.now() - ONE_MINUTE).toISOString()
|
||||||
|
}
|
||||||
|
|
||||||
|
function finalise(ctx: any, opts: FinaliseOpts = {}) {
|
||||||
|
ctx.publicEndpoint = opts.publicEndpoint || false
|
||||||
|
ctx.isAuthenticated = opts.authenticated || false
|
||||||
|
ctx.user = opts.user
|
||||||
|
ctx.internal = opts.internal || false
|
||||||
|
ctx.version = opts.version
|
||||||
|
}
|
||||||
|
|
||||||
|
async function checkApiKey(apiKey: string, populateUser?: Function) {
|
||||||
if (apiKey === env.INTERNAL_API_KEY) {
|
if (apiKey === env.INTERNAL_API_KEY) {
|
||||||
return { valid: true }
|
return { valid: true }
|
||||||
}
|
}
|
||||||
|
@ -56,10 +67,12 @@ async function checkApiKey(apiKey, populateUser) {
|
||||||
*/
|
*/
|
||||||
module.exports = (
|
module.exports = (
|
||||||
noAuthPatterns = [],
|
noAuthPatterns = [],
|
||||||
opts = { publicAllowed: false, populateUser: null }
|
opts: { publicAllowed: boolean; populateUser?: Function } = {
|
||||||
|
publicAllowed: false,
|
||||||
|
}
|
||||||
) => {
|
) => {
|
||||||
const noAuthOptions = noAuthPatterns ? buildMatcherRegex(noAuthPatterns) : []
|
const noAuthOptions = noAuthPatterns ? buildMatcherRegex(noAuthPatterns) : []
|
||||||
return async (ctx, next) => {
|
return async (ctx: any, next: any) => {
|
||||||
let publicEndpoint = false
|
let publicEndpoint = false
|
||||||
const version = ctx.request.headers[Headers.API_VER]
|
const version = ctx.request.headers[Headers.API_VER]
|
||||||
// the path is not authenticated
|
// the path is not authenticated
|
||||||
|
@ -71,45 +84,40 @@ module.exports = (
|
||||||
// check the actual user is authenticated first, try header or cookie
|
// check the actual user is authenticated first, try header or cookie
|
||||||
const headerToken = ctx.request.headers[Headers.TOKEN]
|
const headerToken = ctx.request.headers[Headers.TOKEN]
|
||||||
const authCookie = getCookie(ctx, Cookies.Auth) || openJwt(headerToken)
|
const authCookie = getCookie(ctx, Cookies.Auth) || openJwt(headerToken)
|
||||||
|
const apiKey = ctx.request.headers[Headers.API_KEY]
|
||||||
|
const tenantId = ctx.request.headers[Headers.TENANT_ID]
|
||||||
let authenticated = false,
|
let authenticated = false,
|
||||||
user = null,
|
user = null,
|
||||||
internal = false
|
internal = false
|
||||||
if (authCookie) {
|
if (authCookie && !apiKey) {
|
||||||
let error = null
|
|
||||||
const sessionId = authCookie.sessionId
|
const sessionId = authCookie.sessionId
|
||||||
const userId = authCookie.userId
|
const userId = authCookie.userId
|
||||||
|
let session
|
||||||
const session = await getSession(userId, sessionId)
|
try {
|
||||||
if (!session) {
|
// getting session handles error checking (if session exists etc)
|
||||||
error = "No session found"
|
session = await getSession(userId, sessionId)
|
||||||
} else {
|
if (opts && opts.populateUser) {
|
||||||
try {
|
user = await getUser(
|
||||||
if (opts && opts.populateUser) {
|
userId,
|
||||||
user = await getUser(
|
session.tenantId,
|
||||||
userId,
|
opts.populateUser(ctx)
|
||||||
session.tenantId,
|
)
|
||||||
opts.populateUser(ctx)
|
} else {
|
||||||
)
|
user = await getUser(userId, session.tenantId)
|
||||||
} else {
|
|
||||||
user = await getUser(userId, session.tenantId)
|
|
||||||
}
|
|
||||||
user.csrfToken = session.csrfToken
|
|
||||||
authenticated = true
|
|
||||||
} catch (err) {
|
|
||||||
error = err
|
|
||||||
}
|
}
|
||||||
}
|
user.csrfToken = session.csrfToken
|
||||||
if (error) {
|
if (session?.lastAccessedAt < timeMinusOneMinute()) {
|
||||||
console.error("Auth Error", error)
|
// make sure we denote that the session is still in use
|
||||||
|
await updateSessionTTL(session)
|
||||||
|
}
|
||||||
|
authenticated = true
|
||||||
|
} catch (err: any) {
|
||||||
|
authenticated = false
|
||||||
|
console.error("Auth Error", err?.message || err)
|
||||||
// remove the cookie as the user does not exist anymore
|
// remove the cookie as the user does not exist anymore
|
||||||
clearCookie(ctx, Cookies.Auth)
|
clearCookie(ctx, Cookies.Auth)
|
||||||
} else {
|
|
||||||
// make sure we denote that the session is still in use
|
|
||||||
await updateSessionTTL(session)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const apiKey = ctx.request.headers[Headers.API_KEY]
|
|
||||||
const tenantId = ctx.request.headers[Headers.TENANT_ID]
|
|
||||||
// this is an internal request, no user made it
|
// this is an internal request, no user made it
|
||||||
if (!authenticated && apiKey) {
|
if (!authenticated && apiKey) {
|
||||||
const populateUser = opts.populateUser ? opts.populateUser(ctx) : null
|
const populateUser = opts.populateUser ? opts.populateUser(ctx) : null
|
||||||
|
@ -127,7 +135,7 @@ module.exports = (
|
||||||
}
|
}
|
||||||
if (!user && tenantId) {
|
if (!user && tenantId) {
|
||||||
user = { tenantId }
|
user = { tenantId }
|
||||||
} else {
|
} else if (user) {
|
||||||
delete user.password
|
delete user.password
|
||||||
}
|
}
|
||||||
// be explicit
|
// be explicit
|
||||||
|
@ -142,7 +150,7 @@ module.exports = (
|
||||||
} else {
|
} else {
|
||||||
return next()
|
return next()
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err: any) {
|
||||||
// invalid token, clear the cookie
|
// invalid token, clear the cookie
|
||||||
if (err && err.name === "JsonWebTokenError") {
|
if (err && err.name === "JsonWebTokenError") {
|
||||||
clearCookie(ctx, Cookies.Auth)
|
clearCookie(ctx, Cookies.Auth)
|
|
@ -9,7 +9,10 @@ const tenancy = require("./tenancy")
|
||||||
const internalApi = require("./internalApi")
|
const internalApi = require("./internalApi")
|
||||||
const datasourceGoogle = require("./passport/datasource/google")
|
const datasourceGoogle = require("./passport/datasource/google")
|
||||||
const csrf = require("./csrf")
|
const csrf = require("./csrf")
|
||||||
|
const adminOnly = require("./adminOnly")
|
||||||
|
const builderOrAdmin = require("./builderOrAdmin")
|
||||||
|
const builderOnly = require("./builderOnly")
|
||||||
|
const joiValidator = require("./joi-validator")
|
||||||
module.exports = {
|
module.exports = {
|
||||||
google,
|
google,
|
||||||
oidc,
|
oidc,
|
||||||
|
@ -25,4 +28,8 @@ module.exports = {
|
||||||
google: datasourceGoogle,
|
google: datasourceGoogle,
|
||||||
},
|
},
|
||||||
csrf,
|
csrf,
|
||||||
|
adminOnly,
|
||||||
|
builderOnly,
|
||||||
|
builderOrAdmin,
|
||||||
|
joiValidator,
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
const Joi = require("joi")
|
||||||
|
|
||||||
function validate(schema, property) {
|
function validate(schema, property) {
|
||||||
// Return a Koa middleware function
|
// Return a Koa middleware function
|
||||||
return (ctx, next) => {
|
return (ctx, next) => {
|
||||||
|
@ -10,6 +12,12 @@ function validate(schema, property) {
|
||||||
} else if (ctx.request[property] != null) {
|
} else if (ctx.request[property] != null) {
|
||||||
params = ctx.request[property]
|
params = ctx.request[property]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
schema = schema.append({
|
||||||
|
createdAt: Joi.any().optional(),
|
||||||
|
updatedAt: Joi.any().optional(),
|
||||||
|
})
|
||||||
|
|
||||||
const { error } = schema.validate(params)
|
const { error } = schema.validate(params)
|
||||||
if (error) {
|
if (error) {
|
||||||
ctx.throw(400, `Invalid ${property} - ${error.message}`)
|
ctx.throw(400, `Invalid ${property} - ${error.message}`)
|
|
@ -37,4 +37,8 @@ export const DEFINITIONS: MigrationDefinition[] = [
|
||||||
type: MigrationType.INSTALLATION,
|
type: MigrationType.INSTALLATION,
|
||||||
name: MigrationName.EVENT_INSTALLATION_BACKFILL,
|
name: MigrationName.EVENT_INSTALLATION_BACKFILL,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
type: MigrationType.GLOBAL,
|
||||||
|
name: MigrationName.GLOBAL_INFO_SYNC_USERS,
|
||||||
|
},
|
||||||
]
|
]
|
||||||
|
|
|
@ -0,0 +1,11 @@
|
||||||
|
const env = require("./environment")
|
||||||
|
|
||||||
|
exports.pinoSettings = () => ({
|
||||||
|
prettyPrint: {
|
||||||
|
levelFirst: true,
|
||||||
|
},
|
||||||
|
level: env.LOG_LEVEL || "error",
|
||||||
|
autoLogging: {
|
||||||
|
ignore: req => req.url.includes("/health"),
|
||||||
|
},
|
||||||
|
})
|
|
@ -76,7 +76,7 @@ function isBuiltin(role) {
|
||||||
/**
|
/**
|
||||||
* Works through the inheritance ranks to see how far up the builtin stack this ID is.
|
* Works through the inheritance ranks to see how far up the builtin stack this ID is.
|
||||||
*/
|
*/
|
||||||
function builtinRoleToNumber(id) {
|
exports.builtinRoleToNumber = id => {
|
||||||
const builtins = exports.getBuiltinRoles()
|
const builtins = exports.getBuiltinRoles()
|
||||||
const MAX = Object.values(BUILTIN_IDS).length + 1
|
const MAX = Object.values(BUILTIN_IDS).length + 1
|
||||||
if (id === BUILTIN_IDS.ADMIN || id === BUILTIN_IDS.BUILDER) {
|
if (id === BUILTIN_IDS.ADMIN || id === BUILTIN_IDS.BUILDER) {
|
||||||
|
@ -104,7 +104,8 @@ exports.lowerBuiltinRoleID = (roleId1, roleId2) => {
|
||||||
if (!roleId2) {
|
if (!roleId2) {
|
||||||
return roleId1
|
return roleId1
|
||||||
}
|
}
|
||||||
return builtinRoleToNumber(roleId1) > builtinRoleToNumber(roleId2)
|
return exports.builtinRoleToNumber(roleId1) >
|
||||||
|
exports.builtinRoleToNumber(roleId2)
|
||||||
? roleId2
|
? roleId2
|
||||||
: roleId1
|
: roleId1
|
||||||
}
|
}
|
||||||
|
@ -202,15 +203,24 @@ exports.getAllRoles = async appId => {
|
||||||
if (appId) {
|
if (appId) {
|
||||||
return doWithDB(appId, internal)
|
return doWithDB(appId, internal)
|
||||||
} else {
|
} else {
|
||||||
return internal(getAppDB())
|
let appDB
|
||||||
|
try {
|
||||||
|
appDB = getAppDB()
|
||||||
|
} catch (error) {
|
||||||
|
// We don't have any apps, so we'll just use the built-in roles
|
||||||
|
}
|
||||||
|
return internal(appDB)
|
||||||
}
|
}
|
||||||
async function internal(db) {
|
async function internal(db) {
|
||||||
const body = await db.allDocs(
|
let roles = []
|
||||||
getRoleParams(null, {
|
if (db) {
|
||||||
include_docs: true,
|
const body = await db.allDocs(
|
||||||
})
|
getRoleParams(null, {
|
||||||
)
|
include_docs: true,
|
||||||
let roles = body.rows.map(row => row.doc)
|
})
|
||||||
|
)
|
||||||
|
roles = body.rows.map(row => row.doc)
|
||||||
|
}
|
||||||
const builtinRoles = exports.getBuiltinRoles()
|
const builtinRoles = exports.getBuiltinRoles()
|
||||||
|
|
||||||
// need to combine builtin with any DB record of them (for sake of permissions)
|
// need to combine builtin with any DB record of them (for sake of permissions)
|
||||||
|
|
|
@ -1,95 +0,0 @@
|
||||||
const redis = require("../redis/init")
|
|
||||||
const { v4: uuidv4 } = require("uuid")
|
|
||||||
|
|
||||||
// a week in seconds
|
|
||||||
const EXPIRY_SECONDS = 86400 * 7
|
|
||||||
|
|
||||||
async function getSessionsForUser(userId) {
|
|
||||||
const client = await redis.getSessionClient()
|
|
||||||
const sessions = await client.scan(userId)
|
|
||||||
return sessions.map(session => session.value)
|
|
||||||
}
|
|
||||||
|
|
||||||
function makeSessionID(userId, sessionId) {
|
|
||||||
return `${userId}/${sessionId}`
|
|
||||||
}
|
|
||||||
|
|
||||||
async function invalidateSessions(userId, sessionIds = null) {
|
|
||||||
try {
|
|
||||||
let sessions = []
|
|
||||||
|
|
||||||
// If no sessionIds, get all the sessions for the user
|
|
||||||
if (!sessionIds) {
|
|
||||||
sessions = await getSessionsForUser(userId)
|
|
||||||
sessions.forEach(
|
|
||||||
session =>
|
|
||||||
(session.key = makeSessionID(session.userId, session.sessionId))
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
// use the passed array of sessionIds
|
|
||||||
sessions = Array.isArray(sessionIds) ? sessionIds : [sessionIds]
|
|
||||||
sessions = sessions.map(sessionId => ({
|
|
||||||
key: makeSessionID(userId, sessionId),
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
|
|
||||||
const client = await redis.getSessionClient()
|
|
||||||
const promises = []
|
|
||||||
for (let session of sessions) {
|
|
||||||
promises.push(client.delete(session.key))
|
|
||||||
}
|
|
||||||
await Promise.all(promises)
|
|
||||||
} catch (err) {
|
|
||||||
console.error(`Error invalidating sessions: ${err}`)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
exports.createASession = async (userId, session) => {
|
|
||||||
// invalidate all other sessions
|
|
||||||
await invalidateSessions(userId)
|
|
||||||
|
|
||||||
const client = await redis.getSessionClient()
|
|
||||||
const sessionId = session.sessionId
|
|
||||||
if (!session.csrfToken) {
|
|
||||||
session.csrfToken = uuidv4()
|
|
||||||
}
|
|
||||||
session = {
|
|
||||||
createdAt: new Date().toISOString(),
|
|
||||||
lastAccessedAt: new Date().toISOString(),
|
|
||||||
...session,
|
|
||||||
userId,
|
|
||||||
}
|
|
||||||
await client.store(makeSessionID(userId, sessionId), session, EXPIRY_SECONDS)
|
|
||||||
}
|
|
||||||
|
|
||||||
exports.updateSessionTTL = async session => {
|
|
||||||
const client = await redis.getSessionClient()
|
|
||||||
const key = makeSessionID(session.userId, session.sessionId)
|
|
||||||
session.lastAccessedAt = new Date().toISOString()
|
|
||||||
await client.store(key, session, EXPIRY_SECONDS)
|
|
||||||
}
|
|
||||||
|
|
||||||
exports.endSession = async (userId, sessionId) => {
|
|
||||||
const client = await redis.getSessionClient()
|
|
||||||
await client.delete(makeSessionID(userId, sessionId))
|
|
||||||
}
|
|
||||||
|
|
||||||
exports.getSession = async (userId, sessionId) => {
|
|
||||||
try {
|
|
||||||
const client = await redis.getSessionClient()
|
|
||||||
return client.get(makeSessionID(userId, sessionId))
|
|
||||||
} catch (err) {
|
|
||||||
// if can't get session don't error, just don't return anything
|
|
||||||
console.error(err)
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
exports.getAllSessions = async () => {
|
|
||||||
const client = await redis.getSessionClient()
|
|
||||||
const sessions = await client.scan()
|
|
||||||
return sessions.map(session => session.value)
|
|
||||||
}
|
|
||||||
|
|
||||||
exports.getUserSessions = getSessionsForUser
|
|
||||||
exports.invalidateSessions = invalidateSessions
|
|
|
@ -0,0 +1,119 @@
|
||||||
|
const redis = require("../redis/init")
|
||||||
|
const { v4: uuidv4 } = require("uuid")
|
||||||
|
const { logWarn } = require("../logging")
|
||||||
|
const env = require("../environment")
|
||||||
|
|
||||||
|
interface Session {
|
||||||
|
key: string
|
||||||
|
userId: string
|
||||||
|
sessionId: string
|
||||||
|
lastAccessedAt: string
|
||||||
|
createdAt: string
|
||||||
|
csrfToken?: string
|
||||||
|
value: string
|
||||||
|
}
|
||||||
|
|
||||||
|
type SessionKey = { key: string }[]
|
||||||
|
|
||||||
|
// a week in seconds
|
||||||
|
const EXPIRY_SECONDS = 86400 * 7
|
||||||
|
|
||||||
|
function makeSessionID(userId: string, sessionId: string) {
|
||||||
|
return `${userId}/${sessionId}`
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getSessionsForUser(userId: string) {
|
||||||
|
if (!userId) {
|
||||||
|
console.trace("Cannot get sessions for undefined userId")
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
const client = await redis.getSessionClient()
|
||||||
|
const sessions = await client.scan(userId)
|
||||||
|
return sessions.map((session: Session) => session.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function invalidateSessions(
|
||||||
|
userId: string,
|
||||||
|
opts: { sessionIds?: string[]; reason?: string } = {}
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
const reason = opts?.reason || "unknown"
|
||||||
|
let sessionIds: string[] = opts.sessionIds || []
|
||||||
|
let sessions: SessionKey
|
||||||
|
|
||||||
|
// If no sessionIds, get all the sessions for the user
|
||||||
|
if (sessionIds.length === 0) {
|
||||||
|
sessions = await getSessionsForUser(userId)
|
||||||
|
sessions.forEach(
|
||||||
|
(session: any) =>
|
||||||
|
(session.key = makeSessionID(session.userId, session.sessionId))
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
// use the passed array of sessionIds
|
||||||
|
sessionIds = Array.isArray(sessionIds) ? sessionIds : [sessionIds]
|
||||||
|
sessions = sessionIds.map((sessionId: string) => ({
|
||||||
|
key: makeSessionID(userId, sessionId),
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sessions && sessions.length > 0) {
|
||||||
|
const client = await redis.getSessionClient()
|
||||||
|
const promises = []
|
||||||
|
for (let session of sessions) {
|
||||||
|
promises.push(client.delete(session.key))
|
||||||
|
}
|
||||||
|
if (!env.isTest()) {
|
||||||
|
logWarn(
|
||||||
|
`Invalidating sessions for ${userId} (reason: ${reason}) - ${sessions
|
||||||
|
.map(session => session.key)
|
||||||
|
.join(", ")}`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
await Promise.all(promises)
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error(`Error invalidating sessions: ${err}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function createASession(userId: string, session: Session) {
|
||||||
|
// invalidate all other sessions
|
||||||
|
await invalidateSessions(userId, { reason: "creation" })
|
||||||
|
|
||||||
|
const client = await redis.getSessionClient()
|
||||||
|
const sessionId = session.sessionId
|
||||||
|
if (!session.csrfToken) {
|
||||||
|
session.csrfToken = uuidv4()
|
||||||
|
}
|
||||||
|
session = {
|
||||||
|
...session,
|
||||||
|
createdAt: new Date().toISOString(),
|
||||||
|
lastAccessedAt: new Date().toISOString(),
|
||||||
|
userId,
|
||||||
|
}
|
||||||
|
await client.store(makeSessionID(userId, sessionId), session, EXPIRY_SECONDS)
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function updateSessionTTL(session: Session) {
|
||||||
|
const client = await redis.getSessionClient()
|
||||||
|
const key = makeSessionID(session.userId, session.sessionId)
|
||||||
|
session.lastAccessedAt = new Date().toISOString()
|
||||||
|
await client.store(key, session, EXPIRY_SECONDS)
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function endSession(userId: string, sessionId: string) {
|
||||||
|
const client = await redis.getSessionClient()
|
||||||
|
await client.delete(makeSessionID(userId, sessionId))
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getSession(userId: string, sessionId: string) {
|
||||||
|
if (!userId || !sessionId) {
|
||||||
|
throw new Error(`Invalid session details - ${userId} - ${sessionId}`)
|
||||||
|
}
|
||||||
|
const client = await redis.getSessionClient()
|
||||||
|
const session = await client.get(makeSessionID(userId, sessionId))
|
||||||
|
if (!session) {
|
||||||
|
throw new Error(`Session not found - ${userId} - ${sessionId}`)
|
||||||
|
}
|
||||||
|
return session
|
||||||
|
}
|
|
@ -0,0 +1,12 @@
|
||||||
|
import * as sessions from "../sessions"
|
||||||
|
|
||||||
|
describe("sessions", () => {
|
||||||
|
describe("getSessionsForUser", () => {
|
||||||
|
it("returns empty when user is undefined", async () => {
|
||||||
|
// @ts-ignore - allow the undefined to be passed
|
||||||
|
const results = await sessions.getSessionsForUser(undefined)
|
||||||
|
|
||||||
|
expect(results).toStrictEqual([])
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
|
@ -1,4 +1,9 @@
|
||||||
const { ViewNames } = require("./db/utils")
|
const {
|
||||||
|
ViewNames,
|
||||||
|
getUsersByAppParams,
|
||||||
|
getProdAppID,
|
||||||
|
generateAppUserID,
|
||||||
|
} = require("./db/utils")
|
||||||
const { queryGlobalView } = require("./db/views")
|
const { queryGlobalView } = require("./db/views")
|
||||||
const { UNICODE_MAX } = require("./db/constants")
|
const { UNICODE_MAX } = require("./db/constants")
|
||||||
|
|
||||||
|
@ -13,12 +18,32 @@ exports.getGlobalUserByEmail = async email => {
|
||||||
throw "Must supply an email address to view"
|
throw "Must supply an email address to view"
|
||||||
}
|
}
|
||||||
|
|
||||||
const response = await queryGlobalView(ViewNames.USER_BY_EMAIL, {
|
return await queryGlobalView(ViewNames.USER_BY_EMAIL, {
|
||||||
key: email.toLowerCase(),
|
key: email.toLowerCase(),
|
||||||
include_docs: true,
|
include_docs: true,
|
||||||
})
|
})
|
||||||
|
}
|
||||||
|
|
||||||
return response
|
exports.searchGlobalUsersByApp = async (appId, opts) => {
|
||||||
|
if (typeof appId !== "string") {
|
||||||
|
throw new Error("Must provide a string based app ID")
|
||||||
|
}
|
||||||
|
const params = getUsersByAppParams(appId, {
|
||||||
|
include_docs: true,
|
||||||
|
})
|
||||||
|
params.startkey = opts && opts.startkey ? opts.startkey : params.startkey
|
||||||
|
let response = await queryGlobalView(ViewNames.USER_BY_APP, params)
|
||||||
|
if (!response) {
|
||||||
|
response = []
|
||||||
|
}
|
||||||
|
return Array.isArray(response) ? response : [response]
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.getGlobalUserByAppPage = (appId, user) => {
|
||||||
|
if (!user) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
return generateAppUserID(getProdAppID(appId), user._id)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -10,7 +10,10 @@ const { queryGlobalView } = require("./db/views")
|
||||||
const { Headers, Cookies, MAX_VALID_DATE } = require("./constants")
|
const { Headers, Cookies, MAX_VALID_DATE } = require("./constants")
|
||||||
const env = require("./environment")
|
const env = require("./environment")
|
||||||
const userCache = require("./cache/user")
|
const userCache = require("./cache/user")
|
||||||
const { getUserSessions, invalidateSessions } = require("./security/sessions")
|
const {
|
||||||
|
getSessionsForUser,
|
||||||
|
invalidateSessions,
|
||||||
|
} = require("./security/sessions")
|
||||||
const events = require("./events")
|
const events = require("./events")
|
||||||
const tenancy = require("./tenancy")
|
const tenancy = require("./tenancy")
|
||||||
|
|
||||||
|
@ -178,7 +181,7 @@ exports.platformLogout = async ({ ctx, userId, keepActiveSession }) => {
|
||||||
if (!ctx) throw new Error("Koa context must be supplied to logout.")
|
if (!ctx) throw new Error("Koa context must be supplied to logout.")
|
||||||
|
|
||||||
const currentSession = exports.getCookie(ctx, Cookies.Auth)
|
const currentSession = exports.getCookie(ctx, Cookies.Auth)
|
||||||
let sessions = await getUserSessions(userId)
|
let sessions = await getSessionsForUser(userId)
|
||||||
|
|
||||||
if (keepActiveSession) {
|
if (keepActiveSession) {
|
||||||
sessions = sessions.filter(
|
sessions = sessions.filter(
|
||||||
|
@ -190,10 +193,8 @@ exports.platformLogout = async ({ ctx, userId, keepActiveSession }) => {
|
||||||
exports.clearCookie(ctx, Cookies.CurrentApp)
|
exports.clearCookie(ctx, Cookies.CurrentApp)
|
||||||
}
|
}
|
||||||
|
|
||||||
await invalidateSessions(
|
const sessionIds = sessions.map(({ sessionId }) => sessionId)
|
||||||
userId,
|
await invalidateSessions(userId, { sessionIds, reason: "logout" })
|
||||||
sessions.map(({ sessionId }) => sessionId)
|
|
||||||
)
|
|
||||||
await events.auth.logout()
|
await events.auth.logout()
|
||||||
await userCache.invalidateUser(userId)
|
await userCache.invalidateUser(userId)
|
||||||
}
|
}
|
||||||
|
|
|
@ -89,6 +89,14 @@ jest.spyOn(events.user, "passwordUpdated")
|
||||||
jest.spyOn(events.user, "passwordResetRequested")
|
jest.spyOn(events.user, "passwordResetRequested")
|
||||||
jest.spyOn(events.user, "passwordReset")
|
jest.spyOn(events.user, "passwordReset")
|
||||||
|
|
||||||
|
jest.spyOn(events.group, "created")
|
||||||
|
jest.spyOn(events.group, "updated")
|
||||||
|
jest.spyOn(events.group, "deleted")
|
||||||
|
jest.spyOn(events.group, "usersAdded")
|
||||||
|
jest.spyOn(events.group, "usersDeleted")
|
||||||
|
jest.spyOn(events.group, "createdOnboarding")
|
||||||
|
jest.spyOn(events.group, "permissionsEdited")
|
||||||
|
|
||||||
jest.spyOn(events.serve, "servedBuilder")
|
jest.spyOn(events.serve, "servedBuilder")
|
||||||
jest.spyOn(events.serve, "servedApp")
|
jest.spyOn(events.serve, "servedApp")
|
||||||
jest.spyOn(events.serve, "servedAppPreview")
|
jest.spyOn(events.serve, "servedAppPreview")
|
||||||
|
|
|
@ -1,7 +1,9 @@
|
||||||
|
const posthog = require("./posthog")
|
||||||
const events = require("./events")
|
const events = require("./events")
|
||||||
const date = require("./date")
|
const date = require("./date")
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
|
posthog,
|
||||||
date,
|
date,
|
||||||
events,
|
events,
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,7 @@
|
||||||
|
jest.mock("posthog-node", () => {
|
||||||
|
return jest.fn().mockImplementation(() => {
|
||||||
|
return {
|
||||||
|
capture: jest.fn(),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
|
@ -291,6 +291,18 @@
|
||||||
resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39"
|
resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39"
|
||||||
integrity sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==
|
integrity sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==
|
||||||
|
|
||||||
|
"@hapi/hoek@^9.0.0":
|
||||||
|
version "9.3.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/@hapi/hoek/-/hoek-9.3.0.tgz#8368869dcb735be2e7f5cb7647de78e167a251fb"
|
||||||
|
integrity sha512-/c6rf4UJlmHlC9b5BaNvzAcFv7HZ2QHaV0D4/HNlBdvFnvQq8RI4kYdhyPCl7Xj+oWvTWQ8ujhqS53LIgAe6KQ==
|
||||||
|
|
||||||
|
"@hapi/topo@^5.0.0":
|
||||||
|
version "5.1.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/@hapi/topo/-/topo-5.1.0.tgz#dc448e332c6c6e37a4dc02fd84ba8d44b9afb012"
|
||||||
|
integrity sha512-foQZKJig7Ob0BMAYBfcJk8d77QtOe7Wo4ox7ff1lQYoNNAb6jwcY1ncdoy2e9wQZzvNy7ODZCYJkK8kzmcAnAg==
|
||||||
|
dependencies:
|
||||||
|
"@hapi/hoek" "^9.0.0"
|
||||||
|
|
||||||
"@istanbuljs/load-nyc-config@^1.0.0":
|
"@istanbuljs/load-nyc-config@^1.0.0":
|
||||||
version "1.1.0"
|
version "1.1.0"
|
||||||
resolved "https://registry.yarnpkg.com/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz#fd3db1d59ecf7cf121e80650bb86712f9b55eced"
|
resolved "https://registry.yarnpkg.com/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz#fd3db1d59ecf7cf121e80650bb86712f9b55eced"
|
||||||
|
@ -539,6 +551,23 @@
|
||||||
koa "^2.13.4"
|
koa "^2.13.4"
|
||||||
node-mocks-http "^1.5.8"
|
node-mocks-http "^1.5.8"
|
||||||
|
|
||||||
|
"@sideway/address@^4.1.3":
|
||||||
|
version "4.1.4"
|
||||||
|
resolved "https://registry.yarnpkg.com/@sideway/address/-/address-4.1.4.tgz#03dccebc6ea47fdc226f7d3d1ad512955d4783f0"
|
||||||
|
integrity sha512-7vwq+rOHVWjyXxVlR76Agnvhy8I9rpzjosTESvmhNeXOXdZZB15Fl+TI9x1SiHZH5Jv2wTGduSxFDIaq0m3DUw==
|
||||||
|
dependencies:
|
||||||
|
"@hapi/hoek" "^9.0.0"
|
||||||
|
|
||||||
|
"@sideway/formula@^3.0.0":
|
||||||
|
version "3.0.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/@sideway/formula/-/formula-3.0.0.tgz#fe158aee32e6bd5de85044be615bc08478a0a13c"
|
||||||
|
integrity sha512-vHe7wZ4NOXVfkoRb8T5otiENVlT7a3IAiw7H5M2+GO+9CDgcVUUsX1zalAztCmwyOr2RUTGJdgB+ZvSVqmdHmg==
|
||||||
|
|
||||||
|
"@sideway/pinpoint@^2.0.0":
|
||||||
|
version "2.0.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/@sideway/pinpoint/-/pinpoint-2.0.0.tgz#cff8ffadc372ad29fd3f78277aeb29e632cc70df"
|
||||||
|
integrity sha512-RNiOoTPkptFtSVzQevY/yWtZwf/RxyVnPy/OcA9HBM3MlGDnBEYL5B41H0MTn0Uec8Hi+2qUtTfG2WWZBmMejQ==
|
||||||
|
|
||||||
"@sindresorhus/is@^0.14.0":
|
"@sindresorhus/is@^0.14.0":
|
||||||
version "0.14.0"
|
version "0.14.0"
|
||||||
resolved "https://registry.yarnpkg.com/@sindresorhus/is/-/is-0.14.0.tgz#9fb3a3cf3132328151f353de4632e01e52102bea"
|
resolved "https://registry.yarnpkg.com/@sindresorhus/is/-/is-0.14.0.tgz#9fb3a3cf3132328151f353de4632e01e52102bea"
|
||||||
|
@ -3193,6 +3222,17 @@ jmespath@0.15.0:
|
||||||
resolved "https://registry.yarnpkg.com/jmespath/-/jmespath-0.15.0.tgz#a3f222a9aae9f966f5d27c796510e28091764217"
|
resolved "https://registry.yarnpkg.com/jmespath/-/jmespath-0.15.0.tgz#a3f222a9aae9f966f5d27c796510e28091764217"
|
||||||
integrity sha512-+kHj8HXArPfpPEKGLZ+kB5ONRTCiGQXo8RQYL0hH8t6pWXUBBK5KkkQmTNOwKK4LEsd0yTsgtjJVm4UBSZea4w==
|
integrity sha512-+kHj8HXArPfpPEKGLZ+kB5ONRTCiGQXo8RQYL0hH8t6pWXUBBK5KkkQmTNOwKK4LEsd0yTsgtjJVm4UBSZea4w==
|
||||||
|
|
||||||
|
joi@17.6.0:
|
||||||
|
version "17.6.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/joi/-/joi-17.6.0.tgz#0bb54f2f006c09a96e75ce687957bd04290054b2"
|
||||||
|
integrity sha512-OX5dG6DTbcr/kbMFj0KGYxuew69HPcAE3K/sZpEV2nP6e/j/C0HV+HNiBPCASxdx5T7DMoa0s8UeHWMnb6n2zw==
|
||||||
|
dependencies:
|
||||||
|
"@hapi/hoek" "^9.0.0"
|
||||||
|
"@hapi/topo" "^5.0.0"
|
||||||
|
"@sideway/address" "^4.1.3"
|
||||||
|
"@sideway/formula" "^3.0.0"
|
||||||
|
"@sideway/pinpoint" "^2.0.0"
|
||||||
|
|
||||||
join-component@^1.1.0:
|
join-component@^1.1.0:
|
||||||
version "1.1.0"
|
version "1.1.0"
|
||||||
resolved "https://registry.yarnpkg.com/join-component/-/join-component-1.1.0.tgz#b8417b750661a392bee2c2537c68b2a9d4977cd5"
|
resolved "https://registry.yarnpkg.com/join-component/-/join-component-1.1.0.tgz#b8417b750661a392bee2c2537c68b2a9d4977cd5"
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"name": "@budibase/bbui",
|
"name": "@budibase/bbui",
|
||||||
"description": "A UI solution used in the different Budibase projects.",
|
"description": "A UI solution used in the different Budibase projects.",
|
||||||
"version": "1.1.22-alpha.0",
|
"version": "1.2.20-alpha.1",
|
||||||
"license": "MPL-2.0",
|
"license": "MPL-2.0",
|
||||||
"svelte": "src/index.js",
|
"svelte": "src/index.js",
|
||||||
"module": "dist/bbui.es.js",
|
"module": "dist/bbui.es.js",
|
||||||
|
@ -38,7 +38,7 @@
|
||||||
],
|
],
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@adobe/spectrum-css-workflow-icons": "^1.2.1",
|
"@adobe/spectrum-css-workflow-icons": "^1.2.1",
|
||||||
"@budibase/string-templates": "^1.1.22-alpha.0",
|
"@budibase/string-templates": "1.2.20-alpha.1",
|
||||||
"@spectrum-css/actionbutton": "^1.0.1",
|
"@spectrum-css/actionbutton": "^1.0.1",
|
||||||
"@spectrum-css/actiongroup": "^1.0.1",
|
"@spectrum-css/actiongroup": "^1.0.1",
|
||||||
"@spectrum-css/avatar": "^3.0.2",
|
"@spectrum-css/avatar": "^3.0.2",
|
||||||
|
|
|
@ -84,6 +84,7 @@
|
||||||
}
|
}
|
||||||
:global([dir="ltr"] .spectrum-ActionButton .spectrum-Icon) {
|
:global([dir="ltr"] .spectrum-ActionButton .spectrum-Icon) {
|
||||||
margin-left: 0;
|
margin-left: 0;
|
||||||
|
transition: color ease-out 130ms;
|
||||||
}
|
}
|
||||||
.is-selected:not(.spectrum-ActionButton--emphasized) {
|
.is-selected:not(.spectrum-ActionButton--emphasized) {
|
||||||
background: var(--spectrum-global-color-gray-300);
|
background: var(--spectrum-global-color-gray-300);
|
||||||
|
@ -92,4 +93,10 @@
|
||||||
padding: 0;
|
padding: 0;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
}
|
}
|
||||||
|
.spectrum-ActionButton--quiet {
|
||||||
|
padding: 0 8px;
|
||||||
|
}
|
||||||
|
.is-selected:not(.emphasized) .spectrum-Icon {
|
||||||
|
color: var(--spectrum-global-color-gray-900);
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -4,7 +4,7 @@
|
||||||
["XXS", "--spectrum-alias-avatar-size-50"],
|
["XXS", "--spectrum-alias-avatar-size-50"],
|
||||||
["XS", "--spectrum-alias-avatar-size-75"],
|
["XS", "--spectrum-alias-avatar-size-75"],
|
||||||
["S", "--spectrum-alias-avatar-size-200"],
|
["S", "--spectrum-alias-avatar-size-200"],
|
||||||
["M", "--spectrum-alias-avatar-size-300"],
|
["M", "--spectrum-alias-avatar-size-400"],
|
||||||
["L", "--spectrum-alias-avatar-size-500"],
|
["L", "--spectrum-alias-avatar-size-500"],
|
||||||
["XL", "--spectrum-alias-avatar-size-600"],
|
["XL", "--spectrum-alias-avatar-size-600"],
|
||||||
["XXL", "--spectrum-alias-avatar-size-700"],
|
["XXL", "--spectrum-alias-avatar-size-700"],
|
||||||
|
@ -13,6 +13,19 @@
|
||||||
export let url = ""
|
export let url = ""
|
||||||
export let disabled = false
|
export let disabled = false
|
||||||
export let initials = "JD"
|
export let initials = "JD"
|
||||||
|
|
||||||
|
const DefaultColor = "#3aab87"
|
||||||
|
|
||||||
|
$: color = getColor(initials)
|
||||||
|
|
||||||
|
const getColor = initials => {
|
||||||
|
if (!initials?.length) {
|
||||||
|
return DefaultColor
|
||||||
|
}
|
||||||
|
const code = initials[0].toLowerCase().charCodeAt(0)
|
||||||
|
const hue = ((code % 26) / 26) * 360
|
||||||
|
return `hsl(${hue}, 50%, 50%)`
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if url}
|
{#if url}
|
||||||
|
@ -25,10 +38,11 @@
|
||||||
/>
|
/>
|
||||||
{:else}
|
{:else}
|
||||||
<div
|
<div
|
||||||
|
class="spectrum-Avatar"
|
||||||
class:is-disabled={disabled}
|
class:is-disabled={disabled}
|
||||||
style="width: var({sizes.get(size)}); height: var({sizes.get(
|
style="width: var({sizes.get(size)}); height: var({sizes.get(
|
||||||
size
|
size
|
||||||
)}); font-size: calc(var({sizes.get(size)}) / 2)"
|
)}); font-size: calc(var({sizes.get(size)}) / 2); background: {color};"
|
||||||
>
|
>
|
||||||
{initials || ""}
|
{initials || ""}
|
||||||
</div>
|
</div>
|
||||||
|
@ -40,7 +54,6 @@
|
||||||
display: grid;
|
display: grid;
|
||||||
place-items: center;
|
place-items: center;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
background: #3aab87;
|
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
|
|
|
@ -0,0 +1,228 @@
|
||||||
|
<script>
|
||||||
|
import "@spectrum-css/inputgroup/dist/index-vars.css"
|
||||||
|
import "@spectrum-css/popover/dist/index-vars.css"
|
||||||
|
import "@spectrum-css/menu/dist/index-vars.css"
|
||||||
|
import { fly } from "svelte/transition"
|
||||||
|
import { createEventDispatcher } from "svelte"
|
||||||
|
import clickOutside from "../../Actions/click_outside"
|
||||||
|
|
||||||
|
export let inputValue
|
||||||
|
export let dropdownValue
|
||||||
|
export let id = null
|
||||||
|
export let inputType = "text"
|
||||||
|
export let placeholder = "Choose an option or type"
|
||||||
|
export let disabled = false
|
||||||
|
export let readonly = false
|
||||||
|
export let updateOnChange = true
|
||||||
|
export let error = null
|
||||||
|
export let options = []
|
||||||
|
export let getOptionLabel = option => extractProperty(option, "label")
|
||||||
|
export let getOptionValue = option => extractProperty(option, "value")
|
||||||
|
|
||||||
|
export let isOptionSelected = () => false
|
||||||
|
|
||||||
|
const dispatch = createEventDispatcher()
|
||||||
|
let open = false
|
||||||
|
let focus = false
|
||||||
|
|
||||||
|
$: fieldText = getFieldText(dropdownValue, options, placeholder)
|
||||||
|
|
||||||
|
const getFieldText = (dropdownValue, options, placeholder) => {
|
||||||
|
// Always use placeholder if no value
|
||||||
|
if (dropdownValue == null || dropdownValue === "") {
|
||||||
|
return placeholder || "Choose an option or type"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait for options to load if there is a value but no options
|
||||||
|
if (!options?.length) {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// Render the label if the selected option is found, otherwise raw value
|
||||||
|
const selected = options.find(
|
||||||
|
option => getOptionValue(option) === dropdownValue
|
||||||
|
)
|
||||||
|
return selected ? getOptionLabel(selected) : dropdownValue
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateValue = newValue => {
|
||||||
|
if (readonly) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
dispatch("change", newValue)
|
||||||
|
}
|
||||||
|
|
||||||
|
const onFocus = () => {
|
||||||
|
if (readonly) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
focus = true
|
||||||
|
}
|
||||||
|
|
||||||
|
const onBlur = event => {
|
||||||
|
if (readonly) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
focus = false
|
||||||
|
updateValue(event.target.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
const onInput = event => {
|
||||||
|
if (readonly || !updateOnChange) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
updateValue(event.target.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateValueOnEnter = event => {
|
||||||
|
if (readonly) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (event.key === "Enter") {
|
||||||
|
updateValue(event.target.value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const onClick = () => {
|
||||||
|
dispatch("click")
|
||||||
|
if (readonly) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
open = true
|
||||||
|
}
|
||||||
|
|
||||||
|
const onPick = newValue => {
|
||||||
|
dispatch("pick", newValue)
|
||||||
|
open = false
|
||||||
|
}
|
||||||
|
|
||||||
|
const extractProperty = (value, property) => {
|
||||||
|
if (value && typeof value === "object") {
|
||||||
|
return value[property]
|
||||||
|
}
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="spectrum-InputGroup"
|
||||||
|
class:is-invalid={!!error}
|
||||||
|
class:is-disabled={disabled}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="spectrum-Textfield spectrum-InputGroup-textfield"
|
||||||
|
class:is-invalid={!!error}
|
||||||
|
class:is-disabled={disabled}
|
||||||
|
class:is-focused={focus}
|
||||||
|
>
|
||||||
|
{#if error}
|
||||||
|
<svg
|
||||||
|
class="spectrum-Icon spectrum-Icon--sizeM spectrum-Textfield-validationIcon"
|
||||||
|
focusable="false"
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
<use xlink:href="#spectrum-icon-18-Alert" />
|
||||||
|
</svg>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<input
|
||||||
|
{id}
|
||||||
|
on:click
|
||||||
|
on:blur
|
||||||
|
on:focus
|
||||||
|
on:input
|
||||||
|
on:keyup
|
||||||
|
on:blur={onBlur}
|
||||||
|
on:focus={onFocus}
|
||||||
|
on:input={onInput}
|
||||||
|
on:keyup={updateValueOnEnter}
|
||||||
|
value={inputValue || ""}
|
||||||
|
placeholder={placeholder || ""}
|
||||||
|
{disabled}
|
||||||
|
{readonly}
|
||||||
|
{inputType}
|
||||||
|
class="spectrum-Textfield-input spectrum-InputGroup-input"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div style="width: 30%">
|
||||||
|
<button
|
||||||
|
{id}
|
||||||
|
class="spectrum-Picker spectrum-Picker--sizeM override-borders"
|
||||||
|
{disabled}
|
||||||
|
class:is-open={open}
|
||||||
|
aria-haspopup="listbox"
|
||||||
|
on:mousedown={onClick}
|
||||||
|
>
|
||||||
|
<span class="spectrum-Picker-label">
|
||||||
|
<div>
|
||||||
|
{fieldText}
|
||||||
|
</div></span
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
class="spectrum-Icon spectrum-UIIcon-ChevronDown100 spectrum-Picker-menuIcon"
|
||||||
|
focusable="false"
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
<use xlink:href="#spectrum-css-icon-Chevron100" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
{#if open}
|
||||||
|
<div
|
||||||
|
use:clickOutside={() => (open = false)}
|
||||||
|
transition:fly|local={{ y: -20, duration: 200 }}
|
||||||
|
class="spectrum-Popover spectrum-Popover--bottom spectrum-Picker-popover is-open"
|
||||||
|
>
|
||||||
|
<ul class="spectrum-Menu" role="listbox">
|
||||||
|
{#each options as option, idx}
|
||||||
|
<li
|
||||||
|
class="spectrum-Menu-item"
|
||||||
|
class:is-selected={isOptionSelected(getOptionValue(option, idx))}
|
||||||
|
role="option"
|
||||||
|
aria-selected="true"
|
||||||
|
tabindex="0"
|
||||||
|
on:click={() => onPick(getOptionValue(option, idx))}
|
||||||
|
>
|
||||||
|
<span class="spectrum-Menu-itemLabel">
|
||||||
|
{getOptionLabel(option, idx)}
|
||||||
|
</span>
|
||||||
|
<svg
|
||||||
|
class="spectrum-Icon spectrum-UIIcon-Checkmark100 spectrum-Menu-checkmark spectrum-Menu-itemIcon"
|
||||||
|
focusable="false"
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
<use xlink:href="#spectrum-css-icon-Checkmark100" />
|
||||||
|
</svg>
|
||||||
|
</li>
|
||||||
|
{/each}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.spectrum-InputGroup {
|
||||||
|
min-width: 0;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.spectrum-InputGroup-input {
|
||||||
|
border-right-width: 1px;
|
||||||
|
}
|
||||||
|
.spectrum-Textfield {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
.spectrum-Textfield-input {
|
||||||
|
width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.override-borders {
|
||||||
|
border-top-left-radius: 0px;
|
||||||
|
border-bottom-left-radius: 0px;
|
||||||
|
}
|
||||||
|
.spectrum-Popover {
|
||||||
|
max-height: 240px;
|
||||||
|
z-index: 999;
|
||||||
|
top: 100%;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -13,6 +13,7 @@
|
||||||
export let readonly = false
|
export let readonly = false
|
||||||
export let autocomplete = false
|
export let autocomplete = false
|
||||||
export let sort = false
|
export let sort = false
|
||||||
|
export let autoWidth = false
|
||||||
|
|
||||||
const dispatch = createEventDispatcher()
|
const dispatch = createEventDispatcher()
|
||||||
$: selectedLookupMap = getSelectedLookupMap(value)
|
$: selectedLookupMap = getSelectedLookupMap(value)
|
||||||
|
@ -85,4 +86,5 @@
|
||||||
{getOptionValue}
|
{getOptionValue}
|
||||||
onSelectOption={toggleOption}
|
onSelectOption={toggleOption}
|
||||||
{sort}
|
{sort}
|
||||||
|
{autoWidth}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -87,10 +87,15 @@
|
||||||
on:mousedown={onClick}
|
on:mousedown={onClick}
|
||||||
>
|
>
|
||||||
{#if fieldIcon}
|
{#if fieldIcon}
|
||||||
<span class="option-icon">
|
<span class="option-extra">
|
||||||
<Icon name={fieldIcon} />
|
<Icon name={fieldIcon} />
|
||||||
</span>
|
</span>
|
||||||
{/if}
|
{/if}
|
||||||
|
{#if fieldColour}
|
||||||
|
<span class="option-extra">
|
||||||
|
<StatusLight square color={fieldColour} />
|
||||||
|
</span>
|
||||||
|
{/if}
|
||||||
<span
|
<span
|
||||||
class="spectrum-Picker-label"
|
class="spectrum-Picker-label"
|
||||||
class:is-placeholder={isPlaceholder}
|
class:is-placeholder={isPlaceholder}
|
||||||
|
@ -108,11 +113,6 @@
|
||||||
<use xlink:href="#spectrum-icon-18-Alert" />
|
<use xlink:href="#spectrum-icon-18-Alert" />
|
||||||
</svg>
|
</svg>
|
||||||
{/if}
|
{/if}
|
||||||
{#if fieldColour}
|
|
||||||
<span class="option-colour">
|
|
||||||
<StatusLight size="L" color={fieldColour} />
|
|
||||||
</span>
|
|
||||||
{/if}
|
|
||||||
<svg
|
<svg
|
||||||
class="spectrum-Icon spectrum-UIIcon-ChevronDown100 spectrum-Picker-menuIcon"
|
class="spectrum-Icon spectrum-UIIcon-ChevronDown100 spectrum-Picker-menuIcon"
|
||||||
focusable="false"
|
focusable="false"
|
||||||
|
@ -166,10 +166,15 @@
|
||||||
on:click={() => onSelectOption(getOptionValue(option, idx))}
|
on:click={() => onSelectOption(getOptionValue(option, idx))}
|
||||||
>
|
>
|
||||||
{#if getOptionIcon(option, idx)}
|
{#if getOptionIcon(option, idx)}
|
||||||
<span class="option-icon">
|
<span class="option-extra">
|
||||||
<Icon name={getOptionIcon(option, idx)} />
|
<Icon name={getOptionIcon(option, idx)} />
|
||||||
</span>
|
</span>
|
||||||
{/if}
|
{/if}
|
||||||
|
{#if getOptionColour(option, idx)}
|
||||||
|
<span class="option-extra">
|
||||||
|
<StatusLight square color={getOptionColour(option, idx)} />
|
||||||
|
</span>
|
||||||
|
{/if}
|
||||||
<span class="spectrum-Menu-itemLabel">
|
<span class="spectrum-Menu-itemLabel">
|
||||||
{getOptionLabel(option, idx)}
|
{getOptionLabel(option, idx)}
|
||||||
</span>
|
</span>
|
||||||
|
@ -180,11 +185,6 @@
|
||||||
>
|
>
|
||||||
<use xlink:href="#spectrum-css-icon-Checkmark100" />
|
<use xlink:href="#spectrum-css-icon-Checkmark100" />
|
||||||
</svg>
|
</svg>
|
||||||
{#if getOptionColour(option, idx)}
|
|
||||||
<span class="option-colour">
|
|
||||||
<StatusLight size="L" color={getOptionColour(option, idx)} />
|
|
||||||
</span>
|
|
||||||
{/if}
|
|
||||||
</li>
|
</li>
|
||||||
{/each}
|
{/each}
|
||||||
{/if}
|
{/if}
|
||||||
|
@ -209,6 +209,9 @@
|
||||||
width: 100%;
|
width: 100%;
|
||||||
box-shadow: none;
|
box-shadow: none;
|
||||||
}
|
}
|
||||||
|
.spectrum-Picker-label.auto-width {
|
||||||
|
margin-right: var(--spacing-xs);
|
||||||
|
}
|
||||||
.spectrum-Picker-label:not(.auto-width) {
|
.spectrum-Picker-label:not(.auto-width) {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
|
@ -221,16 +224,16 @@
|
||||||
.spectrum-Picker-label.auto-width.is-placeholder {
|
.spectrum-Picker-label.auto-width.is-placeholder {
|
||||||
padding-right: 2px;
|
padding-right: 2px;
|
||||||
}
|
}
|
||||||
|
.auto-width .spectrum-Menu-item {
|
||||||
|
padding-right: var(--spacing-xl);
|
||||||
|
}
|
||||||
|
|
||||||
/* Icon and colour alignment */
|
/* Icon and colour alignment */
|
||||||
.spectrum-Menu-checkmark {
|
.spectrum-Menu-checkmark {
|
||||||
align-self: center;
|
align-self: center;
|
||||||
margin-top: 0;
|
margin-top: 0;
|
||||||
}
|
}
|
||||||
.option-colour {
|
.option-extra {
|
||||||
padding-left: 8px;
|
|
||||||
}
|
|
||||||
.option-icon {
|
|
||||||
padding-right: 8px;
|
padding-right: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,436 @@
|
||||||
|
<script>
|
||||||
|
import "@spectrum-css/inputgroup/dist/index-vars.css"
|
||||||
|
import "@spectrum-css/popover/dist/index-vars.css"
|
||||||
|
import "@spectrum-css/menu/dist/index-vars.css"
|
||||||
|
import { fly } from "svelte/transition"
|
||||||
|
import { createEventDispatcher } from "svelte"
|
||||||
|
import clickOutside from "../../Actions/click_outside"
|
||||||
|
import Icon from "../../Icon/Icon.svelte"
|
||||||
|
import StatusLight from "../../StatusLight/StatusLight.svelte"
|
||||||
|
import Detail from "../../Typography/Detail.svelte"
|
||||||
|
import Search from "./Search.svelte"
|
||||||
|
|
||||||
|
export let primaryLabel = ""
|
||||||
|
export let primaryValue = null
|
||||||
|
export let id = null
|
||||||
|
export let placeholder = "Choose an option or type"
|
||||||
|
export let disabled = false
|
||||||
|
export let updateOnChange = true
|
||||||
|
export let error = null
|
||||||
|
export let secondaryOptions = []
|
||||||
|
export let primaryOptions = []
|
||||||
|
export let secondaryFieldText = ""
|
||||||
|
export let secondaryFieldIcon = ""
|
||||||
|
export let secondaryFieldColour = ""
|
||||||
|
export let getPrimaryOptionValue = option => option
|
||||||
|
export let getPrimaryOptionColour = () => null
|
||||||
|
export let getPrimaryOptionIcon = () => null
|
||||||
|
export let getSecondaryOptionLabel = option => option
|
||||||
|
export let getSecondaryOptionValue = option => option
|
||||||
|
export let getSecondaryOptionColour = () => null
|
||||||
|
export let onSelectOption = () => {}
|
||||||
|
export let autoWidth = false
|
||||||
|
export let autocomplete = false
|
||||||
|
export let isOptionSelected = () => false
|
||||||
|
export let isPlaceholder = false
|
||||||
|
export let placeholderOption = null
|
||||||
|
export let showClearIcon = true
|
||||||
|
|
||||||
|
const dispatch = createEventDispatcher()
|
||||||
|
let primaryOpen = false
|
||||||
|
let secondaryOpen = false
|
||||||
|
let focus = false
|
||||||
|
let searchTerm = null
|
||||||
|
|
||||||
|
$: groupTitles = Object.keys(primaryOptions)
|
||||||
|
let iconData
|
||||||
|
|
||||||
|
const updateSearch = e => {
|
||||||
|
dispatch("search", e.detail)
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateValue = newValue => {
|
||||||
|
dispatch("change", newValue)
|
||||||
|
}
|
||||||
|
|
||||||
|
const onClickSecondary = () => {
|
||||||
|
dispatch("click")
|
||||||
|
secondaryOpen = true
|
||||||
|
}
|
||||||
|
|
||||||
|
const onPickPrimary = newValue => {
|
||||||
|
dispatch("pickprimary", newValue)
|
||||||
|
primaryOpen = false
|
||||||
|
}
|
||||||
|
|
||||||
|
const onClearPrimary = () => {
|
||||||
|
dispatch("pickprimary", null)
|
||||||
|
primaryOpen = false
|
||||||
|
}
|
||||||
|
|
||||||
|
const onPickSecondary = newValue => {
|
||||||
|
dispatch("picksecondary", newValue)
|
||||||
|
secondaryOpen = false
|
||||||
|
}
|
||||||
|
|
||||||
|
const onBlur = event => {
|
||||||
|
focus = false
|
||||||
|
updateValue(event.target.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
const onInput = event => {
|
||||||
|
updateValue(event.target.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateValueOnEnter = event => {
|
||||||
|
if (event.key === "Enter") {
|
||||||
|
updateValue(event.target.value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="spectrum-InputGroup"
|
||||||
|
class:is-invalid={!!error}
|
||||||
|
class:is-disabled={disabled}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="spectrum-Textfield spectrum-InputGroup-textfield"
|
||||||
|
class:is-invalid={!!error}
|
||||||
|
class:is-disabled={disabled}
|
||||||
|
class:is-focused={focus}
|
||||||
|
class:is-full-width={!secondaryOptions.length}
|
||||||
|
>
|
||||||
|
{#if iconData}
|
||||||
|
<svg
|
||||||
|
width="16px"
|
||||||
|
height="16px"
|
||||||
|
class="spectrum-Icon iconPadding"
|
||||||
|
style="color: {iconData?.color}"
|
||||||
|
focusable="false"
|
||||||
|
>
|
||||||
|
<use xlink:href="#spectrum-icon-18-{iconData?.icon}" />
|
||||||
|
</svg>
|
||||||
|
{/if}
|
||||||
|
<input
|
||||||
|
{id}
|
||||||
|
on:click={() => (primaryOpen = true)}
|
||||||
|
on:blur
|
||||||
|
on:focus
|
||||||
|
on:input
|
||||||
|
on:keyup
|
||||||
|
on:blur={onBlur}
|
||||||
|
on:input={onInput}
|
||||||
|
on:keyup={updateValueOnEnter}
|
||||||
|
value={primaryLabel || ""}
|
||||||
|
placeholder={placeholder || ""}
|
||||||
|
{disabled}
|
||||||
|
readonly
|
||||||
|
class="spectrum-Textfield-input spectrum-InputGroup-input"
|
||||||
|
class:labelPadding={iconData}
|
||||||
|
class:open={primaryOpen}
|
||||||
|
/>
|
||||||
|
{#if primaryValue && showClearIcon}
|
||||||
|
<button
|
||||||
|
on:click={() => onClearPrimary()}
|
||||||
|
type="reset"
|
||||||
|
class="spectrum-ClearButton spectrum-Search-clearButton"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
class="spectrum-Icon spectrum-UIIcon-Cross75"
|
||||||
|
focusable="false"
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
<use xlink:href="#spectrum-css-icon-Cross75" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{#if primaryOpen}
|
||||||
|
<div
|
||||||
|
use:clickOutside={() => (primaryOpen = false)}
|
||||||
|
transition:fly|local={{ y: -20, duration: 200 }}
|
||||||
|
class="spectrum-Popover spectrum-Popover--bottom spectrum-Picker-popover is-open"
|
||||||
|
class:auto-width={autoWidth}
|
||||||
|
class:is-full-width={!secondaryOptions.length}
|
||||||
|
>
|
||||||
|
{#if autocomplete}
|
||||||
|
<Search
|
||||||
|
value={searchTerm}
|
||||||
|
on:change={event => updateSearch(event)}
|
||||||
|
{disabled}
|
||||||
|
placeholder="Search"
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<ul class="spectrum-Menu" role="listbox">
|
||||||
|
{#if placeholderOption}
|
||||||
|
<li
|
||||||
|
class="spectrum-Menu-item placeholder"
|
||||||
|
class:is-selected={isPlaceholder}
|
||||||
|
role="option"
|
||||||
|
aria-selected="true"
|
||||||
|
tabindex="0"
|
||||||
|
on:click={() => onSelectOption(null)}
|
||||||
|
>
|
||||||
|
<span class="spectrum-Menu-itemLabel">{placeholderOption}</span>
|
||||||
|
<svg
|
||||||
|
class="spectrum-Icon spectrum-UIIcon-Checkmark100 spectrum-Menu-checkmark spectrum-Menu-itemIcon"
|
||||||
|
focusable="false"
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
<use xlink:href="#spectrum-css-icon-Checkmark100" />
|
||||||
|
</svg>
|
||||||
|
</li>
|
||||||
|
{/if}
|
||||||
|
{#each groupTitles as title}
|
||||||
|
<div class="spectrum-Menu-item title">
|
||||||
|
<Detail>{title}</Detail>
|
||||||
|
</div>
|
||||||
|
{#if primaryOptions}
|
||||||
|
{#each primaryOptions[title].data as option, idx}
|
||||||
|
<li
|
||||||
|
class="spectrum-Menu-item"
|
||||||
|
class:is-selected={isOptionSelected(
|
||||||
|
getPrimaryOptionValue(option, idx)
|
||||||
|
)}
|
||||||
|
role="option"
|
||||||
|
aria-selected="true"
|
||||||
|
tabindex="0"
|
||||||
|
on:click={() =>
|
||||||
|
onPickPrimary({
|
||||||
|
value: primaryOptions[title].getValue(option),
|
||||||
|
label: primaryOptions[title].getLabel(option),
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
{#if primaryOptions[title].getIcon(option)}
|
||||||
|
<div
|
||||||
|
style="background: {primaryOptions[title].getColour(
|
||||||
|
option
|
||||||
|
)};"
|
||||||
|
class="circle"
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<Icon
|
||||||
|
size="S"
|
||||||
|
name={primaryOptions[title].getIcon(option)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{:else if getPrimaryOptionColour(option, idx)}
|
||||||
|
<span class="option-left">
|
||||||
|
<StatusLight
|
||||||
|
square
|
||||||
|
color={getPrimaryOptionColour(option, idx)}
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
{/if}
|
||||||
|
<span class="spectrum-Menu-itemLabel">
|
||||||
|
<span
|
||||||
|
class:spacing-group={primaryOptions[title].getIcon(option)}
|
||||||
|
>
|
||||||
|
{primaryOptions[title].getLabel(option)}
|
||||||
|
<span />
|
||||||
|
</span>
|
||||||
|
<svg
|
||||||
|
class="spectrum-Icon spectrum-UIIcon-Checkmark100 spectrum-Menu-checkmark spectrum-Menu-itemIcon"
|
||||||
|
focusable="false"
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
<use xlink:href="#spectrum-css-icon-Checkmark100" />
|
||||||
|
</svg>
|
||||||
|
{#if getPrimaryOptionIcon(option, idx) && getPrimaryOptionColour(option, idx)}
|
||||||
|
<span class="option-right">
|
||||||
|
<StatusLight
|
||||||
|
square
|
||||||
|
color={getPrimaryOptionColour(option, idx)}
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
{/if}
|
||||||
|
</span>
|
||||||
|
</li>
|
||||||
|
{/each}
|
||||||
|
{/if}
|
||||||
|
{/each}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
{#if secondaryOptions.length}
|
||||||
|
<div style="width: 30%">
|
||||||
|
<button
|
||||||
|
{id}
|
||||||
|
class="spectrum-Picker spectrum-Picker--sizeM override-borders"
|
||||||
|
{disabled}
|
||||||
|
class:is-open={secondaryOpen}
|
||||||
|
aria-haspopup="listbox"
|
||||||
|
on:mousedown={onClickSecondary}
|
||||||
|
>
|
||||||
|
{#if secondaryFieldIcon}
|
||||||
|
<span class="option-left">
|
||||||
|
<Icon name={secondaryFieldIcon} />
|
||||||
|
</span>
|
||||||
|
{:else if secondaryFieldColour}
|
||||||
|
<span class="option-left">
|
||||||
|
<StatusLight square color={secondaryFieldColour} />
|
||||||
|
</span>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<span class:auto-width={autoWidth} class="spectrum-Picker-label">
|
||||||
|
{secondaryFieldText}
|
||||||
|
</span>
|
||||||
|
<svg
|
||||||
|
class="spectrum-Icon spectrum-UIIcon-ChevronDown100 spectrum-Picker-menuIcon"
|
||||||
|
focusable="false"
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
<use xlink:href="#spectrum-css-icon-Chevron100" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
{#if secondaryOpen}
|
||||||
|
<div
|
||||||
|
use:clickOutside={() => (secondaryOpen = false)}
|
||||||
|
transition:fly|local={{ y: -20, duration: 200 }}
|
||||||
|
class="spectrum-Popover spectrum-Popover--bottom spectrum-Picker-popover is-open"
|
||||||
|
style="width: 30%"
|
||||||
|
>
|
||||||
|
<ul class="spectrum-Menu" role="listbox">
|
||||||
|
{#each secondaryOptions as option, idx}
|
||||||
|
<li
|
||||||
|
class="spectrum-Menu-item"
|
||||||
|
class:is-selected={isOptionSelected(
|
||||||
|
getSecondaryOptionValue(option, idx)
|
||||||
|
)}
|
||||||
|
role="option"
|
||||||
|
aria-selected="true"
|
||||||
|
tabindex="0"
|
||||||
|
on:click={() =>
|
||||||
|
onPickSecondary(getSecondaryOptionValue(option, idx))}
|
||||||
|
>
|
||||||
|
{#if getSecondaryOptionColour(option, idx)}
|
||||||
|
<span class="option-left">
|
||||||
|
<StatusLight
|
||||||
|
square
|
||||||
|
color={getSecondaryOptionColour(option, idx)}
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<span class="spectrum-Menu-itemLabel">
|
||||||
|
{getSecondaryOptionLabel(option, idx)}
|
||||||
|
</span>
|
||||||
|
<svg
|
||||||
|
class="spectrum-Icon spectrum-UIIcon-Checkmark100 spectrum-Menu-checkmark spectrum-Menu-itemIcon"
|
||||||
|
focusable="false"
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
<use xlink:href="#spectrum-css-icon-Checkmark100" />
|
||||||
|
</svg>
|
||||||
|
</li>
|
||||||
|
{/each}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.spacing-group {
|
||||||
|
margin-left: var(--spacing-m);
|
||||||
|
}
|
||||||
|
.spectrum-InputGroup {
|
||||||
|
min-width: 0;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
.spectrum-InputGroup :global(.spectrum-Search-input) {
|
||||||
|
border: none;
|
||||||
|
border-bottom: 1px solid var(--spectrum-global-color-gray-300);
|
||||||
|
border-bottom-left-radius: 0;
|
||||||
|
border-bottom-right-radius: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.override-borders {
|
||||||
|
border-top-left-radius: 0px;
|
||||||
|
border-bottom-left-radius: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.spectrum-Popover {
|
||||||
|
max-height: 240px;
|
||||||
|
z-index: 999;
|
||||||
|
top: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.option-left {
|
||||||
|
padding-right: 8px;
|
||||||
|
}
|
||||||
|
.option-right {
|
||||||
|
padding-left: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.circle {
|
||||||
|
border-radius: 50%;
|
||||||
|
height: 28px;
|
||||||
|
color: white;
|
||||||
|
font-weight: bold;
|
||||||
|
line-height: 48px;
|
||||||
|
font-size: 1.2em;
|
||||||
|
width: 28px;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.circle > div {
|
||||||
|
position: absolute;
|
||||||
|
text-decoration: none;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%) translateY(-50%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.iconPadding {
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 10px;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
color: silver;
|
||||||
|
margin-right: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.labelPadding {
|
||||||
|
padding-left: calc(1em + 10px + 8px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.spectrum-Textfield.spectrum-InputGroup-textfield {
|
||||||
|
width: 70%;
|
||||||
|
}
|
||||||
|
.spectrum-Textfield.spectrum-InputGroup-textfield.is-full-width {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
.spectrum-Textfield.spectrum-InputGroup-textfield.is-full-width input {
|
||||||
|
border-right-width: thin;
|
||||||
|
}
|
||||||
|
|
||||||
|
.spectrum-Popover.spectrum-Popover--bottom.spectrum-Picker-popover.is-open {
|
||||||
|
width: 70%;
|
||||||
|
}
|
||||||
|
.spectrum-Popover.spectrum-Popover--bottom.spectrum-Picker-popover.is-open.is-full-width {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.spectrum-Search-clearButton {
|
||||||
|
position: absolute;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Fix focus borders to show only when opened */
|
||||||
|
.spectrum-Textfield-input {
|
||||||
|
border-color: var(--spectrum-global-color-gray-400) !important;
|
||||||
|
border-right-width: 1px;
|
||||||
|
}
|
||||||
|
.spectrum-Textfield-input.open {
|
||||||
|
border-color: var(--spectrum-global-color-blue-400) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Fix being able to hover and select titles */
|
||||||
|
.spectrum-Menu-item.title {
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -17,7 +17,6 @@
|
||||||
export let autoWidth = false
|
export let autoWidth = false
|
||||||
export let autocomplete = false
|
export let autocomplete = false
|
||||||
export let sort = false
|
export let sort = false
|
||||||
|
|
||||||
const dispatch = createEventDispatcher()
|
const dispatch = createEventDispatcher()
|
||||||
let open = false
|
let open = false
|
||||||
$: fieldText = getFieldText(value, options, placeholder)
|
$: fieldText = getFieldText(value, options, placeholder)
|
||||||
|
|
|
@ -0,0 +1,55 @@
|
||||||
|
<script>
|
||||||
|
import Field from "./Field.svelte"
|
||||||
|
import InputDropdown from "./Core/InputDropdown.svelte"
|
||||||
|
import { createEventDispatcher } from "svelte"
|
||||||
|
|
||||||
|
export let inputValue = null
|
||||||
|
export let dropdownValue = null
|
||||||
|
export let inputType = "text"
|
||||||
|
export let label = null
|
||||||
|
export let labelPosition = "above"
|
||||||
|
export let placeholder = null
|
||||||
|
export let disabled = false
|
||||||
|
export let readonly = false
|
||||||
|
export let error = null
|
||||||
|
export let updateOnChange = true
|
||||||
|
export let quiet = false
|
||||||
|
export let dataCy
|
||||||
|
export let autofocus
|
||||||
|
export let options = []
|
||||||
|
|
||||||
|
const dispatch = createEventDispatcher()
|
||||||
|
|
||||||
|
const onPick = e => {
|
||||||
|
dropdownValue = e.detail
|
||||||
|
dispatch("pick", e.detail)
|
||||||
|
}
|
||||||
|
const onChange = e => {
|
||||||
|
inputValue = e.detail
|
||||||
|
dispatch("change", e.detail)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Field {label} {labelPosition} {error}>
|
||||||
|
<InputDropdown
|
||||||
|
{dataCy}
|
||||||
|
{updateOnChange}
|
||||||
|
{error}
|
||||||
|
{disabled}
|
||||||
|
{readonly}
|
||||||
|
{inputValue}
|
||||||
|
{dropdownValue}
|
||||||
|
{placeholder}
|
||||||
|
{inputType}
|
||||||
|
{quiet}
|
||||||
|
{autofocus}
|
||||||
|
{options}
|
||||||
|
on:change={onChange}
|
||||||
|
on:pick={onPick}
|
||||||
|
on:click
|
||||||
|
on:input
|
||||||
|
on:blur
|
||||||
|
on:focus
|
||||||
|
on:keyup
|
||||||
|
/>
|
||||||
|
</Field>
|
|
@ -14,7 +14,7 @@
|
||||||
export let getOptionLabel = option => option
|
export let getOptionLabel = option => option
|
||||||
export let getOptionValue = option => option
|
export let getOptionValue = option => option
|
||||||
export let sort = false
|
export let sort = false
|
||||||
|
export let autoWidth = false
|
||||||
const dispatch = createEventDispatcher()
|
const dispatch = createEventDispatcher()
|
||||||
const onChange = e => {
|
const onChange = e => {
|
||||||
value = e.detail
|
value = e.detail
|
||||||
|
@ -33,6 +33,7 @@
|
||||||
{sort}
|
{sort}
|
||||||
{getOptionLabel}
|
{getOptionLabel}
|
||||||
{getOptionValue}
|
{getOptionValue}
|
||||||
|
{autoWidth}
|
||||||
on:change={onChange}
|
on:change={onChange}
|
||||||
on:click
|
on:click
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -0,0 +1,134 @@
|
||||||
|
<script>
|
||||||
|
import Field from "./Field.svelte"
|
||||||
|
import PickerDropdown from "./Core/PickerDropdown.svelte"
|
||||||
|
import { createEventDispatcher } from "svelte"
|
||||||
|
|
||||||
|
export let primaryValue = null
|
||||||
|
export let secondaryValue = null
|
||||||
|
export let inputType = "text"
|
||||||
|
export let label = null
|
||||||
|
export let labelPosition = "above"
|
||||||
|
export let secondaryPlaceholder = null
|
||||||
|
export let autocomplete
|
||||||
|
export let placeholder = null
|
||||||
|
export let disabled = false
|
||||||
|
export let readonly = false
|
||||||
|
export let error = null
|
||||||
|
export let updateOnChange = true
|
||||||
|
export let getSecondaryOptionLabel = option =>
|
||||||
|
extractProperty(option, "label")
|
||||||
|
export let getSecondaryOptionValue = option =>
|
||||||
|
extractProperty(option, "value")
|
||||||
|
export let getSecondaryOptionColour = () => {}
|
||||||
|
export let getSecondaryOptionIcon = () => {}
|
||||||
|
export let quiet = false
|
||||||
|
export let dataCy
|
||||||
|
export let autofocus
|
||||||
|
export let primaryOptions = []
|
||||||
|
export let secondaryOptions = []
|
||||||
|
export let searchTerm
|
||||||
|
export let showClearIcon = true
|
||||||
|
|
||||||
|
let primaryLabel
|
||||||
|
let secondaryLabel
|
||||||
|
const dispatch = createEventDispatcher()
|
||||||
|
|
||||||
|
$: secondaryFieldText = getSecondaryFieldText(
|
||||||
|
secondaryValue,
|
||||||
|
secondaryOptions,
|
||||||
|
secondaryPlaceholder
|
||||||
|
)
|
||||||
|
$: secondaryFieldIcon = getSecondaryFieldAttribute(
|
||||||
|
getSecondaryOptionIcon,
|
||||||
|
secondaryValue,
|
||||||
|
secondaryOptions
|
||||||
|
)
|
||||||
|
$: secondaryFieldColour = getSecondaryFieldAttribute(
|
||||||
|
getSecondaryOptionColour,
|
||||||
|
secondaryValue,
|
||||||
|
secondaryOptions
|
||||||
|
)
|
||||||
|
|
||||||
|
const getSecondaryFieldAttribute = (getAttribute, value, options) => {
|
||||||
|
// Wait for options to load if there is a value but no options
|
||||||
|
|
||||||
|
if (!options?.length) {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
const index = options.findIndex(
|
||||||
|
(option, idx) => getSecondaryOptionValue(option, idx) === value
|
||||||
|
)
|
||||||
|
|
||||||
|
return index !== -1 ? getAttribute(options[index], index) : null
|
||||||
|
}
|
||||||
|
|
||||||
|
const getSecondaryFieldText = (value, options, placeholder) => {
|
||||||
|
// Always use placeholder if no value
|
||||||
|
if (value == null || value === "") {
|
||||||
|
return placeholder || "Choose an option"
|
||||||
|
}
|
||||||
|
|
||||||
|
return getSecondaryFieldAttribute(getSecondaryOptionLabel, value, options)
|
||||||
|
}
|
||||||
|
|
||||||
|
const onPickPrimary = e => {
|
||||||
|
primaryLabel = e?.detail?.label || null
|
||||||
|
primaryValue = e?.detail?.value || null
|
||||||
|
dispatch("pickprimary", e?.detail?.value || {})
|
||||||
|
}
|
||||||
|
|
||||||
|
const onPickSecondary = e => {
|
||||||
|
secondaryValue = e.detail
|
||||||
|
dispatch("picksecondary", e.detail)
|
||||||
|
}
|
||||||
|
|
||||||
|
const extractProperty = (value, property) => {
|
||||||
|
if (value && typeof value === "object") {
|
||||||
|
return value[property]
|
||||||
|
}
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateSearchTerm = e => {
|
||||||
|
searchTerm = e.detail
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Field {label} {labelPosition} {error}>
|
||||||
|
<PickerDropdown
|
||||||
|
{searchTerm}
|
||||||
|
{autocomplete}
|
||||||
|
{dataCy}
|
||||||
|
{updateOnChange}
|
||||||
|
{error}
|
||||||
|
{disabled}
|
||||||
|
{readonly}
|
||||||
|
{placeholder}
|
||||||
|
{inputType}
|
||||||
|
{quiet}
|
||||||
|
{autofocus}
|
||||||
|
{primaryOptions}
|
||||||
|
{secondaryOptions}
|
||||||
|
{getSecondaryOptionLabel}
|
||||||
|
{getSecondaryOptionValue}
|
||||||
|
{getSecondaryOptionIcon}
|
||||||
|
{getSecondaryOptionColour}
|
||||||
|
{secondaryFieldText}
|
||||||
|
{secondaryFieldIcon}
|
||||||
|
{secondaryFieldColour}
|
||||||
|
{primaryValue}
|
||||||
|
{secondaryValue}
|
||||||
|
{primaryLabel}
|
||||||
|
{secondaryLabel}
|
||||||
|
{showClearIcon}
|
||||||
|
on:pickprimary={onPickPrimary}
|
||||||
|
on:picksecondary={onPickSecondary}
|
||||||
|
on:search={updateSearchTerm}
|
||||||
|
on:click
|
||||||
|
on:input
|
||||||
|
on:blur
|
||||||
|
on:focus
|
||||||
|
on:keyup
|
||||||
|
/>
|
||||||
|
</Field>
|
|
@ -0,0 +1,177 @@
|
||||||
|
<script>
|
||||||
|
//import { createEventDispatcher } from "svelte"
|
||||||
|
import "@spectrum-css/popover/dist/index-vars.css"
|
||||||
|
import clickOutside from "../Actions/click_outside"
|
||||||
|
import { fly } from "svelte/transition"
|
||||||
|
import Icon from "../Icon/Icon.svelte"
|
||||||
|
import { createEventDispatcher } from "svelte"
|
||||||
|
|
||||||
|
export let value
|
||||||
|
export let size = "M"
|
||||||
|
export let alignRight = false
|
||||||
|
|
||||||
|
let open = false
|
||||||
|
|
||||||
|
const dispatch = createEventDispatcher()
|
||||||
|
|
||||||
|
const iconList = [
|
||||||
|
{
|
||||||
|
label: "Icons",
|
||||||
|
icons: [
|
||||||
|
"Apps",
|
||||||
|
"Actions",
|
||||||
|
"ConversionFunnel",
|
||||||
|
"App",
|
||||||
|
"Briefcase",
|
||||||
|
"Money",
|
||||||
|
"ShoppingCart",
|
||||||
|
"Form",
|
||||||
|
"Help",
|
||||||
|
"Monitoring",
|
||||||
|
"Sandbox",
|
||||||
|
"Project",
|
||||||
|
"Organisations",
|
||||||
|
"Magnify",
|
||||||
|
"Launch",
|
||||||
|
"Car",
|
||||||
|
"Camera",
|
||||||
|
"Bug",
|
||||||
|
"Channel",
|
||||||
|
"Calculator",
|
||||||
|
"Calendar",
|
||||||
|
"GraphDonut",
|
||||||
|
"GraphBarHorizontal",
|
||||||
|
"Demographic",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
const onChange = value => {
|
||||||
|
dispatch("change", value)
|
||||||
|
open = false
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="container">
|
||||||
|
<div class="preview size--{size || 'M'}" on:click={() => (open = true)}>
|
||||||
|
<div
|
||||||
|
class="fill"
|
||||||
|
style={value ? `background: ${value};` : ""}
|
||||||
|
class:placeholder={!value}
|
||||||
|
>
|
||||||
|
<Icon name={value || "UserGroup"} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{#if open}
|
||||||
|
<div
|
||||||
|
use:clickOutside={() => (open = false)}
|
||||||
|
transition:fly={{ y: -20, duration: 200 }}
|
||||||
|
class="spectrum-Popover spectrum-Popover--bottom spectrum-Picker-popover is-open"
|
||||||
|
class:spectrum-Popover--align-right={alignRight}
|
||||||
|
>
|
||||||
|
{#each iconList as icon}
|
||||||
|
<div class="category">
|
||||||
|
<div class="heading">{icon.label}</div>
|
||||||
|
<div class="icons">
|
||||||
|
{#each icon.icons as icon}
|
||||||
|
<div
|
||||||
|
on:click={() => {
|
||||||
|
onChange(icon)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Icon name={icon} />
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.container {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
.preview {
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
position: relative;
|
||||||
|
box-shadow: 0 0 0 1px var(--spectrum-global-color-gray-400);
|
||||||
|
}
|
||||||
|
.preview:hover {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.fill {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
}
|
||||||
|
.size--S {
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
}
|
||||||
|
.size--M {
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
}
|
||||||
|
.size--L {
|
||||||
|
width: 48px;
|
||||||
|
height: 48px;
|
||||||
|
}
|
||||||
|
.spectrum-Popover {
|
||||||
|
width: 210px;
|
||||||
|
z-index: 999;
|
||||||
|
top: 100%;
|
||||||
|
padding: var(--spacing-l) var(--spacing-xl);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: flex-start;
|
||||||
|
align-items: stretch;
|
||||||
|
gap: var(--spacing-xl);
|
||||||
|
}
|
||||||
|
.spectrum-Popover--align-right {
|
||||||
|
right: 0;
|
||||||
|
}
|
||||||
|
.icons {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr 1fr 1fr 1fr 1fr;
|
||||||
|
gap: var(--spacing-m);
|
||||||
|
}
|
||||||
|
.heading {
|
||||||
|
font-size: var(--font-size-s);
|
||||||
|
font-weight: 600;
|
||||||
|
letter-spacing: 0.14px;
|
||||||
|
flex: 1 1 auto;
|
||||||
|
text-transform: uppercase;
|
||||||
|
grid-column: 1 / 5;
|
||||||
|
margin-bottom: var(--spacing-s);
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon {
|
||||||
|
height: 16px;
|
||||||
|
width: 16px;
|
||||||
|
border-radius: 100%;
|
||||||
|
box-shadow: 0 0 0 1px var(--spectrum-global-color-gray-300);
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
.icon:hover {
|
||||||
|
cursor: pointer;
|
||||||
|
box-shadow: 0 0 2px 2px var(--spectrum-global-color-gray-300);
|
||||||
|
}
|
||||||
|
.custom {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr auto;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--spacing-m);
|
||||||
|
margin-right: var(--spacing-xs);
|
||||||
|
}
|
||||||
|
|
||||||
|
.spectrum-wrapper {
|
||||||
|
background-color: transparent;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -1,53 +0,0 @@
|
||||||
<script>
|
|
||||||
import { View } from "svench";
|
|
||||||
import DetailSummary from "./DetailSummary.svelte";
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<svelte:head>
|
|
||||||
<link href="https://cdn.jsdelivr.net/npm/remixicon@2.5.0/fonts/remixicon.css" rel="stylesheet">
|
|
||||||
</svelte:head>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
div {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
justify-content: flex-start;
|
|
||||||
align-items: stretch;
|
|
||||||
gap: var(--spacing-m);
|
|
||||||
width: 120px;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
||||||
<View name="default">
|
|
||||||
<div>
|
|
||||||
<DetailSummary name="Category 1">
|
|
||||||
<span>1</span>
|
|
||||||
<span>2</span>
|
|
||||||
<span>3</span>
|
|
||||||
<span>4</span>
|
|
||||||
</DetailSummary>
|
|
||||||
<DetailSummary name="Category 2">
|
|
||||||
<span>1</span>
|
|
||||||
<span>2</span>
|
|
||||||
<span>3</span>
|
|
||||||
<span>4</span>
|
|
||||||
</DetailSummary>
|
|
||||||
</div>
|
|
||||||
</View>
|
|
||||||
|
|
||||||
<View name="thin">
|
|
||||||
<div>
|
|
||||||
<DetailSummary thin name="Category 1">
|
|
||||||
<span>1</span>
|
|
||||||
<span>2</span>
|
|
||||||
<span>3</span>
|
|
||||||
<span>4</span>
|
|
||||||
</DetailSummary>
|
|
||||||
<DetailSummary thin name="Category 2">
|
|
||||||
<span>1</span>
|
|
||||||
<span>2</span>
|
|
||||||
<span>3</span>
|
|
||||||
<span>4</span>
|
|
||||||
</DetailSummary>
|
|
||||||
</div>
|
|
||||||
</View>
|
|
|
@ -0,0 +1,28 @@
|
||||||
|
<script>
|
||||||
|
import Detail from "../Typography/Detail.svelte"
|
||||||
|
|
||||||
|
export let title = null
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
{#if title}
|
||||||
|
<div class="title">
|
||||||
|
<Detail>{title}</Detail>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
<div class="list-items">
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.title {
|
||||||
|
margin-bottom: 6px;
|
||||||
|
}
|
||||||
|
.list-items {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: flex-start;
|
||||||
|
align-items: stretch;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,98 @@
|
||||||
|
<script>
|
||||||
|
import Body from "../Typography/Body.svelte"
|
||||||
|
import Icon from "../Icon/Icon.svelte"
|
||||||
|
import Label from "../Label/Label.svelte"
|
||||||
|
import Avatar from "../Avatar/Avatar.svelte"
|
||||||
|
|
||||||
|
export let icon = null
|
||||||
|
export let iconBackground = null
|
||||||
|
export let avatar = false
|
||||||
|
export let title = null
|
||||||
|
export let subtitle = null
|
||||||
|
export let hoverable = false
|
||||||
|
|
||||||
|
$: initials = avatar ? title?.[0] : null
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="list-item" class:hoverable on:click>
|
||||||
|
<div class="left">
|
||||||
|
{#if icon}
|
||||||
|
<div class="icon" style="background: {iconBackground || `transparent`};">
|
||||||
|
<Icon name={icon} size="S" color={iconBackground ? "white" : null} />
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
{#if avatar}
|
||||||
|
<Avatar {initials} />
|
||||||
|
{/if}
|
||||||
|
{#if title}
|
||||||
|
<Body>{title}</Body>
|
||||||
|
{/if}
|
||||||
|
{#if subtitle}
|
||||||
|
<Label>{subtitle}</Label>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
<div class="right">
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.list-item {
|
||||||
|
padding: 0 16px;
|
||||||
|
height: 56px;
|
||||||
|
background: var(--spectrum-global-color-gray-50);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: space-between;
|
||||||
|
border: 1px solid var(--spectrum-global-color-gray-300);
|
||||||
|
transition: background 130ms ease-out;
|
||||||
|
}
|
||||||
|
.list-item:not(:first-child) {
|
||||||
|
border-top: none;
|
||||||
|
}
|
||||||
|
.list-item:first-child {
|
||||||
|
border-top-left-radius: 4px;
|
||||||
|
border-top-right-radius: 4px;
|
||||||
|
}
|
||||||
|
.list-item:last-child {
|
||||||
|
border-bottom-left-radius: 4px;
|
||||||
|
border-bottom-right-radius: 4px;
|
||||||
|
}
|
||||||
|
.hoverable:hover {
|
||||||
|
cursor: pointer;
|
||||||
|
background: var(--spectrum-global-color-gray-75);
|
||||||
|
}
|
||||||
|
.left,
|
||||||
|
.right {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--spacing-xl);
|
||||||
|
}
|
||||||
|
.left {
|
||||||
|
width: 0;
|
||||||
|
flex: 1 1 auto;
|
||||||
|
}
|
||||||
|
.right {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
}
|
||||||
|
.list-item :global(.spectrum-Icon),
|
||||||
|
.list-item :global(.spectrum-Avatar) {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
}
|
||||||
|
.list-item :global(.spectrum-Body) {
|
||||||
|
color: var(--spectrum-global-color-gray-900);
|
||||||
|
}
|
||||||
|
.list-item :global(.spectrum-Body) {
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
.icon {
|
||||||
|
width: var(--spectrum-alias-avatar-size-400);
|
||||||
|
height: var(--spectrum-alias-avatar-size-400);
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -106,7 +106,9 @@
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if showCancelButton}
|
{#if showCancelButton}
|
||||||
<Button group secondary on:click={close}>{cancelText}</Button>
|
<Button group secondary newStyles on:click={close}>
|
||||||
|
{cancelText}
|
||||||
|
</Button>
|
||||||
{/if}
|
{/if}
|
||||||
{#if showConfirmButton}
|
{#if showConfirmButton}
|
||||||
<span class="confirm-wrap">
|
<span class="confirm-wrap">
|
||||||
|
|
|
@ -18,11 +18,16 @@
|
||||||
export let disabled = false
|
export let disabled = false
|
||||||
export let active = false
|
export let active = false
|
||||||
export let color = null
|
export let color = null
|
||||||
|
export let square = false
|
||||||
|
export let hoverable = false
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
|
on:click
|
||||||
class="spectrum-StatusLight spectrum-StatusLight--size{size}"
|
class="spectrum-StatusLight spectrum-StatusLight--size{size}"
|
||||||
class:custom={!!color}
|
class:custom={!!color}
|
||||||
|
class:square
|
||||||
|
class:hoverable
|
||||||
style={`--color: ${color};`}
|
style={`--color: ${color};`}
|
||||||
class:spectrum-StatusLight--celery={celery}
|
class:spectrum-StatusLight--celery={celery}
|
||||||
class:spectrum-StatusLight--yellow={yellow}
|
class:spectrum-StatusLight--yellow={yellow}
|
||||||
|
@ -54,6 +59,7 @@
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
padding-top: 0;
|
padding-top: 0;
|
||||||
padding-bottom: 0;
|
padding-bottom: 0;
|
||||||
|
transition: color ease-out 130ms;
|
||||||
}
|
}
|
||||||
.spectrum-StatusLight.withText::before {
|
.spectrum-StatusLight.withText::before {
|
||||||
margin-right: 10px;
|
margin-right: 10px;
|
||||||
|
@ -61,4 +67,14 @@
|
||||||
.custom::before {
|
.custom::before {
|
||||||
background: var(--color) !important;
|
background: var(--color) !important;
|
||||||
}
|
}
|
||||||
|
.square::before {
|
||||||
|
width: 14px;
|
||||||
|
height: 14px;
|
||||||
|
border-radius: 4px;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
.hoverable:hover {
|
||||||
|
cursor: pointer;
|
||||||
|
color: var(--spectrum-global-color-gray-900);
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
<script>
|
<script>
|
||||||
import Tooltip from "../Tooltip/Tooltip.svelte"
|
|
||||||
import Link from "../Link/Link.svelte"
|
import Link from "../Link/Link.svelte"
|
||||||
|
|
||||||
export let value
|
export let value
|
||||||
|
@ -17,18 +16,16 @@
|
||||||
{#each attachments as attachment}
|
{#each attachments as attachment}
|
||||||
{#if isImage(attachment.extension)}
|
{#if isImage(attachment.extension)}
|
||||||
<Link quiet target="_blank" href={attachment.url}>
|
<Link quiet target="_blank" href={attachment.url}>
|
||||||
<div class="center">
|
<div class="center" title={attachment.name}>
|
||||||
<img src={attachment.url} alt={attachment.extension} />
|
<img src={attachment.url} alt={attachment.extension} />
|
||||||
</div>
|
</div>
|
||||||
</Link>
|
</Link>
|
||||||
{:else}
|
{:else}
|
||||||
<Tooltip text={attachment.name} direction="right">
|
<div class="file" title={attachment.name}>
|
||||||
<div class="file">
|
<Link quiet target="_blank" href={attachment.url}>
|
||||||
<Link quiet target="_blank" href={attachment.url}>
|
{attachment.extension}
|
||||||
{attachment.extension}
|
</Link>
|
||||||
</Link>
|
</div>
|
||||||
</div>
|
|
||||||
</Tooltip>
|
|
||||||
{/if}
|
{/if}
|
||||||
{/each}
|
{/each}
|
||||||
{#if leftover}
|
{#if leftover}
|
||||||
|
@ -52,7 +49,7 @@
|
||||||
padding: 0 8px;
|
padding: 0 8px;
|
||||||
color: var(--spectrum-global-color-gray-800);
|
color: var(--spectrum-global-color-gray-800);
|
||||||
border: 1px solid var(--spectrum-global-color-gray-300);
|
border: 1px solid var(--spectrum-global-color-gray-300);
|
||||||
border-radius: 2px;
|
border-radius: 4px;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
|
|
|
@ -37,6 +37,7 @@
|
||||||
export let autoSortColumns = true
|
export let autoSortColumns = true
|
||||||
export let compact = false
|
export let compact = false
|
||||||
export let customPlaceholder = false
|
export let customPlaceholder = false
|
||||||
|
export let showHeaderBorder = true
|
||||||
export let placeholderText = "No rows found"
|
export let placeholderText = "No rows found"
|
||||||
|
|
||||||
const dispatch = createEventDispatcher()
|
const dispatch = createEventDispatcher()
|
||||||
|
@ -286,6 +287,7 @@
|
||||||
<div class="spectrum-Table-head">
|
<div class="spectrum-Table-head">
|
||||||
{#if showEditColumn}
|
{#if showEditColumn}
|
||||||
<div
|
<div
|
||||||
|
class:noBorderHeader={!showHeaderBorder}
|
||||||
class="spectrum-Table-headCell spectrum-Table-headCell--divider spectrum-Table-headCell--edit"
|
class="spectrum-Table-headCell spectrum-Table-headCell--divider spectrum-Table-headCell--edit"
|
||||||
>
|
>
|
||||||
{#if allowSelectRows}
|
{#if allowSelectRows}
|
||||||
|
@ -301,6 +303,7 @@
|
||||||
{#each fields as field}
|
{#each fields as field}
|
||||||
<div
|
<div
|
||||||
class="spectrum-Table-headCell"
|
class="spectrum-Table-headCell"
|
||||||
|
class:noBorderHeader={!showHeaderBorder}
|
||||||
class:spectrum-Table-headCell--alignCenter={schema[field]
|
class:spectrum-Table-headCell--alignCenter={schema[field]
|
||||||
.align === "Center"}
|
.align === "Center"}
|
||||||
class:spectrum-Table-headCell--alignRight={schema[field].align ===
|
class:spectrum-Table-headCell--alignRight={schema[field].align ===
|
||||||
|
@ -348,6 +351,7 @@
|
||||||
<div class="spectrum-Table-row">
|
<div class="spectrum-Table-row">
|
||||||
{#if showEditColumn}
|
{#if showEditColumn}
|
||||||
<div
|
<div
|
||||||
|
class:noBorderCheckbox={!showHeaderBorder}
|
||||||
class="spectrum-Table-cell spectrum-Table-cell--divider spectrum-Table-cell--edit"
|
class="spectrum-Table-cell spectrum-Table-cell--divider spectrum-Table-cell--edit"
|
||||||
on:click={e => {
|
on:click={e => {
|
||||||
toggleSelectRow(row)
|
toggleSelectRow(row)
|
||||||
|
@ -481,25 +485,31 @@
|
||||||
.spectrum-Table-headCell:last-of-type {
|
.spectrum-Table-headCell:last-of-type {
|
||||||
border-right: var(--table-border);
|
border-right: var(--table-border);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.noBorderHeader {
|
||||||
|
border-top: none !important;
|
||||||
|
border-right: none !important;
|
||||||
|
border-left: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.noBorderCheckbox {
|
||||||
|
border-top: none !important;
|
||||||
|
border-right: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
.spectrum-Table-headCell--alignCenter {
|
.spectrum-Table-headCell--alignCenter {
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
}
|
}
|
||||||
.spectrum-Table-headCell--alignRight {
|
.spectrum-Table-headCell--alignRight {
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
}
|
}
|
||||||
.spectrum-Table-headCell--divider {
|
|
||||||
padding-right: var(--cell-padding);
|
|
||||||
}
|
|
||||||
.spectrum-Table-headCell--divider + .spectrum-Table-headCell {
|
|
||||||
padding-left: var(--cell-padding);
|
|
||||||
}
|
|
||||||
.spectrum-Table-headCell--edit {
|
.spectrum-Table-headCell--edit {
|
||||||
position: sticky;
|
position: sticky;
|
||||||
left: 0;
|
left: 0;
|
||||||
z-index: 3;
|
z-index: 3;
|
||||||
}
|
}
|
||||||
.spectrum-Table-headCell .title {
|
.spectrum-Table-headCell .title {
|
||||||
overflow: hidden;
|
overflow: visible;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
}
|
}
|
||||||
.spectrum-Table-headCell:hover .spectrum-Table-editIcon {
|
.spectrum-Table-headCell:hover .spectrum-Table-editIcon {
|
||||||
|
@ -562,13 +572,7 @@
|
||||||
gap: 4px;
|
gap: 4px;
|
||||||
border-bottom: 1px solid var(--spectrum-alias-border-color-mid);
|
border-bottom: 1px solid var(--spectrum-alias-border-color-mid);
|
||||||
background-color: var(--table-bg);
|
background-color: var(--table-bg);
|
||||||
z-index: 1;
|
z-index: auto;
|
||||||
}
|
|
||||||
.spectrum-Table-cell--divider {
|
|
||||||
padding-right: var(--cell-padding);
|
|
||||||
}
|
|
||||||
.spectrum-Table-cell--divider + .spectrum-Table-cell {
|
|
||||||
padding-left: var(--cell-padding);
|
|
||||||
}
|
}
|
||||||
.spectrum-Table-cell--edit {
|
.spectrum-Table-cell--edit {
|
||||||
position: sticky;
|
position: sticky;
|
||||||
|
|
|
@ -26,5 +26,9 @@
|
||||||
<style>
|
<style>
|
||||||
.tooltip {
|
.tooltip {
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
|
background: var(--spectrum-global-color-gray-500);
|
||||||
|
}
|
||||||
|
.spectrum-Tooltip-tip {
|
||||||
|
border-top-color: var(--spectrum-global-color-gray-500);
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -23,6 +23,8 @@ export { default as Icon, directions } from "./Icon/Icon.svelte"
|
||||||
export { default as Toggle } from "./Form/Toggle.svelte"
|
export { default as Toggle } from "./Form/Toggle.svelte"
|
||||||
export { default as RadioGroup } from "./Form/RadioGroup.svelte"
|
export { default as RadioGroup } from "./Form/RadioGroup.svelte"
|
||||||
export { default as Checkbox } from "./Form/Checkbox.svelte"
|
export { default as Checkbox } from "./Form/Checkbox.svelte"
|
||||||
|
export { default as InputDropdown } from "./Form/InputDropdown.svelte"
|
||||||
|
export { default as PickerDropdown } from "./Form/PickerDropdown.svelte"
|
||||||
export { default as DetailSummary } from "./DetailSummary/DetailSummary.svelte"
|
export { default as DetailSummary } from "./DetailSummary/DetailSummary.svelte"
|
||||||
export { default as Popover } from "./Popover/Popover.svelte"
|
export { default as Popover } from "./Popover/Popover.svelte"
|
||||||
export { default as ProgressBar } from "./ProgressBar/ProgressBar.svelte"
|
export { default as ProgressBar } from "./ProgressBar/ProgressBar.svelte"
|
||||||
|
@ -58,12 +60,15 @@ export { default as Pagination } from "./Pagination/Pagination.svelte"
|
||||||
export { default as Badge } from "./Badge/Badge.svelte"
|
export { default as Badge } from "./Badge/Badge.svelte"
|
||||||
export { default as StatusLight } from "./StatusLight/StatusLight.svelte"
|
export { default as StatusLight } from "./StatusLight/StatusLight.svelte"
|
||||||
export { default as ColorPicker } from "./ColorPicker/ColorPicker.svelte"
|
export { default as ColorPicker } from "./ColorPicker/ColorPicker.svelte"
|
||||||
|
export { default as IconPicker } from "./IconPicker/IconPicker.svelte"
|
||||||
export { default as InlineAlert } from "./InlineAlert/InlineAlert.svelte"
|
export { default as InlineAlert } from "./InlineAlert/InlineAlert.svelte"
|
||||||
export { default as Banner } from "./Banner/Banner.svelte"
|
export { default as Banner } from "./Banner/Banner.svelte"
|
||||||
export { default as BannerDisplay } from "./Banner/BannerDisplay.svelte"
|
export { default as BannerDisplay } from "./Banner/BannerDisplay.svelte"
|
||||||
export { default as MarkdownEditor } from "./Markdown/MarkdownEditor.svelte"
|
export { default as MarkdownEditor } from "./Markdown/MarkdownEditor.svelte"
|
||||||
export { default as MarkdownViewer } from "./Markdown/MarkdownViewer.svelte"
|
export { default as MarkdownViewer } from "./Markdown/MarkdownViewer.svelte"
|
||||||
export { default as RichTextField } from "./Form/RichTextField.svelte"
|
export { default as RichTextField } from "./Form/RichTextField.svelte"
|
||||||
|
export { default as List } from "./List/List.svelte"
|
||||||
|
export { default as ListItem } from "./List/ListItem.svelte"
|
||||||
export { default as IconSideNav } from "./IconSideNav/IconSideNav.svelte"
|
export { default as IconSideNav } from "./IconSideNav/IconSideNav.svelte"
|
||||||
export { default as IconSideNavItem } from "./IconSideNav/IconSideNavItem.svelte"
|
export { default as IconSideNavItem } from "./IconSideNav/IconSideNavItem.svelte"
|
||||||
export { default as Slider } from "./Form/Slider.svelte"
|
export { default as Slider } from "./Form/Slider.svelte"
|
||||||
|
@ -71,6 +76,7 @@ export { default as Slider } from "./Form/Slider.svelte"
|
||||||
// Renderers
|
// Renderers
|
||||||
export { default as BoldRenderer } from "./Table/BoldRenderer.svelte"
|
export { default as BoldRenderer } from "./Table/BoldRenderer.svelte"
|
||||||
export { default as CodeRenderer } from "./Table/CodeRenderer.svelte"
|
export { default as CodeRenderer } from "./Table/CodeRenderer.svelte"
|
||||||
|
export { default as InternalRenderer } from "./Table/InternalRenderer.svelte"
|
||||||
|
|
||||||
// Typography
|
// Typography
|
||||||
export { default as Body } from "./Typography/Body.svelte"
|
export { default as Body } from "./Typography/Body.svelte"
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -19,9 +19,14 @@ filterTests(["smoke", "all"], () => {
|
||||||
cy.wait(500)
|
cy.wait(500)
|
||||||
|
|
||||||
// Reset password
|
// Reset password
|
||||||
cy.get(".spectrum-ActionButton-label", { timeout: 2000 }).contains("Force password reset").click({ force: true })
|
cy.get(".title").within(() => {
|
||||||
|
cy.get(interact.SPECTRUM_ICON).click({ force: true })
|
||||||
|
})
|
||||||
|
cy.get(interact.SPECTRUM_MENU).within(() => {
|
||||||
|
cy.get(interact.SPECTRUM_MENU_ITEM).contains("Force Password Reset").click({ force: true })
|
||||||
|
})
|
||||||
|
|
||||||
cy.get(".spectrum-Dialog-grid")
|
cy.get(interact.SPECTRUM_DIALOG_GRID)
|
||||||
.find(interact.SPECTRUM_TEXTFIELD_INPUT).invoke('val').as('pwd')
|
.find(interact.SPECTRUM_TEXTFIELD_INPUT).invoke('val').as('pwd')
|
||||||
|
|
||||||
cy.get(interact.SPECTRUM_BUTTON).contains("Reset password").click({ force: true })
|
cy.get(interact.SPECTRUM_BUTTON).contains("Reset password").click({ force: true })
|
||||||
|
@ -39,23 +44,14 @@ filterTests(["smoke", "all"], () => {
|
||||||
cy.logoutNoAppGrid()
|
cy.logoutNoAppGrid()
|
||||||
})
|
})
|
||||||
|
|
||||||
it("should verify Admin Portal", () => {
|
xit("should verify Admin Portal", () => {
|
||||||
cy.login()
|
cy.login()
|
||||||
cy.contains("Users").click()
|
// Configure user role
|
||||||
cy.contains("bbuser").click()
|
cy.setUserRole("bbuser", "Admin")
|
||||||
|
|
||||||
// Enable Development & Administration access
|
|
||||||
cy.wait(500)
|
|
||||||
for (let i = 4; i < 6; i++) {
|
|
||||||
cy.get(interact.FIELD).eq(i).within(() => {
|
|
||||||
cy.get(interact.SPECTRUM_SWITCH_INPUT).click({ force: true })
|
|
||||||
cy.get(interact.SPECTRUM_SWITCH_INPUT).should('be.enabled')
|
|
||||||
})
|
|
||||||
}
|
|
||||||
bbUserLogin()
|
bbUserLogin()
|
||||||
|
|
||||||
// Verify available options for Admin portal
|
// Verify available options for Admin portal
|
||||||
cy.get(".spectrum-SideNav")
|
cy.get(interact.SPECTRUM_SIDENAV)
|
||||||
.should('contain', 'Apps')
|
.should('contain', 'Apps')
|
||||||
//.and('contain', 'Usage')
|
//.and('contain', 'Usage')
|
||||||
.and('contain', 'Users')
|
.and('contain', 'Users')
|
||||||
|
@ -72,13 +68,7 @@ filterTests(["smoke", "all"], () => {
|
||||||
it("should verify Development Portal", () => {
|
it("should verify Development Portal", () => {
|
||||||
// Only Development access should be enabled
|
// Only Development access should be enabled
|
||||||
cy.login()
|
cy.login()
|
||||||
cy.contains("Users").click()
|
cy.setUserRole("bbuser", "Developer")
|
||||||
cy.contains("bbuser").click()
|
|
||||||
cy.wait(500)
|
|
||||||
cy.get(interact.FIELD).eq(5).within(() => {
|
|
||||||
cy.get(interact.SPECTRUM_SWITCH_INPUT).click({ force: true })
|
|
||||||
})
|
|
||||||
|
|
||||||
bbUserLogin()
|
bbUserLogin()
|
||||||
|
|
||||||
// Verify available options for Admin portal
|
// Verify available options for Admin portal
|
||||||
|
@ -99,13 +89,7 @@ filterTests(["smoke", "all"], () => {
|
||||||
it("should verify Standard Portal", () => {
|
it("should verify Standard Portal", () => {
|
||||||
// Development access should be disabled (Admin access is already disabled)
|
// Development access should be disabled (Admin access is already disabled)
|
||||||
cy.login()
|
cy.login()
|
||||||
cy.contains("Users").click()
|
cy.setUserRole("bbuser", "App User")
|
||||||
cy.contains("bbuser").click()
|
|
||||||
cy.wait(500)
|
|
||||||
cy.get(interact.FIELD).eq(4).within(() => {
|
|
||||||
cy.get(interact.SPECTRUM_SWITCH_INPUT).click({ force: true })
|
|
||||||
})
|
|
||||||
|
|
||||||
bbUserLogin()
|
bbUserLogin()
|
||||||
|
|
||||||
// Verify Standard Portal
|
// Verify Standard Portal
|
||||||
|
|
|
@ -15,25 +15,16 @@ filterTests(["smoke", "all"], () => {
|
||||||
cy.get(interact.SPECTRUM_TABLE).should("contain", "bbuser")
|
cy.get(interact.SPECTRUM_TABLE).should("contain", "bbuser")
|
||||||
})
|
})
|
||||||
|
|
||||||
it("should confirm basic permission for a New User", () => {
|
it("should confirm App User role for a New User", () => {
|
||||||
// Basic permission = development & administraton disabled
|
|
||||||
cy.contains("bbuser").click()
|
cy.contains("bbuser").click()
|
||||||
// Confirm development and admin access are disabled
|
cy.get(".spectrum-Form-itemField").eq(2).should('contain', 'App User')
|
||||||
for (let i = 4; i < 6; i++) {
|
|
||||||
cy.wait(500)
|
// User should not have app access
|
||||||
cy.get(interact.FIELD).eq(i).within(() => {
|
cy.get(interact.LIST_ITEMS, { timeout: 500 }).should("contain", "No apps")
|
||||||
//cy.get(interact.SPECTRUM_SWITCH_INPUT).should('be.disabled')
|
|
||||||
cy.get(".spectrum-Switch-switch").should('not.be.checked')
|
|
||||||
})
|
|
||||||
}
|
|
||||||
// Existing apps appear within the No Access table
|
|
||||||
cy.get(interact.SPECTRUM_TABLE, { timeout: 500 }).eq(1).should("not.contain", "No rows found")
|
|
||||||
// Configure roles table should not contain apps
|
|
||||||
cy.get(interact.SPECTRUM_TABLE).eq(0).contains("No rows found")
|
|
||||||
})
|
})
|
||||||
|
|
||||||
if (Cypress.env("TEST_ENV")) {
|
if (Cypress.env("TEST_ENV")) {
|
||||||
it("should assign role types", () => {
|
xit("should assign role types", () => {
|
||||||
// 3 apps minimum required - to assign an app to each role type
|
// 3 apps minimum required - to assign an app to each role type
|
||||||
cy.request(`${Cypress.config().baseUrl}/api/applications?status=all`)
|
cy.request(`${Cypress.config().baseUrl}/api/applications?status=all`)
|
||||||
.its("body")
|
.its("body")
|
||||||
|
@ -57,6 +48,7 @@ filterTests(["smoke", "all"], () => {
|
||||||
cy.visit(`${Cypress.config().baseUrl}/builder`, { timeout: 5000})
|
cy.visit(`${Cypress.config().baseUrl}/builder`, { timeout: 5000})
|
||||||
cy.get(interact.SPECTRUM_SIDENAV).contains("Users").click()
|
cy.get(interact.SPECTRUM_SIDENAV).contains("Users").click()
|
||||||
cy.get(interact.SPECTRUM_TABLE, { timeout: 1000 }).contains("bbuser").click()
|
cy.get(interact.SPECTRUM_TABLE, { timeout: 1000 }).contains("bbuser").click()
|
||||||
|
cy.get(interact.SPECTRUM_HEADING).contains("bbuser", { timeout: 2000})
|
||||||
for (let i = 0; i < 3; i++) {
|
for (let i = 0; i < 3; i++) {
|
||||||
cy.get(interact.SPECTRUM_TABLE, { timeout: 3000})
|
cy.get(interact.SPECTRUM_TABLE, { timeout: 3000})
|
||||||
.eq(1)
|
.eq(1)
|
||||||
|
@ -95,7 +87,7 @@ filterTests(["smoke", "all"], () => {
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
it("should unassign role types", () => {
|
xit("should unassign role types", () => {
|
||||||
// Set each app within Configure roles table to 'No Access'
|
// Set each app within Configure roles table to 'No Access'
|
||||||
cy.get(interact.SPECTRUM_TABLE)
|
cy.get(interact.SPECTRUM_TABLE)
|
||||||
.eq(0)
|
.eq(0)
|
||||||
|
@ -124,7 +116,7 @@ filterTests(["smoke", "all"], () => {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
it("should enable Developer access and verify application access", () => {
|
xit("should enable Developer access and verify application access", () => {
|
||||||
// Enable Developer access
|
// Enable Developer access
|
||||||
cy.get(interact.FIELD)
|
cy.get(interact.FIELD)
|
||||||
.eq(4)
|
.eq(4)
|
||||||
|
@ -156,7 +148,7 @@ filterTests(["smoke", "all"], () => {
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
it("should disable Developer access and verify application access", () => {
|
xit("should disable Developer access and verify application access", () => {
|
||||||
// Disable Developer access
|
// Disable Developer access
|
||||||
cy.get(interact.FIELD)
|
cy.get(interact.FIELD)
|
||||||
.eq(4)
|
.eq(4)
|
||||||
|
@ -174,12 +166,12 @@ filterTests(["smoke", "all"], () => {
|
||||||
|
|
||||||
it("Should edit user details within user details page", () => {
|
it("Should edit user details within user details page", () => {
|
||||||
// Add First name
|
// Add First name
|
||||||
cy.get(interact.FIELD, { timeout: 1000 }).eq(2).within(() => {
|
cy.get(interact.FIELD, { timeout: 1000 }).eq(0).within(() => {
|
||||||
cy.wait(500)
|
cy.wait(500)
|
||||||
cy.get(interact.SPECTRUM_TEXTFIELD_INPUT, { timeout: 1000 }).wait(500).clear().click().type("bb")
|
cy.get(interact.SPECTRUM_TEXTFIELD_INPUT, { timeout: 1000 }).wait(500).clear().click().type("bb")
|
||||||
})
|
})
|
||||||
// Add Last name
|
// Add Last name
|
||||||
cy.get(interact.FIELD, { timeout: 1000 }).eq(3).within(() => {
|
cy.get(interact.FIELD, { timeout: 1000 }).eq(1).within(() => {
|
||||||
cy.wait(500)
|
cy.wait(500)
|
||||||
cy.get(interact.SPECTRUM_TEXTFIELD_INPUT, { timeout: 1000 }).click().wait(500).clear().type("test")
|
cy.get(interact.SPECTRUM_TEXTFIELD_INPUT, { timeout: 1000 }).click().wait(500).clear().type("test")
|
||||||
})
|
})
|
||||||
|
@ -188,16 +180,21 @@ filterTests(["smoke", "all"], () => {
|
||||||
cy.reload()
|
cy.reload()
|
||||||
|
|
||||||
// Confirm details have been saved
|
// Confirm details have been saved
|
||||||
cy.get(interact.FIELD, { timeout: 1000 }).eq(2).within(() => {
|
cy.get(interact.FIELD, { timeout: 1000 }).eq(0).within(() => {
|
||||||
cy.get(interact.SPECTRUM_TEXTFIELD_INPUT).should('have.value', "bb")
|
cy.get(interact.SPECTRUM_TEXTFIELD_INPUT).should('have.value', "bb")
|
||||||
})
|
})
|
||||||
cy.get(interact.FIELD, { timeout: 1000 }).eq(3).within(() => {
|
cy.get(interact.FIELD, { timeout: 1000 }).eq(1).within(() => {
|
||||||
cy.get(interact.SPECTRUM_TEXTFIELD_INPUT, { timeout: 1000 }).should('have.value', "test")
|
cy.get(interact.SPECTRUM_TEXTFIELD_INPUT, { timeout: 1000 }).should('have.value', "test")
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
it("should reset the users password", () => {
|
it("should reset the users password", () => {
|
||||||
cy.get(interact.REGENERATE, { timeout: 500 }).contains("Force password reset").click({ force: true })
|
cy.get(".title").within(() => {
|
||||||
|
cy.get(interact.SPECTRUM_ICON).click({ force: true })
|
||||||
|
})
|
||||||
|
cy.get(interact.SPECTRUM_MENU).within(() => {
|
||||||
|
cy.get(interact.SPECTRUM_MENU_ITEM).contains("Force Password Reset").click({ force: true })
|
||||||
|
})
|
||||||
|
|
||||||
// Reset password modal
|
// Reset password modal
|
||||||
cy.get(interact.SPECTRUM_DIALOG_GRID)
|
cy.get(interact.SPECTRUM_DIALOG_GRID)
|
||||||
|
|
|
@ -19,10 +19,10 @@ filterTests(["smoke", "all"], () => {
|
||||||
cy.contains("Users").click()
|
cy.contains("Users").click()
|
||||||
cy.contains("test@test.com").click()
|
cy.contains("test@test.com").click()
|
||||||
|
|
||||||
cy.get(interact.FIELD, { timeout: 1000 }).eq(2).within(() => {
|
cy.get(interact.FIELD, { timeout: 1000 }).eq(0).within(() => {
|
||||||
cy.get(interact.SPECTRUM_TEXTFIELD_INPUT).should('have.value', fname)
|
cy.get(interact.SPECTRUM_TEXTFIELD_INPUT).should('have.value', fname)
|
||||||
})
|
})
|
||||||
cy.get(interact.FIELD).eq(3).within(() => {
|
cy.get(interact.FIELD).eq(1).within(() => {
|
||||||
cy.get(interact.SPECTRUM_TEXTFIELD_INPUT).should('have.value', lname)
|
cy.get(interact.SPECTRUM_TEXTFIELD_INPUT).should('have.value', lname)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
|
@ -10,10 +10,8 @@ filterTests(['smoke', 'all'], () => {
|
||||||
|
|
||||||
it("should disable the autogenerated screen options if no sources are available", () => {
|
it("should disable the autogenerated screen options if no sources are available", () => {
|
||||||
cy.createApp("First Test App", false)
|
cy.createApp("First Test App", false)
|
||||||
|
|
||||||
cy.closeModal();
|
cy.closeModal();
|
||||||
|
|
||||||
cy.contains("Design").click()
|
|
||||||
cy.navigateToAutogeneratedModal()
|
cy.navigateToAutogeneratedModal()
|
||||||
cy.get(interact.CONFIRM_WRAP_SPE_BUTTON).should('be.disabled')
|
cy.get(interact.CONFIRM_WRAP_SPE_BUTTON).should('be.disabled')
|
||||||
|
|
||||||
|
|
|
@ -199,15 +199,16 @@ filterTests(["all"], () => {
|
||||||
.within(() => {
|
.within(() => {
|
||||||
cy.get("input").clear().type(queryRename)
|
cy.get("input").clear().type(queryRename)
|
||||||
})
|
})
|
||||||
// Save query
|
// Click on a nav item
|
||||||
cy.get(".spectrum-Button").contains("Save Query").click({ force: true })
|
cy.get(".nav-item").first().click()
|
||||||
|
// Confirm name change
|
||||||
cy.get(".nav-item").should("contain", queryRename)
|
cy.get(".nav-item").should("contain", queryRename)
|
||||||
})
|
})
|
||||||
|
|
||||||
it("should delete a query", () => {
|
it("should delete a query", () => {
|
||||||
// Get query nav item - QueryName
|
// Get query nav item - QueryName
|
||||||
cy.get(".nav-item")
|
cy.get(".nav-item")
|
||||||
.contains(queryName)
|
.contains(queryRename)
|
||||||
.parent()
|
.parent()
|
||||||
.within(() => {
|
.within(() => {
|
||||||
cy.get(".spectrum-Icon").eq(1).click({ force: true })
|
cy.get(".spectrum-Icon").eq(1).click({ force: true })
|
||||||
|
@ -218,7 +219,7 @@ filterTests(["all"], () => {
|
||||||
.contains("Delete Query")
|
.contains("Delete Query")
|
||||||
.click({ force: true })
|
.click({ force: true })
|
||||||
// Confirm deletion
|
// Confirm deletion
|
||||||
cy.get(".nav-item", { timeout: 1000 }).should("not.contain", queryName)
|
cy.get(".nav-item", { timeout: 1000 }).should("not.contain", queryRename)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
|
@ -150,7 +150,9 @@ filterTests(["all"], () => {
|
||||||
cy.get("@query").its("response.statusCode").should("eq", 200)
|
cy.get("@query").its("response.statusCode").should("eq", 200)
|
||||||
cy.get("@query").its("response.body").should("not.be.empty")
|
cy.get("@query").its("response.body").should("not.be.empty")
|
||||||
// Save query
|
// Save query
|
||||||
|
cy.intercept("**/queries").as("saveQuery")
|
||||||
cy.get(".spectrum-Button").contains("Save Query").click({ force: true })
|
cy.get(".spectrum-Button").contains("Save Query").click({ force: true })
|
||||||
|
cy.wait("@saveQuery")
|
||||||
cy.get(".spectrum-Tabs-content", { timeout: 2000 }).should("contain", queryName)
|
cy.get(".spectrum-Tabs-content", { timeout: 2000 }).should("contain", queryName)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -218,7 +220,8 @@ filterTests(["all"], () => {
|
||||||
it("should edit a query name", () => {
|
it("should edit a query name", () => {
|
||||||
// Access query
|
// Access query
|
||||||
cy.get(".hierarchy-items-container", { timeout: 2000 })
|
cy.get(".hierarchy-items-container", { timeout: 2000 })
|
||||||
.contains(queryName + " (1)")
|
//.contains(queryName + " (1)")
|
||||||
|
.contains(queryName)
|
||||||
.click({ force: true })
|
.click({ force: true })
|
||||||
|
|
||||||
// Rename query
|
// Rename query
|
||||||
|
@ -229,18 +232,16 @@ filterTests(["all"], () => {
|
||||||
cy.get("input").clear().type(queryRename)
|
cy.get("input").clear().type(queryRename)
|
||||||
})
|
})
|
||||||
|
|
||||||
// Run and Save query
|
// Click on a nav item and confirm name change
|
||||||
cy.get(".spectrum-Button", { timeout: 2000 }).contains("Run Query").click({ force: true })
|
cy.get(".nav-item").first().click()
|
||||||
cy.wait(1000)
|
// Confirm name change
|
||||||
cy.get(".spectrum-Button", { timeout: 2000 }).contains("Save Query").click({ force: true })
|
cy.get(".nav-item").should("contain", queryRename)
|
||||||
cy.reload({ timeout: 5000 })
|
|
||||||
cy.get(".nav-item", { timeout: 2000 }).should("contain", queryRename)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
it("should delete a query", () => {
|
it("should delete a query", () => {
|
||||||
// Get query nav item - QueryName
|
// Get query nav item - QueryName
|
||||||
cy.get(".nav-item")
|
cy.get(".nav-item")
|
||||||
.contains(queryName)
|
.contains(queryRename)
|
||||||
.parent()
|
.parent()
|
||||||
.within(() => {
|
.within(() => {
|
||||||
cy.get(".spectrum-Icon").eq(1).click({ force: true })
|
cy.get(".spectrum-Icon").eq(1).click({ force: true })
|
||||||
|
@ -252,7 +253,7 @@ filterTests(["all"], () => {
|
||||||
.click({ force: true })
|
.click({ force: true })
|
||||||
// Confirm deletion
|
// Confirm deletion
|
||||||
cy.reload({ timeout: 5000 })
|
cy.reload({ timeout: 5000 })
|
||||||
cy.get(".nav-item", { timeout: 1000 }).should("not.contain", queryName)
|
cy.get(".nav-item", { timeout: 1000 }).should("not.contain", queryRename)
|
||||||
})
|
})
|
||||||
|
|
||||||
const switchSchema = schema => {
|
const switchSchema = schema => {
|
||||||
|
|
|
@ -15,7 +15,7 @@ filterTests(['smoke', 'all'], () => {
|
||||||
})
|
})
|
||||||
cy.get(interact.SPECTRUM_MODAL).within(() => {
|
cy.get(interact.SPECTRUM_MODAL).within(() => {
|
||||||
// Enter app name before revert
|
// Enter app name before revert
|
||||||
cy.get("input").type("Cypress Tests")
|
cy.get(interact.SPECTRUM_TEXTFIELD_INPUT).type("Cypress Tests")
|
||||||
cy.intercept('**/revert').as('revertApp')
|
cy.intercept('**/revert').as('revertApp')
|
||||||
// Click Revert
|
// Click Revert
|
||||||
cy.get(interact.SPECTRUM_BUTTON).contains("Revert").click({ force: true })
|
cy.get(interact.SPECTRUM_BUTTON).contains("Revert").click({ force: true })
|
||||||
|
|
|
@ -5,30 +5,31 @@ Cypress.on("uncaught:exception", () => {
|
||||||
// ACCOUNTS & USERS
|
// ACCOUNTS & USERS
|
||||||
Cypress.Commands.add("login", (email, password) => {
|
Cypress.Commands.add("login", (email, password) => {
|
||||||
cy.visit(`${Cypress.config().baseUrl}/builder`, { timeout: 10000 })
|
cy.visit(`${Cypress.config().baseUrl}/builder`, { timeout: 10000 })
|
||||||
cy.wait(2000)
|
cy.url()
|
||||||
cy.url().then(url => {
|
.should("include", "/builder/")
|
||||||
if (url.includes("builder/admin")) {
|
.then(url => {
|
||||||
// create admin user
|
if (url.includes("builder/admin")) {
|
||||||
cy.get("input").first().type("test@test.com")
|
// create admin user
|
||||||
cy.get('input[type="password"]').first().type("test")
|
cy.get("input").first().type("test@test.com")
|
||||||
cy.get('input[type="password"]').eq(1).type("test")
|
cy.get('input[type="password"]').first().type("test")
|
||||||
cy.contains("Create super admin user").click({ force: true })
|
cy.get('input[type="password"]').eq(1).type("test")
|
||||||
}
|
cy.contains("Create super admin user").click({ force: true })
|
||||||
if (url.includes("builder/auth/login") || url.includes("builder/admin")) {
|
}
|
||||||
// login
|
if (url.includes("builder/auth") || url.includes("builder/admin")) {
|
||||||
cy.contains("Sign in to Budibase").then(() => {
|
// login
|
||||||
if (email == null) {
|
cy.contains("Sign in to Budibase").then(() => {
|
||||||
cy.get("input").first().type("test@test.com")
|
if (email == null) {
|
||||||
cy.get('input[type="password"]').type("test")
|
cy.get("input").first().type("test@test.com")
|
||||||
} else {
|
cy.get('input[type="password"]').type("test")
|
||||||
cy.get("input").first().type(email)
|
} else {
|
||||||
cy.get('input[type="password"]').type(password)
|
cy.get("input").first().type(email)
|
||||||
}
|
cy.get('input[type="password"]').type(password)
|
||||||
cy.get("button").first().click({ force: true })
|
}
|
||||||
cy.wait(1000)
|
cy.get("button").first().click({ force: true })
|
||||||
})
|
cy.wait(1000)
|
||||||
}
|
})
|
||||||
})
|
}
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
Cypress.Commands.add("logOut", () => {
|
Cypress.Commands.add("logOut", () => {
|
||||||
|
@ -50,23 +51,36 @@ Cypress.Commands.add("logoutNoAppGrid", () => {
|
||||||
cy.wait(2000)
|
cy.wait(2000)
|
||||||
})
|
})
|
||||||
|
|
||||||
Cypress.Commands.add("createUser", email => {
|
Cypress.Commands.add("createUser", (email, permission) => {
|
||||||
// quick hacky recorded way to create a user
|
|
||||||
cy.contains("Users").click()
|
cy.contains("Users").click()
|
||||||
cy.get(`[data-cy="add-user"]`).click()
|
cy.get(`[data-cy="add-user"]`).click()
|
||||||
cy.get(".spectrum-Dialog-grid").within(() => {
|
cy.get(".spectrum-Dialog-grid").within(() => {
|
||||||
cy.get(".spectrum-Picker-label").click()
|
// Enter email
|
||||||
cy.get(
|
cy.get(".spectrum-Textfield-input").clear().click().type(email)
|
||||||
".spectrum-Menu-item:nth-child(2) > .spectrum-Menu-itemLabel"
|
|
||||||
).click()
|
|
||||||
|
|
||||||
// Onboarding type selector
|
// Select permission, if applicable
|
||||||
cy.get(".spectrum-Textfield-input")
|
// Default is App User
|
||||||
.eq(0)
|
if (permission != null) {
|
||||||
.first()
|
cy.get(".spectrum-Picker-label").click()
|
||||||
.type(email, { force: true })
|
cy.get(".spectrum-Menu").within(() => {
|
||||||
cy.get(".spectrum-Button--cta").click({ force: true })
|
cy.get(".spectrum-Menu-item")
|
||||||
|
.contains(permission)
|
||||||
|
.click({ force: true })
|
||||||
|
})
|
||||||
|
}
|
||||||
|
// Add user and wait for modal to change
|
||||||
|
cy.get(".spectrum-Button").contains("Add user").click({ force: true })
|
||||||
|
cy.get(".spectrum-ActionButton").contains("Add email").should("not.exist")
|
||||||
})
|
})
|
||||||
|
// Onboarding modal
|
||||||
|
cy.get(".spectrum-Dialog-grid").within(() => {
|
||||||
|
cy.get(".onboarding-type").eq(1).click()
|
||||||
|
cy.get(".spectrum-Button").contains("Done").click({ force: true })
|
||||||
|
cy.get(".spectrum-Button").contains("Cancel").should("not.exist")
|
||||||
|
})
|
||||||
|
|
||||||
|
// Accounts created modal - Click Done button
|
||||||
|
cy.get(".spectrum-Button").contains("Done").click({ force: true })
|
||||||
})
|
})
|
||||||
|
|
||||||
Cypress.Commands.add("deleteUser", email => {
|
Cypress.Commands.add("deleteUser", email => {
|
||||||
|
@ -74,18 +88,13 @@ Cypress.Commands.add("deleteUser", email => {
|
||||||
cy.contains("Users", { timeout: 2000 }).click()
|
cy.contains("Users", { timeout: 2000 }).click()
|
||||||
cy.contains(email).click()
|
cy.contains(email).click()
|
||||||
|
|
||||||
// Click Delete user button
|
cy.get(".title").within(() => {
|
||||||
cy.get(".spectrum-Button")
|
cy.get(".spectrum-Icon").click({ force: true })
|
||||||
.contains("Delete user")
|
})
|
||||||
.click({ force: true })
|
cy.get(".spectrum-Menu").within(() => {
|
||||||
.then(() => {
|
cy.get(".spectrum-Menu-item").contains("Delete").click({ force: true })
|
||||||
// Confirm deletion within modal
|
})
|
||||||
cy.get(".spectrum-Dialog-grid", { timeout: 500 }).within(() => {
|
cy.get(".spectrum-Dialog-grid").contains("Delete user").click({ force: true })
|
||||||
cy.get(".spectrum-Button")
|
|
||||||
.contains("Delete user")
|
|
||||||
.click({ force: true })
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
|
|
||||||
Cypress.Commands.add("updateUserInformation", (firstName, lastName) => {
|
Cypress.Commands.add("updateUserInformation", (firstName, lastName) => {
|
||||||
|
@ -120,9 +129,27 @@ Cypress.Commands.add("updateUserInformation", (firstName, lastName) => {
|
||||||
.blur()
|
.blur()
|
||||||
}
|
}
|
||||||
cy.get("button").contains("Update information").click({ force: true })
|
cy.get("button").contains("Update information").click({ force: true })
|
||||||
|
cy.get(".spectrum-Dialog-grid").should("not.exist")
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
Cypress.Commands.add("setUserRole", (user, role) => {
|
||||||
|
cy.contains("Users").click()
|
||||||
|
cy.contains(user).click()
|
||||||
|
|
||||||
|
// Set Role
|
||||||
|
cy.wait(500)
|
||||||
|
cy.get(".spectrum-Form-itemField")
|
||||||
|
.eq(2)
|
||||||
|
.within(() => {
|
||||||
|
cy.get(".spectrum-Picker-label").click({ force: true })
|
||||||
|
})
|
||||||
|
cy.get(".spectrum-Menu").within(() => {
|
||||||
|
cy.get(".spectrum-Menu-itemLabel").contains(role).click({ force: true })
|
||||||
|
})
|
||||||
|
cy.get(".spectrum-Form-itemField").eq(2).should("contain", role)
|
||||||
|
})
|
||||||
|
|
||||||
// APPLICATIONS
|
// APPLICATIONS
|
||||||
Cypress.Commands.add("createTestApp", () => {
|
Cypress.Commands.add("createTestApp", () => {
|
||||||
const appName = "Cypress Tests"
|
const appName = "Cypress Tests"
|
||||||
|
@ -289,7 +316,7 @@ Cypress.Commands.add("updateAppName", (changedName, noName) => {
|
||||||
})
|
})
|
||||||
|
|
||||||
Cypress.Commands.add("publishApp", resolvedAppPath => {
|
Cypress.Commands.add("publishApp", resolvedAppPath => {
|
||||||
//Assumes you have navigated to an application first
|
// Assumes you have navigated to an application first
|
||||||
cy.get(".toprightnav button.spectrum-Button")
|
cy.get(".toprightnav button.spectrum-Button")
|
||||||
.contains("Publish")
|
.contains("Publish")
|
||||||
.click({ force: true })
|
.click({ force: true })
|
||||||
|
@ -301,7 +328,7 @@ Cypress.Commands.add("publishApp", resolvedAppPath => {
|
||||||
cy.wait(1000)
|
cy.wait(1000)
|
||||||
})
|
})
|
||||||
|
|
||||||
//Verify that the app url is presented correctly to the user
|
// Verify that the app url is presented correctly to the user
|
||||||
cy.get(".spectrum-Modal [data-cy='deploy-app-success-modal']")
|
cy.get(".spectrum-Modal [data-cy='deploy-app-success-modal']")
|
||||||
.should("be.visible")
|
.should("be.visible")
|
||||||
.within(() => {
|
.within(() => {
|
||||||
|
@ -422,7 +449,12 @@ Cypress.Commands.add("createTable", (tableName, initialTable) => {
|
||||||
cy.get("input", { timeout: 2000 }).first().type(tableName).blur()
|
cy.get("input", { timeout: 2000 }).first().type(tableName).blur()
|
||||||
cy.get(".spectrum-ButtonGroup").contains("Create").click()
|
cy.get(".spectrum-ButtonGroup").contains("Create").click()
|
||||||
})
|
})
|
||||||
cy.contains(tableName).should("be.visible")
|
// Ensure modal has closed and table is created
|
||||||
|
cy.get(".spectrum-Modal").should("not.exist")
|
||||||
|
cy.get(".spectrum-Tabs-content", { timeout: 1000 }).should(
|
||||||
|
"contain",
|
||||||
|
tableName
|
||||||
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
Cypress.Commands.add("createTestTableWithData", () => {
|
Cypress.Commands.add("createTestTableWithData", () => {
|
||||||
|
@ -511,14 +543,22 @@ Cypress.Commands.add("addCustomSourceOptions", totalOptions => {
|
||||||
// DESIGN SECTION
|
// DESIGN SECTION
|
||||||
Cypress.Commands.add("searchAndAddComponent", component => {
|
Cypress.Commands.add("searchAndAddComponent", component => {
|
||||||
// Open component menu
|
// Open component menu
|
||||||
cy.get(".spectrum-Button").contains("Component").click({ force: true })
|
cy.get(".icon-side-nav").within(() => {
|
||||||
|
cy.get(".icon-side-nav-item").eq(1).click()
|
||||||
|
})
|
||||||
|
cy.get(".add-component > .spectrum-Button")
|
||||||
|
.contains("Add component")
|
||||||
|
.click({ force: true })
|
||||||
|
cy.get(".container", { timeout: 1000 }).within(() => {
|
||||||
|
cy.get(".title").should("contain", "Add component")
|
||||||
|
|
||||||
// Search and add component
|
// Search and add component
|
||||||
cy.get(".spectrum-Textfield-input").wait(500).clear().type(component)
|
cy.get(".spectrum-Textfield-input").clear().type(component)
|
||||||
cy.get(".body").within(() => {
|
cy.get(".body").within(() => {
|
||||||
cy.get(".component")
|
cy.get(".component")
|
||||||
.contains(new RegExp("^" + component + "$"), { timeout: 3000 })
|
.contains(new RegExp("^" + component + "$"), { timeout: 3000 })
|
||||||
.click({ force: true })
|
.click({ force: true })
|
||||||
|
})
|
||||||
})
|
})
|
||||||
cy.wait(1000)
|
cy.wait(1000)
|
||||||
cy.location().then(loc => {
|
cy.location().then(loc => {
|
||||||
|
@ -564,7 +604,7 @@ Cypress.Commands.add("getComponent", componentId => {
|
||||||
Cypress.Commands.add("createScreen", (route, accessLevelLabel) => {
|
Cypress.Commands.add("createScreen", (route, accessLevelLabel) => {
|
||||||
// Blank Screen
|
// Blank Screen
|
||||||
cy.contains("Design").click()
|
cy.contains("Design").click()
|
||||||
cy.get(".header > .add-button").click()
|
cy.get(".spectrum-Button").contains("Add screen").click({ force: true })
|
||||||
cy.get(".spectrum-Modal").within(() => {
|
cy.get(".spectrum-Modal").within(() => {
|
||||||
cy.get("[data-cy='blank-screen']").click()
|
cy.get("[data-cy='blank-screen']").click()
|
||||||
cy.get(".spectrum-Button").contains("Continue").click({ force: true })
|
cy.get(".spectrum-Button").contains("Continue").click({ force: true })
|
||||||
|
@ -589,7 +629,7 @@ Cypress.Commands.add(
|
||||||
"createDatasourceScreen",
|
"createDatasourceScreen",
|
||||||
(datasourceNames, accessLevelLabel) => {
|
(datasourceNames, accessLevelLabel) => {
|
||||||
cy.contains("Design").click()
|
cy.contains("Design").click()
|
||||||
cy.get(".header > .add-button").click()
|
cy.get(".spectrum-Button").contains("Add screen").click({ force: true })
|
||||||
cy.get(".spectrum-Modal").within(() => {
|
cy.get(".spectrum-Modal").within(() => {
|
||||||
cy.get(".item").contains("Autogenerated screens").click()
|
cy.get(".item").contains("Autogenerated screens").click()
|
||||||
cy.get(".spectrum-Button").contains("Continue").click({ force: true })
|
cy.get(".spectrum-Button").contains("Continue").click({ force: true })
|
||||||
|
@ -709,7 +749,7 @@ Cypress.Commands.add("navigateToDataSection", () => {
|
||||||
Cypress.Commands.add("navigateToAutogeneratedModal", () => {
|
Cypress.Commands.add("navigateToAutogeneratedModal", () => {
|
||||||
// Screen name must already exist within data source
|
// Screen name must already exist within data source
|
||||||
cy.contains("Design").click()
|
cy.contains("Design").click()
|
||||||
cy.get(".header > .add-button").click()
|
cy.get(".spectrum-Button").contains("Add screen").click({ force: true })
|
||||||
cy.get(".spectrum-Modal").within(() => {
|
cy.get(".spectrum-Modal").within(() => {
|
||||||
cy.get(".item", { timeout: 2000 })
|
cy.get(".item", { timeout: 2000 })
|
||||||
.contains("Autogenerated screens")
|
.contains("Autogenerated screens")
|
||||||
|
|
|
@ -108,6 +108,9 @@ export const CONTAINER = ".container"
|
||||||
export const REGENERATE = ".regenerate"
|
export const REGENERATE = ".regenerate"
|
||||||
export const SPECTRUM_DIALOG_CONTENT = ".spectrum-Dialog-content"
|
export const SPECTRUM_DIALOG_CONTENT = ".spectrum-Dialog-content"
|
||||||
export const SPECTRUM_ICON = ".spectrum-Icon"
|
export const SPECTRUM_ICON = ".spectrum-Icon"
|
||||||
|
export const SPECTRUM_HEADING = ".spectrum-Heading"
|
||||||
|
export const SPECTRUM_FORM_ITEMFIELD = ".spectrum-Form-itemField"
|
||||||
|
export const LIST_ITEMS = ".list-items"
|
||||||
|
|
||||||
//createView
|
//createView
|
||||||
export const SPECTRUM_MENU_ITEM_LABEL = ".spectrum-Menu-itemLabel"
|
export const SPECTRUM_MENU_ITEM_LABEL = ".spectrum-Menu-itemLabel"
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@budibase/builder",
|
"name": "@budibase/builder",
|
||||||
"version": "1.1.22-alpha.0",
|
"version": "1.2.20-alpha.1",
|
||||||
"license": "GPL-3.0",
|
"license": "GPL-3.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
@ -69,10 +69,10 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@budibase/bbui": "^1.1.22-alpha.0",
|
"@budibase/bbui": "1.2.20-alpha.1",
|
||||||
"@budibase/client": "^1.1.22-alpha.0",
|
"@budibase/client": "1.2.20-alpha.1",
|
||||||
"@budibase/frontend-core": "^1.1.22-alpha.0",
|
"@budibase/frontend-core": "1.2.20-alpha.1",
|
||||||
"@budibase/string-templates": "^1.1.22-alpha.0",
|
"@budibase/string-templates": "1.2.20-alpha.1",
|
||||||
"@sentry/browser": "5.19.1",
|
"@sentry/browser": "5.19.1",
|
||||||
"@spectrum-css/page": "^3.0.1",
|
"@spectrum-css/page": "^3.0.1",
|
||||||
"@spectrum-css/vars": "^3.0.1",
|
"@spectrum-css/vars": "^3.0.1",
|
||||||
|
@ -113,7 +113,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.49.0",
|
"svelte": "^3.48.0",
|
||||||
"svelte-jester": "^1.3.2",
|
"svelte-jester": "^1.3.2",
|
||||||
"ts-node": "^10.4.0",
|
"ts-node": "^10.4.0",
|
||||||
"tsconfig-paths": "4.0.0",
|
"tsconfig-paths": "4.0.0",
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
import posthog from "posthog-js"
|
import posthog from "posthog-js"
|
||||||
import { Events } from "./constants"
|
import { Events } from "./constants"
|
||||||
|
import { get } from "svelte/store"
|
||||||
|
import { admin } from "../stores/portal"
|
||||||
|
|
||||||
export default class PosthogClient {
|
export default class PosthogClient {
|
||||||
constructor(token) {
|
constructor(token) {
|
||||||
|
@ -9,9 +11,15 @@ export default class PosthogClient {
|
||||||
init() {
|
init() {
|
||||||
if (!this.token) return
|
if (!this.token) return
|
||||||
|
|
||||||
|
// enable page views in cloud only
|
||||||
|
let capturePageViews = false
|
||||||
|
if (get(admin).cloud) {
|
||||||
|
capturePageViews = true
|
||||||
|
}
|
||||||
|
|
||||||
posthog.init(this.token, {
|
posthog.init(this.token, {
|
||||||
autocapture: false,
|
autocapture: false,
|
||||||
capture_pageview: true,
|
capture_pageview: capturePageViews,
|
||||||
})
|
})
|
||||||
posthog.set_config({ persistence: "cookie" })
|
posthog.set_config({ persistence: "cookie" })
|
||||||
|
|
||||||
|
|
|
@ -1,11 +1,10 @@
|
||||||
import { createLocalStorageStore } from "@budibase/frontend-core"
|
import { Constants, createLocalStorageStore } from "@budibase/frontend-core"
|
||||||
|
|
||||||
export const getThemeStore = () => {
|
export const getThemeStore = () => {
|
||||||
const themeElement = document.documentElement
|
const themeElement = document.documentElement
|
||||||
|
|
||||||
const initialValue = {
|
const initialValue = {
|
||||||
theme: "darkest",
|
theme: "darkest",
|
||||||
options: ["lightest", "light", "dark", "darkest", "nord"],
|
|
||||||
}
|
}
|
||||||
const store = createLocalStorageStore("bb-theme", initialValue)
|
const store = createLocalStorageStore("bb-theme", initialValue)
|
||||||
|
|
||||||
|
@ -17,13 +16,19 @@ export const getThemeStore = () => {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
state.options.forEach(option => {
|
// Update global class names to use the new theme and remove others
|
||||||
|
Constants.Themes.forEach(option => {
|
||||||
themeElement.classList.toggle(
|
themeElement.classList.toggle(
|
||||||
`spectrum--${option}`,
|
`spectrum--${option.class}`,
|
||||||
option === state.theme
|
option.class === state.theme
|
||||||
)
|
)
|
||||||
themeElement.classList.add("spectrum--darkest")
|
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Add base theme if required
|
||||||
|
const selectedTheme = Constants.Themes.find(x => x.class === state.theme)
|
||||||
|
if (selectedTheme?.base) {
|
||||||
|
themeElement.classList.add(`spectrum--${selectedTheme.base}`)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
return store
|
return store
|
||||||
|
|
|
@ -52,8 +52,9 @@
|
||||||
x => x.blockToLoop === block.id
|
x => x.blockToLoop === block.id
|
||||||
)
|
)
|
||||||
|
|
||||||
$: setPermissions(role)
|
$: isAppAction = block?.stepId === TriggerStepID.APP
|
||||||
$: getPermissions(automationId)
|
$: isAppAction && setPermissions(role)
|
||||||
|
$: isAppAction && getPermissions(automationId)
|
||||||
|
|
||||||
async function setPermissions(role) {
|
async function setPermissions(role) {
|
||||||
if (!role || !automationId) {
|
if (!role || !automationId) {
|
||||||
|
@ -238,7 +239,7 @@
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if block.stepId === TriggerStepID.APP}
|
{#if isAppAction}
|
||||||
<Label>Role</Label>
|
<Label>Role</Label>
|
||||||
<RoleSelect bind:value={role} />
|
<RoleSelect bind:value={role} />
|
||||||
{/if}
|
{/if}
|
||||||
|
|
|
@ -32,7 +32,8 @@
|
||||||
if (!results) {
|
if (!results) {
|
||||||
return {}
|
return {}
|
||||||
}
|
}
|
||||||
if (results.outputs?.status?.toLowerCase() === "stopped") {
|
const lcStatus = results.outputs?.status?.toLowerCase()
|
||||||
|
if (lcStatus === "stopped" || lcStatus === "stopped_error") {
|
||||||
return { yellow: true, message: "Stopped" }
|
return { yellow: true, message: "Stopped" }
|
||||||
} else if (results.outputs?.success || isTrigger) {
|
} else if (results.outputs?.success || isTrigger) {
|
||||||
return { positive: true, message: "Success" }
|
return { positive: true, message: "Success" }
|
||||||
|
|
|
@ -15,16 +15,20 @@
|
||||||
let trigger = {}
|
let trigger = {}
|
||||||
let schemaProperties = {}
|
let schemaProperties = {}
|
||||||
|
|
||||||
// clone the trigger so we're not mutating the reference
|
$: {
|
||||||
$: trigger = cloneDeep(
|
// clone the trigger so we're not mutating the reference
|
||||||
$automationStore.selectedAutomation.automation.definition.trigger
|
trigger = cloneDeep(
|
||||||
)
|
$automationStore.selectedAutomation.automation.definition.trigger
|
||||||
|
)
|
||||||
|
|
||||||
// get the outputs so we can define the fields
|
// get the outputs so we can define the fields
|
||||||
$: schemaProperties = Object.entries(trigger?.schema?.outputs?.properties)
|
let schema = Object.entries(trigger.schema?.outputs?.properties || {})
|
||||||
|
|
||||||
if (!$automationStore.selectedAutomation.automation.testData) {
|
if (trigger?.event === "app:trigger") {
|
||||||
$automationStore.selectedAutomation.automation.testData = {}
|
schema = [["fields", { customType: "fields" }]]
|
||||||
|
}
|
||||||
|
|
||||||
|
schemaProperties = schema
|
||||||
}
|
}
|
||||||
|
|
||||||
// check to see if there is existing test data in the store
|
// check to see if there is existing test data in the store
|
||||||
|
|
|
@ -5,9 +5,8 @@
|
||||||
import { ActionStepID } from "constants/backend/automations"
|
import { ActionStepID } from "constants/backend/automations"
|
||||||
|
|
||||||
export let automation
|
export let automation
|
||||||
export let testResults
|
|
||||||
|
|
||||||
let blocks
|
let blocks, testResults
|
||||||
|
|
||||||
$: {
|
$: {
|
||||||
blocks = []
|
blocks = []
|
||||||
|
@ -18,15 +17,11 @@
|
||||||
blocks = blocks
|
blocks = blocks
|
||||||
.concat(automation.definition.steps || [])
|
.concat(automation.definition.steps || [])
|
||||||
.filter(x => x.stepId !== ActionStepID.LOOP)
|
.filter(x => x.stepId !== ActionStepID.LOOP)
|
||||||
} else if (testResults) {
|
} else if ($automationStore.selectedAutomation) {
|
||||||
blocks = testResults.steps || []
|
automation = $automationStore.selectedAutomation
|
||||||
}
|
|
||||||
}
|
|
||||||
$: {
|
|
||||||
if (!testResults) {
|
|
||||||
testResults = $automationStore.selectedAutomation?.testResults
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
$: testResults = $automationStore.selectedAutomation?.testResults
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="title">
|
<div class="title">
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
<script>
|
<script>
|
||||||
import TableSelector from "./TableSelector.svelte"
|
import TableSelector from "./TableSelector.svelte"
|
||||||
import RowSelector from "./RowSelector.svelte"
|
import RowSelector from "./RowSelector.svelte"
|
||||||
|
import FieldSelector from "./FieldSelector.svelte"
|
||||||
import SchemaSetup from "./SchemaSetup.svelte"
|
import SchemaSetup from "./SchemaSetup.svelte"
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
|
@ -31,6 +32,7 @@
|
||||||
import { getSchemaForTable } from "builderStore/dataBinding"
|
import { getSchemaForTable } from "builderStore/dataBinding"
|
||||||
import { Utils } from "@budibase/frontend-core"
|
import { Utils } from "@budibase/frontend-core"
|
||||||
import { TriggerStepID, ActionStepID } from "constants/backend/automations"
|
import { TriggerStepID, ActionStepID } from "constants/backend/automations"
|
||||||
|
import { cloneDeep } from "lodash/fp"
|
||||||
|
|
||||||
export let block
|
export let block
|
||||||
export let testData
|
export let testData
|
||||||
|
@ -41,13 +43,25 @@
|
||||||
let tempFilters = lookForFilters(schemaProperties) || []
|
let tempFilters = lookForFilters(schemaProperties) || []
|
||||||
let fillWidth = true
|
let fillWidth = true
|
||||||
let codeBindingOpen = false
|
let codeBindingOpen = false
|
||||||
|
let inputData
|
||||||
|
|
||||||
$: stepId = block.stepId
|
$: stepId = block.stepId
|
||||||
$: bindings = getAvailableBindings(
|
$: bindings = getAvailableBindings(
|
||||||
block || $automationStore.selectedBlock,
|
block || $automationStore.selectedBlock,
|
||||||
$automationStore.selectedAutomation?.automation?.definition
|
$automationStore.selectedAutomation?.automation?.definition
|
||||||
)
|
)
|
||||||
$: inputData = testData ? testData : block.inputs
|
|
||||||
|
$: getInputData(testData, block.inputs)
|
||||||
|
const getInputData = (testData, blockInputs) => {
|
||||||
|
let newInputData = testData || blockInputs
|
||||||
|
|
||||||
|
if (block.event === "app:trigger" && !newInputData?.fields) {
|
||||||
|
newInputData = cloneDeep(blockInputs)
|
||||||
|
}
|
||||||
|
|
||||||
|
inputData = newInputData
|
||||||
|
}
|
||||||
|
|
||||||
$: tableId = inputData ? inputData.tableId : null
|
$: tableId = inputData ? inputData.tableId : null
|
||||||
$: table = tableId
|
$: table = tableId
|
||||||
? $tables.list.find(table => table._id === inputData.tableId)
|
? $tables.list.find(table => table._id === inputData.tableId)
|
||||||
|
@ -73,15 +87,13 @@
|
||||||
[key]: e.detail,
|
[key]: e.detail,
|
||||||
})
|
})
|
||||||
testData[key] = e.detail
|
testData[key] = e.detail
|
||||||
await automationStore.actions.save(
|
|
||||||
$automationStore.selectedAutomation?.automation
|
|
||||||
)
|
|
||||||
} else {
|
} else {
|
||||||
block.inputs[key] = e.detail
|
block.inputs[key] = e.detail
|
||||||
await automationStore.actions.save(
|
|
||||||
$automationStore.selectedAutomation?.automation
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await automationStore.actions.save(
|
||||||
|
$automationStore.selectedAutomation?.automation
|
||||||
|
)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
notifications.error("Error saving automation")
|
notifications.error("Error saving automation")
|
||||||
}
|
}
|
||||||
|
@ -185,11 +197,13 @@
|
||||||
<div class="fields">
|
<div class="fields">
|
||||||
{#each schemaProperties as [key, value]}
|
{#each schemaProperties as [key, value]}
|
||||||
<div class="block-field">
|
<div class="block-field">
|
||||||
<Label
|
{#if key !== "fields"}
|
||||||
tooltip={value.title === "Binding / Value"
|
<Label
|
||||||
? "If using the String input type, please use a comma or newline separated string"
|
tooltip={value.title === "Binding / Value"
|
||||||
: null}>{value.title || (key === "row" ? "Table" : key)}</Label
|
? "If using the String input type, please use a comma or newline separated string"
|
||||||
>
|
: null}>{value.title || (key === "row" ? "Table" : key)}</Label
|
||||||
|
>
|
||||||
|
{/if}
|
||||||
{#if value.type === "string" && value.enum}
|
{#if value.type === "string" && value.enum}
|
||||||
<Select
|
<Select
|
||||||
on:change={e => onChange(e, key)}
|
on:change={e => onChange(e, key)}
|
||||||
|
@ -281,6 +295,14 @@
|
||||||
on:change={e => onChange(e, key)}
|
on:change={e => onChange(e, key)}
|
||||||
value={inputData[key]}
|
value={inputData[key]}
|
||||||
/>
|
/>
|
||||||
|
{:else if value.customType === "fields"}
|
||||||
|
<FieldSelector
|
||||||
|
{block}
|
||||||
|
value={inputData[key]}
|
||||||
|
on:change={e => onChange(e, key)}
|
||||||
|
{bindings}
|
||||||
|
{isTestModal}
|
||||||
|
/>
|
||||||
{:else if value.customType === "triggerSchema"}
|
{:else if value.customType === "triggerSchema"}
|
||||||
<SchemaSetup on:change={e => onChange(e, key)} value={inputData[key]} />
|
<SchemaSetup on:change={e => onChange(e, key)} value={inputData[key]} />
|
||||||
{:else if value.customType === "code"}
|
{:else if value.customType === "code"}
|
||||||
|
|
|
@ -0,0 +1,114 @@
|
||||||
|
<script>
|
||||||
|
import { createEventDispatcher } from "svelte"
|
||||||
|
import RowSelectorTypes from "./RowSelectorTypes.svelte"
|
||||||
|
|
||||||
|
const dispatch = createEventDispatcher()
|
||||||
|
|
||||||
|
export let value
|
||||||
|
export let bindings
|
||||||
|
export let block
|
||||||
|
export let isTestModal
|
||||||
|
|
||||||
|
let schemaFields
|
||||||
|
|
||||||
|
$: {
|
||||||
|
let fields = {}
|
||||||
|
|
||||||
|
for (const [key, type] of Object.entries(block?.inputs?.fields)) {
|
||||||
|
fields = {
|
||||||
|
...fields,
|
||||||
|
[key]: {
|
||||||
|
type: type,
|
||||||
|
name: key,
|
||||||
|
fieldName: key,
|
||||||
|
constraints: { type: type },
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
if (value[key] === type) {
|
||||||
|
value[key] = INITIAL_VALUES[type.toUpperCase()]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
schemaFields = Object.entries(fields)
|
||||||
|
}
|
||||||
|
|
||||||
|
const INITIAL_VALUES = {
|
||||||
|
BOOLEAN: null,
|
||||||
|
NUMBER: null,
|
||||||
|
DATETIME: null,
|
||||||
|
STRING: "",
|
||||||
|
OPTIONS: [],
|
||||||
|
ARRAY: [],
|
||||||
|
}
|
||||||
|
|
||||||
|
const coerce = (value, type) => {
|
||||||
|
const re = new RegExp(/{{([^{].*?)}}/g)
|
||||||
|
if (re.test(value)) {
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
||||||
|
if (type === "boolean") {
|
||||||
|
if (typeof value === "boolean") {
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
return value === "true"
|
||||||
|
}
|
||||||
|
if (type === "number") {
|
||||||
|
if (typeof value === "number") {
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
return Number(value)
|
||||||
|
}
|
||||||
|
if (type === "options") {
|
||||||
|
return [value]
|
||||||
|
}
|
||||||
|
if (type === "array") {
|
||||||
|
if (Array.isArray(value)) {
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
return value.split(",").map(x => x.trim())
|
||||||
|
}
|
||||||
|
|
||||||
|
if (type === "link") {
|
||||||
|
if (Array.isArray(value)) {
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
||||||
|
return [value]
|
||||||
|
}
|
||||||
|
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
||||||
|
const onChange = (e, field, type) => {
|
||||||
|
value[field] = coerce(e.detail, type)
|
||||||
|
dispatch("change", value)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if schemaFields.length && isTestModal}
|
||||||
|
<div class="schema-fields">
|
||||||
|
{#each schemaFields as [field, schema]}
|
||||||
|
<RowSelectorTypes
|
||||||
|
{isTestModal}
|
||||||
|
{field}
|
||||||
|
{schema}
|
||||||
|
{bindings}
|
||||||
|
{value}
|
||||||
|
{onChange}
|
||||||
|
/>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.schema-fields {
|
||||||
|
display: grid;
|
||||||
|
grid-gap: var(--spacing-s);
|
||||||
|
margin-top: var(--spacing-s);
|
||||||
|
}
|
||||||
|
.schema-fields :global(label) {
|
||||||
|
text-transform: capitalize;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -14,7 +14,13 @@
|
||||||
import Table from "./Table.svelte"
|
import Table from "./Table.svelte"
|
||||||
import { TableNames } from "constants"
|
import { TableNames } from "constants"
|
||||||
import CreateEditRow from "./modals/CreateEditRow.svelte"
|
import CreateEditRow from "./modals/CreateEditRow.svelte"
|
||||||
import { Pagination, Heading, Body, Layout } from "@budibase/bbui"
|
import {
|
||||||
|
Pagination,
|
||||||
|
Heading,
|
||||||
|
Body,
|
||||||
|
Layout,
|
||||||
|
notifications,
|
||||||
|
} from "@budibase/bbui"
|
||||||
import { fetchData } from "@budibase/frontend-core"
|
import { fetchData } from "@budibase/frontend-core"
|
||||||
import { API } from "api"
|
import { API } from "api"
|
||||||
|
|
||||||
|
@ -29,6 +35,13 @@
|
||||||
$: fetch = createFetch(id)
|
$: fetch = createFetch(id)
|
||||||
$: hasCols = checkHasCols(schema)
|
$: hasCols = checkHasCols(schema)
|
||||||
$: hasRows = !!$fetch.rows?.length
|
$: hasRows = !!$fetch.rows?.length
|
||||||
|
$: showError($fetch.error)
|
||||||
|
|
||||||
|
const showError = error => {
|
||||||
|
if (error) {
|
||||||
|
notifications.error(error?.message || "Unable to fetch data.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const enrichSchema = schema => {
|
const enrichSchema = schema => {
|
||||||
let tempSchema = { ...schema }
|
let tempSchema = { ...schema }
|
||||||
|
|
|
@ -5,6 +5,7 @@
|
||||||
|
|
||||||
export let selectedRows
|
export let selectedRows
|
||||||
export let deleteRows
|
export let deleteRows
|
||||||
|
export let item = "row"
|
||||||
|
|
||||||
const dispatch = createEventDispatcher()
|
const dispatch = createEventDispatcher()
|
||||||
let modal
|
let modal
|
||||||
|
@ -14,12 +15,14 @@
|
||||||
modal?.hide()
|
modal?.hide()
|
||||||
dispatch("updaterows")
|
dispatch("updaterows")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$: text = `${item}${selectedRows?.length === 1 ? "" : "s"}`
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Button icon="Delete" size="s" primary quiet on:click={modal.show}>
|
<Button icon="Delete" size="s" primary quiet on:click={modal.show}>
|
||||||
Delete
|
Delete
|
||||||
{selectedRows.length}
|
{selectedRows.length}
|
||||||
row(s)
|
{text}
|
||||||
</Button>
|
</Button>
|
||||||
<ConfirmDialog
|
<ConfirmDialog
|
||||||
bind:this={modal}
|
bind:this={modal}
|
||||||
|
@ -29,5 +32,5 @@
|
||||||
>
|
>
|
||||||
Are you sure you want to delete
|
Are you sure you want to delete
|
||||||
{selectedRows.length}
|
{selectedRows.length}
|
||||||
row{selectedRows.length > 1 ? "s" : ""}?
|
{text}?
|
||||||
</ConfirmDialog>
|
</ConfirmDialog>
|
||||||
|
|
|
@ -211,7 +211,6 @@
|
||||||
bindings={getAuthBindings()}
|
bindings={getAuthBindings()}
|
||||||
on:change={e => {
|
on:change={e => {
|
||||||
form.bearer.token = e.detail
|
form.bearer.token = e.detail
|
||||||
console.log(e.detail)
|
|
||||||
onFieldChange()
|
onFieldChange()
|
||||||
}}
|
}}
|
||||||
on:blur={() => {
|
on:blur={() => {
|
||||||
|
|
|
@ -6,6 +6,8 @@
|
||||||
Modal,
|
Modal,
|
||||||
notifications,
|
notifications,
|
||||||
ProgressCircle,
|
ProgressCircle,
|
||||||
|
Layout,
|
||||||
|
Body,
|
||||||
} from "@budibase/bbui"
|
} from "@budibase/bbui"
|
||||||
import { auth, apps } from "stores/portal"
|
import { auth, apps } from "stores/portal"
|
||||||
import { processStringSync } from "@budibase/string-templates"
|
import { processStringSync } from "@budibase/string-templates"
|
||||||
|
@ -72,62 +74,67 @@
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Modal bind:this={appLockModal}>
|
{#key app}
|
||||||
<ModalContent
|
<div>
|
||||||
title={lockedByHeading}
|
<Modal bind:this={appLockModal}>
|
||||||
dataCy={"app-lock-modal"}
|
<ModalContent
|
||||||
showConfirmButton={false}
|
title={lockedByHeading}
|
||||||
showCancelButton={false}
|
dataCy={"app-lock-modal"}
|
||||||
>
|
showConfirmButton={false}
|
||||||
<p>
|
showCancelButton={false}
|
||||||
Apps are locked to prevent work from being lost from overlapping changes
|
>
|
||||||
between your team.
|
<Layout noPadding>
|
||||||
</p>
|
<Body size="S">
|
||||||
|
Apps are locked to prevent work from being lost from overlapping
|
||||||
{#if lockedByYou && getExpiryDuration(app) > 0}
|
changes between your team.
|
||||||
<span class="lock-expiry-body">
|
</Body>
|
||||||
{processStringSync(
|
{#if lockedByYou && getExpiryDuration(app) > 0}
|
||||||
"This lock will expire in {{ duration time 'millisecond' }} from now.",
|
<span class="lock-expiry-body">
|
||||||
{
|
{processStringSync(
|
||||||
time: getExpiryDuration(app),
|
"This lock will expire in {{ duration time 'millisecond' }} from now. This lock will expire in This lock will expire in ",
|
||||||
}
|
{
|
||||||
)}
|
time: getExpiryDuration(app),
|
||||||
</span>
|
}
|
||||||
{/if}
|
)}
|
||||||
<div class="lock-modal-actions">
|
</span>
|
||||||
<ButtonGroup>
|
{/if}
|
||||||
<Button
|
<div class="lock-modal-actions">
|
||||||
secondary
|
<ButtonGroup>
|
||||||
quiet={lockedBy && lockedByYou}
|
<Button
|
||||||
disabled={processing}
|
secondary
|
||||||
on:click={() => {
|
quiet={lockedBy && lockedByYou}
|
||||||
appLockModal.hide()
|
disabled={processing}
|
||||||
}}
|
on:click={() => {
|
||||||
>
|
appLockModal.hide()
|
||||||
<span class="cancel"
|
}}
|
||||||
>{lockedBy && !lockedByYou ? "Done" : "Cancel"}</span
|
>
|
||||||
>
|
<span class="cancel"
|
||||||
</Button>
|
>{lockedBy && !lockedByYou ? "Done" : "Cancel"}</span
|
||||||
{#if lockedByYou}
|
>
|
||||||
<Button
|
</Button>
|
||||||
secondary
|
{#if lockedByYou}
|
||||||
disabled={processing}
|
<Button
|
||||||
on:click={() => {
|
secondary
|
||||||
releaseLock()
|
disabled={processing}
|
||||||
appLockModal.hide()
|
on:click={() => {
|
||||||
}}
|
releaseLock()
|
||||||
>
|
appLockModal.hide()
|
||||||
{#if processing}
|
}}
|
||||||
<ProgressCircle overBackground={true} size="S" />
|
>
|
||||||
{:else}
|
{#if processing}
|
||||||
<span class="unlock">Release Lock</span>
|
<ProgressCircle overBackground={true} size="S" />
|
||||||
{/if}
|
{:else}
|
||||||
</Button>
|
<span class="unlock">Release Lock</span>
|
||||||
{/if}
|
{/if}
|
||||||
</ButtonGroup>
|
</Button>
|
||||||
</div>
|
{/if}
|
||||||
</ModalContent>
|
</ButtonGroup>
|
||||||
</Modal>
|
</div>
|
||||||
|
</Layout>
|
||||||
|
</ModalContent>
|
||||||
|
</Modal>
|
||||||
|
</div>
|
||||||
|
{/key}
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.lock-modal-actions {
|
.lock-modal-actions {
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
<script>
|
<script>
|
||||||
import { Icon, StatusLight } from "@budibase/bbui"
|
import { Icon } from "@budibase/bbui"
|
||||||
import { createEventDispatcher, getContext } from "svelte"
|
import { createEventDispatcher, getContext } from "svelte"
|
||||||
|
|
||||||
export let icon
|
export let icon
|
||||||
|
@ -14,8 +14,8 @@
|
||||||
export let iconText
|
export let iconText
|
||||||
export let iconColor
|
export let iconColor
|
||||||
export let scrollable = false
|
export let scrollable = false
|
||||||
export let color
|
|
||||||
export let highlighted = false
|
export let highlighted = false
|
||||||
|
export let rightAlignIcon = false
|
||||||
|
|
||||||
const scrollApi = getContext("scroll")
|
const scrollApi = getContext("scroll")
|
||||||
const dispatch = createEventDispatcher()
|
const dispatch = createEventDispatcher()
|
||||||
|
@ -78,7 +78,7 @@
|
||||||
{iconText}
|
{iconText}
|
||||||
</div>
|
</div>
|
||||||
{:else if icon}
|
{:else if icon}
|
||||||
<div class="icon">
|
<div class="icon" class:right={rightAlignIcon}>
|
||||||
<Icon color={iconColor} size="S" name={icon} />
|
<Icon color={iconColor} size="S" name={icon} />
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
@ -88,9 +88,9 @@
|
||||||
<slot />
|
<slot />
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
{#if color}
|
{#if $$slots.right}
|
||||||
<div class="light">
|
<div class="right">
|
||||||
<StatusLight size="L" {color} />
|
<slot name="right" />
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
@ -107,7 +107,7 @@
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
justify-content: flex-start;
|
justify-content: flex-start;
|
||||||
align-items: center;
|
align-items: stretch;
|
||||||
}
|
}
|
||||||
.nav-item.scrollable {
|
.nav-item.scrollable {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
@ -135,10 +135,8 @@
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: var(--spacing-xs);
|
gap: var(--spacing-xs);
|
||||||
width: max-content;
|
width: max-content;
|
||||||
overflow: hidden;
|
|
||||||
position: relative;
|
position: relative;
|
||||||
padding-left: var(--spacing-l);
|
padding-left: var(--spacing-l);
|
||||||
pointer-events: none;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Needed to fully display the actions icon */
|
/* Needed to fully display the actions icon */
|
||||||
|
@ -153,10 +151,15 @@
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
color: var(--spectrum-global-color-gray-600);
|
color: var(--spectrum-global-color-gray-600);
|
||||||
|
order: 1;
|
||||||
|
}
|
||||||
|
.icon.right {
|
||||||
|
order: 4;
|
||||||
}
|
}
|
||||||
.icon.arrow {
|
.icon.arrow {
|
||||||
flex: 0 0 20px;
|
flex: 0 0 20px;
|
||||||
pointer-events: all;
|
pointer-events: all;
|
||||||
|
order: 0;
|
||||||
}
|
}
|
||||||
.icon.arrow.absolute {
|
.icon.arrow.absolute {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
|
@ -188,11 +191,14 @@
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
flex: 1 1 auto;
|
flex: 1 1 auto;
|
||||||
color: var(--spectrum-global-color-gray-800);
|
color: var(--spectrum-global-color-gray-900);
|
||||||
|
order: 2;
|
||||||
|
width: 0;
|
||||||
}
|
}
|
||||||
.scrollable .text {
|
.scrollable .text {
|
||||||
flex: 0 0 auto;
|
flex: 0 0 auto;
|
||||||
max-width: 160px;
|
max-width: 160px;
|
||||||
|
width: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.actions {
|
.actions {
|
||||||
|
@ -201,18 +207,17 @@
|
||||||
display: grid;
|
display: grid;
|
||||||
place-items: center;
|
place-items: center;
|
||||||
visibility: hidden;
|
visibility: hidden;
|
||||||
}
|
order: 3;
|
||||||
.actions,
|
opacity: 0;
|
||||||
.light :global(.spectrum-StatusLight) {
|
|
||||||
width: 20px;
|
width: 20px;
|
||||||
height: 20px;
|
height: 20px;
|
||||||
margin-left: var(--spacing-s);
|
margin-left: var(--spacing-xs);
|
||||||
}
|
}
|
||||||
.light {
|
.nav-item.withActions:hover .actions {
|
||||||
position: absolute;
|
opacity: 1;
|
||||||
right: 0;
|
|
||||||
}
|
}
|
||||||
.nav-item.withActions:hover .light {
|
|
||||||
display: none;
|
.right {
|
||||||
|
order: 10;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -0,0 +1,24 @@
|
||||||
|
<script>
|
||||||
|
import { Select } from "@budibase/bbui"
|
||||||
|
import { roles } from "stores/backend"
|
||||||
|
import { RoleUtils } from "@budibase/frontend-core"
|
||||||
|
|
||||||
|
export let value
|
||||||
|
export let error
|
||||||
|
export let placeholder = null
|
||||||
|
export let autoWidth = false
|
||||||
|
export let quiet = false
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Select
|
||||||
|
{autoWidth}
|
||||||
|
{quiet}
|
||||||
|
bind:value
|
||||||
|
on:change
|
||||||
|
options={$roles}
|
||||||
|
getOptionLabel={role => role.name}
|
||||||
|
getOptionValue={role => role._id}
|
||||||
|
getOptionColour={role => RoleUtils.getRoleColour(role._id)}
|
||||||
|
{placeholder}
|
||||||
|
{error}
|
||||||
|
/>
|
|
@ -8,6 +8,7 @@
|
||||||
Tab,
|
Tab,
|
||||||
Body,
|
Body,
|
||||||
Layout,
|
Layout,
|
||||||
|
Button,
|
||||||
} from "@budibase/bbui"
|
} from "@budibase/bbui"
|
||||||
import { createEventDispatcher, onMount } from "svelte"
|
import { createEventDispatcher, onMount } from "svelte"
|
||||||
import {
|
import {
|
||||||
|
@ -15,10 +16,15 @@
|
||||||
decodeJSBinding,
|
decodeJSBinding,
|
||||||
encodeJSBinding,
|
encodeJSBinding,
|
||||||
} from "@budibase/string-templates"
|
} from "@budibase/string-templates"
|
||||||
import { readableToRuntimeBinding } from "builderStore/dataBinding"
|
import {
|
||||||
|
readableToRuntimeBinding,
|
||||||
|
runtimeToReadableBinding,
|
||||||
|
} from "builderStore/dataBinding"
|
||||||
import { handlebarsCompletions } from "constants/completions"
|
import { handlebarsCompletions } from "constants/completions"
|
||||||
import { addHBSBinding, addJSBinding } from "./utils"
|
import { addHBSBinding, addJSBinding } from "./utils"
|
||||||
import CodeMirrorEditor from "components/common/CodeMirrorEditor.svelte"
|
import CodeMirrorEditor from "components/common/CodeMirrorEditor.svelte"
|
||||||
|
import { convertToJS } from "@budibase/string-templates"
|
||||||
|
import { admin } from "stores/portal"
|
||||||
|
|
||||||
const dispatch = createEventDispatcher()
|
const dispatch = createEventDispatcher()
|
||||||
|
|
||||||
|
@ -62,15 +68,24 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Adds a HBS helper to the expression
|
// Adds a JS/HBS helper to the expression
|
||||||
const addHelper = helper => {
|
const addHelper = (helper, js) => {
|
||||||
hbsValue = addHBSBinding(hbsValue, getCaretPosition(), helper.text)
|
let tempVal
|
||||||
updateValue(hbsValue)
|
const pos = getCaretPosition()
|
||||||
|
if (js) {
|
||||||
|
const decoded = decodeJSBinding(jsValue)
|
||||||
|
tempVal = jsValue = encodeJSBinding(
|
||||||
|
addJSBinding(decoded, pos, helper.text, { helper: true })
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
tempVal = hbsValue = addHBSBinding(hbsValue, pos, helper.text)
|
||||||
|
}
|
||||||
|
updateValue(tempVal)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Adds a data binding to the expression
|
// Adds a data binding to the expression
|
||||||
const addBinding = binding => {
|
const addBinding = (binding, { forceJS } = {}) => {
|
||||||
if (usingJS) {
|
if (usingJS || forceJS) {
|
||||||
let js = decodeJSBinding(jsValue)
|
let js = decodeJSBinding(jsValue)
|
||||||
js = addJSBinding(js, getCaretPosition(), binding.readableBinding)
|
js = addJSBinding(js, getCaretPosition(), binding.readableBinding)
|
||||||
jsValue = encodeJSBinding(js)
|
jsValue = encodeJSBinding(js)
|
||||||
|
@ -100,6 +115,26 @@
|
||||||
updateValue(jsValue)
|
updateValue(jsValue)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const convert = () => {
|
||||||
|
const runtime = readableToRuntimeBinding(bindings, hbsValue)
|
||||||
|
const runtimeJs = encodeJSBinding(convertToJS(runtime))
|
||||||
|
jsValue = runtimeToReadableBinding(bindings, runtimeJs)
|
||||||
|
hbsValue = null
|
||||||
|
mode = "JavaScript"
|
||||||
|
addBinding("", { forceJS: true })
|
||||||
|
}
|
||||||
|
|
||||||
|
const getHelperExample = (helper, js) => {
|
||||||
|
let example = helper.example || ""
|
||||||
|
if (js) {
|
||||||
|
example = convertToJS(example).split("\n")[0].split("= ")[1]
|
||||||
|
if (example === "null;") {
|
||||||
|
example = ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return example || ""
|
||||||
|
}
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
valid = isValid(readableToRuntimeBinding(bindings, value))
|
valid = isValid(readableToRuntimeBinding(bindings, value))
|
||||||
})
|
})
|
||||||
|
@ -135,18 +170,21 @@
|
||||||
</section>
|
</section>
|
||||||
{/if}
|
{/if}
|
||||||
{/each}
|
{/each}
|
||||||
{#if filteredHelpers?.length && !usingJS}
|
{#if filteredHelpers?.length}
|
||||||
<section>
|
<section>
|
||||||
<div class="heading">Helpers</div>
|
<div class="heading">Helpers</div>
|
||||||
<ul>
|
<ul>
|
||||||
{#each filteredHelpers as helper}
|
{#each filteredHelpers as helper}
|
||||||
<li on:click={() => addHelper(helper)}>
|
<li on:click={() => addHelper(helper, usingJS)}>
|
||||||
<div class="helper">
|
<div class="helper">
|
||||||
<div class="helper__name">{helper.displayText}</div>
|
<div class="helper__name">{helper.displayText}</div>
|
||||||
<div class="helper__description">
|
<div class="helper__description">
|
||||||
{@html helper.description}
|
{@html helper.description}
|
||||||
</div>
|
</div>
|
||||||
<pre class="helper__example">{helper.example || ""}</pre>
|
<pre class="helper__example">{getHelperExample(
|
||||||
|
helper,
|
||||||
|
usingJS
|
||||||
|
)}</pre>
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
{/each}
|
{/each}
|
||||||
|
@ -172,6 +210,11 @@
|
||||||
for more details.
|
for more details.
|
||||||
</p>
|
</p>
|
||||||
{/if}
|
{/if}
|
||||||
|
{#if $admin.isDev}
|
||||||
|
<div class="convert">
|
||||||
|
<Button secondary on:click={convert}>Convert to JS</Button>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</Tab>
|
</Tab>
|
||||||
{#if allowJS}
|
{#if allowJS}
|
||||||
|
@ -306,4 +349,8 @@
|
||||||
color: var(--red);
|
color: var(--red);
|
||||||
text-decoration: underline;
|
text-decoration: underline;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.convert {
|
||||||
|
padding-top: var(--spacing-m);
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -18,10 +18,14 @@ export function addHBSBinding(value, caretPos, binding) {
|
||||||
return value
|
return value
|
||||||
}
|
}
|
||||||
|
|
||||||
export function addJSBinding(value, caretPos, binding) {
|
export function addJSBinding(value, caretPos, binding, { helper } = {}) {
|
||||||
binding = typeof binding === "string" ? binding : binding.path
|
binding = typeof binding === "string" ? binding : binding.path
|
||||||
value = value == null ? "" : value
|
value = value == null ? "" : value
|
||||||
binding = `$("${binding}")`
|
if (!helper) {
|
||||||
|
binding = `$("${binding}")`
|
||||||
|
} else {
|
||||||
|
binding = `helper.${binding}()`
|
||||||
|
}
|
||||||
if (caretPos.start) {
|
if (caretPos.start) {
|
||||||
value =
|
value =
|
||||||
value.substring(0, caretPos.start) +
|
value.substring(0, caretPos.start) +
|
||||||
|
|
|
@ -56,6 +56,10 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const previewApp = () => {
|
||||||
|
window.open(`/${application}`)
|
||||||
|
}
|
||||||
|
|
||||||
const viewApp = () => {
|
const viewApp = () => {
|
||||||
analytics.captureEvent(Events.APP_VIEW_PUBLISHED, {
|
analytics.captureEvent(Events.APP_VIEW_PUBLISHED, {
|
||||||
appId: selectedApp.appId,
|
appId: selectedApp.appId,
|
||||||
|
@ -174,7 +178,10 @@
|
||||||
Are you sure you want to unpublish the app <b>{selectedApp?.name}</b>?
|
Are you sure you want to unpublish the app <b>{selectedApp?.name}</b>?
|
||||||
</ConfirmDialog>
|
</ConfirmDialog>
|
||||||
|
|
||||||
<DeployModal onOk={completePublish} />
|
<div class="buttons">
|
||||||
|
<Button on:click={previewApp} newStyles secondary>Preview</Button>
|
||||||
|
<DeployModal onOk={completePublish} />
|
||||||
|
</div>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.publish-popover-actions :global([data-cy="publish-popover-action"]) {
|
.publish-popover-actions :global([data-cy="publish-popover-action"]) {
|
||||||
|
@ -183,4 +190,11 @@
|
||||||
:global([data-cy="publish-popover-menu"]) {
|
:global([data-cy="publish-popover-menu"]) {
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
}
|
}
|
||||||
|
.buttons {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: flex-end;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--spacing-m);
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
<script>
|
<script>
|
||||||
import {
|
import {
|
||||||
Icon,
|
|
||||||
Modal,
|
Modal,
|
||||||
notifications,
|
notifications,
|
||||||
ModalContent,
|
ModalContent,
|
||||||
Body,
|
Body,
|
||||||
Button,
|
Button,
|
||||||
|
StatusLight,
|
||||||
} from "@budibase/bbui"
|
} from "@budibase/bbui"
|
||||||
import { store } from "builderStore"
|
import { store } from "builderStore"
|
||||||
import { API } from "api"
|
import { API } from "api"
|
||||||
|
@ -67,17 +67,10 @@
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if !hideIcon}
|
{#if !hideIcon && updateAvailable}
|
||||||
<div class="icon-wrapper" class:highlight={updateAvailable}>
|
<StatusLight hoverable on:click={updateModal.show} notice>
|
||||||
<Icon
|
Update available
|
||||||
name="Refresh"
|
</StatusLight>
|
||||||
hoverable
|
|
||||||
on:click={updateModal.show}
|
|
||||||
tooltip={updateAvailable
|
|
||||||
? "An update is available"
|
|
||||||
: "No updates are available"}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
{/if}
|
{/if}
|
||||||
<Modal bind:this={updateModal}>
|
<Modal bind:this={updateModal}>
|
||||||
<ModalContent
|
<ModalContent
|
||||||
|
|
|
@ -3,11 +3,13 @@
|
||||||
|
|
||||||
export let title
|
export let title
|
||||||
export let icon
|
export let icon
|
||||||
|
export let expandable = false
|
||||||
export let showAddButton = false
|
export let showAddButton = false
|
||||||
export let showBackButton = false
|
export let showBackButton = false
|
||||||
export let showExpandIcon = false
|
export let showCloseButton = false
|
||||||
export let onClickAddButton
|
export let onClickAddButton
|
||||||
export let onClickBackButton
|
export let onClickBackButton
|
||||||
|
export let onClickCloseButton
|
||||||
export let borderLeft = false
|
export let borderLeft = false
|
||||||
export let borderRight = false
|
export let borderRight = false
|
||||||
|
|
||||||
|
@ -25,7 +27,7 @@
|
||||||
<div class="title">
|
<div class="title">
|
||||||
<Heading size="XXS">{title || ""}</Heading>
|
<Heading size="XXS">{title || ""}</Heading>
|
||||||
</div>
|
</div>
|
||||||
{#if showExpandIcon}
|
{#if expandable}
|
||||||
<Icon
|
<Icon
|
||||||
name={wide ? "Minimize" : "Maximize"}
|
name={wide ? "Minimize" : "Maximize"}
|
||||||
hoverable
|
hoverable
|
||||||
|
@ -37,6 +39,9 @@
|
||||||
<Icon name="Add" />
|
<Icon name="Add" />
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
{#if showCloseButton}
|
||||||
|
<Icon name="Close" hoverable on:click={onClickCloseButton} />
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
<div class="body">
|
<div class="body">
|
||||||
<slot />
|
<slot />
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
<script>
|
<script>
|
||||||
import { Select, Label, Checkbox } from "@budibase/bbui"
|
import { Select, Label } from "@budibase/bbui"
|
||||||
import { currentAsset, store } from "builderStore"
|
import { currentAsset, store } from "builderStore"
|
||||||
import { getActionProviderComponents } from "builderStore/dataBinding"
|
import { getActionProviderComponents } from "builderStore/dataBinding"
|
||||||
|
|
||||||
|
@ -21,10 +21,6 @@
|
||||||
getOptionValue={x => x._id}
|
getOptionValue={x => x._id}
|
||||||
/>
|
/>
|
||||||
<div />
|
<div />
|
||||||
<Checkbox
|
|
||||||
text="Validate only current step"
|
|
||||||
bind:value={parameters.onlyCurrentStep}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
<script>
|
<script>
|
||||||
import { Layout, Icon, ActionButton } from "@budibase/bbui"
|
import { Layout, Icon, ActionButton, InlineAlert } from "@budibase/bbui"
|
||||||
import StatusRenderer from "./StatusRenderer.svelte"
|
import StatusRenderer from "./StatusRenderer.svelte"
|
||||||
import DateTimeRenderer from "components/common/renderers/DateTimeRenderer.svelte"
|
import DateTimeRenderer from "components/common/renderers/DateTimeRenderer.svelte"
|
||||||
import TestDisplay from "components/automation/AutomationBuilder/TestDisplay.svelte"
|
import TestDisplay from "components/automation/AutomationBuilder/TestDisplay.svelte"
|
||||||
|
@ -9,6 +9,7 @@
|
||||||
export let history
|
export let history
|
||||||
export let appId
|
export let appId
|
||||||
export let close
|
export let close
|
||||||
|
const STOPPED_ERROR = "stopped_error"
|
||||||
|
|
||||||
$: exists = $automationStore.automations?.find(
|
$: exists = $automationStore.automations?.find(
|
||||||
auto => auto._id === history?.automationId
|
auto => auto._id === history?.automationId
|
||||||
|
@ -32,6 +33,15 @@
|
||||||
<Icon name="JourneyVoyager" />
|
<Icon name="JourneyVoyager" />
|
||||||
<div>{history.automationName}</div>
|
<div>{history.automationName}</div>
|
||||||
</div>
|
</div>
|
||||||
|
{#if history.status === STOPPED_ERROR}
|
||||||
|
<div class="cron-error">
|
||||||
|
<InlineAlert
|
||||||
|
type="error"
|
||||||
|
header="CRON automation disabled"
|
||||||
|
message="Fix the error and re-publish your app to re-activate."
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
<div>
|
<div>
|
||||||
{#if exists}
|
{#if exists}
|
||||||
<ActionButton
|
<ActionButton
|
||||||
|
@ -87,4 +97,10 @@
|
||||||
grid-template-columns: 1fr auto;
|
grid-template-columns: 1fr auto;
|
||||||
gap: var(--spacing-s);
|
gap: var(--spacing-s);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.cron-error {
|
||||||
|
display: flex;
|
||||||
|
width: 100%;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -3,7 +3,8 @@
|
||||||
export let value
|
export let value
|
||||||
|
|
||||||
$: isError = !value || value.toLowerCase() === "error"
|
$: isError = !value || value.toLowerCase() === "error"
|
||||||
$: isStopped = value?.toLowerCase() === "stopped"
|
$: isStoppedError = value?.toLowerCase() === "stopped_error"
|
||||||
|
$: isStopped = value?.toLowerCase() === "stopped" || isStoppedError
|
||||||
$: status = getStatus(isError, isStopped)
|
$: status = getStatus(isError, isStopped)
|
||||||
|
|
||||||
function getStatus(error, stopped) {
|
function getStatus(error, stopped) {
|
||||||
|
|
|
@ -0,0 +1,75 @@
|
||||||
|
<script>
|
||||||
|
import { ActionButton, Icon, Search, Divider, Detail } from "@budibase/bbui"
|
||||||
|
|
||||||
|
export let searchTerm = ""
|
||||||
|
export let selected
|
||||||
|
export let filtered = []
|
||||||
|
export let addAll
|
||||||
|
export let select
|
||||||
|
export let title
|
||||||
|
export let key
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div style="padding: var(--spacing-m)">
|
||||||
|
<Search placeholder="Search" bind:value={searchTerm} />
|
||||||
|
<div class="header sub-header">
|
||||||
|
<div>
|
||||||
|
<Detail
|
||||||
|
>{filtered.length} {title}{filtered.length === 1 ? "" : "s"}</Detail
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<ActionButton on:click={addAll} emphasized size="S">Add all</ActionButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Divider noMargin />
|
||||||
|
<div>
|
||||||
|
{#each filtered as item}
|
||||||
|
<div
|
||||||
|
on:click={() => {
|
||||||
|
select(item._id)
|
||||||
|
}}
|
||||||
|
style="padding-bottom: var(--spacing-m)"
|
||||||
|
class="selection"
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
{item[key]}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{#if selected.includes(item._id)}
|
||||||
|
<div>
|
||||||
|
<Icon
|
||||||
|
color="var(--spectrum-global-color-blue-600);"
|
||||||
|
name="Checkmark"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.header {
|
||||||
|
align-items: center;
|
||||||
|
padding: var(--spacing-m) 0 var(--spacing-m) 0;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.selection {
|
||||||
|
align-items: end;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.selection > :first-child {
|
||||||
|
padding-top: var(--spacing-m);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sub-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -30,7 +30,7 @@
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
<div class="desktop">
|
<div class="desktop">
|
||||||
<AppLockModal {app} buttonSize="M" />
|
<span><AppLockModal {app} buttonSize="M" /></span>
|
||||||
</div>
|
</div>
|
||||||
<div class="desktop">
|
<div class="desktop">
|
||||||
<div class="app-status">
|
<div class="app-status">
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue