Merge remote-tracking branch 'origin/develop' into feature/foreach-block
This commit is contained in:
commit
53c676672a
|
@ -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,9 +24,28 @@
|
|||
{
|
||||
"files": ["*.svelte"],
|
||||
"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": {
|
||||
"no-self-assign": "off"
|
||||
},
|
||||
"globals": {
|
||||
"GeolocationPositionError": true
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,6 +2,8 @@ name: Budibase Smoke Test
|
|||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
schedule:
|
||||
- cron: "0 5 * * *" # every day at 5AM
|
||||
|
||||
jobs:
|
||||
release:
|
||||
|
@ -23,10 +25,13 @@ jobs:
|
|||
-o packages/builder/cypress.env.json \
|
||||
-L https://api.github.com/repos/budibase/budibase-infra/contents/test/cypress.env.json
|
||||
wc -l packages/builder/cypress.env.json
|
||||
- run: yarn test:e2e:ci
|
||||
env:
|
||||
CI: true
|
||||
name: Budibase CI
|
||||
|
||||
- name: Cypress run
|
||||
id: cypress
|
||||
uses: cypress-io/github-action@v2
|
||||
with:
|
||||
install: false
|
||||
command: yarn test:e2e:ci
|
||||
|
||||
# TODO: upload recordings to s3
|
||||
# - name: Configure AWS Credentials
|
||||
|
@ -36,11 +41,11 @@ jobs:
|
|||
# aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
|
||||
# aws-region: eu-west-1
|
||||
|
||||
# TODO look at cypress reporters
|
||||
# - name: Discord Webhook Action
|
||||
# uses: tsickert/discord-webhook@v4.0.0
|
||||
# with:
|
||||
# webhook-url: ${{ secrets.PROD_DEPLOY_WEBHOOK_URL }}
|
||||
# content: "Production Deployment Complete: ${{ env.RELEASE_VERSION }} deployed to Budibase Cloud."
|
||||
# embed-title: ${{ env.RELEASE_VERSION }}
|
||||
- name: Discord Webhook Action
|
||||
uses: tsickert/discord-webhook@v4.0.0
|
||||
with:
|
||||
webhook-url: ${{ secrets.BUDI_QA_WEBHOOK }}
|
||||
content: "Smoke test run completed with ${{ steps.cypress.outcome }}. See results at ${{ steps.cypress.dashboardUrl }}"
|
||||
embed-title: ${{ steps.cypress.outcome }}
|
||||
embed-color: ${{ steps.cypress.outcome == 'success' && '3066993' || '15548997' }}
|
||||
|
||||
|
|
|
@ -22,9 +22,16 @@
|
|||
"name": "Budibase Worker",
|
||||
"type": "node",
|
||||
"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"
|
||||
}
|
||||
},
|
||||
],
|
||||
"compounds": [
|
||||
{
|
||||
|
|
16
README.md
16
README.md
|
@ -102,15 +102,19 @@ Budibase is made to scale. With Budibase, you can self-host on your own infrastr
|
|||
|
||||
- Checkout the promo video: https://youtu.be/xoljVpty_Kw
|
||||
|
||||
<br /><br />
|
||||
<br />
|
||||
|
||||
### Extend Budibase with its Public API
|
||||
---
|
||||
|
||||
<br />
|
||||
|
||||
|
||||
## Budibase Public API
|
||||
As with anything that we build in Budibase, our new public API is simple to use, flexible, and introduces new extensibility. To summarize, the Budibase API enables:
|
||||
|
||||
- Budibase as a backend
|
||||
- Inter-operability
|
||||
- Interoperability
|
||||
|
||||
Guide: [Build an app with Budibase and Next.js](https://budibase.com/blog/building-a-crud-app-with-budibase-and-next.js/)
|
||||
|
||||
#### Docs
|
||||
You can learn more about the Budibase API at the following places:
|
||||
|
@ -118,6 +122,10 @@ You can learn more about the Budibase API at the following places:
|
|||
- [General documentation](https://docs.budibase.com/docs/public-api) : Learn how to get your API key, how to use spec, and how to use with Postman
|
||||
- [Interactive API documentation](https://docs.budibase.com/reference/post_applications) : Learn how to interact with the API
|
||||
|
||||
#### Guides
|
||||
|
||||
- [Build an app with Budibase and Next.js](https://budibase.com/blog/building-a-crud-app-with-budibase-and-next.js/)
|
||||
|
||||
<p align="center">
|
||||
<img alt="Budibase data" src="https://res.cloudinary.com/daog6scxm/image/upload/v1647858558/Feb%20release/Start_building_with_Budibase_s_API_3_rhlzhv.png">
|
||||
</p>
|
||||
|
|
|
@ -15,7 +15,7 @@ version: 0.2.8
|
|||
appVersion: 1.0.48
|
||||
dependencies:
|
||||
- name: couchdb
|
||||
version: 3.3.4
|
||||
version: 3.6.1
|
||||
repository: https://apache.github.io/couchdb-helm
|
||||
condition: services.couchdb.enabled
|
||||
- name: ingress-nginx
|
||||
|
|
|
@ -110,6 +110,10 @@ spec:
|
|||
value: {{ .Values.globals.cookieDomain | quote }}
|
||||
- name: HTTP_MIGRATIONS
|
||||
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 }}
|
||||
imagePullPolicy: Always
|
||||
name: bbapps
|
||||
|
|
|
@ -1894,9 +1894,9 @@ minimist-options@4.1.0:
|
|||
kind-of "^6.0.3"
|
||||
|
||||
minimist@^1.2.0:
|
||||
version "1.2.5"
|
||||
resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602"
|
||||
integrity sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==
|
||||
version "1.2.6"
|
||||
resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.6.tgz#8637a5b759ea0d6e98702cfb3a9283323c93af44"
|
||||
integrity sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==
|
||||
|
||||
minipass-collect@^1.0.2:
|
||||
version "1.0.2"
|
||||
|
|
|
@ -12,7 +12,7 @@ All ports are BLOCKED except 22 (SSH), 80 (HTTP), 443 (HTTPS), and 10000
|
|||
|
||||
* Budibase website: http://budibase.com
|
||||
|
||||
For help and more information, visit https://docs.budibase.com/self-hosting/hosting-methods/digitalocean
|
||||
For help and more information, visit https://docs.budibase.com/docs/digitalocean
|
||||
|
||||
********************************************************************************
|
||||
To delete this message of the day: rm -rf $(readlink -f ${0})
|
||||
|
|
|
@ -5,7 +5,7 @@ version: "3"
|
|||
services:
|
||||
minio-service:
|
||||
container_name: budi-minio-dev
|
||||
restart: always
|
||||
restart: on-failure
|
||||
image: minio/minio
|
||||
volumes:
|
||||
- minio_data:/data
|
||||
|
@ -23,7 +23,7 @@ services:
|
|||
|
||||
proxy-service:
|
||||
container_name: budi-nginx-dev
|
||||
restart: always
|
||||
restart: on-failure
|
||||
image: nginx:latest
|
||||
volumes:
|
||||
- ./.generated-nginx.dev.conf:/etc/nginx/nginx.conf
|
||||
|
@ -38,7 +38,7 @@ services:
|
|||
couchdb-service:
|
||||
# platform: linux/amd64
|
||||
container_name: budi-couchdb-dev
|
||||
restart: always
|
||||
restart: on-failure
|
||||
image: ibmcom/couchdb3
|
||||
environment:
|
||||
- COUCHDB_PASSWORD=${COUCH_DB_PASSWORD}
|
||||
|
@ -59,7 +59,7 @@ services:
|
|||
|
||||
redis-service:
|
||||
container_name: budi-redis-dev
|
||||
restart: always
|
||||
restart: on-failure
|
||||
image: redis
|
||||
command: redis-server --requirepass ${REDIS_PASSWORD}
|
||||
ports:
|
||||
|
|
|
@ -4,7 +4,7 @@ version: "3"
|
|||
|
||||
services:
|
||||
app-service:
|
||||
restart: always
|
||||
restart: unless-stopped
|
||||
image: budibase.docker.scarf.sh/budibase/apps
|
||||
container_name: bbapps
|
||||
environment:
|
||||
|
@ -28,7 +28,7 @@ services:
|
|||
- redis-service
|
||||
|
||||
worker-service:
|
||||
restart: always
|
||||
restart: unless-stopped
|
||||
image: budibase.docker.scarf.sh/budibase/worker
|
||||
container_name: bbworker
|
||||
environment:
|
||||
|
@ -53,7 +53,7 @@ services:
|
|||
- couch-init
|
||||
|
||||
minio-service:
|
||||
restart: always
|
||||
restart: unless-stopped
|
||||
image: minio/minio
|
||||
volumes:
|
||||
- minio_data:/data
|
||||
|
@ -69,7 +69,7 @@ services:
|
|||
retries: 3
|
||||
|
||||
proxy-service:
|
||||
restart: always
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "${MAIN_PORT}:10000"
|
||||
container_name: bbproxy
|
||||
|
@ -81,7 +81,7 @@ services:
|
|||
- couchdb-service
|
||||
|
||||
couchdb-service:
|
||||
restart: always
|
||||
restart: unless-stopped
|
||||
image: ibmcom/couchdb3
|
||||
environment:
|
||||
- COUCHDB_PASSWORD=${COUCH_DB_PASSWORD}
|
||||
|
@ -98,13 +98,14 @@ services:
|
|||
command: ["sh","-c","sleep 10 && $${PUT_CALL}/_users && $${PUT_CALL}/_replicator; fg;"]
|
||||
|
||||
redis-service:
|
||||
restart: always
|
||||
restart: unless-stopped
|
||||
image: redis
|
||||
command: redis-server --requirepass ${REDIS_PASSWORD}
|
||||
volumes:
|
||||
- redis_data:/data
|
||||
|
||||
watchtower-service:
|
||||
restart: always
|
||||
image: containrrr/watchtower
|
||||
volumes:
|
||||
- /var/run/docker.sock:/var/run/docker.sock
|
||||
|
@ -116,7 +117,6 @@ services:
|
|||
labels:
|
||||
- "com.centurylinklabs.watchtower.enable=false"
|
||||
|
||||
|
||||
volumes:
|
||||
couchdb3_data:
|
||||
driver: local
|
||||
|
|
|
@ -52,9 +52,8 @@ http {
|
|||
proxy_pass http://{{ address }}:4001;
|
||||
}
|
||||
|
||||
location /app/ {
|
||||
location /app {
|
||||
proxy_pass http://{{ address }}:4001;
|
||||
rewrite ^/app/(.*)$ /$1 break;
|
||||
}
|
||||
|
||||
location /builder {
|
||||
|
|
|
@ -22,9 +22,8 @@ http {
|
|||
resolver {{ resolver }} valid=10s ipv6=off;
|
||||
|
||||
# buffering
|
||||
client_body_buffer_size 1K;
|
||||
client_header_buffer_size 1k;
|
||||
client_max_body_size 10M;
|
||||
client_max_body_size 20M;
|
||||
ignore_invalid_headers off;
|
||||
proxy_buffering off;
|
||||
|
||||
|
@ -43,13 +42,25 @@ http {
|
|||
client_max_body_size 1000m;
|
||||
ignore_invalid_headers 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
|
||||
add_header X-Frame-Options SAMEORIGIN always;
|
||||
add_header X-Content-Type-Options nosniff 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
|
||||
set $apps {{ apps }};
|
||||
|
@ -62,7 +73,6 @@ http {
|
|||
|
||||
location /app {
|
||||
proxy_pass http://$apps:4002;
|
||||
rewrite ^/app/(.*)$ /$1 break;
|
||||
}
|
||||
|
||||
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
|
|
@ -39,7 +39,7 @@
|
|||
</p>
|
||||
|
||||
<h3 align="center">
|
||||
<a href="https://docs.budibase.com/getting-started">Los Geht's</a>
|
||||
<a href="https://docs.budibase.com/docs/quickstart-tutorials">Los Geht's</a>
|
||||
<span> · </span>
|
||||
<a href="https://docs.budibase.com">Dokumentation</a>
|
||||
<span> · </span>
|
||||
|
@ -109,7 +109,7 @@ $ budi hosting --start
|
|||
4. Lege einen Admin-Benutzer an.
|
||||
Gib die E-Mail und das Passwort für den neuen Admin-Benutzer ein.
|
||||
|
||||
Schon geschafft! Jetzt kann es losgehen mit der minutenschnellen Entwicklung deiner Tools. Für weitere Informationen und Tipps schau doch mal in unsere [Dokumentation](https://docs.budibase.com/getting-started).
|
||||
Schon geschafft! Jetzt kann es losgehen mit der minutenschnellen Entwicklung deiner Tools. Für weitere Informationen und Tipps schau doch mal in unsere [Dokumentation](https://docs.budibase.com/docs/quickstart-tutorials).
|
||||
|
||||
<br />
|
||||
|
||||
|
|
|
@ -112,7 +112,7 @@ The Budibase builder runs in Electron, on Mac, PC and Linux. Follow the steps be
|
|||
|
||||
Budibase wants to make sure anyone can use the tools we develop and we know a lot of people need to be able to host the apps they make on their own systems - that is why we've decided to try and make self hosting as easy as possible!
|
||||
|
||||
Currently, you can host your apps using Docker or Digital Ocean. The documentation for self-hosting can be found [here](https://docs.budibase.com/self-hosting/introduction-to-self-hosting).
|
||||
Currently, you can host your apps using Docker or Digital Ocean. The documentation for self-hosting can be found [here](https://docs.budibase.com/docs/hosting-methods).
|
||||
|
||||
[![Deploy to DO](https://www.deploytodo.com/do-btn-blue.svg)](https://cloud.digitalocean.com/droplets/new?onboarding_origin=marketplace&i=09038e&fleetUuid=bb04f9c8-1de8-4687-b2ae-1d5177a0535b&appId=77729671&type=applications&size=s-4vcpu-8gb®ion=nyc1&refcode=0caaa6085a82&image=budibase-20-04)
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"version": "1.0.91-alpha.6",
|
||||
"version": "1.0.105-alpha.8",
|
||||
"npmClient": "yarn",
|
||||
"packages": [
|
||||
"packages/*"
|
||||
|
|
18
package.json
18
package.json
|
@ -15,7 +15,9 @@
|
|||
"prettier-plugin-svelte": "^2.3.0",
|
||||
"rimraf": "^3.0.2",
|
||||
"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": {
|
||||
"setup": "node ./hosting/scripts/setup.js && yarn && yarn bootstrap && yarn build && yarn dev",
|
||||
|
@ -31,17 +33,19 @@
|
|||
"nuke:docker": "lerna run --parallel dev:stack:nuke",
|
||||
"clean": "lerna clean",
|
||||
"kill-port": "kill-port 4001",
|
||||
"dev": "yarn run kill-port && lerna link && lerna run --parallel dev:builder --concurrency 1",
|
||||
"dev:noserver": "lerna link && lerna run dev:stack:up && lerna run --parallel dev:builder --concurrency 1 --ignore @budibase/server --ignore @budibase/worker",
|
||||
"dev:server": "lerna run --parallel dev:builder --concurrency 1 --scope @budibase/worker --scope @budibase/server",
|
||||
"kill-builder": "kill-port 3000",
|
||||
"kill-server": "kill-port 4001 4002",
|
||||
"kill-all": "yarn run kill-builder && yarn run kill-server",
|
||||
"dev": "yarn run kill-all && lerna link && lerna run --parallel dev:builder --concurrency 1",
|
||||
"dev:noserver": "yarn run kill-builder && lerna link && lerna run dev:stack:up && lerna run --parallel dev:builder --concurrency 1 --ignore @budibase/server --ignore @budibase/worker",
|
||||
"dev:server": "yarn run kill-server && lerna run --parallel dev:builder --concurrency 1 --scope @budibase/worker --scope @budibase/server",
|
||||
"test": "lerna run test",
|
||||
"lint:eslint": "eslint packages",
|
||||
"lint:prettier": "prettier --check \"packages/**/*.{js,ts,svelte}\"",
|
||||
"lint": "yarn run lint:eslint && yarn run lint:prettier",
|
||||
"lint:fix:eslint": "eslint --fix packages",
|
||||
"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:ts && yarn run lint:fix:prettier && yarn run lint:fix:eslint",
|
||||
"lint:fix": "yarn run lint:fix:prettier && yarn run lint:fix:eslint",
|
||||
"test:e2e": "lerna run cy:test --stream",
|
||||
"test:e2e:ci": "lerna run cy:ci --stream",
|
||||
"build:specs": "lerna run specs",
|
||||
|
@ -54,6 +58,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:airgap": "node hosting/scripts/airgapped/airgappedDockerBuild",
|
||||
"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",
|
||||
"release:helm": "node scripts/releaseHelmChart",
|
||||
"env:multi:enable": "lerna run env:multi:enable",
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@budibase/backend-core",
|
||||
"version": "1.0.91-alpha.6",
|
||||
"version": "1.0.105-alpha.8",
|
||||
"description": "Budibase backend core libraries used in server and worker",
|
||||
"main": "src/index.js",
|
||||
"author": "Budibase",
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
const env = require("../environment")
|
||||
const { Headers } = require("../../constants")
|
||||
const { SEPARATOR, DocumentTypes } = require("../db/constants")
|
||||
const cls = require("./FunctionContext")
|
||||
const { getCouch } = require("../db")
|
||||
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) => {
|
||||
if (!appId) {
|
||||
throw new Error("appId is required")
|
||||
}
|
||||
return cls.run(() => {
|
||||
// set the app tenant id
|
||||
setAppTenantId(appId)
|
||||
|
||||
// set the app ID
|
||||
cls.setOnContext(ContextKeys.APP_ID, appId)
|
||||
|
||||
|
|
|
@ -9,11 +9,7 @@ const {
|
|||
APP_PREFIX,
|
||||
APP_DEV,
|
||||
} = require("./constants")
|
||||
const {
|
||||
getTenantId,
|
||||
getTenantIDFromAppID,
|
||||
getGlobalDBName,
|
||||
} = require("../tenancy")
|
||||
const { getTenantId, getGlobalDBName } = require("../tenancy")
|
||||
const fetch = require("node-fetch")
|
||||
const { getCouch } = require("./index")
|
||||
const { getAppMetadata } = require("../cache/appMetadata")
|
||||
|
@ -39,7 +35,6 @@ exports.DocumentTypes = DocumentTypes
|
|||
exports.APP_PREFIX = APP_PREFIX
|
||||
exports.APP_DEV = exports.APP_DEV_PREFIX = APP_DEV
|
||||
exports.SEPARATOR = SEPARATOR
|
||||
exports.getTenantIDFromAppID = getTenantIDFromAppID
|
||||
exports.isDevApp = isDevApp
|
||||
exports.isProdAppID = isProdAppID
|
||||
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 auditLog = require("./auditLog")
|
||||
const tenancy = require("./tenancy")
|
||||
const appTenancy = require("./appTenancy")
|
||||
const internalApi = require("./internalApi")
|
||||
const datasourceGoogle = require("./passport/datasource/google")
|
||||
const csrf = require("./csrf")
|
||||
|
@ -19,7 +18,6 @@ module.exports = {
|
|||
authenticated,
|
||||
auditLog,
|
||||
tenancy,
|
||||
appTenancy,
|
||||
authError,
|
||||
internalApi,
|
||||
datasource: {
|
||||
|
|
|
@ -1,15 +1,29 @@
|
|||
const google = require("../google")
|
||||
const { Cookies } = require("../../../constants")
|
||||
const { Cookies, Configs } = require("../../../constants")
|
||||
const { clearCookie, getCookie } = require("../../../utils")
|
||||
const { getDB } = require("../../../db")
|
||||
const { getScopedConfig } = require("../../../db/utils")
|
||||
const environment = require("../../../environment")
|
||||
const { getGlobalDB } = require("../../../tenancy")
|
||||
|
||||
async function preAuth(passport, ctx, next) {
|
||||
// get the relevant config
|
||||
const googleConfig = {
|
||||
async function fetchGoogleCreds() {
|
||||
// try and get the config from the tenant
|
||||
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,
|
||||
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`
|
||||
const strategy = await google.strategyFactory(googleConfig, callbackUrl)
|
||||
|
||||
|
@ -26,10 +40,7 @@ async function preAuth(passport, ctx, next) {
|
|||
|
||||
async function postAuth(passport, ctx, next) {
|
||||
// get the relevant config
|
||||
const config = {
|
||||
clientID: environment.GOOGLE_CLIENT_ID,
|
||||
clientSecret: environment.GOOGLE_CLIENT_SECRET,
|
||||
}
|
||||
const config = await fetchGoogleCreds()
|
||||
|
||||
let callbackUrl = `${environment.PLATFORM_URL}/api/global/auth/datasource/google/callback`
|
||||
const strategy = await google.strategyFactory(
|
||||
|
|
|
@ -51,7 +51,10 @@ exports.strategyFactory = async function (
|
|||
)
|
||||
} catch (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
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
const { cloneDeep } = require("lodash/fp")
|
||||
const { BUILTIN_PERMISSION_IDS } = require("./permissions")
|
||||
const { BUILTIN_PERMISSION_IDS, PermissionLevels } = require("./permissions")
|
||||
const {
|
||||
generateRoleID,
|
||||
getRoleParams,
|
||||
|
@ -180,6 +180,20 @@ exports.getUserRoleHierarchy = async (userRoleId, opts = { idOnly: true }) => {
|
|||
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.
|
||||
* @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))
|
||||
}
|
||||
}
|
||||
// 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
|
||||
}
|
||||
|
||||
/**
|
||||
* This retrieves the required role
|
||||
* @param permLevel
|
||||
* @param resourceId
|
||||
* @param subResourceId
|
||||
* @return {Promise<{permissions}|Object>}
|
||||
* This retrieves the required role for a resource
|
||||
* @param permLevel The level of request
|
||||
* @param resourceId The resource being requested
|
||||
* @param subResourceId The sub resource being requested
|
||||
* @return {Promise<{permissions}|Object>} returns the permissions required to access.
|
||||
*/
|
||||
exports.getRequiredResourceRole = async (
|
||||
permLevel,
|
||||
|
|
|
@ -14,22 +14,7 @@ function makeSessionID(userId, sessionId) {
|
|||
return `${userId}/${sessionId}`
|
||||
}
|
||||
|
||||
exports.createASession = async (userId, session) => {
|
||||
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) => {
|
||||
async function invalidateSessions(userId, sessionIds = null) {
|
||||
let sessions = []
|
||||
|
||||
// If no sessionIds, get all the sessions for the user
|
||||
|
@ -55,6 +40,24 @@ exports.invalidateSessions = async (userId, sessionIds = null) => {
|
|||
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 => {
|
||||
const client = await redis.getSessionClient()
|
||||
const key = makeSessionID(session.userId, session.sessionId)
|
||||
|
@ -67,8 +70,6 @@ exports.endSession = async (userId, sessionId) => {
|
|||
await client.delete(makeSessionID(userId, sessionId))
|
||||
}
|
||||
|
||||
exports.getUserSessions = getSessionsForUser
|
||||
|
||||
exports.getSession = async (userId, sessionId) => {
|
||||
try {
|
||||
const client = await redis.getSessionClient()
|
||||
|
@ -84,3 +85,6 @@ exports.getAllSessions = async () => {
|
|||
const sessions = await client.scan()
|
||||
return sessions.map(session => session.value)
|
||||
}
|
||||
|
||||
exports.getUserSessions = getSessionsForUser
|
||||
exports.invalidateSessions = invalidateSessions
|
||||
|
|
|
@ -1,6 +1,11 @@
|
|||
const { getDB } = require("../db")
|
||||
const { SEPARATOR, StaticDatabases, DocumentTypes } = require("../db/constants")
|
||||
const { getTenantId, DEFAULT_TENANT_ID, isMultiTenant } = require("../context")
|
||||
const { SEPARATOR, StaticDatabases } = require("../db/constants")
|
||||
const {
|
||||
getTenantId,
|
||||
DEFAULT_TENANT_ID,
|
||||
isMultiTenant,
|
||||
getTenantIDFromAppID,
|
||||
} = require("../context")
|
||||
const env = require("../environment")
|
||||
|
||||
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) => {
|
||||
let userTenantId
|
||||
if (user) {
|
||||
|
@ -145,7 +130,7 @@ exports.isUserInAppTenant = (appId, user = null) => {
|
|||
} else {
|
||||
userTenantId = getTenantId()
|
||||
}
|
||||
const tenantId = exports.getTenantIDFromAppID(appId) || DEFAULT_TENANT_ID
|
||||
const tenantId = getTenantIDFromAppID(appId) || DEFAULT_TENANT_ID
|
||||
return tenantId === userTenantId
|
||||
}
|
||||
|
||||
|
|
|
@ -3,6 +3,7 @@ const {
|
|||
SEPARATOR,
|
||||
ViewNames,
|
||||
generateGlobalUserID,
|
||||
getAllApps,
|
||||
} = require("./db/utils")
|
||||
const jwt = require("jsonwebtoken")
|
||||
const { options } = require("./middleware/passport/jwt")
|
||||
|
@ -20,8 +21,10 @@ const { hash } = require("./hashing")
|
|||
const userCache = require("./cache/user")
|
||||
const env = require("./environment")
|
||||
const { getUserSessions, invalidateSessions } = require("./security/sessions")
|
||||
const tenancy = require("./tenancy")
|
||||
|
||||
const APP_PREFIX = DocumentTypes.APP + SEPARATOR
|
||||
const PROD_APP_PREFIX = "/app/"
|
||||
|
||||
function confirmAppId(possibleAppId) {
|
||||
return possibleAppId && possibleAppId.startsWith(APP_PREFIX)
|
||||
|
@ -29,16 +32,35 @@ function confirmAppId(possibleAppId) {
|
|||
: 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
|
||||
* @param {object} ctx The main request body to look through.
|
||||
* @returns {string|undefined} If an appId was found it will be returned.
|
||||
*/
|
||||
exports.getAppId = ctx => {
|
||||
const options = [ctx.headers[Headers.APP_ID], ctx.params.appId]
|
||||
if (ctx.subdomains) {
|
||||
options.push(ctx.subdomains[1])
|
||||
}
|
||||
exports.getAppIdFromCtx = async ctx => {
|
||||
// look in headers
|
||||
const options = [ctx.headers[Headers.APP_ID]]
|
||||
let appId
|
||||
for (let option of options) {
|
||||
appId = confirmAppId(option)
|
||||
|
@ -47,16 +69,24 @@ exports.getAppId = ctx => {
|
|||
}
|
||||
}
|
||||
|
||||
// look in body if can't find it in subdomain
|
||||
// look in body
|
||||
if (!appId && ctx.request.body && ctx.request.body.appId) {
|
||||
appId = confirmAppId(ctx.request.body.appId)
|
||||
}
|
||||
|
||||
// look in the url - dev app
|
||||
let appPath =
|
||||
ctx.request.headers.referrer ||
|
||||
ctx.path.split("/").filter(subPath => subPath.startsWith(APP_PREFIX))
|
||||
if (!appId && appPath.length !== 0) {
|
||||
if (!appId && appPath.length) {
|
||||
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
|
||||
}
|
||||
|
||||
|
|
|
@ -3338,9 +3338,9 @@ mimic-fn@^2.1.0:
|
|||
brace-expansion "^1.1.7"
|
||||
|
||||
minimist@^1.1.1, minimist@^1.2.0, minimist@^1.2.5:
|
||||
version "1.2.5"
|
||||
resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602"
|
||||
integrity sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==
|
||||
version "1.2.6"
|
||||
resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.6.tgz#8637a5b759ea0d6e98702cfb3a9283323c93af44"
|
||||
integrity sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==
|
||||
|
||||
mixin-deep@^1.2.0:
|
||||
version "1.3.2"
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "@budibase/bbui",
|
||||
"description": "A UI solution used in the different Budibase projects.",
|
||||
"version": "1.0.91-alpha.6",
|
||||
"version": "1.0.105-alpha.8",
|
||||
"license": "MPL-2.0",
|
||||
"svelte": "src/index.js",
|
||||
"module": "dist/bbui.es.js",
|
||||
|
@ -38,7 +38,7 @@
|
|||
],
|
||||
"dependencies": {
|
||||
"@adobe/spectrum-css-workflow-icons": "^1.2.1",
|
||||
"@budibase/string-templates": "^1.0.91-alpha.6",
|
||||
"@budibase/string-templates": "^1.0.105-alpha.8",
|
||||
"@spectrum-css/actionbutton": "^1.0.1",
|
||||
"@spectrum-css/actiongroup": "^1.0.1",
|
||||
"@spectrum-css/avatar": "^3.0.2",
|
||||
|
|
|
@ -13,6 +13,7 @@
|
|||
export let icon = undefined
|
||||
export let active = false
|
||||
export let tooltip = undefined
|
||||
export let dataCy
|
||||
|
||||
let showTooltip = false
|
||||
</script>
|
||||
|
@ -27,6 +28,7 @@
|
|||
class:active
|
||||
class="spectrum-Button spectrum-Button--size{size.toUpperCase()}"
|
||||
{disabled}
|
||||
data-cy={dataCy}
|
||||
on:click|preventDefault
|
||||
on:mouseover={() => (showTooltip = true)}
|
||||
on:focus={() => (showTooltip = true)}
|
||||
|
|
|
@ -19,18 +19,33 @@
|
|||
const dispatch = createEventDispatcher()
|
||||
const flatpickrId = `${uuid()}-wrapper`
|
||||
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 = {
|
||||
element: `#${flatpickrId}`,
|
||||
enableTime: isTimeOnly || enableTime || false,
|
||||
noCalendar: isTimeOnly || false,
|
||||
enableTime: timeOnly || enableTime || false,
|
||||
noCalendar: timeOnly || false,
|
||||
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,
|
||||
appendTo,
|
||||
disableMobile: "true",
|
||||
onReady: () => {
|
||||
let timestamp = resolveTimeStamp(value)
|
||||
if (timeOnly && timestamp) {
|
||||
dispatch("change", timestamp.toISOString())
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
const handleChange = event => {
|
||||
|
@ -39,10 +54,9 @@
|
|||
if (newValue) {
|
||||
newValue = newValue.toISOString()
|
||||
}
|
||||
// if time only set date component to today
|
||||
// if time only set date component to 2000-01-01
|
||||
if (timeOnly) {
|
||||
const todayDate = new Date().toISOString().split("T")[0]
|
||||
newValue = `${todayDate}T${newValue.split("T")[1]}`
|
||||
newValue = `2000-01-01T${newValue.split("T")[1]}`
|
||||
}
|
||||
dispatch("change", newValue)
|
||||
}
|
||||
|
@ -76,10 +90,13 @@
|
|||
return null
|
||||
}
|
||||
let date
|
||||
let time = new Date(`0-${val}`)
|
||||
let time
|
||||
|
||||
// it is a string like 00:00:00, just time
|
||||
if (timeOnly || (typeof val === "string" && !isNaN(time))) {
|
||||
date = time
|
||||
let ts = resolveTimeStamp(val)
|
||||
|
||||
if (timeOnly && ts) {
|
||||
date = ts
|
||||
} else if (val instanceof Date) {
|
||||
// Use real date obj if already parsed
|
||||
date = val
|
||||
|
@ -101,7 +118,7 @@
|
|||
}
|
||||
</script>
|
||||
|
||||
{#key isTimeOnly}
|
||||
{#key timeOnly}
|
||||
<Flatpickr
|
||||
bind:flatpickr
|
||||
value={parseDate(value)}
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
import Divider from "../Divider/Divider.svelte"
|
||||
import Icon from "../Icon/Icon.svelte"
|
||||
import Context from "../context"
|
||||
import ProgressCircle from "../ProgressCircle/ProgressCircle.svelte"
|
||||
|
||||
export let title = undefined
|
||||
export let size = "S"
|
||||
|
@ -102,15 +103,22 @@
|
|||
<Button group secondary on:click={close}>{cancelText}</Button>
|
||||
{/if}
|
||||
{#if showConfirmButton}
|
||||
<Button
|
||||
group
|
||||
cta
|
||||
{...$$restProps}
|
||||
disabled={confirmDisabled}
|
||||
on:click={confirm}
|
||||
>
|
||||
{confirmText}
|
||||
</Button>
|
||||
<span class="confirm-wrap">
|
||||
<Button
|
||||
group
|
||||
cta
|
||||
{...$$restProps}
|
||||
disabled={confirmDisabled}
|
||||
on:click={confirm}
|
||||
>
|
||||
{#if loading}
|
||||
<ProgressCircle overBackground={true} size="S" />
|
||||
{/if}
|
||||
{#if !loading}
|
||||
{confirmText}
|
||||
{/if}
|
||||
</Button>
|
||||
</span>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
|
@ -169,4 +177,8 @@
|
|||
.spectrum-Dialog-buttonGroup {
|
||||
padding-left: 0;
|
||||
}
|
||||
|
||||
.confirm-wrap :global(.spectrum-Button-label) {
|
||||
display: contents;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -53,10 +53,10 @@
|
|||
to-gfm-code-block "^0.1.1"
|
||||
year "^0.2.1"
|
||||
|
||||
"@budibase/string-templates@^1.0.84":
|
||||
version "1.0.84"
|
||||
resolved "https://registry.yarnpkg.com/@budibase/string-templates/-/string-templates-1.0.84.tgz#66669e9898ed20c6be2c46a5eb19d55003eb6bcb"
|
||||
integrity sha512-6Tv/TfGkmr3uBwNdZ3eKAPKwdsRTZbuQ+02puH+EcJK2leCerINo1SpAHf1BOmjQJynKeslKpSkUiisRVerMEg==
|
||||
"@budibase/string-templates@^1.0.104":
|
||||
version "1.0.104"
|
||||
resolved "https://registry.yarnpkg.com/@budibase/string-templates/-/string-templates-1.0.104.tgz#f812700f2b21f638fd1e48dde065ae693fae2897"
|
||||
integrity sha512-3caq3qwpIieyb9m8eSl8OhcE0ppzuyJ/0ubDlWmtpbmwmG2v3ynI+DwxpbG4CcVQFuebD2yxU0CZfioU76vKCQ==
|
||||
dependencies:
|
||||
"@budibase/handlebars-helpers" "^0.11.8"
|
||||
dayjs "^1.10.4"
|
||||
|
@ -2177,9 +2177,9 @@ minimatch@^3.0.4:
|
|||
brace-expansion "^1.1.7"
|
||||
|
||||
minimist@^1.2.0, minimist@^1.2.5:
|
||||
version "1.2.5"
|
||||
resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602"
|
||||
integrity sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==
|
||||
version "1.2.6"
|
||||
resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.6.tgz#8637a5b759ea0d6e98702cfb3a9283323c93af44"
|
||||
integrity sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==
|
||||
|
||||
mixin-deep@^1.2.0:
|
||||
version "1.3.2"
|
||||
|
|
|
@ -5,10 +5,10 @@ filterTests(['all'], () => {
|
|||
before(() => {
|
||||
cy.login()
|
||||
})
|
||||
|
||||
|
||||
it("should change the icon and colour for an application", () => {
|
||||
// Search for test application
|
||||
cy.searchForApplication("Cypress Tests")
|
||||
cy.applicationInAppTable("Cypress Tests")
|
||||
cy.get(".appTable")
|
||||
.within(() => {
|
||||
cy.get(".spectrum-Icon").eq(1).click()
|
||||
|
|
|
@ -2,11 +2,164 @@ import filterTests from '../support/filterTests'
|
|||
|
||||
filterTests(['smoke', 'all'], () => {
|
||||
context("Create an Application", () => {
|
||||
it("should create a new application", () => {
|
||||
|
||||
before(() => {
|
||||
cy.login()
|
||||
cy.createTestApp()
|
||||
cy.visit(`${Cypress.config().baseUrl}/builder`)
|
||||
cy.contains("Cypress Tests").should("exist")
|
||||
cy.deleteApp("Cypress Tests")
|
||||
})
|
||||
|
||||
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", () => {
|
||||
cy.visit(`${Cypress.config().baseUrl}/builder`)
|
||||
cy.wait(1000)
|
||||
cy.createUser("bbuser@test.com")
|
||||
cy.get(".spectrum-Table").should("contain", "bbuser")
|
||||
})
|
||||
|
@ -21,95 +23,104 @@ filterTests(["smoke", "all"], () => {
|
|||
cy.get(".spectrum-Table").eq(0).contains("No rows found")
|
||||
})
|
||||
|
||||
it("should assign role types", () => {
|
||||
// 3 apps minimum required - to assign an app to each role type
|
||||
cy.request(`${Cypress.config().baseUrl}/api/applications?status=all`)
|
||||
.its("body")
|
||||
.then(val => {
|
||||
if (val.length < 3) {
|
||||
for (let i = 1; i < 3; i++) {
|
||||
const uuid = () => Cypress._.random(0, 1e6)
|
||||
const name = uuid()
|
||||
cy.createApp(name)
|
||||
if (Cypress.env("TEST_ENV")) {
|
||||
it("should assign role types", () => {
|
||||
// 3 apps minimum required - to assign an app to each role type
|
||||
cy.request(`${Cypress.config().baseUrl}/api/applications?status=all`)
|
||||
.its("body")
|
||||
.then(val => {
|
||||
if (val.length < 3) {
|
||||
for (let i = 1; i < 3; i++) {
|
||||
const uuid = () => Cypress._.random(0, 1e6)
|
||||
const name = uuid()
|
||||
if(i < 1){
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
// Navigate back to the user
|
||||
cy.visit(`${Cypress.config().baseUrl}/builder`)
|
||||
cy.wait(500)
|
||||
cy.get(".spectrum-SideNav").contains("Users").click()
|
||||
cy.wait(500)
|
||||
cy.get(".spectrum-Table").contains("bbuser").click()
|
||||
cy.wait(1000)
|
||||
for (let i = 0; i < 3; i++) {
|
||||
cy.get(".spectrum-Table")
|
||||
.eq(1)
|
||||
.find(".spectrum-Table-row")
|
||||
.eq(0)
|
||||
.find(".spectrum-Table-cell")
|
||||
.eq(0)
|
||||
.click()
|
||||
cy.wait(500)
|
||||
cy.get(".spectrum-Dialog-grid")
|
||||
.contains("Choose an option")
|
||||
.click()
|
||||
.then(() => {
|
||||
cy.wait(1000)
|
||||
if (i == 0) {
|
||||
cy.get(".spectrum-Popover").contains("Admin").click()
|
||||
}
|
||||
if (i == 1) {
|
||||
cy.get(".spectrum-Popover").contains("Power").click()
|
||||
}
|
||||
if (i == 2) {
|
||||
cy.get(".spectrum-Popover").contains("Basic").click()
|
||||
}
|
||||
cy.wait(1000)
|
||||
cy.get(".spectrum-Button")
|
||||
.contains("Update role")
|
||||
.click({ force: true })
|
||||
})
|
||||
}
|
||||
// Confirm roles exist within Configure roles table
|
||||
cy.wait(2000)
|
||||
cy.get(".spectrum-Table")
|
||||
.eq(0)
|
||||
.within(assginedRoles => {
|
||||
expect(assginedRoles).to.contain("Admin")
|
||||
expect(assginedRoles).to.contain("Power")
|
||||
expect(assginedRoles).to.contain("Basic")
|
||||
})
|
||||
})
|
||||
|
||||
it("should unassign role types", () => {
|
||||
// Set each app within Configure roles table to 'No Access'
|
||||
cy.get(".spectrum-Table")
|
||||
.eq(0)
|
||||
.find(".spectrum-Table-row")
|
||||
.its("length")
|
||||
.then(len => {
|
||||
for (let i = 0; i < len; i++) {
|
||||
cy.get(".spectrum-Table")
|
||||
.eq(0)
|
||||
.find(".spectrum-Table-row")
|
||||
.eq(0)
|
||||
.find(".spectrum-Table-cell")
|
||||
.eq(0)
|
||||
.click()
|
||||
.then(() => {
|
||||
cy.get(".spectrum-Picker").eq(1).click({ force: true })
|
||||
cy.wait(500)
|
||||
cy.get(".spectrum-Popover").contains("No Access").click()
|
||||
})
|
||||
cy.get(".spectrum-Button")
|
||||
.contains("Update role")
|
||||
.click({ force: true })
|
||||
cy.wait(1000)
|
||||
}
|
||||
})
|
||||
// Confirm Configure roles table no longer has any apps in it
|
||||
cy.get(".spectrum-Table").eq(0).contains("No rows found")
|
||||
})
|
||||
// Navigate back to the user
|
||||
cy.visit(`${Cypress.config().baseUrl}/builder`)
|
||||
cy.wait(500)
|
||||
cy.get(".spectrum-SideNav").contains("Users").click()
|
||||
cy.wait(500)
|
||||
cy.get(".spectrum-Table").contains("bbuser").click()
|
||||
cy.wait(1000)
|
||||
for (let i = 0; i < 3; i++) {
|
||||
cy.get(".spectrum-Table")
|
||||
.eq(1)
|
||||
.find(".spectrum-Table-row")
|
||||
.eq(0)
|
||||
.find(".spectrum-Table-cell")
|
||||
.eq(0)
|
||||
.click()
|
||||
cy.wait(500)
|
||||
cy.get(".spectrum-Dialog-grid")
|
||||
.contains("Choose an option")
|
||||
.click()
|
||||
.then(() => {
|
||||
cy.wait(1000)
|
||||
if (i == 0) {
|
||||
cy.get(".spectrum-Popover").contains("Admin").click()
|
||||
}
|
||||
if (i == 1) {
|
||||
cy.get(".spectrum-Popover").contains("Power").click()
|
||||
}
|
||||
if (i == 2) {
|
||||
cy.get(".spectrum-Popover").contains("Basic").click()
|
||||
}
|
||||
cy.wait(1000)
|
||||
cy.get(".spectrum-Button")
|
||||
.contains("Update role")
|
||||
.click({ force: true })
|
||||
})
|
||||
}
|
||||
// Confirm roles exist within Configure roles table
|
||||
cy.wait(2000)
|
||||
cy.get(".spectrum-Table")
|
||||
.eq(0)
|
||||
.within(assginedRoles => {
|
||||
expect(assginedRoles).to.contain("Admin")
|
||||
expect(assginedRoles).to.contain("Power")
|
||||
expect(assginedRoles).to.contain("Basic")
|
||||
})
|
||||
})
|
||||
|
||||
it("should unassign role types", () => {
|
||||
// Set each app within Configure roles table to 'No Access'
|
||||
cy.get(".spectrum-Table")
|
||||
.eq(0)
|
||||
.find(".spectrum-Table-row")
|
||||
.its("length")
|
||||
.then(len => {
|
||||
for (let i = 0; i < len; i++) {
|
||||
cy.get(".spectrum-Table")
|
||||
.eq(0)
|
||||
.find(".spectrum-Table-row")
|
||||
.eq(0)
|
||||
.find(".spectrum-Table-cell")
|
||||
.eq(0)
|
||||
.click()
|
||||
.then(() => {
|
||||
cy.get(".spectrum-Picker").eq(1).click({ force: true })
|
||||
cy.wait(500)
|
||||
cy.get(".spectrum-Popover").contains("No Access").click()
|
||||
})
|
||||
cy.get(".spectrum-Button")
|
||||
.contains("Update role")
|
||||
.click({ force: true })
|
||||
cy.wait(1000)
|
||||
}
|
||||
})
|
||||
// Confirm Configure roles table no longer has any apps in it
|
||||
cy.get(".spectrum-Table").eq(0).contains("No rows found")
|
||||
})
|
||||
}
|
||||
|
||||
it("should enable Developer access", () => {
|
||||
// Enable Developer access
|
||||
|
|
|
@ -4,6 +4,7 @@ filterTests(['smoke', 'all'], () => {
|
|||
context("Create a View", () => {
|
||||
before(() => {
|
||||
cy.login()
|
||||
|
||||
cy.createTestApp()
|
||||
cy.createTable("data")
|
||||
cy.addColumn("data", "group", "Text")
|
||||
|
|
|
@ -4,8 +4,7 @@ filterTests(["smoke", "all"], () => {
|
|||
context("Query Level Transformers", () => {
|
||||
before(() => {
|
||||
cy.login()
|
||||
cy.deleteApp("Cypress Tests")
|
||||
cy.createApp("Cypress Tests")
|
||||
cy.createTestApp()
|
||||
})
|
||||
|
||||
it("should write a transformer function", () => {
|
||||
|
|
|
@ -1,133 +1,128 @@
|
|||
import filterTests from "../support/filterTests"
|
||||
|
||||
filterTests(['all'], () => {
|
||||
context("Rename an App", () => {
|
||||
beforeEach(() => {
|
||||
cy.login()
|
||||
cy.createTestApp()
|
||||
})
|
||||
context("Rename an App", () => {
|
||||
beforeEach(() => {
|
||||
cy.login()
|
||||
cy.createTestApp()
|
||||
})
|
||||
|
||||
it("should rename an unpublished application", () => {
|
||||
const appName = "Cypress Tests"
|
||||
const appRename = "Cypress Renamed"
|
||||
// Rename app, Search for app, Confirm name was changed
|
||||
cy.get(".home-logo").click()
|
||||
renameApp(appName, appRename)
|
||||
cy.reload()
|
||||
cy.wait(1000)
|
||||
cy.searchForApplication(appRename)
|
||||
cy.get(".appTable").find(".title").should("have.length", 1)
|
||||
// Set app name back to Cypress Tests
|
||||
cy.reload()
|
||||
cy.wait(1000)
|
||||
renameApp(appRename, appName)
|
||||
const appName = "Cypress Tests"
|
||||
const appRename = "Cypress Renamed"
|
||||
// Rename app, Search for app, Confirm name was changed
|
||||
cy.visit(`${Cypress.config().baseUrl}/builder`)
|
||||
cy.wait(500)
|
||||
renameApp(appName, appRename)
|
||||
cy.reload()
|
||||
cy.wait(1000)
|
||||
cy.searchForApplication(appRename)
|
||||
cy.get(".appTable").find(".title").should("have.length", 1)
|
||||
cy.applicationInAppTable(appRename)
|
||||
// Set app name back to Cypress Tests
|
||||
cy.reload()
|
||||
cy.wait(1000)
|
||||
renameApp(appRename, appName)
|
||||
})
|
||||
|
||||
|
||||
xit("Should rename a published application", () => {
|
||||
// It is not possible to rename a published application
|
||||
const appName = "Cypress Tests"
|
||||
const appRename = "Cypress Renamed"
|
||||
// Publish the app
|
||||
cy.get(".toprightnav")
|
||||
cy.get(".spectrum-Button").contains("Publish").click({force: true})
|
||||
cy.get(".spectrum-Dialog-grid")
|
||||
.within(() => {
|
||||
// Click publish again within the modal
|
||||
cy.get(".spectrum-Button").contains("Publish").click({force: true})
|
||||
})
|
||||
// Rename app, Search for app, Confirm name was changed
|
||||
cy.get(".home-logo").click()
|
||||
renameApp(appName, appRename, true)
|
||||
cy.searchForApplication(appRename)
|
||||
cy.get(".appTable").find(".wrapper").should("have.length", 1)
|
||||
// It is not possible to rename a published application
|
||||
const appName = "Cypress Tests"
|
||||
const appRename = "Cypress Renamed"
|
||||
// Publish the app
|
||||
cy.get(".toprightnav")
|
||||
cy.get(".spectrum-Button").contains("Publish").click({ force: true })
|
||||
cy.get(".spectrum-Dialog-grid")
|
||||
.within(() => {
|
||||
// Click publish again within the modal
|
||||
cy.get(".spectrum-Button").contains("Publish").click({ force: true })
|
||||
})
|
||||
// Rename app, Search for app, Confirm name was changed
|
||||
cy.visit(`${Cypress.config().baseUrl}/builder`)
|
||||
cy.wait(500)
|
||||
renameApp(appName, appRename, true)
|
||||
cy.get(".appTable").find(".wrapper").should("have.length", 1)
|
||||
cy.applicationInAppTable(appRename)
|
||||
})
|
||||
|
||||
it("Should try to rename an application to have no name", () => {
|
||||
const appName = "Cypress Tests"
|
||||
cy.get(".home-logo").click()
|
||||
renameApp(appName, " ", false, true)
|
||||
cy.wait(500)
|
||||
// Close modal and confirm name has not been changed
|
||||
cy.get(".spectrum-Dialog-grid").contains("Cancel").click()
|
||||
cy.reload()
|
||||
cy.wait(1000)
|
||||
cy.searchForApplication(appName)
|
||||
cy.get(".appTable").find(".title").should("have.length", 1)
|
||||
|
||||
const appName = "Cypress Tests"
|
||||
cy.visit(`${Cypress.config().baseUrl}/builder`)
|
||||
cy.wait(500)
|
||||
renameApp(appName, " ", false, true)
|
||||
cy.wait(500)
|
||||
// Close modal and confirm name has not been changed
|
||||
cy.get(".spectrum-Dialog-grid").contains("Cancel").click()
|
||||
cy.reload()
|
||||
cy.wait(1000)
|
||||
cy.applicationInAppTable(appName)
|
||||
})
|
||||
|
||||
xit("Should create two applications with the same name", () => {
|
||||
// It is not possible to have applications with the same name
|
||||
const appName = "Cypress Tests"
|
||||
cy.visit(`${Cypress.config().baseUrl}/builder`)
|
||||
cy.wait(500)
|
||||
cy.get(".spectrum-Button").contains("Create app").click({force: true})
|
||||
cy.contains(/Start from scratch/).click()
|
||||
cy.get(".spectrum-Modal")
|
||||
// It is not possible to have applications with the same name
|
||||
const appName = "Cypress Tests"
|
||||
cy.visit(`${Cypress.config().baseUrl}/builder`)
|
||||
cy.wait(500)
|
||||
cy.get(".spectrum-Button").contains("Create app").click({ force: true })
|
||||
cy.contains(/Start from scratch/).click()
|
||||
cy.get(".spectrum-Modal")
|
||||
.within(() => {
|
||||
cy.get("input").eq(0).type(appName)
|
||||
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("input").eq(0).type(appName)
|
||||
cy.get(".spectrum-ButtonGroup").contains("Create app").click({ force: true })
|
||||
cy.get(".error").should("have.text", "Another app with the same name already exists")
|
||||
})
|
||||
})
|
||||
|
||||
it("should validate application names", () => {
|
||||
// App name must be letters, numbers and spaces only
|
||||
// This test checks numbers and special characters specifically
|
||||
const appName = "Cypress Tests"
|
||||
const numberName = 12345
|
||||
const specialCharName = "£$%^"
|
||||
cy.get(".home-logo").click()
|
||||
renameApp(appName, numberName)
|
||||
cy.reload()
|
||||
cy.wait(1000)
|
||||
cy.searchForApplication(numberName)
|
||||
cy.get(".appTable").find(".title").should("have.length", 1)
|
||||
cy.reload()
|
||||
cy.wait(1000)
|
||||
renameApp(numberName, specialCharName)
|
||||
cy.get(".error").should("have.text", "App name must be letters, numbers and spaces only")
|
||||
// Set app name back to Cypress Tests
|
||||
cy.reload()
|
||||
cy.wait(1000)
|
||||
renameApp(numberName, appName)
|
||||
// App name must be letters, numbers and spaces only
|
||||
// This test checks numbers and special characters specifically
|
||||
const appName = "Cypress Tests"
|
||||
const numberName = 12345
|
||||
const specialCharName = "£$%^"
|
||||
cy.visit(`${Cypress.config().baseUrl}/builder`)
|
||||
cy.wait(500)
|
||||
renameApp(appName, numberName)
|
||||
cy.reload()
|
||||
cy.wait(1000)
|
||||
cy.applicationInAppTable(numberName)
|
||||
cy.reload()
|
||||
cy.wait(1000)
|
||||
renameApp(numberName, specialCharName)
|
||||
cy.get(".error").should("have.text", "App name must be letters, numbers and spaces only")
|
||||
// Set app name back to Cypress Tests
|
||||
cy.reload()
|
||||
cy.wait(1000)
|
||||
renameApp(numberName, appName)
|
||||
})
|
||||
|
||||
const renameApp = (originalName, changedName, published, noName) => {
|
||||
cy.searchForApplication(originalName)
|
||||
cy.request(`${Cypress.config().baseUrl}/api/applications?status=all`)
|
||||
.its("body")
|
||||
.then(val => {
|
||||
if (val.length > 0) {
|
||||
cy.get(".appTable")
|
||||
.within(() => {
|
||||
cy.get(".spectrum-Icon").eq(1).click()
|
||||
})
|
||||
// Check for when an app is published
|
||||
if (published == true){
|
||||
// Should not have Edit as option, will unpublish app
|
||||
cy.should("not.have.value", "Edit")
|
||||
cy.get(".spectrum-Menu").contains("Unpublish").click()
|
||||
cy.get(".spectrum-Dialog-grid").contains("Unpublish app").click()
|
||||
cy.get(".appTable > :nth-child(5) > :nth-child(2) > .spectrum-Icon").click()
|
||||
}
|
||||
cy.contains("Edit").click()
|
||||
cy.get(".spectrum-Modal")
|
||||
.within(() => {
|
||||
if (noName == true){
|
||||
cy.get("input").clear()
|
||||
cy.get(".spectrum-Dialog-grid").click()
|
||||
.contains("App name must be letters, numbers and spaces only")
|
||||
return cy
|
||||
}
|
||||
cy.get("input").clear()
|
||||
cy.get("input").eq(0).type(changedName).should("have.value", changedName).blur()
|
||||
cy.get(".spectrum-ButtonGroup").contains("Save").click({force: true})
|
||||
cy.wait(500)
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
}
|
||||
const renameApp = (originalName, changedName, published, noName) => {
|
||||
cy.searchForApplication(originalName)
|
||||
cy.get(".appTable")
|
||||
.within(() => {
|
||||
cy.get(".spectrum-Icon").eq(1).click()
|
||||
})
|
||||
// Check for when an app is published
|
||||
if (published == true) {
|
||||
// Should not have Edit as option, will unpublish app
|
||||
cy.should("not.have.value", "Edit")
|
||||
cy.get(".spectrum-Menu").contains("Unpublish").click()
|
||||
cy.get(".spectrum-Dialog-grid").contains("Unpublish app").click()
|
||||
cy.get(".appTable > :nth-child(5) > :nth-child(2) > .spectrum-Icon").click()
|
||||
}
|
||||
cy.contains("Edit").click()
|
||||
cy.get(".spectrum-Modal")
|
||||
.within(() => {
|
||||
if (noName == true) {
|
||||
cy.get("input").clear()
|
||||
cy.get(".spectrum-Dialog-grid").click()
|
||||
.contains("App name must be letters, numbers and spaces only")
|
||||
return cy
|
||||
}
|
||||
cy.get("input").clear()
|
||||
cy.get("input").eq(0).type(changedName).should("have.value", changedName).blur()
|
||||
cy.get(".spectrum-ButtonGroup").contains("Save").click({ force: true })
|
||||
cy.wait(500)
|
||||
})
|
||||
}
|
||||
})
|
||||
})
|
||||
|
|
|
@ -35,7 +35,17 @@ Cypress.Commands.add("login", () => {
|
|||
Cypress.Commands.add("createApp", name => {
|
||||
cy.visit(`${Cypress.config().baseUrl}/builder`)
|
||||
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("input").eq(0).type(name).should("have.value", name).blur()
|
||||
cy.get(".spectrum-ButtonGroup").contains("Create app").click()
|
||||
|
@ -51,10 +61,30 @@ Cypress.Commands.add("deleteApp", name => {
|
|||
.its("body")
|
||||
.then(val => {
|
||||
if (val.length > 0) {
|
||||
cy.searchForApplication(name)
|
||||
cy.get(".appTable").within(() => {
|
||||
cy.get(".spectrum-Icon").eq(1).click()
|
||||
})
|
||||
if (Cypress.env("TEST_ENV")) {
|
||||
cy.searchForApplication(name)
|
||||
cy.get(".appTable").within(() => {
|
||||
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 => {
|
||||
if ($menu.text().includes("Unpublish")) {
|
||||
cy.get(".spectrum-Menu").contains("Unpublish").click()
|
||||
|
@ -80,22 +110,18 @@ Cypress.Commands.add("deleteAllApps", () => {
|
|||
.its("body")
|
||||
.then(val => {
|
||||
for (let i = 0; i < val.length; i++) {
|
||||
cy.get(".spectrum-Heading")
|
||||
.eq(1)
|
||||
.then(app => {
|
||||
const name = app.text()
|
||||
cy.get(".title")
|
||||
.children()
|
||||
.within(() => {
|
||||
cy.get(".spectrum-Icon").eq(0).click()
|
||||
})
|
||||
cy.get(".spectrum-Menu").contains("Delete").click()
|
||||
cy.get(".spectrum-Dialog-grid").within(() => {
|
||||
cy.get("input").type(name)
|
||||
cy.get(".spectrum-Button--warning").click()
|
||||
})
|
||||
cy.reload()
|
||||
})
|
||||
const appIdParsed = val[i].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").contains("Delete").click()
|
||||
cy.get(".spectrum-Dialog-grid").within(() => {
|
||||
cy.get("input").type(val[i].name)
|
||||
cy.get(".spectrum-Button--warning").click()
|
||||
})
|
||||
cy.reload()
|
||||
}
|
||||
})
|
||||
})
|
||||
|
@ -190,9 +216,11 @@ Cypress.Commands.add("addRowMultiValue", values => {
|
|||
Cypress.Commands.add("createUser", email => {
|
||||
// quick hacky recorded way to create a user
|
||||
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-Menu-item:nth-child(2) > .spectrum-Menu-itemLabel").click()
|
||||
|
||||
//Onboarding type selector
|
||||
cy.get(
|
||||
":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 => {
|
||||
cy.wait(1000)
|
||||
// Searches for the app
|
||||
cy.get(".filter").then(() => {
|
||||
cy.get(".spectrum-Textfield").within(() => {
|
||||
cy.get("input").eq(0).type(appName)
|
||||
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
|
||||
cy.get(".filter").then(() => {
|
||||
cy.get(".spectrum-Textfield").within(() => {
|
||||
cy.get("input").eq(0).clear()
|
||||
cy.get("input").eq(0).type(appName)
|
||||
})
|
||||
})
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
//Assumes there are no others
|
||||
Cypress.Commands.add("applicationInAppTable", appName => {
|
||||
cy.get(".appTable").within(() => {
|
||||
cy.get(".title").contains(appName).should("exist")
|
||||
})
|
||||
// Confirms app exists after search
|
||||
cy.get(".appTable").contains(appName)
|
||||
})
|
||||
|
||||
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 => {
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@budibase/builder",
|
||||
"version": "1.0.91-alpha.6",
|
||||
"version": "1.0.105-alpha.8",
|
||||
"license": "GPL-3.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
|
@ -65,10 +65,10 @@
|
|||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"@budibase/bbui": "^1.0.91-alpha.6",
|
||||
"@budibase/client": "^1.0.91-alpha.6",
|
||||
"@budibase/frontend-core": "^1.0.91-alpha.6",
|
||||
"@budibase/string-templates": "^1.0.91-alpha.6",
|
||||
"@budibase/bbui": "^1.0.105-alpha.8",
|
||||
"@budibase/client": "^1.0.105-alpha.8",
|
||||
"@budibase/frontend-core": "^1.0.105-alpha.8",
|
||||
"@budibase/string-templates": "^1.0.105-alpha.8",
|
||||
"@sentry/browser": "5.19.1",
|
||||
"@spectrum-css/page": "^3.0.1",
|
||||
"@spectrum-css/vars": "^3.0.1",
|
||||
|
|
|
@ -7,7 +7,11 @@ import {
|
|||
getComponentSettings,
|
||||
} from "./componentUtils"
|
||||
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 {
|
||||
makePropSafe,
|
||||
isJSBinding,
|
||||
|
@ -33,6 +37,7 @@ export const getBindableProperties = (asset, componentId) => {
|
|||
const deviceBindings = getDeviceBindings()
|
||||
const stateBindings = getStateBindings()
|
||||
const selectedRowsBindings = getSelectedRowsBindings(asset)
|
||||
const roleBindings = getRoleBindings()
|
||||
return [
|
||||
...contextBindings,
|
||||
...urlBindings,
|
||||
|
@ -40,6 +45,7 @@ export const getBindableProperties = (asset, componentId) => {
|
|||
...userBindings,
|
||||
...deviceBindings,
|
||||
...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
|
||||
* 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
|
||||
const index = actions?.findIndex(action => action.id === actionId)
|
||||
if (index == null || index === -1) {
|
||||
return []
|
||||
return bindings
|
||||
}
|
||||
const prevActions = actions.slice(0, index)
|
||||
|
||||
// Generate bindings for any steps which provide context
|
||||
let bindings = []
|
||||
prevActions.forEach((action, idx) => {
|
||||
const def = ActionDefinitions.actions.find(
|
||||
x => x.name === action["##eventHandlerType"]
|
||||
|
@ -418,6 +461,7 @@ export const getButtonContextBindings = (actions, actionId) => {
|
|||
})
|
||||
}
|
||||
})
|
||||
|
||||
return bindings
|
||||
}
|
||||
|
||||
|
|
|
@ -3,7 +3,7 @@ import { getAutomationStore } from "./store/automation"
|
|||
import { getThemeStore } from "./store/theme"
|
||||
import { derived, writable } from "svelte/store"
|
||||
import { FrontendTypes, LAYOUT_NAMES } from "../constants"
|
||||
import { findComponent } from "./componentUtils"
|
||||
import { findComponent, findComponentPath } from "./componentUtils"
|
||||
|
||||
export const store = getFrontendStore()
|
||||
export const automationStore = getAutomationStore()
|
||||
|
@ -29,6 +29,16 @@ export const selectedComponent = derived(
|
|||
}
|
||||
)
|
||||
|
||||
export const selectedComponentPath = derived(
|
||||
[store, currentAsset],
|
||||
([$store, $currentAsset]) => {
|
||||
return findComponentPath(
|
||||
$currentAsset?.props,
|
||||
$store.selectedComponentId
|
||||
).map(component => component._id)
|
||||
}
|
||||
)
|
||||
|
||||
export const currentAssetId = derived(store, $store => {
|
||||
return $store.currentFrontEndType === FrontendTypes.SCREEN
|
||||
? $store.selectedScreenId
|
||||
|
|
|
@ -45,6 +45,7 @@ const INITIAL_FRONTEND_STATE = {
|
|||
customThemes: false,
|
||||
devicePreview: false,
|
||||
messagePassing: false,
|
||||
continueIfAction: false,
|
||||
},
|
||||
currentFrontEndType: "none",
|
||||
selectedScreenId: "",
|
||||
|
|
|
@ -2,9 +2,10 @@ import { createLocalStorageStore } from "@budibase/frontend-core"
|
|||
|
||||
export const getThemeStore = () => {
|
||||
const themeElement = document.documentElement
|
||||
|
||||
const initialValue = {
|
||||
theme: "darkest",
|
||||
options: ["lightest", "light", "dark", "darkest"],
|
||||
options: ["lightest", "light", "dark", "darkest", "nord"],
|
||||
}
|
||||
const store = createLocalStorageStore("bb-theme", initialValue)
|
||||
|
||||
|
@ -21,6 +22,7 @@ export const getThemeStore = () => {
|
|||
`spectrum--${option}`,
|
||||
option === state.theme
|
||||
)
|
||||
themeElement.classList.add("spectrum--darkest")
|
||||
})
|
||||
})
|
||||
|
||||
|
|
|
@ -10,12 +10,10 @@
|
|||
<div class="title">
|
||||
<Tabs selected="Automations">
|
||||
<Tab title="Automations">
|
||||
<div class="tab-content-padding">
|
||||
<AutomationList />
|
||||
<Modal bind:this={modal}>
|
||||
<CreateAutomationModal {webhookModal} />
|
||||
</Modal>
|
||||
</div>
|
||||
<AutomationList />
|
||||
<Modal bind:this={modal}>
|
||||
<CreateAutomationModal {webhookModal} />
|
||||
</Modal>
|
||||
</Tab>
|
||||
</Tabs>
|
||||
<div class="add-button" data-cy="new-screen">
|
||||
|
@ -24,9 +22,6 @@
|
|||
</div>
|
||||
|
||||
<style>
|
||||
.tab-content-padding {
|
||||
padding: 0 var(--spacing-xl);
|
||||
}
|
||||
.add-button {
|
||||
position: absolute;
|
||||
top: var(--spacing-l);
|
||||
|
|
|
@ -56,7 +56,7 @@
|
|||
<a
|
||||
slot="footer"
|
||||
target="_blank"
|
||||
href="https://docs.budibase.com/automate/introduction-to-automate"
|
||||
href="https://docs.budibase.com/docs/automation-steps"
|
||||
>
|
||||
<Icon name="InfoOutline" />
|
||||
<span>Learn about automations</span>
|
||||
|
|
|
@ -169,7 +169,7 @@
|
|||
<Select
|
||||
on:change={e => onChange(e, key)}
|
||||
value={inputData[key]}
|
||||
options={Object.keys(table.schema)}
|
||||
options={Object.keys(table?.schema || {})}
|
||||
/>
|
||||
{:else if value.customType === "filters"}
|
||||
<ActionButton on:click={drawer.show}>Define filters</ActionButton>
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<script>
|
||||
import { Input, Select } from "@budibase/bbui"
|
||||
import { Input, Select, Button } from "@budibase/bbui"
|
||||
import { createEventDispatcher } from "svelte"
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
@ -62,9 +62,6 @@
|
|||
</script>
|
||||
|
||||
<div class="root">
|
||||
<div class="add-field">
|
||||
<i class="ri-add-line" on:click={addField} />
|
||||
</div>
|
||||
<div class="spacer" />
|
||||
{#each fieldsArray as field}
|
||||
<div class="field">
|
||||
|
@ -88,6 +85,7 @@
|
|||
/>
|
||||
</div>
|
||||
{/each}
|
||||
<Button quiet secondary icon="Add" on:click={addField}>Add field</Button>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
|
@ -103,52 +101,11 @@
|
|||
|
||||
.field {
|
||||
max-width: 100%;
|
||||
background-color: var(--grey-2);
|
||||
margin-bottom: var(--spacing-m);
|
||||
border-style: solid;
|
||||
border-width: 1px;
|
||||
border-color: var(--grey-4);
|
||||
display: grid;
|
||||
/*grid-template-rows: auto auto;
|
||||
grid-template-columns: auto;*/
|
||||
grid-template-columns: 1fr 1fr auto;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.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;
|
||||
align-items: center;
|
||||
gap: var(--spacing-m);
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -69,7 +69,7 @@
|
|||
<a
|
||||
slot="footer"
|
||||
target="_blank"
|
||||
href="https://docs.budibase.com/automate/steps/triggers"
|
||||
href="https://docs.budibase.com/docs/trigger"
|
||||
>
|
||||
<Icon name="InfoOutline" />
|
||||
<span>Learn about webhooks</span>
|
||||
|
|
|
@ -19,10 +19,22 @@
|
|||
export let value = defaultValue || (meta.type === "boolean" ? false : "")
|
||||
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 =
|
||||
typeof value === "object" ? JSON.stringify(value, null, 2) : value
|
||||
$: type = meta?.type
|
||||
$: label = meta.name ? capitalise(meta.name) : ""
|
||||
|
||||
const timeStamp = resolveTimeStamp(value)
|
||||
const isTimeStamp = timeStamp ? true : false
|
||||
</script>
|
||||
|
||||
{#if type === "options"}
|
||||
|
@ -34,7 +46,7 @@
|
|||
sort
|
||||
/>
|
||||
{:else if type === "datetime"}
|
||||
<DatePicker {label} bind:value />
|
||||
<DatePicker {label} timeOnly={isTimeStamp} bind:value />
|
||||
{:else if type === "attachment"}
|
||||
<Dropzone {label} bind:value />
|
||||
{:else if type === "boolean"}
|
||||
|
|
|
@ -12,7 +12,7 @@
|
|||
Modal,
|
||||
notifications,
|
||||
} from "@budibase/bbui"
|
||||
import { createEventDispatcher } from "svelte"
|
||||
import { createEventDispatcher, onMount } from "svelte"
|
||||
import { cloneDeep } from "lodash/fp"
|
||||
import { tables } from "stores/backend"
|
||||
import { TableNames, UNEDITABLE_USER_FIELDS } from "constants"
|
||||
|
@ -321,6 +321,12 @@
|
|||
}
|
||||
return newError
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
if (primaryDisplay) {
|
||||
field.constraints.presence = { allowEmpty: false }
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<ModalContent
|
||||
|
|
|
@ -22,10 +22,11 @@
|
|||
const selected = $datasources.selected === datasource._id
|
||||
const open = openDataSources.includes(datasource._id)
|
||||
const containsSelected = containsActiveEntity(datasource)
|
||||
const onlySource = $datasources.list.length === 1
|
||||
return {
|
||||
...datasource,
|
||||
selected,
|
||||
open: selected || open || containsSelected,
|
||||
open: selected || open || containsSelected || onlySource,
|
||||
}
|
||||
})
|
||||
: []
|
||||
|
|
|
@ -15,6 +15,7 @@
|
|||
import ArrayRenderer from "components/common/renderers/ArrayRenderer.svelte"
|
||||
import ConfirmDialog from "components/common/ConfirmDialog.svelte"
|
||||
import { goto } from "@roxi/routify"
|
||||
import GoogleButton from "../_components/GoogleButton.svelte"
|
||||
|
||||
export let datasource
|
||||
export let save
|
||||
|
@ -160,6 +161,11 @@
|
|||
Fetch tables
|
||||
</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>
|
||||
<Body>
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<script>
|
||||
import { Icon } from "@budibase/bbui"
|
||||
import { createEventDispatcher } from "svelte"
|
||||
import { createEventDispatcher, getContext } from "svelte"
|
||||
|
||||
export let icon
|
||||
export let withArrow = false
|
||||
|
@ -14,29 +14,46 @@
|
|||
export let iconText
|
||||
export let iconColor
|
||||
|
||||
const scrollApi = getContext("scroll")
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
function onIconClick(event) {
|
||||
event.stopPropagation()
|
||||
let contentRef
|
||||
$: selected && contentRef && scrollToView()
|
||||
|
||||
const onClick = () => {
|
||||
scrollToView()
|
||||
dispatch("click")
|
||||
}
|
||||
|
||||
const onIconClick = e => {
|
||||
e.stopPropagation()
|
||||
dispatch("iconClick")
|
||||
}
|
||||
|
||||
const scrollToView = () => {
|
||||
if (!scrollApi || !contentRef) {
|
||||
return
|
||||
}
|
||||
const bounds = contentRef.getBoundingClientRect()
|
||||
scrollApi.scrollTo(bounds)
|
||||
}
|
||||
</script>
|
||||
|
||||
<div
|
||||
class="nav-item"
|
||||
class:border
|
||||
class:selected
|
||||
style={`padding-left: ${indentLevel * 14}px`}
|
||||
style={`padding-left: ${20 + indentLevel * 14}px`}
|
||||
{draggable}
|
||||
on:dragend
|
||||
on:dragstart
|
||||
on:dragover
|
||||
on:drop
|
||||
on:click
|
||||
on:click={onClick}
|
||||
ondragover="return false"
|
||||
ondragenter="return false"
|
||||
>
|
||||
<div class="content">
|
||||
<div class="nav-item-content" bind:this={contentRef}>
|
||||
{#if withArrow}
|
||||
<div class:opened class="icon arrow" on:click={onIconClick}>
|
||||
<Icon size="S" name="ChevronRight" />
|
||||
|
@ -64,11 +81,16 @@
|
|||
|
||||
<style>
|
||||
.nav-item {
|
||||
border-radius: var(--border-radius-s);
|
||||
cursor: pointer;
|
||||
color: var(--grey-7);
|
||||
transition: background-color
|
||||
var(--spectrum-global-animation-duration-100, 130ms) ease-in-out;
|
||||
padding: 0 var(--spacing-m) 0 var(--spacing-xl);
|
||||
height: 32px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: flex-start;
|
||||
}
|
||||
.nav-item.selected {
|
||||
background-color: var(--grey-2);
|
||||
|
@ -81,14 +103,14 @@
|
|||
visibility: visible;
|
||||
}
|
||||
|
||||
.content {
|
||||
padding: 0 var(--spacing-s);
|
||||
height: 32px;
|
||||
.nav-item-content {
|
||||
flex: 1 1 auto;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
gap: var(--spacing-xs);
|
||||
width: max-content;
|
||||
}
|
||||
|
||||
.icon {
|
||||
|
@ -111,12 +133,13 @@
|
|||
}
|
||||
|
||||
.text {
|
||||
flex: 1 1 auto;
|
||||
font-weight: 600;
|
||||
font-size: var(--spectrum-global-dimension-font-size-75);
|
||||
white-space: nowrap;
|
||||
max-width: 160px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.actions {
|
||||
|
@ -125,9 +148,9 @@
|
|||
height: 20px;
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
display: grid;
|
||||
margin-left: var(--spacing-s);
|
||||
place-items: center;
|
||||
}
|
||||
|
||||
.iconText {
|
||||
|
|
|
@ -0,0 +1,125 @@
|
|||
<script>
|
||||
export let backgroundColour
|
||||
export let imageSrc
|
||||
export let name
|
||||
export let icon
|
||||
export let overlayEnabled = true
|
||||
|
||||
let imageError = false
|
||||
let imageLoaded = false
|
||||
|
||||
const imageRenderError = () => {
|
||||
imageError = true
|
||||
}
|
||||
|
||||
const imageLoadSuccess = () => {
|
||||
imageLoaded = true
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="template-card" style="background-color:{backgroundColour};">
|
||||
<div class="template-thumbnail card-body">
|
||||
<img
|
||||
alt={name}
|
||||
src={imageSrc}
|
||||
on:error={imageRenderError}
|
||||
on:load={imageLoadSuccess}
|
||||
class={`${imageLoaded ? "loaded" : ""}`}
|
||||
/>
|
||||
<div style={`display:${imageError ? "block" : "none"}`}>
|
||||
<svg
|
||||
width="26px"
|
||||
height="26px"
|
||||
class="spectrum-Icon"
|
||||
style="color: white"
|
||||
focusable="false"
|
||||
>
|
||||
<use xlink:href="#spectrum-icon-18-{icon}" />
|
||||
</svg>
|
||||
</div>
|
||||
<div class={overlayEnabled ? "template-thumbnail-action-overlay" : ""}>
|
||||
<slot />
|
||||
</div>
|
||||
</div>
|
||||
<div class="template-thumbnail-text">
|
||||
<div>{name}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.template-thumbnail {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.template-card:hover .template-thumbnail-action-overlay {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.template-thumbnail-action-overlay {
|
||||
position: absolute;
|
||||
top: 0px;
|
||||
left: 0px;
|
||||
width: 100%;
|
||||
height: 70%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background-color: rgba(0, 0, 0, 0.7);
|
||||
opacity: 0;
|
||||
transition: opacity var(--spectrum-global-animation-duration-100) ease;
|
||||
border-top-right-radius: inherit;
|
||||
border-top-left-radius: inherit;
|
||||
}
|
||||
|
||||
.template-thumbnail-text {
|
||||
position: absolute;
|
||||
bottom: 0px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 30%;
|
||||
width: 100%;
|
||||
color: var(
|
||||
--spectrum-heading-xs-text-color,
|
||||
var(--spectrum-alias-heading-text-color)
|
||||
);
|
||||
background-color: var(--spectrum-global-color-gray-50);
|
||||
}
|
||||
|
||||
.template-thumbnail-text > div {
|
||||
padding-left: 1rem;
|
||||
padding-right: 1rem;
|
||||
}
|
||||
|
||||
.template-card {
|
||||
position: relative;
|
||||
display: flex;
|
||||
border-radius: var(--border-radius-s);
|
||||
border: 1px solid var(--spectrum-global-color-gray-300);
|
||||
overflow: hidden;
|
||||
min-height: 200px;
|
||||
}
|
||||
|
||||
.template-card > * {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.template-card img.loaded {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.template-card img {
|
||||
display: none;
|
||||
max-width: 100%;
|
||||
border-radius: var(--border-radius-s) 0px var(--border-radius-s) 0px;
|
||||
}
|
||||
|
||||
.template-card:hover {
|
||||
background: var(--spectrum-alias-background-color-tertiary);
|
||||
}
|
||||
|
||||
.card-body {
|
||||
padding-left: 1rem;
|
||||
padding-top: 1rem;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,152 @@
|
|||
<script>
|
||||
import {
|
||||
Layout,
|
||||
Detail,
|
||||
Heading,
|
||||
Button,
|
||||
Modal,
|
||||
ActionGroup,
|
||||
ActionButton,
|
||||
} from "@budibase/bbui"
|
||||
import TemplateCard from "components/common/TemplateCard.svelte"
|
||||
import CreateAppModal from "components/start/CreateAppModal.svelte"
|
||||
|
||||
export let templates
|
||||
|
||||
let selectedTemplateCategory
|
||||
let creationModal
|
||||
let template
|
||||
|
||||
const groupTemplatesByCategory = (templates, categoryFilter) => {
|
||||
let grouped = templates.reduce((acc, template) => {
|
||||
if (
|
||||
typeof categoryFilter === "string" &&
|
||||
[categoryFilter].indexOf(template.category) < 0
|
||||
) {
|
||||
return acc
|
||||
}
|
||||
|
||||
acc[template.category] = !acc[template.category]
|
||||
? []
|
||||
: acc[template.category]
|
||||
acc[template.category].push(template)
|
||||
|
||||
return acc
|
||||
}, {})
|
||||
return grouped
|
||||
}
|
||||
|
||||
$: filteredTemplates = groupTemplatesByCategory(
|
||||
templates,
|
||||
selectedTemplateCategory
|
||||
)
|
||||
|
||||
$: filteredTemplateCategories = filteredTemplates
|
||||
? Object.keys(filteredTemplates).sort()
|
||||
: []
|
||||
|
||||
$: templateCategories = templates
|
||||
? Object.keys(groupTemplatesByCategory(templates)).sort()
|
||||
: []
|
||||
|
||||
const stopAppCreation = () => {
|
||||
template = null
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="template-header">
|
||||
<Layout noPadding gap="S">
|
||||
<Heading size="S">Templates</Heading>
|
||||
<div class="template-category-filters spectrum-ActionGroup">
|
||||
<ActionGroup>
|
||||
<ActionButton
|
||||
selected={!selectedTemplateCategory}
|
||||
on:click={() => {
|
||||
selectedTemplateCategory = null
|
||||
}}
|
||||
>
|
||||
All
|
||||
</ActionButton>
|
||||
{#each templateCategories as templateCategoryKey}
|
||||
<ActionButton
|
||||
dataCy={templateCategoryKey}
|
||||
selected={templateCategoryKey == selectedTemplateCategory}
|
||||
on:click={() => {
|
||||
selectedTemplateCategory = templateCategoryKey
|
||||
}}
|
||||
>
|
||||
{templateCategoryKey}
|
||||
</ActionButton>
|
||||
{/each}
|
||||
</ActionGroup>
|
||||
</div>
|
||||
</Layout>
|
||||
</div>
|
||||
|
||||
<div class="template-categories">
|
||||
<Layout gap="XL" noPadding>
|
||||
{#each filteredTemplateCategories as templateCategoryKey}
|
||||
<div class="template-category" data-cy={templateCategoryKey}>
|
||||
<Detail size="M">{templateCategoryKey}</Detail>
|
||||
<div class="template-grid">
|
||||
{#each filteredTemplates[templateCategoryKey] as templateEntry}
|
||||
<TemplateCard
|
||||
name={templateEntry.name}
|
||||
imageSrc={templateEntry.image}
|
||||
backgroundColour={templateEntry.background}
|
||||
icon={templateEntry.icon}
|
||||
>
|
||||
<Button
|
||||
cta
|
||||
on:click={() => {
|
||||
template = templateEntry
|
||||
creationModal.show()
|
||||
}}
|
||||
>
|
||||
Use template
|
||||
</Button>
|
||||
<a
|
||||
href={templateEntry.url}
|
||||
target="_blank"
|
||||
class="overlay-preview-link spectrum-Button spectrum-Button--sizeM spectrum-Button--secondary"
|
||||
on:click|stopPropagation
|
||||
>
|
||||
Details
|
||||
</a>
|
||||
</TemplateCard>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
</Layout>
|
||||
</div>
|
||||
|
||||
<Modal
|
||||
bind:this={creationModal}
|
||||
padding={false}
|
||||
width="600px"
|
||||
on:hide={stopAppCreation}
|
||||
>
|
||||
<CreateAppModal {template} />
|
||||
</Modal>
|
||||
|
||||
<style>
|
||||
.template-grid {
|
||||
padding-top: 10px;
|
||||
display: grid;
|
||||
grid-gap: var(--spacing-xl);
|
||||
grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
|
||||
}
|
||||
|
||||
a:hover.spectrum-Button.spectrum-Button--secondary.overlay-preview-link {
|
||||
background-color: #c8c8c8;
|
||||
border-color: #c8c8c8;
|
||||
color: #505050;
|
||||
}
|
||||
|
||||
a.spectrum-Button--secondary.overlay-preview-link {
|
||||
margin-top: 20px;
|
||||
border-color: #c8c8c8;
|
||||
color: #c8c8c8;
|
||||
}
|
||||
</style>
|
|
@ -51,7 +51,7 @@
|
|||
}
|
||||
</script>
|
||||
|
||||
<div class="control">
|
||||
<div class="control" class:disabled>
|
||||
<Combobox
|
||||
{label}
|
||||
{disabled}
|
||||
|
@ -121,4 +121,8 @@
|
|||
background-color: var(--spectrum-global-color-gray-50);
|
||||
border-color: var(--spectrum-alias-border-color-hover);
|
||||
}
|
||||
|
||||
.control:not(.disabled) :global(.spectrum-Textfield-input) {
|
||||
padding-right: 40px;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -36,7 +36,7 @@
|
|||
}
|
||||
</script>
|
||||
|
||||
<div class="control">
|
||||
<div class="control" class:disabled>
|
||||
<Input
|
||||
{label}
|
||||
{disabled}
|
||||
|
@ -103,4 +103,8 @@
|
|||
background-color: var(--spectrum-global-color-gray-50);
|
||||
border-color: var(--spectrum-alias-border-color-hover);
|
||||
}
|
||||
|
||||
.control:not(.disabled) :global(.spectrum-Textfield-input) {
|
||||
padding-right: 40px;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -12,8 +12,8 @@
|
|||
const enrichBindings = bindings => {
|
||||
return bindings?.map(binding => ({
|
||||
...binding,
|
||||
readableBinding: binding.label || binding.readableBinding,
|
||||
runtimeBinding: binding.path || binding.runtimeBinding,
|
||||
readableBinding: binding.readableBinding || binding.label,
|
||||
runtimeBinding: binding.runtimeBinding || binding.path,
|
||||
}))
|
||||
}
|
||||
</script>
|
||||
|
|
|
@ -40,7 +40,7 @@
|
|||
<a
|
||||
slot="footer"
|
||||
target="_blank"
|
||||
href="https://docs.budibase.com/automate/steps/triggers"
|
||||
href="https://docs.budibase.com/docs/trigger"
|
||||
>
|
||||
<i class="ri-information-line" />
|
||||
<span>Learn about webhooks</span>
|
||||
|
|
|
@ -68,6 +68,7 @@
|
|||
customTheme: $store.customTheme,
|
||||
previewDevice: $store.previewDevice,
|
||||
messagePassing: $store.clientFeatures.messagePassing,
|
||||
isBudibaseEvent: true
|
||||
}
|
||||
$: json = JSON.stringify(previewData)
|
||||
|
||||
|
|
|
@ -82,7 +82,8 @@
|
|||
"link",
|
||||
"icon",
|
||||
"embed",
|
||||
"markdownviewer"
|
||||
"markdownviewer",
|
||||
"embeddedmap"
|
||||
]
|
||||
}
|
||||
]
|
||||
|
|
|
@ -52,7 +52,7 @@ export default `
|
|||
console.error("Client received invalid JSON")
|
||||
// Ignore
|
||||
}
|
||||
if (!parsed) {
|
||||
if (!parsed || !parsed.isBudibaseEvent) {
|
||||
return
|
||||
}
|
||||
|
||||
|
|
|
@ -138,3 +138,10 @@
|
|||
onOk={deleteComponent}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
.icon {
|
||||
display: grid;
|
||||
place-items: center;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
import NavItem from "components/common/NavItem.svelte"
|
||||
import { capitalise } from "helpers"
|
||||
import { notifications } from "@budibase/bbui"
|
||||
import { selectedComponentPath } from "builderStore"
|
||||
|
||||
export let components = []
|
||||
export let currentComponent
|
||||
|
@ -71,10 +72,20 @@
|
|||
notifications.error("Error saving component")
|
||||
}
|
||||
}
|
||||
|
||||
const isOpen = (component, selectedComponentPath, closedNodes) => {
|
||||
if (!component?._children?.length) {
|
||||
return false
|
||||
}
|
||||
if (selectedComponentPath.includes(component._id)) {
|
||||
return true
|
||||
}
|
||||
return !closedNodes[component._id]
|
||||
}
|
||||
</script>
|
||||
|
||||
<ul>
|
||||
{#each components as component, index (component._id)}
|
||||
{#each components || [] as component, index (component._id)}
|
||||
<li on:click|stopPropagation={() => selectComponent(component)}>
|
||||
{#if $dragDropStore?.targetComponent === component && $dragDropStore.dropPosition === DropPosition.ABOVE}
|
||||
<div
|
||||
|
@ -97,12 +108,12 @@
|
|||
withArrow
|
||||
indentLevel={level + 1}
|
||||
selected={$store.selectedComponentId === component._id}
|
||||
opened={!closedNodes[component._id] && component?._children?.length}
|
||||
opened={isOpen(component, $selectedComponentPath, closedNodes)}
|
||||
>
|
||||
<ComponentDropdownMenu {component} />
|
||||
</NavItem>
|
||||
|
||||
{#if component._children && !closedNodes[component._id]}
|
||||
{#if isOpen(component, $selectedComponentPath, closedNodes)}
|
||||
<svelte:self
|
||||
components={component._children}
|
||||
{currentComponent}
|
||||
|
@ -133,6 +144,10 @@
|
|||
padding-left: 0;
|
||||
margin: 0;
|
||||
}
|
||||
ul,
|
||||
li {
|
||||
min-width: max-content;
|
||||
}
|
||||
|
||||
.drop-item {
|
||||
border-radius: var(--border-radius-m);
|
||||
|
|
|
@ -65,3 +65,10 @@
|
|||
<Input thin type="text" label="Name" bind:value={name} />
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
|
||||
<style>
|
||||
.icon {
|
||||
display: grid;
|
||||
place-items: center;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -75,4 +75,8 @@
|
|||
align-items: flex-start;
|
||||
padding-left: var(--spacing-xl);
|
||||
}
|
||||
.icon {
|
||||
display: grid;
|
||||
place-items: center;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -29,6 +29,7 @@
|
|||
export let border
|
||||
|
||||
let routeManuallyOpened = false
|
||||
|
||||
$: selectedScreen = $currentAsset
|
||||
$: allScreens = getAllScreens(route)
|
||||
$: filteredScreens = getFilteredScreens(allScreens, $screenSearchString)
|
||||
|
@ -83,7 +84,8 @@
|
|||
<NavItem
|
||||
icon="WebPage"
|
||||
indentLevel={indent || 1}
|
||||
selected={$store.selectedScreenId === screen.id}
|
||||
selected={$store.selectedScreenId === screen.id &&
|
||||
$store.currentView === "detail"}
|
||||
opened={$store.selectedScreenId === screen.id}
|
||||
text={ROUTE_NAME_MAP[screen.route]?.[screen.role] || screen.route}
|
||||
withArrow={route.subpaths}
|
||||
|
|
|
@ -103,3 +103,10 @@
|
|||
confirmText="Duplicate"
|
||||
/>
|
||||
</Modal>
|
||||
|
||||
<style>
|
||||
.icon {
|
||||
display: grid;
|
||||
place-items: center;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -55,11 +55,10 @@
|
|||
}
|
||||
</script>
|
||||
|
||||
<div class="root">
|
||||
<div class="root" class:has-screens={!!paths?.length}>
|
||||
{#each paths as path, idx (path)}
|
||||
<PathTree border={idx > 0} {path} route={routes[path]} />
|
||||
{/each}
|
||||
|
||||
{#if !paths.length}
|
||||
<div class="empty">
|
||||
There aren't any screens configured with this access role.
|
||||
|
@ -68,9 +67,12 @@
|
|||
</div>
|
||||
|
||||
<style>
|
||||
.root.has-screens {
|
||||
min-width: max-content;
|
||||
}
|
||||
div.empty {
|
||||
font-size: var(--font-size-xs);
|
||||
font-size: var(--font-size-s);
|
||||
color: var(--grey-5);
|
||||
padding-top: var(--spacing-xs);
|
||||
padding: var(--spacing-xs) var(--spacing-xl);
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<script>
|
||||
import { onMount } from "svelte"
|
||||
import { onMount, setContext } from "svelte"
|
||||
import { goto, params } from "@roxi/routify"
|
||||
import {
|
||||
store,
|
||||
|
@ -18,11 +18,63 @@
|
|||
Search,
|
||||
Tabs,
|
||||
Tab,
|
||||
Layout as BBUILayout,
|
||||
notifications,
|
||||
} from "@budibase/bbui"
|
||||
|
||||
export let showModal
|
||||
|
||||
let scrollRef
|
||||
|
||||
const scrollTo = bounds => {
|
||||
if (!bounds) {
|
||||
return
|
||||
}
|
||||
|
||||
const sidebarWidth = 259
|
||||
const navItemHeight = 32
|
||||
const { scrollLeft, scrollTop, offsetHeight } = scrollRef
|
||||
|
||||
let scrollBounds = scrollRef.getBoundingClientRect()
|
||||
let newOffsets = {}
|
||||
|
||||
// Calculate left offset
|
||||
const offsetX = bounds.left + bounds.width + scrollLeft + 20
|
||||
if (offsetX > sidebarWidth) {
|
||||
newOffsets.left = offsetX - sidebarWidth
|
||||
} else {
|
||||
newOffsets.left = 0
|
||||
}
|
||||
if (newOffsets.left === scrollLeft) {
|
||||
delete newOffsets.left
|
||||
}
|
||||
|
||||
// Calculate top offset
|
||||
const offsetY = bounds.top - scrollBounds?.top + scrollTop
|
||||
if (offsetY > scrollTop + offsetHeight - 2 * navItemHeight) {
|
||||
newOffsets.top = offsetY - offsetHeight + 2 * navItemHeight
|
||||
} else if (offsetY < scrollTop + navItemHeight) {
|
||||
newOffsets.top = offsetY - navItemHeight
|
||||
} else {
|
||||
delete newOffsets.top
|
||||
}
|
||||
|
||||
// Skip if offset is unchanged
|
||||
if (newOffsets.left == null && newOffsets.top == null) {
|
||||
return
|
||||
}
|
||||
|
||||
// Smoothly scroll to the offset
|
||||
scrollRef.scroll({
|
||||
...newOffsets,
|
||||
behavior: "smooth",
|
||||
})
|
||||
}
|
||||
|
||||
setContext("scroll", {
|
||||
scrollTo,
|
||||
})
|
||||
|
||||
const tabs = [
|
||||
{
|
||||
title: "Screens",
|
||||
|
@ -79,7 +131,7 @@
|
|||
<Tabs {selected} on:select={navigate}>
|
||||
<Tab title="Screens">
|
||||
<div class="tab-content-padding">
|
||||
<div class="role-select">
|
||||
<BBUILayout noPadding gap="XS">
|
||||
<Select
|
||||
on:change={updateAccessRole}
|
||||
value={$selectedAccessRole}
|
||||
|
@ -93,17 +145,24 @@
|
|||
label="Search Screens"
|
||||
bind:value={$screenSearchString}
|
||||
/>
|
||||
</div>
|
||||
<div class="nav-items-container">
|
||||
</BBUILayout>
|
||||
<div class="nav-items-container" bind:this={scrollRef}>
|
||||
<ComponentNavigationTree />
|
||||
</div>
|
||||
</div>
|
||||
</Tab>
|
||||
<Tab title="Layouts">
|
||||
<div class="tab-content-padding">
|
||||
{#each $store.layouts as layout, idx (layout._id)}
|
||||
<Layout {layout} border={idx > 0} />
|
||||
{/each}
|
||||
<div
|
||||
class="nav-items-container nav-items-container--layouts"
|
||||
bind:this={scrollRef}
|
||||
>
|
||||
<div class="layouts-container">
|
||||
{#each $store.layouts as layout, idx (layout._id)}
|
||||
<Layout {layout} border={idx > 0} />
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
<Modal bind:this={newLayoutModal}>
|
||||
<NewLayoutModal />
|
||||
</Modal>
|
||||
|
@ -126,23 +185,45 @@
|
|||
justify-content: flex-start;
|
||||
align-items: stretch;
|
||||
position: relative;
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
.title :global(.spectrum-Tabs-content),
|
||||
.title :global(.spectrum-Tabs-content > div),
|
||||
.title :global(.spectrum-Tabs-content > div > div) {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.add-button {
|
||||
position: absolute;
|
||||
top: var(--spacing-l);
|
||||
right: var(--spacing-xl);
|
||||
}
|
||||
|
||||
.role-select {
|
||||
.tab-content-padding {
|
||||
padding: 0 var(--spacing-xl);
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-start;
|
||||
align-items: stretch;
|
||||
margin-bottom: var(--spacing-m);
|
||||
gap: var(--spacing-m);
|
||||
gap: var(--spacing-xl);
|
||||
}
|
||||
|
||||
.tab-content-padding {
|
||||
padding: 0 var(--spacing-xl);
|
||||
.nav-items-container {
|
||||
border-top: var(--border-light);
|
||||
margin: 0 calc(-1 * var(--spacing-xl));
|
||||
padding: var(--spacing-m) 0;
|
||||
flex: 1 1 auto;
|
||||
overflow: auto;
|
||||
height: 0;
|
||||
position: relative;
|
||||
}
|
||||
.nav-items-container--layouts {
|
||||
border-top: none;
|
||||
margin-top: calc(-1 * var(--spectrum-global-dimension-static-size-150));
|
||||
}
|
||||
|
||||
.layouts-container {
|
||||
min-width: max-content;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -12,11 +12,13 @@
|
|||
import { getAvailableActions } from "./index"
|
||||
import { generate } from "shortid"
|
||||
import { getButtonContextBindings } from "builderStore/dataBinding"
|
||||
import { currentAsset, store } from "builderStore"
|
||||
|
||||
const flipDurationMs = 150
|
||||
const EVENT_TYPE_KEY = "##eventHandlerType"
|
||||
const actionTypes = getAvailableActions()
|
||||
|
||||
export let key
|
||||
export let actions
|
||||
export let bindings = []
|
||||
|
||||
|
@ -24,6 +26,9 @@
|
|||
|
||||
// These are ephemeral bindings which only exist while executing actions
|
||||
$: buttonContextBindings = getButtonContextBindings(
|
||||
$currentAsset,
|
||||
$store.selectedComponentId,
|
||||
key,
|
||||
actions,
|
||||
selectedAction?.id
|
||||
)
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
export let key
|
||||
export let value = []
|
||||
export let name
|
||||
export let bindings
|
||||
|
@ -81,5 +82,6 @@
|
|||
bind:actions={tmpValue}
|
||||
eventType={name}
|
||||
{bindings}
|
||||
{key}
|
||||
/>
|
||||
</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>
|
|
@ -13,3 +13,4 @@ export { default as RefreshDataProvider } from "./RefreshDataProvider.svelte"
|
|||
export { default as DuplicateRow } from "./DuplicateRow.svelte"
|
||||
export { default as S3Upload } from "./S3Upload.svelte"
|
||||
export { default as ExportData } from "./ExportData.svelte"
|
||||
export { default as ContinueIf } from "./ContinueIf.svelte"
|
||||
|
|
|
@ -84,6 +84,11 @@
|
|||
{
|
||||
"name": "Export Data",
|
||||
"component": "ExportData"
|
||||
},
|
||||
{
|
||||
"name": "Continue if / Stop if",
|
||||
"component": "ContinueIf",
|
||||
"dependsOnFeature": "continueIfAction"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
|
@ -79,6 +79,7 @@
|
|||
bindings={allBindings}
|
||||
name={key}
|
||||
text={label}
|
||||
{key}
|
||||
{type}
|
||||
{...props}
|
||||
/>
|
||||
|
|
|
@ -1,171 +0,0 @@
|
|||
<script>
|
||||
import analytics from "analytics"
|
||||
import { createEventDispatcher } from "svelte"
|
||||
import { fade, fly } from "svelte/transition"
|
||||
import {
|
||||
ActionButton,
|
||||
ClearButton,
|
||||
RadioGroup,
|
||||
TextArea,
|
||||
ButtonGroup,
|
||||
Button,
|
||||
Heading,
|
||||
Detail,
|
||||
Divider,
|
||||
Layout,
|
||||
notifications,
|
||||
} from "@budibase/bbui"
|
||||
import { auth } from "stores/portal"
|
||||
|
||||
let step = 0
|
||||
let ratings = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
|
||||
let options = [
|
||||
"Importing / managing data",
|
||||
"Designing",
|
||||
"Automations",
|
||||
"Managing users / groups",
|
||||
"Deployment / hosting",
|
||||
"Documentation",
|
||||
]
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
// Data to send off
|
||||
let rating
|
||||
let improvements = ""
|
||||
let comment = ""
|
||||
|
||||
function selectNumber(n) {
|
||||
rating = n
|
||||
step = 1
|
||||
}
|
||||
|
||||
function submitFeedback() {
|
||||
analytics.submitFeedback({
|
||||
rating,
|
||||
improvements,
|
||||
comment,
|
||||
})
|
||||
try {
|
||||
auth.updateSelf({
|
||||
flags: {
|
||||
feedbackSubmitted: true,
|
||||
},
|
||||
})
|
||||
} catch (error) {
|
||||
notifications.error("Error updating user")
|
||||
}
|
||||
dispatch("complete")
|
||||
}
|
||||
|
||||
function cancelFeedback() {
|
||||
try {
|
||||
auth.updateSelf({
|
||||
flags: {
|
||||
feedbackSubmitted: true,
|
||||
},
|
||||
})
|
||||
} catch (error) {
|
||||
notifications.error("Error updating user")
|
||||
}
|
||||
dispatch("complete")
|
||||
}
|
||||
</script>
|
||||
|
||||
<div
|
||||
class="position"
|
||||
in:fade={{ duration: 200 }}
|
||||
out:fade|local={{ duration: 200 }}
|
||||
>
|
||||
<div
|
||||
class="feedback-frame"
|
||||
in:fly={{ y: 30, duration: 200 }}
|
||||
out:fly|local={{ y: 30, duration: 200 }}
|
||||
>
|
||||
<div class="close">
|
||||
<ClearButton on:click={cancelFeedback} />
|
||||
</div>
|
||||
<Layout gap="XS">
|
||||
{#if step === 0}
|
||||
<Heading size="XS"
|
||||
>How likely are you to recommend Budibase to a colleague?</Heading
|
||||
>
|
||||
<Divider />
|
||||
<div class="ratings">
|
||||
{#each ratings as number}
|
||||
<ActionButton
|
||||
size="L"
|
||||
emphasized
|
||||
selected={number === rating}
|
||||
on:click={() => selectNumber(number)}
|
||||
>
|
||||
{number}
|
||||
</ActionButton>
|
||||
{/each}
|
||||
</div>
|
||||
<div class="footer">
|
||||
<Detail size="S">NOT LIKELY</Detail>
|
||||
<Detail size="S">EXTREMELY LIKELY</Detail>
|
||||
</div>
|
||||
{:else if step === 1}
|
||||
<Heading size="XS">What could be improved most in Budibase?</Heading>
|
||||
<Divider />
|
||||
<RadioGroup bind:value={improvements} {options} />
|
||||
<div class="footer">
|
||||
<Detail size="S">STEP 2 OF 3</Detail>
|
||||
<ButtonGroup>
|
||||
<Button secondary on:click={() => (step -= 1)}>Previous</Button>
|
||||
<Button primary on:click={() => (step += 1)}>Next</Button>
|
||||
</ButtonGroup>
|
||||
</div>
|
||||
{:else}
|
||||
<Heading size="XS">How can we improve your experience?</Heading>
|
||||
<Divider />
|
||||
<TextArea bind:value={comment} placeholder="Add comments" />
|
||||
<div class="footer">
|
||||
<Detail size="S">STEP 3 OF 3</Detail>
|
||||
<ButtonGroup>
|
||||
<Button secondary on:click={() => (step -= 1)}>Previous</Button>
|
||||
<Button cta on:click={submitFeedback}>Complete</Button>
|
||||
</ButtonGroup>
|
||||
</div>
|
||||
{/if}
|
||||
</Layout>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.feedback-frame :global(textarea) {
|
||||
min-height: 180px !important;
|
||||
}
|
||||
|
||||
.position {
|
||||
position: absolute;
|
||||
right: var(--spacing-l);
|
||||
bottom: calc(5 * var(--spacing-xl));
|
||||
}
|
||||
.feedback-frame {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
min-width: 510px;
|
||||
background: var(--background);
|
||||
border-radius: var(--spectrum-global-dimension-size-50);
|
||||
border: 2px solid var(--spectrum-global-color-blue-400);
|
||||
padding: var(--spacing-xl);
|
||||
}
|
||||
.ratings {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
.close {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
}
|
||||
.footer {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
</style>
|
|
@ -33,8 +33,7 @@
|
|||
let parameters
|
||||
let data = []
|
||||
let saveId
|
||||
const transformerDocs =
|
||||
"https://docs.budibase.com/building-apps/data/transformers"
|
||||
const transformerDocs = "https://docs.budibase.com/docs/transformers"
|
||||
|
||||
$: datasource = $datasources.list.find(ds => ds._id === query.datasourceId)
|
||||
$: query.schema = fieldsToSchema(fields)
|
||||
|
|
|
@ -61,7 +61,7 @@
|
|||
{#if app.deployed}Published{:else}Unpublished{/if}
|
||||
</StatusLight>
|
||||
</div>
|
||||
<div>
|
||||
<div data-cy={`row_actions_${app.appId}`}>
|
||||
<Button
|
||||
size="S"
|
||||
disabled={app.lockedOther}
|
||||
|
|
|
@ -9,17 +9,57 @@
|
|||
import { goto } from "@roxi/routify"
|
||||
import { createValidationStore } from "helpers/validation/yup"
|
||||
import * as appValidation from "helpers/validation/yup/app"
|
||||
import TemplateCard from "components/common/TemplateCard.svelte"
|
||||
|
||||
export let template
|
||||
|
||||
let creating = false
|
||||
|
||||
const values = writable({ name: "", url: null })
|
||||
const validation = createValidationStore()
|
||||
$: validation.check($values)
|
||||
|
||||
onMount(async () => {
|
||||
$values.name = resolveAppName(template, $values.name)
|
||||
nameToUrl($values.name)
|
||||
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 applications = svelteGet(apps)
|
||||
appValidation.name(validation, { apps: applications })
|
||||
|
@ -30,6 +70,8 @@
|
|||
}
|
||||
|
||||
async function createNewApp() {
|
||||
creating = true
|
||||
|
||||
try {
|
||||
// Create form data to create app
|
||||
let data = new FormData()
|
||||
|
@ -64,17 +106,11 @@
|
|||
await auth.setInitInfo({})
|
||||
$goto(`/builder/app/${createdApp.instance._id}`)
|
||||
} catch (error) {
|
||||
creating = false
|
||||
console.error(error)
|
||||
notifications.error("Error creating app")
|
||||
}
|
||||
}
|
||||
|
||||
// auto add slash to url
|
||||
$: {
|
||||
if ($values.url && !$values.url.startsWith("/")) {
|
||||
$values.url = `/${$values.url}`
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<ModalContent
|
||||
|
@ -83,6 +119,15 @@
|
|||
onConfirm={createNewApp}
|
||||
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}
|
||||
<Dropzone
|
||||
error={$validation.touched.file && $validation.errors.file}
|
||||
|
@ -97,20 +142,42 @@
|
|||
{/if}
|
||||
<Input
|
||||
bind:value={$values.name}
|
||||
disabled={creating}
|
||||
error={$validation.touched.name && $validation.errors.name}
|
||||
on:blur={() => ($validation.touched.name = true)}
|
||||
on:change={nameToUrl($values.name)}
|
||||
label="Name"
|
||||
placeholder={$auth.user.firstName
|
||||
placeholder={$auth.user?.firstName
|
||||
? `${$auth.user.firstName}s app`
|
||||
: "My app"}
|
||||
/>
|
||||
<Input
|
||||
bind:value={$values.url}
|
||||
error={$validation.touched.url && $validation.errors.url}
|
||||
on:blur={() => ($validation.touched.url = true)}
|
||||
label="URL"
|
||||
placeholder={$values.name
|
||||
? "/" + encodeURIComponent($values.name).toLowerCase()
|
||||
: "/"}
|
||||
/>
|
||||
<span>
|
||||
<Input
|
||||
bind:value={$values.url}
|
||||
disabled={creating}
|
||||
error={$validation.touched.url && $validation.errors.url}
|
||||
on:blur={() => ($validation.touched.url = true)}
|
||||
on:change={tidyUrl($values.url)}
|
||||
label="URL"
|
||||
placeholder={$values.url
|
||||
? $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>
|
||||
|
||||
<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
|
||||
$: {
|
||||
if ($values.url && !$values.url.startsWith("/")) {
|
||||
$values.url = `/${$values.url}`
|
||||
const resolveAppUrl = (template, name) => {
|
||||
let parsedName
|
||||
const resolvedName = resolveAppName(null, 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(null, appName)
|
||||
tidyUrl(resolvedUrl)
|
||||
}
|
||||
</script>
|
||||
|
||||
|
@ -61,15 +81,17 @@
|
|||
bind:value={$values.name}
|
||||
error={$validation.touched.name && $validation.errors.name}
|
||||
on:blur={() => ($validation.touched.name = true)}
|
||||
on:change={nameToUrl($values.name)}
|
||||
label="Name"
|
||||
/>
|
||||
<Input
|
||||
bind:value={$values.url}
|
||||
error={$validation.touched.url && $validation.errors.url}
|
||||
on:blur={() => ($validation.touched.url = true)}
|
||||
on:change={tidyUrl($values.url)}
|
||||
label="URL"
|
||||
placeholder={$values.name
|
||||
? "/" + encodeURIComponent($values.name).toLowerCase()
|
||||
: "/"}
|
||||
placeholder={$values.url
|
||||
? $values.url
|
||||
: `/${resolveAppUrl(null, $values.name)}`}
|
||||
/>
|
||||
</ModalContent>
|
||||
|
|
|
@ -35,13 +35,14 @@ export const url = (validation, { apps, currentApp } = { apps: [] }) => {
|
|||
validation.addValidator(
|
||||
"url",
|
||||
string()
|
||||
.trim()
|
||||
.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(
|
||||
"non-existing-app-url",
|
||||
"Another app with the same URL already exists",
|
||||
value => {
|
||||
// url is nullable
|
||||
if (!value) {
|
||||
return true
|
||||
}
|
||||
|
|
|
@ -5,7 +5,6 @@
|
|||
import DeployModal from "components/deploy/DeployModal.svelte"
|
||||
import RevertModal from "components/deploy/RevertModal.svelte"
|
||||
import VersionModal from "components/deploy/VersionModal.svelte"
|
||||
import NPSFeedbackForm from "components/feedback/NPSFeedbackForm.svelte"
|
||||
import { API } from "api"
|
||||
import { auth, admin } from "stores/portal"
|
||||
import { isActive, goto, layout, redirect } from "@roxi/routify"
|
||||
|
@ -21,15 +20,11 @@
|
|||
|
||||
// Sync once when you load the app
|
||||
let hasSynced = false
|
||||
let userShouldPostFeedback = false
|
||||
$: selected = capitalise(
|
||||
$layout.children.find(layout => $isActive(layout.path))?.title ?? "data"
|
||||
)
|
||||
|
||||
function previewApp() {
|
||||
if (!$auth?.user?.flags?.feedbackSubmitted) {
|
||||
userShouldPostFeedback = true
|
||||
}
|
||||
window.open(`/${application}`)
|
||||
}
|
||||
|
||||
|
@ -126,10 +121,6 @@
|
|||
<p>Something went wrong: {error.message}</p>
|
||||
{/await}
|
||||
|
||||
{#if userShouldPostFeedback}
|
||||
<NPSFeedbackForm on:complete={() => (userShouldPostFeedback = false)} />
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
.loading {
|
||||
min-height: 100%;
|
||||
|
|
|
@ -23,10 +23,8 @@
|
|||
<div class="nav">
|
||||
<Tabs {selected} on:select={selectFirstDatasource}>
|
||||
<Tab title="Sources">
|
||||
<div class="tab-content-padding">
|
||||
<DatasourceNavigator />
|
||||
<CreateDatasourceModal bind:modal />
|
||||
</div>
|
||||
<DatasourceNavigator />
|
||||
<CreateDatasourceModal bind:modal />
|
||||
</Tab>
|
||||
</Tabs>
|
||||
<div
|
||||
|
@ -63,10 +61,6 @@
|
|||
display: contents;
|
||||
}
|
||||
|
||||
.tab-content-padding {
|
||||
padding: 0 var(--spacing-xl);
|
||||
}
|
||||
|
||||
.nav {
|
||||
overflow-y: auto;
|
||||
background: var(--background);
|
||||
|
|
|
@ -415,9 +415,7 @@
|
|||
<Banner
|
||||
extraButtonText="Learn more"
|
||||
extraButtonAction={() =>
|
||||
window.open(
|
||||
"https://docs.budibase.com/building-apps/data/transformers"
|
||||
)}
|
||||
window.open("https://docs.budibase.com/docs/transformers")}
|
||||
on:change={() => updateFlag("queryTransformerBanner", true)}
|
||||
>
|
||||
Add a JavaScript function to transform the query result.
|
||||
|
|
|
@ -244,8 +244,6 @@
|
|||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-l);
|
||||
padding: 0 0 60px 0;
|
||||
overflow-y: auto;
|
||||
border-right: var(--border-light);
|
||||
}
|
||||
|
||||
|
|
|
@ -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 = false
|
||||
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,
|
||||
Body,
|
||||
Search,
|
||||
Icon,
|
||||
Divider,
|
||||
} from "@budibase/bbui"
|
||||
import TemplateDisplay from "components/common/TemplateDisplay.svelte"
|
||||
import Spinner from "components/common/Spinner.svelte"
|
||||
import CreateAppModal from "components/start/CreateAppModal.svelte"
|
||||
import UpdateAppModal from "components/start/UpdateAppModal.svelte"
|
||||
|
@ -45,6 +46,21 @@
|
|||
let appName = ""
|
||||
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)
|
||||
$: filteredApps = enrichedApps.filter(app =>
|
||||
app?.name?.toLowerCase().includes(searchTerm.toLowerCase())
|
||||
|
@ -79,9 +95,13 @@
|
|||
}
|
||||
|
||||
const initiateAppCreation = () => {
|
||||
template = null
|
||||
creationModal.show()
|
||||
creatingApp = true
|
||||
if ($apps?.length) {
|
||||
$goto("/builder/portal/apps/create")
|
||||
} else {
|
||||
template = null
|
||||
creationModal.show()
|
||||
creatingApp = true
|
||||
}
|
||||
}
|
||||
|
||||
const initiateAppsExport = () => {
|
||||
|
@ -268,148 +288,120 @@
|
|||
|
||||
<Page wide>
|
||||
<Layout noPadding gap="XL">
|
||||
<div class="title">
|
||||
<Layout noPadding gap="XS">
|
||||
<Heading size="M">Welcome to Budibase</Heading>
|
||||
<Body size="S">
|
||||
Manage your apps and get a head start with templates
|
||||
</Body>
|
||||
</Layout>
|
||||
{#if loaded}
|
||||
<div class="title">
|
||||
<div class="welcome">
|
||||
<Layout noPadding gap="XS">
|
||||
<Heading size="M">{welcomeHeader}</Heading>
|
||||
<Body size="S">
|
||||
{welcomeBody}
|
||||
</Body>
|
||||
</Layout>
|
||||
|
||||
<div class="buttons">
|
||||
{#if cloud}
|
||||
<Button
|
||||
size="L"
|
||||
icon="Export"
|
||||
quiet
|
||||
secondary
|
||||
on:click={initiateAppsExport}
|
||||
>
|
||||
Export apps
|
||||
</Button>
|
||||
{/if}
|
||||
<Button
|
||||
icon="Import"
|
||||
size="L"
|
||||
quiet
|
||||
secondary
|
||||
on:click={initiateAppImport}
|
||||
>
|
||||
Import app
|
||||
</Button>
|
||||
<Button size="L" icon="Add" cta on:click={initiateAppCreation}>
|
||||
Create app
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Layout noPadding gap="S">
|
||||
<Detail size="L">Quick start templates</Detail>
|
||||
<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
|
||||
<div class="buttons">
|
||||
<Button
|
||||
dataCy="create-app-btn"
|
||||
size="L"
|
||||
icon="Add"
|
||||
cta
|
||||
on:click={initiateAppCreation}
|
||||
>
|
||||
<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>
|
||||
{createAppButtonText}
|
||||
</Button>
|
||||
{#if $apps?.length > 0}
|
||||
<Button
|
||||
icon="Experience"
|
||||
size="L"
|
||||
quiet
|
||||
secondary
|
||||
on:click={$goto("/builder/portal/apps/templates")}
|
||||
>
|
||||
Templates
|
||||
</Button>
|
||||
{/if}
|
||||
{#if !$apps?.length}
|
||||
<Button
|
||||
dataCy="import-app-btn"
|
||||
icon="Import"
|
||||
size="L"
|
||||
quiet
|
||||
secondary
|
||||
on:click={initiateAppImport}
|
||||
>
|
||||
Import app
|
||||
</Button>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<Layout gap="S" justifyItems="center">
|
||||
<img class="img-logo img-size" alt="logo" src={Logo} />
|
||||
</Layout>
|
||||
</div>
|
||||
<Divider size="S" />
|
||||
</div>
|
||||
|
||||
{#if !$apps?.length && $templates?.length}
|
||||
<TemplateDisplay templates={$templates} />
|
||||
{/if}
|
||||
|
||||
{#if enrichedApps.length}
|
||||
<Layout noPadding gap="S">
|
||||
<div class="title">
|
||||
<Detail size="L">My 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">
|
||||
<Select
|
||||
quiet
|
||||
autoWidth
|
||||
bind:value={sortBy}
|
||||
placeholder={null}
|
||||
options={[
|
||||
{ label: "Sort by name", value: "name" },
|
||||
{ label: "Sort by recently updated", value: "updated" },
|
||||
{ label: "Sort by status", value: "status" },
|
||||
]}
|
||||
/>
|
||||
<Search placeholder="Search" bind:value={searchTerm} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</Layout>
|
||||
|
||||
{#if loaded && enrichedApps.length}
|
||||
<Layout noPadding gap="S">
|
||||
<div class="title">
|
||||
<Detail size="L">My apps</Detail>
|
||||
<div class="filter">
|
||||
<Select
|
||||
quiet
|
||||
autoWidth
|
||||
bind:value={sortBy}
|
||||
placeholder={null}
|
||||
options={[
|
||||
{ label: "Sort by name", value: "name" },
|
||||
{ label: "Sort by recently updated", value: "updated" },
|
||||
{ label: "Sort by status", value: "status" },
|
||||
]}
|
||||
/>
|
||||
<Search placeholder="Search" bind:value={searchTerm} />
|
||||
<div class="appTable">
|
||||
{#each filteredApps as app (app.appId)}
|
||||
<AppRow
|
||||
{releaseLock}
|
||||
{editIcon}
|
||||
{app}
|
||||
{unpublishApp}
|
||||
{viewApp}
|
||||
{editApp}
|
||||
{exportApp}
|
||||
{deleteApp}
|
||||
{updateApp}
|
||||
/>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="appTable">
|
||||
{#each filteredApps as app (app.appId)}
|
||||
<AppRow
|
||||
{releaseLock}
|
||||
{editIcon}
|
||||
{app}
|
||||
{unpublishApp}
|
||||
{viewApp}
|
||||
{editApp}
|
||||
{exportApp}
|
||||
{deleteApp}
|
||||
{updateApp}
|
||||
/>
|
||||
{/each}
|
||||
</div>
|
||||
</Layout>
|
||||
{/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>
|
||||
</Layout>
|
||||
{/if}
|
||||
{/if}
|
||||
|
||||
{#if creatingFromTemplate}
|
||||
<div class="empty-wrapper">
|
||||
<img class="img-logo img-size" alt="logo" src={Logo} />
|
||||
<p>Creating your Budibase app from your selected template...</p>
|
||||
<Spinner size="10" />
|
||||
</div>
|
||||
|
@ -459,6 +451,15 @@
|
|||
<ChooseIconModal app={selectedApp} bind:this={iconModal} />
|
||||
|
||||
<style>
|
||||
.app-actions {
|
||||
display: flex;
|
||||
}
|
||||
.app-actions :global(> button) {
|
||||
margin-right: 10px;
|
||||
}
|
||||
.title .welcome > .buttons {
|
||||
padding-top: 30px;
|
||||
}
|
||||
.title {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
|
@ -475,13 +476,11 @@
|
|||
gap: var(--spacing-xl);
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
@media (max-width: 640px) {
|
||||
.buttons {
|
||||
flex-direction: row-reverse;
|
||||
justify-content: flex-end;
|
||||
@media (max-width: 1000px) {
|
||||
.img-logo {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.filter {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
|
@ -489,49 +488,6 @@
|
|||
align-items: center;
|
||||
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 {
|
||||
display: grid;
|
||||
grid-template-rows: auto;
|
||||
|
@ -557,7 +513,6 @@
|
|||
grid-template-columns: 1fr auto;
|
||||
}
|
||||
}
|
||||
|
||||
.empty-wrapper {
|
||||
flex: 1 1 auto;
|
||||
height: 100%;
|
||||
|
@ -566,42 +521,8 @@
|
|||
justify-content: 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 {
|
||||
width: 160px;
|
||||
height: 160px;
|
||||
}
|
||||
|
||||
.background-icon {
|
||||
margin-top: 4px;
|
||||
margin-right: 4px;
|
||||
}
|
||||
</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 = false
|
||||
|
||||
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>
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue