Merge branch 'feature/licensing' into feature/posthog-v2
This commit is contained in:
commit
33719fcb09
|
@ -0,0 +1,9 @@
|
||||||
|
packages/server/node_modules
|
||||||
|
packages/builder
|
||||||
|
packages/frontend-core
|
||||||
|
packages/backend-core
|
||||||
|
packages/worker/node_modules
|
||||||
|
packages/cli
|
||||||
|
packages/client
|
||||||
|
packages/bbui
|
||||||
|
packages/string-templates
|
|
@ -24,6 +24,22 @@
|
||||||
{
|
{
|
||||||
"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": {
|
||||||
|
|
|
@ -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,14 @@ 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
|
continue-on-error: true
|
||||||
|
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 +42,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": [
|
||||||
{
|
{
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -106,6 +106,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
|
||||||
|
|
|
@ -1894,9 +1894,9 @@ minimist-options@4.1.0:
|
||||||
kind-of "^6.0.3"
|
kind-of "^6.0.3"
|
||||||
|
|
||||||
minimist@^1.2.0:
|
minimist@^1.2.0:
|
||||||
version "1.2.5"
|
version "1.2.6"
|
||||||
resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602"
|
resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.6.tgz#8637a5b759ea0d6e98702cfb3a9283323c93af44"
|
||||||
integrity sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==
|
integrity sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==
|
||||||
|
|
||||||
minipass-collect@^1.0.2:
|
minipass-collect@^1.0.2:
|
||||||
version "1.0.2"
|
version "1.0.2"
|
||||||
|
|
|
@ -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
|
||||||
|
@ -116,7 +117,6 @@ services:
|
||||||
labels:
|
labels:
|
||||||
- "com.centurylinklabs.watchtower.enable=false"
|
- "com.centurylinklabs.watchtower.enable=false"
|
||||||
|
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
couchdb3_data:
|
couchdb3_data:
|
||||||
driver: local
|
driver: local
|
||||||
|
|
|
@ -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 = / {
|
||||||
|
|
|
@ -0,0 +1,94 @@
|
||||||
|
{
|
||||||
|
"version": "2",
|
||||||
|
"templates": [
|
||||||
|
{
|
||||||
|
"type": 3,
|
||||||
|
"title": "Budibase",
|
||||||
|
"categories": ["Tools"],
|
||||||
|
"description": "Build modern business apps in minutes",
|
||||||
|
"logo": "https://budibase.com/favicon.ico",
|
||||||
|
"platform": "linux",
|
||||||
|
"repository": {
|
||||||
|
"url": "https://github.com/Budibase/budibase",
|
||||||
|
"stackfile": "hosting/docker-compose.yaml"
|
||||||
|
},
|
||||||
|
"env": [
|
||||||
|
{
|
||||||
|
"name": "MAIN_PORT",
|
||||||
|
"label": "Main port",
|
||||||
|
"default": "10000"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "JWT_SECRET",
|
||||||
|
"label": "JWT secret",
|
||||||
|
"default": "change-me"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "MINIO_ACCESS_KEY",
|
||||||
|
"label": "MinIO access key",
|
||||||
|
"default": "change-me"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "MINIO_SECRET_KEY",
|
||||||
|
"label": "MinIO secret key",
|
||||||
|
"default": "change-me"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "COUCH_DB_USER",
|
||||||
|
"default": "budibase",
|
||||||
|
"preset": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "COUCH_DB_PASSWORD",
|
||||||
|
"label": "Couch DB password",
|
||||||
|
"default": "change-me"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "REDIS_PASSWORD",
|
||||||
|
"label": "Redis password",
|
||||||
|
"default": "change-me"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "INTERNAL_API_KEY",
|
||||||
|
"label": "Internal API key",
|
||||||
|
"default": "change-me"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "APP_PORT",
|
||||||
|
"default": "4002",
|
||||||
|
"preset": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "WORKER_PORT",
|
||||||
|
"default": "4003",
|
||||||
|
"preset": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "MINIO_PORT",
|
||||||
|
"default": "4004",
|
||||||
|
"preset": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "COUCH_DB_PORT",
|
||||||
|
"default": "4005",
|
||||||
|
"preset": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "REDIS_PORT",
|
||||||
|
"default": "6379",
|
||||||
|
"preset": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "WATCHTOWER_PORT",
|
||||||
|
"default": "6161",
|
||||||
|
"preset": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "BUDIBASE_ENVIRONMENT",
|
||||||
|
"default": "PRODUCTION",
|
||||||
|
"preset": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
|
@ -0,0 +1,97 @@
|
||||||
|
FROM couchdb
|
||||||
|
|
||||||
|
ENV COUCHDB_PASSWORD=budibase
|
||||||
|
ENV COUCHDB_USER=budibase
|
||||||
|
ENV COUCH_DB_URL=http://budibase:budibase@localhost:5984
|
||||||
|
ENV BUDIBASE_ENVIRONMENT=PRODUCTION
|
||||||
|
ENV MINIO_URL=http://localhost:9000
|
||||||
|
ENV REDIS_URL=localhost:6379
|
||||||
|
ENV WORKER_URL=http://localhost:4002
|
||||||
|
ENV INTERNAL_API_KEY=budibase
|
||||||
|
ENV JWT_SECRET=testsecret
|
||||||
|
ENV MINIO_ACCESS_KEY=budibase
|
||||||
|
ENV MINIO_SECRET_KEY=budibase
|
||||||
|
ENV SELF_HOSTED=1
|
||||||
|
ENV CLUSTER_PORT=10000
|
||||||
|
ENV REDIS_PASSWORD=budibase
|
||||||
|
ENV ARCHITECTURE=amd
|
||||||
|
ENV APP_PORT=4001
|
||||||
|
ENV WORKER_PORT=4002
|
||||||
|
|
||||||
|
RUN apt-get update
|
||||||
|
RUN apt-get install software-properties-common wget nginx -y
|
||||||
|
RUN apt-add-repository 'deb http://security.debian.org/debian-security stretch/updates main'
|
||||||
|
RUN apt-get update
|
||||||
|
|
||||||
|
# setup nginx
|
||||||
|
ADD hosting/single/nginx.conf /etc/nginx
|
||||||
|
RUN mkdir /etc/nginx/logs
|
||||||
|
RUN useradd www
|
||||||
|
RUN touch /etc/nginx/logs/error.log
|
||||||
|
RUN touch /etc/nginx/logs/nginx.pid
|
||||||
|
|
||||||
|
# install java
|
||||||
|
RUN apt-get install openjdk-8-jdk -y
|
||||||
|
|
||||||
|
# setup nodejs
|
||||||
|
WORKDIR /nodejs
|
||||||
|
RUN curl -sL https://deb.nodesource.com/setup_16.x -o /tmp/nodesource_setup.sh
|
||||||
|
RUN bash /tmp/nodesource_setup.sh
|
||||||
|
RUN apt-get install nodejs
|
||||||
|
RUN npm install --global yarn
|
||||||
|
RUN npm install --global pm2
|
||||||
|
|
||||||
|
# setup redis
|
||||||
|
RUN apt install redis-server -y
|
||||||
|
|
||||||
|
# setup server
|
||||||
|
WORKDIR /app
|
||||||
|
ADD packages/server .
|
||||||
|
RUN ls -al
|
||||||
|
RUN yarn
|
||||||
|
RUN yarn build
|
||||||
|
# Install client for oracle datasource
|
||||||
|
RUN apt-get install unzip libaio1
|
||||||
|
RUN /bin/bash -e scripts/integrations/oracle/instantclient/linux/x86-64/install.sh
|
||||||
|
|
||||||
|
# setup worker
|
||||||
|
WORKDIR /worker
|
||||||
|
ADD packages/worker .
|
||||||
|
RUN yarn
|
||||||
|
RUN yarn build
|
||||||
|
|
||||||
|
# setup clouseau
|
||||||
|
WORKDIR /
|
||||||
|
RUN wget https://github.com/cloudant-labs/clouseau/releases/download/2.21.0/clouseau-2.21.0-dist.zip
|
||||||
|
RUN unzip clouseau-2.21.0-dist.zip
|
||||||
|
RUN mv clouseau-2.21.0 /opt/clouseau
|
||||||
|
RUN rm clouseau-2.21.0-dist.zip
|
||||||
|
|
||||||
|
WORKDIR /opt/clouseau
|
||||||
|
RUN mkdir ./bin
|
||||||
|
ADD hosting/single/clouseau ./bin/
|
||||||
|
ADD hosting/single/log4j.properties .
|
||||||
|
ADD hosting/single/clouseau.ini .
|
||||||
|
RUN chmod +x ./bin/clouseau
|
||||||
|
|
||||||
|
# setup CouchDB
|
||||||
|
WORKDIR /opt/couchdb
|
||||||
|
ADD hosting/single/vm.args ./etc/
|
||||||
|
|
||||||
|
# setup minio
|
||||||
|
WORKDIR /minio
|
||||||
|
RUN wget https://dl.min.io/server/minio/release/linux-${ARCHITECTURE}64/minio
|
||||||
|
RUN chmod +x minio
|
||||||
|
|
||||||
|
# setup runner file
|
||||||
|
WORKDIR /
|
||||||
|
ADD hosting/single/runner.sh .
|
||||||
|
RUN chmod +x ./runner.sh
|
||||||
|
|
||||||
|
EXPOSE 10000
|
||||||
|
VOLUME /opt/couchdb/data
|
||||||
|
VOLUME /minio
|
||||||
|
|
||||||
|
# must set this just before running
|
||||||
|
ENV NODE_ENV=production
|
||||||
|
CMD ["./runner.sh"]
|
|
@ -0,0 +1,12 @@
|
||||||
|
#!/bin/sh
|
||||||
|
/usr/bin/java -server \
|
||||||
|
-Xmx2G \
|
||||||
|
-Dsun.net.inetaddr.ttl=30 \
|
||||||
|
-Dsun.net.inetaddr.negative.ttl=30 \
|
||||||
|
-Dlog4j.configuration=file:/opt/clouseau/log4j.properties \
|
||||||
|
-XX:OnOutOfMemoryError="kill -9 %p" \
|
||||||
|
-XX:+UseConcMarkSweepGC \
|
||||||
|
-XX:+CMSParallelRemarkEnabled \
|
||||||
|
-classpath '/opt/clouseau/*' \
|
||||||
|
com.cloudant.clouseau.Main \
|
||||||
|
/opt/clouseau/clouseau.ini
|
|
@ -0,0 +1,13 @@
|
||||||
|
[clouseau]
|
||||||
|
|
||||||
|
; the name of the Erlang node created by the service, leave this unchanged
|
||||||
|
name=clouseau@127.0.0.1
|
||||||
|
|
||||||
|
; set this to the same distributed Erlang cookie used by the CouchDB nodes
|
||||||
|
cookie=monster
|
||||||
|
|
||||||
|
; the path where you would like to store the search index files
|
||||||
|
dir=/opt/couchdb/data/search
|
||||||
|
|
||||||
|
; the number of search indexes that can be open simultaneously
|
||||||
|
max_indexes_open=500
|
|
@ -0,0 +1,4 @@
|
||||||
|
log4j.rootLogger=debug, CONSOLE
|
||||||
|
log4j.appender.CONSOLE=org.apache.log4j.ConsoleAppender
|
||||||
|
log4j.appender.CONSOLE.layout=org.apache.log4j.PatternLayout
|
||||||
|
log4j.appender.CONSOLE.layout.ConversionPattern=%d{ISO8601} %c [%p] %m%n
|
|
@ -0,0 +1,116 @@
|
||||||
|
user www www;
|
||||||
|
error_log /etc/nginx/logs/error.log;
|
||||||
|
pid /etc/nginx/logs/nginx.pid;
|
||||||
|
worker_processes auto;
|
||||||
|
worker_rlimit_nofile 8192;
|
||||||
|
|
||||||
|
events {
|
||||||
|
worker_connections 1024;
|
||||||
|
}
|
||||||
|
|
||||||
|
http {
|
||||||
|
limit_req_zone $binary_remote_addr zone=ratelimit:10m rate=20r/s;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
charset utf-8;
|
||||||
|
sendfile on;
|
||||||
|
tcp_nopush on;
|
||||||
|
tcp_nodelay on;
|
||||||
|
server_tokens off;
|
||||||
|
types_hash_max_size 2048;
|
||||||
|
|
||||||
|
# buffering
|
||||||
|
client_header_buffer_size 1k;
|
||||||
|
client_max_body_size 20M;
|
||||||
|
ignore_invalid_headers off;
|
||||||
|
proxy_buffering off;
|
||||||
|
|
||||||
|
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
|
||||||
|
'$status $body_bytes_sent "$http_referer" '
|
||||||
|
'"$http_user_agent" "$http_x_forwarded_for"';
|
||||||
|
|
||||||
|
map $http_upgrade $connection_upgrade {
|
||||||
|
default "upgrade";
|
||||||
|
}
|
||||||
|
|
||||||
|
server {
|
||||||
|
listen 10000 default_server;
|
||||||
|
listen [::]:10000 default_server;
|
||||||
|
server_name _;
|
||||||
|
client_max_body_size 1000m;
|
||||||
|
ignore_invalid_headers off;
|
||||||
|
proxy_buffering off;
|
||||||
|
# port_in_redirect off;
|
||||||
|
|
||||||
|
location /app {
|
||||||
|
proxy_pass http://127.0.0.1:4001;
|
||||||
|
}
|
||||||
|
|
||||||
|
location = / {
|
||||||
|
proxy_pass http://127.0.0.1:4001;
|
||||||
|
}
|
||||||
|
|
||||||
|
location ~ ^/(builder|app_) {
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
proxy_set_header Connection $connection_upgrade;
|
||||||
|
proxy_set_header Upgrade $http_upgrade;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_pass http://127.0.0.1:4001;
|
||||||
|
}
|
||||||
|
|
||||||
|
location ~ ^/api/(system|admin|global)/ {
|
||||||
|
proxy_pass http://127.0.0.1:4002;
|
||||||
|
}
|
||||||
|
|
||||||
|
location /worker/ {
|
||||||
|
proxy_pass http://127.0.0.1:4002;
|
||||||
|
rewrite ^/worker/(.*)$ /$1 break;
|
||||||
|
}
|
||||||
|
|
||||||
|
location /api/ {
|
||||||
|
# calls to the API are rate limited with bursting
|
||||||
|
limit_req zone=ratelimit burst=20 nodelay;
|
||||||
|
|
||||||
|
# 120s timeout on API requests
|
||||||
|
proxy_read_timeout 120s;
|
||||||
|
proxy_connect_timeout 120s;
|
||||||
|
proxy_send_timeout 120s;
|
||||||
|
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
proxy_set_header Connection $connection_upgrade;
|
||||||
|
proxy_set_header Upgrade $http_upgrade;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
|
||||||
|
proxy_pass http://127.0.0.1:4001;
|
||||||
|
}
|
||||||
|
|
||||||
|
location /db/ {
|
||||||
|
proxy_pass http://127.0.0.1:5984;
|
||||||
|
rewrite ^/db/(.*)$ /$1 break;
|
||||||
|
}
|
||||||
|
|
||||||
|
location / {
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
|
||||||
|
proxy_connect_timeout 300;
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
proxy_set_header Connection "";
|
||||||
|
chunked_transfer_encoding off;
|
||||||
|
proxy_pass http://127.0.0.1:9000;
|
||||||
|
}
|
||||||
|
|
||||||
|
client_header_timeout 60;
|
||||||
|
client_body_timeout 60;
|
||||||
|
keepalive_timeout 60;
|
||||||
|
|
||||||
|
# gzip
|
||||||
|
gzip on;
|
||||||
|
gzip_vary on;
|
||||||
|
gzip_proxied any;
|
||||||
|
gzip_comp_level 6;
|
||||||
|
gzip_types text/plain text/css text/xml application/json application/javascript application/rss+xml application/atom+xml image/svg+xml;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,16 @@
|
||||||
|
redis-server --requirepass $REDIS_PASSWORD &
|
||||||
|
/opt/clouseau/bin/clouseau &
|
||||||
|
/minio/minio server /minio &
|
||||||
|
/docker-entrypoint.sh /opt/couchdb/bin/couchdb &
|
||||||
|
/etc/init.d/nginx restart
|
||||||
|
pushd app
|
||||||
|
pm2 start --name app "yarn run:docker"
|
||||||
|
popd
|
||||||
|
pushd worker
|
||||||
|
pm2 start --name worker "yarn run:docker"
|
||||||
|
popd
|
||||||
|
sleep 10
|
||||||
|
URL=http://${COUCHDB_USER}:${COUCHDB_PASSWORD}@localhost:5984
|
||||||
|
curl -X PUT ${URL}/_users
|
||||||
|
curl -X PUT ${URL}/_replicator
|
||||||
|
sleep infinity
|
|
@ -0,0 +1,32 @@
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License"); you may not
|
||||||
|
# use this file except in compliance with the License. You may obtain a copy of
|
||||||
|
# the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
# License for the specific language governing permissions and limitations under
|
||||||
|
# the License.
|
||||||
|
|
||||||
|
# erlang cookie for clouseau security
|
||||||
|
-name couchdb@127.0.0.1
|
||||||
|
-setcookie monster
|
||||||
|
|
||||||
|
# Ensure that the Erlang VM listens on a known port
|
||||||
|
-kernel inet_dist_listen_min 9100
|
||||||
|
-kernel inet_dist_listen_max 9100
|
||||||
|
|
||||||
|
# Tell kernel and SASL not to log anything
|
||||||
|
-kernel error_logger silent
|
||||||
|
-sasl sasl_error_logger false
|
||||||
|
|
||||||
|
# Use kernel poll functionality if supported by emulator
|
||||||
|
+K true
|
||||||
|
|
||||||
|
# Start a pool of asynchronous IO threads
|
||||||
|
+A 16
|
||||||
|
|
||||||
|
# Comment this line out to enable the interactive Erlang shell on startup
|
||||||
|
+Bd -noinput
|
|
@ -1,5 +1,5 @@
|
||||||
{
|
{
|
||||||
"version": "1.0.91-alpha.17",
|
"version": "1.0.105-alpha.10",
|
||||||
"npmClient": "yarn",
|
"npmClient": "yarn",
|
||||||
"packages": [
|
"packages": [
|
||||||
"packages/*"
|
"packages/*"
|
||||||
|
|
|
@ -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",
|
||||||
|
@ -41,8 +43,7 @@
|
||||||
"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",
|
||||||
|
@ -55,6 +56,8 @@
|
||||||
"build:docker:develop": "node scripts/pinVersions && lerna run build:docker && npm run build:docker:proxy:compose && cd hosting/scripts/linux/ && ./release-to-docker-hub.sh develop && cd -",
|
"build:docker:develop": "node scripts/pinVersions && lerna run build:docker && npm run build:docker:proxy:compose && cd hosting/scripts/linux/ && ./release-to-docker-hub.sh develop && cd -",
|
||||||
"build:docker:airgap": "node hosting/scripts/airgapped/airgappedDockerBuild",
|
"build:docker:airgap": "node hosting/scripts/airgapped/airgappedDockerBuild",
|
||||||
"build:digitalocean": "cd hosting/digitalocean && ./build.sh && cd -",
|
"build:digitalocean": "cd hosting/digitalocean && ./build.sh && cd -",
|
||||||
|
"build:docker:single:image": "docker build -f hosting/single/Dockerfile -t budibase:latest .",
|
||||||
|
"build:docker:single": "lerna run build && lerna run predocker && npm run build:docker:single:image",
|
||||||
"build:docs": "lerna run build:docs",
|
"build:docs": "lerna run build:docs",
|
||||||
"release:helm": "node scripts/releaseHelmChart",
|
"release:helm": "node scripts/releaseHelmChart",
|
||||||
"env:multi:enable": "lerna run env:multi:enable",
|
"env:multi:enable": "lerna run env:multi:enable",
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@budibase/backend-core",
|
"name": "@budibase/backend-core",
|
||||||
"version": "1.0.91-alpha.17",
|
"version": "1.0.105-alpha.10",
|
||||||
"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 { getDB } = require("../db")
|
const { getDB } = 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 { getDB, allDbs } = require("./index")
|
const { getDB, allDbs } = require("./index")
|
||||||
const { getCouchUrl } = require("./pouch")
|
const { getCouchUrl } = require("./pouch")
|
||||||
|
@ -41,7 +37,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 (config, callbackUrl, saveUserFn) {
|
||||||
)
|
)
|
||||||
} 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,
|
||||||
|
|
|
@ -14,22 +14,7 @@ function makeSessionID(userId, sessionId) {
|
||||||
return `${userId}/${sessionId}`
|
return `${userId}/${sessionId}`
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.createASession = async (userId, session) => {
|
async function invalidateSessions(userId, sessionIds = null) {
|
||||||
const client = await redis.getSessionClient()
|
|
||||||
const sessionId = session.sessionId
|
|
||||||
if (!session.csrfToken) {
|
|
||||||
session.csrfToken = uuidv4()
|
|
||||||
}
|
|
||||||
session = {
|
|
||||||
createdAt: new Date().toISOString(),
|
|
||||||
lastAccessedAt: new Date().toISOString(),
|
|
||||||
...session,
|
|
||||||
userId,
|
|
||||||
}
|
|
||||||
await client.store(makeSessionID(userId, sessionId), session, EXPIRY_SECONDS)
|
|
||||||
}
|
|
||||||
|
|
||||||
exports.invalidateSessions = async (userId, sessionIds = null) => {
|
|
||||||
let sessions = []
|
let sessions = []
|
||||||
|
|
||||||
// If no sessionIds, get all the sessions for the user
|
// If no sessionIds, get all the sessions for the user
|
||||||
|
@ -55,6 +40,24 @@ exports.invalidateSessions = async (userId, sessionIds = null) => {
|
||||||
await Promise.all(promises)
|
await Promise.all(promises)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
exports.createASession = async (userId, session) => {
|
||||||
|
// invalidate all other sessions
|
||||||
|
await invalidateSessions(userId)
|
||||||
|
|
||||||
|
const client = await redis.getSessionClient()
|
||||||
|
const sessionId = session.sessionId
|
||||||
|
if (!session.csrfToken) {
|
||||||
|
session.csrfToken = uuidv4()
|
||||||
|
}
|
||||||
|
session = {
|
||||||
|
createdAt: new Date().toISOString(),
|
||||||
|
lastAccessedAt: new Date().toISOString(),
|
||||||
|
...session,
|
||||||
|
userId,
|
||||||
|
}
|
||||||
|
await client.store(makeSessionID(userId, sessionId), session, EXPIRY_SECONDS)
|
||||||
|
}
|
||||||
|
|
||||||
exports.updateSessionTTL = async session => {
|
exports.updateSessionTTL = async session => {
|
||||||
const client = await redis.getSessionClient()
|
const client = await redis.getSessionClient()
|
||||||
const key = makeSessionID(session.userId, session.sessionId)
|
const key = makeSessionID(session.userId, session.sessionId)
|
||||||
|
@ -67,8 +70,6 @@ exports.endSession = async (userId, sessionId) => {
|
||||||
await client.delete(makeSessionID(userId, sessionId))
|
await client.delete(makeSessionID(userId, sessionId))
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.getUserSessions = getSessionsForUser
|
|
||||||
|
|
||||||
exports.getSession = async (userId, sessionId) => {
|
exports.getSession = async (userId, sessionId) => {
|
||||||
try {
|
try {
|
||||||
const client = await redis.getSessionClient()
|
const client = await redis.getSessionClient()
|
||||||
|
@ -84,3 +85,6 @@ exports.getAllSessions = async () => {
|
||||||
const sessions = await client.scan()
|
const sessions = await client.scan()
|
||||||
return sessions.map(session => session.value)
|
return sessions.map(session => session.value)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
exports.getUserSessions = getSessionsForUser
|
||||||
|
exports.invalidateSessions = invalidateSessions
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,9 @@
|
||||||
const { DocumentTypes, SEPARATOR, ViewNames } = require("./db/utils")
|
const {
|
||||||
|
DocumentTypes,
|
||||||
|
SEPARATOR,
|
||||||
|
ViewNames,
|
||||||
|
getAllApps,
|
||||||
|
} = require("./db/utils")
|
||||||
const jwt = require("jsonwebtoken")
|
const jwt = require("jsonwebtoken")
|
||||||
const { options } = require("./middleware/passport/jwt")
|
const { options } = require("./middleware/passport/jwt")
|
||||||
const { queryGlobalView } = require("./db/views")
|
const { queryGlobalView } = require("./db/views")
|
||||||
|
@ -7,8 +12,10 @@ const env = require("./environment")
|
||||||
const userCache = require("./cache/user")
|
const userCache = require("./cache/user")
|
||||||
const { getUserSessions, invalidateSessions } = require("./security/sessions")
|
const { getUserSessions, invalidateSessions } = require("./security/sessions")
|
||||||
const events = require("./events")
|
const events = require("./events")
|
||||||
|
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)
|
||||||
|
@ -16,16 +23,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)
|
||||||
|
@ -34,16 +60,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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -3619,9 +3619,9 @@ mimic-fn@^2.1.0:
|
||||||
brace-expansion "^1.1.7"
|
brace-expansion "^1.1.7"
|
||||||
|
|
||||||
minimist@^1.1.1, minimist@^1.2.0, minimist@^1.2.5:
|
minimist@^1.1.1, minimist@^1.2.0, minimist@^1.2.5:
|
||||||
version "1.2.5"
|
version "1.2.6"
|
||||||
resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602"
|
resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.6.tgz#8637a5b759ea0d6e98702cfb3a9283323c93af44"
|
||||||
integrity sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==
|
integrity sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==
|
||||||
|
|
||||||
mixin-deep@^1.2.0:
|
mixin-deep@^1.2.0:
|
||||||
version "1.3.2"
|
version "1.3.2"
|
||||||
|
|
|
@ -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.91-alpha.17",
|
"version": "1.0.105-alpha.10",
|
||||||
"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.91-alpha.17",
|
"@budibase/string-templates": "^1.0.105-alpha.10",
|
||||||
"@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)}
|
||||||
|
|
|
@ -19,18 +19,33 @@
|
||||||
const dispatch = createEventDispatcher()
|
const dispatch = createEventDispatcher()
|
||||||
const flatpickrId = `${uuid()}-wrapper`
|
const flatpickrId = `${uuid()}-wrapper`
|
||||||
let open = false
|
let open = false
|
||||||
let flatpickr, flatpickrOptions, isTimeOnly
|
let flatpickr, flatpickrOptions
|
||||||
|
|
||||||
|
const resolveTimeStamp = timestamp => {
|
||||||
|
let maskedDate = new Date(`0-${timestamp}`)
|
||||||
|
|
||||||
|
if (maskedDate instanceof Date && !isNaN(maskedDate.getTime())) {
|
||||||
|
return maskedDate
|
||||||
|
} else {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
$: isTimeOnly = !timeOnly && value ? !isNaN(new Date(`0-${value}`)) : timeOnly
|
|
||||||
$: flatpickrOptions = {
|
$: flatpickrOptions = {
|
||||||
element: `#${flatpickrId}`,
|
element: `#${flatpickrId}`,
|
||||||
enableTime: isTimeOnly || enableTime || false,
|
enableTime: timeOnly || enableTime || false,
|
||||||
noCalendar: isTimeOnly || false,
|
noCalendar: timeOnly || false,
|
||||||
altInput: true,
|
altInput: true,
|
||||||
altFormat: isTimeOnly ? "H:i" : enableTime ? "F j Y, H:i" : "F j, Y",
|
altFormat: timeOnly ? "H:i" : enableTime ? "F j Y, H:i" : "F j, Y",
|
||||||
wrap: true,
|
wrap: true,
|
||||||
appendTo,
|
appendTo,
|
||||||
disableMobile: "true",
|
disableMobile: "true",
|
||||||
|
onReady: () => {
|
||||||
|
let timestamp = resolveTimeStamp(value)
|
||||||
|
if (timeOnly && timestamp) {
|
||||||
|
dispatch("change", timestamp.toISOString())
|
||||||
|
}
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleChange = event => {
|
const handleChange = event => {
|
||||||
|
@ -39,10 +54,9 @@
|
||||||
if (newValue) {
|
if (newValue) {
|
||||||
newValue = newValue.toISOString()
|
newValue = newValue.toISOString()
|
||||||
}
|
}
|
||||||
// if time only set date component to today
|
// if time only set date component to 2000-01-01
|
||||||
if (timeOnly) {
|
if (timeOnly) {
|
||||||
const todayDate = new Date().toISOString().split("T")[0]
|
newValue = `2000-01-01T${newValue.split("T")[1]}`
|
||||||
newValue = `${todayDate}T${newValue.split("T")[1]}`
|
|
||||||
}
|
}
|
||||||
dispatch("change", newValue)
|
dispatch("change", newValue)
|
||||||
}
|
}
|
||||||
|
@ -76,10 +90,13 @@
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
let date
|
let date
|
||||||
let time = new Date(`0-${val}`)
|
let time
|
||||||
|
|
||||||
// it is a string like 00:00:00, just time
|
// it is a string like 00:00:00, just time
|
||||||
if (timeOnly || (typeof val === "string" && !isNaN(time))) {
|
let ts = resolveTimeStamp(val)
|
||||||
date = time
|
|
||||||
|
if (timeOnly && ts) {
|
||||||
|
date = ts
|
||||||
} else if (val instanceof Date) {
|
} else if (val instanceof Date) {
|
||||||
// Use real date obj if already parsed
|
// Use real date obj if already parsed
|
||||||
date = val
|
date = val
|
||||||
|
@ -101,7 +118,7 @@
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#key isTimeOnly}
|
{#key timeOnly}
|
||||||
<Flatpickr
|
<Flatpickr
|
||||||
bind:flatpickr
|
bind:flatpickr
|
||||||
value={parseDate(value)}
|
value={parseDate(value)}
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -1574,9 +1574,9 @@ minimatch@^3.0.4:
|
||||||
brace-expansion "^1.1.7"
|
brace-expansion "^1.1.7"
|
||||||
|
|
||||||
minimist@^1.2.0, minimist@^1.2.5:
|
minimist@^1.2.0, minimist@^1.2.5:
|
||||||
version "1.2.5"
|
version "1.2.6"
|
||||||
resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602"
|
resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.6.tgz#8637a5b759ea0d6e98702cfb3a9283323c93af44"
|
||||||
integrity sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==
|
integrity sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==
|
||||||
|
|
||||||
mkdirp@~0.5.1:
|
mkdirp@~0.5.1:
|
||||||
version "0.5.5"
|
version "0.5.5"
|
||||||
|
|
|
@ -8,7 +8,7 @@ filterTests(['all'], () => {
|
||||||
|
|
||||||
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,164 @@ 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", () => {
|
|
||||||
|
before(() => {
|
||||||
cy.login()
|
cy.login()
|
||||||
cy.createTestApp()
|
cy.deleteApp("Cypress Tests")
|
||||||
cy.visit(`${Cypress.config().baseUrl}/builder`)
|
|
||||||
cy.contains("Cypress Tests").should("exist")
|
|
||||||
})
|
})
|
||||||
|
|
||||||
|
if (!(Cypress.env("TEST_ENV"))) {
|
||||||
|
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)
|
||||||
|
|
||||||
|
if (Cypress.env("TEST_ENV")) {
|
||||||
|
cy.get(".spectrum-Button").contains("Templates").click({force: true})
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
// Start create app process. If apps already exist, click second button
|
||||||
|
cy.get(`[data-cy="create-app-btn"]`).click({ force: true })
|
||||||
|
cy.request(`${Cypress.config().baseUrl}/api/applications?status=all`)
|
||||||
|
.its("body")
|
||||||
|
.then(val => {
|
||||||
|
if (val.length > 0) {
|
||||||
|
cy.get(`[data-cy="create-app-btn"]`).click({ force: true })
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const appName = "Cypress Tests"
|
||||||
|
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", "/cypress-tests")
|
||||||
|
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.createApp(appName)
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
// Navigate to Create new app section if apps already exist
|
||||||
|
cy.request(`${Cypress.config().baseUrl}/api/applications?status=all`)
|
||||||
|
.its("body")
|
||||||
|
.then(val => {
|
||||||
|
if (val.length > 0) {
|
||||||
|
cy.get(`[data-cy="create-app-btn"]`).click({ force: true })
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
cy.get(".template-category-filters").should("exist")
|
||||||
|
cy.get(".template-categories").should("exist")
|
||||||
|
|
||||||
|
// Select template
|
||||||
|
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.deleteApp(templateNameText)
|
||||||
|
});
|
||||||
|
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should display a second application and app filtering", () => {
|
||||||
|
// Create first app
|
||||||
|
const appName = "Cypress Tests"
|
||||||
|
cy.createApp(appName)
|
||||||
|
|
||||||
|
cy.visit(`${Cypress.config().baseUrl}/builder`)
|
||||||
|
cy.wait(500)
|
||||||
|
|
||||||
|
// Create second app
|
||||||
|
const secondAppName = "Second App Demo"
|
||||||
|
cy.createApp(secondAppName)
|
||||||
|
|
||||||
|
cy.visit(`${Cypress.config().baseUrl}/builder`)
|
||||||
|
cy.wait(500)
|
||||||
|
|
||||||
|
//Both applications should exist and be searchable
|
||||||
|
cy.searchForApplication(appName)
|
||||||
|
cy.searchForApplication(secondAppName)
|
||||||
|
|
||||||
|
cy.deleteApp(secondAppName)
|
||||||
|
})
|
||||||
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
|
@ -7,6 +7,8 @@ filterTests(["smoke", "all"], () => {
|
||||||
})
|
})
|
||||||
|
|
||||||
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 +23,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 +33,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 +120,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,7 @@ filterTests(['smoke', 'all'], () => {
|
||||||
context("Create a View", () => {
|
context("Create a View", () => {
|
||||||
before(() => {
|
before(() => {
|
||||||
cy.login()
|
cy.login()
|
||||||
|
|
||||||
cy.createTestApp()
|
cy.createTestApp()
|
||||||
cy.createTable("data")
|
cy.createTable("data")
|
||||||
cy.addColumn("data", "group", "Text")
|
cy.addColumn("data", "group", "Text")
|
||||||
|
|
|
@ -4,8 +4,7 @@ filterTests(["smoke", "all"], () => {
|
||||||
context("Query Level Transformers", () => {
|
context("Query Level Transformers", () => {
|
||||||
before(() => {
|
before(() => {
|
||||||
cy.login()
|
cy.login()
|
||||||
cy.deleteApp("Cypress Tests")
|
cy.createTestApp()
|
||||||
cy.createApp("Cypress Tests")
|
|
||||||
})
|
})
|
||||||
|
|
||||||
it("should write a transformer function", () => {
|
it("should write a transformer function", () => {
|
||||||
|
|
|
@ -11,12 +11,14 @@ filterTests(['all'], () => {
|
||||||
const appName = "Cypress Tests"
|
const appName = "Cypress Tests"
|
||||||
const appRename = "Cypress Renamed"
|
const appRename = "Cypress Renamed"
|
||||||
// Rename app, Search for app, Confirm name was changed
|
// Rename app, Search for app, Confirm name was changed
|
||||||
cy.get(".home-logo").click()
|
cy.visit(`${Cypress.config().baseUrl}/builder`)
|
||||||
|
cy.wait(500)
|
||||||
renameApp(appName, appRename)
|
renameApp(appName, appRename)
|
||||||
cy.reload()
|
cy.reload()
|
||||||
cy.wait(1000)
|
cy.wait(1000)
|
||||||
cy.searchForApplication(appRename)
|
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,31 +31,31 @@ 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.visit(`${Cypress.config().baseUrl}/builder`)
|
||||||
|
cy.wait(500)
|
||||||
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", () => {
|
||||||
const appName = "Cypress Tests"
|
const appName = "Cypress Tests"
|
||||||
cy.get(".home-logo").click()
|
cy.visit(`${Cypress.config().baseUrl}/builder`)
|
||||||
|
cy.wait(500)
|
||||||
renameApp(appName, " ", false, true)
|
renameApp(appName, " ", false, true)
|
||||||
cy.wait(500)
|
cy.wait(500)
|
||||||
// Close modal and confirm name has not been changed
|
// Close modal and confirm name has not been changed
|
||||||
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)
|
|
||||||
|
|
||||||
})
|
})
|
||||||
|
|
||||||
xit("Should create two applications with the same name", () => {
|
xit("Should create two applications with the same name", () => {
|
||||||
|
@ -61,12 +63,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")
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
@ -77,12 +79,12 @@ filterTests(['all'], () => {
|
||||||
const appName = "Cypress Tests"
|
const appName = "Cypress Tests"
|
||||||
const numberName = 12345
|
const numberName = 12345
|
||||||
const specialCharName = "£$%^"
|
const specialCharName = "£$%^"
|
||||||
cy.get(".home-logo").click()
|
cy.visit(`${Cypress.config().baseUrl}/builder`)
|
||||||
|
cy.wait(500)
|
||||||
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.reload()
|
cy.reload()
|
||||||
cy.wait(1000)
|
cy.wait(1000)
|
||||||
renameApp(numberName, specialCharName)
|
renameApp(numberName, specialCharName)
|
||||||
|
@ -95,16 +97,12 @@ filterTests(['all'], () => {
|
||||||
|
|
||||||
const renameApp = (originalName, changedName, published, noName) => {
|
const renameApp = (originalName, changedName, published, noName) => {
|
||||||
cy.searchForApplication(originalName)
|
cy.searchForApplication(originalName)
|
||||||
cy.request(`${Cypress.config().baseUrl}/api/applications?status=all`)
|
|
||||||
.its("body")
|
|
||||||
.then(val => {
|
|
||||||
if (val.length > 0) {
|
|
||||||
cy.get(".appTable")
|
cy.get(".appTable")
|
||||||
.within(() => {
|
.within(() => {
|
||||||
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 +112,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,12 +120,9 @@ 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,17 @@ 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 })
|
||||||
|
|
||||||
|
// If apps already exist
|
||||||
|
cy.request(`${Cypress.config().baseUrl}/api/applications?status=all`)
|
||||||
|
.its("body")
|
||||||
|
.then(val => {
|
||||||
|
if (val.length > 0) {
|
||||||
|
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 +61,30 @@ Cypress.Commands.add("deleteApp", name => {
|
||||||
.its("body")
|
.its("body")
|
||||||
.then(val => {
|
.then(val => {
|
||||||
if (val.length > 0) {
|
if (val.length > 0) {
|
||||||
|
if (Cypress.env("TEST_ENV")) {
|
||||||
cy.searchForApplication(name)
|
cy.searchForApplication(name)
|
||||||
cy.get(".appTable").within(() => {
|
cy.get(".appTable").within(() => {
|
||||||
cy.get(".spectrum-Icon").eq(1).click()
|
cy.get(".spectrum-Icon").eq(1).click()
|
||||||
})
|
})
|
||||||
|
} else {
|
||||||
|
const appId = val.reduce((acc, app) => {
|
||||||
|
if (name === app.name) {
|
||||||
|
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 +110,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 +216,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"
|
||||||
)
|
)
|
||||||
|
@ -312,16 +340,46 @@ 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.visit(`${Cypress.config().baseUrl}/builder`)
|
||||||
|
cy.wait(2000)
|
||||||
|
|
||||||
|
// No app filter functionality if only 1 app exists
|
||||||
|
cy.request(`${Cypress.config().baseUrl}/api/applications?status=all`)
|
||||||
|
.its("body")
|
||||||
|
.then(val => {
|
||||||
|
if (val.length < 2) {
|
||||||
|
return
|
||||||
|
} else {
|
||||||
// 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
|
}
|
||||||
cy.get(".appTable").contains(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.91-alpha.17",
|
"version": "1.0.105-alpha.10",
|
||||||
"license": "GPL-3.0",
|
"license": "GPL-3.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
@ -65,10 +65,10 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@budibase/bbui": "^1.0.91-alpha.17",
|
"@budibase/bbui": "^1.0.105-alpha.10",
|
||||||
"@budibase/client": "^1.0.91-alpha.17",
|
"@budibase/client": "^1.0.105-alpha.10",
|
||||||
"@budibase/frontend-core": "^1.0.91-alpha.17",
|
"@budibase/frontend-core": "^1.0.105-alpha.10",
|
||||||
"@budibase/string-templates": "^1.0.91-alpha.17",
|
"@budibase/string-templates": "^1.0.105-alpha.10",
|
||||||
"@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",
|
||||||
|
|
|
@ -7,7 +7,11 @@ import {
|
||||||
getComponentSettings,
|
getComponentSettings,
|
||||||
} from "./componentUtils"
|
} from "./componentUtils"
|
||||||
import { store } from "builderStore"
|
import { store } from "builderStore"
|
||||||
import { queries as queriesStores, tables as tablesStore } from "stores/backend"
|
import {
|
||||||
|
queries as queriesStores,
|
||||||
|
tables as tablesStore,
|
||||||
|
roles as rolesStore,
|
||||||
|
} from "stores/backend"
|
||||||
import {
|
import {
|
||||||
makePropSafe,
|
makePropSafe,
|
||||||
isJSBinding,
|
isJSBinding,
|
||||||
|
@ -33,6 +37,7 @@ export const getBindableProperties = (asset, componentId) => {
|
||||||
const deviceBindings = getDeviceBindings()
|
const deviceBindings = getDeviceBindings()
|
||||||
const stateBindings = getStateBindings()
|
const stateBindings = getStateBindings()
|
||||||
const selectedRowsBindings = getSelectedRowsBindings(asset)
|
const selectedRowsBindings = getSelectedRowsBindings(asset)
|
||||||
|
const roleBindings = getRoleBindings()
|
||||||
return [
|
return [
|
||||||
...contextBindings,
|
...contextBindings,
|
||||||
...urlBindings,
|
...urlBindings,
|
||||||
|
@ -40,6 +45,7 @@ export const getBindableProperties = (asset, componentId) => {
|
||||||
...userBindings,
|
...userBindings,
|
||||||
...deviceBindings,
|
...deviceBindings,
|
||||||
...selectedRowsBindings,
|
...selectedRowsBindings,
|
||||||
|
...roleBindings,
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -391,20 +397,57 @@ const getUrlBindings = asset => {
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const getRoleBindings = () => {
|
||||||
|
return (get(rolesStore) || []).map(role => {
|
||||||
|
return {
|
||||||
|
type: "context",
|
||||||
|
runtimeBinding: `trim "${role._id}"`,
|
||||||
|
readableBinding: `Role.${role.name}`,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 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 +461,7 @@ export const getButtonContextBindings = (actions, actionId) => {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
return bindings
|
return bindings
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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: "",
|
||||||
|
|
|
@ -2,9 +2,15 @@ export default function (url) {
|
||||||
return url
|
return url
|
||||||
.split("/")
|
.split("/")
|
||||||
.map(part => {
|
.map(part => {
|
||||||
// if parameter, then use as is
|
part = decodeURIComponent(part)
|
||||||
if (part.startsWith(":")) return part
|
part = part.replace(/ /g, "-")
|
||||||
return encodeURIComponent(part.replace(/ /g, "-"))
|
|
||||||
|
// If parameter, then use as is
|
||||||
|
if (!part.startsWith(":")) {
|
||||||
|
part = encodeURIComponent(part)
|
||||||
|
}
|
||||||
|
|
||||||
|
return part
|
||||||
})
|
})
|
||||||
.join("/")
|
.join("/")
|
||||||
.toLowerCase()
|
.toLowerCase()
|
||||||
|
|
|
@ -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)
|
||||||
|
|
||||||
|
@ -21,6 +22,7 @@ export const getThemeStore = () => {
|
||||||
`spectrum--${option}`,
|
`spectrum--${option}`,
|
||||||
option === state.theme
|
option === state.theme
|
||||||
)
|
)
|
||||||
|
themeElement.classList.add("spectrum--darkest")
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -19,10 +19,22 @@
|
||||||
export let value = defaultValue || (meta.type === "boolean" ? false : "")
|
export let value = defaultValue || (meta.type === "boolean" ? false : "")
|
||||||
export let readonly
|
export let readonly
|
||||||
|
|
||||||
|
const resolveTimeStamp = timestamp => {
|
||||||
|
let maskedDate = new Date(`0-${timestamp}`)
|
||||||
|
if (maskedDate instanceof Date && !isNaN(maskedDate.getTime())) {
|
||||||
|
return maskedDate
|
||||||
|
} else {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
$: stringVal =
|
$: stringVal =
|
||||||
typeof value === "object" ? JSON.stringify(value, null, 2) : value
|
typeof value === "object" ? JSON.stringify(value, null, 2) : value
|
||||||
$: type = meta?.type
|
$: type = meta?.type
|
||||||
$: label = meta.name ? capitalise(meta.name) : ""
|
$: label = meta.name ? capitalise(meta.name) : ""
|
||||||
|
|
||||||
|
const timeStamp = resolveTimeStamp(value)
|
||||||
|
const isTimeStamp = timeStamp ? true : false
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if type === "options"}
|
{#if type === "options"}
|
||||||
|
@ -34,7 +46,7 @@
|
||||||
sort
|
sort
|
||||||
/>
|
/>
|
||||||
{:else if type === "datetime"}
|
{:else if type === "datetime"}
|
||||||
<DatePicker {label} bind:value />
|
<DatePicker {label} timeOnly={isTimeStamp} bind:value />
|
||||||
{:else if type === "attachment"}
|
{:else if type === "attachment"}
|
||||||
<Dropzone {label} bind:value />
|
<Dropzone {label} bind:value />
|
||||||
{:else if type === "boolean"}
|
{:else if type === "boolean"}
|
||||||
|
|
|
@ -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,118 @@
|
||||||
|
<script>
|
||||||
|
export let backgroundColour
|
||||||
|
export let imageSrc
|
||||||
|
export let name
|
||||||
|
export let icon
|
||||||
|
export let overlayEnabled = true
|
||||||
|
|
||||||
|
let imageError = false
|
||||||
|
|
||||||
|
const imageRenderError = () => {
|
||||||
|
imageError = true
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="template-card" style="background-color:{backgroundColour};">
|
||||||
|
<div class="template-thumbnail card-body">
|
||||||
|
<img
|
||||||
|
alt={name}
|
||||||
|
src={imageSrc}
|
||||||
|
on:error={imageRenderError}
|
||||||
|
class:error={imageError}
|
||||||
|
/>
|
||||||
|
<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 {
|
||||||
|
display: block;
|
||||||
|
max-width: 100%;
|
||||||
|
border-radius: var(--border-radius-s) 0px var(--border-radius-s) 0px;
|
||||||
|
}
|
||||||
|
.template-card img.error {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.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>
|
|
@ -51,7 +51,7 @@
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="control">
|
<div class="control" class:disabled>
|
||||||
<Combobox
|
<Combobox
|
||||||
{label}
|
{label}
|
||||||
{disabled}
|
{disabled}
|
||||||
|
@ -121,4 +121,8 @@
|
||||||
background-color: var(--spectrum-global-color-gray-50);
|
background-color: var(--spectrum-global-color-gray-50);
|
||||||
border-color: var(--spectrum-alias-border-color-hover);
|
border-color: var(--spectrum-alias-border-color-hover);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.control:not(.disabled) :global(.spectrum-Textfield-input) {
|
||||||
|
padding-right: 40px;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -36,7 +36,7 @@
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="control">
|
<div class="control" class:disabled>
|
||||||
<Input
|
<Input
|
||||||
{label}
|
{label}
|
||||||
{disabled}
|
{disabled}
|
||||||
|
@ -103,4 +103,8 @@
|
||||||
background-color: var(--spectrum-global-color-gray-50);
|
background-color: var(--spectrum-global-color-gray-50);
|
||||||
border-color: var(--spectrum-alias-border-color-hover);
|
border-color: var(--spectrum-alias-border-color-hover);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.control:not(.disabled) :global(.spectrum-Textfield-input) {
|
||||||
|
padding-right: 40px;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -3,13 +3,14 @@
|
||||||
import PathTree from "./PathTree.svelte"
|
import PathTree from "./PathTree.svelte"
|
||||||
|
|
||||||
let routes = {}
|
let routes = {}
|
||||||
$: paths = Object.keys(routes || {}).sort()
|
let paths = []
|
||||||
|
|
||||||
$: {
|
$: allRoutes = $store.routes
|
||||||
const allRoutes = $store.routes
|
$: selectedScreenId = $store.selectedScreenId
|
||||||
|
$: updatePaths(allRoutes, $selectedAccessRole, selectedScreenId)
|
||||||
|
|
||||||
|
const updatePaths = (allRoutes, selectedRoleId, selectedScreenId) => {
|
||||||
const sortedPaths = Object.keys(allRoutes || {}).sort()
|
const sortedPaths = Object.keys(allRoutes || {}).sort()
|
||||||
const selectedRoleId = $selectedAccessRole
|
|
||||||
const selectedScreenId = $store.selectedScreenId
|
|
||||||
|
|
||||||
let found = false
|
let found = false
|
||||||
let firstValidScreenId
|
let firstValidScreenId
|
||||||
|
@ -41,11 +42,15 @@
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
routes = filteredRoutes
|
routes = { ...filteredRoutes }
|
||||||
|
paths = Object.keys(routes || {}).sort()
|
||||||
|
|
||||||
// Select the correct role for the current screen ID
|
// Select the correct role for the current screen ID
|
||||||
if (!found && screenRoleId) {
|
if (!found && screenRoleId) {
|
||||||
selectedAccessRole.set(screenRoleId)
|
selectedAccessRole.set(screenRoleId)
|
||||||
|
if (screenRoleId !== selectedRoleId) {
|
||||||
|
updatePaths(allRoutes, screenRoleId, selectedScreenId)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// If the selected screen isn't in this filtered list, select the first one
|
// If the selected screen isn't in this filtered list, select the first one
|
||||||
|
|
|
@ -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>
|
|
@ -19,7 +19,7 @@
|
||||||
.filter(a => a.definition.trigger?.stepId === "APP")
|
.filter(a => a.definition.trigger?.stepId === "APP")
|
||||||
.map(automation => {
|
.map(automation => {
|
||||||
const schema = Object.entries(
|
const schema = Object.entries(
|
||||||
automation.definition.trigger.inputs.fields
|
automation.definition.trigger.inputs.fields || {}
|
||||||
).map(([name, type]) => ({ name, type }))
|
).map(([name, type]) => ({ name, type }))
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
|
@ -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"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
|
@ -79,6 +79,7 @@
|
||||||
bindings={allBindings}
|
bindings={allBindings}
|
||||||
name={key}
|
name={key}
|
||||||
text={label}
|
text={label}
|
||||||
|
{key}
|
||||||
{type}
|
{type}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -3,6 +3,7 @@
|
||||||
import { roles } from "stores/backend"
|
import { roles } from "stores/backend"
|
||||||
|
|
||||||
export let value
|
export let value
|
||||||
|
export let error
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Select
|
<Select
|
||||||
|
@ -11,4 +12,5 @@
|
||||||
options={$roles}
|
options={$roles}
|
||||||
getOptionLabel={role => role.name}
|
getOptionLabel={role => role.name}
|
||||||
getOptionValue={role => role._id}
|
getOptionValue={role => role._id}
|
||||||
|
{error}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -8,14 +8,50 @@
|
||||||
import { currentAsset, store } from "builderStore"
|
import { currentAsset, store } from "builderStore"
|
||||||
import { FrontendTypes } from "constants"
|
import { FrontendTypes } from "constants"
|
||||||
import sanitizeUrl from "builderStore/store/screenTemplates/utils/sanitizeUrl"
|
import sanitizeUrl from "builderStore/store/screenTemplates/utils/sanitizeUrl"
|
||||||
|
import { allScreens, selectedAccessRole } from "builderStore"
|
||||||
|
|
||||||
export let componentInstance
|
export let componentInstance
|
||||||
export let bindings
|
export let bindings
|
||||||
|
|
||||||
function setAssetProps(name, value, parser) {
|
let errors = {}
|
||||||
if (parser && typeof parser === "function") {
|
|
||||||
|
const routeTaken = url => {
|
||||||
|
const roleId = get(selectedAccessRole) || "BASIC"
|
||||||
|
return get(allScreens).some(
|
||||||
|
screen =>
|
||||||
|
screen.routing.route.toLowerCase() === url.toLowerCase() &&
|
||||||
|
screen.routing.roleId === roleId
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const roleTaken = roleId => {
|
||||||
|
const url = get(currentAsset)?.routing.route
|
||||||
|
return get(allScreens).some(
|
||||||
|
screen =>
|
||||||
|
screen.routing.route.toLowerCase() === url.toLowerCase() &&
|
||||||
|
screen.routing.roleId === roleId
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const setAssetProps = (name, value, parser, validate) => {
|
||||||
|
if (parser) {
|
||||||
value = parser(value)
|
value = parser(value)
|
||||||
}
|
}
|
||||||
|
if (validate) {
|
||||||
|
const error = validate(value)
|
||||||
|
errors = {
|
||||||
|
...errors,
|
||||||
|
[name]: error,
|
||||||
|
}
|
||||||
|
if (error) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
errors = {
|
||||||
|
...errors,
|
||||||
|
[name]: null,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const selectedAsset = get(currentAsset)
|
const selectedAsset = get(currentAsset)
|
||||||
store.update(state => {
|
store.update(state => {
|
||||||
|
@ -38,7 +74,6 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
const screenSettings = [
|
const screenSettings = [
|
||||||
// { key: "description", label: "Description", control: Input },
|
|
||||||
{
|
{
|
||||||
key: "routing.route",
|
key: "routing.route",
|
||||||
label: "Route",
|
label: "Route",
|
||||||
|
@ -49,8 +84,26 @@
|
||||||
}
|
}
|
||||||
return sanitizeUrl(val)
|
return sanitizeUrl(val)
|
||||||
},
|
},
|
||||||
|
validate: val => {
|
||||||
|
const exisingValue = get(currentAsset)?.routing.route
|
||||||
|
if (val !== exisingValue && routeTaken(val)) {
|
||||||
|
return "That URL is already in use for this role"
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "routing.roleId",
|
||||||
|
label: "Access",
|
||||||
|
control: RoleSelect,
|
||||||
|
validate: val => {
|
||||||
|
const exisingValue = get(currentAsset)?.routing.roleId
|
||||||
|
if (val !== exisingValue && roleTaken(val)) {
|
||||||
|
return "That role is already in use for this URL"
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{ key: "routing.roleId", label: "Access", control: RoleSelect },
|
|
||||||
{ key: "layoutId", label: "Layout", control: LayoutSelect },
|
{ key: "layoutId", label: "Layout", control: LayoutSelect },
|
||||||
]
|
]
|
||||||
</script>
|
</script>
|
||||||
|
@ -62,9 +115,11 @@
|
||||||
control={def.control}
|
control={def.control}
|
||||||
label={def.label}
|
label={def.label}
|
||||||
key={def.key}
|
key={def.key}
|
||||||
|
error="asdasds"
|
||||||
value={deepGet($currentAsset, def.key)}
|
value={deepGet($currentAsset, def.key)}
|
||||||
onChange={val => setAssetProps(def.key, val, def.parser)}
|
onChange={val => setAssetProps(def.key, val, def.parser, def.validate)}
|
||||||
{bindings}
|
{bindings}
|
||||||
|
props={{ error: errors[def.key] }}
|
||||||
/>
|
/>
|
||||||
{/each}
|
{/each}
|
||||||
</DetailSummary>
|
</DetailSummary>
|
||||||
|
|
|
@ -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()
|
||||||
|
@ -65,17 +107,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
|
||||||
|
@ -84,6 +120,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}
|
||||||
|
@ -98,20 +143,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>
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,151 @@
|
||||||
|
<script>
|
||||||
|
import { goto } from "@roxi/routify"
|
||||||
|
import {
|
||||||
|
Layout,
|
||||||
|
Page,
|
||||||
|
notifications,
|
||||||
|
Button,
|
||||||
|
Heading,
|
||||||
|
Body,
|
||||||
|
Modal,
|
||||||
|
Divider,
|
||||||
|
} from "@budibase/bbui"
|
||||||
|
import CreateAppModal from "components/start/CreateAppModal.svelte"
|
||||||
|
import TemplateDisplay from "components/common/TemplateDisplay.svelte"
|
||||||
|
import { onMount } from "svelte"
|
||||||
|
import { templates } from "stores/portal"
|
||||||
|
|
||||||
|
let loaded = $templates?.length
|
||||||
|
let template
|
||||||
|
let creationModal = false
|
||||||
|
let creatingApp = false
|
||||||
|
|
||||||
|
const welcomeBody =
|
||||||
|
"Start from scratch or get a head start with one of our templates"
|
||||||
|
const createAppTitle = "Create new app"
|
||||||
|
const createAppButtonText = "Start from scratch"
|
||||||
|
|
||||||
|
onMount(async () => {
|
||||||
|
try {
|
||||||
|
await templates.load()
|
||||||
|
if ($templates?.length === 0) {
|
||||||
|
notifications.error(
|
||||||
|
"There was a problem loading quick start templates."
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
notifications.error("Error loading apps and templates")
|
||||||
|
}
|
||||||
|
loaded = true
|
||||||
|
})
|
||||||
|
|
||||||
|
const initiateAppCreation = () => {
|
||||||
|
template = null
|
||||||
|
creationModal.show()
|
||||||
|
creatingApp = true
|
||||||
|
}
|
||||||
|
|
||||||
|
const stopAppCreation = () => {
|
||||||
|
template = null
|
||||||
|
creatingApp = false
|
||||||
|
}
|
||||||
|
|
||||||
|
const initiateAppImport = () => {
|
||||||
|
template = { fromFile: true }
|
||||||
|
creationModal.show()
|
||||||
|
creatingApp = true
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Page wide>
|
||||||
|
<Layout noPadding gap="XL">
|
||||||
|
<span>
|
||||||
|
<Button
|
||||||
|
quiet
|
||||||
|
secondary
|
||||||
|
icon={"ChevronLeft"}
|
||||||
|
on:click={() => {
|
||||||
|
$goto("../")
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Back
|
||||||
|
</Button>
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<div class="title">
|
||||||
|
<div class="welcome">
|
||||||
|
<Layout noPadding gap="XS">
|
||||||
|
<Heading size="M">{createAppTitle}</Heading>
|
||||||
|
<Body size="S">
|
||||||
|
{welcomeBody}
|
||||||
|
</Body>
|
||||||
|
</Layout>
|
||||||
|
|
||||||
|
<div class="buttons">
|
||||||
|
<Button
|
||||||
|
dataCy="create-app-btn"
|
||||||
|
size="L"
|
||||||
|
icon="Add"
|
||||||
|
cta
|
||||||
|
on:click={initiateAppCreation}
|
||||||
|
>
|
||||||
|
{createAppButtonText}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
dataCy="import-app-btn"
|
||||||
|
icon="Import"
|
||||||
|
size="L"
|
||||||
|
quiet
|
||||||
|
secondary
|
||||||
|
on:click={initiateAppImport}
|
||||||
|
>
|
||||||
|
Import app
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Divider size="S" />
|
||||||
|
|
||||||
|
{#if loaded && $templates?.length}
|
||||||
|
<TemplateDisplay templates={$templates} />
|
||||||
|
{/if}
|
||||||
|
</Layout>
|
||||||
|
</Page>
|
||||||
|
|
||||||
|
<Modal
|
||||||
|
bind:this={creationModal}
|
||||||
|
padding={false}
|
||||||
|
width="600px"
|
||||||
|
on:hide={stopAppCreation}
|
||||||
|
>
|
||||||
|
<CreateAppModal {template} />
|
||||||
|
</Modal>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.title .welcome > .buttons {
|
||||||
|
padding-top: 30px;
|
||||||
|
}
|
||||||
|
.title {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--spacing-xl);
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
.buttons {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: flex-start;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--spacing-xl);
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
@media (max-width: 640px) {
|
||||||
|
.buttons {
|
||||||
|
flex-direction: row-reverse;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -11,8 +11,9 @@
|
||||||
notifications,
|
notifications,
|
||||||
Body,
|
Body,
|
||||||
Search,
|
Search,
|
||||||
Icon,
|
Divider,
|
||||||
} from "@budibase/bbui"
|
} from "@budibase/bbui"
|
||||||
|
import TemplateDisplay from "components/common/TemplateDisplay.svelte"
|
||||||
import Spinner from "components/common/Spinner.svelte"
|
import Spinner from "components/common/Spinner.svelte"
|
||||||
import CreateAppModal from "components/start/CreateAppModal.svelte"
|
import CreateAppModal from "components/start/CreateAppModal.svelte"
|
||||||
import UpdateAppModal from "components/start/UpdateAppModal.svelte"
|
import UpdateAppModal from "components/start/UpdateAppModal.svelte"
|
||||||
|
@ -39,12 +40,27 @@
|
||||||
let unpublishModal
|
let unpublishModal
|
||||||
let iconModal
|
let iconModal
|
||||||
let creatingApp = false
|
let creatingApp = false
|
||||||
let loaded = false
|
let loaded = $apps?.length || $templates?.length
|
||||||
let searchTerm = ""
|
let searchTerm = ""
|
||||||
let cloud = $admin.cloud
|
let cloud = $admin.cloud
|
||||||
let appName = ""
|
let appName = ""
|
||||||
let creatingFromTemplate = false
|
let creatingFromTemplate = false
|
||||||
|
|
||||||
|
const resolveWelcomeMessage = (auth, apps) => {
|
||||||
|
const userWelcome = auth?.user?.firstName
|
||||||
|
? `Welcome ${auth?.user?.firstName}!`
|
||||||
|
: "Welcome back!"
|
||||||
|
return apps?.length ? userWelcome : "Let's create your first app!"
|
||||||
|
}
|
||||||
|
$: welcomeHeader = resolveWelcomeMessage($auth, $apps)
|
||||||
|
$: welcomeBody = $apps?.length
|
||||||
|
? "Manage your apps and get a head start with templates"
|
||||||
|
: "Start from scratch or get a head start with one of our templates"
|
||||||
|
|
||||||
|
$: createAppButtonText = $apps?.length
|
||||||
|
? "Create new app"
|
||||||
|
: "Start from scratch"
|
||||||
|
|
||||||
$: enrichedApps = enrichApps($apps, $auth.user, sortBy)
|
$: enrichedApps = enrichApps($apps, $auth.user, sortBy)
|
||||||
$: filteredApps = enrichedApps.filter(app =>
|
$: filteredApps = enrichedApps.filter(app =>
|
||||||
app?.name?.toLowerCase().includes(searchTerm.toLowerCase())
|
app?.name?.toLowerCase().includes(searchTerm.toLowerCase())
|
||||||
|
@ -79,10 +95,14 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
const initiateAppCreation = () => {
|
const initiateAppCreation = () => {
|
||||||
|
if ($apps?.length) {
|
||||||
|
$goto("/builder/portal/apps/create")
|
||||||
|
} else {
|
||||||
template = null
|
template = null
|
||||||
creationModal.show()
|
creationModal.show()
|
||||||
creatingApp = true
|
creatingApp = true
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const initiateAppsExport = () => {
|
const initiateAppsExport = () => {
|
||||||
try {
|
try {
|
||||||
|
@ -269,27 +289,40 @@
|
||||||
|
|
||||||
<Page wide>
|
<Page wide>
|
||||||
<Layout noPadding gap="XL">
|
<Layout noPadding gap="XL">
|
||||||
|
{#if loaded}
|
||||||
<div class="title">
|
<div class="title">
|
||||||
|
<div class="welcome">
|
||||||
<Layout noPadding gap="XS">
|
<Layout noPadding gap="XS">
|
||||||
<Heading size="M">Welcome to Budibase</Heading>
|
<Heading size="L">{welcomeHeader}</Heading>
|
||||||
<Body size="S">
|
<Body size="M">
|
||||||
Manage your apps and get a head start with templates
|
{welcomeBody}
|
||||||
</Body>
|
</Body>
|
||||||
</Layout>
|
</Layout>
|
||||||
|
|
||||||
<div class="buttons">
|
<div class="buttons">
|
||||||
{#if cloud}
|
|
||||||
<Button
|
<Button
|
||||||
size="L"
|
dataCy="create-app-btn"
|
||||||
icon="Export"
|
size="M"
|
||||||
|
icon="Add"
|
||||||
|
cta
|
||||||
|
on:click={initiateAppCreation}
|
||||||
|
>
|
||||||
|
{createAppButtonText}
|
||||||
|
</Button>
|
||||||
|
{#if $apps?.length > 0}
|
||||||
|
<Button
|
||||||
|
icon="Experience"
|
||||||
|
size="M"
|
||||||
quiet
|
quiet
|
||||||
secondary
|
secondary
|
||||||
on:click={initiateAppsExport}
|
on:click={$goto("/builder/portal/apps/templates")}
|
||||||
>
|
>
|
||||||
Export apps
|
Templates
|
||||||
</Button>
|
</Button>
|
||||||
{/if}
|
{/if}
|
||||||
|
{#if !$apps?.length}
|
||||||
<Button
|
<Button
|
||||||
|
dataCy="import-app-btn"
|
||||||
icon="Import"
|
icon="Import"
|
||||||
size="L"
|
size="L"
|
||||||
quiet
|
quiet
|
||||||
|
@ -298,60 +331,38 @@
|
||||||
>
|
>
|
||||||
Import app
|
Import app
|
||||||
</Button>
|
</Button>
|
||||||
<Button size="L" icon="Add" cta on:click={initiateAppCreation}>
|
{/if}
|
||||||
Create app
|
|
||||||
</Button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div>
|
||||||
<Layout noPadding gap="S">
|
<Layout gap="S" justifyItems="center">
|
||||||
<Detail size="L">Quick start templates</Detail>
|
<img class="img-logo img-size" alt="logo" src={Logo} />
|
||||||
<div class="grid">
|
|
||||||
{#each $templates as item}
|
|
||||||
<div
|
|
||||||
on:click={() => {
|
|
||||||
template = item
|
|
||||||
creationModal.show()
|
|
||||||
creatingApp = true
|
|
||||||
}}
|
|
||||||
class="template-card"
|
|
||||||
>
|
|
||||||
<a
|
|
||||||
href={item.url}
|
|
||||||
target="_blank"
|
|
||||||
class="external-link"
|
|
||||||
on:click|stopPropagation
|
|
||||||
>
|
|
||||||
<Icon name="LinkOut" size="S" />
|
|
||||||
</a>
|
|
||||||
<div class="card-body">
|
|
||||||
<div style="color: {item.background}" class="iconAlign">
|
|
||||||
<svg
|
|
||||||
width="26px"
|
|
||||||
height="26px"
|
|
||||||
class="spectrum-Icon"
|
|
||||||
style="color:{item.background};"
|
|
||||||
focusable="false"
|
|
||||||
>
|
|
||||||
<use xlink:href="#spectrum-icon-18-{item.icon}" />
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
<div class="iconAlign">
|
|
||||||
<Body weight="900" size="S">{item.name}</Body>
|
|
||||||
<div style="font-size: 10px;">
|
|
||||||
<Body size="S">{item.category.toUpperCase()}</Body>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{/each}
|
|
||||||
</div>
|
|
||||||
</Layout>
|
</Layout>
|
||||||
|
</div>
|
||||||
|
<Divider size="S" />
|
||||||
|
</div>
|
||||||
|
|
||||||
{#if loaded && enrichedApps.length}
|
{#if !$apps?.length && $templates?.length}
|
||||||
|
<TemplateDisplay templates={$templates} />
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
{#if enrichedApps.length}
|
||||||
<Layout noPadding gap="S">
|
<Layout noPadding gap="S">
|
||||||
<div class="title">
|
<div class="title">
|
||||||
<Detail size="L">My apps</Detail>
|
<Detail size="L">Apps</Detail>
|
||||||
|
{#if enrichedApps.length > 1}
|
||||||
|
<div class="app-actions">
|
||||||
|
{#if cloud}
|
||||||
|
<Button
|
||||||
|
size="M"
|
||||||
|
icon="Export"
|
||||||
|
quiet
|
||||||
|
secondary
|
||||||
|
on:click={initiateAppsExport}
|
||||||
|
>
|
||||||
|
Export apps
|
||||||
|
</Button>
|
||||||
|
{/if}
|
||||||
<div class="filter">
|
<div class="filter">
|
||||||
<Select
|
<Select
|
||||||
quiet
|
quiet
|
||||||
|
@ -367,6 +378,8 @@
|
||||||
<Search placeholder="Search" bind:value={searchTerm} />
|
<Search placeholder="Search" bind:value={searchTerm} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="appTable">
|
<div class="appTable">
|
||||||
{#each filteredApps as app (app.appId)}
|
{#each filteredApps as app (app.appId)}
|
||||||
|
@ -385,32 +398,11 @@
|
||||||
</div>
|
</div>
|
||||||
</Layout>
|
</Layout>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if !enrichedApps.length && !creatingApp && loaded}
|
|
||||||
<div class="empty-wrapper">
|
|
||||||
<div class="centered">
|
|
||||||
<div class="main">
|
|
||||||
<Layout gap="S" justifyItems="center">
|
|
||||||
<img class="img-size" alt="logo" src={Logo} />
|
|
||||||
<div class="new-screen-text">
|
|
||||||
<Detail size="M">Create a business app in minutes!</Detail>
|
|
||||||
</div>
|
|
||||||
<Button on:click={() => initiateAppCreation()} size="M" cta>
|
|
||||||
<div class="new-screen-button">
|
|
||||||
<div class="background-icon" style="color: white;">
|
|
||||||
<Icon name="Add" />
|
|
||||||
</div>
|
|
||||||
Create App
|
|
||||||
</div></Button
|
|
||||||
>
|
|
||||||
</Layout>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if creatingFromTemplate}
|
{#if creatingFromTemplate}
|
||||||
<div class="empty-wrapper">
|
<div class="empty-wrapper">
|
||||||
|
<img class="img-logo img-size" alt="logo" src={Logo} />
|
||||||
<p>Creating your Budibase app from your selected template...</p>
|
<p>Creating your Budibase app from your selected template...</p>
|
||||||
<Spinner size="10" />
|
<Spinner size="10" />
|
||||||
</div>
|
</div>
|
||||||
|
@ -460,6 +452,15 @@
|
||||||
<ChooseIconModal app={selectedApp} bind:this={iconModal} />
|
<ChooseIconModal app={selectedApp} bind:this={iconModal} />
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
.app-actions {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
.app-actions :global(> button) {
|
||||||
|
margin-right: 10px;
|
||||||
|
}
|
||||||
|
.title .welcome > .buttons {
|
||||||
|
padding-top: 30px;
|
||||||
|
}
|
||||||
.title {
|
.title {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
|
@ -476,13 +477,11 @@
|
||||||
gap: var(--spacing-xl);
|
gap: var(--spacing-xl);
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
}
|
}
|
||||||
@media (max-width: 640px) {
|
@media (max-width: 1000px) {
|
||||||
.buttons {
|
.img-logo {
|
||||||
flex-direction: row-reverse;
|
display: none;
|
||||||
justify-content: flex-end;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.filter {
|
.filter {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
|
@ -490,49 +489,6 @@
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: var(--spacing-xl);
|
gap: var(--spacing-xl);
|
||||||
}
|
}
|
||||||
|
|
||||||
.grid {
|
|
||||||
height: 200px;
|
|
||||||
display: grid;
|
|
||||||
overflow: hidden;
|
|
||||||
grid-gap: var(--spacing-xl);
|
|
||||||
grid-template-columns: repeat(auto-fill, minmax(270px, 1fr));
|
|
||||||
grid-template-rows: minmax(70px, 1fr) minmax(100px, 1fr) minmax(0px, 0);
|
|
||||||
}
|
|
||||||
.template-card {
|
|
||||||
height: 70px;
|
|
||||||
border-radius: var(--border-radius-s);
|
|
||||||
border: 1px solid var(--spectrum-global-color-gray-300);
|
|
||||||
cursor: pointer;
|
|
||||||
display: flex;
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
.template-card:hover {
|
|
||||||
background: var(--spectrum-alias-background-color-tertiary);
|
|
||||||
}
|
|
||||||
.card-body {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
padding: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.external-link {
|
|
||||||
position: absolute;
|
|
||||||
top: 5px;
|
|
||||||
right: 5px;
|
|
||||||
color: var(--spectrum-global-color-gray-300);
|
|
||||||
z-index: 99;
|
|
||||||
}
|
|
||||||
.external-link:hover {
|
|
||||||
color: var(--spectrum-global-color-gray-500);
|
|
||||||
}
|
|
||||||
|
|
||||||
.iconAlign {
|
|
||||||
padding: 0 0 0 var(--spacing-m);
|
|
||||||
display: inline-block;
|
|
||||||
}
|
|
||||||
|
|
||||||
.appTable {
|
.appTable {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-rows: auto;
|
grid-template-rows: auto;
|
||||||
|
@ -558,7 +514,6 @@
|
||||||
grid-template-columns: 1fr auto;
|
grid-template-columns: 1fr auto;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.empty-wrapper {
|
.empty-wrapper {
|
||||||
flex: 1 1 auto;
|
flex: 1 1 auto;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
@ -567,42 +522,8 @@
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.centered {
|
|
||||||
width: calc(100% - 350px);
|
|
||||||
height: calc(100% - 100px);
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.main {
|
|
||||||
width: 300px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.new-screen-text {
|
|
||||||
width: 160px;
|
|
||||||
text-align: center;
|
|
||||||
color: #2c2c2c;
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
|
|
||||||
.new-screen-button {
|
|
||||||
margin-left: 5px;
|
|
||||||
height: 20px;
|
|
||||||
width: 100px;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.img-size {
|
.img-size {
|
||||||
width: 160px;
|
width: 160px;
|
||||||
height: 160px;
|
height: 160px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.background-icon {
|
|
||||||
margin-top: 4px;
|
|
||||||
margin-right: 4px;
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -0,0 +1,43 @@
|
||||||
|
<script>
|
||||||
|
import { goto } from "@roxi/routify"
|
||||||
|
import { Layout, Page, notifications, Button } from "@budibase/bbui"
|
||||||
|
import TemplateDisplay from "components/common/TemplateDisplay.svelte"
|
||||||
|
import { onMount } from "svelte"
|
||||||
|
import { templates } from "stores/portal"
|
||||||
|
|
||||||
|
let loaded = $templates?.length
|
||||||
|
|
||||||
|
onMount(async () => {
|
||||||
|
try {
|
||||||
|
await templates.load()
|
||||||
|
if ($templates?.length === 0) {
|
||||||
|
notifications.error(
|
||||||
|
"There was a problem loading quick start templates."
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
notifications.error("Error loading apps and templates")
|
||||||
|
}
|
||||||
|
loaded = true
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Page wide>
|
||||||
|
<Layout noPadding gap="XL">
|
||||||
|
<span>
|
||||||
|
<Button
|
||||||
|
quiet
|
||||||
|
secondary
|
||||||
|
icon={"ChevronLeft"}
|
||||||
|
on:click={() => {
|
||||||
|
$goto("../")
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Back
|
||||||
|
</Button>
|
||||||
|
</span>
|
||||||
|
{#if loaded && $templates?.length}
|
||||||
|
<TemplateDisplay templates={$templates} />
|
||||||
|
{/if}
|
||||||
|
</Layout>
|
||||||
|
</Page>
|
|
@ -71,7 +71,9 @@
|
||||||
<Heading size="S">Users</Heading>
|
<Heading size="S">Users</Heading>
|
||||||
<ButtonGroup>
|
<ButtonGroup>
|
||||||
<Button disabled secondary>Import users</Button>
|
<Button disabled secondary>Import users</Button>
|
||||||
<Button primary on:click={createUserModal.show}>Add user</Button>
|
<Button primary dataCy="add-user" on:click={createUserModal.show}
|
||||||
|
>Add user</Button
|
||||||
|
>
|
||||||
</ButtonGroup>
|
</ButtonGroup>
|
||||||
</div>
|
</div>
|
||||||
<div class="field">
|
<div class="field">
|
||||||
|
|
|
@ -4424,9 +4424,9 @@ minimatch@^3.0.4:
|
||||||
brace-expansion "^1.1.7"
|
brace-expansion "^1.1.7"
|
||||||
|
|
||||||
minimist@^1.1.1, minimist@^1.2.0, minimist@^1.2.5:
|
minimist@^1.1.1, minimist@^1.2.0, minimist@^1.2.5:
|
||||||
version "1.2.5"
|
version "1.2.6"
|
||||||
resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602"
|
resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.6.tgz#8637a5b759ea0d6e98702cfb3a9283323c93af44"
|
||||||
integrity sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==
|
integrity sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==
|
||||||
|
|
||||||
mixin-deep@^1.2.0:
|
mixin-deep@^1.2.0:
|
||||||
version "1.3.2"
|
version "1.3.2"
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@budibase/cli",
|
"name": "@budibase/cli",
|
||||||
"version": "1.0.91-alpha.17",
|
"version": "1.0.105-alpha.10",
|
||||||
"description": "Budibase CLI, for developers, self hosting and migrations.",
|
"description": "Budibase CLI, for developers, self hosting and migrations.",
|
||||||
"main": "src/index.js",
|
"main": "src/index.js",
|
||||||
"bin": {
|
"bin": {
|
||||||
|
|
|
@ -1123,9 +1123,9 @@ minimatch@^3.0.4:
|
||||||
brace-expansion "^1.1.7"
|
brace-expansion "^1.1.7"
|
||||||
|
|
||||||
minimist@^1.2.0, minimist@^1.2.3, minimist@^1.2.5:
|
minimist@^1.2.0, minimist@^1.2.3, minimist@^1.2.5:
|
||||||
version "1.2.5"
|
version "1.2.6"
|
||||||
resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602"
|
resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.6.tgz#8637a5b759ea0d6e98702cfb3a9283323c93af44"
|
||||||
integrity sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==
|
integrity sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==
|
||||||
|
|
||||||
mkdirp-classic@^0.5.2, mkdirp-classic@^0.5.3:
|
mkdirp-classic@^0.5.2, mkdirp-classic@^0.5.3:
|
||||||
version "0.5.3"
|
version "0.5.3"
|
||||||
|
|
|
@ -7,7 +7,8 @@
|
||||||
"customThemes": true,
|
"customThemes": true,
|
||||||
"devicePreview": true,
|
"devicePreview": true,
|
||||||
"messagePassing": true,
|
"messagePassing": true,
|
||||||
"rowSelection": true
|
"rowSelection": true,
|
||||||
|
"continueIfAction": true
|
||||||
},
|
},
|
||||||
"layout": {
|
"layout": {
|
||||||
"name": "Layout",
|
"name": "Layout",
|
||||||
|
@ -2528,69 +2529,103 @@
|
||||||
"name": "Embedded Map",
|
"name": "Embedded Map",
|
||||||
"icon": "Location",
|
"icon": "Location",
|
||||||
"styles": ["size"],
|
"styles": ["size"],
|
||||||
"editable": true,
|
|
||||||
"draggable": false,
|
"draggable": false,
|
||||||
"illegalChildren": [
|
"illegalChildren": ["section"],
|
||||||
"section"
|
|
||||||
],
|
|
||||||
"settings": [
|
"settings": [
|
||||||
{
|
{
|
||||||
"type": "dataProvider",
|
"type": "dataProvider",
|
||||||
"label": "Provider",
|
"label": "Provider",
|
||||||
"key": "dataProvider"
|
"key": "dataProvider"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"type": "field",
|
||||||
|
"label": "Latitude Key",
|
||||||
|
"key": "latitudeKey",
|
||||||
|
"dependsOn": "dataProvider"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "field",
|
||||||
|
"label": "Longitude Key",
|
||||||
|
"key": "longitudeKey",
|
||||||
|
"dependsOn": "dataProvider"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "field",
|
||||||
|
"label": "Title Key",
|
||||||
|
"key": "titleKey",
|
||||||
|
"dependsOn": "dataProvider"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "event",
|
||||||
|
"label": "On Click Marker",
|
||||||
|
"key": "onClickMarker",
|
||||||
|
"context": [
|
||||||
|
{
|
||||||
|
"label": "Clicked marker",
|
||||||
|
"key": "marker"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"type": "boolean",
|
"type": "boolean",
|
||||||
"label": "Enable Fullscreen",
|
"label": "Enable creating markers",
|
||||||
|
"key": "creationEnabled",
|
||||||
|
"defaultValue": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "event",
|
||||||
|
"label": "On Create Marker",
|
||||||
|
"key": "onCreateMarker",
|
||||||
|
"dependsOn": "creationEnabled",
|
||||||
|
"context": [
|
||||||
|
{
|
||||||
|
"label": "New marker latitude",
|
||||||
|
"key": "lat"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "New marker longitude",
|
||||||
|
"key": "lng"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "boolean",
|
||||||
|
"label": "Enable fullscreen",
|
||||||
"key": "fullScreenEnabled",
|
"key": "fullScreenEnabled",
|
||||||
"defaultValue": true
|
"defaultValue": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "boolean",
|
"type": "boolean",
|
||||||
"label": "Enable Location",
|
"label": "Enable location",
|
||||||
"key": "locationEnabled",
|
"key": "locationEnabled",
|
||||||
"defaultValue": true
|
"defaultValue": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "boolean",
|
"type": "boolean",
|
||||||
"label": "Enable Zoom",
|
"label": "Enable zoom",
|
||||||
"key": "zoomEnabled",
|
"key": "zoomEnabled",
|
||||||
"defaultValue": true
|
"defaultValue": true
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"type": "number",
|
|
||||||
"label": "Zoom Level (0-100)",
|
|
||||||
"key": "zoomLevel",
|
|
||||||
"defaultValue": 72,
|
|
||||||
"max" : 100,
|
|
||||||
"min" : 0
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "field",
|
|
||||||
"label": "Latitude Key",
|
|
||||||
"key": "latitudeKey"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "field",
|
|
||||||
"label": "Longitude Key",
|
|
||||||
"key": "longitudeKey"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "field",
|
|
||||||
"label": "Title Key",
|
|
||||||
"key": "titleKey"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"type": "text",
|
"type": "text",
|
||||||
"label": "Tile URL",
|
"label": "Tile URL",
|
||||||
"key": "tileURL",
|
"key": "tileURL",
|
||||||
"defaultValue": "https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
|
"defaultValue": "https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
"type": "text",
|
"type": "text",
|
||||||
"label": "Default Location (lat,lng)",
|
"label": "Default Location (when empty)",
|
||||||
"key": "defaultLocation",
|
"key": "defaultLocation",
|
||||||
"defaultValue": "51.5072,-0.1276"
|
"placeholder": "51.5072,-0.1276"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "number",
|
||||||
|
"label": "Default Location Zoom Level (0-100)",
|
||||||
|
"key": "zoomLevel",
|
||||||
|
"placeholder": 50,
|
||||||
|
"max": 100,
|
||||||
|
"min": 0
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "text",
|
"type": "text",
|
||||||
|
@ -3595,7 +3630,6 @@
|
||||||
"name": "Markdown Viewer",
|
"name": "Markdown Viewer",
|
||||||
"icon": "TaskList",
|
"icon": "TaskList",
|
||||||
"styles": ["size"],
|
"styles": ["size"],
|
||||||
"editable": true,
|
|
||||||
"settings": [
|
"settings": [
|
||||||
{
|
{
|
||||||
"type": "text",
|
"type": "text",
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@budibase/client",
|
"name": "@budibase/client",
|
||||||
"version": "1.0.91-alpha.17",
|
"version": "1.0.105-alpha.10",
|
||||||
"license": "MPL-2.0",
|
"license": "MPL-2.0",
|
||||||
"module": "dist/budibase-client.js",
|
"module": "dist/budibase-client.js",
|
||||||
"main": "dist/budibase-client.js",
|
"main": "dist/budibase-client.js",
|
||||||
|
@ -19,9 +19,9 @@
|
||||||
"dev:builder": "rollup -cw"
|
"dev:builder": "rollup -cw"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@budibase/bbui": "^1.0.91-alpha.17",
|
"@budibase/bbui": "^1.0.105-alpha.10",
|
||||||
"@budibase/frontend-core": "^1.0.91-alpha.17",
|
"@budibase/frontend-core": "^1.0.105-alpha.10",
|
||||||
"@budibase/string-templates": "^1.0.91-alpha.17",
|
"@budibase/string-templates": "^1.0.105-alpha.10",
|
||||||
"@spectrum-css/button": "^3.0.3",
|
"@spectrum-css/button": "^3.0.3",
|
||||||
"@spectrum-css/card": "^3.0.3",
|
"@spectrum-css/card": "^3.0.3",
|
||||||
"@spectrum-css/divider": "^1.0.3",
|
"@spectrum-css/divider": "^1.0.3",
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
<script context="module">
|
<script context="module">
|
||||||
// Cache the definition of settings for each component type
|
// Cache the definition of settings for each component type
|
||||||
let SettingsDefinitionCache = {}
|
let SettingsDefinitionCache = {}
|
||||||
|
let SettingsDefinitionMapCache = {}
|
||||||
|
|
||||||
// Cache the settings of each component ID.
|
// Cache the settings of each component ID.
|
||||||
// This speeds up remounting as well as repeaters.
|
// This speeds up remounting as well as repeaters.
|
||||||
|
@ -74,6 +75,8 @@
|
||||||
// Component information derived during initialisation
|
// Component information derived during initialisation
|
||||||
let constructor
|
let constructor
|
||||||
let definition
|
let definition
|
||||||
|
let settingsDefinition
|
||||||
|
let settingsDefinitionMap
|
||||||
|
|
||||||
// Set up initial state for each new component instance
|
// Set up initial state for each new component instance
|
||||||
$: initialise(instance)
|
$: initialise(instance)
|
||||||
|
@ -118,7 +121,7 @@
|
||||||
$: emptyState = empty && showEmptyState
|
$: emptyState = empty && showEmptyState
|
||||||
|
|
||||||
// Enrich component settings
|
// Enrich component settings
|
||||||
$: enrichComponentSettings($context)
|
$: enrichComponentSettings($context, settingsDefinitionMap)
|
||||||
|
|
||||||
// Evaluate conditional UI settings and store any component setting changes
|
// Evaluate conditional UI settings and store any component setting changes
|
||||||
// which need to be made. This is broken into 2 lines to avoid svelte
|
// which need to be made. This is broken into 2 lines to avoid svelte
|
||||||
|
@ -168,12 +171,14 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get the settings definition for this component, and cache it
|
// Get the settings definition for this component, and cache it
|
||||||
let settingsDefinition
|
|
||||||
if (SettingsDefinitionCache[definition.name]) {
|
if (SettingsDefinitionCache[definition.name]) {
|
||||||
settingsDefinition = SettingsDefinitionCache[definition.name]
|
settingsDefinition = SettingsDefinitionCache[definition.name]
|
||||||
|
settingsDefinitionMap = SettingsDefinitionMapCache[definition.name]
|
||||||
} else {
|
} else {
|
||||||
settingsDefinition = getSettingsDefinition(definition)
|
settingsDefinition = getSettingsDefinition(definition)
|
||||||
|
settingsDefinitionMap = getSettingsDefinitionMap(settingsDefinition)
|
||||||
SettingsDefinitionCache[definition.name] = settingsDefinition
|
SettingsDefinitionCache[definition.name] = settingsDefinition
|
||||||
|
SettingsDefinitionMapCache[definition.name] = settingsDefinitionMap
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parse the instance settings, and cache them
|
// Parse the instance settings, and cache them
|
||||||
|
@ -190,7 +195,9 @@
|
||||||
dynamicSettings = instanceSettings.dynamicSettings
|
dynamicSettings = instanceSettings.dynamicSettings
|
||||||
|
|
||||||
// Force an initial enrichment of the new settings
|
// Force an initial enrichment of the new settings
|
||||||
enrichComponentSettings(get(context), { force: true })
|
enrichComponentSettings(get(context), settingsDefinitionMap, {
|
||||||
|
force: true,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// Gets the component constructor for the specified component
|
// Gets the component constructor for the specified component
|
||||||
|
@ -226,6 +233,14 @@
|
||||||
return settings
|
return settings
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const getSettingsDefinitionMap = settingsDefinition => {
|
||||||
|
let map = {}
|
||||||
|
settingsDefinition?.forEach(setting => {
|
||||||
|
map[setting.key] = setting
|
||||||
|
})
|
||||||
|
return map
|
||||||
|
}
|
||||||
|
|
||||||
const getInstanceSettings = (instance, settingsDefinition) => {
|
const getInstanceSettings = (instance, settingsDefinition) => {
|
||||||
// Get raw settings
|
// Get raw settings
|
||||||
let settings = {}
|
let settings = {}
|
||||||
|
@ -248,7 +263,7 @@
|
||||||
} else if (typeof value === "string" && value.includes("{{")) {
|
} else if (typeof value === "string" && value.includes("{{")) {
|
||||||
// Strings can be trivially checked
|
// Strings can be trivially checked
|
||||||
delete newStaticSettings[setting.key]
|
delete newStaticSettings[setting.key]
|
||||||
} else if (value[0]?.["##eventHandlerType"] != null) {
|
} else if (setting.type === "event") {
|
||||||
// Always treat button actions as dynamic
|
// Always treat button actions as dynamic
|
||||||
delete newStaticSettings[setting.key]
|
delete newStaticSettings[setting.key]
|
||||||
} else if (typeof value === "object") {
|
} else if (typeof value === "object") {
|
||||||
|
@ -273,7 +288,11 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
// Enriches any string component props using handlebars
|
// Enriches any string component props using handlebars
|
||||||
const enrichComponentSettings = (context, options = { force: false }) => {
|
const enrichComponentSettings = (
|
||||||
|
context,
|
||||||
|
settingsDefinitionMap,
|
||||||
|
options = { force: false }
|
||||||
|
) => {
|
||||||
const contextChanged = context.key !== lastContextKey
|
const contextChanged = context.key !== lastContextKey
|
||||||
if (!contextChanged && !options?.force) {
|
if (!contextChanged && !options?.force) {
|
||||||
return
|
return
|
||||||
|
@ -285,7 +304,11 @@
|
||||||
const enrichmentTime = latestUpdateTime
|
const enrichmentTime = latestUpdateTime
|
||||||
|
|
||||||
// Enrich settings with context
|
// Enrich settings with context
|
||||||
const newEnrichedSettings = enrichProps(dynamicSettings, context)
|
const newEnrichedSettings = enrichProps(
|
||||||
|
dynamicSettings,
|
||||||
|
context,
|
||||||
|
settingsDefinitionMap
|
||||||
|
)
|
||||||
|
|
||||||
// Abandon this update if a newer update has started
|
// Abandon this update if a newer update has started
|
||||||
if (enrichmentTime !== latestUpdateTime) {
|
if (enrichmentTime !== latestUpdateTime) {
|
||||||
|
|
|
@ -22,18 +22,19 @@
|
||||||
$: componentText = getComponentText(text, $builderStore, $component)
|
$: componentText = getComponentText(text, $builderStore, $component)
|
||||||
|
|
||||||
const getComponentText = (text, builderState, componentState) => {
|
const getComponentText = (text, builderState, componentState) => {
|
||||||
if (!builderState.inBuilder || componentState.editing) {
|
if (componentState.editing) {
|
||||||
return text || " "
|
return text || " "
|
||||||
}
|
}
|
||||||
return text || componentState.name || "Placeholder text"
|
return text || componentState.name || "Placeholder text"
|
||||||
}
|
}
|
||||||
|
|
||||||
const updateText = e => {
|
const updateText = e => {
|
||||||
builderStore.actions.updateProp("text", e.target.textContent.trim())
|
builderStore.actions.updateProp("text", e.target.textContent)
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<button
|
{#key $component.editing}
|
||||||
|
<button
|
||||||
class={`spectrum-Button spectrum-Button--size${size} spectrum-Button--${type}`}
|
class={`spectrum-Button spectrum-Button--size${size} spectrum-Button--${type}`}
|
||||||
class:spectrum-Button--quiet={quiet}
|
class:spectrum-Button--quiet={quiet}
|
||||||
{disabled}
|
{disabled}
|
||||||
|
@ -43,7 +44,7 @@
|
||||||
on:blur={$component.editing ? updateText : null}
|
on:blur={$component.editing ? updateText : null}
|
||||||
bind:this={node}
|
bind:this={node}
|
||||||
class:active
|
class:active
|
||||||
>
|
>
|
||||||
{#if icon}
|
{#if icon}
|
||||||
<svg
|
<svg
|
||||||
class:hasText={componentText?.length > 0}
|
class:hasText={componentText?.length > 0}
|
||||||
|
@ -56,7 +57,8 @@
|
||||||
</svg>
|
</svg>
|
||||||
{/if}
|
{/if}
|
||||||
{componentText}
|
{componentText}
|
||||||
</button>
|
</button>
|
||||||
|
{/key}
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
button {
|
button {
|
||||||
|
|
|
@ -47,12 +47,12 @@
|
||||||
|
|
||||||
// Convert contenteditable HTML to text and save
|
// Convert contenteditable HTML to text and save
|
||||||
const updateText = e => {
|
const updateText = e => {
|
||||||
const sanitized = e.target.innerHTML.replace(/<br>/gi, "\n").trim()
|
builderStore.actions.updateProp("text", e.target.textContent)
|
||||||
builderStore.actions.updateProp("text", sanitized)
|
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<h1
|
{#key $component.editing}
|
||||||
|
<h1
|
||||||
bind:this={node}
|
bind:this={node}
|
||||||
contenteditable={$component.editing}
|
contenteditable={$component.editing}
|
||||||
use:styleable={styles}
|
use:styleable={styles}
|
||||||
|
@ -62,9 +62,10 @@
|
||||||
class:underline
|
class:underline
|
||||||
class="spectrum-Heading {sizeClass} {alignClass}"
|
class="spectrum-Heading {sizeClass} {alignClass}"
|
||||||
on:blur={$component.editing ? updateText : null}
|
on:blur={$component.editing ? updateText : null}
|
||||||
>
|
>
|
||||||
{componentText}
|
{componentText}
|
||||||
</h1>
|
</h1>
|
||||||
|
{/key}
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
h1 {
|
h1 {
|
||||||
|
|
|
@ -61,7 +61,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
const updateText = e => {
|
const updateText = e => {
|
||||||
builderStore.actions.updateProp("text", e.target.textContent.trim())
|
builderStore.actions.updateProp("text", e.target.textContent)
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
|
@ -102,7 +102,7 @@
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
.spectrum-Card-footer {
|
.spectrum-Card-footer {
|
||||||
word-wrap: anywhere;
|
word-wrap: break-word;
|
||||||
white-space: pre-wrap;
|
white-space: pre-wrap;
|
||||||
}
|
}
|
||||||
.horizontal .spectrum-Card-coverPhoto {
|
.horizontal .spectrum-Card-coverPhoto {
|
||||||
|
|
|
@ -46,12 +46,12 @@
|
||||||
|
|
||||||
// Convert contenteditable HTML to text and save
|
// Convert contenteditable HTML to text and save
|
||||||
const updateText = e => {
|
const updateText = e => {
|
||||||
const sanitized = e.target.innerHTML.replace(/<br>/gi, "\n").trim()
|
builderStore.actions.updateProp("text", e.target.textContent)
|
||||||
builderStore.actions.updateProp("text", sanitized)
|
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<p
|
{#key $component.editing}
|
||||||
|
<p
|
||||||
bind:this={node}
|
bind:this={node}
|
||||||
contenteditable={$component.editing}
|
contenteditable={$component.editing}
|
||||||
use:styleable={styles}
|
use:styleable={styles}
|
||||||
|
@ -61,9 +61,10 @@
|
||||||
class:underline
|
class:underline
|
||||||
class="spectrum-Body {sizeClass} {alignClass}"
|
class="spectrum-Body {sizeClass} {alignClass}"
|
||||||
on:blur={$component.editing ? updateText : null}
|
on:blur={$component.editing ? updateText : null}
|
||||||
>
|
>
|
||||||
{componentText}
|
{componentText}
|
||||||
</p>
|
</p>
|
||||||
|
{/key}
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
p {
|
p {
|
||||||
|
|
|
@ -125,7 +125,11 @@
|
||||||
{#if schemaLoaded}
|
{#if schemaLoaded}
|
||||||
<Block>
|
<Block>
|
||||||
<div class="card-list" use:styleable={$component.styles}>
|
<div class="card-list" use:styleable={$component.styles}>
|
||||||
<BlockComponent type="form" bind:id={formId} props={{ dataSource }}>
|
<BlockComponent
|
||||||
|
type="form"
|
||||||
|
bind:id={formId}
|
||||||
|
props={{ dataSource, disableValidation: true }}
|
||||||
|
>
|
||||||
{#if title || enrichedSearchColumns?.length || showTitleButton}
|
{#if title || enrichedSearchColumns?.length || showTitleButton}
|
||||||
<div class="header" class:mobile={$context.device.mobile}>
|
<div class="header" class:mobile={$context.device.mobile}>
|
||||||
<div class="title">
|
<div class="title">
|
||||||
|
|
|
@ -106,7 +106,11 @@
|
||||||
{#if schemaLoaded}
|
{#if schemaLoaded}
|
||||||
<Block>
|
<Block>
|
||||||
<div class={size} use:styleable={$component.styles}>
|
<div class={size} use:styleable={$component.styles}>
|
||||||
<BlockComponent type="form" bind:id={formId} props={{ dataSource }}>
|
<BlockComponent
|
||||||
|
type="form"
|
||||||
|
bind:id={formId}
|
||||||
|
props={{ dataSource, disableValidation: true }}
|
||||||
|
>
|
||||||
{#if title || enrichedSearchColumns?.length || showTitleButton}
|
{#if title || enrichedSearchColumns?.length || showTitleButton}
|
||||||
<div class="header" class:mobile={$context.device.mobile}>
|
<div class="header" class:mobile={$context.device.mobile}>
|
||||||
<div class="title">
|
<div class="title">
|
||||||
|
|
|
@ -34,7 +34,7 @@
|
||||||
color: var(--spectrum-global-color-gray-700) !important;
|
color: var(--spectrum-global-color-gray-700) !important;
|
||||||
}
|
}
|
||||||
div :global(.apexcharts-datalabel) {
|
div :global(.apexcharts-datalabel) {
|
||||||
fill: var(--spectrum-global-color-gray-800);
|
fill: white;
|
||||||
}
|
}
|
||||||
div :global(.apexcharts-tooltip) {
|
div :global(.apexcharts-tooltip) {
|
||||||
background-color: var(--spectrum-global-color-gray-200) !important;
|
background-color: var(--spectrum-global-color-gray-200) !important;
|
||||||
|
@ -45,4 +45,12 @@
|
||||||
background-color: var(--spectrum-global-color-gray-100) !important;
|
background-color: var(--spectrum-global-color-gray-100) !important;
|
||||||
border-color: var(--spectrum-global-color-gray-300) !important;
|
border-color: var(--spectrum-global-color-gray-300) !important;
|
||||||
}
|
}
|
||||||
|
div :global(.apexcharts-theme-dark .apexcharts-tooltip-text) {
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
div
|
||||||
|
:global(.apexcharts-theme-dark
|
||||||
|
.apexcharts-tooltip-series-group.apexcharts-active) {
|
||||||
|
padding-bottom: 0;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -2,8 +2,8 @@
|
||||||
import L from "leaflet"
|
import L from "leaflet"
|
||||||
import sanitizeHtml from "sanitize-html"
|
import sanitizeHtml from "sanitize-html"
|
||||||
import "leaflet/dist/leaflet.css"
|
import "leaflet/dist/leaflet.css"
|
||||||
import { Helpers } from "@budibase/bbui"
|
import { Helpers, Button } from "@budibase/bbui"
|
||||||
import { getContext } from "svelte"
|
import { onMount, getContext } from "svelte"
|
||||||
import {
|
import {
|
||||||
FullScreenControl,
|
FullScreenControl,
|
||||||
LocationControl,
|
LocationControl,
|
||||||
|
@ -24,86 +24,16 @@
|
||||||
export let defaultLocation
|
export let defaultLocation
|
||||||
export let tileURL = "https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
|
export let tileURL = "https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
|
||||||
export let mapAttribution
|
export let mapAttribution
|
||||||
|
export let creationEnabled = false
|
||||||
|
export let onClickMarker
|
||||||
|
export let onCreateMarker
|
||||||
|
|
||||||
const { styleable, notificationStore } = getContext("sdk")
|
const { styleable, notificationStore } = getContext("sdk")
|
||||||
const component = getContext("component")
|
const component = getContext("component")
|
||||||
const embeddedMapId = `${Helpers.uuid()}-wrapper`
|
const embeddedMapId = `${Helpers.uuid()}-wrapper`
|
||||||
|
|
||||||
let cachedDeviceCoordinates
|
|
||||||
const fallbackCoordinates = [51.5072, -0.1276] //London
|
|
||||||
|
|
||||||
let mapInstance
|
|
||||||
let mapMarkerGroup = new L.FeatureGroup()
|
|
||||||
let mapMarkers = []
|
|
||||||
|
|
||||||
let minZoomLevel = 0
|
|
||||||
let maxZoomLevel = 18
|
|
||||||
let adjustedZoomLevel = !Number.isInteger(zoomLevel)
|
|
||||||
? 72
|
|
||||||
: Math.round(zoomLevel * (maxZoomLevel / 100))
|
|
||||||
|
|
||||||
$: zoomControlUpdated(mapInstance, zoomEnabled)
|
|
||||||
$: locationControlUpdated(mapInstance, locationEnabled)
|
|
||||||
$: fullScreenControlUpdated(mapInstance, fullScreenEnabled)
|
|
||||||
$: updateMapDimensions(
|
|
||||||
mapInstance,
|
|
||||||
$component.styles.normal.width,
|
|
||||||
$component.styles.normal.height
|
|
||||||
)
|
|
||||||
$: addMapMarkers(
|
|
||||||
mapInstance,
|
|
||||||
dataProvider?.rows,
|
|
||||||
latitudeKey,
|
|
||||||
longitudeKey,
|
|
||||||
titleKey
|
|
||||||
)
|
|
||||||
$: if (typeof mapInstance === "object" && mapMarkers.length > 0) {
|
|
||||||
mapInstance.setZoom(0)
|
|
||||||
mapInstance.fitBounds(mapMarkerGroup.getBounds(), {
|
|
||||||
paddingTopLeft: [0, 24],
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const updateMapDimensions = mapInstance => {
|
|
||||||
if (typeof mapInstance !== "object") {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
mapInstance.invalidateSize()
|
|
||||||
}
|
|
||||||
|
|
||||||
let isValidLatitude = value => {
|
|
||||||
return !isNaN(value) && value > -90 && value < 90
|
|
||||||
}
|
|
||||||
|
|
||||||
let isValidLongitude = value => {
|
|
||||||
return !isNaN(value) && value > -180 && value < 180
|
|
||||||
}
|
|
||||||
|
|
||||||
const parseDefaultLocation = defaultLocation => {
|
|
||||||
if (typeof defaultLocation !== "string") {
|
|
||||||
return fallbackCoordinates
|
|
||||||
}
|
|
||||||
let defaultLocationParts = defaultLocation.split(",")
|
|
||||||
if (defaultLocationParts.length !== 2) {
|
|
||||||
return fallbackCoordinates
|
|
||||||
}
|
|
||||||
|
|
||||||
let parsedDefaultLatitude = parseFloat(defaultLocationParts[0].trim())
|
|
||||||
let parsedDefaultLongitude = parseFloat(defaultLocationParts[1].trim())
|
|
||||||
|
|
||||||
return isValidLatitude(parsedDefaultLatitude) === true &&
|
|
||||||
isValidLongitude(parsedDefaultLongitude) === true
|
|
||||||
? [parsedDefaultLatitude, parsedDefaultLongitude]
|
|
||||||
: fallbackCoordinates
|
|
||||||
}
|
|
||||||
|
|
||||||
$: defaultCoordinates =
|
|
||||||
mapMarkers.length > 0
|
|
||||||
? parseDefaultLocation(defaultLocation)
|
|
||||||
: fallbackCoordinates
|
|
||||||
|
|
||||||
// Map Button Controls
|
// Map Button Controls
|
||||||
let locationControl = new LocationControl({
|
const locationControl = new LocationControl({
|
||||||
position: "bottomright",
|
position: "bottomright",
|
||||||
onLocationFail: err => {
|
onLocationFail: err => {
|
||||||
if (err.code === GeolocationPositionError.PERMISSION_DENIED) {
|
if (err.code === GeolocationPositionError.PERMISSION_DENIED) {
|
||||||
|
@ -129,13 +59,135 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
let fullScreenControl = new FullScreenControl({
|
const fullScreenControl = new FullScreenControl({
|
||||||
position: "topright",
|
position: "topright",
|
||||||
})
|
})
|
||||||
let zoomControl = L.control.zoom({
|
const zoomControl = L.control.zoom({
|
||||||
position: "bottomright",
|
position: "bottomright",
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Map and marker configuration
|
||||||
|
const defaultMarkerOptions = {
|
||||||
|
html:
|
||||||
|
'<div><svg width="26px" height="26px" class="spectrum-Icon" focusable="false" stroke="#b12b27" stroke-width="1%">' +
|
||||||
|
'<use xlink:href="#spectrum-icon-18-Location" /></svg></div>',
|
||||||
|
className: "embedded-map-marker",
|
||||||
|
iconSize: [26, 26],
|
||||||
|
iconAnchor: [13, 26],
|
||||||
|
popupAnchor: [0, -13],
|
||||||
|
}
|
||||||
|
const mapMarkerOptions = {
|
||||||
|
icon: L.divIcon(defaultMarkerOptions),
|
||||||
|
draggable: false,
|
||||||
|
alt: "Location Marker",
|
||||||
|
}
|
||||||
|
const candidateMarkerOptions = {
|
||||||
|
icon: L.divIcon({
|
||||||
|
...defaultMarkerOptions,
|
||||||
|
className: "embedded-map-marker--candidate",
|
||||||
|
}),
|
||||||
|
draggable: false,
|
||||||
|
alt: "Location Marker",
|
||||||
|
}
|
||||||
|
const mapOptions = {
|
||||||
|
fullScreen: false,
|
||||||
|
zoomControl: false,
|
||||||
|
scrollWheelZoom: zoomEnabled,
|
||||||
|
minZoomLevel,
|
||||||
|
maxZoomLevel,
|
||||||
|
}
|
||||||
|
const fallbackCoordinates = [51.5072, -0.1276] //London
|
||||||
|
|
||||||
|
let mapInstance
|
||||||
|
let mapMarkerGroup = new L.FeatureGroup()
|
||||||
|
let candidateMarkerGroup = new L.FeatureGroup()
|
||||||
|
let candidateMarkerPosition
|
||||||
|
let mounted = false
|
||||||
|
let initialMarkerZoomCompleted = false
|
||||||
|
let minZoomLevel = 0
|
||||||
|
let maxZoomLevel = 18
|
||||||
|
let cachedDeviceCoordinates
|
||||||
|
|
||||||
|
$: validRows = getValidRows(dataProvider?.rows, latitudeKey, longitudeKey)
|
||||||
|
$: safeZoomLevel = parseZoomLevel(zoomLevel)
|
||||||
|
$: defaultCoordinates = parseDefaultLocation(defaultLocation)
|
||||||
|
$: initMap(tileURL, mapAttribution, safeZoomLevel)
|
||||||
|
$: zoomControlUpdated(mapInstance, zoomEnabled)
|
||||||
|
$: locationControlUpdated(mapInstance, locationEnabled)
|
||||||
|
$: fullScreenControlUpdated(mapInstance, fullScreenEnabled)
|
||||||
|
$: width = $component.styles.normal.width
|
||||||
|
$: height = $component.styles.normal.height
|
||||||
|
$: width, height, mapInstance?.invalidateSize()
|
||||||
|
$: defaultCoordinates, resetView()
|
||||||
|
$: addMapMarkers(
|
||||||
|
mapInstance,
|
||||||
|
validRows,
|
||||||
|
latitudeKey,
|
||||||
|
longitudeKey,
|
||||||
|
titleKey,
|
||||||
|
onClickMarker
|
||||||
|
)
|
||||||
|
|
||||||
|
const isValidLatitude = value => {
|
||||||
|
return !isNaN(value) && value > -90 && value < 90
|
||||||
|
}
|
||||||
|
|
||||||
|
const isValidLongitude = value => {
|
||||||
|
return !isNaN(value) && value > -180 && value < 180
|
||||||
|
}
|
||||||
|
|
||||||
|
const getValidRows = (rows, latKey, lngKey) => {
|
||||||
|
if (!rows?.length || !latKey || !lngKey) {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
return rows.filter(row => {
|
||||||
|
return isValidLatitude(row[latKey]) && isValidLongitude(row[lngKey])
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const parseZoomLevel = zoomLevel => {
|
||||||
|
let zoom = zoomLevel
|
||||||
|
if (zoom == null || isNaN(zoom)) {
|
||||||
|
zoom = 50
|
||||||
|
} else {
|
||||||
|
zoom = parseFloat(zoom)
|
||||||
|
zoom = Math.max(0, Math.min(100, zoom))
|
||||||
|
}
|
||||||
|
return Math.round((zoom * maxZoomLevel) / 100)
|
||||||
|
}
|
||||||
|
|
||||||
|
const parseDefaultLocation = defaultLocation => {
|
||||||
|
if (typeof defaultLocation !== "string") {
|
||||||
|
return fallbackCoordinates
|
||||||
|
}
|
||||||
|
let defaultLocationParts = defaultLocation.split(",")
|
||||||
|
if (defaultLocationParts.length !== 2) {
|
||||||
|
return fallbackCoordinates
|
||||||
|
}
|
||||||
|
|
||||||
|
let parsedDefaultLatitude = parseFloat(defaultLocationParts[0].trim())
|
||||||
|
let parsedDefaultLongitude = parseFloat(defaultLocationParts[1].trim())
|
||||||
|
|
||||||
|
return isValidLatitude(parsedDefaultLatitude) === true &&
|
||||||
|
isValidLongitude(parsedDefaultLongitude) === true
|
||||||
|
? [parsedDefaultLatitude, parsedDefaultLongitude]
|
||||||
|
: fallbackCoordinates
|
||||||
|
}
|
||||||
|
|
||||||
|
const resetView = () => {
|
||||||
|
if (!mapInstance) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (mapMarkerGroup.getLayers().length) {
|
||||||
|
mapInstance.setZoom(0)
|
||||||
|
mapInstance.fitBounds(mapMarkerGroup.getBounds(), {
|
||||||
|
paddingTopLeft: [0, 24],
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
mapInstance.setView(defaultCoordinates, safeZoomLevel)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const locationControlUpdated = (mapInstance, locationEnabled) => {
|
const locationControlUpdated = (mapInstance, locationEnabled) => {
|
||||||
if (typeof mapInstance !== "object") {
|
if (typeof mapInstance !== "object") {
|
||||||
return
|
return
|
||||||
|
@ -171,44 +223,25 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
//Map icon and marker configuration
|
const addMapMarkers = (
|
||||||
const mapIconMarkup =
|
mapInstance,
|
||||||
'<div><svg width="26px" height="26px" class="spectrum-Icon" focusable="false" stroke="#b12b27" stroke-width="1%">' +
|
validRows,
|
||||||
'<use xlink:href="#spectrum-icon-18-Location" /></svg></div>'
|
latKey,
|
||||||
const mapIcon = L.divIcon({
|
lngKey,
|
||||||
html: mapIconMarkup,
|
titleKey,
|
||||||
className: "embedded-map-marker",
|
onClick
|
||||||
iconSize: [26, 26],
|
) => {
|
||||||
iconAnchor: [13, 26],
|
if (!mapInstance) {
|
||||||
popupAnchor: [0, -13],
|
|
||||||
})
|
|
||||||
const mapMarkerOptions = {
|
|
||||||
icon: mapIcon,
|
|
||||||
draggable: false,
|
|
||||||
alt: "Location Marker",
|
|
||||||
}
|
|
||||||
let mapOptions = {
|
|
||||||
fullScreen: false,
|
|
||||||
zoomControl: false,
|
|
||||||
scrollWheelZoom: zoomEnabled,
|
|
||||||
minZoomLevel,
|
|
||||||
maxZoomLevel,
|
|
||||||
}
|
|
||||||
|
|
||||||
const addMapMarkers = (mapInstance, rows, latKey, lngKey, titleKey) => {
|
|
||||||
if (typeof mapInstance !== "object" || !rows || !latKey || !lngKey) {
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
mapMarkerGroup.clearLayers()
|
mapMarkerGroup.clearLayers()
|
||||||
|
if (!validRows?.length) {
|
||||||
const validRows = rows.filter(row => {
|
return
|
||||||
return isValidLatitude(row[latKey]) && isValidLongitude(row[lngKey])
|
}
|
||||||
})
|
|
||||||
|
|
||||||
validRows.forEach(row => {
|
validRows.forEach(row => {
|
||||||
let markerCoords = [row[latKey], row[lngKey]]
|
let markerCoords = [row[latKey], row[lngKey]]
|
||||||
|
|
||||||
let marker = L.marker(markerCoords, mapMarkerOptions).addTo(mapInstance)
|
let marker = L.marker(markerCoords, mapMarkerOptions).addTo(mapInstance)
|
||||||
let markerContent = generateMarkerPopupContent(
|
let markerContent = generateMarkerPopupContent(
|
||||||
row[latKey],
|
row[latKey],
|
||||||
|
@ -216,52 +249,105 @@
|
||||||
row[titleKey]
|
row[titleKey]
|
||||||
)
|
)
|
||||||
|
|
||||||
marker.bindPopup(markerContent).addTo(mapMarkerGroup)
|
marker
|
||||||
|
.bindTooltip(markerContent, {
|
||||||
|
direction: "top",
|
||||||
|
offset: [0, -25],
|
||||||
|
})
|
||||||
|
.addTo(mapMarkerGroup)
|
||||||
|
|
||||||
//https://github.com/Leaflet/Leaflet/issues/7331
|
if (onClick) {
|
||||||
marker.on("click", function () {
|
marker.on("click", () => {
|
||||||
this.openPopup()
|
onClick({
|
||||||
|
marker: row,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
mapMarkers = [...mapMarkers, marker]
|
// Zoom to markers if this is the first time
|
||||||
})
|
if (!initialMarkerZoomCompleted) {
|
||||||
|
resetView()
|
||||||
|
initialMarkerZoomCompleted = true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const generateMarkerPopupContent = (latitude, longitude, text) => {
|
const generateMarkerPopupContent = (latitude, longitude, text) => {
|
||||||
return text || latitude + "," + longitude
|
return text || latitude + "," + longitude
|
||||||
}
|
}
|
||||||
|
|
||||||
const initMap = () => {
|
const initMap = (tileURL, attribution, zoom) => {
|
||||||
const initCoords = defaultCoordinates
|
if (!mounted) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (mapInstance) {
|
||||||
|
mapInstance.remove()
|
||||||
|
}
|
||||||
mapInstance = L.map(embeddedMapId, mapOptions)
|
mapInstance = L.map(embeddedMapId, mapOptions)
|
||||||
mapMarkerGroup.addTo(mapInstance)
|
mapMarkerGroup.addTo(mapInstance)
|
||||||
|
candidateMarkerGroup.addTo(mapInstance)
|
||||||
|
|
||||||
const cleanAttribution = sanitizeHtml(mapAttribution, {
|
// Add attribution
|
||||||
|
const cleanAttribution = sanitizeHtml(attribution, {
|
||||||
allowedTags: ["a"],
|
allowedTags: ["a"],
|
||||||
allowedAttributes: {
|
allowedAttributes: {
|
||||||
a: ["href", "target"],
|
a: ["href", "target"],
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
L.tileLayer(tileURL, {
|
L.tileLayer(tileURL, {
|
||||||
attribution: "© " + cleanAttribution,
|
attribution: "© " + cleanAttribution,
|
||||||
zoom: adjustedZoomLevel,
|
zoom,
|
||||||
}).addTo(mapInstance)
|
}).addTo(mapInstance)
|
||||||
|
|
||||||
//Initialise the map
|
// Add click handler
|
||||||
mapInstance.setView(initCoords, adjustedZoomLevel)
|
mapInstance.on("click", handleMapClick)
|
||||||
|
|
||||||
|
// Reset view
|
||||||
|
resetView()
|
||||||
}
|
}
|
||||||
|
|
||||||
const mapAction = () => {
|
const handleMapClick = e => {
|
||||||
initMap()
|
if (!creationEnabled) {
|
||||||
return {
|
return
|
||||||
destroy() {
|
}
|
||||||
mapInstance.remove()
|
candidateMarkerGroup.clearLayers()
|
||||||
mapInstance = undefined
|
candidateMarkerPosition = [e.latlng.lat, e.latlng.lng]
|
||||||
},
|
let candidateMarker = L.marker(
|
||||||
|
candidateMarkerPosition,
|
||||||
|
candidateMarkerOptions
|
||||||
|
)
|
||||||
|
candidateMarker
|
||||||
|
.bindTooltip("New marker", {
|
||||||
|
permanent: true,
|
||||||
|
direction: "top",
|
||||||
|
offset: [0, -25],
|
||||||
|
})
|
||||||
|
.addTo(candidateMarkerGroup)
|
||||||
|
.on("click", clearCandidateMarker)
|
||||||
|
}
|
||||||
|
|
||||||
|
const createMarker = async () => {
|
||||||
|
if (!onCreateMarker) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const res = await onCreateMarker({
|
||||||
|
lat: candidateMarkerPosition[0],
|
||||||
|
lng: candidateMarkerPosition[1],
|
||||||
|
})
|
||||||
|
if (res !== false) {
|
||||||
|
clearCandidateMarker()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const clearCandidateMarker = () => {
|
||||||
|
candidateMarkerGroup.clearLayers()
|
||||||
|
candidateMarkerPosition = null
|
||||||
|
}
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
mounted = true
|
||||||
|
initMap(tileURL, mapAttribution, safeZoomLevel)
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="embedded-map-wrapper map-default" use:styleable={$component.styles}>
|
<div class="embedded-map-wrapper map-default" use:styleable={$component.styles}>
|
||||||
|
@ -269,12 +355,20 @@
|
||||||
<div>{error}</div>
|
<div>{error}</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<div id={embeddedMapId} class="embedded embedded-map" use:mapAction />
|
<div id={embeddedMapId} class="embedded embedded-map" />
|
||||||
|
|
||||||
|
{#if candidateMarkerPosition}
|
||||||
|
<div class="button-container">
|
||||||
|
<Button secondary quiet on:click={clearCandidateMarker}>Cancel</Button>
|
||||||
|
<Button cta on:click={createMarker}>Create marker</Button>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.embedded-map-wrapper {
|
.embedded-map-wrapper {
|
||||||
background-color: #f1f1f1;
|
background-color: #f1f1f1;
|
||||||
|
height: 320px;
|
||||||
}
|
}
|
||||||
.map-default {
|
.map-default {
|
||||||
min-height: 180px;
|
min-height: 180px;
|
||||||
|
@ -287,6 +381,9 @@
|
||||||
.embedded-map :global(.embedded-map-marker) {
|
.embedded-map :global(.embedded-map-marker) {
|
||||||
color: #ee3b35;
|
color: #ee3b35;
|
||||||
}
|
}
|
||||||
|
.embedded-map :global(.embedded-map-marker--candidate) {
|
||||||
|
color: var(--primaryColor);
|
||||||
|
}
|
||||||
.embedded-map :global(.embedded-map-control) {
|
.embedded-map :global(.embedded-map-control) {
|
||||||
font-size: 22px;
|
font-size: 22px;
|
||||||
}
|
}
|
||||||
|
@ -294,4 +391,12 @@
|
||||||
height: 100%;
|
height: 100%;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
.button-container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--spacing-xl);
|
||||||
|
margin-top: var(--spacing-xl);
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -50,12 +50,13 @@
|
||||||
$: labelClass = labelPos === "above" ? "" : `spectrum-FieldLabel--${labelPos}`
|
$: labelClass = labelPos === "above" ? "" : `spectrum-FieldLabel--${labelPos}`
|
||||||
|
|
||||||
const updateLabel = e => {
|
const updateLabel = e => {
|
||||||
builderStore.actions.updateProp("label", e.target.textContent.trim())
|
builderStore.actions.updateProp("label", e.target.textContent)
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<FieldGroupFallback>
|
<FieldGroupFallback>
|
||||||
<div class="spectrum-Form-item" use:styleable={$component.styles}>
|
<div class="spectrum-Form-item" use:styleable={$component.styles}>
|
||||||
|
{#key $component.editing}
|
||||||
<label
|
<label
|
||||||
bind:this={labelNode}
|
bind:this={labelNode}
|
||||||
contenteditable={$component.editing}
|
contenteditable={$component.editing}
|
||||||
|
@ -66,6 +67,7 @@
|
||||||
>
|
>
|
||||||
{label || " "}
|
{label || " "}
|
||||||
</label>
|
</label>
|
||||||
|
{/key}
|
||||||
<div class="spectrum-Form-itemField">
|
<div class="spectrum-Form-itemField">
|
||||||
{#if !formContext}
|
{#if !formContext}
|
||||||
<Placeholder text="Form components need to be wrapped in a form" />
|
<Placeholder text="Form components need to be wrapped in a form" />
|
||||||
|
|
|
@ -9,6 +9,10 @@
|
||||||
export let disabled = false
|
export let disabled = false
|
||||||
export let actionType = "Create"
|
export let actionType = "Create"
|
||||||
|
|
||||||
|
// Not exposed as a builder setting. Used internally to disable validation
|
||||||
|
// for fields rendered in things like search blocks.
|
||||||
|
export let disableValidation = false
|
||||||
|
|
||||||
const context = getContext("context")
|
const context = getContext("context")
|
||||||
const { API, fetchDatasourceSchema } = getContext("sdk")
|
const { API, fetchDatasourceSchema } = getContext("sdk")
|
||||||
|
|
||||||
|
@ -102,6 +106,7 @@
|
||||||
{schema}
|
{schema}
|
||||||
{table}
|
{table}
|
||||||
{initialValues}
|
{initialValues}
|
||||||
|
{disableValidation}
|
||||||
>
|
>
|
||||||
<slot />
|
<slot />
|
||||||
</InnerForm>
|
</InnerForm>
|
||||||
|
|
|
@ -10,6 +10,7 @@
|
||||||
export let size
|
export let size
|
||||||
export let schema
|
export let schema
|
||||||
export let table
|
export let table
|
||||||
|
export let disableValidation = false
|
||||||
|
|
||||||
const component = getContext("component")
|
const component = getContext("component")
|
||||||
const { styleable, Provider, ActionTypes } = getContext("sdk")
|
const { styleable, Provider, ActionTypes } = getContext("sdk")
|
||||||
|
@ -141,7 +142,9 @@
|
||||||
|
|
||||||
// Create validation function based on field schema
|
// Create validation function based on field schema
|
||||||
const schemaConstraints = schema?.[field]?.constraints
|
const schemaConstraints = schema?.[field]?.constraints
|
||||||
const validator = createValidatorFromConstraints(
|
const validator = disableValidation
|
||||||
|
? null
|
||||||
|
: createValidatorFromConstraints(
|
||||||
schemaConstraints,
|
schemaConstraints,
|
||||||
validationRules,
|
validationRules,
|
||||||
field,
|
field,
|
||||||
|
@ -164,7 +167,7 @@
|
||||||
// If this field has already been registered and we previously had an
|
// If this field has already been registered and we previously had an
|
||||||
// error set, then re-run the validator to see if we can unset it
|
// error set, then re-run the validator to see if we can unset it
|
||||||
if (fieldState.error) {
|
if (fieldState.error) {
|
||||||
initialError = validator(initialValue)
|
initialError = validator?.(initialValue)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -254,7 +257,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update field state
|
// Update field state
|
||||||
const error = validator ? validator(value) : null
|
const error = validator?.(value)
|
||||||
fieldInfo.update(state => {
|
fieldInfo.update(state => {
|
||||||
state.fieldState.value = value
|
state.fieldState.value = value
|
||||||
state.fieldState.error = error
|
state.fieldState.error = error
|
||||||
|
@ -288,7 +291,9 @@
|
||||||
|
|
||||||
// Create new validator
|
// Create new validator
|
||||||
const schemaConstraints = schema?.[field]?.constraints
|
const schemaConstraints = schema?.[field]?.constraints
|
||||||
const validator = createValidatorFromConstraints(
|
const validator = disableValidation
|
||||||
|
? null
|
||||||
|
: createValidatorFromConstraints(
|
||||||
schemaConstraints,
|
schemaConstraints,
|
||||||
validationRules,
|
validationRules,
|
||||||
field,
|
field,
|
||||||
|
|
|
@ -20,8 +20,8 @@
|
||||||
let listenersAttached = false
|
let listenersAttached = false
|
||||||
|
|
||||||
const proxyInvalidation = event => {
|
const proxyInvalidation = event => {
|
||||||
const { dataSourceId } = event.detail
|
const { dataSourceId, options } = event.detail
|
||||||
dataSourceStore.actions.invalidateDataSource(dataSourceId)
|
dataSourceStore.actions.invalidateDataSource(dataSourceId, options)
|
||||||
}
|
}
|
||||||
|
|
||||||
const proxyNotification = event => {
|
const proxyNotification = event => {
|
||||||
|
|
|
@ -66,6 +66,11 @@
|
||||||
newTop = deviceBottom - 44
|
newTop = deviceBottom - 44
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//If element is at the very top of the screen, put the bar below the element
|
||||||
|
if (elBounds.top < elBounds.height) {
|
||||||
|
newTop = elBounds.bottom + verticalOffset
|
||||||
|
}
|
||||||
|
|
||||||
// Horizontally, try to center first.
|
// Horizontally, try to center first.
|
||||||
// Failing that, render to left edge of component.
|
// Failing that, render to left edge of component.
|
||||||
// Failing that, render to right edge of component,
|
// Failing that, render to right edge of component,
|
||||||
|
|
|
@ -4,30 +4,36 @@ const initialState = {
|
||||||
showConfirmation: false,
|
showConfirmation: false,
|
||||||
title: null,
|
title: null,
|
||||||
text: null,
|
text: null,
|
||||||
callback: null,
|
onConfirm: null,
|
||||||
|
onCancel: null,
|
||||||
}
|
}
|
||||||
|
|
||||||
const createConfirmationStore = () => {
|
const createConfirmationStore = () => {
|
||||||
const store = writable(initialState)
|
const store = writable(initialState)
|
||||||
|
|
||||||
const showConfirmation = (title, text, callback) => {
|
const showConfirmation = (title, text, onConfirm, onCancel) => {
|
||||||
store.set({
|
store.set({
|
||||||
showConfirmation: true,
|
showConfirmation: true,
|
||||||
title,
|
title,
|
||||||
text,
|
text,
|
||||||
callback,
|
onConfirm,
|
||||||
|
onCancel,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
const confirm = async () => {
|
const confirm = async () => {
|
||||||
const state = get(store)
|
const state = get(store)
|
||||||
if (!state.showConfirmation || !state.callback) {
|
if (!state.showConfirmation || !state.onConfirm) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
store.set(initialState)
|
store.set(initialState)
|
||||||
await state.callback()
|
await state.onConfirm()
|
||||||
}
|
}
|
||||||
const cancel = () => {
|
const cancel = () => {
|
||||||
|
const state = get(store)
|
||||||
store.set(initialState)
|
store.set(initialState)
|
||||||
|
if (state.onCancel) {
|
||||||
|
state.onCancel()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
|
@ -54,18 +54,24 @@ export const createDataSourceStore = () => {
|
||||||
|
|
||||||
// Invalidates a specific dataSource ID by refreshing all instances
|
// Invalidates a specific dataSource ID by refreshing all instances
|
||||||
// which depend on data from that dataSource
|
// which depend on data from that dataSource
|
||||||
const invalidateDataSource = async dataSourceId => {
|
const invalidateDataSource = async (dataSourceId, options) => {
|
||||||
if (!dataSourceId) {
|
if (!dataSourceId) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Merge default options
|
||||||
|
options = {
|
||||||
|
invalidateRelationships: false,
|
||||||
|
...options,
|
||||||
|
}
|
||||||
|
|
||||||
// Emit this as a window event, so parent screens which are iframing us in
|
// Emit this as a window event, so parent screens which are iframing us in
|
||||||
// can also invalidate the same datasource
|
// can also invalidate the same datasource
|
||||||
const inModal = get(routeStore).queryParams?.peek
|
const inModal = get(routeStore).queryParams?.peek
|
||||||
if (inModal) {
|
if (inModal) {
|
||||||
window.parent.postMessage({
|
window.parent.postMessage({
|
||||||
type: "invalidate-datasource",
|
type: "invalidate-datasource",
|
||||||
detail: { dataSourceId },
|
detail: { dataSourceId, options },
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -73,13 +79,14 @@ export const createDataSourceStore = () => {
|
||||||
|
|
||||||
// Fetch related table IDs from table schema
|
// Fetch related table IDs from table schema
|
||||||
let schema
|
let schema
|
||||||
|
if (options.invalidateRelationships) {
|
||||||
try {
|
try {
|
||||||
const definition = await API.fetchTableDefinition(dataSourceId)
|
const definition = await API.fetchTableDefinition(dataSourceId)
|
||||||
schema = definition?.schema
|
schema = definition?.schema
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
schema = null
|
schema = null
|
||||||
}
|
}
|
||||||
|
}
|
||||||
if (schema) {
|
if (schema) {
|
||||||
Object.values(schema).forEach(fieldSchema => {
|
Object.values(schema).forEach(fieldSchema => {
|
||||||
if (
|
if (
|
||||||
|
|
|
@ -37,7 +37,9 @@ const saveRowHandler = async (action, context) => {
|
||||||
notificationStore.actions.success("Row saved")
|
notificationStore.actions.success("Row saved")
|
||||||
|
|
||||||
// Refresh related datasources
|
// Refresh related datasources
|
||||||
await dataSourceStore.actions.invalidateDataSource(row.tableId)
|
await dataSourceStore.actions.invalidateDataSource(row.tableId, {
|
||||||
|
invalidateRelationships: true,
|
||||||
|
})
|
||||||
|
|
||||||
return { row }
|
return { row }
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
@ -65,7 +67,9 @@ const duplicateRowHandler = async (action, context) => {
|
||||||
notificationStore.actions.success("Row saved")
|
notificationStore.actions.success("Row saved")
|
||||||
|
|
||||||
// Refresh related datasources
|
// Refresh related datasources
|
||||||
await dataSourceStore.actions.invalidateDataSource(row.tableId)
|
await dataSourceStore.actions.invalidateDataSource(row.tableId, {
|
||||||
|
invalidateRelationships: true,
|
||||||
|
})
|
||||||
|
|
||||||
return { row }
|
return { row }
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
@ -83,7 +87,9 @@ const deleteRowHandler = async action => {
|
||||||
notificationStore.actions.success("Row deleted")
|
notificationStore.actions.success("Row deleted")
|
||||||
|
|
||||||
// Refresh related datasources
|
// Refresh related datasources
|
||||||
await dataSourceStore.actions.invalidateDataSource(tableId)
|
await dataSourceStore.actions.invalidateDataSource(tableId, {
|
||||||
|
invalidateRelationships: true,
|
||||||
|
})
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// Abort next actions
|
// Abort next actions
|
||||||
return false
|
return false
|
||||||
|
@ -261,6 +267,26 @@ const exportDataHandler = async action => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const continueIfHandler = action => {
|
||||||
|
const { type, value, operator, referenceValue } = action.parameters
|
||||||
|
if (!type || !operator) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
let match = false
|
||||||
|
if (value == null && referenceValue == null) {
|
||||||
|
match = true
|
||||||
|
} else if (value === referenceValue) {
|
||||||
|
match = true
|
||||||
|
} else {
|
||||||
|
match = JSON.stringify(value) === JSON.stringify(referenceValue)
|
||||||
|
}
|
||||||
|
if (type === "continue") {
|
||||||
|
return operator === "equal" ? match : !match
|
||||||
|
} else {
|
||||||
|
return operator === "equal" ? !match : match
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const handlerMap = {
|
const handlerMap = {
|
||||||
["Save Row"]: saveRowHandler,
|
["Save Row"]: saveRowHandler,
|
||||||
["Duplicate Row"]: duplicateRowHandler,
|
["Duplicate Row"]: duplicateRowHandler,
|
||||||
|
@ -277,6 +303,7 @@ const handlerMap = {
|
||||||
["Update State"]: updateStateHandler,
|
["Update State"]: updateStateHandler,
|
||||||
["Upload File to S3"]: s3UploadHandler,
|
["Upload File to S3"]: s3UploadHandler,
|
||||||
["Export Data"]: exportDataHandler,
|
["Export Data"]: exportDataHandler,
|
||||||
|
["Continue if / Stop if"]: continueIfHandler,
|
||||||
}
|
}
|
||||||
|
|
||||||
const confirmTextMap = {
|
const confirmTextMap = {
|
||||||
|
@ -302,13 +329,13 @@ export const enrichButtonActions = (actions, context) => {
|
||||||
return actions
|
return actions
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handlers = actions.map(def => handlerMap[def["##eventHandlerType"]])
|
||||||
|
return async eventContext => {
|
||||||
// Button context is built up as actions are executed.
|
// Button context is built up as actions are executed.
|
||||||
// Inherit any previous button context which may have come from actions
|
// Inherit any previous button context which may have come from actions
|
||||||
// before a confirmable action since this breaks the chain.
|
// before a confirmable action since this breaks the chain.
|
||||||
let buttonContext = context.actions || []
|
let buttonContext = context.actions || []
|
||||||
|
|
||||||
const handlers = actions.map(def => handlerMap[def["##eventHandlerType"]])
|
|
||||||
return async () => {
|
|
||||||
for (let i = 0; i < handlers.length; i++) {
|
for (let i = 0; i < handlers.length; i++) {
|
||||||
try {
|
try {
|
||||||
// Skip any non-existent action definitions
|
// Skip any non-existent action definitions
|
||||||
|
@ -317,7 +344,12 @@ export const enrichButtonActions = (actions, context) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Built total context for this action
|
// Built total context for this action
|
||||||
const totalContext = { ...context, actions: buttonContext }
|
const totalContext = {
|
||||||
|
...context,
|
||||||
|
state: get(stateStore),
|
||||||
|
actions: buttonContext,
|
||||||
|
eventContext,
|
||||||
|
}
|
||||||
|
|
||||||
// Get and enrich this button action with the total context
|
// Get and enrich this button action with the total context
|
||||||
let action = actions[i]
|
let action = actions[i]
|
||||||
|
@ -327,6 +359,7 @@ export const enrichButtonActions = (actions, context) => {
|
||||||
// If this action is confirmable, show confirmation and await a
|
// If this action is confirmable, show confirmation and await a
|
||||||
// callback to execute further actions
|
// callback to execute further actions
|
||||||
if (action.parameters?.confirm) {
|
if (action.parameters?.confirm) {
|
||||||
|
return new Promise(resolve => {
|
||||||
const defaultText = confirmTextMap[action["##eventHandlerType"]]
|
const defaultText = confirmTextMap[action["##eventHandlerType"]]
|
||||||
const confirmText = action.parameters?.confirmText || defaultText
|
const confirmText = action.parameters?.confirmText || defaultText
|
||||||
confirmationStore.actions.showConfirmation(
|
confirmationStore.actions.showConfirmation(
|
||||||
|
@ -346,14 +379,16 @@ export const enrichButtonActions = (actions, context) => {
|
||||||
actions.slice(i + 1),
|
actions.slice(i + 1),
|
||||||
newContext
|
newContext
|
||||||
)
|
)
|
||||||
await next()
|
resolve(await next())
|
||||||
|
} else {
|
||||||
|
resolve(false)
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
() => {
|
||||||
|
resolve(false)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
})
|
||||||
// Stop enriching actions when encountering a confirmable action,
|
|
||||||
// as the callback continues the action chain
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// For non-confirmable actions, execute the handler immediately
|
// For non-confirmable actions, execute the handler immediately
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue