Merge branch 'master' of github.com:Budibase/budibase into global-bindings

This commit is contained in:
Andrew Kingston 2023-11-28 15:08:12 +00:00
commit b1a218beb0
387 changed files with 4562 additions and 2220 deletions

View File

@ -57,7 +57,10 @@
"destructuredArrayIgnorePattern": "^_" "destructuredArrayIgnorePattern": "^_"
} }
], ],
"import/no-relative-packages": "error" "import/no-relative-packages": "error",
"import/export": "error",
"import/no-duplicates": "error",
"import/newline-after-import": "error"
}, },
"globals": { "globals": {
"GeolocationPositionError": true "GeolocationPositionError": true

View File

@ -1,13 +1,11 @@
node_modules node_modules
dist dist
*.spec.js
packages/builder/src/components/design/AppPreview/CurrentItemPreview.svelte
packages/server/builder packages/server/builder
packages/server/coverage packages/server/coverage
packages/worker/coverage
packages/backend-core/coverage
packages/server/client packages/server/client
packages/server/src/definitions/openapi.ts packages/server/src/definitions/openapi.ts
packages/worker/coverage
packages/backend-core/coverage
packages/builder/.routify packages/builder/.routify
packages/sdk/sdk packages/sdk/sdk
packages/pro/coverage packages/pro/coverage

View File

@ -29,7 +29,6 @@ WORKDIR /opt/couchdb
ADD couch/vm.args couch/local.ini ./etc/ ADD couch/vm.args couch/local.ini ./etc/
WORKDIR / WORKDIR /
ADD build-target-paths.sh .
ADD runner.sh ./bbcouch-runner.sh ADD runner.sh ./bbcouch-runner.sh
RUN chmod +x ./bbcouch-runner.sh /opt/clouseau/bin/clouseau ./build-target-paths.sh RUN chmod +x ./bbcouch-runner.sh /opt/clouseau/bin/clouseau
CMD ["./bbcouch-runner.sh"] CMD ["./bbcouch-runner.sh"]

View File

@ -1,24 +0,0 @@
#!/bin/bash
echo ${TARGETBUILD} > /buildtarget.txt
if [[ "${TARGETBUILD}" = "aas" ]]; then
# Azure AppService uses /home for persistent data & SSH on port 2222
DATA_DIR="${DATA_DIR:-/home}"
WEBSITES_ENABLE_APP_SERVICE_STORAGE=true
mkdir -p $DATA_DIR/{search,minio,couch}
mkdir -p $DATA_DIR/couch/{dbs,views}
chown -R couchdb:couchdb $DATA_DIR/couch/
apt update
apt-get install -y openssh-server
echo "root:Docker!" | chpasswd
mkdir -p /tmp
chmod +x /tmp/ssh_setup.sh \
&& (sleep 1;/tmp/ssh_setup.sh 2>&1 > /dev/null)
cp /etc/sshd_config /etc/ssh/sshd_config
/etc/init.d/ssh restart
sed -i "s#DATA_DIR#/home#g" /opt/clouseau/clouseau.ini
sed -i "s#DATA_DIR#/home#g" /opt/couchdb/etc/local.ini
else
sed -i "s#DATA_DIR#/data#g" /opt/clouseau/clouseau.ini
sed -i "s#DATA_DIR#/data#g" /opt/couchdb/etc/local.ini
fi

View File

@ -1,14 +1,73 @@
#!/bin/bash #!/bin/bash
DATA_DIR=${DATA_DIR:-/data} DATA_DIR=${DATA_DIR:-/data}
mkdir -p ${DATA_DIR} mkdir -p ${DATA_DIR}
mkdir -p ${DATA_DIR}/couch/{dbs,views} mkdir -p ${DATA_DIR}/couch/{dbs,views}
mkdir -p ${DATA_DIR}/search mkdir -p ${DATA_DIR}/search
chown -R couchdb:couchdb ${DATA_DIR}/couch chown -R couchdb:couchdb ${DATA_DIR}/couch
/build-target-paths.sh
echo ${TARGETBUILD} > /buildtarget.txt
if [[ "${TARGETBUILD}" = "aas" ]]; then
# Azure AppService uses /home for persistent data & SSH on port 2222
DATA_DIR="${DATA_DIR:-/home}"
WEBSITES_ENABLE_APP_SERVICE_STORAGE=true
mkdir -p $DATA_DIR/{search,minio,couch}
mkdir -p $DATA_DIR/couch/{dbs,views}
chown -R couchdb:couchdb $DATA_DIR/couch/
apt update
apt-get install -y openssh-server
echo "root:Docker!" | chpasswd
mkdir -p /tmp
chmod +x /tmp/ssh_setup.sh \
&& (sleep 1;/tmp/ssh_setup.sh 2>&1 > /dev/null)
cp /etc/sshd_config /etc/ssh/sshd_config
/etc/init.d/ssh restart
sed -i "s#DATA_DIR#/home#g" /opt/clouseau/clouseau.ini
sed -i "s#DATA_DIR#/home#g" /opt/couchdb/etc/local.ini
elif [[ "${TARGETBUILD}" = "single" ]]; then
# In the single image build, the Dockerfile specifies /data as a volume
# mount, so we use that for all persistent data.
sed -i "s#DATA_DIR#/data#g" /opt/clouseau/clouseau.ini
sed -i "s#DATA_DIR#/data#g" /opt/couchdb/etc/local.ini
elif [[ -n $KUBERNETES_SERVICE_HOST ]]; then
# In Kubernetes the directory /opt/couchdb/data has a persistent volume
# mount for storing database data.
sed -i "s#DATA_DIR#/opt/couchdb/data#g" /opt/clouseau/clouseau.ini
# We remove the database_dir and view_index_dir settings from the local.ini
# in Kubernetes because it will default to /opt/couchdb/data which is what
# our Helm chart was using prior to us switching to using our own CouchDB
# image.
sed -i "s#^database_dir.*\$##g" /opt/couchdb/etc/local.ini
sed -i "s#^view_index_dir.*\$##g" /opt/couchdb/etc/local.ini
# We remove the -name setting from the vm.args file in Kubernetes because
# it will default to the pod FQDN, which is what's required for clustering
# to work.
sed -i "s/^-name .*$//g" /opt/couchdb/etc/vm.args
else
# For all other builds, we use /data for persistent data.
sed -i "s#DATA_DIR#/data#g" /opt/clouseau/clouseau.ini
sed -i "s#DATA_DIR#/data#g" /opt/couchdb/etc/local.ini
fi
# Start Clouseau. Budibase won't function correctly without Clouseau running, it
# powers the search API endpoints which are used to do all sorts, including
# populating app grids.
/opt/clouseau/bin/clouseau > /dev/stdout 2>&1 & /opt/clouseau/bin/clouseau > /dev/stdout 2>&1 &
# Start CouchDB.
/docker-entrypoint.sh /opt/couchdb/bin/couchdb & /docker-entrypoint.sh /opt/couchdb/bin/couchdb &
sleep 10
# Wati for CouchDB to start up.
while [[ $(curl -s -w "%{http_code}\n" http://localhost:5984/_up -o /dev/null) -ne 200 ]]; do
echo 'Waiting for CouchDB to start...';
sleep 5;
done
# CouchDB needs the `_users` and `_replicator` databases to exist before it will
# function correctly, so we create them here.
curl -X PUT http://${COUCHDB_USER}:${COUCHDB_PASSWORD}@localhost:5984/_users curl -X PUT http://${COUCHDB_USER}:${COUCHDB_PASSWORD}@localhost:5984/_users
curl -X PUT http://${COUCHDB_USER}:${COUCHDB_PASSWORD}@localhost:5984/_replicator curl -X PUT http://${COUCHDB_USER}:${COUCHDB_PASSWORD}@localhost:5984/_replicator
sleep infinity sleep infinity

View File

@ -6,7 +6,7 @@ services:
app-service: app-service:
build: build:
context: .. context: ..
dockerfile: packages/server/Dockerfile.v2 dockerfile: packages/server/Dockerfile
args: args:
- BUDIBASE_VERSION=0.0.0+dev-docker - BUDIBASE_VERSION=0.0.0+dev-docker
container_name: build-bbapps container_name: build-bbapps
@ -36,7 +36,7 @@ services:
worker-service: worker-service:
build: build:
context: .. context: ..
dockerfile: packages/worker/Dockerfile.v2 dockerfile: packages/worker/Dockerfile
args: args:
- BUDIBASE_VERSION=0.0.0+dev-docker - BUDIBASE_VERSION=0.0.0+dev-docker
container_name: build-bbworker container_name: build-bbworker

View File

@ -1,24 +0,0 @@
#!/bin/bash
echo ${TARGETBUILD} > /buildtarget.txt
if [[ "${TARGETBUILD}" = "aas" ]]; then
# Azure AppService uses /home for persistent data & SSH on port 2222
DATA_DIR="${DATA_DIR:-/home}"
WEBSITES_ENABLE_APP_SERVICE_STORAGE=true
mkdir -p $DATA_DIR/{search,minio,couch}
mkdir -p $DATA_DIR/couch/{dbs,views}
chown -R couchdb:couchdb $DATA_DIR/couch/
apt update
apt-get install -y openssh-server
echo "root:Docker!" | chpasswd
mkdir -p /tmp
chmod +x /tmp/ssh_setup.sh \
&& (sleep 1;/tmp/ssh_setup.sh 2>&1 > /dev/null)
cp /etc/sshd_config /etc/ssh/sshd_config
/etc/init.d/ssh restart
sed -i "s#DATA_DIR#/home#g" /opt/clouseau/clouseau.ini
sed -i "s#DATA_DIR#/home#g" /opt/couchdb/etc/local.ini
else
sed -i "s#DATA_DIR#/data#g" /opt/clouseau/clouseau.ini
sed -i "s#DATA_DIR#/data#g" /opt/couchdb/etc/local.ini
fi

View File

@ -1,44 +1,59 @@
FROM node:18-slim as build FROM node:18-slim as build
# install node-gyp dependencies # install node-gyp dependencies
RUN apt-get update && apt-get upgrade -y && apt-get install -y --no-install-recommends apt-utils cron g++ make python3 RUN apt-get update && apt-get install -y --no-install-recommends g++ make python3 jq
# add pin script
WORKDIR /
ADD scripts/cleanup.sh ./
RUN chmod +x /cleanup.sh
# build server # copy and install dependencies
WORKDIR /app WORKDIR /app
ADD packages/server . COPY package.json .
COPY yarn.lock . COPY yarn.lock .
RUN yarn install --production=true --network-timeout 1000000 COPY lerna.json .
RUN /cleanup.sh COPY .yarnrc .
# build worker COPY packages/server/package.json packages/server/package.json
WORKDIR /worker COPY packages/worker/package.json packages/worker/package.json
ADD packages/worker . # string-templates does not get bundled during the esbuild process, so we want to use the local version
COPY yarn.lock . COPY packages/string-templates/package.json packages/string-templates/package.json
RUN yarn install --production=true --network-timeout 1000000
RUN /cleanup.sh
FROM budibase/couchdb
COPY scripts/removeWorkspaceDependencies.sh scripts/removeWorkspaceDependencies.sh
RUN chmod +x ./scripts/removeWorkspaceDependencies.sh
RUN ./scripts/removeWorkspaceDependencies.sh packages/server/package.json
RUN ./scripts/removeWorkspaceDependencies.sh packages/worker/package.json
# We will never want to sync pro, but the script is still required
RUN echo '' > scripts/syncProPackage.js
RUN jq 'del(.scripts.postinstall)' package.json > temp.json && mv temp.json package.json
RUN ./scripts/removeWorkspaceDependencies.sh package.json
RUN --mount=type=cache,target=/root/.yarn YARN_CACHE_FOLDER=/root/.yarn yarn install --production
# copy the actual code
COPY packages/server/dist packages/server/dist
COPY packages/server/pm2.config.js packages/server/pm2.config.js
COPY packages/server/client packages/server/client
COPY packages/server/builder packages/server/builder
COPY packages/worker/dist packages/worker/dist
COPY packages/worker/pm2.config.js packages/worker/pm2.config.js
COPY packages/string-templates packages/string-templates
FROM budibase/couchdb as runner
ARG TARGETARCH ARG TARGETARCH
ENV TARGETARCH $TARGETARCH ENV TARGETARCH $TARGETARCH
ENV NODE_MAJOR 18
#TARGETBUILD can be set to single (for single docker image) or aas (for azure app service) #TARGETBUILD can be set to single (for single docker image) or aas (for azure app service)
# e.g. docker build --build-arg TARGETBUILD=aas .... # e.g. docker build --build-arg TARGETBUILD=aas ....
ARG TARGETBUILD=single ARG TARGETBUILD=single
ENV TARGETBUILD $TARGETBUILD ENV TARGETBUILD $TARGETBUILD
COPY --from=build /app /app
COPY --from=build /worker /worker
# install base dependencies # install base dependencies
RUN apt-get update && \ RUN apt-get update && \
apt-get install -y --no-install-recommends software-properties-common nginx uuid-runtime redis-server apt-get install -y --no-install-recommends software-properties-common nginx uuid-runtime redis-server libaio1
# Install postgres client for pg_dump utils # Install postgres client for pg_dump utils
RUN apt install software-properties-common apt-transport-https gpg -y \ RUN apt install -y software-properties-common apt-transport-https ca-certificates gnupg \
&& curl -fsSl https://www.postgresql.org/media/keys/ACCC4CF8.asc | gpg --dearmor | tee /usr/share/keyrings/postgresql.gpg > /dev/null \ && curl -fsSl https://www.postgresql.org/media/keys/ACCC4CF8.asc | gpg --dearmor | tee /usr/share/keyrings/postgresql.gpg > /dev/null \
&& echo deb [arch=amd64,arm64,ppc64el signed-by=/usr/share/keyrings/postgresql.gpg] http://apt.postgresql.org/pub/repos/apt/ $(lsb_release -cs)-pgdg main | tee /etc/apt/sources.list.d/postgresql.list \ && echo deb [arch=amd64,arm64,ppc64el signed-by=/usr/share/keyrings/postgresql.gpg] http://apt.postgresql.org/pub/repos/apt/ $(lsb_release -cs)-pgdg main | tee /etc/apt/sources.list.d/postgresql.list \
&& apt update -y \ && apt update -y \
@ -47,14 +62,12 @@ RUN apt install software-properties-common apt-transport-https gpg -y \
# install other dependencies, nodejs, oracle requirements, jdk8, redis, nginx # install other dependencies, nodejs, oracle requirements, jdk8, redis, nginx
WORKDIR /nodejs WORKDIR /nodejs
RUN curl -sL https://deb.nodesource.com/setup_16.x -o /tmp/nodesource_setup.sh && \ COPY scripts/install-node.sh ./install.sh
bash /tmp/nodesource_setup.sh && \ RUN chmod +x install.sh && ./install.sh
apt-get install -y --no-install-recommends libaio1 nodejs && \
npm install --global yarn pm2
# setup nginx # setup nginx
ADD hosting/single/nginx/nginx.conf /etc/nginx COPY hosting/single/nginx/nginx.conf /etc/nginx
ADD hosting/single/nginx/nginx-default-site.conf /etc/nginx/sites-enabled/default COPY hosting/single/nginx/nginx-default-site.conf /etc/nginx/sites-enabled/default
RUN mkdir -p /var/log/nginx && \ RUN mkdir -p /var/log/nginx && \
touch /var/log/nginx/error.log && \ touch /var/log/nginx/error.log && \
touch /var/run/nginx.pid && \ touch /var/run/nginx.pid && \
@ -62,29 +75,39 @@ RUN mkdir -p /var/log/nginx && \
WORKDIR / WORKDIR /
RUN mkdir -p scripts/integrations/oracle RUN mkdir -p scripts/integrations/oracle
ADD packages/server/scripts/integrations/oracle scripts/integrations/oracle COPY packages/server/scripts/integrations/oracle scripts/integrations/oracle
RUN /bin/bash -e ./scripts/integrations/oracle/instantclient/linux/install.sh RUN /bin/bash -e ./scripts/integrations/oracle/instantclient/linux/install.sh
# setup minio # setup minio
WORKDIR /minio WORKDIR /minio
ADD scripts/install-minio.sh ./install.sh COPY scripts/install-minio.sh ./install.sh
RUN chmod +x install.sh && ./install.sh RUN chmod +x install.sh && ./install.sh
# setup runner file # setup runner file
WORKDIR / WORKDIR /
ADD hosting/single/runner.sh . COPY hosting/single/runner.sh .
RUN chmod +x ./runner.sh RUN chmod +x ./runner.sh
ADD hosting/single/healthcheck.sh . COPY hosting/single/healthcheck.sh .
RUN chmod +x ./healthcheck.sh RUN chmod +x ./healthcheck.sh
# Script below sets the path for storing data based on $DATA_DIR # Script below sets the path for storing data based on $DATA_DIR
# For Azure App Service install SSH & point data locations to /home # For Azure App Service install SSH & point data locations to /home
ADD hosting/single/ssh/sshd_config /etc/ COPY hosting/single/ssh/sshd_config /etc/
ADD hosting/single/ssh/ssh_setup.sh /tmp COPY hosting/single/ssh/ssh_setup.sh /tmp
RUN /build-target-paths.sh
# setup letsencrypt certificate
RUN apt-get install -y certbot python3-certbot-nginx
COPY hosting/letsencrypt /app/letsencrypt
RUN chmod +x /app/letsencrypt/certificate-request.sh /app/letsencrypt/certificate-renew.sh
COPY --from=build /app/node_modules /node_modules
COPY --from=build /app/package.json /package.json
COPY --from=build /app/packages/server /app
COPY --from=build /app/packages/worker /worker
COPY --from=build /app/packages/string-templates /string-templates
RUN cd /string-templates && yarn link && cd ../app && yarn link @budibase/string-templates && cd ../worker && yarn link @budibase/string-templates
# cleanup cache
RUN yarn cache clean -f
EXPOSE 80 EXPOSE 80
EXPOSE 443 EXPOSE 443
@ -92,20 +115,10 @@ EXPOSE 443
EXPOSE 2222 EXPOSE 2222
VOLUME /data VOLUME /data
# setup letsencrypt certificate ARG BUDIBASE_VERSION
RUN apt-get install -y certbot python3-certbot-nginx # Ensuring the version argument is sent
ADD hosting/letsencrypt /app/letsencrypt RUN test -n "$BUDIBASE_VERSION"
RUN chmod +x /app/letsencrypt/certificate-request.sh /app/letsencrypt/certificate-renew.sh ENV BUDIBASE_VERSION=$BUDIBASE_VERSION
# Remove cached files
RUN rm -rf \
/root/.cache \
/root/.npm \
/root/.pip \
/usr/local/share/doc \
/usr/share/doc \
/usr/share/man \
/var/lib/apt/lists/* \
/tmp/*
HEALTHCHECK --interval=15s --timeout=15s --start-period=45s CMD "/healthcheck.sh" HEALTHCHECK --interval=15s --timeout=15s --start-period=45s CMD "/healthcheck.sh"

View File

@ -1,131 +0,0 @@
FROM node:18-slim as build
# install node-gyp dependencies
RUN apt-get update && apt-get install -y --no-install-recommends g++ make python3 jq
# copy and install dependencies
WORKDIR /app
COPY package.json .
COPY yarn.lock .
COPY lerna.json .
COPY .yarnrc .
COPY packages/server/package.json packages/server/package.json
COPY packages/worker/package.json packages/worker/package.json
# string-templates does not get bundled during the esbuild process, so we want to use the local version
COPY packages/string-templates/package.json packages/string-templates/package.json
COPY scripts/removeWorkspaceDependencies.sh scripts/removeWorkspaceDependencies.sh
RUN chmod +x ./scripts/removeWorkspaceDependencies.sh
RUN ./scripts/removeWorkspaceDependencies.sh packages/server/package.json
RUN ./scripts/removeWorkspaceDependencies.sh packages/worker/package.json
# We will never want to sync pro, but the script is still required
RUN echo '' > scripts/syncProPackage.js
RUN jq 'del(.scripts.postinstall)' package.json > temp.json && mv temp.json package.json
RUN ./scripts/removeWorkspaceDependencies.sh package.json
RUN --mount=type=cache,target=/root/.yarn YARN_CACHE_FOLDER=/root/.yarn yarn install --production
# copy the actual code
COPY packages/server/dist packages/server/dist
COPY packages/server/pm2.config.js packages/server/pm2.config.js
COPY packages/server/client packages/server/client
COPY packages/server/builder packages/server/builder
COPY packages/worker/dist packages/worker/dist
COPY packages/worker/pm2.config.js packages/worker/pm2.config.js
COPY packages/string-templates packages/string-templates
FROM budibase/couchdb as runner
ARG TARGETARCH
ENV TARGETARCH $TARGETARCH
ENV NODE_MAJOR 18
#TARGETBUILD can be set to single (for single docker image) or aas (for azure app service)
# e.g. docker build --build-arg TARGETBUILD=aas ....
ARG TARGETBUILD=single
ENV TARGETBUILD $TARGETBUILD
# install base dependencies
RUN apt-get update && \
apt-get install -y --no-install-recommends software-properties-common nginx uuid-runtime redis-server libaio1
# Install postgres client for pg_dump utils
RUN apt install -y software-properties-common apt-transport-https ca-certificates gnupg \
&& curl -fsSl https://www.postgresql.org/media/keys/ACCC4CF8.asc | gpg --dearmor | tee /usr/share/keyrings/postgresql.gpg > /dev/null \
&& echo deb [arch=amd64,arm64,ppc64el signed-by=/usr/share/keyrings/postgresql.gpg] http://apt.postgresql.org/pub/repos/apt/ $(lsb_release -cs)-pgdg main | tee /etc/apt/sources.list.d/postgresql.list \
&& apt update -y \
&& apt install postgresql-client-15 -y \
&& apt remove software-properties-common apt-transport-https gpg -y
# install other dependencies, nodejs, oracle requirements, jdk8, redis, nginx
WORKDIR /nodejs
COPY scripts/install-node.sh ./install.sh
RUN chmod +x install.sh && ./install.sh
# setup nginx
COPY hosting/single/nginx/nginx.conf /etc/nginx
COPY hosting/single/nginx/nginx-default-site.conf /etc/nginx/sites-enabled/default
RUN mkdir -p /var/log/nginx && \
touch /var/log/nginx/error.log && \
touch /var/run/nginx.pid && \
usermod -a -G tty www-data
WORKDIR /
RUN mkdir -p scripts/integrations/oracle
COPY packages/server/scripts/integrations/oracle scripts/integrations/oracle
RUN /bin/bash -e ./scripts/integrations/oracle/instantclient/linux/install.sh
# setup minio
WORKDIR /minio
COPY scripts/install-minio.sh ./install.sh
RUN chmod +x install.sh && ./install.sh
# setup runner file
WORKDIR /
COPY hosting/single/runner.sh .
RUN chmod +x ./runner.sh
COPY hosting/single/healthcheck.sh .
RUN chmod +x ./healthcheck.sh
# Script below sets the path for storing data based on $DATA_DIR
# For Azure App Service install SSH & point data locations to /home
COPY hosting/single/ssh/sshd_config /etc/
COPY hosting/single/ssh/ssh_setup.sh /tmp
RUN /build-target-paths.sh
# setup letsencrypt certificate
RUN apt-get install -y certbot python3-certbot-nginx
COPY hosting/letsencrypt /app/letsencrypt
RUN chmod +x /app/letsencrypt/certificate-request.sh /app/letsencrypt/certificate-renew.sh
COPY --from=build /app/node_modules /node_modules
COPY --from=build /app/package.json /package.json
COPY --from=build /app/packages/server /app
COPY --from=build /app/packages/worker /worker
COPY --from=build /app/packages/string-templates /string-templates
RUN cd /string-templates && yarn link && cd ../app && yarn link @budibase/string-templates && cd ../worker && yarn link @budibase/string-templates
EXPOSE 80
EXPOSE 443
# Expose port 2222 for SSH on Azure App Service build
EXPOSE 2222
VOLUME /data
ARG BUDIBASE_VERSION
# Ensuring the version argument is sent
RUN test -n "$BUDIBASE_VERSION"
ENV BUDIBASE_VERSION=$BUDIBASE_VERSION
HEALTHCHECK --interval=15s --timeout=15s --start-period=45s CMD "/healthcheck.sh"
# must set this just before running
ENV NODE_ENV=production
WORKDIR /
CMD ["./runner.sh"]

View File

@ -25,7 +25,7 @@ if [[ $(curl -s -w "%{http_code}\n" http://localhost:4002/health -o /dev/null) -
healthy=false healthy=false
fi fi
if [[ $(curl -s -w "%{http_code}\n" http://localhost:5984/ -o /dev/null) -ne 200 ]]; then if [[ $(curl -s -w "%{http_code}\n" http://localhost:5984/_up -o /dev/null) -ne 200 ]]; then
echo 'ERROR: CouchDB is not running'; echo 'ERROR: CouchDB is not running';
healthy=false healthy=false
fi fi

View File

@ -22,11 +22,11 @@ declare -a DOCKER_VARS=("APP_PORT" "APPS_URL" "ARCHITECTURE" "BUDIBASE_ENVIRONME
# Azure App Service customisations # Azure App Service customisations
if [[ "${TARGETBUILD}" = "aas" ]]; then if [[ "${TARGETBUILD}" = "aas" ]]; then
DATA_DIR="${DATA_DIR:-/home}" export DATA_DIR="${DATA_DIR:-/home}"
WEBSITES_ENABLE_APP_SERVICE_STORAGE=true WEBSITES_ENABLE_APP_SERVICE_STORAGE=true
/etc/init.d/ssh start /etc/init.d/ssh start
else else
DATA_DIR=${DATA_DIR:-/data} export DATA_DIR=${DATA_DIR:-/data}
fi fi
mkdir -p ${DATA_DIR} mkdir -p ${DATA_DIR}
# Mount NFS or GCP Filestore if env vars exist for it # Mount NFS or GCP Filestore if env vars exist for it

View File

@ -1,5 +1,5 @@
{ {
"version": "2.13.11", "version": "2.13.19",
"npmClient": "yarn", "npmClient": "yarn",
"packages": [ "packages": [
"packages/*" "packages/*"

View File

@ -1,5 +1,6 @@
const _passport = require("koa-passport") const _passport = require("koa-passport")
const LocalStrategy = require("passport-local").Strategy const LocalStrategy = require("passport-local").Strategy
import { getGlobalDB } from "../context" import { getGlobalDB } from "../context"
import { Cookie } from "../constants" import { Cookie } from "../constants"
import { getSessionsForUser, invalidateSessions } from "../security/sessions" import { getSessionsForUser, invalidateSessions } from "../security/sessions"
@ -26,6 +27,7 @@ import { clearCookie, getCookie } from "../utils"
import { ssoSaveUserNoOp } from "../middleware/passport/sso/sso" import { ssoSaveUserNoOp } from "../middleware/passport/sso/sso"
const refresh = require("passport-oauth2-refresh") const refresh = require("passport-oauth2-refresh")
export { export {
auditLog, auditLog,
authError, authError,

View File

@ -17,7 +17,6 @@ import { DocumentType, SEPARATOR } from "../constants"
import { CacheKey, TTL, withCache } from "../cache" import { CacheKey, TTL, withCache } from "../cache"
import * as context from "../context" import * as context from "../context"
import env from "../environment" import env from "../environment"
import environment from "../environment"
// UTILS // UTILS
@ -181,10 +180,10 @@ export async function getGoogleDatasourceConfig(): Promise<
} }
export function getDefaultGoogleConfig(): GoogleInnerConfig | undefined { export function getDefaultGoogleConfig(): GoogleInnerConfig | undefined {
if (environment.GOOGLE_CLIENT_ID && environment.GOOGLE_CLIENT_SECRET) { if (env.GOOGLE_CLIENT_ID && env.GOOGLE_CLIENT_SECRET) {
return { return {
clientID: environment.GOOGLE_CLIENT_ID!, clientID: env.GOOGLE_CLIENT_ID!,
clientSecret: environment.GOOGLE_CLIENT_SECRET!, clientSecret: env.GOOGLE_CLIENT_SECRET!,
activated: true, activated: true,
} }
} }

View File

@ -1,4 +1,5 @@
import { prefixed, DocumentType } from "@budibase/types" import { prefixed, DocumentType } from "@budibase/types"
export { export {
SEPARATOR, SEPARATOR,
UNICODE_MAX, UNICODE_MAX,

View File

@ -5,7 +5,6 @@ const { getDB } = require("../db")
describe("db", () => { describe("db", () => {
describe("getDB", () => { describe("getDB", () => {
it("returns a db", async () => { it("returns a db", async () => {
const dbName = structures.db.id() const dbName = structures.db.id()
const db = getDB(dbName) const db = getDB(dbName)
expect(db).toBeDefined() expect(db).toBeDefined()

View File

@ -6,6 +6,7 @@ import { AppState, DeletedApp, getAppMetadata } from "../cache/appMetadata"
import { isDevApp, isDevAppID, getProdAppID } from "../docIds/conversions" import { isDevApp, isDevAppID, getProdAppID } from "../docIds/conversions"
import { App, Database } from "@budibase/types" import { App, Database } from "@budibase/types"
import { getStartEndKeyURL } from "../docIds" import { getStartEndKeyURL } from "../docIds"
export * from "../docIds" export * from "../docIds"
/** /**

View File

@ -1,5 +1,6 @@
import { APP_DEV_PREFIX, APP_PREFIX } from "../constants" import { APP_DEV_PREFIX, APP_PREFIX } from "../constants"
import { App } from "@budibase/types" import { App } from "@budibase/types"
const NO_APP_ERROR = "No app provided" const NO_APP_ERROR = "No app provided"
export function isDevAppID(appId?: string) { export function isDevAppID(appId?: string) {

View File

@ -1,2 +1,3 @@
import PosthogProcessor from "./PosthogProcessor" import PosthogProcessor from "./PosthogProcessor"
export default PosthogProcessor export default PosthogProcessor

View File

@ -1,7 +1,9 @@
import { testEnv } from "../../../../../tests/extra" import { testEnv } from "../../../../../tests/extra"
import PosthogProcessor from "../PosthogProcessor" import PosthogProcessor from "../PosthogProcessor"
import { Event, IdentityType, Hosting } from "@budibase/types" import { Event, IdentityType, Hosting } from "@budibase/types"
const tk = require("timekeeper") const tk = require("timekeeper")
import * as cache from "../../../../cache/generic" import * as cache from "../../../../cache/generic"
import { CacheKey } from "../../../../cache/generic" import { CacheKey } from "../../../../cache/generic"
import * as context from "../../../../context" import * as context from "../../../../context"

View File

@ -1,5 +1,6 @@
import env from "../environment" import env from "../environment"
import * as context from "../context" import * as context from "../context"
export * from "./installation" export * from "./installation"
/** /**

View File

@ -38,6 +38,7 @@ export * as docIds from "./docIds"
// circular dependencies // circular dependencies
import * as context from "./context" import * as context from "./context"
import * as _tenancy from "./tenancy" import * as _tenancy from "./tenancy"
export const tenancy = { export const tenancy = {
..._tenancy, ..._tenancy,
...context, ...context,

View File

@ -1,7 +1,6 @@
import { newid } from "./utils" import { newid } from "./utils"
import * as events from "./events" import * as events from "./events"
import { StaticDatabases } from "./db" import { StaticDatabases, doWithDB } from "./db"
import { doWithDB } from "./db"
import { Installation, IdentityType, Database } from "@budibase/types" import { Installation, IdentityType, Database } from "@budibase/types"
import * as context from "./context" import * as context from "./context"
import semver from "semver" import semver from "semver"

View File

@ -1,4 +1,5 @@
import { Header } from "../../constants" import { Header } from "../../constants"
const correlator = require("correlation-id") const correlator = require("correlation-id")
export const setHeader = (headers: any) => { export const setHeader = (headers: any) => {

View File

@ -1,5 +1,6 @@
import { Header } from "../../constants" import { Header } from "../../constants"
import { v4 as uuid } from "uuid" import { v4 as uuid } from "uuid"
const correlator = require("correlation-id") const correlator = require("correlation-id")
const correlation = (ctx: any, next: any) => { const correlation = (ctx: any, next: any) => {

View File

@ -1,9 +1,12 @@
import env from "../../environment" import env from "../../environment"
import { logger } from "./logger" import { logger } from "./logger"
import { IncomingMessage } from "http" import { IncomingMessage } from "http"
const pino = require("koa-pino-logger") const pino = require("koa-pino-logger")
import { Options } from "pino-http" import { Options } from "pino-http"
import { Ctx } from "@budibase/types" import { Ctx } from "@budibase/types"
const correlator = require("correlation-id") const correlator = require("correlation-id")
export function pinoSettings(): Options { export function pinoSettings(): Options {

View File

@ -2,6 +2,7 @@ export * as local from "./passport/local"
export * as google from "./passport/sso/google" export * as google from "./passport/sso/google"
export * as oidc from "./passport/sso/oidc" export * as oidc from "./passport/sso/oidc"
import * as datasourceGoogle from "./passport/datasource/google" import * as datasourceGoogle from "./passport/datasource/google"
export const datasource = { export const datasource = {
google: datasourceGoogle, google: datasourceGoogle,
} }

View File

@ -8,6 +8,7 @@ import {
SaveSSOUserFunction, SaveSSOUserFunction,
GoogleInnerConfig, GoogleInnerConfig,
} from "@budibase/types" } from "@budibase/types"
const GoogleStrategy = require("passport-google-oauth").OAuth2Strategy const GoogleStrategy = require("passport-google-oauth").OAuth2Strategy
export function buildVerifyFn(saveUserFn: SaveSSOUserFunction) { export function buildVerifyFn(saveUserFn: SaveSSOUserFunction) {

View File

@ -6,6 +6,7 @@ const mockStrategy = require("passport-google-oauth").OAuth2Strategy
jest.mock("../sso") jest.mock("../sso")
import * as _sso from "../sso" import * as _sso from "../sso"
const sso = jest.mocked(_sso) const sso = jest.mocked(_sso)
const mockSaveUserFn = jest.fn() const mockSaveUserFn = jest.fn()

View File

@ -11,6 +11,7 @@ const mockSaveUser = jest.fn()
jest.mock("../../../../users") jest.mock("../../../../users")
import * as _users from "../../../../users" import * as _users from "../../../../users"
const users = jest.mocked(_users) const users = jest.mocked(_users)
const getErrorMessage = () => { const getErrorMessage = () => {

View File

@ -5,6 +5,7 @@ import { structures } from "../../../tests"
import { ContextUser, ServiceType } from "@budibase/types" import { ContextUser, ServiceType } from "@budibase/types"
import { doInAppContext } from "../../context" import { doInAppContext } from "../../context"
import env from "../../environment" import env from "../../environment"
env._set("SERVICE_TYPE", ServiceType.APPS) env._set("SERVICE_TYPE", ServiceType.APPS)
const appId = "app_aaa" const appId = "app_aaa"

View File

@ -1,4 +1,5 @@
const sanitize = require("sanitize-s3-objectkey") const sanitize = require("sanitize-s3-objectkey")
import AWS from "aws-sdk" import AWS from "aws-sdk"
import stream, { Readable } from "stream" import stream, { Readable } from "stream"
import fetch from "node-fetch" import fetch from "node-fetch"
@ -259,12 +260,12 @@ export async function listAllObjects(bucketName: string, path: string) {
} }
/** /**
* Generate a presigned url with a default TTL of 1 hour * Generate a presigned url with a default TTL of 36 hours
*/ */
export function getPresignedUrl( export function getPresignedUrl(
bucketName: string, bucketName: string,
key: string, key: string,
durationSeconds: number = 3600 durationSeconds: number = 129600
) { ) {
const objectStore = ObjectStore(bucketName, { presigning: true }) const objectStore = ObjectStore(bucketName, { presigning: true })
const params = { const params = {

View File

@ -3,6 +3,7 @@ import { getLockClient } from "./init"
import { LockOptions, LockType } from "@budibase/types" import { LockOptions, LockType } from "@budibase/types"
import * as context from "../context" import * as context from "../context"
import env from "../environment" import env from "../environment"
import { logWarn } from "../logging"
async function getClient( async function getClient(
type: LockType, type: LockType,
@ -116,7 +117,7 @@ export async function doWithLock<T>(
const result = await task() const result = await task()
return { executed: true, result } return { executed: true, result }
} catch (e: any) { } catch (e: any) {
console.warn("lock error") logWarn(`lock type: ${opts.type} error`, e)
// lock limit exceeded // lock limit exceeded
if (e.name === "LockError") { if (e.name === "LockError") {
if (opts.type === LockType.TRY_ONCE) { if (opts.type === LockType.TRY_ONCE) {
@ -124,11 +125,9 @@ export async function doWithLock<T>(
// due to retry count (0) exceeded // due to retry count (0) exceeded
return { executed: false } return { executed: false }
} else { } else {
console.error(e)
throw e throw e
} }
} else { } else {
console.error(e)
throw e throw e
} }
} finally { } finally {

View File

@ -75,10 +75,12 @@ export function getRedisConnectionDetails() {
} }
const [host, port] = url.split(":") const [host, port] = url.split(":")
const portNumber = parseInt(port)
return { return {
host, host,
password, password,
port: parseInt(port), // assume default port for redis if invalid found
port: isNaN(portNumber) ? 6379 : portNumber,
} }
} }

View File

@ -160,4 +160,5 @@ export function isPermissionLevelHigherThanRead(level: PermissionLevel) {
// utility as a lot of things need simply the builder permission // utility as a lot of things need simply the builder permission
export const BUILDER = PermissionType.BUILDER export const BUILDER = PermissionType.BUILDER
export const CREATOR = PermissionType.CREATOR
export const GLOBAL_BUILDER = PermissionType.GLOBAL_BUILDER export const GLOBAL_BUILDER = PermissionType.GLOBAL_BUILDER

View File

@ -1,7 +1,12 @@
import { BuiltinPermissionID, PermissionLevel } from "./permissions" import { BuiltinPermissionID, PermissionLevel } from "./permissions"
import { prefixRoleID, getRoleParams, DocumentType, SEPARATOR } from "../db" import {
prefixRoleID,
getRoleParams,
DocumentType,
SEPARATOR,
doWithDB,
} from "../db"
import { getAppDB } from "../context" import { getAppDB } from "../context"
import { doWithDB } from "../db"
import { Screen, Role as RoleDoc } from "@budibase/types" import { Screen, Role as RoleDoc } from "@budibase/types"
import cloneDeep from "lodash/fp/cloneDeep" import cloneDeep from "lodash/fp/cloneDeep"

View File

@ -1,6 +1,7 @@
const redis = require("../redis/init") const redis = require("../redis/init")
const { v4: uuidv4 } = require("uuid") const { v4: uuidv4 } = require("uuid")
const { logWarn } = require("../logging") const { logWarn } = require("../logging")
import env from "../environment" import env from "../environment"
import { import {
Session, Session,

View File

@ -1,9 +1,8 @@
import env from "../environment" import env from "../environment"
import * as eventHelpers from "./events" import * as eventHelpers from "./events"
import * as accounts from "../accounts"
import * as accountSdk from "../accounts" import * as accountSdk from "../accounts"
import * as cache from "../cache" import * as cache from "../cache"
import { getGlobalDB, getIdentity, getTenantId } from "../context" import { doInTenant, getGlobalDB, getIdentity, getTenantId } from "../context"
import * as dbUtils from "../db" import * as dbUtils from "../db"
import { EmailUnavailableError, HTTPError } from "../errors" import { EmailUnavailableError, HTTPError } from "../errors"
import * as platform from "../platform" import * as platform from "../platform"
@ -11,12 +10,10 @@ import * as sessions from "../security/sessions"
import * as usersCore from "./users" import * as usersCore from "./users"
import { import {
Account, Account,
AllDocsResponse,
BulkUserCreated, BulkUserCreated,
BulkUserDeleted, BulkUserDeleted,
isSSOAccount, isSSOAccount,
isSSOUser, isSSOUser,
RowResponse,
SaveUserOpts, SaveUserOpts,
User, User,
UserStatus, UserStatus,
@ -149,12 +146,12 @@ export class UserDB {
static async allUsers() { static async allUsers() {
const db = getGlobalDB() const db = getGlobalDB()
const response = await db.allDocs( const response = await db.allDocs<User>(
dbUtils.getGlobalUserParams(null, { dbUtils.getGlobalUserParams(null, {
include_docs: true, include_docs: true,
}) })
) )
return response.rows.map((row: any) => row.doc) return response.rows.map(row => row.doc!)
} }
static async countUsersByApp(appId: string) { static async countUsersByApp(appId: string) {
@ -212,13 +209,6 @@ export class UserDB {
throw new Error("_id or email is required") throw new Error("_id or email is required")
} }
if (
user.builder?.apps?.length &&
!(await UserDB.features.isAppBuildersEnabled())
) {
throw new Error("Unable to update app builders, please check license")
}
let dbUser: User | undefined let dbUser: User | undefined
if (_id) { if (_id) {
// try to get existing user from db // try to get existing user from db
@ -467,7 +457,7 @@ export class UserDB {
if (!env.SELF_HOSTED && !env.DISABLE_ACCOUNT_PORTAL) { if (!env.SELF_HOSTED && !env.DISABLE_ACCOUNT_PORTAL) {
// root account holder can't be deleted from inside budibase // root account holder can't be deleted from inside budibase
const email = dbUser.email const email = dbUser.email
const account = await accounts.getAccount(email) const account = await accountSdk.getAccount(email)
if (account) { if (account) {
if (dbUser.userId === getIdentity()!._id) { if (dbUser.userId === getIdentity()!._id) {
throw new HTTPError('Please visit "Account" to delete this user', 400) throw new HTTPError('Please visit "Account" to delete this user', 400)
@ -488,6 +478,37 @@ export class UserDB {
await sessions.invalidateSessions(userId, { reason: "deletion" }) await sessions.invalidateSessions(userId, { reason: "deletion" })
} }
static async createAdminUser(
email: string,
password: string,
tenantId: string,
opts?: { ssoId?: string; hashPassword?: boolean; requirePassword?: boolean }
) {
const user: User = {
email: email,
password: password,
createdAt: Date.now(),
roles: {},
builder: {
global: true,
},
admin: {
global: true,
},
tenantId,
}
if (opts?.ssoId) {
user.ssoId = opts.ssoId
}
// always bust checklist beforehand, if an error occurs but can proceed, don't get
// stuck in a cycle
await cache.bustCache(cache.CacheKey.CHECKLIST)
return await UserDB.save(user, {
hashPassword: opts?.hashPassword,
requirePassword: opts?.requirePassword,
})
}
static async getGroups(groupIds: string[]) { static async getGroups(groupIds: string[]) {
return await this.groups.getBulk(groupIds) return await this.groups.getBulk(groupIds)
} }

View File

@ -25,6 +25,7 @@ import {
import { getGlobalDB } from "../context" import { getGlobalDB } from "../context"
import * as context from "../context" import * as context from "../context"
import { isCreator } from "./utils" import { isCreator } from "./utils"
import { UserDB } from "./db"
type GetOpts = { cleanup?: boolean } type GetOpts = { cleanup?: boolean }
@ -43,7 +44,7 @@ function removeUserPassword(users: User | User[]) {
return users return users
} }
export const isSupportedUserSearch = (query: SearchQuery) => { export function isSupportedUserSearch(query: SearchQuery) {
const allowed = [ const allowed = [
{ op: SearchQueryOperators.STRING, key: "email" }, { op: SearchQueryOperators.STRING, key: "email" },
{ op: SearchQueryOperators.EQUAL, key: "_id" }, { op: SearchQueryOperators.EQUAL, key: "_id" },
@ -68,10 +69,10 @@ export const isSupportedUserSearch = (query: SearchQuery) => {
return true return true
} }
export const bulkGetGlobalUsersById = async ( export async function bulkGetGlobalUsersById(
userIds: string[], userIds: string[],
opts?: GetOpts opts?: GetOpts
) => { ) {
const db = getGlobalDB() const db = getGlobalDB()
let users = ( let users = (
await db.allDocs({ await db.allDocs({
@ -85,7 +86,7 @@ export const bulkGetGlobalUsersById = async (
return users return users
} }
export const getAllUserIds = async () => { export async function getAllUserIds() {
const db = getGlobalDB() const db = getGlobalDB()
const startKey = `${DocumentType.USER}${SEPARATOR}` const startKey = `${DocumentType.USER}${SEPARATOR}`
const response = await db.allDocs({ const response = await db.allDocs({
@ -95,7 +96,7 @@ export const getAllUserIds = async () => {
return response.rows.map(row => row.id) return response.rows.map(row => row.id)
} }
export const bulkUpdateGlobalUsers = async (users: User[]) => { export async function bulkUpdateGlobalUsers(users: User[]) {
const db = getGlobalDB() const db = getGlobalDB()
return (await db.bulkDocs(users)) as BulkDocsResponse return (await db.bulkDocs(users)) as BulkDocsResponse
} }
@ -113,10 +114,10 @@ export async function getById(id: string, opts?: GetOpts): Promise<User> {
* Given an email address this will use a view to search through * Given an email address this will use a view to search through
* all the users to find one with this email address. * all the users to find one with this email address.
*/ */
export const getGlobalUserByEmail = async ( export async function getGlobalUserByEmail(
email: String, email: String,
opts?: GetOpts opts?: GetOpts
): Promise<User | undefined> => { ): Promise<User | undefined> {
if (email == null) { if (email == null) {
throw "Must supply an email address to view" throw "Must supply an email address to view"
} }
@ -139,11 +140,23 @@ export const getGlobalUserByEmail = async (
return user return user
} }
export const searchGlobalUsersByApp = async ( export async function doesUserExist(email: string) {
try {
const user = await getGlobalUserByEmail(email)
if (Array.isArray(user) || user != null) {
return true
}
} catch (err) {
return false
}
return false
}
export async function searchGlobalUsersByApp(
appId: any, appId: any,
opts: DatabaseQueryOpts, opts: DatabaseQueryOpts,
getOpts?: GetOpts getOpts?: GetOpts
) => { ) {
if (typeof appId !== "string") { if (typeof appId !== "string") {
throw new Error("Must provide a string based app ID") throw new Error("Must provide a string based app ID")
} }
@ -167,10 +180,10 @@ export const searchGlobalUsersByApp = async (
Return any user who potentially has access to the application Return any user who potentially has access to the application
Admins, developers and app users with the explicitly role. Admins, developers and app users with the explicitly role.
*/ */
export const searchGlobalUsersByAppAccess = async ( export async function searchGlobalUsersByAppAccess(
appId: any, appId: any,
opts?: { limit?: number } opts?: { limit?: number }
) => { ) {
const roleSelector = `roles.${appId}` const roleSelector = `roles.${appId}`
let orQuery: any[] = [ let orQuery: any[] = [
@ -205,7 +218,7 @@ export const searchGlobalUsersByAppAccess = async (
return resp.rows return resp.rows
} }
export const getGlobalUserByAppPage = (appId: string, user: User) => { export function getGlobalUserByAppPage(appId: string, user: User) {
if (!user) { if (!user) {
return return
} }
@ -215,11 +228,11 @@ export const getGlobalUserByAppPage = (appId: string, user: User) => {
/** /**
* Performs a starts with search on the global email view. * Performs a starts with search on the global email view.
*/ */
export const searchGlobalUsersByEmail = async ( export async function searchGlobalUsersByEmail(
email: string | unknown, email: string | unknown,
opts: any, opts: any,
getOpts?: GetOpts getOpts?: GetOpts
) => { ) {
if (typeof email !== "string") { if (typeof email !== "string") {
throw new Error("Must provide a string to search by") throw new Error("Must provide a string to search by")
} }
@ -242,12 +255,12 @@ export const searchGlobalUsersByEmail = async (
} }
const PAGE_LIMIT = 8 const PAGE_LIMIT = 8
export const paginatedUsers = async ({ export async function paginatedUsers({
bookmark, bookmark,
query, query,
appId, appId,
limit, limit,
}: SearchUsersRequest = {}) => { }: SearchUsersRequest = {}) {
const db = getGlobalDB() const db = getGlobalDB()
const pageSize = limit ?? PAGE_LIMIT const pageSize = limit ?? PAGE_LIMIT
const pageLimit = pageSize + 1 const pageLimit = pageSize + 1
@ -324,3 +337,20 @@ export function cleanseUserObject(user: User | ContextUser, base?: User) {
} }
return user return user
} }
export async function addAppBuilder(user: User, appId: string) {
const prodAppId = getProdAppID(appId)
user.builder ??= {}
user.builder.creator = true
user.builder.apps ??= []
user.builder.apps.push(prodAppId)
await UserDB.save(user, { hashPassword: false })
}
export async function removeAppBuilder(user: User, appId: string) {
const prodAppId = getProdAppID(appId)
if (user.builder && user.builder.apps?.includes(prodAppId)) {
user.builder.apps = user.builder.apps.filter(id => id !== prodAppId)
}
await UserDB.save(user, { hashPassword: false })
}

View File

@ -1,4 +1,5 @@
import env from "../environment" import env from "../environment"
export * from "../docIds/newid" export * from "../docIds/newid"
const bcrypt = env.JS_BCRYPT ? require("bcryptjs") : require("bcrypt") const bcrypt = env.JS_BCRYPT ? require("bcryptjs") : require("bcrypt")

View File

@ -11,6 +11,7 @@ import {
TenantResolutionStrategy, TenantResolutionStrategy,
} from "@budibase/types" } from "@budibase/types"
import type { SetOption } from "cookies" import type { SetOption } from "cookies"
const jwt = require("jsonwebtoken") const jwt = require("jsonwebtoken")
const APP_PREFIX = DocumentType.APP + SEPARATOR const APP_PREFIX = DocumentType.APP + SEPARATOR

View File

@ -1,5 +1,5 @@
const _ = require('lodash/fp') const _ = require("lodash/fp")
const {structures} = require("../../../tests") const { structures } = require("../../../tests")
jest.mock("../../../src/context") jest.mock("../../../src/context")
jest.mock("../../../src/db") jest.mock("../../../src/db")
@ -7,10 +7,9 @@ jest.mock("../../../src/db")
const context = require("../../../src/context") const context = require("../../../src/context")
const db = require("../../../src/db") const db = require("../../../src/db")
const {getCreatorCount} = require('../../../src/users/users') const { getCreatorCount } = require("../../../src/users/users")
describe("Users", () => { describe("Users", () => {
let getGlobalDBMock let getGlobalDBMock
let getGlobalUserParamsMock let getGlobalUserParamsMock
let paginationMock let paginationMock
@ -26,26 +25,26 @@ describe("Users", () => {
it("Retrieves the number of creators", async () => { it("Retrieves the number of creators", async () => {
const getUsers = (offset, limit, creators = false) => { const getUsers = (offset, limit, creators = false) => {
const range = _.range(offset, limit) const range = _.range(offset, limit)
const opts = creators ? {builder: {global: true}} : undefined const opts = creators ? { builder: { global: true } } : undefined
return range.map(() => structures.users.user(opts)) return range.map(() => structures.users.user(opts))
} }
const page1Data = getUsers(0, 8) const page1Data = getUsers(0, 8)
const page2Data = getUsers(8, 12, true) const page2Data = getUsers(8, 12, true)
getGlobalDBMock.mockImplementation(() => ({ getGlobalDBMock.mockImplementation(() => ({
name : "fake-db", name: "fake-db",
allDocs: () => ({ allDocs: () => ({
rows: [...page1Data, ...page2Data] rows: [...page1Data, ...page2Data],
}) }),
})) }))
paginationMock.mockImplementationOnce(() => ({ paginationMock.mockImplementationOnce(() => ({
data: page1Data, data: page1Data,
hasNextPage: true, hasNextPage: true,
nextPage: "1" nextPage: "1",
})) }))
paginationMock.mockImplementation(() => ({ paginationMock.mockImplementation(() => ({
data: page2Data, data: page2Data,
hasNextPage: false, hasNextPage: false,
nextPage: undefined nextPage: undefined,
})) }))
const creatorsCount = await getCreatorCount() const creatorsCount = await getCreatorCount()
expect(creatorsCount).toBe(4) expect(creatorsCount).toBe(4)

View File

@ -1,3 +1,4 @@
jest.mock("../../../../src/logging/alerts") jest.mock("../../../../src/logging/alerts")
import * as _alerts from "../../../../src/logging/alerts" import * as _alerts from "../../../../src/logging/alerts"
export const alerts = jest.mocked(_alerts) export const alerts = jest.mocked(_alerts)

View File

@ -1,5 +1,6 @@
jest.mock("../../../../src/accounts") jest.mock("../../../../src/accounts")
import * as _accounts from "../../../../src/accounts" import * as _accounts from "../../../../src/accounts"
export const accounts = jest.mocked(_accounts) export const accounts = jest.mocked(_accounts)
export * as date from "./date" export * as date from "./date"

View File

@ -1,2 +1,3 @@
import Chance from "./Chance" import Chance from "./Chance"
export const generator = new Chance() export const generator = new Chance()

View File

@ -9,6 +9,7 @@ mocks.fetch.enable()
// mock all dates to 2020-01-01T00:00:00.000Z // mock all dates to 2020-01-01T00:00:00.000Z
// use tk.reset() to use real dates in individual tests // use tk.reset() to use real dates in individual tests
import tk from "timekeeper" import tk from "timekeeper"
tk.freeze(mocks.date.MOCK_DATE) tk.freeze(mocks.date.MOCK_DATE)
if (!process.env.DEBUG) { if (!process.env.DEBUG) {

View File

@ -1,5 +1,6 @@
<script> <script>
import "@spectrum-css/actiongroup/dist/index-vars.css" import "@spectrum-css/actiongroup/dist/index-vars.css"
export let vertical = false export let vertical = false
export let justified = false export let justified = false
export let quiet = false export let quiet = false

View File

@ -1,5 +1,6 @@
<script> <script>
import "@spectrum-css/avatar/dist/index-vars.css" import "@spectrum-css/avatar/dist/index-vars.css"
let sizes = new Map([ let sizes = new Map([
["XXS", "--spectrum-alias-avatar-size-50"], ["XXS", "--spectrum-alias-avatar-size-50"],
["XS", "--spectrum-alias-avatar-size-75"], ["XS", "--spectrum-alias-avatar-size-75"],

View File

@ -1,7 +1,8 @@
<script> <script>
import "@spectrum-css/buttongroup/dist/index-vars.css" import "@spectrum-css/buttongroup/dist/index-vars.css"
export let vertical = false export let vertical = false
export let gap = "" export let gap = "M"
$: gapStyle = $: gapStyle =
gap === "L" gap === "L"

View File

@ -1,5 +1,6 @@
<script> <script>
import "@spectrum-css/divider/dist/index-vars.css" import "@spectrum-css/divider/dist/index-vars.css"
export let size = "M" export let size = "M"
export let vertical = false export let vertical = false

View File

@ -3,8 +3,7 @@
import Button from "../Button/Button.svelte" import Button from "../Button/Button.svelte"
import Body from "../Typography/Body.svelte" import Body from "../Typography/Body.svelte"
import Heading from "../Typography/Heading.svelte" import Heading from "../Typography/Heading.svelte"
import { setContext } from "svelte" import { setContext, createEventDispatcher } from "svelte"
import { createEventDispatcher } from "svelte"
import { generate } from "shortid" import { generate } from "shortid"
export let title export let title

View File

@ -12,11 +12,13 @@
export let error = null export let error = null
export let validate = null export let validate = null
export let options = [] export let options = []
export let footer = null
export let isOptionEnabled = () => true export let isOptionEnabled = () => true
export let getOptionLabel = option => extractProperty(option, "label") export let getOptionLabel = option => extractProperty(option, "label")
export let getOptionValue = option => extractProperty(option, "value") export let getOptionValue = option => extractProperty(option, "value")
export let getOptionSubtitle = option => extractProperty(option, "subtitle") export let getOptionSubtitle = option => extractProperty(option, "subtitle")
export let getOptionColour = () => null export let getOptionColour = () => null
const dispatch = createEventDispatcher() const dispatch = createEventDispatcher()
let open = false let open = false
@ -100,6 +102,7 @@
{error} {error}
{disabled} {disabled}
{options} {options}
{footer}
{getOptionLabel} {getOptionLabel}
{getOptionValue} {getOptionValue}
{getOptionSubtitle} {getOptionSubtitle}

View File

@ -10,6 +10,7 @@
export let disabled = false export let disabled = false
export let error = null export let error = null
export let size = "M" export let size = "M"
export let helpText = null
const dispatch = createEventDispatcher() const dispatch = createEventDispatcher()
const onChange = e => { const onChange = e => {
@ -18,6 +19,6 @@
} }
</script> </script>
<Field {label} {labelPosition} {error}> <Field {helpText} {label} {labelPosition} {error}>
<Checkbox {error} {disabled} {text} {value} {size} on:change={onChange} /> <Checkbox {error} {disabled} {text} {value} {size} on:change={onChange} />
</Field> </Field>

View File

@ -11,6 +11,7 @@
export let error = null export let error = null
export let placeholder = "Choose an option or type" export let placeholder = "Choose an option or type"
export let options = [] export let options = []
export let helpText = null
export let getOptionLabel = option => extractProperty(option, "label") export let getOptionLabel = option => extractProperty(option, "label")
export let getOptionValue = option => extractProperty(option, "value") export let getOptionValue = option => extractProperty(option, "value")
@ -27,7 +28,7 @@
} }
</script> </script>
<Field {label} {labelPosition} {error}> <Field {helpText} {label} {labelPosition} {error}>
<Combobox <Combobox
{error} {error}
{disabled} {disabled}

View File

@ -4,7 +4,6 @@
import { createEventDispatcher } from "svelte" import { createEventDispatcher } from "svelte"
export let value = false export let value = false
export let error = null
export let id = null export let id = null
export let text = null export let text = null
export let disabled = false export let disabled = false
@ -22,7 +21,6 @@
<label <label
class="spectrum-Checkbox spectrum-Checkbox--emphasized {sizeClass}" class="spectrum-Checkbox spectrum-Checkbox--emphasized {sizeClass}"
class:is-invalid={!!error}
class:checked={value} class:checked={value}
class:is-indeterminate={indeterminate} class:is-indeterminate={indeterminate}
class:readonly class:readonly

View File

@ -6,7 +6,6 @@
export let direction = "vertical" export let direction = "vertical"
export let value = [] export let value = []
export let options = [] export let options = []
export let error = null
export let disabled = false export let disabled = false
export let readonly = false export let readonly = false
export let getOptionLabel = option => option export let getOptionLabel = option => option
@ -34,7 +33,6 @@
<div <div
title={getOptionLabel(option)} title={getOptionLabel(option)}
class="spectrum-Checkbox spectrum-FieldGroup-item" class="spectrum-Checkbox spectrum-FieldGroup-item"
class:is-invalid={!!error}
class:readonly class:readonly
> >
<label <label

View File

@ -10,7 +10,6 @@
export let placeholder = "Choose an option or type" export let placeholder = "Choose an option or type"
export let disabled = false export let disabled = false
export let readonly = false export let readonly = false
export let error = null
export let options = [] export let options = []
export let getOptionLabel = option => option export let getOptionLabel = option => option
export let getOptionValue = option => option export let getOptionValue = option => option
@ -39,12 +38,10 @@
<div <div
class="spectrum-InputGroup" class="spectrum-InputGroup"
class:is-focused={open || focus} class:is-focused={open || focus}
class:is-invalid={!!error}
class:is-disabled={disabled} class:is-disabled={disabled}
> >
<div <div
class="spectrum-Textfield spectrum-InputGroup-textfield" class="spectrum-Textfield spectrum-InputGroup-textfield"
class:is-invalid={!!error}
class:is-disabled={disabled} class:is-disabled={disabled}
class:is-focused={open || focus} class:is-focused={open || focus}
> >

View File

@ -10,7 +10,6 @@
export let id = null export let id = null
export let disabled = false export let disabled = false
export let readonly = false export let readonly = false
export let error = null
export let enableTime = true export let enableTime = true
export let value = null export let value = null
export let placeholder = null export let placeholder = null
@ -188,7 +187,6 @@
<div <div
id={flatpickrId} id={flatpickrId}
class:is-disabled={disabled || readonly} class:is-disabled={disabled || readonly}
class:is-invalid={!!error}
class="flatpickr spectrum-InputGroup spectrum-Datepicker" class="flatpickr spectrum-InputGroup spectrum-Datepicker"
class:is-focused={open} class:is-focused={open}
aria-readonly="false" aria-readonly="false"
@ -199,17 +197,7 @@
on:click={flatpickr?.open} on:click={flatpickr?.open}
class="spectrum-Textfield spectrum-InputGroup-textfield" class="spectrum-Textfield spectrum-InputGroup-textfield"
class:is-disabled={disabled} class:is-disabled={disabled}
class:is-invalid={!!error}
> >
{#if !!error}
<svg
class="spectrum-Icon spectrum-Icon--sizeM spectrum-Textfield-validationIcon"
focusable="false"
aria-hidden="true"
>
<use xlink:href="#spectrum-icon-18-Alert" />
</svg>
{/if}
<input <input
{disabled} {disabled}
{readonly} {readonly}
@ -227,7 +215,6 @@
class="spectrum-Picker spectrum-Picker--sizeM spectrum-InputGroup-button" class="spectrum-Picker spectrum-Picker--sizeM spectrum-InputGroup-button"
tabindex="-1" tabindex="-1"
class:is-disabled={disabled} class:is-disabled={disabled}
class:is-invalid={!!error}
on:click={flatpickr?.open} on:click={flatpickr?.open}
> >
<svg <svg

View File

@ -22,7 +22,6 @@
export let handleFileTooLarge = null export let handleFileTooLarge = null
export let handleTooManyFiles = null export let handleTooManyFiles = null
export let gallery = true export let gallery = true
export let error = null
export let fileTags = [] export let fileTags = []
export let maximum = null export let maximum = null
export let extensions = "*" export let extensions = "*"
@ -222,7 +221,6 @@
{#if showDropzone} {#if showDropzone}
<div <div
class="spectrum-Dropzone" class="spectrum-Dropzone"
class:is-invalid={!!error}
class:disabled class:disabled
role="region" role="region"
tabindex="0" tabindex="0"
@ -351,9 +349,6 @@
.spectrum-Dropzone { .spectrum-Dropzone {
user-select: none; user-select: none;
} }
.spectrum-Dropzone.is-invalid {
border-color: var(--spectrum-global-color-red-400);
}
input[type="file"] { input[type="file"] {
display: none; display: none;
} }

View File

@ -14,11 +14,10 @@
export let disabled = false export let disabled = false
export let readonly = false export let readonly = false
export let updateOnChange = true export let updateOnChange = true
export let error = null
export let options = [] export let options = []
export let getOptionLabel = option => extractProperty(option, "label") export let getOptionLabel = option => extractProperty(option, "label")
export let getOptionValue = option => extractProperty(option, "value") export let getOptionValue = option => extractProperty(option, "value")
export let getOptionSubtitle = option => option?.subtitle
export let isOptionSelected = () => false export let isOptionSelected = () => false
const dispatch = createEventDispatcher() const dispatch = createEventDispatcher()
@ -111,27 +110,12 @@
} }
</script> </script>
<div <div class="spectrum-InputGroup" class:is-disabled={disabled}>
class="spectrum-InputGroup"
class:is-invalid={!!error}
class:is-disabled={disabled}
>
<div <div
class="spectrum-Textfield spectrum-InputGroup-textfield" class="spectrum-Textfield spectrum-InputGroup-textfield"
class:is-invalid={!!error}
class:is-disabled={disabled} class:is-disabled={disabled}
class:is-focused={focus} class:is-focused={focus}
> >
{#if error}
<svg
class="spectrum-Icon spectrum-Icon--sizeM spectrum-Textfield-validationIcon"
focusable="false"
aria-hidden="true"
>
<use xlink:href="#spectrum-icon-18-Alert" />
</svg>
{/if}
<input <input
{id} {id}
on:click on:click
@ -151,7 +135,7 @@
class="spectrum-Textfield-input spectrum-InputGroup-input" class="spectrum-Textfield-input spectrum-InputGroup-input"
/> />
</div> </div>
<div style="width: 30%"> <div style="width: 40%">
<button <button
{id} {id}
class="spectrum-Picker spectrum-Picker--sizeM override-borders" class="spectrum-Picker spectrum-Picker--sizeM override-borders"
@ -173,38 +157,43 @@
<use xlink:href="#spectrum-css-icon-Chevron100" /> <use xlink:href="#spectrum-css-icon-Chevron100" />
</svg> </svg>
</button> </button>
{#if open}
<div
use:clickOutside={handleOutsideClick}
transition:fly|local={{ y: -20, duration: 200 }}
class="spectrum-Popover spectrum-Popover--bottom spectrum-Picker-popover is-open"
>
<ul class="spectrum-Menu" role="listbox">
{#each options as option, idx}
<li
class="spectrum-Menu-item"
class:is-selected={isOptionSelected(getOptionValue(option, idx))}
role="option"
aria-selected="true"
tabindex="0"
on:click={() => onPick(getOptionValue(option, idx))}
>
<span class="spectrum-Menu-itemLabel">
{getOptionLabel(option, idx)}
</span>
<svg
class="spectrum-Icon spectrum-UIIcon-Checkmark100 spectrum-Menu-checkmark spectrum-Menu-itemIcon"
focusable="false"
aria-hidden="true"
>
<use xlink:href="#spectrum-css-icon-Checkmark100" />
</svg>
</li>
{/each}
</ul>
</div>
{/if}
</div> </div>
{#if open}
<div
use:clickOutside={handleOutsideClick}
transition:fly|local={{ y: -20, duration: 200 }}
class="spectrum-Popover spectrum-Popover--bottom spectrum-Picker-popover is-open"
>
<ul class="spectrum-Menu" role="listbox">
{#each options as option, idx}
<li
class="spectrum-Menu-item"
class:is-selected={isOptionSelected(getOptionValue(option, idx))}
role="option"
aria-selected="true"
tabindex="0"
on:click={() => onPick(getOptionValue(option, idx))}
>
<span class="spectrum-Menu-itemLabel">
{getOptionLabel(option, idx)}
{#if getOptionSubtitle(option, idx)}
<span class="subtitle-text">
{getOptionSubtitle(option, idx)}
</span>
{/if}
</span>
<svg
class="spectrum-Icon spectrum-UIIcon-Checkmark100 spectrum-Menu-checkmark spectrum-Menu-itemIcon"
focusable="false"
aria-hidden="true"
>
<use xlink:href="#spectrum-css-icon-Checkmark100" />
</svg>
</li>
{/each}
</ul>
</div>
{/if}
</div> </div>
<style> <style>
@ -212,7 +201,6 @@
min-width: 0; min-width: 0;
width: 100%; width: 100%;
} }
.spectrum-InputGroup-input { .spectrum-InputGroup-input {
border-right-width: 1px; border-right-width: 1px;
} }
@ -222,7 +210,6 @@
.spectrum-Textfield-input { .spectrum-Textfield-input {
width: 0; width: 0;
} }
.override-borders { .override-borders {
border-top-left-radius: 0px; border-top-left-radius: 0px;
border-bottom-left-radius: 0px; border-bottom-left-radius: 0px;
@ -231,5 +218,18 @@
max-height: 240px; max-height: 240px;
z-index: 999; z-index: 999;
top: 100%; top: 100%;
width: 100%;
}
.subtitle-text {
font-size: 12px;
line-height: 15px;
font-weight: 500;
color: var(--spectrum-global-color-gray-600);
display: block;
margin-top: var(--spacing-s);
}
.spectrum-Menu-checkmark {
align-self: center;
margin-top: 0;
} }
</style> </style>

View File

@ -6,7 +6,6 @@
export let id = null export let id = null
export let placeholder = null export let placeholder = null
export let disabled = false export let disabled = false
export let error = null
export let options = [] export let options = []
export let getOptionLabel = option => option export let getOptionLabel = option => option
export let getOptionValue = option => option export let getOptionValue = option => option
@ -84,7 +83,6 @@
<Picker <Picker
on:loadMore on:loadMore
{id} {id}
{error}
{disabled} {disabled}
{readonly} {readonly}
{fieldText} {fieldText}

View File

@ -14,7 +14,6 @@
export let id = null export let id = null
export let disabled = false export let disabled = false
export let error = null
export let fieldText = "" export let fieldText = ""
export let fieldIcon = "" export let fieldIcon = ""
export let fieldColour = "" export let fieldColour = ""
@ -113,7 +112,6 @@
class="spectrum-Picker spectrum-Picker--sizeM" class="spectrum-Picker spectrum-Picker--sizeM"
class:spectrum-Picker--quiet={quiet} class:spectrum-Picker--quiet={quiet}
{disabled} {disabled}
class:is-invalid={!!error}
class:is-open={open} class:is-open={open}
aria-haspopup="listbox" aria-haspopup="listbox"
on:click={onClick} on:click={onClick}
@ -142,16 +140,6 @@
> >
{fieldText} {fieldText}
</span> </span>
{#if error}
<svg
class="spectrum-Icon spectrum-Icon--sizeM spectrum-Picker-validationIcon"
focusable="false"
aria-hidden="true"
aria-label="Folder"
>
<use xlink:href="#spectrum-icon-18-Alert" />
</svg>
{/if}
<svg <svg
class="spectrum-Icon spectrum-UIIcon-ChevronDown100 spectrum-Picker-menuIcon" class="spectrum-Icon spectrum-UIIcon-ChevronDown100 spectrum-Picker-menuIcon"
focusable="false" focusable="false"
@ -236,13 +224,12 @@
</span> </span>
{/if} {/if}
<span class="spectrum-Menu-itemLabel"> <span class="spectrum-Menu-itemLabel">
{#if getOptionSubtitle(option, idx)}
<span class="subtitle-text"
>{getOptionSubtitle(option, idx)}</span
>
{/if}
{getOptionLabel(option, idx)} {getOptionLabel(option, idx)}
{#if getOptionSubtitle(option, idx)}
<span class="subtitle-text">
{getOptionSubtitle(option, idx)}
</span>
{/if}
</span> </span>
{#if option.tag} {#if option.tag}
<span class="option-tag"> <span class="option-tag">
@ -287,10 +274,9 @@
font-size: 12px; font-size: 12px;
line-height: 15px; line-height: 15px;
font-weight: 500; font-weight: 500;
top: 10px;
color: var(--spectrum-global-color-gray-600); color: var(--spectrum-global-color-gray-600);
display: block; display: block;
margin-bottom: var(--spacing-s); margin-top: var(--spacing-s);
} }
.spectrum-Picker-label.auto-width { .spectrum-Picker-label.auto-width {

View File

@ -16,7 +16,6 @@
export let id = null export let id = null
export let placeholder = "Choose an option or type" export let placeholder = "Choose an option or type"
export let disabled = false export let disabled = false
export let error = null
export let secondaryOptions = [] export let secondaryOptions = []
export let primaryOptions = [] export let primaryOptions = []
export let secondaryFieldText = "" export let secondaryFieldText = ""
@ -105,14 +104,9 @@
} }
</script> </script>
<div <div class="spectrum-InputGroup" class:is-disabled={disabled}>
class="spectrum-InputGroup"
class:is-invalid={!!error}
class:is-disabled={disabled}
>
<div <div
class="spectrum-Textfield spectrum-InputGroup-textfield" class="spectrum-Textfield spectrum-InputGroup-textfield"
class:is-invalid={!!error}
class:is-disabled={disabled} class:is-disabled={disabled}
class:is-focused={focus} class:is-focused={focus}
class:is-full-width={!secondaryOptions.length} class:is-full-width={!secondaryOptions.length}

View File

@ -6,7 +6,6 @@
export let direction = "vertical" export let direction = "vertical"
export let value = null export let value = null
export let options = [] export let options = []
export let error = null
export let disabled = false export let disabled = false
export let readonly = false export let readonly = false
export let getOptionLabel = option => option export let getOptionLabel = option => option
@ -40,7 +39,6 @@
<div <div
title={getOptionTitle(option)} title={getOptionTitle(option)}
class="spectrum-Radio spectrum-FieldGroup-item spectrum-Radio--emphasized" class="spectrum-Radio spectrum-FieldGroup-item spectrum-Radio--emphasized"
class:is-invalid={!!error}
class:readonly class:readonly
> >
<input <input

View File

@ -5,14 +5,13 @@
export let placeholder = null export let placeholder = null
export let disabled = false export let disabled = false
export let readonly = false export let readonly = false
export let error = null
export let height = null export let height = null
export let id = null export let id = null
export let fullScreenOffset = null export let fullScreenOffset = null
export let easyMDEOptions = null export let easyMDEOptions = null
</script> </script>
<div class:error> <div>
<MarkdownEditor <MarkdownEditor
{value} {value}
{placeholder} {placeholder}
@ -27,18 +26,4 @@
</div> </div>
<style> <style>
.error :global(.EasyMDEContainer .editor-toolbar) {
border-top-color: var(--spectrum-semantic-negative-color-default);
border-left-color: var(--spectrum-semantic-negative-color-default);
border-right-color: var(--spectrum-semantic-negative-color-default);
}
.error :global(.EasyMDEContainer .CodeMirror) {
border-bottom-color: var(--spectrum-semantic-negative-color-default);
border-left-color: var(--spectrum-semantic-negative-color-default);
border-right-color: var(--spectrum-semantic-negative-color-default);
}
.error :global(.EasyMDEContainer .editor-preview-side) {
border-bottom-color: var(--spectrum-semantic-negative-color-default);
border-right-color: var(--spectrum-semantic-negative-color-default);
}
</style> </style>

View File

@ -6,13 +6,13 @@
export let id = null export let id = null
export let placeholder = "Choose an option" export let placeholder = "Choose an option"
export let disabled = false export let disabled = false
export let error = null
export let options = [] export let options = []
export let getOptionLabel = option => option export let getOptionLabel = option => option
export let getOptionValue = option => option export let getOptionValue = option => option
export let getOptionIcon = () => null export let getOptionIcon = () => null
export let useOptionIconImage = false
export let getOptionColour = () => null export let getOptionColour = () => null
export let getOptionSubtitle = () => null
export let useOptionIconImage = false
export let isOptionEnabled export let isOptionEnabled
export let readonly = false export let readonly = false
export let quiet = false export let quiet = false
@ -71,7 +71,6 @@
on:loadMore on:loadMore
{quiet} {quiet}
{id} {id}
{error}
{disabled} {disabled}
{readonly} {readonly}
{fieldText} {fieldText}
@ -84,8 +83,9 @@
{getOptionLabel} {getOptionLabel}
{getOptionValue} {getOptionValue}
{getOptionIcon} {getOptionIcon}
{useOptionIconImage}
{getOptionColour} {getOptionColour}
{getOptionSubtitle}
{useOptionIconImage}
{isOptionEnabled} {isOptionEnabled}
{autocomplete} {autocomplete}
{sort} {sort}

View File

@ -7,7 +7,6 @@
export let value = null export let value = null
export let placeholder = null export let placeholder = null
export let disabled = false export let disabled = false
export let error = null
export let id = null export let id = null
export let readonly = false export let readonly = false
export let updateOnChange = true export let updateOnChange = true
@ -98,20 +97,9 @@
<div <div
class="spectrum-Stepper" class="spectrum-Stepper"
class:spectrum-Stepper--quiet={quiet} class:spectrum-Stepper--quiet={quiet}
class:is-invalid={!!error}
class:is-disabled={disabled} class:is-disabled={disabled}
class:is-focused={focus} class:is-focused={focus}
> >
{#if error}
<svg
class="spectrum-Icon spectrum-Icon--sizeM spectrum-Textfield-validationIcon"
focusable="false"
aria-hidden="true"
>
<use xlink:href="#spectrum-icon-18-Alert" />
</svg>
{/if}
<div class="spectrum-Textfield spectrum-Stepper-textfield"> <div class="spectrum-Textfield spectrum-Stepper-textfield">
<input <input
{disabled} {disabled}

View File

@ -6,7 +6,6 @@
export let placeholder = null export let placeholder = null
export let disabled = false export let disabled = false
export let readonly = false export let readonly = false
export let error = null
export let id = null export let id = null
export let height = null export let height = null
export let minHeight = null export let minHeight = null
@ -41,20 +40,9 @@
<div <div
style={`${heightString}${minHeightString}`} style={`${heightString}${minHeightString}`}
class="spectrum-Textfield spectrum-Textfield--multiline" class="spectrum-Textfield spectrum-Textfield--multiline"
class:is-invalid={!!error}
class:is-disabled={disabled} class:is-disabled={disabled}
class:is-focused={focus} class:is-focused={focus}
> >
{#if error}
<svg
class="spectrum-Icon spectrum-Icon--sizeM
spectrum-Textfield-validationIcon"
focusable="false"
aria-hidden="true"
>
<use xlink:href="#spectrum-icon-18-Alert" />
</svg>
{/if}
<!-- prettier-ignore --> <!-- prettier-ignore -->
<textarea <textarea
bind:this={textarea} bind:this={textarea}

View File

@ -6,7 +6,6 @@
export let placeholder = null export let placeholder = null
export let type = "text" export let type = "text"
export let disabled = false export let disabled = false
export let error = null
export let id = null export let id = null
export let readonly = false export let readonly = false
export let updateOnChange = true export let updateOnChange = true
@ -78,19 +77,9 @@
<div <div
class="spectrum-Textfield" class="spectrum-Textfield"
class:spectrum-Textfield--quiet={quiet} class:spectrum-Textfield--quiet={quiet}
class:is-invalid={!!error}
class:is-disabled={disabled} class:is-disabled={disabled}
class:is-focused={focus} class:is-focused={focus}
> >
{#if error}
<svg
class="spectrum-Icon spectrum-Icon--sizeM spectrum-Textfield-validationIcon"
focusable="false"
aria-hidden="true"
>
<use xlink:href="#spectrum-icon-18-Alert" />
</svg>
{/if}
<input <input
bind:this={field} bind:this={field}
{disabled} {disabled}

View File

@ -16,6 +16,7 @@
export let appendTo = undefined export let appendTo = undefined
export let ignoreTimezones = false export let ignoreTimezones = false
export let range = false export let range = false
export let helpText = null
const dispatch = createEventDispatcher() const dispatch = createEventDispatcher()
const onChange = e => { const onChange = e => {
@ -30,7 +31,7 @@
} }
</script> </script>
<Field {label} {labelPosition} {error}> <Field {helpText} {label} {labelPosition} {error}>
<DatePicker <DatePicker
{error} {error}
{disabled} {disabled}

View File

@ -17,6 +17,7 @@
export let fileTags = [] export let fileTags = []
export let maximum = undefined export let maximum = undefined
export let compact = false export let compact = false
export let helpText = null
const dispatch = createEventDispatcher() const dispatch = createEventDispatcher()
const onChange = e => { const onChange = e => {
@ -25,7 +26,7 @@
} }
</script> </script>
<Field {label} {labelPosition} {error}> <Field {helpText} {label} {labelPosition} {error}>
<CoreDropzone <CoreDropzone
{error} {error}
{disabled} {disabled}

View File

@ -16,6 +16,7 @@
export let autofocus export let autofocus
export let variables export let variables
export let showModal export let showModal
export let helpText = null
export let environmentVariablesEnabled export let environmentVariablesEnabled
export let handleUpgradePanel export let handleUpgradePanel
const dispatch = createEventDispatcher() const dispatch = createEventDispatcher()
@ -25,7 +26,7 @@
} }
</script> </script>
<Field {label} {labelPosition} {error}> <Field {helpText} {label} {labelPosition} {error}>
<EnvDropdown <EnvDropdown
{updateOnChange} {updateOnChange}
{error} {error}

View File

@ -1,11 +1,13 @@
<script> <script>
import "@spectrum-css/fieldlabel/dist/index-vars.css" import "@spectrum-css/fieldlabel/dist/index-vars.css"
import FieldLabel from "./FieldLabel.svelte" import FieldLabel from "./FieldLabel.svelte"
import Icon from "../Icon/Icon.svelte"
export let id = null export let id = null
export let label = null export let label = null
export let labelPosition = "above" export let labelPosition = "above"
export let error = null export let error = null
export let helpText = null
export let tooltip = "" export let tooltip = ""
</script> </script>
@ -17,6 +19,10 @@
<slot /> <slot />
{#if error} {#if error}
<div class="error">{error}</div> <div class="error">{error}</div>
{:else if helpText}
<div class="helpText">
<Icon name="HelpOutline" /> <span>{helpText}</span>
</div>
{/if} {/if}
</div> </div>
</div> </div>
@ -39,4 +45,21 @@
font-size: var(--spectrum-global-dimension-font-size-75); font-size: var(--spectrum-global-dimension-font-size-75);
margin-top: var(--spectrum-global-dimension-size-75); margin-top: var(--spectrum-global-dimension-size-75);
} }
.helpText {
display: flex;
margin-top: var(--spectrum-global-dimension-size-75);
align-items: center;
}
.helpText :global(svg) {
width: 14px;
color: var(--grey-5);
margin-right: 6px;
}
.helpText span {
color: var(--grey-7);
font-size: var(--spectrum-global-dimension-font-size-75);
}
</style> </style>

View File

@ -14,6 +14,7 @@
export let title = null export let title = null
export let value = null export let value = null
export let tooltip = null export let tooltip = null
export let helpText = null
const dispatch = createEventDispatcher() const dispatch = createEventDispatcher()
const onChange = e => { const onChange = e => {
@ -22,7 +23,7 @@
} }
</script> </script>
<Field {label} {labelPosition} {error} {tooltip}> <Field {helpText} {label} {labelPosition} {error} {tooltip}>
<CoreFile <CoreFile
{error} {error}
{disabled} {disabled}

View File

@ -15,6 +15,7 @@
export let quiet = false export let quiet = false
export let autofocus export let autofocus
export let autocomplete export let autocomplete
export let helpText = null
const dispatch = createEventDispatcher() const dispatch = createEventDispatcher()
const onChange = e => { const onChange = e => {
@ -23,7 +24,7 @@
} }
</script> </script>
<Field {label} {labelPosition} {error}> <Field {helpText} {label} {labelPosition} {error}>
<TextField <TextField
{updateOnChange} {updateOnChange}
{error} {error}

View File

@ -15,6 +15,7 @@
export let updateOnChange = true export let updateOnChange = true
export let quiet = false export let quiet = false
export let autofocus export let autofocus
export let helpText = null
export let options = [] export let options = []
const dispatch = createEventDispatcher() const dispatch = createEventDispatcher()
@ -29,7 +30,7 @@
} }
</script> </script>
<Field {label} {labelPosition} {error}> <Field {helpText} {label} {labelPosition} {error}>
<InputDropdown <InputDropdown
{updateOnChange} {updateOnChange}
{error} {error}
@ -42,6 +43,7 @@
{quiet} {quiet}
{autofocus} {autofocus}
{options} {options}
isOptionSelected={option => option === dropdownValue}
on:change={onChange} on:change={onChange}
on:pick={onPick} on:pick={onPick}
on:click on:click

View File

@ -18,6 +18,7 @@
export let autocomplete = false export let autocomplete = false
export let searchTerm = null export let searchTerm = null
export let customPopoverHeight export let customPopoverHeight
export let helpText = null
const dispatch = createEventDispatcher() const dispatch = createEventDispatcher()
const onChange = e => { const onChange = e => {
@ -26,7 +27,7 @@
} }
</script> </script>
<Field {label} {labelPosition} {error}> <Field {helpText} {label} {labelPosition} {error}>
<Multiselect <Multiselect
{error} {error}
{disabled} {disabled}

View File

@ -26,6 +26,7 @@
export let secondaryOptions = [] export let secondaryOptions = []
export let searchTerm export let searchTerm
export let showClearIcon = true export let showClearIcon = true
export let helpText = null
let primaryLabel let primaryLabel
let secondaryLabel let secondaryLabel
@ -93,7 +94,7 @@
} }
</script> </script>
<Field {label} {labelPosition} {error}> <Field {helpText} {label} {labelPosition} {error}>
<PickerDropdown <PickerDropdown
{searchTerm} {searchTerm}
{autocomplete} {autocomplete}

View File

@ -13,6 +13,7 @@
export let getOptionLabel = option => extractProperty(option, "label") export let getOptionLabel = option => extractProperty(option, "label")
export let getOptionValue = option => extractProperty(option, "value") export let getOptionValue = option => extractProperty(option, "value")
export let getOptionTitle = option => extractProperty(option, "label") export let getOptionTitle = option => extractProperty(option, "label")
export let helpText = null
const dispatch = createEventDispatcher() const dispatch = createEventDispatcher()
const onChange = e => { const onChange = e => {
@ -27,7 +28,7 @@
} }
</script> </script>
<Field {label} {labelPosition} {error}> <Field {helpText} {label} {labelPosition} {error}>
<RadioGroup <RadioGroup
{error} {error}
{disabled} {disabled}

View File

@ -13,6 +13,7 @@
export let id = null export let id = null
export let fullScreenOffset = null export let fullScreenOffset = null
export let easyMDEOptions = null export let easyMDEOptions = null
export let helpText = null
const dispatch = createEventDispatcher() const dispatch = createEventDispatcher()
const onChange = e => { const onChange = e => {
@ -21,7 +22,7 @@
} }
</script> </script>
<Field {label} {labelPosition} {error}> <Field {helpText} {label} {labelPosition} {error}>
<RichTextField <RichTextField
{error} {error}
{disabled} {disabled}

View File

@ -11,6 +11,7 @@
export let updateOnChange = true export let updateOnChange = true
export let quiet = false export let quiet = false
export let inputRef export let inputRef
export let helpText = null
const dispatch = createEventDispatcher() const dispatch = createEventDispatcher()
const onChange = e => { const onChange = e => {
@ -19,7 +20,7 @@
} }
</script> </script>
<Field {label} {labelPosition}> <Field {helpText} {label} {labelPosition}>
<Search <Search
{updateOnChange} {updateOnChange}
{disabled} {disabled}

View File

@ -13,9 +13,10 @@
export let options = [] export let options = []
export let getOptionLabel = option => extractProperty(option, "label") export let getOptionLabel = option => extractProperty(option, "label")
export let getOptionValue = option => extractProperty(option, "value") export let getOptionValue = option => extractProperty(option, "value")
export let getOptionSubtitle = option => option?.subtitle
export let getOptionIcon = option => option?.icon export let getOptionIcon = option => option?.icon
export let useOptionIconImage = false
export let getOptionColour = option => option?.colour export let getOptionColour = option => option?.colour
export let useOptionIconImage = false
export let isOptionEnabled export let isOptionEnabled
export let quiet = false export let quiet = false
export let autoWidth = false export let autoWidth = false
@ -26,6 +27,7 @@
export let align export let align
export let footer = null export let footer = null
export let tag = null export let tag = null
export let helpText = null
const dispatch = createEventDispatcher() const dispatch = createEventDispatcher()
const onChange = e => { const onChange = e => {
value = e.detail value = e.detail
@ -40,7 +42,7 @@
} }
</script> </script>
<Field {label} {labelPosition} {error} {tooltip}> <Field {helpText} {label} {labelPosition} {error} {tooltip}>
<Select <Select
{quiet} {quiet}
{error} {error}
@ -57,6 +59,7 @@
{getOptionValue} {getOptionValue}
{getOptionIcon} {getOptionIcon}
{getOptionColour} {getOptionColour}
{getOptionSubtitle}
{useOptionIconImage} {useOptionIconImage}
{isOptionEnabled} {isOptionEnabled}
{autocomplete} {autocomplete}

View File

@ -11,6 +11,7 @@
export let step = 1 export let step = 1
export let disabled = false export let disabled = false
export let error = null export let error = null
export let helpText = null
const dispatch = createEventDispatcher() const dispatch = createEventDispatcher()
const onChange = e => { const onChange = e => {
@ -19,6 +20,6 @@
} }
</script> </script>
<Field {label} {labelPosition} {error}> <Field {helpText} {label} {labelPosition} {error}>
<Slider {disabled} {value} {min} {max} {step} on:change={onChange} /> <Slider {disabled} {value} {min} {max} {step} on:change={onChange} />
</Field> </Field>

View File

@ -15,6 +15,7 @@
export let min = null export let min = null
export let max = null export let max = null
export let step = 1 export let step = 1
export let helpText = null
const dispatch = createEventDispatcher() const dispatch = createEventDispatcher()
const onChange = e => { const onChange = e => {
@ -23,7 +24,7 @@
} }
</script> </script>
<Field {label} {labelPosition} {error}> <Field {helpText} {label} {labelPosition} {error}>
<Stepper <Stepper
{updateOnChange} {updateOnChange}
{error} {error}

View File

@ -12,6 +12,7 @@
export let getCaretPosition = null export let getCaretPosition = null
export let height = null export let height = null
export let minHeight = null export let minHeight = null
export let helpText = null
const dispatch = createEventDispatcher() const dispatch = createEventDispatcher()
const onChange = e => { const onChange = e => {
@ -20,7 +21,7 @@
} }
</script> </script>
<Field {label} {labelPosition} {error}> <Field {helpText} {label} {labelPosition} {error}>
<TextArea <TextArea
bind:getCaretPosition bind:getCaretPosition
{error} {error}

View File

@ -9,6 +9,7 @@
export let text = null export let text = null
export let disabled = false export let disabled = false
export let error = null export let error = null
export let helpText = null
const dispatch = createEventDispatcher() const dispatch = createEventDispatcher()
const onChange = e => { const onChange = e => {
@ -17,6 +18,6 @@
} }
</script> </script>
<Field {label} {labelPosition} {error}> <Field {helpText} {label} {labelPosition} {error}>
<Switch {error} {disabled} {text} {value} on:change={onChange} /> <Switch {error} {disabled} {text} {value} on:change={onChange} />
</Field> </Field>

View File

@ -16,10 +16,9 @@
const dispatch = createEventDispatcher() const dispatch = createEventDispatcher()
const onClick = e => { const onClick = () => {
if (!disabled) { if (!disabled) {
dispatch("click") dispatch("click")
e.stopPropagation()
} }
} }
</script> </script>

View File

@ -1,5 +1,6 @@
<script> <script>
import Input from "../Form/Input.svelte" import Input from "../Form/Input.svelte"
let value = "" let value = ""
</script> </script>

View File

@ -4,6 +4,7 @@
import Icon from "../Icon/Icon.svelte" import Icon from "../Icon/Icon.svelte"
import Popover from "../Popover/Popover.svelte" import Popover from "../Popover/Popover.svelte"
import { onMount } from "svelte" import { onMount } from "svelte"
const flipDurationMs = 150 const flipDurationMs = 150
export let constraints export let constraints

View File

@ -1,11 +1,10 @@
<script> <script>
import "@spectrum-css/popover/dist/index-vars.css" import "@spectrum-css/popover/dist/index-vars.css"
import Portal from "svelte-portal" import Portal from "svelte-portal"
import { createEventDispatcher } from "svelte" import { createEventDispatcher, getContext } from "svelte"
import positionDropdown from "../Actions/position_dropdown" import positionDropdown from "../Actions/position_dropdown"
import clickOutside from "../Actions/click_outside" import clickOutside from "../Actions/click_outside"
import { fly } from "svelte/transition" import { fly } from "svelte/transition"
import { getContext } from "svelte"
import Context from "../context" import Context from "../context"
const dispatch = createEventDispatcher() const dispatch = createEventDispatcher()

View File

@ -1,7 +1,9 @@
<script> <script>
import { getContext } from "svelte" import { getContext } from "svelte"
const multilevel = getContext("sidenav-type") const multilevel = getContext("sidenav-type")
import Badge from "../Badge/Badge.svelte" import Badge from "../Badge/Badge.svelte"
export let href = "" export let href = ""
export let external = false export let external = false
export let heading = "" export let heading = ""

View File

@ -1,6 +1,7 @@
<script> <script>
import { setContext } from "svelte" import { setContext } from "svelte"
import "@spectrum-css/sidenav/dist/index-vars.css" import "@spectrum-css/sidenav/dist/index-vars.css"
export let multilevel = false export let multilevel = false
setContext("sidenav-type", multilevel) setContext("sidenav-type", multilevel)
</script> </script>

View File

@ -1,6 +1,7 @@
<script> <script>
import "@spectrum-css/label/dist/index-vars.css" import "@spectrum-css/label/dist/index-vars.css"
import Badge from "../Badge/Badge.svelte" import Badge from "../Badge/Badge.svelte"
export let value export let value
const displayLimit = 5 const displayLimit = 5

View File

@ -1,6 +1,7 @@
<script> <script>
import { getContext, onMount, createEventDispatcher } from "svelte" import { getContext, onMount, createEventDispatcher } from "svelte"
import Portal from "svelte-portal" import Portal from "svelte-portal"
export let title export let title
export let icon = "" export let icon = ""
export let id export let id

View File

@ -1,4 +1,5 @@
import { helpers } from "@budibase/shared-core" import { helpers } from "@budibase/shared-core"
export const deepGet = helpers.deepGet export const deepGet = helpers.deepGet
/** /**

View File

@ -260,20 +260,42 @@ export const getComponentContexts = (
/** /**
* Gets all data provider components above a component. * Gets all data provider components above a component.
*/ */
export const getActionProviderComponents = (asset, componentId, actionType) => { export const getActionProviders = (
asset,
componentId,
actionType,
options = { includeSelf: false }
) => {
if (!asset) { if (!asset) {
return [] return []
} }
// Get all components
const components = findAllComponents(asset.props) const components = findAllComponents(asset.props)
return components.filter(component => {
// Ignore ourselves // Find matching contexts and generate bindings
if (componentId && component._id === componentId) { let providers = []
return false components.forEach(component => {
if (!options?.includeSelf && component._id === componentId) {
return
} }
// Find components when expose this action
const def = store.actions.components.getDefinition(component._component) const def = store.actions.components.getDefinition(component._component)
return def?.actions?.includes(actionType) const actions = (def?.actions || []).map(action => {
return typeof action === "string" ? { type: action } : action
})
const action = actions.find(x => x.type === actionType)
if (action) {
let runtimeBinding = component._id
if (action.suffix) {
runtimeBinding += `-${action.suffix}`
}
providers.push({
readableBinding: component._instanceName,
runtimeBinding,
})
}
}) })
return providers
} }
/** /**
@ -1116,17 +1138,18 @@ export const removeBindings = (obj, replacement = "Invalid binding") => {
* When converting from readable to runtime it can sometimes add too many square brackets, * When converting from readable to runtime it can sometimes add too many square brackets,
* this makes sure that doesn't happen. * this makes sure that doesn't happen.
*/ */
const shouldReplaceBinding = (currentValue, convertFrom, convertTo) => { const shouldReplaceBinding = (currentValue, from, convertTo, binding) => {
if (!currentValue?.includes(convertFrom)) { if (!currentValue?.includes(from)) {
return false return false
} }
if (convertTo === "readableBinding") { if (convertTo === "readableBinding") {
return true // Dont replace if the value already matches the readable binding
return currentValue.indexOf(binding.readableBinding) === -1
} }
// remove all the spaces, if the input is surrounded by spaces e.g. [ Auto ID ] then // remove all the spaces, if the input is surrounded by spaces e.g. [ Auto ID ] then
// this makes sure it is detected // this makes sure it is detected
const noSpaces = currentValue.replace(/\s+/g, "") const noSpaces = currentValue.replace(/\s+/g, "")
const fromNoSpaces = convertFrom.replace(/\s+/g, "") const fromNoSpaces = from.replace(/\s+/g, "")
const invalids = [ const invalids = [
`[${fromNoSpaces}]`, `[${fromNoSpaces}]`,
`"${fromNoSpaces}"`, `"${fromNoSpaces}"`,
@ -1178,8 +1201,11 @@ const bindingReplacement = (
// in the search, working from longest to shortest so always use best match first // in the search, working from longest to shortest so always use best match first
let searchString = newBoundValue let searchString = newBoundValue
for (let from of convertFromProps) { for (let from of convertFromProps) {
if (isJS || shouldReplaceBinding(newBoundValue, from, convertTo)) { const binding = bindableProperties.find(el => el[convertFrom] === from)
const binding = bindableProperties.find(el => el[convertFrom] === from) if (
isJS ||
shouldReplaceBinding(newBoundValue, from, convertTo, binding)
) {
let idx let idx
do { do {
// see if any instances of this binding exist in the search string // see if any instances of this binding exist in the search string

View File

@ -4,11 +4,10 @@ import { getTemporalStore } from "./store/temporal"
import { getThemeStore } from "./store/theme" import { getThemeStore } from "./store/theme"
import { getUserStore } from "./store/users" import { getUserStore } from "./store/users"
import { getDeploymentStore } from "./store/deployments" import { getDeploymentStore } from "./store/deployments"
import { derived, writable } from "svelte/store" import { derived, writable, get } from "svelte/store"
import { findComponent, findComponentPath } from "./componentUtils" import { findComponent, findComponentPath } from "./componentUtils"
import { RoleUtils } from "@budibase/frontend-core" import { RoleUtils } from "@budibase/frontend-core"
import { createHistoryStore } from "builderStore/store/history" import { createHistoryStore } from "builderStore/store/history"
import { get } from "svelte/store"
export const store = getFrontendStore() export const store = getFrontendStore()
export const automationStore = getAutomationStore() export const automationStore = getAutomationStore()

View File

@ -0,0 +1,86 @@
import { expect, describe, it, vi } from "vitest"
import {
runtimeToReadableBinding,
readableToRuntimeBinding,
} from "../dataBinding"
vi.mock("@budibase/frontend-core")
vi.mock("builderStore/componentUtils")
vi.mock("builderStore/store")
vi.mock("builderStore/store/theme")
vi.mock("builderStore/store/temporal")
describe("runtimeToReadableBinding", () => {
const bindableProperties = [
{
category: "Current User",
icon: "User",
providerId: "user",
readableBinding: "Current User.firstName",
runtimeBinding: "[user].[firstName]",
type: "context",
},
{
category: "Bindings",
icon: "Brackets",
readableBinding: "Binding.count",
runtimeBinding: "count",
type: "context",
},
]
it("should convert a runtime binding to a readable one", () => {
const textWithBindings = `Hello {{ [user].[firstName] }}! The count is {{ count }}.`
expect(
runtimeToReadableBinding(
bindableProperties,
textWithBindings,
"readableBinding"
)
).toEqual(
`Hello {{ Current User.firstName }}! The count is {{ Binding.count }}.`
)
})
it("should not convert to readable binding if it is already readable", () => {
const textWithBindings = `Hello {{ [user].[firstName] }}! The count is {{ Binding.count }}.`
expect(
runtimeToReadableBinding(
bindableProperties,
textWithBindings,
"readableBinding"
)
).toEqual(
`Hello {{ Current User.firstName }}! The count is {{ Binding.count }}.`
)
})
})
describe("readableToRuntimeBinding", () => {
const bindableProperties = [
{
category: "Current User",
icon: "User",
providerId: "user",
readableBinding: "Current User.firstName",
runtimeBinding: "[user].[firstName]",
type: "context",
},
{
category: "Bindings",
icon: "Brackets",
readableBinding: "Binding.count",
runtimeBinding: "count",
type: "context",
},
]
it("should convert a readable binding to a runtime one", () => {
const textWithBindings = `Hello {{ Current User.firstName }}! The count is {{ Binding.count }}.`
expect(
readableToRuntimeBinding(
bindableProperties,
textWithBindings,
"runtimeBinding"
)
).toEqual(`Hello {{ [user].[firstName] }}! The count is {{ count }}.`)
})
})

View File

@ -7,11 +7,9 @@ import {
} from "builderStore" } from "builderStore"
import { datasources, tables } from "stores/backend" import { datasources, tables } from "stores/backend"
import { get } from "svelte/store" import { get } from "svelte/store"
import { auth } from "stores/portal" import { auth, apps } from "stores/portal"
import { SocketEvent, BuilderSocketEvent } from "@budibase/shared-core" import { SocketEvent, BuilderSocketEvent, helpers } from "@budibase/shared-core"
import { apps } from "stores/portal"
import { notifications } from "@budibase/bbui" import { notifications } from "@budibase/bbui"
import { helpers } from "@budibase/shared-core"
export const createBuilderWebsocket = appId => { export const createBuilderWebsocket = appId => {
const socket = createWebsocket("/socket/builder") const socket = createWebsocket("/socket/builder")

View File

@ -1,5 +1,9 @@
<script> <script>
import { automationStore, selectedAutomation } from "builderStore" import {
automationStore,
selectedAutomation,
automationHistoryStore,
} from "builderStore"
import ConfirmDialog from "components/common/ConfirmDialog.svelte" import ConfirmDialog from "components/common/ConfirmDialog.svelte"
import FlowItem from "./FlowItem.svelte" import FlowItem from "./FlowItem.svelte"
import TestDataModal from "./TestDataModal.svelte" import TestDataModal from "./TestDataModal.svelte"
@ -8,7 +12,6 @@
import { Icon, notifications, Modal } from "@budibase/bbui" import { Icon, notifications, Modal } from "@budibase/bbui"
import { ActionStepID } from "constants/backend/automations" import { ActionStepID } from "constants/backend/automations"
import UndoRedoControl from "components/common/UndoRedoControl.svelte" import UndoRedoControl from "components/common/UndoRedoControl.svelte"
import { automationHistoryStore } from "builderStore"
export let automation export let automation

Some files were not shown because too many files have changed in this diff Show More