Merge branch 'develop' of github.com:Budibase/budibase into nord-theme
This commit is contained in:
commit
0427b9dc7f
|
@ -24,9 +24,28 @@
|
||||||
{
|
{
|
||||||
"files": ["*.svelte"],
|
"files": ["*.svelte"],
|
||||||
"processor": "svelte3/svelte3"
|
"processor": "svelte3/svelte3"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"files": ["**/*.ts"],
|
||||||
|
"parser": "@typescript-eslint/parser",
|
||||||
|
"plugins": [],
|
||||||
|
"extends": [
|
||||||
|
"eslint:recommended"
|
||||||
|
],
|
||||||
|
"rules": {
|
||||||
|
"no-unused-vars": "off",
|
||||||
|
"no-inner-declarations": "off",
|
||||||
|
"no-case-declarations": "off",
|
||||||
|
"no-useless-escape": "off",
|
||||||
|
"no-undef": "off",
|
||||||
|
"no-prototype-builtins": "off"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"rules": {
|
"rules": {
|
||||||
"no-self-assign": "off"
|
"no-self-assign": "off"
|
||||||
|
},
|
||||||
|
"globals": {
|
||||||
|
"GeolocationPositionError": true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,6 +2,8 @@ name: Budibase Smoke Test
|
||||||
|
|
||||||
on:
|
on:
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
schedule:
|
||||||
|
- cron: "0 5 * * *" # every day at 5AM
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
release:
|
release:
|
||||||
|
@ -23,10 +25,13 @@ jobs:
|
||||||
-o packages/builder/cypress.env.json \
|
-o packages/builder/cypress.env.json \
|
||||||
-L https://api.github.com/repos/budibase/budibase-infra/contents/test/cypress.env.json
|
-L https://api.github.com/repos/budibase/budibase-infra/contents/test/cypress.env.json
|
||||||
wc -l packages/builder/cypress.env.json
|
wc -l packages/builder/cypress.env.json
|
||||||
- run: yarn test:e2e:ci
|
|
||||||
env:
|
- name: Cypress run
|
||||||
CI: true
|
id: cypress
|
||||||
name: Budibase CI
|
uses: cypress-io/github-action@v2
|
||||||
|
with:
|
||||||
|
install: false
|
||||||
|
command: yarn test:e2e:ci
|
||||||
|
|
||||||
# TODO: upload recordings to s3
|
# TODO: upload recordings to s3
|
||||||
# - name: Configure AWS Credentials
|
# - name: Configure AWS Credentials
|
||||||
|
@ -36,11 +41,11 @@ jobs:
|
||||||
# aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
|
# aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
|
||||||
# aws-region: eu-west-1
|
# aws-region: eu-west-1
|
||||||
|
|
||||||
# TODO look at cypress reporters
|
- name: Discord Webhook Action
|
||||||
# - name: Discord Webhook Action
|
uses: tsickert/discord-webhook@v4.0.0
|
||||||
# uses: tsickert/discord-webhook@v4.0.0
|
with:
|
||||||
# with:
|
webhook-url: ${{ secrets.BUDI_QA_WEBHOOK }}
|
||||||
# webhook-url: ${{ secrets.PROD_DEPLOY_WEBHOOK_URL }}
|
content: "Smoke test run completed with ${{ steps.cypress.outcome }}. See results at ${{ steps.cypress.dashboardUrl }}"
|
||||||
# content: "Production Deployment Complete: ${{ env.RELEASE_VERSION }} deployed to Budibase Cloud."
|
embed-title: ${{ steps.cypress.outcome }}
|
||||||
# embed-title: ${{ env.RELEASE_VERSION }}
|
embed-color: ${{ steps.cypress.outcome == 'success' && '3066993' || '15548997' }}
|
||||||
|
|
||||||
|
|
|
@ -22,9 +22,16 @@
|
||||||
"name": "Budibase Worker",
|
"name": "Budibase Worker",
|
||||||
"type": "node",
|
"type": "node",
|
||||||
"request": "launch",
|
"request": "launch",
|
||||||
"program": "${workspaceFolder}/packages/worker/src/index.js",
|
"runtimeArgs": [
|
||||||
|
"--nolazy",
|
||||||
|
"-r",
|
||||||
|
"ts-node/register/transpile-only"
|
||||||
|
],
|
||||||
|
"args": [
|
||||||
|
"${workspaceFolder}/packages/worker/src/index.ts"
|
||||||
|
],
|
||||||
"cwd": "${workspaceFolder}/packages/worker"
|
"cwd": "${workspaceFolder}/packages/worker"
|
||||||
}
|
},
|
||||||
],
|
],
|
||||||
"compounds": [
|
"compounds": [
|
||||||
{
|
{
|
||||||
|
|
29
README.md
29
README.md
|
@ -102,6 +102,35 @@ Budibase is made to scale. With Budibase, you can self-host on your own infrastr
|
||||||
|
|
||||||
- Checkout the promo video: https://youtu.be/xoljVpty_Kw
|
- Checkout the promo video: https://youtu.be/xoljVpty_Kw
|
||||||
|
|
||||||
|
<br />
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<br />
|
||||||
|
|
||||||
|
|
||||||
|
## Budibase Public API
|
||||||
|
As with anything that we build in Budibase, our new public API is simple to use, flexible, and introduces new extensibility. To summarize, the Budibase API enables:
|
||||||
|
|
||||||
|
- Budibase as a backend
|
||||||
|
- Interoperability
|
||||||
|
|
||||||
|
|
||||||
|
#### Docs
|
||||||
|
You can learn more about the Budibase API at the following places:
|
||||||
|
|
||||||
|
- [General documentation](https://docs.budibase.com/docs/public-api) : Learn how to get your API key, how to use spec, and how to use with Postman
|
||||||
|
- [Interactive API documentation](https://docs.budibase.com/reference/post_applications) : Learn how to interact with the API
|
||||||
|
|
||||||
|
#### Guides
|
||||||
|
|
||||||
|
- [Build an app with Budibase and 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 />
|
<br /><br /><br />
|
||||||
|
|
||||||
## 🏁 Get started
|
## 🏁 Get started
|
||||||
|
|
|
@ -15,7 +15,7 @@ version: 0.2.8
|
||||||
appVersion: 1.0.48
|
appVersion: 1.0.48
|
||||||
dependencies:
|
dependencies:
|
||||||
- name: couchdb
|
- name: couchdb
|
||||||
version: 3.3.4
|
version: 3.6.1
|
||||||
repository: https://apache.github.io/couchdb-helm
|
repository: https://apache.github.io/couchdb-helm
|
||||||
condition: services.couchdb.enabled
|
condition: services.couchdb.enabled
|
||||||
- name: ingress-nginx
|
- name: ingress-nginx
|
||||||
|
|
|
@ -110,6 +110,10 @@ spec:
|
||||||
value: {{ .Values.globals.cookieDomain | quote }}
|
value: {{ .Values.globals.cookieDomain | quote }}
|
||||||
- name: HTTP_MIGRATIONS
|
- name: HTTP_MIGRATIONS
|
||||||
value: {{ .Values.globals.httpMigrations | quote }}
|
value: {{ .Values.globals.httpMigrations | quote }}
|
||||||
|
- name: GOOGLE_CLIENT_ID
|
||||||
|
value: {{ .Values.globals.google.clientId | quote }}
|
||||||
|
- name: GOOGLE_CLIENT_SECRET
|
||||||
|
value: {{ .Values.globals.google.secret | quote }}
|
||||||
image: budibase/apps:{{ .Values.globals.appVersion }}
|
image: budibase/apps:{{ .Values.globals.appVersion }}
|
||||||
imagePullPolicy: Always
|
imagePullPolicy: Always
|
||||||
name: bbapps
|
name: bbapps
|
||||||
|
|
|
@ -12,7 +12,7 @@ All ports are BLOCKED except 22 (SSH), 80 (HTTP), 443 (HTTPS), and 10000
|
||||||
|
|
||||||
* Budibase website: http://budibase.com
|
* Budibase website: http://budibase.com
|
||||||
|
|
||||||
For help and more information, visit https://docs.budibase.com/self-hosting/hosting-methods/digitalocean
|
For help and more information, visit https://docs.budibase.com/docs/digitalocean
|
||||||
|
|
||||||
********************************************************************************
|
********************************************************************************
|
||||||
To delete this message of the day: rm -rf $(readlink -f ${0})
|
To delete this message of the day: rm -rf $(readlink -f ${0})
|
||||||
|
|
|
@ -5,7 +5,7 @@ version: "3"
|
||||||
services:
|
services:
|
||||||
minio-service:
|
minio-service:
|
||||||
container_name: budi-minio-dev
|
container_name: budi-minio-dev
|
||||||
restart: always
|
restart: on-failure
|
||||||
image: minio/minio
|
image: minio/minio
|
||||||
volumes:
|
volumes:
|
||||||
- minio_data:/data
|
- minio_data:/data
|
||||||
|
@ -23,7 +23,7 @@ services:
|
||||||
|
|
||||||
proxy-service:
|
proxy-service:
|
||||||
container_name: budi-nginx-dev
|
container_name: budi-nginx-dev
|
||||||
restart: always
|
restart: on-failure
|
||||||
image: nginx:latest
|
image: nginx:latest
|
||||||
volumes:
|
volumes:
|
||||||
- ./.generated-nginx.dev.conf:/etc/nginx/nginx.conf
|
- ./.generated-nginx.dev.conf:/etc/nginx/nginx.conf
|
||||||
|
@ -38,7 +38,7 @@ services:
|
||||||
couchdb-service:
|
couchdb-service:
|
||||||
# platform: linux/amd64
|
# platform: linux/amd64
|
||||||
container_name: budi-couchdb-dev
|
container_name: budi-couchdb-dev
|
||||||
restart: always
|
restart: on-failure
|
||||||
image: ibmcom/couchdb3
|
image: ibmcom/couchdb3
|
||||||
environment:
|
environment:
|
||||||
- COUCHDB_PASSWORD=${COUCH_DB_PASSWORD}
|
- COUCHDB_PASSWORD=${COUCH_DB_PASSWORD}
|
||||||
|
@ -59,7 +59,7 @@ services:
|
||||||
|
|
||||||
redis-service:
|
redis-service:
|
||||||
container_name: budi-redis-dev
|
container_name: budi-redis-dev
|
||||||
restart: always
|
restart: on-failure
|
||||||
image: redis
|
image: redis
|
||||||
command: redis-server --requirepass ${REDIS_PASSWORD}
|
command: redis-server --requirepass ${REDIS_PASSWORD}
|
||||||
ports:
|
ports:
|
||||||
|
|
|
@ -4,7 +4,7 @@ version: "3"
|
||||||
|
|
||||||
services:
|
services:
|
||||||
app-service:
|
app-service:
|
||||||
restart: always
|
restart: unless-stopped
|
||||||
image: budibase.docker.scarf.sh/budibase/apps
|
image: budibase.docker.scarf.sh/budibase/apps
|
||||||
container_name: bbapps
|
container_name: bbapps
|
||||||
environment:
|
environment:
|
||||||
|
@ -28,7 +28,7 @@ services:
|
||||||
- redis-service
|
- redis-service
|
||||||
|
|
||||||
worker-service:
|
worker-service:
|
||||||
restart: always
|
restart: unless-stopped
|
||||||
image: budibase.docker.scarf.sh/budibase/worker
|
image: budibase.docker.scarf.sh/budibase/worker
|
||||||
container_name: bbworker
|
container_name: bbworker
|
||||||
environment:
|
environment:
|
||||||
|
@ -53,7 +53,7 @@ services:
|
||||||
- couch-init
|
- couch-init
|
||||||
|
|
||||||
minio-service:
|
minio-service:
|
||||||
restart: always
|
restart: unless-stopped
|
||||||
image: minio/minio
|
image: minio/minio
|
||||||
volumes:
|
volumes:
|
||||||
- minio_data:/data
|
- minio_data:/data
|
||||||
|
@ -69,7 +69,7 @@ services:
|
||||||
retries: 3
|
retries: 3
|
||||||
|
|
||||||
proxy-service:
|
proxy-service:
|
||||||
restart: always
|
restart: unless-stopped
|
||||||
ports:
|
ports:
|
||||||
- "${MAIN_PORT}:10000"
|
- "${MAIN_PORT}:10000"
|
||||||
container_name: bbproxy
|
container_name: bbproxy
|
||||||
|
@ -81,7 +81,7 @@ services:
|
||||||
- couchdb-service
|
- couchdb-service
|
||||||
|
|
||||||
couchdb-service:
|
couchdb-service:
|
||||||
restart: always
|
restart: unless-stopped
|
||||||
image: ibmcom/couchdb3
|
image: ibmcom/couchdb3
|
||||||
environment:
|
environment:
|
||||||
- COUCHDB_PASSWORD=${COUCH_DB_PASSWORD}
|
- COUCHDB_PASSWORD=${COUCH_DB_PASSWORD}
|
||||||
|
@ -98,13 +98,14 @@ services:
|
||||||
command: ["sh","-c","sleep 10 && $${PUT_CALL}/_users && $${PUT_CALL}/_replicator; fg;"]
|
command: ["sh","-c","sleep 10 && $${PUT_CALL}/_users && $${PUT_CALL}/_replicator; fg;"]
|
||||||
|
|
||||||
redis-service:
|
redis-service:
|
||||||
restart: always
|
restart: unless-stopped
|
||||||
image: redis
|
image: redis
|
||||||
command: redis-server --requirepass ${REDIS_PASSWORD}
|
command: redis-server --requirepass ${REDIS_PASSWORD}
|
||||||
volumes:
|
volumes:
|
||||||
- redis_data:/data
|
- redis_data:/data
|
||||||
|
|
||||||
watchtower-service:
|
watchtower-service:
|
||||||
|
restart: always
|
||||||
image: containrrr/watchtower
|
image: containrrr/watchtower
|
||||||
volumes:
|
volumes:
|
||||||
- /var/run/docker.sock:/var/run/docker.sock
|
- /var/run/docker.sock:/var/run/docker.sock
|
||||||
|
|
|
@ -52,9 +52,8 @@ http {
|
||||||
proxy_pass http://{{ address }}:4001;
|
proxy_pass http://{{ address }}:4001;
|
||||||
}
|
}
|
||||||
|
|
||||||
location /app/ {
|
location /app {
|
||||||
proxy_pass http://{{ address }}:4001;
|
proxy_pass http://{{ address }}:4001;
|
||||||
rewrite ^/app/(.*)$ /$1 break;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
location /builder {
|
location /builder {
|
||||||
|
|
|
@ -22,9 +22,8 @@ http {
|
||||||
resolver {{ resolver }} valid=10s ipv6=off;
|
resolver {{ resolver }} valid=10s ipv6=off;
|
||||||
|
|
||||||
# buffering
|
# buffering
|
||||||
client_body_buffer_size 1K;
|
|
||||||
client_header_buffer_size 1k;
|
client_header_buffer_size 1k;
|
||||||
client_max_body_size 10M;
|
client_max_body_size 20M;
|
||||||
ignore_invalid_headers off;
|
ignore_invalid_headers off;
|
||||||
proxy_buffering off;
|
proxy_buffering off;
|
||||||
|
|
||||||
|
@ -43,13 +42,25 @@ http {
|
||||||
client_max_body_size 1000m;
|
client_max_body_size 1000m;
|
||||||
ignore_invalid_headers off;
|
ignore_invalid_headers off;
|
||||||
proxy_buffering off;
|
proxy_buffering off;
|
||||||
# port_in_redirect off;
|
|
||||||
|
set $csp_default "default-src 'self'"
|
||||||
|
set $csp_script "script-src 'self' 'unsafe-inline' 'unsafe-eval' https://cdn.budi.live https://js.intercomcdn.com https://widget.intercom.io";
|
||||||
|
set $csp_style "style-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net https://fonts.googleapis.com https://rsms.me https://maxcdn.bootstrapcdn.com";
|
||||||
|
set $csp_object "object-src 'none'";
|
||||||
|
set $csp_base_uri "base-uri 'self'";
|
||||||
|
set $csp_connect "connect-src 'self' https://api-iam.intercom.io https://api-iam.intercom.io https://api-ping.intercom.io https://app.posthog.com wss://nexus-websocket-a.intercom.io wss://nexus-websocket-b.intercom.io https://nexus-websocket-a.intercom.io https://nexus-websocket-b.intercom.io https://uploads.intercomcdn.com https://uploads.intercomusercontent.com";
|
||||||
|
set $csp_font "font-src 'self' data: https://cdn.jsdelivr.net https://fonts.gstatic.com https://rsms.me https://maxcdn.bootstrapcdn.com https://js.intercomcdn.com https://fonts.intercomcdn.com";
|
||||||
|
set $csp_frame "frame-src 'self' https:";
|
||||||
|
set $csp_img "img-src http: https: data: blob:";
|
||||||
|
set $csp_manifest "manifest-src 'self'";
|
||||||
|
set $csp_media "media-src 'self' https://js.intercomcdn.com";
|
||||||
|
set $csp_worker "worker-src 'none'";
|
||||||
|
|
||||||
# Security Headers
|
# Security Headers
|
||||||
add_header X-Frame-Options SAMEORIGIN always;
|
add_header X-Frame-Options SAMEORIGIN always;
|
||||||
add_header X-Content-Type-Options nosniff always;
|
add_header X-Content-Type-Options nosniff always;
|
||||||
add_header X-XSS-Protection "1; mode=block" always;
|
add_header X-XSS-Protection "1; mode=block" always;
|
||||||
add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval' https://cdn.budi.live https://js.intercomcdn.com https://widget.intercom.io; style-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net https://fonts.googleapis.com https://rsms.me https://maxcdn.bootstrapcdn.com; object-src 'none'; base-uri 'self'; connect-src 'self' https://api-iam.intercom.io https://app.posthog.com wss://nexus-websocket-a.intercom.io ; font-src 'self' data https://cdn.jsdelivr.net https://fonts.gstatic.com https://rsms.me https://maxcdn.bootstrapcdn.com; frame-src 'self' https:; img-src http: https: data; manifest-src 'self'; media-src 'self'; worker-src 'none';" always;
|
add_header Content-Security-Policy ${csp_default}; ${csp_script}; ${csp_style}; ${csp_object}; ${csp_base_uri}; ${csp_connect}; ${csp_font}; ${csp_frame}; ${csp_img}; ${csp_manifest}; ${csp_media}; ${csp_worker};" always;
|
||||||
|
|
||||||
# upstreams
|
# upstreams
|
||||||
set $apps {{ apps }};
|
set $apps {{ apps }};
|
||||||
|
@ -62,7 +73,6 @@ http {
|
||||||
|
|
||||||
location /app {
|
location /app {
|
||||||
proxy_pass http://$apps:4002;
|
proxy_pass http://$apps:4002;
|
||||||
rewrite ^/app/(.*)$ /$1 break;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
location = / {
|
location = / {
|
||||||
|
|
|
@ -39,7 +39,7 @@
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<h3 align="center">
|
<h3 align="center">
|
||||||
<a href="https://docs.budibase.com/getting-started">Los Geht's</a>
|
<a href="https://docs.budibase.com/docs/quickstart-tutorials">Los Geht's</a>
|
||||||
<span> · </span>
|
<span> · </span>
|
||||||
<a href="https://docs.budibase.com">Dokumentation</a>
|
<a href="https://docs.budibase.com">Dokumentation</a>
|
||||||
<span> · </span>
|
<span> · </span>
|
||||||
|
@ -109,7 +109,7 @@ $ budi hosting --start
|
||||||
4. Lege einen Admin-Benutzer an.
|
4. Lege einen Admin-Benutzer an.
|
||||||
Gib die E-Mail und das Passwort für den neuen Admin-Benutzer ein.
|
Gib die E-Mail und das Passwort für den neuen Admin-Benutzer ein.
|
||||||
|
|
||||||
Schon geschafft! Jetzt kann es losgehen mit der minutenschnellen Entwicklung deiner Tools. Für weitere Informationen und Tipps schau doch mal in unsere [Dokumentation](https://docs.budibase.com/getting-started).
|
Schon geschafft! Jetzt kann es losgehen mit der minutenschnellen Entwicklung deiner Tools. Für weitere Informationen und Tipps schau doch mal in unsere [Dokumentation](https://docs.budibase.com/docs/quickstart-tutorials).
|
||||||
|
|
||||||
<br />
|
<br />
|
||||||
|
|
||||||
|
|
|
@ -112,7 +112,7 @@ The Budibase builder runs in Electron, on Mac, PC and Linux. Follow the steps be
|
||||||
|
|
||||||
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!
|
Budibase wants to make sure anyone can use the tools we develop and we know a lot of people need to be able to host the apps they make on their own systems - that is why we've decided to try and make self hosting as easy as possible!
|
||||||
|
|
||||||
Currently, you can host your apps using Docker or Digital Ocean. The documentation for self-hosting can be found [here](https://docs.budibase.com/self-hosting/introduction-to-self-hosting).
|
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)
|
[![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)
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
{
|
{
|
||||||
"version": "1.0.89-alpha.0",
|
"version": "1.0.104-alpha.0",
|
||||||
"npmClient": "yarn",
|
"npmClient": "yarn",
|
||||||
"packages": [
|
"packages": [
|
||||||
"packages/*"
|
"packages/*"
|
||||||
|
|
16
package.json
16
package.json
|
@ -15,7 +15,9 @@
|
||||||
"prettier-plugin-svelte": "^2.3.0",
|
"prettier-plugin-svelte": "^2.3.0",
|
||||||
"rimraf": "^3.0.2",
|
"rimraf": "^3.0.2",
|
||||||
"rollup-plugin-replace": "^2.2.0",
|
"rollup-plugin-replace": "^2.2.0",
|
||||||
"svelte": "^3.38.2"
|
"svelte": "^3.38.2",
|
||||||
|
"@typescript-eslint/parser": "4.28.0",
|
||||||
|
"typescript": "4.5.5"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"setup": "node ./hosting/scripts/setup.js && yarn && yarn bootstrap && yarn build && yarn dev",
|
"setup": "node ./hosting/scripts/setup.js && yarn && yarn bootstrap && yarn build && yarn dev",
|
||||||
|
@ -31,17 +33,19 @@
|
||||||
"nuke:docker": "lerna run --parallel dev:stack:nuke",
|
"nuke:docker": "lerna run --parallel dev:stack:nuke",
|
||||||
"clean": "lerna clean",
|
"clean": "lerna clean",
|
||||||
"kill-port": "kill-port 4001",
|
"kill-port": "kill-port 4001",
|
||||||
"dev": "yarn run kill-port && lerna link && lerna run --parallel dev:builder --concurrency 1",
|
"kill-builder": "kill-port 3000",
|
||||||
"dev:noserver": "lerna link && lerna run dev:stack:up && lerna run --parallel dev:builder --concurrency 1 --ignore @budibase/server --ignore @budibase/worker",
|
"kill-server": "kill-port 4001 4002",
|
||||||
"dev:server": "lerna run --parallel dev:builder --concurrency 1 --scope @budibase/worker --scope @budibase/server",
|
"kill-all": "yarn run kill-builder && yarn run kill-server",
|
||||||
|
"dev": "yarn run kill-all && lerna link && lerna run --parallel dev:builder --concurrency 1",
|
||||||
|
"dev:noserver": "yarn run kill-builder && lerna link && lerna run dev:stack:up && lerna run --parallel dev:builder --concurrency 1 --ignore @budibase/server --ignore @budibase/worker",
|
||||||
|
"dev:server": "yarn run kill-server && lerna run --parallel dev:builder --concurrency 1 --scope @budibase/worker --scope @budibase/server",
|
||||||
"test": "lerna run test",
|
"test": "lerna run test",
|
||||||
"lint:eslint": "eslint packages",
|
"lint:eslint": "eslint packages",
|
||||||
"lint:prettier": "prettier --check \"packages/**/*.{js,ts,svelte}\"",
|
"lint:prettier": "prettier --check \"packages/**/*.{js,ts,svelte}\"",
|
||||||
"lint": "yarn run lint:eslint && yarn run lint:prettier",
|
"lint": "yarn run lint:eslint && yarn run lint:prettier",
|
||||||
"lint:fix:eslint": "eslint --fix packages",
|
"lint:fix:eslint": "eslint --fix packages",
|
||||||
"lint:fix:prettier": "prettier --write \"packages/**/*.{js,ts,svelte}\" && prettier --write \"examples/**/*.{js,ts,svelte}\"",
|
"lint:fix:prettier": "prettier --write \"packages/**/*.{js,ts,svelte}\" && prettier --write \"examples/**/*.{js,ts,svelte}\"",
|
||||||
"lint:fix:ts": "lerna run lint:fix",
|
"lint:fix": "yarn run lint:fix:prettier && yarn run lint:fix:eslint",
|
||||||
"lint:fix": "yarn run lint:fix:ts && yarn run lint:fix:prettier && yarn run lint:fix:eslint",
|
|
||||||
"test:e2e": "lerna run cy:test --stream",
|
"test:e2e": "lerna run cy:test --stream",
|
||||||
"test:e2e:ci": "lerna run cy:ci --stream",
|
"test:e2e:ci": "lerna run cy:ci --stream",
|
||||||
"build:specs": "lerna run specs",
|
"build:specs": "lerna run specs",
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@budibase/backend-core",
|
"name": "@budibase/backend-core",
|
||||||
"version": "1.0.89-alpha.0",
|
"version": "1.0.104-alpha.0",
|
||||||
"description": "Budibase backend core libraries used in server and worker",
|
"description": "Budibase backend core libraries used in server and worker",
|
||||||
"main": "src/index.js",
|
"main": "src/index.js",
|
||||||
"author": "Budibase",
|
"author": "Budibase",
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
const env = require("../environment")
|
const env = require("../environment")
|
||||||
const { Headers } = require("../../constants")
|
const { Headers } = require("../../constants")
|
||||||
|
const { SEPARATOR, DocumentTypes } = require("../db/constants")
|
||||||
const cls = require("./FunctionContext")
|
const cls = require("./FunctionContext")
|
||||||
const { getCouch } = require("../db")
|
const { getCouch } = require("../db")
|
||||||
const { getProdAppID, getDevelopmentAppID } = require("../db/conversions")
|
const { getProdAppID, getDevelopmentAppID } = require("../db/conversions")
|
||||||
|
@ -42,8 +43,39 @@ exports.doInTenant = (tenantId, task) => {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Given an app ID this will attempt to retrieve the tenant ID from it.
|
||||||
|
* @return {null|string} The tenant ID found within the app ID.
|
||||||
|
*/
|
||||||
|
exports.getTenantIDFromAppID = appId => {
|
||||||
|
if (!appId) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
const split = appId.split(SEPARATOR)
|
||||||
|
const hasDev = split[1] === DocumentTypes.DEV
|
||||||
|
if ((hasDev && split.length === 3) || (!hasDev && split.length === 2)) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
if (hasDev) {
|
||||||
|
return split[2]
|
||||||
|
} else {
|
||||||
|
return split[1]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const setAppTenantId = appId => {
|
||||||
|
const appTenantId = this.getTenantIDFromAppID(appId) || this.DEFAULT_TENANT_ID
|
||||||
|
this.updateTenantId(appTenantId)
|
||||||
|
}
|
||||||
|
|
||||||
exports.doInAppContext = (appId, task) => {
|
exports.doInAppContext = (appId, task) => {
|
||||||
|
if (!appId) {
|
||||||
|
throw new Error("appId is required")
|
||||||
|
}
|
||||||
return cls.run(() => {
|
return cls.run(() => {
|
||||||
|
// set the app tenant id
|
||||||
|
setAppTenantId(appId)
|
||||||
|
|
||||||
// set the app ID
|
// set the app ID
|
||||||
cls.setOnContext(ContextKeys.APP_ID, appId)
|
cls.setOnContext(ContextKeys.APP_ID, appId)
|
||||||
|
|
||||||
|
|
|
@ -9,11 +9,7 @@ const {
|
||||||
APP_PREFIX,
|
APP_PREFIX,
|
||||||
APP_DEV,
|
APP_DEV,
|
||||||
} = require("./constants")
|
} = require("./constants")
|
||||||
const {
|
const { getTenantId, getGlobalDBName } = require("../tenancy")
|
||||||
getTenantId,
|
|
||||||
getTenantIDFromAppID,
|
|
||||||
getGlobalDBName,
|
|
||||||
} = require("../tenancy")
|
|
||||||
const fetch = require("node-fetch")
|
const fetch = require("node-fetch")
|
||||||
const { getCouch } = require("./index")
|
const { getCouch } = require("./index")
|
||||||
const { getAppMetadata } = require("../cache/appMetadata")
|
const { getAppMetadata } = require("../cache/appMetadata")
|
||||||
|
@ -39,7 +35,6 @@ exports.DocumentTypes = DocumentTypes
|
||||||
exports.APP_PREFIX = APP_PREFIX
|
exports.APP_PREFIX = APP_PREFIX
|
||||||
exports.APP_DEV = exports.APP_DEV_PREFIX = APP_DEV
|
exports.APP_DEV = exports.APP_DEV_PREFIX = APP_DEV
|
||||||
exports.SEPARATOR = SEPARATOR
|
exports.SEPARATOR = SEPARATOR
|
||||||
exports.getTenantIDFromAppID = getTenantIDFromAppID
|
|
||||||
exports.isDevApp = isDevApp
|
exports.isDevApp = isDevApp
|
||||||
exports.isProdAppID = isProdAppID
|
exports.isProdAppID = isProdAppID
|
||||||
exports.isDevAppID = isDevAppID
|
exports.isDevAppID = isDevAppID
|
||||||
|
|
|
@ -1,27 +0,0 @@
|
||||||
const {
|
|
||||||
isMultiTenant,
|
|
||||||
updateTenantId,
|
|
||||||
isTenantIdSet,
|
|
||||||
DEFAULT_TENANT_ID,
|
|
||||||
updateAppId,
|
|
||||||
} = require("../tenancy")
|
|
||||||
const ContextFactory = require("../context/FunctionContext")
|
|
||||||
const { getTenantIDFromAppID } = require("../db/utils")
|
|
||||||
|
|
||||||
module.exports = () => {
|
|
||||||
return ContextFactory.getMiddleware(ctx => {
|
|
||||||
// if not in multi-tenancy mode make sure its default and exit
|
|
||||||
if (!isMultiTenant()) {
|
|
||||||
updateTenantId(DEFAULT_TENANT_ID)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
// if tenant ID already set no need to continue
|
|
||||||
if (isTenantIdSet()) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
const appId = ctx.appId ? ctx.appId : ctx.user ? ctx.user.appId : null
|
|
||||||
const tenantId = getTenantIDFromAppID(appId) || DEFAULT_TENANT_ID
|
|
||||||
updateTenantId(tenantId)
|
|
||||||
updateAppId(appId)
|
|
||||||
})
|
|
||||||
}
|
|
|
@ -6,7 +6,6 @@ const { authError } = require("./passport/utils")
|
||||||
const authenticated = require("./authenticated")
|
const authenticated = require("./authenticated")
|
||||||
const auditLog = require("./auditLog")
|
const auditLog = require("./auditLog")
|
||||||
const tenancy = require("./tenancy")
|
const tenancy = require("./tenancy")
|
||||||
const appTenancy = require("./appTenancy")
|
|
||||||
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")
|
||||||
|
@ -19,7 +18,6 @@ module.exports = {
|
||||||
authenticated,
|
authenticated,
|
||||||
auditLog,
|
auditLog,
|
||||||
tenancy,
|
tenancy,
|
||||||
appTenancy,
|
|
||||||
authError,
|
authError,
|
||||||
internalApi,
|
internalApi,
|
||||||
datasource: {
|
datasource: {
|
||||||
|
|
|
@ -1,15 +1,29 @@
|
||||||
const google = require("../google")
|
const google = require("../google")
|
||||||
const { Cookies } = require("../../../constants")
|
const { Cookies, Configs } = require("../../../constants")
|
||||||
const { clearCookie, getCookie } = require("../../../utils")
|
const { clearCookie, getCookie } = require("../../../utils")
|
||||||
const { getDB } = require("../../../db")
|
const { getDB } = require("../../../db")
|
||||||
|
const { getScopedConfig } = require("../../../db/utils")
|
||||||
const environment = require("../../../environment")
|
const environment = require("../../../environment")
|
||||||
|
const { getGlobalDB } = require("../../../tenancy")
|
||||||
|
|
||||||
async function preAuth(passport, ctx, next) {
|
async function fetchGoogleCreds() {
|
||||||
// get the relevant config
|
// try and get the config from the tenant
|
||||||
const googleConfig = {
|
const db = getGlobalDB()
|
||||||
|
const googleConfig = await getScopedConfig(db, {
|
||||||
|
type: Configs.GOOGLE,
|
||||||
|
})
|
||||||
|
// or fall back to env variables
|
||||||
|
const config = googleConfig || {
|
||||||
clientID: environment.GOOGLE_CLIENT_ID,
|
clientID: environment.GOOGLE_CLIENT_ID,
|
||||||
clientSecret: environment.GOOGLE_CLIENT_SECRET,
|
clientSecret: environment.GOOGLE_CLIENT_SECRET,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return config
|
||||||
|
}
|
||||||
|
|
||||||
|
async function preAuth(passport, ctx, next) {
|
||||||
|
// get the relevant config
|
||||||
|
const googleConfig = await fetchGoogleCreds()
|
||||||
let callbackUrl = `${environment.PLATFORM_URL}/api/global/auth/datasource/google/callback`
|
let callbackUrl = `${environment.PLATFORM_URL}/api/global/auth/datasource/google/callback`
|
||||||
const strategy = await google.strategyFactory(googleConfig, callbackUrl)
|
const strategy = await google.strategyFactory(googleConfig, callbackUrl)
|
||||||
|
|
||||||
|
@ -26,10 +40,7 @@ async function preAuth(passport, ctx, next) {
|
||||||
|
|
||||||
async function postAuth(passport, ctx, next) {
|
async function postAuth(passport, ctx, next) {
|
||||||
// get the relevant config
|
// get the relevant config
|
||||||
const config = {
|
const config = await fetchGoogleCreds()
|
||||||
clientID: environment.GOOGLE_CLIENT_ID,
|
|
||||||
clientSecret: environment.GOOGLE_CLIENT_SECRET,
|
|
||||||
}
|
|
||||||
|
|
||||||
let callbackUrl = `${environment.PLATFORM_URL}/api/global/auth/datasource/google/callback`
|
let callbackUrl = `${environment.PLATFORM_URL}/api/global/auth/datasource/google/callback`
|
||||||
const strategy = await google.strategyFactory(
|
const strategy = await google.strategyFactory(
|
||||||
|
|
|
@ -51,7 +51,10 @@ exports.strategyFactory = async function (
|
||||||
)
|
)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err)
|
console.error(err)
|
||||||
throw new Error("Error constructing google authentication strategy", err)
|
throw new Error(
|
||||||
|
`Error constructing google authentication strategy: ${err}`,
|
||||||
|
err
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// expose for testing
|
// expose for testing
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
const { cloneDeep } = require("lodash/fp")
|
const { cloneDeep } = require("lodash/fp")
|
||||||
const { BUILTIN_PERMISSION_IDS } = require("./permissions")
|
const { BUILTIN_PERMISSION_IDS, PermissionLevels } = require("./permissions")
|
||||||
const {
|
const {
|
||||||
generateRoleID,
|
generateRoleID,
|
||||||
getRoleParams,
|
getRoleParams,
|
||||||
|
@ -180,6 +180,20 @@ exports.getUserRoleHierarchy = async (userRoleId, opts = { idOnly: true }) => {
|
||||||
return opts.idOnly ? roles.map(role => role._id) : roles
|
return opts.idOnly ? roles.map(role => role._id) : roles
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// this function checks that the provided permissions are in an array format
|
||||||
|
// some templates/older apps will use a simple string instead of array for roles
|
||||||
|
// convert the string to an array using the theory that write is higher than read
|
||||||
|
exports.checkForRoleResourceArray = (rolePerms, resourceId) => {
|
||||||
|
if (rolePerms && !Array.isArray(rolePerms[resourceId])) {
|
||||||
|
const permLevel = rolePerms[resourceId]
|
||||||
|
rolePerms[resourceId] = [permLevel]
|
||||||
|
if (permLevel === PermissionLevels.WRITE) {
|
||||||
|
rolePerms[resourceId].push(PermissionLevels.READ)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return rolePerms
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Given an app ID this will retrieve all of the roles that are currently within that app.
|
* Given an app ID this will retrieve all of the roles that are currently within that app.
|
||||||
* @return {Promise<object[]>} An array of the role objects that were found.
|
* @return {Promise<object[]>} An array of the role objects that were found.
|
||||||
|
@ -209,15 +223,27 @@ exports.getAllRoles = async appId => {
|
||||||
roles.push(Object.assign(builtinRole, dbBuiltin))
|
roles.push(Object.assign(builtinRole, dbBuiltin))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// check permissions
|
||||||
|
for (let role of roles) {
|
||||||
|
if (!role.permissions) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
for (let resourceId of Object.keys(role.permissions)) {
|
||||||
|
role.permissions = exports.checkForRoleResourceArray(
|
||||||
|
role.permissions,
|
||||||
|
resourceId
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
return roles
|
return roles
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This retrieves the required role
|
* This retrieves the required role for a resource
|
||||||
* @param permLevel
|
* @param permLevel The level of request
|
||||||
* @param resourceId
|
* @param resourceId The resource being requested
|
||||||
* @param subResourceId
|
* @param subResourceId The sub resource being requested
|
||||||
* @return {Promise<{permissions}|Object>}
|
* @return {Promise<{permissions}|Object>} returns the permissions required to access.
|
||||||
*/
|
*/
|
||||||
exports.getRequiredResourceRole = async (
|
exports.getRequiredResourceRole = async (
|
||||||
permLevel,
|
permLevel,
|
||||||
|
|
|
@ -1,6 +1,11 @@
|
||||||
const { getDB } = require("../db")
|
const { getDB } = require("../db")
|
||||||
const { SEPARATOR, StaticDatabases, DocumentTypes } = require("../db/constants")
|
const { SEPARATOR, StaticDatabases } = require("../db/constants")
|
||||||
const { getTenantId, DEFAULT_TENANT_ID, isMultiTenant } = require("../context")
|
const {
|
||||||
|
getTenantId,
|
||||||
|
DEFAULT_TENANT_ID,
|
||||||
|
isMultiTenant,
|
||||||
|
getTenantIDFromAppID,
|
||||||
|
} = require("../context")
|
||||||
const env = require("../environment")
|
const env = require("../environment")
|
||||||
|
|
||||||
const TENANT_DOC = StaticDatabases.PLATFORM_INFO.docs.tenants
|
const TENANT_DOC = StaticDatabases.PLATFORM_INFO.docs.tenants
|
||||||
|
@ -118,26 +123,6 @@ exports.getTenantUser = async identifier => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Given an app ID this will attempt to retrieve the tenant ID from it.
|
|
||||||
* @return {null|string} The tenant ID found within the app ID.
|
|
||||||
*/
|
|
||||||
exports.getTenantIDFromAppID = appId => {
|
|
||||||
if (!appId) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
const split = appId.split(SEPARATOR)
|
|
||||||
const hasDev = split[1] === DocumentTypes.DEV
|
|
||||||
if ((hasDev && split.length === 3) || (!hasDev && split.length === 2)) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
if (hasDev) {
|
|
||||||
return split[2]
|
|
||||||
} else {
|
|
||||||
return split[1]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
exports.isUserInAppTenant = (appId, user = null) => {
|
exports.isUserInAppTenant = (appId, user = null) => {
|
||||||
let userTenantId
|
let userTenantId
|
||||||
if (user) {
|
if (user) {
|
||||||
|
@ -145,7 +130,7 @@ exports.isUserInAppTenant = (appId, user = null) => {
|
||||||
} else {
|
} else {
|
||||||
userTenantId = getTenantId()
|
userTenantId = getTenantId()
|
||||||
}
|
}
|
||||||
const tenantId = exports.getTenantIDFromAppID(appId) || DEFAULT_TENANT_ID
|
const tenantId = getTenantIDFromAppID(appId) || DEFAULT_TENANT_ID
|
||||||
return tenantId === userTenantId
|
return tenantId === userTenantId
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -3,6 +3,7 @@ const {
|
||||||
SEPARATOR,
|
SEPARATOR,
|
||||||
ViewNames,
|
ViewNames,
|
||||||
generateGlobalUserID,
|
generateGlobalUserID,
|
||||||
|
getAllApps,
|
||||||
} = require("./db/utils")
|
} = require("./db/utils")
|
||||||
const jwt = require("jsonwebtoken")
|
const jwt = require("jsonwebtoken")
|
||||||
const { options } = require("./middleware/passport/jwt")
|
const { options } = require("./middleware/passport/jwt")
|
||||||
|
@ -20,8 +21,10 @@ const { hash } = require("./hashing")
|
||||||
const userCache = require("./cache/user")
|
const userCache = require("./cache/user")
|
||||||
const env = require("./environment")
|
const env = require("./environment")
|
||||||
const { getUserSessions, invalidateSessions } = require("./security/sessions")
|
const { getUserSessions, invalidateSessions } = require("./security/sessions")
|
||||||
|
const tenancy = require("./tenancy")
|
||||||
|
|
||||||
const APP_PREFIX = DocumentTypes.APP + SEPARATOR
|
const APP_PREFIX = DocumentTypes.APP + SEPARATOR
|
||||||
|
const PROD_APP_PREFIX = "/app/"
|
||||||
|
|
||||||
function confirmAppId(possibleAppId) {
|
function confirmAppId(possibleAppId) {
|
||||||
return possibleAppId && possibleAppId.startsWith(APP_PREFIX)
|
return possibleAppId && possibleAppId.startsWith(APP_PREFIX)
|
||||||
|
@ -29,16 +32,35 @@ function confirmAppId(possibleAppId) {
|
||||||
: undefined
|
: undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function resolveAppUrl(ctx) {
|
||||||
|
const appUrl = ctx.path.split("/")[2]
|
||||||
|
let possibleAppUrl = `/${appUrl.toLowerCase()}`
|
||||||
|
|
||||||
|
let tenantId = tenancy.getTenantId()
|
||||||
|
if (!env.SELF_HOSTED && ctx.subdomains.length) {
|
||||||
|
// always use the tenant id from the url in cloud
|
||||||
|
tenantId = ctx.subdomains[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
// search prod apps for a url that matches
|
||||||
|
const apps = await tenancy.doInTenant(tenantId, () =>
|
||||||
|
getAllApps({ dev: false })
|
||||||
|
)
|
||||||
|
const app = apps.filter(
|
||||||
|
a => a.url && a.url.toLowerCase() === possibleAppUrl
|
||||||
|
)[0]
|
||||||
|
|
||||||
|
return app && app.appId ? app.appId : undefined
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Given a request tries to find the appId, which can be located in various places
|
* Given a request tries to find the appId, which can be located in various places
|
||||||
* @param {object} ctx The main request body to look through.
|
* @param {object} ctx The main request body to look through.
|
||||||
* @returns {string|undefined} If an appId was found it will be returned.
|
* @returns {string|undefined} If an appId was found it will be returned.
|
||||||
*/
|
*/
|
||||||
exports.getAppId = ctx => {
|
exports.getAppIdFromCtx = async ctx => {
|
||||||
const options = [ctx.headers[Headers.APP_ID], ctx.params.appId]
|
// look in headers
|
||||||
if (ctx.subdomains) {
|
const options = [ctx.headers[Headers.APP_ID]]
|
||||||
options.push(ctx.subdomains[1])
|
|
||||||
}
|
|
||||||
let appId
|
let appId
|
||||||
for (let option of options) {
|
for (let option of options) {
|
||||||
appId = confirmAppId(option)
|
appId = confirmAppId(option)
|
||||||
|
@ -47,16 +69,24 @@ exports.getAppId = ctx => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// look in body if can't find it in subdomain
|
// look in body
|
||||||
if (!appId && ctx.request.body && ctx.request.body.appId) {
|
if (!appId && ctx.request.body && ctx.request.body.appId) {
|
||||||
appId = confirmAppId(ctx.request.body.appId)
|
appId = confirmAppId(ctx.request.body.appId)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// look in the url - dev app
|
||||||
let appPath =
|
let appPath =
|
||||||
ctx.request.headers.referrer ||
|
ctx.request.headers.referrer ||
|
||||||
ctx.path.split("/").filter(subPath => subPath.startsWith(APP_PREFIX))
|
ctx.path.split("/").filter(subPath => subPath.startsWith(APP_PREFIX))
|
||||||
if (!appId && appPath.length !== 0) {
|
if (!appId && appPath.length) {
|
||||||
appId = confirmAppId(appPath[0])
|
appId = confirmAppId(appPath[0])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// look in the url - prod app
|
||||||
|
if (!appId && ctx.path.startsWith(PROD_APP_PREFIX)) {
|
||||||
|
appId = confirmAppId(await resolveAppUrl(ctx))
|
||||||
|
}
|
||||||
|
|
||||||
return appId
|
return appId
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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.0.89-alpha.0",
|
"version": "1.0.104-alpha.0",
|
||||||
"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.0.89-alpha.0",
|
"@budibase/string-templates": "^1.0.104-alpha.0",
|
||||||
"@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",
|
||||||
|
|
|
@ -13,6 +13,7 @@
|
||||||
export let icon = undefined
|
export let icon = undefined
|
||||||
export let active = false
|
export let active = false
|
||||||
export let tooltip = undefined
|
export let tooltip = undefined
|
||||||
|
export let dataCy
|
||||||
|
|
||||||
let showTooltip = false
|
let showTooltip = false
|
||||||
</script>
|
</script>
|
||||||
|
@ -27,6 +28,7 @@
|
||||||
class:active
|
class:active
|
||||||
class="spectrum-Button spectrum-Button--size{size.toUpperCase()}"
|
class="spectrum-Button spectrum-Button--size{size.toUpperCase()}"
|
||||||
{disabled}
|
{disabled}
|
||||||
|
data-cy={dataCy}
|
||||||
on:click|preventDefault
|
on:click|preventDefault
|
||||||
on:mouseover={() => (showTooltip = true)}
|
on:mouseover={() => (showTooltip = true)}
|
||||||
on:focus={() => (showTooltip = true)}
|
on:focus={() => (showTooltip = true)}
|
||||||
|
|
|
@ -5,6 +5,7 @@
|
||||||
import Divider from "../Divider/Divider.svelte"
|
import Divider from "../Divider/Divider.svelte"
|
||||||
import Icon from "../Icon/Icon.svelte"
|
import Icon from "../Icon/Icon.svelte"
|
||||||
import Context from "../context"
|
import Context from "../context"
|
||||||
|
import ProgressCircle from "../ProgressCircle/ProgressCircle.svelte"
|
||||||
|
|
||||||
export let title = undefined
|
export let title = undefined
|
||||||
export let size = "S"
|
export let size = "S"
|
||||||
|
@ -102,6 +103,7 @@
|
||||||
<Button group secondary on:click={close}>{cancelText}</Button>
|
<Button group secondary on:click={close}>{cancelText}</Button>
|
||||||
{/if}
|
{/if}
|
||||||
{#if showConfirmButton}
|
{#if showConfirmButton}
|
||||||
|
<span class="confirm-wrap">
|
||||||
<Button
|
<Button
|
||||||
group
|
group
|
||||||
cta
|
cta
|
||||||
|
@ -109,8 +111,14 @@
|
||||||
disabled={confirmDisabled}
|
disabled={confirmDisabled}
|
||||||
on:click={confirm}
|
on:click={confirm}
|
||||||
>
|
>
|
||||||
|
{#if loading}
|
||||||
|
<ProgressCircle overBackground={true} size="S" />
|
||||||
|
{/if}
|
||||||
|
{#if !loading}
|
||||||
{confirmText}
|
{confirmText}
|
||||||
|
{/if}
|
||||||
</Button>
|
</Button>
|
||||||
|
</span>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
@ -169,4 +177,8 @@
|
||||||
.spectrum-Dialog-buttonGroup {
|
.spectrum-Dialog-buttonGroup {
|
||||||
padding-left: 0;
|
padding-left: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.confirm-wrap :global(.spectrum-Button-label) {
|
||||||
|
display: contents;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -56,6 +56,7 @@
|
||||||
$: if (!loading) loaded = true
|
$: if (!loading) loaded = true
|
||||||
$: fields = getFields(schema, showAutoColumns, autoSortColumns)
|
$: fields = getFields(schema, showAutoColumns, autoSortColumns)
|
||||||
$: rows = fields?.length ? data || [] : []
|
$: rows = fields?.length ? data || [] : []
|
||||||
|
$: totalRowCount = rows?.length || 0
|
||||||
$: visibleRowCount = getVisibleRowCount(
|
$: visibleRowCount = getVisibleRowCount(
|
||||||
loaded,
|
loaded,
|
||||||
height,
|
height,
|
||||||
|
@ -63,7 +64,12 @@
|
||||||
rowCount,
|
rowCount,
|
||||||
rowHeight
|
rowHeight
|
||||||
)
|
)
|
||||||
$: contentStyle = getContentStyle(visibleRowCount, rowCount, rowHeight)
|
$: heightStyle = getHeightStyle(
|
||||||
|
visibleRowCount,
|
||||||
|
rowCount,
|
||||||
|
totalRowCount,
|
||||||
|
rowHeight
|
||||||
|
)
|
||||||
$: sortedRows = sortRows(rows, sortColumn, sortOrder)
|
$: sortedRows = sortRows(rows, sortColumn, sortOrder)
|
||||||
$: gridStyle = getGridStyle(fields, schema, showEditColumn)
|
$: gridStyle = getGridStyle(fields, schema, showEditColumn)
|
||||||
$: showEditColumn = allowEditRows || allowSelectRows
|
$: showEditColumn = allowEditRows || allowSelectRows
|
||||||
|
@ -107,11 +113,16 @@
|
||||||
return Math.min(allRows, Math.ceil(height / rowHeight))
|
return Math.min(allRows, Math.ceil(height / rowHeight))
|
||||||
}
|
}
|
||||||
|
|
||||||
const getContentStyle = (visibleRows, rowCount, rowHeight) => {
|
const getHeightStyle = (
|
||||||
if (!rowCount || !visibleRows) {
|
visibleRowCount,
|
||||||
|
rowCount,
|
||||||
|
totalRowCount,
|
||||||
|
rowHeight
|
||||||
|
) => {
|
||||||
|
if (!rowCount || !visibleRowCount || totalRowCount <= rowCount) {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
return `height: ${headerHeight + visibleRows * rowHeight}px;`
|
return `height: ${headerHeight + visibleRowCount * rowHeight}px;`
|
||||||
}
|
}
|
||||||
|
|
||||||
const getGridStyle = (fields, schema, showEditColumn) => {
|
const getGridStyle = (fields, schema, showEditColumn) => {
|
||||||
|
@ -264,11 +275,11 @@
|
||||||
style={`--row-height: ${rowHeight}px; --header-height: ${headerHeight}px;`}
|
style={`--row-height: ${rowHeight}px; --header-height: ${headerHeight}px;`}
|
||||||
>
|
>
|
||||||
{#if !loaded}
|
{#if !loaded}
|
||||||
<div class="loading" style={contentStyle}>
|
<div class="loading" style={heightStyle}>
|
||||||
<ProgressCircle />
|
<ProgressCircle />
|
||||||
</div>
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
<div class="spectrum-Table" style={`${contentStyle}${gridStyle}`}>
|
<div class="spectrum-Table" style={`${heightStyle}${gridStyle}`}>
|
||||||
{#if fields.length}
|
{#if fields.length}
|
||||||
<div class="spectrum-Table-head">
|
<div class="spectrum-Table-head">
|
||||||
{#if showEditColumn}
|
{#if showEditColumn}
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -6,9 +6,13 @@ filterTests(['all'], () => {
|
||||||
cy.login()
|
cy.login()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
after(() => {
|
||||||
|
cy.deleteAllApps()
|
||||||
|
})
|
||||||
|
|
||||||
it("should change the icon and colour for an application", () => {
|
it("should change the icon and colour for an application", () => {
|
||||||
// Search for test application
|
// Search for test application
|
||||||
cy.searchForApplication("Cypress Tests")
|
cy.applicationInAppTable("Cypress Tests")
|
||||||
cy.get(".appTable")
|
cy.get(".appTable")
|
||||||
.within(() => {
|
.within(() => {
|
||||||
cy.get(".spectrum-Icon").eq(1).click()
|
cy.get(".spectrum-Icon").eq(1).click()
|
||||||
|
|
|
@ -2,11 +2,146 @@ import filterTests from '../support/filterTests'
|
||||||
|
|
||||||
filterTests(['smoke', 'all'], () => {
|
filterTests(['smoke', 'all'], () => {
|
||||||
context("Create an Application", () => {
|
context("Create an Application", () => {
|
||||||
it("should create a new application", () => {
|
|
||||||
|
beforeEach(() => {
|
||||||
cy.login()
|
cy.login()
|
||||||
cy.createTestApp()
|
|
||||||
cy.visit(`${Cypress.config().baseUrl}/builder`)
|
|
||||||
cy.contains("Cypress Tests").should("exist")
|
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it("should show the new user UI/UX", () => {
|
||||||
|
cy.visit(`${Cypress.config().baseUrl}/builder`)
|
||||||
|
cy.get(`[data-cy="create-app-btn"]`).contains('Start from scratch').should("exist")
|
||||||
|
cy.get(`[data-cy="import-app-btn"]`).should("exist")
|
||||||
|
|
||||||
|
cy.get(".template-category-filters").should("exist")
|
||||||
|
cy.get(".template-categories").should("exist")
|
||||||
|
|
||||||
|
cy.get(".appTable").should("not.exist")
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should provide filterable templates", () => {
|
||||||
|
cy.visit(`${Cypress.config().baseUrl}/builder`)
|
||||||
|
cy.wait(500)
|
||||||
|
|
||||||
|
cy.get(".template-category-filters").should("exist")
|
||||||
|
cy.get(".template-categories").should("exist")
|
||||||
|
|
||||||
|
cy.get(".template-category").its('length').should('be.gt', 1)
|
||||||
|
cy.get(".template-category-filters .spectrum-ActionButton").its('length').should('be.gt', 2)
|
||||||
|
|
||||||
|
cy.get(".template-category-filters .spectrum-ActionButton").eq(1).click()
|
||||||
|
cy.get(".template-category").should('have.length', 1)
|
||||||
|
|
||||||
|
cy.get(".template-category-filters .spectrum-ActionButton").eq(0).click()
|
||||||
|
cy.get(".template-category").its('length').should('be.gt', 1)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should enforce a valid url before submission", () => {
|
||||||
|
cy.visit(`${Cypress.config().baseUrl}/builder`)
|
||||||
|
cy.wait(500)
|
||||||
|
|
||||||
|
const appName = "A New App"
|
||||||
|
|
||||||
|
cy.get(`[data-cy="create-app-btn"]`).contains('Start from scratch').click({force: true})
|
||||||
|
cy.get(".spectrum-Modal").within(() => {
|
||||||
|
|
||||||
|
//Auto fill
|
||||||
|
cy.get("input").eq(0).type(appName).should("have.value", appName).blur()
|
||||||
|
cy.get("input").eq(1).should("have.value", "/a-new-app")
|
||||||
|
cy.get(".spectrum-ButtonGroup").contains("Create app").should('not.be.disabled')
|
||||||
|
|
||||||
|
//Empty the app url - disabled create
|
||||||
|
cy.get("input").eq(1).clear().blur()
|
||||||
|
cy.get(".spectrum-ButtonGroup").contains("Create app").should('be.disabled')
|
||||||
|
|
||||||
|
//Invalid url
|
||||||
|
cy.get("input").eq(1).type("/new app-url").blur()
|
||||||
|
cy.get(".spectrum-ButtonGroup").contains("Create app").should('be.disabled')
|
||||||
|
|
||||||
|
//Specifically alter the url
|
||||||
|
cy.get("input").eq(1).clear()
|
||||||
|
cy.get("input").eq(1).type("another-app-name").blur()
|
||||||
|
cy.get("input").eq(1).should("have.value", "/another-app-name")
|
||||||
|
cy.get("input").eq(0).should("have.value", appName)
|
||||||
|
cy.get(".spectrum-ButtonGroup").contains("Create app").should('not.be.disabled')
|
||||||
|
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should create the first application from scratch", () => {
|
||||||
|
const appName = "Cypress Tests"
|
||||||
|
cy.deleteApp(appName)
|
||||||
|
cy.createApp(appName, "This app is used for Cypress testing.")
|
||||||
|
|
||||||
|
cy.visit(`${Cypress.config().baseUrl}/builder`)
|
||||||
|
cy.wait(1000)
|
||||||
|
|
||||||
|
cy.applicationInAppTable(appName)
|
||||||
|
cy.deleteApp(appName)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should generate the first application from a template", () => {
|
||||||
|
cy.visit(`${Cypress.config().baseUrl}/builder`)
|
||||||
|
cy.wait(500)
|
||||||
|
|
||||||
|
cy.get(".template-category-filters").should("exist")
|
||||||
|
cy.get(".template-categories").should("exist")
|
||||||
|
|
||||||
|
//### Select nth template and choose to create?
|
||||||
|
cy.get('.template-category').eq(0).within(() => {
|
||||||
|
const card = cy.get('.template-card').eq(0).should("exist");
|
||||||
|
const cardOverlay = card.get('.template-thumbnail-action-overlay').should("exist")
|
||||||
|
cardOverlay.invoke("show")
|
||||||
|
cardOverlay.get("button").contains("Use template").should("exist").click({force: true})
|
||||||
|
})
|
||||||
|
|
||||||
|
//### CMD Create app from theme card
|
||||||
|
cy.get(".spectrum-Modal").should('be.visible')
|
||||||
|
|
||||||
|
const templateName = cy.get(".spectrum-Modal .template-thumbnail-text")
|
||||||
|
templateName.invoke('text')
|
||||||
|
.then(templateNameText => {
|
||||||
|
const templateNameParsed = "/"+templateNameText.toLowerCase().replace(/\s+/g, "-")
|
||||||
|
cy.get(".spectrum-Modal input").eq(0).should("have.value", templateNameText)
|
||||||
|
cy.get(".spectrum-Modal input").eq(1).should("have.value", templateNameParsed)
|
||||||
|
|
||||||
|
cy.get(".spectrum-Modal .spectrum-ButtonGroup").contains("Create app").click()
|
||||||
|
cy.wait(5000)
|
||||||
|
|
||||||
|
cy.visit(`${Cypress.config().baseUrl}/builder`)
|
||||||
|
cy.wait(1000)
|
||||||
|
|
||||||
|
cy.applicationInAppTable(templateNameText)
|
||||||
|
cy.deleteAllApps()
|
||||||
|
});
|
||||||
|
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should display a second application and app filtering", () => {
|
||||||
|
const appName = "Cypress Tests"
|
||||||
|
cy.deleteApp(appName)
|
||||||
|
cy.createApp(appName, "This app is used for Cypress testing.")
|
||||||
|
|
||||||
|
cy.visit(`${Cypress.config().baseUrl}/builder`)
|
||||||
|
cy.wait(500)
|
||||||
|
|
||||||
|
const secondAppName = "Second App Demo"
|
||||||
|
cy.deleteApp(secondAppName)
|
||||||
|
|
||||||
|
cy.get(`[data-cy="create-app-btn"]`).contains('Create new app').click({force: true})
|
||||||
|
cy.wait(500)
|
||||||
|
cy.url().should('include', '/builder/portal/apps/create')
|
||||||
|
|
||||||
|
cy.createAppFromScratch(secondAppName)
|
||||||
|
|
||||||
|
cy.visit(`${Cypress.config().baseUrl}/builder`)
|
||||||
|
cy.wait(500)
|
||||||
|
|
||||||
|
//Both applications should exist and be searchable
|
||||||
|
cy.searchForApplication(appName)
|
||||||
|
cy.searchForApplication(secondAppName)
|
||||||
|
|
||||||
|
cy.deleteAllApps()
|
||||||
|
})
|
||||||
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
|
@ -7,6 +7,10 @@ filterTests(["smoke", "all"], () => {
|
||||||
cy.createTestApp()
|
cy.createTestApp()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
after(() => {
|
||||||
|
cy.deleteAllApps()
|
||||||
|
})
|
||||||
|
|
||||||
it("should create a new Table", () => {
|
it("should create a new Table", () => {
|
||||||
cy.createTable("dog")
|
cy.createTable("dog")
|
||||||
cy.wait(1000)
|
cy.wait(1000)
|
||||||
|
|
|
@ -4,9 +4,14 @@ filterTests(["smoke", "all"], () => {
|
||||||
context("Create a User and Assign Roles", () => {
|
context("Create a User and Assign Roles", () => {
|
||||||
before(() => {
|
before(() => {
|
||||||
cy.login()
|
cy.login()
|
||||||
|
cy.visit(`${Cypress.config().baseUrl}/builder`)
|
||||||
|
cy.wait(500)
|
||||||
|
cy.createAppFromScratch("Initial App")
|
||||||
})
|
})
|
||||||
|
|
||||||
it("should create a user", () => {
|
it("should create a user", () => {
|
||||||
|
cy.visit(`${Cypress.config().baseUrl}/builder`)
|
||||||
|
cy.wait(1000)
|
||||||
cy.createUser("bbuser@test.com")
|
cy.createUser("bbuser@test.com")
|
||||||
cy.get(".spectrum-Table").should("contain", "bbuser")
|
cy.get(".spectrum-Table").should("contain", "bbuser")
|
||||||
})
|
})
|
||||||
|
@ -21,6 +26,7 @@ filterTests(["smoke", "all"], () => {
|
||||||
cy.get(".spectrum-Table").eq(0).contains("No rows found")
|
cy.get(".spectrum-Table").eq(0).contains("No rows found")
|
||||||
})
|
})
|
||||||
|
|
||||||
|
if (Cypress.env("TEST_ENV")) {
|
||||||
it("should assign role types", () => {
|
it("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`)
|
||||||
|
@ -30,7 +36,14 @@ filterTests(["smoke", "all"], () => {
|
||||||
for (let i = 1; i < 3; i++) {
|
for (let i = 1; i < 3; i++) {
|
||||||
const uuid = () => Cypress._.random(0, 1e6)
|
const uuid = () => Cypress._.random(0, 1e6)
|
||||||
const name = uuid()
|
const name = uuid()
|
||||||
|
if(i < 1){
|
||||||
cy.createApp(name)
|
cy.createApp(name)
|
||||||
|
} else {
|
||||||
|
cy.visit(`${Cypress.config().baseUrl}/builder`)
|
||||||
|
cy.wait(500)
|
||||||
|
cy.get(`[data-cy="create-app-btn"]`).click({ force: true })
|
||||||
|
cy.createAppFromScratch(name)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
@ -110,6 +123,7 @@ filterTests(["smoke", "all"], () => {
|
||||||
// Confirm Configure roles table no longer has any apps in it
|
// Confirm Configure roles table no longer has any apps in it
|
||||||
cy.get(".spectrum-Table").eq(0).contains("No rows found")
|
cy.get(".spectrum-Table").eq(0).contains("No rows found")
|
||||||
})
|
})
|
||||||
|
}
|
||||||
|
|
||||||
it("should enable Developer access", () => {
|
it("should enable Developer access", () => {
|
||||||
// Enable Developer access
|
// Enable Developer access
|
||||||
|
|
|
@ -4,6 +4,8 @@ filterTests(['smoke', 'all'], () => {
|
||||||
context("Create a View", () => {
|
context("Create a View", () => {
|
||||||
before(() => {
|
before(() => {
|
||||||
cy.login()
|
cy.login()
|
||||||
|
cy.deleteAllApps()
|
||||||
|
|
||||||
cy.createTestApp()
|
cy.createTestApp()
|
||||||
cy.createTable("data")
|
cy.createTable("data")
|
||||||
cy.addColumn("data", "group", "Text")
|
cy.addColumn("data", "group", "Text")
|
||||||
|
|
|
@ -4,6 +4,7 @@ filterTests(["smoke", "all"], () => {
|
||||||
context("REST Datasource Testing", () => {
|
context("REST Datasource Testing", () => {
|
||||||
before(() => {
|
before(() => {
|
||||||
cy.login()
|
cy.login()
|
||||||
|
cy.deleteAllApps()
|
||||||
cy.createTestApp()
|
cy.createTestApp()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
@ -4,7 +4,7 @@ filterTests(["smoke", "all"], () => {
|
||||||
context("Query Level Transformers", () => {
|
context("Query Level Transformers", () => {
|
||||||
before(() => {
|
before(() => {
|
||||||
cy.login()
|
cy.login()
|
||||||
cy.deleteApp("Cypress Tests")
|
cy.deleteAllApps()
|
||||||
cy.createApp("Cypress Tests")
|
cy.createApp("Cypress Tests")
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
@ -15,8 +15,8 @@ filterTests(['all'], () => {
|
||||||
renameApp(appName, appRename)
|
renameApp(appName, appRename)
|
||||||
cy.reload()
|
cy.reload()
|
||||||
cy.wait(1000)
|
cy.wait(1000)
|
||||||
cy.searchForApplication(appRename)
|
|
||||||
cy.get(".appTable").find(".title").should("have.length", 1)
|
cy.get(".appTable").find(".title").should("have.length", 1)
|
||||||
|
cy.applicationInAppTable(appRename)
|
||||||
// Set app name back to Cypress Tests
|
// Set app name back to Cypress Tests
|
||||||
cy.reload()
|
cy.reload()
|
||||||
cy.wait(1000)
|
cy.wait(1000)
|
||||||
|
@ -29,17 +29,17 @@ filterTests(['all'], () => {
|
||||||
const appRename = "Cypress Renamed"
|
const appRename = "Cypress Renamed"
|
||||||
// Publish the app
|
// Publish the app
|
||||||
cy.get(".toprightnav")
|
cy.get(".toprightnav")
|
||||||
cy.get(".spectrum-Button").contains("Publish").click({force: true})
|
cy.get(".spectrum-Button").contains("Publish").click({ force: true })
|
||||||
cy.get(".spectrum-Dialog-grid")
|
cy.get(".spectrum-Dialog-grid")
|
||||||
.within(() => {
|
.within(() => {
|
||||||
// Click publish again within the modal
|
// Click publish again within the modal
|
||||||
cy.get(".spectrum-Button").contains("Publish").click({force: true})
|
cy.get(".spectrum-Button").contains("Publish").click({ force: true })
|
||||||
})
|
})
|
||||||
// Rename app, Search for app, Confirm name was changed
|
// Rename app, Search for app, Confirm name was changed
|
||||||
cy.get(".home-logo").click()
|
cy.get(".home-logo").click()
|
||||||
renameApp(appName, appRename, true)
|
renameApp(appName, appRename, true)
|
||||||
cy.searchForApplication(appRename)
|
|
||||||
cy.get(".appTable").find(".wrapper").should("have.length", 1)
|
cy.get(".appTable").find(".wrapper").should("have.length", 1)
|
||||||
|
cy.applicationInAppTable(appRename)
|
||||||
})
|
})
|
||||||
|
|
||||||
it("Should try to rename an application to have no name", () => {
|
it("Should try to rename an application to have no name", () => {
|
||||||
|
@ -51,7 +51,7 @@ filterTests(['all'], () => {
|
||||||
cy.get(".spectrum-Dialog-grid").contains("Cancel").click()
|
cy.get(".spectrum-Dialog-grid").contains("Cancel").click()
|
||||||
cy.reload()
|
cy.reload()
|
||||||
cy.wait(1000)
|
cy.wait(1000)
|
||||||
cy.searchForApplication(appName)
|
cy.applicationInAppTable(appName)
|
||||||
cy.get(".appTable").find(".title").should("have.length", 1)
|
cy.get(".appTable").find(".title").should("have.length", 1)
|
||||||
|
|
||||||
})
|
})
|
||||||
|
@ -61,12 +61,12 @@ filterTests(['all'], () => {
|
||||||
const appName = "Cypress Tests"
|
const appName = "Cypress Tests"
|
||||||
cy.visit(`${Cypress.config().baseUrl}/builder`)
|
cy.visit(`${Cypress.config().baseUrl}/builder`)
|
||||||
cy.wait(500)
|
cy.wait(500)
|
||||||
cy.get(".spectrum-Button").contains("Create app").click({force: true})
|
cy.get(".spectrum-Button").contains("Create app").click({ force: true })
|
||||||
cy.contains(/Start from scratch/).click()
|
cy.contains(/Start from scratch/).click()
|
||||||
cy.get(".spectrum-Modal")
|
cy.get(".spectrum-Modal")
|
||||||
.within(() => {
|
.within(() => {
|
||||||
cy.get("input").eq(0).type(appName)
|
cy.get("input").eq(0).type(appName)
|
||||||
cy.get(".spectrum-ButtonGroup").contains("Create app").click({force: true})
|
cy.get(".spectrum-ButtonGroup").contains("Create app").click({ force: true })
|
||||||
cy.get(".error").should("have.text", "Another app with the same name already exists")
|
cy.get(".error").should("have.text", "Another app with the same name already exists")
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
@ -81,7 +81,7 @@ filterTests(['all'], () => {
|
||||||
renameApp(appName, numberName)
|
renameApp(appName, numberName)
|
||||||
cy.reload()
|
cy.reload()
|
||||||
cy.wait(1000)
|
cy.wait(1000)
|
||||||
cy.searchForApplication(numberName)
|
cy.applicationInAppTable(numberName)
|
||||||
cy.get(".appTable").find(".title").should("have.length", 1)
|
cy.get(".appTable").find(".title").should("have.length", 1)
|
||||||
cy.reload()
|
cy.reload()
|
||||||
cy.wait(1000)
|
cy.wait(1000)
|
||||||
|
@ -94,7 +94,7 @@ filterTests(['all'], () => {
|
||||||
})
|
})
|
||||||
|
|
||||||
const renameApp = (originalName, changedName, published, noName) => {
|
const renameApp = (originalName, changedName, published, noName) => {
|
||||||
cy.searchForApplication(originalName)
|
cy.applicationInAppTable(originalName)
|
||||||
cy.request(`${Cypress.config().baseUrl}/api/applications?status=all`)
|
cy.request(`${Cypress.config().baseUrl}/api/applications?status=all`)
|
||||||
.its("body")
|
.its("body")
|
||||||
.then(val => {
|
.then(val => {
|
||||||
|
@ -104,7 +104,7 @@ filterTests(['all'], () => {
|
||||||
cy.get(".spectrum-Icon").eq(1).click()
|
cy.get(".spectrum-Icon").eq(1).click()
|
||||||
})
|
})
|
||||||
// Check for when an app is published
|
// Check for when an app is published
|
||||||
if (published == true){
|
if (published == true) {
|
||||||
// Should not have Edit as option, will unpublish app
|
// Should not have Edit as option, will unpublish app
|
||||||
cy.should("not.have.value", "Edit")
|
cy.should("not.have.value", "Edit")
|
||||||
cy.get(".spectrum-Menu").contains("Unpublish").click()
|
cy.get(".spectrum-Menu").contains("Unpublish").click()
|
||||||
|
@ -114,7 +114,7 @@ filterTests(['all'], () => {
|
||||||
cy.contains("Edit").click()
|
cy.contains("Edit").click()
|
||||||
cy.get(".spectrum-Modal")
|
cy.get(".spectrum-Modal")
|
||||||
.within(() => {
|
.within(() => {
|
||||||
if (noName == true){
|
if (noName == true) {
|
||||||
cy.get("input").clear()
|
cy.get("input").clear()
|
||||||
cy.get(".spectrum-Dialog-grid").click()
|
cy.get(".spectrum-Dialog-grid").click()
|
||||||
.contains("App name must be letters, numbers and spaces only")
|
.contains("App name must be letters, numbers and spaces only")
|
||||||
|
@ -122,7 +122,7 @@ filterTests(['all'], () => {
|
||||||
}
|
}
|
||||||
cy.get("input").clear()
|
cy.get("input").clear()
|
||||||
cy.get("input").eq(0).type(changedName).should("have.value", changedName).blur()
|
cy.get("input").eq(0).type(changedName).should("have.value", changedName).blur()
|
||||||
cy.get(".spectrum-ButtonGroup").contains("Save").click({force: true})
|
cy.get(".spectrum-ButtonGroup").contains("Save").click({ force: true })
|
||||||
cy.wait(500)
|
cy.wait(500)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -35,7 +35,9 @@ Cypress.Commands.add("login", () => {
|
||||||
Cypress.Commands.add("createApp", name => {
|
Cypress.Commands.add("createApp", name => {
|
||||||
cy.visit(`${Cypress.config().baseUrl}/builder`)
|
cy.visit(`${Cypress.config().baseUrl}/builder`)
|
||||||
cy.wait(500)
|
cy.wait(500)
|
||||||
cy.get(".spectrum-Button").contains("Create app").click({ force: true })
|
|
||||||
|
cy.get(`[data-cy="create-app-btn"]`).click({ force: true })
|
||||||
|
|
||||||
cy.get(".spectrum-Modal").within(() => {
|
cy.get(".spectrum-Modal").within(() => {
|
||||||
cy.get("input").eq(0).type(name).should("have.value", name).blur()
|
cy.get("input").eq(0).type(name).should("have.value", name).blur()
|
||||||
cy.get(".spectrum-ButtonGroup").contains("Create app").click()
|
cy.get(".spectrum-ButtonGroup").contains("Create app").click()
|
||||||
|
@ -51,10 +53,23 @@ Cypress.Commands.add("deleteApp", name => {
|
||||||
.its("body")
|
.its("body")
|
||||||
.then(val => {
|
.then(val => {
|
||||||
if (val.length > 0) {
|
if (val.length > 0) {
|
||||||
cy.searchForApplication(name)
|
const appId = val.reduce((acc, app) => {
|
||||||
cy.get(".appTable").within(() => {
|
if (name === app.name) {
|
||||||
cy.get(".spectrum-Icon").eq(1).click()
|
acc = app.appId
|
||||||
|
}
|
||||||
|
return acc
|
||||||
|
}, "")
|
||||||
|
|
||||||
|
if (appId == "") {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const appIdParsed = appId.split("_").pop()
|
||||||
|
const actionEleId = `[data-cy=row_actions_${appIdParsed}]`
|
||||||
|
cy.get(actionEleId).within(() => {
|
||||||
|
cy.get(".spectrum-Icon").eq(0).click()
|
||||||
})
|
})
|
||||||
|
|
||||||
cy.get(".spectrum-Menu").then($menu => {
|
cy.get(".spectrum-Menu").then($menu => {
|
||||||
if ($menu.text().includes("Unpublish")) {
|
if ($menu.text().includes("Unpublish")) {
|
||||||
cy.get(".spectrum-Menu").contains("Unpublish").click()
|
cy.get(".spectrum-Menu").contains("Unpublish").click()
|
||||||
|
@ -80,22 +95,18 @@ Cypress.Commands.add("deleteAllApps", () => {
|
||||||
.its("body")
|
.its("body")
|
||||||
.then(val => {
|
.then(val => {
|
||||||
for (let i = 0; i < val.length; i++) {
|
for (let i = 0; i < val.length; i++) {
|
||||||
cy.get(".spectrum-Heading")
|
const appIdParsed = val[i].appId.split("_").pop()
|
||||||
.eq(1)
|
const actionEleId = `[data-cy=row_actions_${appIdParsed}]`
|
||||||
.then(app => {
|
cy.get(actionEleId).within(() => {
|
||||||
const name = app.text()
|
|
||||||
cy.get(".title")
|
|
||||||
.children()
|
|
||||||
.within(() => {
|
|
||||||
cy.get(".spectrum-Icon").eq(0).click()
|
cy.get(".spectrum-Icon").eq(0).click()
|
||||||
})
|
})
|
||||||
|
|
||||||
cy.get(".spectrum-Menu").contains("Delete").click()
|
cy.get(".spectrum-Menu").contains("Delete").click()
|
||||||
cy.get(".spectrum-Dialog-grid").within(() => {
|
cy.get(".spectrum-Dialog-grid").within(() => {
|
||||||
cy.get("input").type(name)
|
cy.get("input").type(val[i].name)
|
||||||
cy.get(".spectrum-Button--warning").click()
|
cy.get(".spectrum-Button--warning").click()
|
||||||
})
|
})
|
||||||
cy.reload()
|
cy.reload()
|
||||||
})
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
@ -190,9 +201,11 @@ Cypress.Commands.add("addRowMultiValue", values => {
|
||||||
Cypress.Commands.add("createUser", email => {
|
Cypress.Commands.add("createUser", email => {
|
||||||
// quick hacky recorded way to create a user
|
// quick hacky recorded way to create a user
|
||||||
cy.contains("Users").click()
|
cy.contains("Users").click()
|
||||||
cy.get(".spectrum-Button--primary").click()
|
cy.get(`[data-cy="add-user"]`).click()
|
||||||
cy.get(".spectrum-Picker-label").click()
|
cy.get(".spectrum-Picker-label").click()
|
||||||
cy.get(".spectrum-Menu-item:nth-child(2) > .spectrum-Menu-itemLabel").click()
|
cy.get(".spectrum-Menu-item:nth-child(2) > .spectrum-Menu-itemLabel").click()
|
||||||
|
|
||||||
|
//Onboarding type selector
|
||||||
cy.get(
|
cy.get(
|
||||||
":nth-child(2) > .spectrum-Form-itemField > .spectrum-Textfield > .spectrum-Textfield-input"
|
":nth-child(2) > .spectrum-Form-itemField > .spectrum-Textfield > .spectrum-Textfield-input"
|
||||||
)
|
)
|
||||||
|
@ -247,7 +260,7 @@ Cypress.Commands.add("createScreen", (screenName, route) => {
|
||||||
cy.get("[aria-label=AddCircle]").click()
|
cy.get("[aria-label=AddCircle]").click()
|
||||||
cy.get(".spectrum-Modal").within(() => {
|
cy.get(".spectrum-Modal").within(() => {
|
||||||
cy.get(".item").contains("Blank").click()
|
cy.get(".item").contains("Blank").click()
|
||||||
cy.get(".spectrum-Button").contains("Add Screens").click({ force: true })
|
cy.get(".spectrum-Button").contains("Add screens").click({ force: true })
|
||||||
cy.wait(500)
|
cy.wait(500)
|
||||||
})
|
})
|
||||||
cy.get(".spectrum-Dialog-grid").within(() => {
|
cy.get(".spectrum-Dialog-grid").within(() => {
|
||||||
|
@ -265,7 +278,7 @@ Cypress.Commands.add("createAutogeneratedScreens", screenNames => {
|
||||||
for (let i = 0; i < screenNames.length; i++) {
|
for (let i = 0; i < screenNames.length; i++) {
|
||||||
cy.get(".item").contains(screenNames[i]).click()
|
cy.get(".item").contains(screenNames[i]).click()
|
||||||
}
|
}
|
||||||
cy.get(".spectrum-Button").contains("Add Screens").click({ force: true })
|
cy.get(".spectrum-Button").contains("Add screens").click({ force: true })
|
||||||
cy.wait(4000)
|
cy.wait(4000)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -312,16 +325,37 @@ Cypress.Commands.add("addCustomSourceOptions", totalOptions => {
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
//Filters visible with 1 or more
|
||||||
Cypress.Commands.add("searchForApplication", appName => {
|
Cypress.Commands.add("searchForApplication", appName => {
|
||||||
cy.wait(1000)
|
cy.wait(1000)
|
||||||
// Searches for the app
|
// Searches for the app
|
||||||
cy.get(".filter").then(() => {
|
cy.get(".filter").then(() => {
|
||||||
cy.get(".spectrum-Textfield").within(() => {
|
cy.get(".spectrum-Textfield").within(() => {
|
||||||
|
cy.get("input").eq(0).clear()
|
||||||
cy.get("input").eq(0).type(appName)
|
cy.get("input").eq(0).type(appName)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
// Confirms app exists after search
|
// Confirms app exists after search
|
||||||
cy.get(".appTable").contains(appName)
|
cy.applicationInAppTable(appName)
|
||||||
|
})
|
||||||
|
|
||||||
|
//Assumes there are no others
|
||||||
|
Cypress.Commands.add("applicationInAppTable", appName => {
|
||||||
|
cy.get(".appTable").within(() => {
|
||||||
|
cy.get(".title").contains(appName).should("exist")
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
Cypress.Commands.add("createAppFromScratch", appName => {
|
||||||
|
cy.get(`[data-cy="create-app-btn"]`)
|
||||||
|
.contains("Start from scratch")
|
||||||
|
.click({ force: true })
|
||||||
|
cy.get(".spectrum-Modal").within(() => {
|
||||||
|
cy.get("input").eq(0).type(appName).should("have.value", appName).blur()
|
||||||
|
cy.get(".spectrum-ButtonGroup").contains("Create app").click()
|
||||||
|
cy.wait(10000)
|
||||||
|
})
|
||||||
|
cy.createTable("Cypress Tests", true)
|
||||||
})
|
})
|
||||||
|
|
||||||
Cypress.Commands.add("selectExternalDatasource", datasourceName => {
|
Cypress.Commands.add("selectExternalDatasource", datasourceName => {
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@budibase/builder",
|
"name": "@budibase/builder",
|
||||||
"version": "1.0.89-alpha.0",
|
"version": "1.0.104-alpha.0",
|
||||||
"license": "GPL-3.0",
|
"license": "GPL-3.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
@ -65,10 +65,10 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@budibase/bbui": "^1.0.89-alpha.0",
|
"@budibase/bbui": "^1.0.104-alpha.0",
|
||||||
"@budibase/client": "^1.0.89-alpha.0",
|
"@budibase/client": "^1.0.104-alpha.0",
|
||||||
"@budibase/frontend-core": "^1.0.89-alpha.0",
|
"@budibase/frontend-core": "^1.0.104-alpha.0",
|
||||||
"@budibase/string-templates": "^1.0.89-alpha.0",
|
"@budibase/string-templates": "^1.0.104-alpha.0",
|
||||||
"@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",
|
||||||
|
|
|
@ -1,4 +1,10 @@
|
||||||
import { store } from "./index"
|
import { store } from "./index"
|
||||||
|
import { Helpers } from "@budibase/bbui"
|
||||||
|
import {
|
||||||
|
decodeJSBinding,
|
||||||
|
encodeJSBinding,
|
||||||
|
findHBSBlocks,
|
||||||
|
} from "@budibase/string-templates"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Recursively searches for a specific component ID
|
* Recursively searches for a specific component ID
|
||||||
|
@ -161,3 +167,58 @@ export const getComponentSettings = componentType => {
|
||||||
|
|
||||||
return settings
|
return settings
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Randomises a components ID's, including all child component IDs, and also
|
||||||
|
* updates all data bindings to still be valid.
|
||||||
|
* This mutates the object in place.
|
||||||
|
* @param component the component to randomise
|
||||||
|
*/
|
||||||
|
export const makeComponentUnique = component => {
|
||||||
|
if (!component) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Replace component ID
|
||||||
|
const oldId = component._id
|
||||||
|
const newId = Helpers.uuid()
|
||||||
|
component._id = newId
|
||||||
|
|
||||||
|
if (component._children?.length) {
|
||||||
|
let children = JSON.stringify(component._children)
|
||||||
|
|
||||||
|
// Replace all instances of this ID in child HBS bindings
|
||||||
|
children = children.replace(new RegExp(oldId, "g"), newId)
|
||||||
|
|
||||||
|
// Replace all instances of this ID in child JS bindings
|
||||||
|
const bindings = findHBSBlocks(children)
|
||||||
|
bindings.forEach(binding => {
|
||||||
|
// JSON.stringify will have escaped double quotes, so we need
|
||||||
|
// to account for that
|
||||||
|
let sanitizedBinding = binding.replace(/\\"/g, '"')
|
||||||
|
|
||||||
|
// Check if this is a valid JS binding
|
||||||
|
let js = decodeJSBinding(sanitizedBinding)
|
||||||
|
if (js != null) {
|
||||||
|
// Replace ID inside JS binding
|
||||||
|
js = js.replace(new RegExp(oldId, "g"), newId)
|
||||||
|
|
||||||
|
// Create new valid JS binding
|
||||||
|
let newBinding = encodeJSBinding(js)
|
||||||
|
|
||||||
|
// Replace escaped double quotes
|
||||||
|
newBinding = newBinding.replace(/"/g, '\\"')
|
||||||
|
|
||||||
|
// Insert new JS back into binding.
|
||||||
|
// A single string replace here is better than a regex as
|
||||||
|
// the binding contains special characters, and we only need
|
||||||
|
// to replace a single instance.
|
||||||
|
children = children.replace(binding, newBinding)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Recurse on all children
|
||||||
|
component._children = JSON.parse(children)
|
||||||
|
component._children.forEach(makeComponentUnique)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -126,7 +126,7 @@ export const getDatasourceForProvider = (asset, component) => {
|
||||||
if (dataProviderSetting) {
|
if (dataProviderSetting) {
|
||||||
const settingValue = component[dataProviderSetting.key]
|
const settingValue = component[dataProviderSetting.key]
|
||||||
const providerId = extractLiteralHandlebarsID(settingValue)
|
const providerId = extractLiteralHandlebarsID(settingValue)
|
||||||
const provider = findComponent(asset.props, providerId)
|
const provider = findComponent(asset?.props, providerId)
|
||||||
return getDatasourceForProvider(asset, provider)
|
return getDatasourceForProvider(asset, provider)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -393,18 +393,45 @@ const getUrlBindings = asset => {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets all bindable properties exposed in a button actions flow up until
|
* Gets all bindable properties exposed in a button actions flow up until
|
||||||
* the specified action ID.
|
* the specified action ID, as well as context provided for the action
|
||||||
|
* setting as a whole by the component.
|
||||||
*/
|
*/
|
||||||
export const getButtonContextBindings = (actions, actionId) => {
|
export const getButtonContextBindings = (
|
||||||
|
asset,
|
||||||
|
componentId,
|
||||||
|
settingKey,
|
||||||
|
actions,
|
||||||
|
actionId
|
||||||
|
) => {
|
||||||
|
let bindings = []
|
||||||
|
|
||||||
|
// Check if any context bindings are provided by the component for this
|
||||||
|
// setting
|
||||||
|
const component = findComponent(asset.props, componentId)
|
||||||
|
const settings = getComponentSettings(component?._component)
|
||||||
|
const eventSetting = settings.find(setting => setting.key === settingKey)
|
||||||
|
if (!eventSetting) {
|
||||||
|
return bindings
|
||||||
|
}
|
||||||
|
if (eventSetting.context?.length) {
|
||||||
|
eventSetting.context.forEach(contextEntry => {
|
||||||
|
bindings.push({
|
||||||
|
readableBinding: contextEntry.label,
|
||||||
|
runtimeBinding: `${makePropSafe("eventContext")}.${makePropSafe(
|
||||||
|
contextEntry.key
|
||||||
|
)}`,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// Get the steps leading up to this value
|
// Get the steps leading up to this value
|
||||||
const index = actions?.findIndex(action => action.id === actionId)
|
const index = actions?.findIndex(action => action.id === actionId)
|
||||||
if (index == null || index === -1) {
|
if (index == null || index === -1) {
|
||||||
return []
|
return bindings
|
||||||
}
|
}
|
||||||
const prevActions = actions.slice(0, index)
|
const prevActions = actions.slice(0, index)
|
||||||
|
|
||||||
// Generate bindings for any steps which provide context
|
// Generate bindings for any steps which provide context
|
||||||
let bindings = []
|
|
||||||
prevActions.forEach((action, idx) => {
|
prevActions.forEach((action, idx) => {
|
||||||
const def = ActionDefinitions.actions.find(
|
const def = ActionDefinitions.actions.find(
|
||||||
x => x.name === action["##eventHandlerType"]
|
x => x.name === action["##eventHandlerType"]
|
||||||
|
@ -418,6 +445,7 @@ export const getButtonContextBindings = (actions, actionId) => {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
return bindings
|
return bindings
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -458,7 +486,7 @@ export const getSchemaForDatasource = (asset, datasource, options) => {
|
||||||
// Determine the entity which backs this datasource.
|
// Determine the entity which backs this datasource.
|
||||||
// "provider" datasources are those targeting another data provider
|
// "provider" datasources are those targeting another data provider
|
||||||
if (type === "provider") {
|
if (type === "provider") {
|
||||||
const component = findComponent(asset.props, datasource.providerId)
|
const component = findComponent(asset?.props, datasource.providerId)
|
||||||
const source = getDatasourceForProvider(asset, component)
|
const source = getDatasourceForProvider(asset, component)
|
||||||
return getSchemaForDatasource(asset, source, options)
|
return getSchemaForDatasource(asset, source, options)
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,7 +3,7 @@ import { getAutomationStore } from "./store/automation"
|
||||||
import { getThemeStore } from "./store/theme"
|
import { getThemeStore } from "./store/theme"
|
||||||
import { derived, writable } from "svelte/store"
|
import { derived, writable } from "svelte/store"
|
||||||
import { FrontendTypes, LAYOUT_NAMES } from "../constants"
|
import { FrontendTypes, LAYOUT_NAMES } from "../constants"
|
||||||
import { findComponent } from "./componentUtils"
|
import { findComponent, findComponentPath } from "./componentUtils"
|
||||||
|
|
||||||
export const store = getFrontendStore()
|
export const store = getFrontendStore()
|
||||||
export const automationStore = getAutomationStore()
|
export const automationStore = getAutomationStore()
|
||||||
|
@ -25,7 +25,17 @@ export const selectedComponent = derived(
|
||||||
if (!$currentAsset || !$store.selectedComponentId) {
|
if (!$currentAsset || !$store.selectedComponentId) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
return findComponent($currentAsset.props, $store.selectedComponentId)
|
return findComponent($currentAsset?.props, $store.selectedComponentId)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
export const selectedComponentPath = derived(
|
||||||
|
[store, currentAsset],
|
||||||
|
([$store, $currentAsset]) => {
|
||||||
|
return findComponentPath(
|
||||||
|
$currentAsset?.props,
|
||||||
|
$store.selectedComponentId
|
||||||
|
).map(component => component._id)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -24,9 +24,9 @@ import {
|
||||||
findAllMatchingComponents,
|
findAllMatchingComponents,
|
||||||
findComponent,
|
findComponent,
|
||||||
getComponentSettings,
|
getComponentSettings,
|
||||||
|
makeComponentUnique,
|
||||||
} from "../componentUtils"
|
} from "../componentUtils"
|
||||||
import { Helpers } from "@budibase/bbui"
|
import { Helpers } from "@budibase/bbui"
|
||||||
import { removeBindings } from "../dataBinding"
|
|
||||||
|
|
||||||
const INITIAL_FRONTEND_STATE = {
|
const INITIAL_FRONTEND_STATE = {
|
||||||
apps: [],
|
apps: [],
|
||||||
|
@ -45,6 +45,7 @@ const INITIAL_FRONTEND_STATE = {
|
||||||
customThemes: false,
|
customThemes: false,
|
||||||
devicePreview: false,
|
devicePreview: false,
|
||||||
messagePassing: false,
|
messagePassing: false,
|
||||||
|
continueIfAction: false,
|
||||||
},
|
},
|
||||||
currentFrontEndType: "none",
|
currentFrontEndType: "none",
|
||||||
selectedScreenId: "",
|
selectedScreenId: "",
|
||||||
|
@ -400,11 +401,11 @@ export const getFrontendStore = () => {
|
||||||
parentComponent = selected
|
parentComponent = selected
|
||||||
} else {
|
} else {
|
||||||
// Otherwise we need to use the parent of this component
|
// Otherwise we need to use the parent of this component
|
||||||
parentComponent = findComponentParent(asset.props, selected._id)
|
parentComponent = findComponentParent(asset?.props, selected._id)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Use screen or layout if no component is selected
|
// Use screen or layout if no component is selected
|
||||||
parentComponent = asset.props
|
parentComponent = asset?.props
|
||||||
}
|
}
|
||||||
|
|
||||||
// Attach component
|
// Attach component
|
||||||
|
@ -490,37 +491,22 @@ export const getFrontendStore = () => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
paste: async (targetComponent, mode, preserveBindings = false) => {
|
paste: async (targetComponent, mode) => {
|
||||||
let promises = []
|
let promises = []
|
||||||
store.update(state => {
|
store.update(state => {
|
||||||
// Stop if we have nothing to paste
|
// Stop if we have nothing to paste
|
||||||
if (!state.componentToPaste) {
|
if (!state.componentToPaste) {
|
||||||
return state
|
return state
|
||||||
}
|
}
|
||||||
|
|
||||||
// defines if this is a copy or a cut
|
|
||||||
const cut = state.componentToPaste.isCut
|
const cut = state.componentToPaste.isCut
|
||||||
|
|
||||||
// immediately need to remove bindings, currently these aren't valid when pasted
|
// Clone the component to paste and make unique if copying
|
||||||
if (!cut && !preserveBindings) {
|
|
||||||
state.componentToPaste = removeBindings(state.componentToPaste, "")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Clone the component to paste
|
|
||||||
// Retain the same ID if cutting as things may be referencing this component
|
|
||||||
delete state.componentToPaste.isCut
|
delete state.componentToPaste.isCut
|
||||||
let componentToPaste = cloneDeep(state.componentToPaste)
|
let componentToPaste = cloneDeep(state.componentToPaste)
|
||||||
if (cut) {
|
if (cut) {
|
||||||
state.componentToPaste = null
|
state.componentToPaste = null
|
||||||
} else {
|
} else {
|
||||||
const randomizeIds = component => {
|
makeComponentUnique(componentToPaste)
|
||||||
if (!component) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
component._id = Helpers.uuid()
|
|
||||||
component._children?.forEach(randomizeIds)
|
|
||||||
}
|
|
||||||
randomizeIds(componentToPaste)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (mode === "inside") {
|
if (mode === "inside") {
|
||||||
|
|
|
@ -10,17 +10,18 @@ const allTemplates = tables => [
|
||||||
]
|
]
|
||||||
|
|
||||||
// Allows us to apply common behaviour to all create() functions
|
// Allows us to apply common behaviour to all create() functions
|
||||||
const createTemplateOverride = (frontendState, create) => () => {
|
const createTemplateOverride = (frontendState, template) => () => {
|
||||||
const screen = create()
|
const screen = template.create()
|
||||||
screen.name = screen.props._id
|
screen.name = screen.props._id
|
||||||
screen.routing.route = screen.routing.route.toLowerCase()
|
screen.routing.route = screen.routing.route.toLowerCase()
|
||||||
|
screen.template = template.id
|
||||||
return screen
|
return screen
|
||||||
}
|
}
|
||||||
|
|
||||||
export default (frontendState, tables) => {
|
export default (frontendState, tables) => {
|
||||||
const enrichTemplate = template => ({
|
const enrichTemplate = template => ({
|
||||||
...template,
|
...template,
|
||||||
create: createTemplateOverride(frontendState, template.create),
|
create: createTemplateOverride(frontendState, template),
|
||||||
})
|
})
|
||||||
|
|
||||||
const fromScratch = enrichTemplate(createFromScratchScreen)
|
const fromScratch = enrichTemplate(createFromScratchScreen)
|
||||||
|
|
|
@ -2,9 +2,10 @@ import { 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"],
|
options: ["lightest", "light", "dark", "darkest", "nord"],
|
||||||
}
|
}
|
||||||
const store = createLocalStorageStore("bb-theme", initialValue)
|
const store = createLocalStorageStore("bb-theme", initialValue)
|
||||||
|
|
||||||
|
|
|
@ -10,12 +10,10 @@
|
||||||
<div class="title">
|
<div class="title">
|
||||||
<Tabs selected="Automations">
|
<Tabs selected="Automations">
|
||||||
<Tab title="Automations">
|
<Tab title="Automations">
|
||||||
<div class="tab-content-padding">
|
|
||||||
<AutomationList />
|
<AutomationList />
|
||||||
<Modal bind:this={modal}>
|
<Modal bind:this={modal}>
|
||||||
<CreateAutomationModal {webhookModal} />
|
<CreateAutomationModal {webhookModal} />
|
||||||
</Modal>
|
</Modal>
|
||||||
</div>
|
|
||||||
</Tab>
|
</Tab>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
<div class="add-button" data-cy="new-screen">
|
<div class="add-button" data-cy="new-screen">
|
||||||
|
@ -24,9 +22,6 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.tab-content-padding {
|
|
||||||
padding: 0 var(--spacing-xl);
|
|
||||||
}
|
|
||||||
.add-button {
|
.add-button {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: var(--spacing-l);
|
top: var(--spacing-l);
|
||||||
|
|
|
@ -56,7 +56,7 @@
|
||||||
<a
|
<a
|
||||||
slot="footer"
|
slot="footer"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
href="https://docs.budibase.com/automate/introduction-to-automate"
|
href="https://docs.budibase.com/docs/automation-steps"
|
||||||
>
|
>
|
||||||
<Icon name="InfoOutline" />
|
<Icon name="InfoOutline" />
|
||||||
<span>Learn about automations</span>
|
<span>Learn about automations</span>
|
||||||
|
|
|
@ -162,7 +162,7 @@
|
||||||
<Select
|
<Select
|
||||||
on:change={e => onChange(e, key)}
|
on:change={e => onChange(e, key)}
|
||||||
value={inputData[key]}
|
value={inputData[key]}
|
||||||
options={Object.keys(table.schema)}
|
options={Object.keys(table?.schema || {})}
|
||||||
/>
|
/>
|
||||||
{:else if value.customType === "filters"}
|
{:else if value.customType === "filters"}
|
||||||
<ActionButton on:click={drawer.show}>Define filters</ActionButton>
|
<ActionButton on:click={drawer.show}>Define filters</ActionButton>
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
<script>
|
<script>
|
||||||
import { Input, Select } from "@budibase/bbui"
|
import { Input, Select, Button } from "@budibase/bbui"
|
||||||
import { createEventDispatcher } from "svelte"
|
import { createEventDispatcher } from "svelte"
|
||||||
|
|
||||||
const dispatch = createEventDispatcher()
|
const dispatch = createEventDispatcher()
|
||||||
|
@ -62,9 +62,6 @@
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="root">
|
<div class="root">
|
||||||
<div class="add-field">
|
|
||||||
<i class="ri-add-line" on:click={addField} />
|
|
||||||
</div>
|
|
||||||
<div class="spacer" />
|
<div class="spacer" />
|
||||||
{#each fieldsArray as field}
|
{#each fieldsArray as field}
|
||||||
<div class="field">
|
<div class="field">
|
||||||
|
@ -88,6 +85,7 @@
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{/each}
|
{/each}
|
||||||
|
<Button quiet secondary icon="Add" on:click={addField}>Add field</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
@ -103,52 +101,11 @@
|
||||||
|
|
||||||
.field {
|
.field {
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
background-color: var(--grey-2);
|
|
||||||
margin-bottom: var(--spacing-m);
|
margin-bottom: var(--spacing-m);
|
||||||
border-style: solid;
|
|
||||||
border-width: 1px;
|
|
||||||
border-color: var(--grey-4);
|
|
||||||
display: grid;
|
display: grid;
|
||||||
/*grid-template-rows: auto auto;
|
grid-template-columns: 1fr 1fr auto;
|
||||||
grid-template-columns: auto;*/
|
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
align-items: center;
|
||||||
|
gap: var(--spacing-m);
|
||||||
.field :global(select) {
|
|
||||||
padding: var(--spacing-xs) 2rem var(--spacing-m) var(--spacing-s) !important;
|
|
||||||
font-size: var(--font-size-xs);
|
|
||||||
color: var(--grey-7);
|
|
||||||
}
|
|
||||||
|
|
||||||
.field :global(.pointer) {
|
|
||||||
padding-bottom: var(--spacing-m) !important;
|
|
||||||
color: var(--grey-2);
|
|
||||||
}
|
|
||||||
|
|
||||||
.field :global(input) {
|
|
||||||
padding: var(--spacing-m) var(--spacing-xl) var(--spacing-xs)
|
|
||||||
var(--spacing-m);
|
|
||||||
font-size: var(--font-size-s);
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
|
|
||||||
.remove-field {
|
|
||||||
cursor: pointer;
|
|
||||||
color: var(--grey-6);
|
|
||||||
position: absolute;
|
|
||||||
top: var(--spacing-m);
|
|
||||||
right: 3px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.remove-field:hover {
|
|
||||||
color: var(--black);
|
|
||||||
}
|
|
||||||
|
|
||||||
.add-field {
|
|
||||||
text-align: right;
|
|
||||||
}
|
|
||||||
|
|
||||||
.add-field > i {
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -69,7 +69,7 @@
|
||||||
<a
|
<a
|
||||||
slot="footer"
|
slot="footer"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
href="https://docs.budibase.com/automate/steps/triggers"
|
href="https://docs.budibase.com/docs/trigger"
|
||||||
>
|
>
|
||||||
<Icon name="InfoOutline" />
|
<Icon name="InfoOutline" />
|
||||||
<span>Learn about webhooks</span>
|
<span>Learn about webhooks</span>
|
||||||
|
|
|
@ -12,7 +12,7 @@
|
||||||
Modal,
|
Modal,
|
||||||
notifications,
|
notifications,
|
||||||
} from "@budibase/bbui"
|
} from "@budibase/bbui"
|
||||||
import { createEventDispatcher } from "svelte"
|
import { createEventDispatcher, onMount } from "svelte"
|
||||||
import { cloneDeep } from "lodash/fp"
|
import { cloneDeep } from "lodash/fp"
|
||||||
import { tables } from "stores/backend"
|
import { tables } from "stores/backend"
|
||||||
import { TableNames, UNEDITABLE_USER_FIELDS } from "constants"
|
import { TableNames, UNEDITABLE_USER_FIELDS } from "constants"
|
||||||
|
@ -321,6 +321,12 @@
|
||||||
}
|
}
|
||||||
return newError
|
return newError
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
if (primaryDisplay) {
|
||||||
|
field.constraints.presence = { allowEmpty: false }
|
||||||
|
}
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<ModalContent
|
<ModalContent
|
||||||
|
|
|
@ -22,10 +22,11 @@
|
||||||
const selected = $datasources.selected === datasource._id
|
const selected = $datasources.selected === datasource._id
|
||||||
const open = openDataSources.includes(datasource._id)
|
const open = openDataSources.includes(datasource._id)
|
||||||
const containsSelected = containsActiveEntity(datasource)
|
const containsSelected = containsActiveEntity(datasource)
|
||||||
|
const onlySource = $datasources.list.length === 1
|
||||||
return {
|
return {
|
||||||
...datasource,
|
...datasource,
|
||||||
selected,
|
selected,
|
||||||
open: selected || open || containsSelected,
|
open: selected || open || containsSelected || onlySource,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
: []
|
: []
|
||||||
|
|
|
@ -15,6 +15,7 @@
|
||||||
import ArrayRenderer from "components/common/renderers/ArrayRenderer.svelte"
|
import ArrayRenderer from "components/common/renderers/ArrayRenderer.svelte"
|
||||||
import ConfirmDialog from "components/common/ConfirmDialog.svelte"
|
import ConfirmDialog from "components/common/ConfirmDialog.svelte"
|
||||||
import { goto } from "@roxi/routify"
|
import { goto } from "@roxi/routify"
|
||||||
|
import GoogleButton from "../_components/GoogleButton.svelte"
|
||||||
|
|
||||||
export let datasource
|
export let datasource
|
||||||
export let save
|
export let save
|
||||||
|
@ -160,6 +161,11 @@
|
||||||
Fetch tables
|
Fetch tables
|
||||||
</Button>
|
</Button>
|
||||||
<Button cta icon="Add" on:click={createNewTable}>New table</Button>
|
<Button cta icon="Add" on:click={createNewTable}>New table</Button>
|
||||||
|
{#if integration.auth}
|
||||||
|
{#if integration.auth.type === "google"}
|
||||||
|
<GoogleButton {datasource} />
|
||||||
|
{/if}
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Body>
|
<Body>
|
||||||
|
|
|
@ -0,0 +1,54 @@
|
||||||
|
<script>
|
||||||
|
export let width = "100"
|
||||||
|
export let height = "100"
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||||
|
viewBox="23 6 469 132"
|
||||||
|
{width}
|
||||||
|
{height}
|
||||||
|
>
|
||||||
|
<defs id="defs202">
|
||||||
|
<linearGradient id="a" x1="-3.49%" x2="100.83%" y1="17.02%" y2="92.9%">
|
||||||
|
<stop offset="0%" stop-color="#fff" stop-opacity=".1" id="stop192" />
|
||||||
|
<stop offset="14%" stop-color="#fff" stop-opacity=".08" id="stop194" />
|
||||||
|
<stop offset="61%" stop-color="#fff" stop-opacity=".02" id="stop196" />
|
||||||
|
<stop offset="100%" stop-color="#fff" stop-opacity="0" id="stop198" />
|
||||||
|
</linearGradient>
|
||||||
|
<path
|
||||||
|
id="b"
|
||||||
|
d="M106.687 35.2742c-.186-1.0977-.967-2-2.0244-2.338s-2.2148-.057-3.0002.73L86.2473 49.166l-12.12-23.1455c-.5133-.9786-1.525-1.5914-2.6273-1.5914s-2.114.6128-2.6273 1.5914l-6.6277 12.656L45.62 7.5726c-.603-1.1297-1.8588-1.746-3.118-1.5297s-2.2394 1.216-2.4335 2.4827L24 111.701l42.9727 24.1654c2.6985 1.5113 5.985 1.5113 8.6836 0L119 111.701l-12.313-76.427z"
|
||||||
|
/>
|
||||||
|
</defs>
|
||||||
|
<g id="g305" transform="matrix(2.9011579,0,0,2.9011579,43.533284,-135.93685)">
|
||||||
|
<path
|
||||||
|
fill="#ffa000"
|
||||||
|
d="M 23.8266,111.7182 39.9588,8.4901 c 0.1972,-1.266 1.1818,-2.264 2.445,-2.4786 1.2632,-0.2146 2.522,0.4028 3.126,1.5327 L 62.2133,38.6615 68.8633,26 c 0.515,-0.979 1.5303,-1.592 2.6366,-1.592 1.1063,0 2.1215,0.613 2.6366,1.592 l 45.0227,85.718 H 23.8266 Z"
|
||||||
|
id="path204"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
fill="#f57c00"
|
||||||
|
d="M 79.566,71.5074 62.2124,38.6472 23.8334,111.7187 Z"
|
||||||
|
id="path206"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
fill="#ffca28"
|
||||||
|
d="m 119.1666,111.7187 -12.356,-76.4603 c -0.1867,-1.098 -0.9703,-2 -2.0315,-2.34 -1.0612,-0.34 -2.2226,-0.057 -3.0107,0.7302 l -77.935,78.069 43.1234,24.1834 c 2.708,1.512 6.006,1.512 8.714,0 l 43.4958,-24.1834 z"
|
||||||
|
id="path208"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
fill="#ffffff"
|
||||||
|
fill-opacity="0.2"
|
||||||
|
d="m 106.8105,35.2584 c -0.1867,-1.098 -0.9703,-2 -2.0315,-2.34 -1.0612,-0.34 -2.2226,-0.057 -3.0107,0.7302 L 86.3,49.1562 74.1365,26 c -0.515,-0.979 -1.5303,-1.592 -2.6366,-1.592 -1.1063,0 -2.1215,0.613 -2.6366,1.592 L 62.2133,38.6615 45.529,7.5447 C 44.924,6.4145 43.6637,5.7981 42.399,6.0143 41.1343,6.2305 40.153,7.231 39.958,8.498 L 23.8333,111.7187 h -0.052 l 0.052,0.0596 0.4245,0.2085 77.488,-77.5775 c 0.7877,-0.7915 1.952,-1.076 3.016,-0.737 1.064,0.339 1.849,1.2445 2.0338,2.3457 l 12.2518,75.775 0.1192,-0.0745 -12.356,-76.4603 z M 23.9748,111.5772 39.9655,9.228 c 0.1948,-1.267 1.1784,-2.2675 2.442,-2.4837 1.2636,-0.2162 2.524,0.4 3.13,1.5304 L 62.22,39.392 68.87,26.7305 c 0.515,-0.979 1.5303,-1.592 2.6366,-1.592 1.1063,0 2.1215,0.613 2.6366,1.592 l 11.9167,22.664 -62.0858,62.1827 z"
|
||||||
|
id="path210"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
fill="#a52714"
|
||||||
|
opacity="0.2"
|
||||||
|
d="m 75.6708,135.1722 c -2.708,1.512 -6.006,1.512 -8.714,0 l -43.0192,-24.1162 -0.1043,0.663 43.1234,24.176 c 2.708,1.512 6.006,1.512 8.714,0 l 43.4958,-24.176 -0.1117,-0.6852 -43.384,24.1387 z"
|
||||||
|
id="path212"
|
||||||
|
/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
|
@ -0,0 +1,67 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<svg
|
||||||
|
viewBox="23 6 469 132"
|
||||||
|
width="100"
|
||||||
|
height="100"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:svg="http://www.w3.org/2000/svg">
|
||||||
|
<defs
|
||||||
|
id="defs202">
|
||||||
|
<linearGradient
|
||||||
|
id="a"
|
||||||
|
x1="-3.49%"
|
||||||
|
x2="100.83%"
|
||||||
|
y1="17.02%"
|
||||||
|
y2="92.9%">
|
||||||
|
<stop
|
||||||
|
offset="0%"
|
||||||
|
stop-color="#fff"
|
||||||
|
stop-opacity=".1"
|
||||||
|
id="stop192" />
|
||||||
|
<stop
|
||||||
|
offset="14%"
|
||||||
|
stop-color="#fff"
|
||||||
|
stop-opacity=".08"
|
||||||
|
id="stop194" />
|
||||||
|
<stop
|
||||||
|
offset="61%"
|
||||||
|
stop-color="#fff"
|
||||||
|
stop-opacity=".02"
|
||||||
|
id="stop196" />
|
||||||
|
<stop
|
||||||
|
offset="100%"
|
||||||
|
stop-color="#fff"
|
||||||
|
stop-opacity="0"
|
||||||
|
id="stop198" />
|
||||||
|
</linearGradient>
|
||||||
|
<path
|
||||||
|
id="b"
|
||||||
|
d="M106.687 35.2742c-.186-1.0977-.967-2-2.0244-2.338s-2.2148-.057-3.0002.73L86.2473 49.166l-12.12-23.1455c-.5133-.9786-1.525-1.5914-2.6273-1.5914s-2.114.6128-2.6273 1.5914l-6.6277 12.656L45.62 7.5726c-.603-1.1297-1.8588-1.746-3.118-1.5297s-2.2394 1.216-2.4335 2.4827L24 111.701l42.9727 24.1654c2.6985 1.5113 5.985 1.5113 8.6836 0L119 111.701l-12.313-76.427z" />
|
||||||
|
</defs>
|
||||||
|
<g
|
||||||
|
id="g305"
|
||||||
|
transform="matrix(2.9011579,0,0,2.9011579,43.533284,-135.93685)">
|
||||||
|
<path
|
||||||
|
fill="#ffa000"
|
||||||
|
d="M 23.8266,111.7182 39.9588,8.4901 c 0.1972,-1.266 1.1818,-2.264 2.445,-2.4786 1.2632,-0.2146 2.522,0.4028 3.126,1.5327 L 62.2133,38.6615 68.8633,26 c 0.515,-0.979 1.5303,-1.592 2.6366,-1.592 1.1063,0 2.1215,0.613 2.6366,1.592 l 45.0227,85.718 H 23.8266 Z"
|
||||||
|
id="path204" />
|
||||||
|
<path
|
||||||
|
fill="#f57c00"
|
||||||
|
d="M 79.566,71.5074 62.2124,38.6472 23.8334,111.7187 Z"
|
||||||
|
id="path206" />
|
||||||
|
<path
|
||||||
|
fill="#ffca28"
|
||||||
|
d="m 119.1666,111.7187 -12.356,-76.4603 c -0.1867,-1.098 -0.9703,-2 -2.0315,-2.34 -1.0612,-0.34 -2.2226,-0.057 -3.0107,0.7302 l -77.935,78.069 43.1234,24.1834 c 2.708,1.512 6.006,1.512 8.714,0 l 43.4958,-24.1834 z"
|
||||||
|
id="path208" />
|
||||||
|
<path
|
||||||
|
fill="#ffffff"
|
||||||
|
fill-opacity="0.2"
|
||||||
|
d="m 106.8105,35.2584 c -0.1867,-1.098 -0.9703,-2 -2.0315,-2.34 -1.0612,-0.34 -2.2226,-0.057 -3.0107,0.7302 L 86.3,49.1562 74.1365,26 c -0.515,-0.979 -1.5303,-1.592 -2.6366,-1.592 -1.1063,0 -2.1215,0.613 -2.6366,1.592 L 62.2133,38.6615 45.529,7.5447 C 44.924,6.4145 43.6637,5.7981 42.399,6.0143 41.1343,6.2305 40.153,7.231 39.958,8.498 L 23.8333,111.7187 h -0.052 l 0.052,0.0596 0.4245,0.2085 77.488,-77.5775 c 0.7877,-0.7915 1.952,-1.076 3.016,-0.737 1.064,0.339 1.849,1.2445 2.0338,2.3457 l 12.2518,75.775 0.1192,-0.0745 -12.356,-76.4603 z M 23.9748,111.5772 39.9655,9.228 c 0.1948,-1.267 1.1784,-2.2675 2.442,-2.4837 1.2636,-0.2162 2.524,0.4 3.13,1.5304 L 62.22,39.392 68.87,26.7305 c 0.515,-0.979 1.5303,-1.592 2.6366,-1.592 1.1063,0 2.1215,0.613 2.6366,1.592 l 11.9167,22.664 -62.0858,62.1827 z"
|
||||||
|
id="path210" />
|
||||||
|
<path
|
||||||
|
fill="#a52714"
|
||||||
|
opacity="0.2"
|
||||||
|
d="m 75.6708,135.1722 c -2.708,1.512 -6.006,1.512 -8.714,0 l -43.0192,-24.1162 -0.1043,0.663 43.1234,24.176 c 2.708,1.512 6.006,1.512 8.714,0 l 43.4958,-24.176 -0.1117,-0.6852 -43.384,24.1387 z"
|
||||||
|
id="path212" />
|
||||||
|
</g>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 3.1 KiB |
|
@ -12,6 +12,7 @@ import Rest from "./Rest.svelte"
|
||||||
import Budibase from "./Budibase.svelte"
|
import Budibase from "./Budibase.svelte"
|
||||||
import Oracle from "./Oracle.svelte"
|
import Oracle from "./Oracle.svelte"
|
||||||
import GoogleSheets from "./GoogleSheets.svelte"
|
import GoogleSheets from "./GoogleSheets.svelte"
|
||||||
|
import Firebase from "./Firebase.svelte"
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
BUDIBASE: Budibase,
|
BUDIBASE: Budibase,
|
||||||
|
@ -28,4 +29,5 @@ export default {
|
||||||
REST: Rest,
|
REST: Rest,
|
||||||
ORACLE: Oracle,
|
ORACLE: Oracle,
|
||||||
GOOGLE_SHEETS: GoogleSheets,
|
GOOGLE_SHEETS: GoogleSheets,
|
||||||
|
FIREBASE: Firebase,
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,7 +7,6 @@
|
||||||
Layout,
|
Layout,
|
||||||
Tabs,
|
Tabs,
|
||||||
Tab,
|
Tab,
|
||||||
Input,
|
|
||||||
Heading,
|
Heading,
|
||||||
TextArea,
|
TextArea,
|
||||||
Dropzone,
|
Dropzone,
|
||||||
|
@ -98,15 +97,16 @@
|
||||||
<Body size="XS"
|
<Body size="XS"
|
||||||
>Import your rest collection using one of the options below</Body
|
>Import your rest collection using one of the options below</Body
|
||||||
>
|
>
|
||||||
<Tabs selected="Link">
|
<Tabs selected="File">
|
||||||
<Tab title="Link">
|
<!-- Commenting until nginx csp issue resolved -->
|
||||||
|
<!-- <Tab title="Link">
|
||||||
<Input
|
<Input
|
||||||
bind:value={$data.url}
|
bind:value={$data.url}
|
||||||
on:change={() => (lastTouched = "url")}
|
on:change={() => (lastTouched = "url")}
|
||||||
label="Enter a URL"
|
label="Enter a URL"
|
||||||
placeholder="e.g. https://petstore.swagger.io/v2/swagger.json"
|
placeholder="e.g. https://petstore.swagger.io/v2/swagger.json"
|
||||||
/>
|
/>
|
||||||
</Tab>
|
</Tab> -->
|
||||||
<Tab title="File">
|
<Tab title="File">
|
||||||
<Dropzone
|
<Dropzone
|
||||||
gallery={false}
|
gallery={false}
|
||||||
|
@ -115,7 +115,14 @@
|
||||||
$data.file = e.detail?.[0]
|
$data.file = e.detail?.[0]
|
||||||
lastTouched = "file"
|
lastTouched = "file"
|
||||||
}}
|
}}
|
||||||
fileTags={["OpenAPI 2.0", "Swagger 2.0", "cURL", "YAML", "JSON"]}
|
fileTags={[
|
||||||
|
"OpenAPI 3.0",
|
||||||
|
"OpenAPI 2.0",
|
||||||
|
"Swagger 2.0",
|
||||||
|
"cURL",
|
||||||
|
"YAML",
|
||||||
|
"JSON",
|
||||||
|
]}
|
||||||
maximum={1}
|
maximum={1}
|
||||||
/>
|
/>
|
||||||
</Tab>
|
</Tab>
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
<script>
|
<script>
|
||||||
import { Icon } from "@budibase/bbui"
|
import { Icon } from "@budibase/bbui"
|
||||||
import { createEventDispatcher } from "svelte"
|
import { createEventDispatcher, getContext } from "svelte"
|
||||||
|
|
||||||
export let icon
|
export let icon
|
||||||
export let withArrow = false
|
export let withArrow = false
|
||||||
|
@ -14,29 +14,46 @@
|
||||||
export let iconText
|
export let iconText
|
||||||
export let iconColor
|
export let iconColor
|
||||||
|
|
||||||
|
const scrollApi = getContext("scroll")
|
||||||
const dispatch = createEventDispatcher()
|
const dispatch = createEventDispatcher()
|
||||||
|
|
||||||
function onIconClick(event) {
|
let contentRef
|
||||||
event.stopPropagation()
|
$: selected && contentRef && scrollToView()
|
||||||
|
|
||||||
|
const onClick = () => {
|
||||||
|
scrollToView()
|
||||||
|
dispatch("click")
|
||||||
|
}
|
||||||
|
|
||||||
|
const onIconClick = e => {
|
||||||
|
e.stopPropagation()
|
||||||
dispatch("iconClick")
|
dispatch("iconClick")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const scrollToView = () => {
|
||||||
|
if (!scrollApi || !contentRef) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const bounds = contentRef.getBoundingClientRect()
|
||||||
|
scrollApi.scrollTo(bounds)
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
class="nav-item"
|
class="nav-item"
|
||||||
class:border
|
class:border
|
||||||
class:selected
|
class:selected
|
||||||
style={`padding-left: ${indentLevel * 14}px`}
|
style={`padding-left: ${20 + indentLevel * 14}px`}
|
||||||
{draggable}
|
{draggable}
|
||||||
on:dragend
|
on:dragend
|
||||||
on:dragstart
|
on:dragstart
|
||||||
on:dragover
|
on:dragover
|
||||||
on:drop
|
on:drop
|
||||||
on:click
|
on:click={onClick}
|
||||||
ondragover="return false"
|
ondragover="return false"
|
||||||
ondragenter="return false"
|
ondragenter="return false"
|
||||||
>
|
>
|
||||||
<div class="content">
|
<div class="nav-item-content" bind:this={contentRef}>
|
||||||
{#if withArrow}
|
{#if withArrow}
|
||||||
<div class:opened class="icon arrow" on:click={onIconClick}>
|
<div class:opened class="icon arrow" on:click={onIconClick}>
|
||||||
<Icon size="S" name="ChevronRight" />
|
<Icon size="S" name="ChevronRight" />
|
||||||
|
@ -64,11 +81,16 @@
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.nav-item {
|
.nav-item {
|
||||||
border-radius: var(--border-radius-s);
|
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
color: var(--grey-7);
|
color: var(--grey-7);
|
||||||
transition: background-color
|
transition: background-color
|
||||||
var(--spectrum-global-animation-duration-100, 130ms) ease-in-out;
|
var(--spectrum-global-animation-duration-100, 130ms) ease-in-out;
|
||||||
|
padding: 0 var(--spacing-m) 0 var(--spacing-xl);
|
||||||
|
height: 32px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: flex-start;
|
||||||
}
|
}
|
||||||
.nav-item.selected {
|
.nav-item.selected {
|
||||||
background-color: var(--grey-2);
|
background-color: var(--grey-2);
|
||||||
|
@ -81,14 +103,14 @@
|
||||||
visibility: visible;
|
visibility: visible;
|
||||||
}
|
}
|
||||||
|
|
||||||
.content {
|
.nav-item-content {
|
||||||
padding: 0 var(--spacing-s);
|
flex: 1 1 auto;
|
||||||
height: 32px;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
justify-content: flex-start;
|
justify-content: flex-start;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: var(--spacing-xs);
|
gap: var(--spacing-xs);
|
||||||
|
width: max-content;
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon {
|
.icon {
|
||||||
|
@ -111,12 +133,13 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.text {
|
.text {
|
||||||
flex: 1 1 auto;
|
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
font-size: var(--spectrum-global-dimension-font-size-75);
|
font-size: var(--spectrum-global-dimension-font-size-75);
|
||||||
|
white-space: nowrap;
|
||||||
|
max-width: 160px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
white-space: nowrap;
|
flex: 0 0 auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.actions {
|
.actions {
|
||||||
|
@ -125,9 +148,9 @@
|
||||||
height: 20px;
|
height: 20px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
position: relative;
|
position: relative;
|
||||||
flex-direction: row;
|
display: grid;
|
||||||
justify-content: center;
|
margin-left: var(--spacing-s);
|
||||||
align-items: center;
|
place-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.iconText {
|
.iconText {
|
||||||
|
|
|
@ -0,0 +1,125 @@
|
||||||
|
<script>
|
||||||
|
export let backgroundColour
|
||||||
|
export let imageSrc
|
||||||
|
export let name
|
||||||
|
export let icon
|
||||||
|
export let overlayEnabled = true
|
||||||
|
|
||||||
|
let imageError = false
|
||||||
|
let imageLoaded = false
|
||||||
|
|
||||||
|
const imageRenderError = () => {
|
||||||
|
imageError = true
|
||||||
|
}
|
||||||
|
|
||||||
|
const imageLoadSuccess = () => {
|
||||||
|
imageLoaded = true
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="template-card" style="background-color:{backgroundColour};">
|
||||||
|
<div class="template-thumbnail card-body">
|
||||||
|
<img
|
||||||
|
alt={name}
|
||||||
|
src={imageSrc}
|
||||||
|
on:error={imageRenderError}
|
||||||
|
on:load={imageLoadSuccess}
|
||||||
|
class={`${imageLoaded ? "loaded" : ""}`}
|
||||||
|
/>
|
||||||
|
<div style={`display:${imageError ? "block" : "none"}`}>
|
||||||
|
<svg
|
||||||
|
width="26px"
|
||||||
|
height="26px"
|
||||||
|
class="spectrum-Icon"
|
||||||
|
style="color: white"
|
||||||
|
focusable="false"
|
||||||
|
>
|
||||||
|
<use xlink:href="#spectrum-icon-18-{icon}" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div class={overlayEnabled ? "template-thumbnail-action-overlay" : ""}>
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="template-thumbnail-text">
|
||||||
|
<div>{name}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.template-thumbnail {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.template-card:hover .template-thumbnail-action-overlay {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.template-thumbnail-action-overlay {
|
||||||
|
position: absolute;
|
||||||
|
top: 0px;
|
||||||
|
left: 0px;
|
||||||
|
width: 100%;
|
||||||
|
height: 70%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background-color: rgba(0, 0, 0, 0.7);
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity var(--spectrum-global-animation-duration-100) ease;
|
||||||
|
border-top-right-radius: inherit;
|
||||||
|
border-top-left-radius: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
.template-thumbnail-text {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
height: 30%;
|
||||||
|
width: 100%;
|
||||||
|
color: var(
|
||||||
|
--spectrum-heading-xs-text-color,
|
||||||
|
var(--spectrum-alias-heading-text-color)
|
||||||
|
);
|
||||||
|
background-color: var(--spectrum-global-color-gray-50);
|
||||||
|
}
|
||||||
|
|
||||||
|
.template-thumbnail-text > div {
|
||||||
|
padding-left: 1rem;
|
||||||
|
padding-right: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.template-card {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
border-radius: var(--border-radius-s);
|
||||||
|
border: 1px solid var(--spectrum-global-color-gray-300);
|
||||||
|
overflow: hidden;
|
||||||
|
min-height: 200px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.template-card > * {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.template-card img.loaded {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.template-card img {
|
||||||
|
display: none;
|
||||||
|
max-width: 100%;
|
||||||
|
border-radius: var(--border-radius-s) 0px var(--border-radius-s) 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.template-card:hover {
|
||||||
|
background: var(--spectrum-alias-background-color-tertiary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-body {
|
||||||
|
padding-left: 1rem;
|
||||||
|
padding-top: 1rem;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,152 @@
|
||||||
|
<script>
|
||||||
|
import {
|
||||||
|
Layout,
|
||||||
|
Detail,
|
||||||
|
Heading,
|
||||||
|
Button,
|
||||||
|
Modal,
|
||||||
|
ActionGroup,
|
||||||
|
ActionButton,
|
||||||
|
} from "@budibase/bbui"
|
||||||
|
import TemplateCard from "components/common/TemplateCard.svelte"
|
||||||
|
import CreateAppModal from "components/start/CreateAppModal.svelte"
|
||||||
|
|
||||||
|
export let templates
|
||||||
|
|
||||||
|
let selectedTemplateCategory
|
||||||
|
let creationModal
|
||||||
|
let template
|
||||||
|
|
||||||
|
const groupTemplatesByCategory = (templates, categoryFilter) => {
|
||||||
|
let grouped = templates.reduce((acc, template) => {
|
||||||
|
if (
|
||||||
|
typeof categoryFilter === "string" &&
|
||||||
|
[categoryFilter].indexOf(template.category) < 0
|
||||||
|
) {
|
||||||
|
return acc
|
||||||
|
}
|
||||||
|
|
||||||
|
acc[template.category] = !acc[template.category]
|
||||||
|
? []
|
||||||
|
: acc[template.category]
|
||||||
|
acc[template.category].push(template)
|
||||||
|
|
||||||
|
return acc
|
||||||
|
}, {})
|
||||||
|
return grouped
|
||||||
|
}
|
||||||
|
|
||||||
|
$: filteredTemplates = groupTemplatesByCategory(
|
||||||
|
templates,
|
||||||
|
selectedTemplateCategory
|
||||||
|
)
|
||||||
|
|
||||||
|
$: filteredTemplateCategories = filteredTemplates
|
||||||
|
? Object.keys(filteredTemplates).sort()
|
||||||
|
: []
|
||||||
|
|
||||||
|
$: templateCategories = templates
|
||||||
|
? Object.keys(groupTemplatesByCategory(templates)).sort()
|
||||||
|
: []
|
||||||
|
|
||||||
|
const stopAppCreation = () => {
|
||||||
|
template = null
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="template-header">
|
||||||
|
<Layout noPadding gap="S">
|
||||||
|
<Heading size="S">Templates</Heading>
|
||||||
|
<div class="template-category-filters spectrum-ActionGroup">
|
||||||
|
<ActionGroup>
|
||||||
|
<ActionButton
|
||||||
|
selected={!selectedTemplateCategory}
|
||||||
|
on:click={() => {
|
||||||
|
selectedTemplateCategory = null
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
All
|
||||||
|
</ActionButton>
|
||||||
|
{#each templateCategories as templateCategoryKey}
|
||||||
|
<ActionButton
|
||||||
|
dataCy={templateCategoryKey}
|
||||||
|
selected={templateCategoryKey == selectedTemplateCategory}
|
||||||
|
on:click={() => {
|
||||||
|
selectedTemplateCategory = templateCategoryKey
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{templateCategoryKey}
|
||||||
|
</ActionButton>
|
||||||
|
{/each}
|
||||||
|
</ActionGroup>
|
||||||
|
</div>
|
||||||
|
</Layout>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="template-categories">
|
||||||
|
<Layout gap="XL" noPadding>
|
||||||
|
{#each filteredTemplateCategories as templateCategoryKey}
|
||||||
|
<div class="template-category" data-cy={templateCategoryKey}>
|
||||||
|
<Detail size="M">{templateCategoryKey}</Detail>
|
||||||
|
<div class="template-grid">
|
||||||
|
{#each filteredTemplates[templateCategoryKey] as templateEntry}
|
||||||
|
<TemplateCard
|
||||||
|
name={templateEntry.name}
|
||||||
|
imageSrc={templateEntry.image}
|
||||||
|
backgroundColour={templateEntry.background}
|
||||||
|
icon={templateEntry.icon}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
cta
|
||||||
|
on:click={() => {
|
||||||
|
template = templateEntry
|
||||||
|
creationModal.show()
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Use template
|
||||||
|
</Button>
|
||||||
|
<a
|
||||||
|
href={templateEntry.url}
|
||||||
|
target="_blank"
|
||||||
|
class="overlay-preview-link spectrum-Button spectrum-Button--sizeM spectrum-Button--secondary"
|
||||||
|
on:click|stopPropagation
|
||||||
|
>
|
||||||
|
Details
|
||||||
|
</a>
|
||||||
|
</TemplateCard>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
</Layout>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Modal
|
||||||
|
bind:this={creationModal}
|
||||||
|
padding={false}
|
||||||
|
width="600px"
|
||||||
|
on:hide={stopAppCreation}
|
||||||
|
>
|
||||||
|
<CreateAppModal {template} />
|
||||||
|
</Modal>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.template-grid {
|
||||||
|
padding-top: 10px;
|
||||||
|
display: grid;
|
||||||
|
grid-gap: var(--spacing-xl);
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
|
||||||
|
}
|
||||||
|
|
||||||
|
a:hover.spectrum-Button.spectrum-Button--secondary.overlay-preview-link {
|
||||||
|
background-color: #c8c8c8;
|
||||||
|
border-color: #c8c8c8;
|
||||||
|
color: #505050;
|
||||||
|
}
|
||||||
|
|
||||||
|
a.spectrum-Button--secondary.overlay-preview-link {
|
||||||
|
margin-top: 20px;
|
||||||
|
border-color: #c8c8c8;
|
||||||
|
color: #c8c8c8;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -238,6 +238,7 @@
|
||||||
border: var(--border-light);
|
border: var(--border-light);
|
||||||
transition: background-color 130ms ease-in-out, color 130ms ease-in-out,
|
transition: background-color 130ms ease-in-out, color 130ms ease-in-out,
|
||||||
border-color 130ms ease-in-out;
|
border-color 130ms ease-in-out;
|
||||||
|
word-wrap: break-word;
|
||||||
}
|
}
|
||||||
li:not(:last-of-type) {
|
li:not(:last-of-type) {
|
||||||
margin-bottom: var(--spacing-s);
|
margin-bottom: var(--spacing-s);
|
||||||
|
|
|
@ -12,8 +12,8 @@
|
||||||
const enrichBindings = bindings => {
|
const enrichBindings = bindings => {
|
||||||
return bindings?.map(binding => ({
|
return bindings?.map(binding => ({
|
||||||
...binding,
|
...binding,
|
||||||
readableBinding: binding.label || binding.readableBinding,
|
readableBinding: binding.readableBinding || binding.label,
|
||||||
runtimeBinding: binding.path || binding.runtimeBinding,
|
runtimeBinding: binding.runtimeBinding || binding.path,
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -40,7 +40,7 @@
|
||||||
<a
|
<a
|
||||||
slot="footer"
|
slot="footer"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
href="https://docs.budibase.com/automate/steps/triggers"
|
href="https://docs.budibase.com/docs/trigger"
|
||||||
>
|
>
|
||||||
<i class="ri-information-line" />
|
<i class="ri-information-line" />
|
||||||
<span>Learn about webhooks</span>
|
<span>Learn about webhooks</span>
|
||||||
|
|
|
@ -68,6 +68,7 @@
|
||||||
customTheme: $store.customTheme,
|
customTheme: $store.customTheme,
|
||||||
previewDevice: $store.previewDevice,
|
previewDevice: $store.previewDevice,
|
||||||
messagePassing: $store.clientFeatures.messagePassing,
|
messagePassing: $store.clientFeatures.messagePassing,
|
||||||
|
isBudibaseEvent: true
|
||||||
}
|
}
|
||||||
$: json = JSON.stringify(previewData)
|
$: json = JSON.stringify(previewData)
|
||||||
|
|
||||||
|
@ -160,6 +161,11 @@
|
||||||
await store.actions.components.updateProp(data.prop, data.value)
|
await store.actions.components.updateProp(data.prop, data.value)
|
||||||
} else if (type === "delete-component" && data.id) {
|
} else if (type === "delete-component" && data.id) {
|
||||||
confirmDeleteComponent(data.id)
|
confirmDeleteComponent(data.id)
|
||||||
|
} else if (type === "duplicate-component" && data.id) {
|
||||||
|
const rootComponent = get(currentAsset).props
|
||||||
|
const component = findComponent(rootComponent, data.id)
|
||||||
|
store.actions.components.copy(component)
|
||||||
|
await store.actions.components.paste(component)
|
||||||
} else if (type === "preview-loaded") {
|
} else if (type === "preview-loaded") {
|
||||||
// Wait for this event to show the client library if intelligent
|
// Wait for this event to show the client library if intelligent
|
||||||
// loading is supported
|
// loading is supported
|
||||||
|
|
|
@ -82,7 +82,8 @@
|
||||||
"link",
|
"link",
|
||||||
"icon",
|
"icon",
|
||||||
"embed",
|
"embed",
|
||||||
"markdownviewer"
|
"markdownviewer",
|
||||||
|
"embeddedmap"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
|
@ -52,7 +52,7 @@ export default `
|
||||||
console.error("Client received invalid JSON")
|
console.error("Client received invalid JSON")
|
||||||
// Ignore
|
// Ignore
|
||||||
}
|
}
|
||||||
if (!parsed) {
|
if (!parsed || !parsed.isBudibaseEvent) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -21,7 +21,7 @@
|
||||||
|
|
||||||
const moveUpComponent = () => {
|
const moveUpComponent = () => {
|
||||||
const asset = get(currentAsset)
|
const asset = get(currentAsset)
|
||||||
const parent = findComponentParent(asset.props, component._id)
|
const parent = findComponentParent(asset?.props, component._id)
|
||||||
if (!parent) {
|
if (!parent) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -41,7 +41,7 @@
|
||||||
|
|
||||||
const moveDownComponent = () => {
|
const moveDownComponent = () => {
|
||||||
const asset = get(currentAsset)
|
const asset = get(currentAsset)
|
||||||
const parent = findComponentParent(asset.props, component._id)
|
const parent = findComponentParent(asset?.props, component._id)
|
||||||
if (!parent) {
|
if (!parent) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -61,7 +61,7 @@
|
||||||
|
|
||||||
const duplicateComponent = () => {
|
const duplicateComponent = () => {
|
||||||
storeComponentForCopy(false)
|
storeComponentForCopy(false)
|
||||||
pasteComponent("below", true)
|
pasteComponent("below")
|
||||||
}
|
}
|
||||||
|
|
||||||
const deleteComponent = async () => {
|
const deleteComponent = async () => {
|
||||||
|
@ -73,14 +73,12 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
const storeComponentForCopy = (cut = false) => {
|
const storeComponentForCopy = (cut = false) => {
|
||||||
// lives in store - also used by drag drop
|
|
||||||
store.actions.components.copy(component, cut)
|
store.actions.components.copy(component, cut)
|
||||||
}
|
}
|
||||||
|
|
||||||
const pasteComponent = (mode, preserveBindings = false) => {
|
const pasteComponent = mode => {
|
||||||
try {
|
try {
|
||||||
// lives in store - also used by drag drop
|
store.actions.components.paste(component, mode)
|
||||||
store.actions.components.paste(component, mode, preserveBindings)
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
notifications.error("Error saving component")
|
notifications.error("Error saving component")
|
||||||
}
|
}
|
||||||
|
@ -140,3 +138,10 @@
|
||||||
onOk={deleteComponent}
|
onOk={deleteComponent}
|
||||||
/>
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.icon {
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -1,10 +1,11 @@
|
||||||
<script>
|
<script>
|
||||||
import { store } from "builderStore"
|
import { store } from "builderStore"
|
||||||
import { DropEffect, DropPosition } from "./dragDropStore"
|
import { DropEffect, DropPosition } from "./dragDropStore"
|
||||||
import ComponentDropdownMenu from "../ComponentDropdownMenu.svelte"
|
import ComponentDropdownMenu from "./ComponentDropdownMenu.svelte"
|
||||||
import NavItem from "components/common/NavItem.svelte"
|
import NavItem from "components/common/NavItem.svelte"
|
||||||
import { capitalise } from "helpers"
|
import { capitalise } from "helpers"
|
||||||
import { notifications } from "@budibase/bbui"
|
import { notifications } from "@budibase/bbui"
|
||||||
|
import { selectedComponentPath } from "builderStore"
|
||||||
|
|
||||||
export let components = []
|
export let components = []
|
||||||
export let currentComponent
|
export let currentComponent
|
||||||
|
@ -71,10 +72,20 @@
|
||||||
notifications.error("Error saving component")
|
notifications.error("Error saving component")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const isOpen = (component, selectedComponentPath, closedNodes) => {
|
||||||
|
if (!component?._children?.length) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if (selectedComponentPath.includes(component._id)) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return !closedNodes[component._id]
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<ul>
|
<ul>
|
||||||
{#each components as component, index (component._id)}
|
{#each components || [] as component, index (component._id)}
|
||||||
<li on:click|stopPropagation={() => selectComponent(component)}>
|
<li on:click|stopPropagation={() => selectComponent(component)}>
|
||||||
{#if $dragDropStore?.targetComponent === component && $dragDropStore.dropPosition === DropPosition.ABOVE}
|
{#if $dragDropStore?.targetComponent === component && $dragDropStore.dropPosition === DropPosition.ABOVE}
|
||||||
<div
|
<div
|
||||||
|
@ -97,12 +108,12 @@
|
||||||
withArrow
|
withArrow
|
||||||
indentLevel={level + 1}
|
indentLevel={level + 1}
|
||||||
selected={$store.selectedComponentId === component._id}
|
selected={$store.selectedComponentId === component._id}
|
||||||
opened={!closedNodes[component._id] && component?._children?.length}
|
opened={isOpen(component, $selectedComponentPath, closedNodes)}
|
||||||
>
|
>
|
||||||
<ComponentDropdownMenu {component} />
|
<ComponentDropdownMenu {component} />
|
||||||
</NavItem>
|
</NavItem>
|
||||||
|
|
||||||
{#if component._children && !closedNodes[component._id]}
|
{#if isOpen(component, $selectedComponentPath, closedNodes)}
|
||||||
<svelte:self
|
<svelte:self
|
||||||
components={component._children}
|
components={component._children}
|
||||||
{currentComponent}
|
{currentComponent}
|
||||||
|
@ -133,6 +144,10 @@
|
||||||
padding-left: 0;
|
padding-left: 0;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
ul,
|
||||||
|
li {
|
||||||
|
min-width: max-content;
|
||||||
|
}
|
||||||
|
|
||||||
.drop-item {
|
.drop-item {
|
||||||
border-radius: var(--border-radius-m);
|
border-radius: var(--border-radius-m);
|
||||||
|
|
|
@ -51,7 +51,7 @@
|
||||||
bind:this={confirmDeleteDialog}
|
bind:this={confirmDeleteDialog}
|
||||||
title="Confirm Deletion"
|
title="Confirm Deletion"
|
||||||
body={"Are you sure you wish to delete this layout?"}
|
body={"Are you sure you wish to delete this layout?"}
|
||||||
okText="Delete Layout"
|
okText="Delete layout"
|
||||||
onOk={deleteLayout}
|
onOk={deleteLayout}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
@ -65,3 +65,10 @@
|
||||||
<Input thin type="text" label="Name" bind:value={name} />
|
<Input thin type="text" label="Name" bind:value={name} />
|
||||||
</ModalContent>
|
</ModalContent>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.icon {
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
|
@ -0,0 +1,82 @@
|
||||||
|
<script>
|
||||||
|
import { goto } from "@roxi/routify"
|
||||||
|
import { store } from "builderStore"
|
||||||
|
import ConfirmDialog from "components/common/ConfirmDialog.svelte"
|
||||||
|
import {
|
||||||
|
ActionMenu,
|
||||||
|
MenuItem,
|
||||||
|
Icon,
|
||||||
|
Layout,
|
||||||
|
notifications,
|
||||||
|
} from "@budibase/bbui"
|
||||||
|
import { get } from "svelte/store"
|
||||||
|
|
||||||
|
export let path
|
||||||
|
export let screens
|
||||||
|
|
||||||
|
let confirmDeleteDialog
|
||||||
|
|
||||||
|
const deleteScreens = async () => {
|
||||||
|
if (!screens?.length) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
for (let { id } of screens) {
|
||||||
|
// We have to fetch the screen to be deleted immediately before deleting
|
||||||
|
// as otherwise we're very likely to 409
|
||||||
|
const screen = get(store).screens.find(screen => screen._id === id)
|
||||||
|
if (!screen) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
await store.actions.screens.delete(screen)
|
||||||
|
}
|
||||||
|
notifications.success("Screens deleted successfully")
|
||||||
|
$goto("../")
|
||||||
|
} catch (error) {
|
||||||
|
notifications.error("Error deleting screens")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<ActionMenu>
|
||||||
|
<div slot="control" class="icon">
|
||||||
|
<Icon size="S" hoverable name="MoreSmallList" />
|
||||||
|
</div>
|
||||||
|
<MenuItem icon="Delete" on:click={confirmDeleteDialog.show}>
|
||||||
|
Delete all screens
|
||||||
|
</MenuItem>
|
||||||
|
</ActionMenu>
|
||||||
|
|
||||||
|
<ConfirmDialog
|
||||||
|
bind:this={confirmDeleteDialog}
|
||||||
|
title="Confirm Deletion"
|
||||||
|
okText="Delete screens"
|
||||||
|
onOk={deleteScreens}
|
||||||
|
>
|
||||||
|
<Layout noPadding gap="S">
|
||||||
|
<div>
|
||||||
|
Are you sure you want to delete all screens under the <b>{path}</b> route?
|
||||||
|
</div>
|
||||||
|
<div>The following screens will be deleted:</div>
|
||||||
|
<div class="to-delete">
|
||||||
|
{#each screens as screen}
|
||||||
|
<div>{screen.route}</div>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</Layout>
|
||||||
|
</ConfirmDialog>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.to-delete {
|
||||||
|
font-weight: bold;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: flex-start;
|
||||||
|
align-items: flex-start;
|
||||||
|
padding-left: var(--spacing-xl);
|
||||||
|
}
|
||||||
|
.icon {
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -8,6 +8,7 @@
|
||||||
import instantiateStore from "./dragDropStore"
|
import instantiateStore from "./dragDropStore"
|
||||||
import ComponentTree from "./ComponentTree.svelte"
|
import ComponentTree from "./ComponentTree.svelte"
|
||||||
import NavItem from "components/common/NavItem.svelte"
|
import NavItem from "components/common/NavItem.svelte"
|
||||||
|
import PathDropdownMenu from "./PathDropdownMenu.svelte"
|
||||||
import ScreenDropdownMenu from "./ScreenDropdownMenu.svelte"
|
import ScreenDropdownMenu from "./ScreenDropdownMenu.svelte"
|
||||||
import { get } from "svelte/store"
|
import { get } from "svelte/store"
|
||||||
|
|
||||||
|
@ -28,6 +29,7 @@
|
||||||
export let border
|
export let border
|
||||||
|
|
||||||
let routeManuallyOpened = false
|
let routeManuallyOpened = false
|
||||||
|
|
||||||
$: selectedScreen = $currentAsset
|
$: selectedScreen = $currentAsset
|
||||||
$: allScreens = getAllScreens(route)
|
$: allScreens = getAllScreens(route)
|
||||||
$: filteredScreens = getFilteredScreens(allScreens, $screenSearchString)
|
$: filteredScreens = getFilteredScreens(allScreens, $screenSearchString)
|
||||||
|
@ -73,14 +75,17 @@
|
||||||
opened={routeOpened}
|
opened={routeOpened}
|
||||||
{border}
|
{border}
|
||||||
withArrow={route.subpaths}
|
withArrow={route.subpaths}
|
||||||
/>
|
>
|
||||||
|
<PathDropdownMenu screens={allScreens} {path} />
|
||||||
|
</NavItem>
|
||||||
|
|
||||||
{#if routeOpened}
|
{#if routeOpened}
|
||||||
{#each filteredScreens as screen (screen.id)}
|
{#each filteredScreens as screen (screen.id)}
|
||||||
<NavItem
|
<NavItem
|
||||||
icon="WebPage"
|
icon="WebPage"
|
||||||
indentLevel={indent || 1}
|
indentLevel={indent || 1}
|
||||||
selected={$store.selectedScreenId === screen.id}
|
selected={$store.selectedScreenId === screen.id &&
|
||||||
|
$store.currentView === "detail"}
|
||||||
opened={$store.selectedScreenId === screen.id}
|
opened={$store.selectedScreenId === screen.id}
|
||||||
text={ROUTE_NAME_MAP[screen.route]?.[screen.role] || screen.route}
|
text={ROUTE_NAME_MAP[screen.route]?.[screen.role] || screen.route}
|
||||||
withArrow={route.subpaths}
|
withArrow={route.subpaths}
|
||||||
|
|
|
@ -2,14 +2,57 @@
|
||||||
import { goto } from "@roxi/routify"
|
import { goto } from "@roxi/routify"
|
||||||
import { store, allScreens } from "builderStore"
|
import { store, allScreens } from "builderStore"
|
||||||
import ConfirmDialog from "components/common/ConfirmDialog.svelte"
|
import ConfirmDialog from "components/common/ConfirmDialog.svelte"
|
||||||
import { ActionMenu, MenuItem, Icon, notifications } from "@budibase/bbui"
|
import {
|
||||||
|
ActionMenu,
|
||||||
|
MenuItem,
|
||||||
|
Icon,
|
||||||
|
Modal,
|
||||||
|
Helpers,
|
||||||
|
notifications,
|
||||||
|
} from "@budibase/bbui"
|
||||||
|
import ScreenDetailsModal from "../ScreenDetailsModal.svelte"
|
||||||
|
import sanitizeUrl from "builderStore/store/screenTemplates/utils/sanitizeUrl"
|
||||||
|
import analytics, { Events } from "analytics"
|
||||||
|
import { makeComponentUnique } from "builderStore/componentUtils"
|
||||||
|
|
||||||
export let screenId
|
export let screenId
|
||||||
|
|
||||||
let confirmDeleteDialog
|
let confirmDeleteDialog
|
||||||
|
let screenDetailsModal
|
||||||
|
|
||||||
$: screen = $allScreens.find(screen => screen._id === screenId)
|
$: screen = $allScreens.find(screen => screen._id === screenId)
|
||||||
|
|
||||||
|
const duplicateScreen = () => {
|
||||||
|
screenDetailsModal.show()
|
||||||
|
}
|
||||||
|
|
||||||
|
const createDuplicateScreen = async ({ screenName, screenUrl }) => {
|
||||||
|
// Create a dupe and ensure it is unique
|
||||||
|
let duplicateScreen = Helpers.cloneDeep(screen)
|
||||||
|
delete duplicateScreen._id
|
||||||
|
delete duplicateScreen._rev
|
||||||
|
makeComponentUnique(duplicateScreen.props)
|
||||||
|
|
||||||
|
// Attach the new name and URL
|
||||||
|
duplicateScreen.routing.route = sanitizeUrl(screenUrl)
|
||||||
|
duplicateScreen.props._instanceName = screenName
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Create the screen
|
||||||
|
await store.actions.screens.save(duplicateScreen)
|
||||||
|
|
||||||
|
// Analytics
|
||||||
|
if (screen.template) {
|
||||||
|
analytics.captureEvent(Events.SCREEN.CREATED, {
|
||||||
|
template: "createFromScratch",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
notifications.error("Error duplicating screen")
|
||||||
|
console.log(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const deleteScreen = async () => {
|
const deleteScreen = async () => {
|
||||||
try {
|
try {
|
||||||
await store.actions.screens.delete(screen)
|
await store.actions.screens.delete(screen)
|
||||||
|
@ -19,12 +62,28 @@
|
||||||
notifications.error("Error deleting screen")
|
notifications.error("Error deleting screen")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const pasteComponent = mode => {
|
||||||
|
try {
|
||||||
|
store.actions.components.paste(screen?.props, mode)
|
||||||
|
} catch (error) {
|
||||||
|
notifications.error("Error saving component")
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<ActionMenu>
|
<ActionMenu>
|
||||||
<div slot="control" class="icon">
|
<div slot="control" class="icon">
|
||||||
<Icon size="S" hoverable name="MoreSmallList" />
|
<Icon size="S" hoverable name="MoreSmallList" />
|
||||||
</div>
|
</div>
|
||||||
|
<MenuItem icon="Duplicate" on:click={duplicateScreen}>Duplicate</MenuItem>
|
||||||
|
<MenuItem
|
||||||
|
icon="ShowOneLayer"
|
||||||
|
on:click={() => pasteComponent("inside")}
|
||||||
|
disabled={!$store.componentToPaste}
|
||||||
|
>
|
||||||
|
Paste inside
|
||||||
|
</MenuItem>
|
||||||
<MenuItem icon="Delete" on:click={confirmDeleteDialog.show}>Delete</MenuItem>
|
<MenuItem icon="Delete" on:click={confirmDeleteDialog.show}>Delete</MenuItem>
|
||||||
</ActionMenu>
|
</ActionMenu>
|
||||||
|
|
||||||
|
@ -32,6 +91,22 @@
|
||||||
bind:this={confirmDeleteDialog}
|
bind:this={confirmDeleteDialog}
|
||||||
title="Confirm Deletion"
|
title="Confirm Deletion"
|
||||||
body={"Are you sure you wish to delete this screen?"}
|
body={"Are you sure you wish to delete this screen?"}
|
||||||
okText="Delete Screen"
|
okText="Delete screen"
|
||||||
onOk={deleteScreen}
|
onOk={deleteScreen}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<Modal bind:this={screenDetailsModal}>
|
||||||
|
<ScreenDetailsModal
|
||||||
|
onConfirm={createDuplicateScreen}
|
||||||
|
screenName={screen?.props._instanceName}
|
||||||
|
screenUrl={screen?.routing.route}
|
||||||
|
confirmText="Duplicate"
|
||||||
|
/>
|
||||||
|
</Modal>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.icon {
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
|
@ -55,11 +55,10 @@
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="root">
|
<div class="root" class:has-screens={!!paths?.length}>
|
||||||
{#each paths as path, idx (path)}
|
{#each paths as path, idx (path)}
|
||||||
<PathTree border={idx > 0} {path} route={routes[path]} />
|
<PathTree border={idx > 0} {path} route={routes[path]} />
|
||||||
{/each}
|
{/each}
|
||||||
|
|
||||||
{#if !paths.length}
|
{#if !paths.length}
|
||||||
<div class="empty">
|
<div class="empty">
|
||||||
There aren't any screens configured with this access role.
|
There aren't any screens configured with this access role.
|
||||||
|
@ -68,9 +67,12 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
.root.has-screens {
|
||||||
|
min-width: max-content;
|
||||||
|
}
|
||||||
div.empty {
|
div.empty {
|
||||||
font-size: var(--font-size-xs);
|
font-size: var(--font-size-s);
|
||||||
color: var(--grey-5);
|
color: var(--grey-5);
|
||||||
padding-top: var(--spacing-xs);
|
padding: var(--spacing-xs) var(--spacing-xl);
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
<script>
|
<script>
|
||||||
import { onMount } from "svelte"
|
import { onMount, setContext } from "svelte"
|
||||||
import { goto, params } from "@roxi/routify"
|
import { goto, params } from "@roxi/routify"
|
||||||
import {
|
import {
|
||||||
store,
|
store,
|
||||||
|
@ -18,11 +18,63 @@
|
||||||
Search,
|
Search,
|
||||||
Tabs,
|
Tabs,
|
||||||
Tab,
|
Tab,
|
||||||
|
Layout as BBUILayout,
|
||||||
notifications,
|
notifications,
|
||||||
} from "@budibase/bbui"
|
} from "@budibase/bbui"
|
||||||
|
|
||||||
export let showModal
|
export let showModal
|
||||||
|
|
||||||
|
let scrollRef
|
||||||
|
|
||||||
|
const scrollTo = bounds => {
|
||||||
|
if (!bounds) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const sidebarWidth = 259
|
||||||
|
const navItemHeight = 32
|
||||||
|
const { scrollLeft, scrollTop, offsetHeight } = scrollRef
|
||||||
|
|
||||||
|
let scrollBounds = scrollRef.getBoundingClientRect()
|
||||||
|
let newOffsets = {}
|
||||||
|
|
||||||
|
// Calculate left offset
|
||||||
|
const offsetX = bounds.left + bounds.width + scrollLeft + 20
|
||||||
|
if (offsetX > sidebarWidth) {
|
||||||
|
newOffsets.left = offsetX - sidebarWidth
|
||||||
|
} else {
|
||||||
|
newOffsets.left = 0
|
||||||
|
}
|
||||||
|
if (newOffsets.left === scrollLeft) {
|
||||||
|
delete newOffsets.left
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate top offset
|
||||||
|
const offsetY = bounds.top - scrollBounds?.top + scrollTop
|
||||||
|
if (offsetY > scrollTop + offsetHeight - 2 * navItemHeight) {
|
||||||
|
newOffsets.top = offsetY - offsetHeight + 2 * navItemHeight
|
||||||
|
} else if (offsetY < scrollTop + navItemHeight) {
|
||||||
|
newOffsets.top = offsetY - navItemHeight
|
||||||
|
} else {
|
||||||
|
delete newOffsets.top
|
||||||
|
}
|
||||||
|
|
||||||
|
// Skip if offset is unchanged
|
||||||
|
if (newOffsets.left == null && newOffsets.top == null) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Smoothly scroll to the offset
|
||||||
|
scrollRef.scroll({
|
||||||
|
...newOffsets,
|
||||||
|
behavior: "smooth",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
setContext("scroll", {
|
||||||
|
scrollTo,
|
||||||
|
})
|
||||||
|
|
||||||
const tabs = [
|
const tabs = [
|
||||||
{
|
{
|
||||||
title: "Screens",
|
title: "Screens",
|
||||||
|
@ -79,7 +131,7 @@
|
||||||
<Tabs {selected} on:select={navigate}>
|
<Tabs {selected} on:select={navigate}>
|
||||||
<Tab title="Screens">
|
<Tab title="Screens">
|
||||||
<div class="tab-content-padding">
|
<div class="tab-content-padding">
|
||||||
<div class="role-select">
|
<BBUILayout noPadding gap="XS">
|
||||||
<Select
|
<Select
|
||||||
on:change={updateAccessRole}
|
on:change={updateAccessRole}
|
||||||
value={$selectedAccessRole}
|
value={$selectedAccessRole}
|
||||||
|
@ -93,17 +145,24 @@
|
||||||
label="Search Screens"
|
label="Search Screens"
|
||||||
bind:value={$screenSearchString}
|
bind:value={$screenSearchString}
|
||||||
/>
|
/>
|
||||||
</div>
|
</BBUILayout>
|
||||||
<div class="nav-items-container">
|
<div class="nav-items-container" bind:this={scrollRef}>
|
||||||
<ComponentNavigationTree />
|
<ComponentNavigationTree />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Tab>
|
</Tab>
|
||||||
<Tab title="Layouts">
|
<Tab title="Layouts">
|
||||||
<div class="tab-content-padding">
|
<div class="tab-content-padding">
|
||||||
|
<div
|
||||||
|
class="nav-items-container nav-items-container--layouts"
|
||||||
|
bind:this={scrollRef}
|
||||||
|
>
|
||||||
|
<div class="layouts-container">
|
||||||
{#each $store.layouts as layout, idx (layout._id)}
|
{#each $store.layouts as layout, idx (layout._id)}
|
||||||
<Layout {layout} border={idx > 0} />
|
<Layout {layout} border={idx > 0} />
|
||||||
{/each}
|
{/each}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<Modal bind:this={newLayoutModal}>
|
<Modal bind:this={newLayoutModal}>
|
||||||
<NewLayoutModal />
|
<NewLayoutModal />
|
||||||
</Modal>
|
</Modal>
|
||||||
|
@ -126,23 +185,45 @@
|
||||||
justify-content: flex-start;
|
justify-content: flex-start;
|
||||||
align-items: stretch;
|
align-items: stretch;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
flex: 1 1 auto;
|
||||||
}
|
}
|
||||||
|
.title :global(.spectrum-Tabs-content),
|
||||||
|
.title :global(.spectrum-Tabs-content > div),
|
||||||
|
.title :global(.spectrum-Tabs-content > div > div) {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
.add-button {
|
.add-button {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: var(--spacing-l);
|
top: var(--spacing-l);
|
||||||
right: var(--spacing-xl);
|
right: var(--spacing-xl);
|
||||||
}
|
}
|
||||||
|
|
||||||
.role-select {
|
.tab-content-padding {
|
||||||
|
padding: 0 var(--spacing-xl);
|
||||||
|
height: 100%;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
justify-content: flex-start;
|
justify-content: flex-start;
|
||||||
align-items: stretch;
|
align-items: stretch;
|
||||||
margin-bottom: var(--spacing-m);
|
gap: var(--spacing-xl);
|
||||||
gap: var(--spacing-m);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.tab-content-padding {
|
.nav-items-container {
|
||||||
padding: 0 var(--spacing-xl);
|
border-top: var(--border-light);
|
||||||
|
margin: 0 calc(-1 * var(--spacing-xl));
|
||||||
|
padding: var(--spacing-m) 0;
|
||||||
|
flex: 1 1 auto;
|
||||||
|
overflow: auto;
|
||||||
|
height: 0;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
.nav-items-container--layouts {
|
||||||
|
border-top: none;
|
||||||
|
margin-top: calc(-1 * var(--spectrum-global-dimension-static-size-150));
|
||||||
|
}
|
||||||
|
|
||||||
|
.layouts-container {
|
||||||
|
min-width: max-content;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -10,39 +10,19 @@
|
||||||
ProgressCircle,
|
ProgressCircle,
|
||||||
} from "@budibase/bbui"
|
} from "@budibase/bbui"
|
||||||
import getTemplates from "builderStore/store/screenTemplates"
|
import getTemplates from "builderStore/store/screenTemplates"
|
||||||
import { onDestroy } from "svelte"
|
|
||||||
|
|
||||||
import { createEventDispatcher } from "svelte"
|
export let onConfirm
|
||||||
|
export let onCancel
|
||||||
export let chooseModal
|
|
||||||
export let save
|
|
||||||
export let showProgressCircle = false
|
export let showProgressCircle = false
|
||||||
|
|
||||||
let selectedScreens = []
|
|
||||||
|
|
||||||
const blankScreen = "createFromScratch"
|
const blankScreen = "createFromScratch"
|
||||||
const dispatch = createEventDispatcher()
|
|
||||||
|
|
||||||
function setScreens() {
|
let selectedScreens = []
|
||||||
dispatch("save", {
|
let templates = getTemplates($store, $tables.list)
|
||||||
screens: selectedScreens,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
$: blankSelected = selectedScreens?.length === 1
|
$: blankSelected = selectedScreens?.length === 1
|
||||||
$: autoSelected = selectedScreens?.length > 0 && !blankSelected
|
$: autoSelected = selectedScreens?.length > 0 && !blankSelected
|
||||||
|
|
||||||
let templates = getTemplates($store, $tables.list)
|
|
||||||
|
|
||||||
const confirm = async () => {
|
|
||||||
if (autoSelected) {
|
|
||||||
setScreens()
|
|
||||||
await save()
|
|
||||||
} else {
|
|
||||||
setScreens()
|
|
||||||
chooseModal(1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const toggleScreenSelection = table => {
|
const toggleScreenSelection = table => {
|
||||||
if (selectedScreens.find(s => s.table === table.name)) {
|
if (selectedScreens.find(s => s.table === table.name)) {
|
||||||
selectedScreens = selectedScreens.filter(
|
selectedScreens = selectedScreens.filter(
|
||||||
|
@ -56,25 +36,25 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onDestroy(() => {
|
const confirmScreenSelection = async () => {
|
||||||
selectedScreens = []
|
await onConfirm(selectedScreens)
|
||||||
})
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<ModalContent
|
<ModalContent
|
||||||
title="Add screens"
|
title="Add screens"
|
||||||
confirmText="Add Screens"
|
confirmText="Add screens"
|
||||||
cancelText="Cancel"
|
cancelText="Cancel"
|
||||||
onConfirm={() => confirm()}
|
onConfirm={confirmScreenSelection}
|
||||||
|
{onCancel}
|
||||||
disabled={!selectedScreens.length}
|
disabled={!selectedScreens.length}
|
||||||
size="L"
|
size="L"
|
||||||
>
|
>
|
||||||
<Body size="S"
|
<Body size="S">
|
||||||
>Please select the screens you would like to add to your application.
|
Please select the screens you would like to add to your application.
|
||||||
Autogenerated screens come with CRUD functionality.</Body
|
Autogenerated screens come with CRUD functionality.
|
||||||
>
|
</Body>
|
||||||
|
|
||||||
<Layout noPadding gap="S">
|
<Layout noPadding gap="S">
|
||||||
<Detail size="S">Blank screen</Detail>
|
<Detail size="S">Blank screen</Detail>
|
||||||
<div
|
<div
|
||||||
|
|
|
@ -2,58 +2,62 @@
|
||||||
import { ModalContent, Input, ProgressCircle } from "@budibase/bbui"
|
import { ModalContent, Input, ProgressCircle } from "@budibase/bbui"
|
||||||
import sanitizeUrl from "builderStore/store/screenTemplates/utils/sanitizeUrl"
|
import sanitizeUrl from "builderStore/store/screenTemplates/utils/sanitizeUrl"
|
||||||
import { selectedAccessRole, allScreens } from "builderStore"
|
import { selectedAccessRole, allScreens } from "builderStore"
|
||||||
import { onDestroy } from "svelte"
|
import { get } from "svelte/store"
|
||||||
|
|
||||||
export let screenName
|
export let onConfirm
|
||||||
export let url
|
export let onCancel
|
||||||
export let chooseModal
|
|
||||||
export let save
|
|
||||||
export let showProgressCircle = false
|
export let showProgressCircle = false
|
||||||
|
export let screenName
|
||||||
|
export let screenUrl
|
||||||
|
export let confirmText = "Continue"
|
||||||
|
|
||||||
let routeError
|
let routeError
|
||||||
let roleId = $selectedAccessRole || "BASIC"
|
let touched = false
|
||||||
|
|
||||||
const routeChanged = event => {
|
const routeChanged = event => {
|
||||||
if (!event.detail.startsWith("/")) {
|
if (!event.detail.startsWith("/")) {
|
||||||
url = "/" + event.detail
|
screenUrl = "/" + event.detail
|
||||||
}
|
}
|
||||||
url = sanitizeUrl(url)
|
touched = true
|
||||||
|
screenUrl = sanitizeUrl(screenUrl)
|
||||||
if (routeExists(url, roleId)) {
|
if (routeExists(screenUrl)) {
|
||||||
routeError = "This URL is already taken for this access role"
|
routeError = "This URL is already taken for this access role"
|
||||||
} else {
|
} else {
|
||||||
routeError = ""
|
routeError = null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const routeExists = (url, roleId) => {
|
const routeExists = url => {
|
||||||
return $allScreens.some(
|
const roleId = get(selectedAccessRole) || "BASIC"
|
||||||
|
return get(allScreens).some(
|
||||||
screen =>
|
screen =>
|
||||||
screen.routing.route.toLowerCase() === url.toLowerCase() &&
|
screen.routing.route.toLowerCase() === url.toLowerCase() &&
|
||||||
screen.routing.roleId === roleId
|
screen.routing.roleId === roleId
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
onDestroy(() => {
|
const confirmScreenDetails = async () => {
|
||||||
screenName = ""
|
await onConfirm({
|
||||||
url = ""
|
screenName,
|
||||||
|
screenUrl,
|
||||||
})
|
})
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<ModalContent
|
<ModalContent
|
||||||
size="M"
|
size="M"
|
||||||
title={"Enter details"}
|
title={"Enter details"}
|
||||||
confirmText={"Continue"}
|
{confirmText}
|
||||||
onCancel={() => chooseModal(0)}
|
onConfirm={confirmScreenDetails}
|
||||||
onConfirm={() => save()}
|
{onCancel}
|
||||||
cancelText={"Back"}
|
cancelText={"Back"}
|
||||||
disabled={!screenName || !url || routeError}
|
disabled={!screenName || !screenUrl || routeError || !touched}
|
||||||
>
|
>
|
||||||
<Input label="Name" bind:value={screenName} />
|
<Input label="Name" bind:value={screenName} />
|
||||||
<Input
|
<Input
|
||||||
label="URL"
|
label="URL"
|
||||||
error={routeError}
|
error={routeError}
|
||||||
bind:value={url}
|
bind:value={screenUrl}
|
||||||
on:change={routeChanged}
|
on:change={routeChanged}
|
||||||
/>
|
/>
|
||||||
<div slot="footer">
|
<div slot="footer">
|
||||||
|
|
|
@ -3,141 +3,133 @@
|
||||||
import NewScreenModal from "components/design/NavigationPanel/NewScreenModal.svelte"
|
import NewScreenModal from "components/design/NavigationPanel/NewScreenModal.svelte"
|
||||||
import sanitizeUrl from "builderStore/store/screenTemplates/utils/sanitizeUrl"
|
import sanitizeUrl from "builderStore/store/screenTemplates/utils/sanitizeUrl"
|
||||||
import { Modal, notifications } from "@budibase/bbui"
|
import { Modal, notifications } from "@budibase/bbui"
|
||||||
import { store, selectedAccessRole, allScreens } from "builderStore"
|
import { store, selectedAccessRole } from "builderStore"
|
||||||
import analytics, { Events } from "analytics"
|
import analytics, { Events } from "analytics"
|
||||||
|
import { get } from "svelte/store"
|
||||||
|
|
||||||
let newScreenModal
|
let pendingScreen
|
||||||
let navigationSelectionModal
|
|
||||||
let screenDetailsModal
|
|
||||||
let screenName = ""
|
|
||||||
let url = ""
|
|
||||||
let selectedScreens = []
|
|
||||||
let showProgressCircle = false
|
let showProgressCircle = false
|
||||||
let routeError
|
|
||||||
let createdScreens = []
|
|
||||||
|
|
||||||
$: roleId = $selectedAccessRole || "BASIC"
|
// Modal refs
|
||||||
|
let newScreenModal
|
||||||
|
let screenDetailsModal
|
||||||
|
|
||||||
const createScreens = async () => {
|
// External handler to show the screen wizard
|
||||||
for (let screen of selectedScreens) {
|
export const showModal = () => {
|
||||||
let test = screen.create()
|
newScreenModal.show()
|
||||||
createdScreens.push(test)
|
|
||||||
analytics.captureEvent(Events.SCREEN.CREATED, {
|
|
||||||
template: screen.id || screen.name,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const save = async () => {
|
// Reset state when showing modal again
|
||||||
showProgressCircle = true
|
pendingScreen = null
|
||||||
try {
|
|
||||||
await createScreens()
|
|
||||||
for (let screen of createdScreens) {
|
|
||||||
await saveScreens(screen)
|
|
||||||
}
|
|
||||||
await store.actions.routing.fetch()
|
|
||||||
selectedScreens = []
|
|
||||||
createdScreens = []
|
|
||||||
screenName = ""
|
|
||||||
url = ""
|
|
||||||
} catch (error) {
|
|
||||||
notifications.error("Error creating screens")
|
|
||||||
}
|
|
||||||
showProgressCircle = false
|
showProgressCircle = false
|
||||||
}
|
}
|
||||||
|
|
||||||
const saveScreens = async draftScreen => {
|
// Creates an array of screens, checking and sanitising their URLs
|
||||||
let existingScreenCount = $store.screens.filter(
|
const createScreens = async screens => {
|
||||||
s => s.props._instanceName == draftScreen.props._instanceName
|
if (!screens?.length) {
|
||||||
).length
|
return
|
||||||
if (existingScreenCount > 0) {
|
|
||||||
let oldUrlArr = draftScreen.routing.route.split("/")
|
|
||||||
oldUrlArr[1] = `${oldUrlArr[1]}-${existingScreenCount + 1}`
|
|
||||||
draftScreen.routing.route = oldUrlArr.join("/")
|
|
||||||
}
|
}
|
||||||
|
showProgressCircle = true
|
||||||
|
|
||||||
let route = url ? sanitizeUrl(`${url}`) : draftScreen.routing.route
|
|
||||||
if (draftScreen) {
|
|
||||||
if (!route) {
|
|
||||||
routeError = "URL is required"
|
|
||||||
} else {
|
|
||||||
if (routeExists(route, roleId)) {
|
|
||||||
routeError = "This URL is already taken for this access role"
|
|
||||||
} else {
|
|
||||||
routeError = ""
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (routeError) return false
|
|
||||||
|
|
||||||
if (screenName) {
|
|
||||||
draftScreen.props._instanceName = screenName
|
|
||||||
}
|
|
||||||
|
|
||||||
draftScreen.routing.route = route
|
|
||||||
draftScreen.routing.roleId = roleId
|
|
||||||
|
|
||||||
await store.actions.screens.save(draftScreen)
|
|
||||||
if (draftScreen.props._instanceName.endsWith("List")) {
|
|
||||||
try {
|
try {
|
||||||
|
for (let screen of screens) {
|
||||||
|
// Check we aren't clashing with an existing URL
|
||||||
|
if (hasExistingUrl(screen.routing.route)) {
|
||||||
|
let suffix = 2
|
||||||
|
let candidateUrl = makeCandidateUrl(screen, suffix)
|
||||||
|
while (hasExistingUrl(candidateUrl)) {
|
||||||
|
candidateUrl = makeCandidateUrl(screen, ++suffix)
|
||||||
|
}
|
||||||
|
screen.routing.route = candidateUrl
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sanitise URL
|
||||||
|
screen.routing.route = sanitizeUrl(screen.routing.route)
|
||||||
|
|
||||||
|
// Use the currently selected role
|
||||||
|
screen.routing.roleId = get(selectedAccessRole) || "BASIC"
|
||||||
|
|
||||||
|
// Create the screen
|
||||||
|
await store.actions.screens.save(screen)
|
||||||
|
|
||||||
|
// Analytics
|
||||||
|
if (screen.template) {
|
||||||
|
analytics.captureEvent(Events.SCREEN.CREATED, {
|
||||||
|
template: screen.template,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add link in layout for list screens
|
||||||
|
if (screen.props._instanceName.endsWith("List")) {
|
||||||
await store.actions.components.links.save(
|
await store.actions.components.links.save(
|
||||||
draftScreen.routing.route,
|
screen.routing.route,
|
||||||
draftScreen.routing.route.split("/")[1]
|
screen.routing.route.split("/")[1]
|
||||||
)
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
notifications.error("Error creating link to screen")
|
notifications.error("Error creating screens")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
showProgressCircle = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Checks if any screens exist in the store with the given route and
|
||||||
|
// currently selected role
|
||||||
|
const hasExistingUrl = url => {
|
||||||
|
const roleId = get(selectedAccessRole) || "BASIC"
|
||||||
|
const screens = get(store).screens.filter(s => s.routing.roleId === roleId)
|
||||||
|
return !!screens.find(s => s.routing?.route === url)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Constructs a candidate URL for a new screen, suffixing the base of the
|
||||||
|
// screen's URL with a given suffix.
|
||||||
|
// e.g. "/sales/:id" => "/sales-1/:id"
|
||||||
|
const makeCandidateUrl = (screen, suffix) => {
|
||||||
|
let url = screen.routing?.route || ""
|
||||||
|
if (url.startsWith("/")) {
|
||||||
|
url = url.slice(1)
|
||||||
|
}
|
||||||
|
if (!url.includes("/")) {
|
||||||
|
return `/${url}-${suffix}`
|
||||||
|
} else {
|
||||||
|
const split = url.split("/")
|
||||||
|
return `/${split[0]}-${suffix}/${split.slice(1).join("/")}`
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const routeExists = (route, roleId) => {
|
// Handler for NewScreenModal
|
||||||
return $allScreens.some(
|
const confirmScreenSelection = async templates => {
|
||||||
screen =>
|
// Handle template selection
|
||||||
screen.routing.route.toLowerCase() === route.toLowerCase() &&
|
if (templates?.length > 1) {
|
||||||
screen.routing.roleId === roleId
|
// Autoscreens, so create immediately
|
||||||
)
|
const screens = templates.map(template => template.create())
|
||||||
}
|
await createScreens(screens)
|
||||||
|
} else {
|
||||||
export const showModal = () => {
|
// Empty screen, so proceed to the next modal
|
||||||
newScreenModal.show()
|
pendingScreen = templates[0].create()
|
||||||
}
|
|
||||||
|
|
||||||
const setScreens = evt => {
|
|
||||||
selectedScreens = evt.detail.screens
|
|
||||||
}
|
|
||||||
|
|
||||||
const chooseModal = index => {
|
|
||||||
/*
|
|
||||||
0 = newScreenModal
|
|
||||||
1 = screenDetailsModal
|
|
||||||
2 = navigationSelectionModal
|
|
||||||
*/
|
|
||||||
if (index === 0) {
|
|
||||||
newScreenModal.show()
|
|
||||||
} else if (index === 1) {
|
|
||||||
screenDetailsModal.show()
|
screenDetailsModal.show()
|
||||||
} else if (index === 2) {
|
|
||||||
navigationSelectionModal.show()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Handler for ScreenDetailsModal
|
||||||
|
const confirmScreenDetails = async ({ screenName, screenUrl }) => {
|
||||||
|
if (!pendingScreen) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
pendingScreen.props._instanceName = screenName
|
||||||
|
pendingScreen.routing.route = screenUrl
|
||||||
|
await createScreens([pendingScreen])
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Modal bind:this={newScreenModal}>
|
<Modal bind:this={newScreenModal}>
|
||||||
<NewScreenModal
|
<NewScreenModal onConfirm={confirmScreenSelection} {showProgressCircle} />
|
||||||
on:save={setScreens}
|
|
||||||
{showProgressCircle}
|
|
||||||
{save}
|
|
||||||
{chooseModal}
|
|
||||||
/>
|
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
||||||
<Modal bind:this={screenDetailsModal}>
|
<Modal bind:this={screenDetailsModal}>
|
||||||
<ScreenDetailsModal
|
<ScreenDetailsModal
|
||||||
bind:screenName
|
|
||||||
bind:url
|
|
||||||
{showProgressCircle}
|
{showProgressCircle}
|
||||||
{save}
|
onConfirm={confirmScreenDetails}
|
||||||
{chooseModal}
|
onCancel={() => newScreenModal.show()}
|
||||||
/>
|
/>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
|
@ -33,7 +33,7 @@
|
||||||
const customSections = settings.filter(setting => setting.section)
|
const customSections = settings.filter(setting => setting.section)
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
name: "General",
|
name: componentDefinition?.name || "General",
|
||||||
info: componentDefinition?.info,
|
info: componentDefinition?.info,
|
||||||
settings: generalSettings,
|
settings: generalSettings,
|
||||||
},
|
},
|
||||||
|
|
|
@ -12,11 +12,13 @@
|
||||||
import { getAvailableActions } from "./index"
|
import { getAvailableActions } from "./index"
|
||||||
import { generate } from "shortid"
|
import { generate } from "shortid"
|
||||||
import { getButtonContextBindings } from "builderStore/dataBinding"
|
import { getButtonContextBindings } from "builderStore/dataBinding"
|
||||||
|
import { currentAsset, store } from "builderStore"
|
||||||
|
|
||||||
const flipDurationMs = 150
|
const flipDurationMs = 150
|
||||||
const EVENT_TYPE_KEY = "##eventHandlerType"
|
const EVENT_TYPE_KEY = "##eventHandlerType"
|
||||||
const actionTypes = getAvailableActions()
|
const actionTypes = getAvailableActions()
|
||||||
|
|
||||||
|
export let key
|
||||||
export let actions
|
export let actions
|
||||||
export let bindings = []
|
export let bindings = []
|
||||||
|
|
||||||
|
@ -24,6 +26,9 @@
|
||||||
|
|
||||||
// These are ephemeral bindings which only exist while executing actions
|
// These are ephemeral bindings which only exist while executing actions
|
||||||
$: buttonContextBindings = getButtonContextBindings(
|
$: buttonContextBindings = getButtonContextBindings(
|
||||||
|
$currentAsset,
|
||||||
|
$store.selectedComponentId,
|
||||||
|
key,
|
||||||
actions,
|
actions,
|
||||||
selectedAction?.id
|
selectedAction?.id
|
||||||
)
|
)
|
||||||
|
|
|
@ -8,6 +8,7 @@
|
||||||
|
|
||||||
const dispatch = createEventDispatcher()
|
const dispatch = createEventDispatcher()
|
||||||
|
|
||||||
|
export let key
|
||||||
export let value = []
|
export let value = []
|
||||||
export let name
|
export let name
|
||||||
export let bindings
|
export let bindings
|
||||||
|
@ -81,5 +82,6 @@
|
||||||
bind:actions={tmpValue}
|
bind:actions={tmpValue}
|
||||||
eventType={name}
|
eventType={name}
|
||||||
{bindings}
|
{bindings}
|
||||||
|
{key}
|
||||||
/>
|
/>
|
||||||
</Drawer>
|
</Drawer>
|
||||||
|
|
|
@ -0,0 +1,78 @@
|
||||||
|
<script>
|
||||||
|
import { Select, Body } from "@budibase/bbui"
|
||||||
|
import { onMount } from "svelte"
|
||||||
|
import DrawerBindableInput from "components/common/bindings/DrawerBindableInput.svelte"
|
||||||
|
export let parameters
|
||||||
|
export let bindings
|
||||||
|
|
||||||
|
const typeOptions = [
|
||||||
|
{
|
||||||
|
label: "Continue if",
|
||||||
|
value: "continue",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Stop if",
|
||||||
|
value: "stop",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
const operatorOptions = [
|
||||||
|
{
|
||||||
|
label: "Equals",
|
||||||
|
value: "equal",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Not equals",
|
||||||
|
value: "notEqual",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
if (!parameters.type) {
|
||||||
|
parameters.type = "continue"
|
||||||
|
}
|
||||||
|
if (!parameters.operator) {
|
||||||
|
parameters.operator = "equal"
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="root">
|
||||||
|
<Body size="S">
|
||||||
|
Configure a condition to be evaluated which can stop further actions from
|
||||||
|
being executed.
|
||||||
|
</Body>
|
||||||
|
<Select
|
||||||
|
bind:value={parameters.type}
|
||||||
|
options={typeOptions}
|
||||||
|
placeholder={null}
|
||||||
|
/>
|
||||||
|
<DrawerBindableInput
|
||||||
|
placeholder="Value"
|
||||||
|
value={parameters.value}
|
||||||
|
on:change={e => (parameters.value = e.detail)}
|
||||||
|
{bindings}
|
||||||
|
/>
|
||||||
|
<Select
|
||||||
|
bind:value={parameters.operator}
|
||||||
|
options={operatorOptions}
|
||||||
|
placeholder={null}
|
||||||
|
/>
|
||||||
|
<DrawerBindableInput
|
||||||
|
placeholder="Reference value"
|
||||||
|
bind:value={parameters.referenceValue}
|
||||||
|
on:change={e => (parameters.referenceValue = e.detail)}
|
||||||
|
{bindings}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.root {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--spacing-l);
|
||||||
|
justify-content: flex-start;
|
||||||
|
align-items: stretch;
|
||||||
|
max-width: 400px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -5,7 +5,7 @@
|
||||||
|
|
||||||
export let parameters
|
export let parameters
|
||||||
|
|
||||||
$: components = findAllMatchingComponents($currentAsset.props, component =>
|
$: components = findAllMatchingComponents($currentAsset?.props, component =>
|
||||||
component._component.endsWith("s3upload")
|
component._component.endsWith("s3upload")
|
||||||
)
|
)
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -13,3 +13,4 @@ export { default as RefreshDataProvider } from "./RefreshDataProvider.svelte"
|
||||||
export { default as DuplicateRow } from "./DuplicateRow.svelte"
|
export { default as DuplicateRow } from "./DuplicateRow.svelte"
|
||||||
export { default as S3Upload } from "./S3Upload.svelte"
|
export { default as S3Upload } from "./S3Upload.svelte"
|
||||||
export { default as ExportData } from "./ExportData.svelte"
|
export { default as ExportData } from "./ExportData.svelte"
|
||||||
|
export { default as ContinueIf } from "./ContinueIf.svelte"
|
||||||
|
|
|
@ -84,6 +84,11 @@
|
||||||
{
|
{
|
||||||
"name": "Export Data",
|
"name": "Export Data",
|
||||||
"component": "ExportData"
|
"component": "ExportData"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Continue if / Stop if",
|
||||||
|
"component": "ContinueIf",
|
||||||
|
"dependsOnFeature": "continueIfAction"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
|
@ -10,7 +10,7 @@
|
||||||
const dispatch = createEventDispatcher()
|
const dispatch = createEventDispatcher()
|
||||||
const getValue = component => `{{ literal ${makePropSafe(component._id)} }}`
|
const getValue = component => `{{ literal ${makePropSafe(component._id)} }}`
|
||||||
|
|
||||||
$: path = findComponentPath($currentAsset.props, $store.selectedComponentId)
|
$: path = findComponentPath($currentAsset?.props, $store.selectedComponentId)
|
||||||
$: providers = path.filter(c => c._component?.endsWith("/dataprovider"))
|
$: providers = path.filter(c => c._component?.endsWith("/dataprovider"))
|
||||||
|
|
||||||
// Set initial value to closest data provider
|
// Set initial value to closest data provider
|
||||||
|
|
|
@ -18,9 +18,7 @@
|
||||||
let tempValue = value || []
|
let tempValue = value || []
|
||||||
|
|
||||||
$: dataSource = getDatasourceForProvider($currentAsset, componentInstance)
|
$: dataSource = getDatasourceForProvider($currentAsset, componentInstance)
|
||||||
$: schema = getSchemaForDatasource($currentAsset, dataSource, {
|
$: schema = getSchemaForDatasource($currentAsset, dataSource)?.schema
|
||||||
searchableSchema: true,
|
|
||||||
})?.schema
|
|
||||||
$: schemaFields = Object.values(schema || {})
|
$: schemaFields = Object.values(schema || {})
|
||||||
|
|
||||||
const saveFilter = async () => {
|
const saveFilter = async () => {
|
||||||
|
|
|
@ -12,7 +12,7 @@
|
||||||
export let type
|
export let type
|
||||||
|
|
||||||
$: form = findClosestMatchingComponent(
|
$: form = findClosestMatchingComponent(
|
||||||
$currentAsset.props,
|
$currentAsset?.props,
|
||||||
componentInstance._id,
|
componentInstance._id,
|
||||||
component => component._component === "@budibase/standard-components/form"
|
component => component._component === "@budibase/standard-components/form"
|
||||||
)
|
)
|
||||||
|
|
|
@ -79,6 +79,7 @@
|
||||||
bindings={allBindings}
|
bindings={allBindings}
|
||||||
name={key}
|
name={key}
|
||||||
text={label}
|
text={label}
|
||||||
|
{key}
|
||||||
{type}
|
{type}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -11,7 +11,7 @@
|
||||||
|
|
||||||
const resetFormFields = async () => {
|
const resetFormFields = async () => {
|
||||||
const form = findClosestMatchingComponent(
|
const form = findClosestMatchingComponent(
|
||||||
$currentAsset.props,
|
$currentAsset?.props,
|
||||||
componentInstance._id,
|
componentInstance._id,
|
||||||
component => component._component.endsWith("/form")
|
component => component._component.endsWith("/form")
|
||||||
)
|
)
|
||||||
|
|
|
@ -1,171 +0,0 @@
|
||||||
<script>
|
|
||||||
import analytics from "analytics"
|
|
||||||
import { createEventDispatcher } from "svelte"
|
|
||||||
import { fade, fly } from "svelte/transition"
|
|
||||||
import {
|
|
||||||
ActionButton,
|
|
||||||
ClearButton,
|
|
||||||
RadioGroup,
|
|
||||||
TextArea,
|
|
||||||
ButtonGroup,
|
|
||||||
Button,
|
|
||||||
Heading,
|
|
||||||
Detail,
|
|
||||||
Divider,
|
|
||||||
Layout,
|
|
||||||
notifications,
|
|
||||||
} from "@budibase/bbui"
|
|
||||||
import { auth } from "stores/portal"
|
|
||||||
|
|
||||||
let step = 0
|
|
||||||
let ratings = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
|
|
||||||
let options = [
|
|
||||||
"Importing / managing data",
|
|
||||||
"Designing",
|
|
||||||
"Automations",
|
|
||||||
"Managing users / groups",
|
|
||||||
"Deployment / hosting",
|
|
||||||
"Documentation",
|
|
||||||
]
|
|
||||||
|
|
||||||
const dispatch = createEventDispatcher()
|
|
||||||
|
|
||||||
// Data to send off
|
|
||||||
let rating
|
|
||||||
let improvements = ""
|
|
||||||
let comment = ""
|
|
||||||
|
|
||||||
function selectNumber(n) {
|
|
||||||
rating = n
|
|
||||||
step = 1
|
|
||||||
}
|
|
||||||
|
|
||||||
function submitFeedback() {
|
|
||||||
analytics.submitFeedback({
|
|
||||||
rating,
|
|
||||||
improvements,
|
|
||||||
comment,
|
|
||||||
})
|
|
||||||
try {
|
|
||||||
auth.updateSelf({
|
|
||||||
flags: {
|
|
||||||
feedbackSubmitted: true,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
} catch (error) {
|
|
||||||
notifications.error("Error updating user")
|
|
||||||
}
|
|
||||||
dispatch("complete")
|
|
||||||
}
|
|
||||||
|
|
||||||
function cancelFeedback() {
|
|
||||||
try {
|
|
||||||
auth.updateSelf({
|
|
||||||
flags: {
|
|
||||||
feedbackSubmitted: true,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
} catch (error) {
|
|
||||||
notifications.error("Error updating user")
|
|
||||||
}
|
|
||||||
dispatch("complete")
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<div
|
|
||||||
class="position"
|
|
||||||
in:fade={{ duration: 200 }}
|
|
||||||
out:fade|local={{ duration: 200 }}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="feedback-frame"
|
|
||||||
in:fly={{ y: 30, duration: 200 }}
|
|
||||||
out:fly|local={{ y: 30, duration: 200 }}
|
|
||||||
>
|
|
||||||
<div class="close">
|
|
||||||
<ClearButton on:click={cancelFeedback} />
|
|
||||||
</div>
|
|
||||||
<Layout gap="XS">
|
|
||||||
{#if step === 0}
|
|
||||||
<Heading size="XS"
|
|
||||||
>How likely are you to recommend Budibase to a colleague?</Heading
|
|
||||||
>
|
|
||||||
<Divider />
|
|
||||||
<div class="ratings">
|
|
||||||
{#each ratings as number}
|
|
||||||
<ActionButton
|
|
||||||
size="L"
|
|
||||||
emphasized
|
|
||||||
selected={number === rating}
|
|
||||||
on:click={() => selectNumber(number)}
|
|
||||||
>
|
|
||||||
{number}
|
|
||||||
</ActionButton>
|
|
||||||
{/each}
|
|
||||||
</div>
|
|
||||||
<div class="footer">
|
|
||||||
<Detail size="S">NOT LIKELY</Detail>
|
|
||||||
<Detail size="S">EXTREMELY LIKELY</Detail>
|
|
||||||
</div>
|
|
||||||
{:else if step === 1}
|
|
||||||
<Heading size="XS">What could be improved most in Budibase?</Heading>
|
|
||||||
<Divider />
|
|
||||||
<RadioGroup bind:value={improvements} {options} />
|
|
||||||
<div class="footer">
|
|
||||||
<Detail size="S">STEP 2 OF 3</Detail>
|
|
||||||
<ButtonGroup>
|
|
||||||
<Button secondary on:click={() => (step -= 1)}>Previous</Button>
|
|
||||||
<Button primary on:click={() => (step += 1)}>Next</Button>
|
|
||||||
</ButtonGroup>
|
|
||||||
</div>
|
|
||||||
{:else}
|
|
||||||
<Heading size="XS">How can we improve your experience?</Heading>
|
|
||||||
<Divider />
|
|
||||||
<TextArea bind:value={comment} placeholder="Add comments" />
|
|
||||||
<div class="footer">
|
|
||||||
<Detail size="S">STEP 3 OF 3</Detail>
|
|
||||||
<ButtonGroup>
|
|
||||||
<Button secondary on:click={() => (step -= 1)}>Previous</Button>
|
|
||||||
<Button cta on:click={submitFeedback}>Complete</Button>
|
|
||||||
</ButtonGroup>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
</Layout>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
.feedback-frame :global(textarea) {
|
|
||||||
min-height: 180px !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.position {
|
|
||||||
position: absolute;
|
|
||||||
right: var(--spacing-l);
|
|
||||||
bottom: calc(5 * var(--spacing-xl));
|
|
||||||
}
|
|
||||||
.feedback-frame {
|
|
||||||
position: absolute;
|
|
||||||
bottom: 0;
|
|
||||||
right: 0;
|
|
||||||
min-width: 510px;
|
|
||||||
background: var(--background);
|
|
||||||
border-radius: var(--spectrum-global-dimension-size-50);
|
|
||||||
border: 2px solid var(--spectrum-global-color-blue-400);
|
|
||||||
padding: var(--spacing-xl);
|
|
||||||
}
|
|
||||||
.ratings {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
}
|
|
||||||
.close {
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
right: 0;
|
|
||||||
}
|
|
||||||
.footer {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
</style>
|
|
|
@ -33,8 +33,7 @@
|
||||||
let parameters
|
let parameters
|
||||||
let data = []
|
let data = []
|
||||||
let saveId
|
let saveId
|
||||||
const transformerDocs =
|
const transformerDocs = "https://docs.budibase.com/docs/transformers"
|
||||||
"https://docs.budibase.com/building-apps/data/transformers"
|
|
||||||
|
|
||||||
$: datasource = $datasources.list.find(ds => ds._id === query.datasourceId)
|
$: datasource = $datasources.list.find(ds => ds._id === query.datasourceId)
|
||||||
$: query.schema = fieldsToSchema(fields)
|
$: query.schema = fieldsToSchema(fields)
|
||||||
|
|
|
@ -61,7 +61,7 @@
|
||||||
{#if app.deployed}Published{:else}Unpublished{/if}
|
{#if app.deployed}Published{:else}Unpublished{/if}
|
||||||
</StatusLight>
|
</StatusLight>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div data-cy={`row_actions_${app.appId}`}>
|
||||||
<Button
|
<Button
|
||||||
size="S"
|
size="S"
|
||||||
disabled={app.lockedOther}
|
disabled={app.lockedOther}
|
||||||
|
|
|
@ -9,17 +9,57 @@
|
||||||
import { goto } from "@roxi/routify"
|
import { goto } from "@roxi/routify"
|
||||||
import { createValidationStore } from "helpers/validation/yup"
|
import { createValidationStore } from "helpers/validation/yup"
|
||||||
import * as appValidation from "helpers/validation/yup/app"
|
import * as appValidation from "helpers/validation/yup/app"
|
||||||
|
import TemplateCard from "components/common/TemplateCard.svelte"
|
||||||
|
|
||||||
export let template
|
export let template
|
||||||
|
|
||||||
|
let creating = false
|
||||||
|
|
||||||
const values = writable({ name: "", url: null })
|
const values = writable({ name: "", url: null })
|
||||||
const validation = createValidationStore()
|
const validation = createValidationStore()
|
||||||
$: validation.check($values)
|
$: validation.check($values)
|
||||||
|
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
|
$values.name = resolveAppName(template, $values.name)
|
||||||
|
nameToUrl($values.name)
|
||||||
await setupValidation()
|
await setupValidation()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const appPrefix = "/app"
|
||||||
|
|
||||||
|
$: appUrl = `${window.location.origin}${
|
||||||
|
$values.url
|
||||||
|
? `${appPrefix}${$values.url}`
|
||||||
|
: `${appPrefix}${resolveAppUrl(template, $values.name)}`
|
||||||
|
}`
|
||||||
|
|
||||||
|
const resolveAppUrl = (template, name) => {
|
||||||
|
let parsedName
|
||||||
|
const resolvedName = resolveAppName(template, name)
|
||||||
|
parsedName = resolvedName ? resolvedName.toLowerCase() : ""
|
||||||
|
const parsedUrl = parsedName ? parsedName.replace(/\s+/g, "-") : ""
|
||||||
|
return encodeURI(parsedUrl)
|
||||||
|
}
|
||||||
|
|
||||||
|
const resolveAppName = (template, name) => {
|
||||||
|
if (template && !name) {
|
||||||
|
return template.name
|
||||||
|
}
|
||||||
|
return name ? name.trim() : null
|
||||||
|
}
|
||||||
|
|
||||||
|
const tidyUrl = url => {
|
||||||
|
if (url && !url.startsWith("/")) {
|
||||||
|
url = `/${url}`
|
||||||
|
}
|
||||||
|
$values.url = url === "" ? null : url
|
||||||
|
}
|
||||||
|
|
||||||
|
const nameToUrl = appName => {
|
||||||
|
let resolvedUrl = resolveAppUrl(template, appName)
|
||||||
|
tidyUrl(resolvedUrl)
|
||||||
|
}
|
||||||
|
|
||||||
const setupValidation = async () => {
|
const setupValidation = async () => {
|
||||||
const applications = svelteGet(apps)
|
const applications = svelteGet(apps)
|
||||||
appValidation.name(validation, { apps: applications })
|
appValidation.name(validation, { apps: applications })
|
||||||
|
@ -30,6 +70,8 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
async function createNewApp() {
|
async function createNewApp() {
|
||||||
|
creating = true
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Create form data to create app
|
// Create form data to create app
|
||||||
let data = new FormData()
|
let data = new FormData()
|
||||||
|
@ -64,17 +106,11 @@
|
||||||
await auth.setInitInfo({})
|
await auth.setInitInfo({})
|
||||||
$goto(`/builder/app/${createdApp.instance._id}`)
|
$goto(`/builder/app/${createdApp.instance._id}`)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
creating = false
|
||||||
console.error(error)
|
console.error(error)
|
||||||
notifications.error("Error creating app")
|
notifications.error("Error creating app")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// auto add slash to url
|
|
||||||
$: {
|
|
||||||
if ($values.url && !$values.url.startsWith("/")) {
|
|
||||||
$values.url = `/${$values.url}`
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<ModalContent
|
<ModalContent
|
||||||
|
@ -83,6 +119,15 @@
|
||||||
onConfirm={createNewApp}
|
onConfirm={createNewApp}
|
||||||
disabled={!$validation.valid}
|
disabled={!$validation.valid}
|
||||||
>
|
>
|
||||||
|
{#if template && !template?.fromFile}
|
||||||
|
<TemplateCard
|
||||||
|
name={template.name}
|
||||||
|
imageSrc={template.image}
|
||||||
|
backgroundColour={template.background}
|
||||||
|
overlayEnabled={false}
|
||||||
|
icon={template.icon}
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
{#if template?.fromFile}
|
{#if template?.fromFile}
|
||||||
<Dropzone
|
<Dropzone
|
||||||
error={$validation.touched.file && $validation.errors.file}
|
error={$validation.touched.file && $validation.errors.file}
|
||||||
|
@ -97,20 +142,42 @@
|
||||||
{/if}
|
{/if}
|
||||||
<Input
|
<Input
|
||||||
bind:value={$values.name}
|
bind:value={$values.name}
|
||||||
|
disabled={creating}
|
||||||
error={$validation.touched.name && $validation.errors.name}
|
error={$validation.touched.name && $validation.errors.name}
|
||||||
on:blur={() => ($validation.touched.name = true)}
|
on:blur={() => ($validation.touched.name = true)}
|
||||||
|
on:change={nameToUrl($values.name)}
|
||||||
label="Name"
|
label="Name"
|
||||||
placeholder={$auth.user.firstName
|
placeholder={$auth.user?.firstName
|
||||||
? `${$auth.user.firstName}s app`
|
? `${$auth.user.firstName}s app`
|
||||||
: "My app"}
|
: "My app"}
|
||||||
/>
|
/>
|
||||||
|
<span>
|
||||||
<Input
|
<Input
|
||||||
bind:value={$values.url}
|
bind:value={$values.url}
|
||||||
|
disabled={creating}
|
||||||
error={$validation.touched.url && $validation.errors.url}
|
error={$validation.touched.url && $validation.errors.url}
|
||||||
on:blur={() => ($validation.touched.url = true)}
|
on:blur={() => ($validation.touched.url = true)}
|
||||||
|
on:change={tidyUrl($values.url)}
|
||||||
label="URL"
|
label="URL"
|
||||||
placeholder={$values.name
|
placeholder={$values.url
|
||||||
? "/" + encodeURIComponent($values.name).toLowerCase()
|
? $values.url
|
||||||
: "/"}
|
: `/${resolveAppUrl(template, $values.name)}`}
|
||||||
/>
|
/>
|
||||||
|
{#if $values.url && $values.url !== "" && !$validation.errors.url}
|
||||||
|
<div class="app-server" title={appUrl}>
|
||||||
|
{appUrl}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</span>
|
||||||
</ModalContent>
|
</ModalContent>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.app-server {
|
||||||
|
color: var(--spectrum-global-color-gray-600);
|
||||||
|
margin-top: 10px;
|
||||||
|
width: 320px;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
|
@ -42,11 +42,31 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// auto add slash to url
|
const resolveAppUrl = (template, name) => {
|
||||||
$: {
|
let parsedName
|
||||||
if ($values.url && !$values.url.startsWith("/")) {
|
const resolvedName = resolveAppName(null, name)
|
||||||
$values.url = `/${$values.url}`
|
parsedName = resolvedName ? resolvedName.toLowerCase() : ""
|
||||||
|
const parsedUrl = parsedName ? parsedName.replace(/\s+/g, "-") : ""
|
||||||
|
return encodeURI(parsedUrl)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const resolveAppName = (template, name) => {
|
||||||
|
if (template && !name) {
|
||||||
|
return template.name
|
||||||
|
}
|
||||||
|
return name ? name.trim() : null
|
||||||
|
}
|
||||||
|
|
||||||
|
const tidyUrl = url => {
|
||||||
|
if (url && !url.startsWith("/")) {
|
||||||
|
url = `/${url}`
|
||||||
|
}
|
||||||
|
$values.url = url === "" ? null : url
|
||||||
|
}
|
||||||
|
|
||||||
|
const nameToUrl = appName => {
|
||||||
|
let resolvedUrl = resolveAppUrl(null, appName)
|
||||||
|
tidyUrl(resolvedUrl)
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
@ -61,15 +81,17 @@
|
||||||
bind:value={$values.name}
|
bind:value={$values.name}
|
||||||
error={$validation.touched.name && $validation.errors.name}
|
error={$validation.touched.name && $validation.errors.name}
|
||||||
on:blur={() => ($validation.touched.name = true)}
|
on:blur={() => ($validation.touched.name = true)}
|
||||||
|
on:change={nameToUrl($values.name)}
|
||||||
label="Name"
|
label="Name"
|
||||||
/>
|
/>
|
||||||
<Input
|
<Input
|
||||||
bind:value={$values.url}
|
bind:value={$values.url}
|
||||||
error={$validation.touched.url && $validation.errors.url}
|
error={$validation.touched.url && $validation.errors.url}
|
||||||
on:blur={() => ($validation.touched.url = true)}
|
on:blur={() => ($validation.touched.url = true)}
|
||||||
|
on:change={tidyUrl($values.url)}
|
||||||
label="URL"
|
label="URL"
|
||||||
placeholder={$values.name
|
placeholder={$values.url
|
||||||
? "/" + encodeURIComponent($values.name).toLowerCase()
|
? $values.url
|
||||||
: "/"}
|
: `/${resolveAppUrl(null, $values.name)}`}
|
||||||
/>
|
/>
|
||||||
</ModalContent>
|
</ModalContent>
|
||||||
|
|
|
@ -178,6 +178,7 @@ export const IntegrationTypes = {
|
||||||
ORACLE: "ORACLE",
|
ORACLE: "ORACLE",
|
||||||
INTERNAL: "INTERNAL",
|
INTERNAL: "INTERNAL",
|
||||||
GOOGLE_SHEETS: "GOOGLE_SHEETS",
|
GOOGLE_SHEETS: "GOOGLE_SHEETS",
|
||||||
|
FIREBASE: "FIREBASE",
|
||||||
}
|
}
|
||||||
|
|
||||||
export const IntegrationNames = {
|
export const IntegrationNames = {
|
||||||
|
@ -195,6 +196,7 @@ export const IntegrationNames = {
|
||||||
[IntegrationTypes.ORACLE]: "Oracle",
|
[IntegrationTypes.ORACLE]: "Oracle",
|
||||||
[IntegrationTypes.INTERNAL]: "Internal",
|
[IntegrationTypes.INTERNAL]: "Internal",
|
||||||
[IntegrationTypes.GOOGLE_SHEETS]: "Google Sheets",
|
[IntegrationTypes.GOOGLE_SHEETS]: "Google Sheets",
|
||||||
|
[IntegrationTypes.FIREBASE]: "Firebase",
|
||||||
}
|
}
|
||||||
|
|
||||||
export const SchemaTypeOptions = [
|
export const SchemaTypeOptions = [
|
||||||
|
|
|
@ -35,13 +35,14 @@ export const url = (validation, { apps, currentApp } = { apps: [] }) => {
|
||||||
validation.addValidator(
|
validation.addValidator(
|
||||||
"url",
|
"url",
|
||||||
string()
|
string()
|
||||||
|
.trim()
|
||||||
.nullable()
|
.nullable()
|
||||||
.matches(APP_URL_REGEX, "App URL must not contain spaces")
|
.required("Your application must have a url")
|
||||||
|
.matches(APP_URL_REGEX, "Please enter a valid url")
|
||||||
.test(
|
.test(
|
||||||
"non-existing-app-url",
|
"non-existing-app-url",
|
||||||
"Another app with the same URL already exists",
|
"Another app with the same URL already exists",
|
||||||
value => {
|
value => {
|
||||||
// url is nullable
|
|
||||||
if (!value) {
|
if (!value) {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,7 +5,6 @@
|
||||||
import DeployModal from "components/deploy/DeployModal.svelte"
|
import DeployModal from "components/deploy/DeployModal.svelte"
|
||||||
import RevertModal from "components/deploy/RevertModal.svelte"
|
import RevertModal from "components/deploy/RevertModal.svelte"
|
||||||
import VersionModal from "components/deploy/VersionModal.svelte"
|
import VersionModal from "components/deploy/VersionModal.svelte"
|
||||||
import NPSFeedbackForm from "components/feedback/NPSFeedbackForm.svelte"
|
|
||||||
import { API } from "api"
|
import { API } from "api"
|
||||||
import { auth, admin } from "stores/portal"
|
import { auth, admin } from "stores/portal"
|
||||||
import { isActive, goto, layout, redirect } from "@roxi/routify"
|
import { isActive, goto, layout, redirect } from "@roxi/routify"
|
||||||
|
@ -21,15 +20,11 @@
|
||||||
|
|
||||||
// Sync once when you load the app
|
// Sync once when you load the app
|
||||||
let hasSynced = false
|
let hasSynced = false
|
||||||
let userShouldPostFeedback = false
|
|
||||||
$: selected = capitalise(
|
$: selected = capitalise(
|
||||||
$layout.children.find(layout => $isActive(layout.path))?.title ?? "data"
|
$layout.children.find(layout => $isActive(layout.path))?.title ?? "data"
|
||||||
)
|
)
|
||||||
|
|
||||||
function previewApp() {
|
function previewApp() {
|
||||||
if (!$auth?.user?.flags?.feedbackSubmitted) {
|
|
||||||
userShouldPostFeedback = true
|
|
||||||
}
|
|
||||||
window.open(`/${application}`)
|
window.open(`/${application}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -126,10 +121,6 @@
|
||||||
<p>Something went wrong: {error.message}</p>
|
<p>Something went wrong: {error.message}</p>
|
||||||
{/await}
|
{/await}
|
||||||
|
|
||||||
{#if userShouldPostFeedback}
|
|
||||||
<NPSFeedbackForm on:complete={() => (userShouldPostFeedback = false)} />
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.loading {
|
.loading {
|
||||||
min-height: 100%;
|
min-height: 100%;
|
||||||
|
|
|
@ -23,10 +23,8 @@
|
||||||
<div class="nav">
|
<div class="nav">
|
||||||
<Tabs {selected} on:select={selectFirstDatasource}>
|
<Tabs {selected} on:select={selectFirstDatasource}>
|
||||||
<Tab title="Sources">
|
<Tab title="Sources">
|
||||||
<div class="tab-content-padding">
|
|
||||||
<DatasourceNavigator />
|
<DatasourceNavigator />
|
||||||
<CreateDatasourceModal bind:modal />
|
<CreateDatasourceModal bind:modal />
|
||||||
</div>
|
|
||||||
</Tab>
|
</Tab>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
<div
|
<div
|
||||||
|
@ -63,10 +61,6 @@
|
||||||
display: contents;
|
display: contents;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tab-content-padding {
|
|
||||||
padding: 0 var(--spacing-xl);
|
|
||||||
}
|
|
||||||
|
|
||||||
.nav {
|
.nav {
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
background: var(--background);
|
background: var(--background);
|
||||||
|
|
|
@ -415,9 +415,7 @@
|
||||||
<Banner
|
<Banner
|
||||||
extraButtonText="Learn more"
|
extraButtonText="Learn more"
|
||||||
extraButtonAction={() =>
|
extraButtonAction={() =>
|
||||||
window.open(
|
window.open("https://docs.budibase.com/docs/transformers")}
|
||||||
"https://docs.budibase.com/building-apps/data/transformers"
|
|
||||||
)}
|
|
||||||
on:change={() => updateFlag("queryTransformerBanner", true)}
|
on:change={() => updateFlag("queryTransformerBanner", true)}
|
||||||
>
|
>
|
||||||
Add a JavaScript function to transform the query result.
|
Add a JavaScript function to transform the query result.
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue