Merge remote-tracking branch 'origin/master' into fix/pc-bug-fixes
This commit is contained in:
commit
4baab262d5
|
@ -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
|
||||||
|
|
|
@ -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"]
|
||||||
|
|
|
@ -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
|
|
|
@ -1,14 +1,52 @@
|
||||||
#!/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
|
||||||
|
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
|
||||||
|
sed -i "s#DATA_DIR#/opt/couchdb/data#g" /opt/couchdb/etc/local.ini
|
||||||
|
sed -i "s/^-name .*$//g" /opt/couchdb/etc/vm.args
|
||||||
|
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
|
||||||
|
|
||||||
/opt/clouseau/bin/clouseau > /dev/stdout 2>&1 &
|
/opt/clouseau/bin/clouseau > /dev/stdout 2>&1 &
|
||||||
/docker-entrypoint.sh /opt/couchdb/bin/couchdb &
|
/docker-entrypoint.sh /opt/couchdb/bin/couchdb &
|
||||||
sleep 10
|
|
||||||
|
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
|
||||||
|
|
||||||
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
|
|
@ -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
|
||||||
|
|
|
@ -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
|
|
|
@ -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"
|
||||||
|
|
||||||
|
|
|
@ -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"]
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
{
|
{
|
||||||
"version": "2.13.12",
|
"version": "2.13.17",
|
||||||
"npmClient": "yarn",
|
"npmClient": "yarn",
|
||||||
"packages": [
|
"packages": [
|
||||||
"packages/*"
|
"packages/*"
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import { prefixed, DocumentType } from "@budibase/types"
|
import { prefixed, DocumentType } from "@budibase/types"
|
||||||
|
|
||||||
export {
|
export {
|
||||||
SEPARATOR,
|
SEPARATOR,
|
||||||
UNICODE_MAX,
|
UNICODE_MAX,
|
||||||
|
|
|
@ -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"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -1,2 +1,3 @@
|
||||||
import PosthogProcessor from "./PosthogProcessor"
|
import PosthogProcessor from "./PosthogProcessor"
|
||||||
|
|
||||||
export default PosthogProcessor
|
export default PosthogProcessor
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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) => {
|
||||||
|
|
|
@ -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) => {
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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,
|
||||||
}
|
}
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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 = () => {
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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"
|
||||||
|
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 })
|
||||||
|
}
|
||||||
|
|
|
@ -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")
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -1,2 +1,3 @@
|
||||||
import Chance from "./Chance"
|
import Chance from "./Chance"
|
||||||
|
|
||||||
export const generator = new Chance()
|
export const generator = new Chance()
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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"],
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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}
|
||||||
|
|
|
@ -17,7 +17,7 @@
|
||||||
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()
|
||||||
|
@ -135,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"
|
||||||
|
@ -157,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>
|
||||||
|
@ -196,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;
|
||||||
}
|
}
|
||||||
|
@ -206,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;
|
||||||
|
@ -215,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>
|
||||||
|
|
|
@ -224,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">
|
||||||
|
@ -275,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 {
|
||||||
|
|
|
@ -10,8 +10,9 @@
|
||||||
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
|
||||||
|
@ -82,8 +83,9 @@
|
||||||
{getOptionLabel}
|
{getOptionLabel}
|
||||||
{getOptionValue}
|
{getOptionValue}
|
||||||
{getOptionIcon}
|
{getOptionIcon}
|
||||||
{useOptionIconImage}
|
|
||||||
{getOptionColour}
|
{getOptionColour}
|
||||||
|
{getOptionSubtitle}
|
||||||
|
{useOptionIconImage}
|
||||||
{isOptionEnabled}
|
{isOptionEnabled}
|
||||||
{autocomplete}
|
{autocomplete}
|
||||||
{sort}
|
{sort}
|
||||||
|
|
|
@ -43,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
|
||||||
|
|
|
@ -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
|
||||||
|
@ -58,6 +59,7 @@
|
||||||
{getOptionValue}
|
{getOptionValue}
|
||||||
{getOptionIcon}
|
{getOptionIcon}
|
||||||
{getOptionColour}
|
{getOptionColour}
|
||||||
|
{getOptionSubtitle}
|
||||||
{useOptionIconImage}
|
{useOptionIconImage}
|
||||||
{isOptionEnabled}
|
{isOptionEnabled}
|
||||||
{autocomplete}
|
{autocomplete}
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
<script>
|
<script>
|
||||||
import Input from "../Form/Input.svelte"
|
import Input from "../Form/Input.svelte"
|
||||||
|
|
||||||
let value = ""
|
let value = ""
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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 = ""
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -228,7 +228,12 @@ export const getContextProviderComponents = (
|
||||||
/**
|
/**
|
||||||
* 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 || !componentId) {
|
if (!asset || !componentId) {
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
|
@ -236,13 +241,30 @@ export const getActionProviderComponents = (asset, componentId, actionType) => {
|
||||||
// Get the component tree leading up to this component, ignoring the component
|
// Get the component tree leading up to this component, ignoring the component
|
||||||
// itself
|
// itself
|
||||||
const path = findComponentPath(asset.props, componentId)
|
const path = findComponentPath(asset.props, componentId)
|
||||||
path.pop()
|
if (!options?.includeSelf) {
|
||||||
|
path.pop()
|
||||||
|
}
|
||||||
|
|
||||||
// Filter by only data provider components
|
// Find matching contexts and generate bindings
|
||||||
return path.filter(component => {
|
let providers = []
|
||||||
|
path.forEach(component => {
|
||||||
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
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -1068,17 +1090,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}"`,
|
||||||
|
@ -1130,8 +1153,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
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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 }}.`)
|
||||||
|
})
|
||||||
|
})
|
|
@ -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")
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
<script>
|
<script>
|
||||||
import { automationStore } from "builderStore"
|
import { automationStore } from "builderStore"
|
||||||
import { notifications } from "@budibase/bbui"
|
|
||||||
import {
|
import {
|
||||||
|
notifications,
|
||||||
Input,
|
Input,
|
||||||
InlineAlert,
|
InlineAlert,
|
||||||
ModalContent,
|
ModalContent,
|
||||||
|
|
|
@ -1,7 +1,12 @@
|
||||||
<script>
|
<script>
|
||||||
import { automationStore } from "builderStore"
|
import { automationStore } from "builderStore"
|
||||||
import { notifications } from "@budibase/bbui"
|
import {
|
||||||
import { Icon, Input, ModalContent, Modal } from "@budibase/bbui"
|
notifications,
|
||||||
|
Icon,
|
||||||
|
Input,
|
||||||
|
ModalContent,
|
||||||
|
Modal,
|
||||||
|
} from "@budibase/bbui"
|
||||||
|
|
||||||
export let automation
|
export let automation
|
||||||
export let onCancel = undefined
|
export let onCancel = undefined
|
||||||
|
|
|
@ -38,12 +38,11 @@
|
||||||
EditorModes,
|
EditorModes,
|
||||||
} from "components/common/CodeEditor"
|
} from "components/common/CodeEditor"
|
||||||
import FilterDrawer from "components/design/settings/controls/FilterEditor/FilterDrawer.svelte"
|
import FilterDrawer from "components/design/settings/controls/FilterEditor/FilterDrawer.svelte"
|
||||||
import { LuceneUtils } from "@budibase/frontend-core"
|
import { LuceneUtils, Utils } from "@budibase/frontend-core"
|
||||||
import {
|
import {
|
||||||
getSchemaForDatasourcePlus,
|
getSchemaForDatasourcePlus,
|
||||||
getEnvironmentBindings,
|
getEnvironmentBindings,
|
||||||
} from "builderStore/dataBinding"
|
} from "builderStore/dataBinding"
|
||||||
import { Utils } from "@budibase/frontend-core"
|
|
||||||
import { TriggerStepID, ActionStepID } from "constants/backend/automations"
|
import { TriggerStepID, ActionStepID } from "constants/backend/automations"
|
||||||
import { onMount } from "svelte"
|
import { onMount } from "svelte"
|
||||||
import { cloneDeep } from "lodash/fp"
|
import { cloneDeep } from "lodash/fp"
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
import { Button, Select, Input, Label } from "@budibase/bbui"
|
import { Button, Select, Input, Label } from "@budibase/bbui"
|
||||||
import { onMount, createEventDispatcher } from "svelte"
|
import { onMount, createEventDispatcher } from "svelte"
|
||||||
import { flags } from "stores/backend"
|
import { flags } from "stores/backend"
|
||||||
|
|
||||||
const dispatch = createEventDispatcher()
|
const dispatch = createEventDispatcher()
|
||||||
|
|
||||||
export let value
|
export let value
|
||||||
|
|
|
@ -1,8 +1,7 @@
|
||||||
<script>
|
<script>
|
||||||
import { Icon, notifications } from "@budibase/bbui"
|
import { Icon, notifications, ModalContent } from "@budibase/bbui"
|
||||||
import { automationStore, selectedAutomation } from "builderStore"
|
import { automationStore, selectedAutomation } from "builderStore"
|
||||||
import WebhookDisplay from "./WebhookDisplay.svelte"
|
import WebhookDisplay from "./WebhookDisplay.svelte"
|
||||||
import { ModalContent } from "@budibase/bbui"
|
|
||||||
import { onMount, onDestroy } from "svelte"
|
import { onMount, onDestroy } from "svelte"
|
||||||
|
|
||||||
const POLL_RATE_MS = 2500
|
const POLL_RATE_MS = 2500
|
||||||
|
|
|
@ -1,11 +1,15 @@
|
||||||
<script>
|
<script>
|
||||||
import { createEventDispatcher } from "svelte"
|
import { createEventDispatcher } from "svelte"
|
||||||
import { tables } from "stores/backend"
|
import { tables, roles } from "stores/backend"
|
||||||
import { roles } from "stores/backend"
|
import {
|
||||||
import { notifications } from "@budibase/bbui"
|
notifications,
|
||||||
|
keepOpen,
|
||||||
|
ModalContent,
|
||||||
|
Select,
|
||||||
|
Link,
|
||||||
|
} from "@budibase/bbui"
|
||||||
import RowFieldControl from "../RowFieldControl.svelte"
|
import RowFieldControl from "../RowFieldControl.svelte"
|
||||||
import { API } from "api"
|
import { API } from "api"
|
||||||
import { keepOpen, ModalContent, Select, Link } from "@budibase/bbui"
|
|
||||||
import ErrorsBox from "components/common/ErrorsBox.svelte"
|
import ErrorsBox from "components/common/ErrorsBox.svelte"
|
||||||
import { goto } from "@roxi/routify"
|
import { goto } from "@roxi/routify"
|
||||||
|
|
||||||
|
|
|
@ -1,8 +1,14 @@
|
||||||
<script>
|
<script>
|
||||||
import { keepOpen, ModalContent, Select, Input, Button } from "@budibase/bbui"
|
import {
|
||||||
|
keepOpen,
|
||||||
|
ModalContent,
|
||||||
|
Select,
|
||||||
|
Input,
|
||||||
|
Button,
|
||||||
|
notifications,
|
||||||
|
} from "@budibase/bbui"
|
||||||
import { onMount } from "svelte"
|
import { onMount } from "svelte"
|
||||||
import { API } from "api"
|
import { API } from "api"
|
||||||
import { notifications } from "@budibase/bbui"
|
|
||||||
import ErrorsBox from "components/common/ErrorsBox.svelte"
|
import ErrorsBox from "components/common/ErrorsBox.svelte"
|
||||||
import { roles } from "stores/backend"
|
import { roles } from "stores/backend"
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
<script>
|
<script>
|
||||||
import { goto, isActive, params } from "@roxi/routify"
|
import { goto, isActive, params } from "@roxi/routify"
|
||||||
|
import { Layout } from "@budibase/bbui"
|
||||||
import { BUDIBASE_INTERNAL_DB_ID } from "constants/backend"
|
import { BUDIBASE_INTERNAL_DB_ID } from "constants/backend"
|
||||||
import {
|
import {
|
||||||
database,
|
database,
|
||||||
|
@ -21,8 +22,11 @@
|
||||||
import IntegrationIcon from "./IntegrationIcon.svelte"
|
import IntegrationIcon from "./IntegrationIcon.svelte"
|
||||||
import { TableNames } from "constants"
|
import { TableNames } from "constants"
|
||||||
import { userSelectedResourceMap } from "builderStore"
|
import { userSelectedResourceMap } from "builderStore"
|
||||||
|
import { enrichDatasources } from "./datasourceUtils"
|
||||||
|
import { onMount } from "svelte"
|
||||||
|
|
||||||
let openDataSources = []
|
export let searchTerm
|
||||||
|
let toggledDatasources = {}
|
||||||
|
|
||||||
$: enrichedDataSources = enrichDatasources(
|
$: enrichedDataSources = enrichDatasources(
|
||||||
$datasources,
|
$datasources,
|
||||||
|
@ -32,52 +36,9 @@
|
||||||
$queries,
|
$queries,
|
||||||
$views,
|
$views,
|
||||||
$viewsV2,
|
$viewsV2,
|
||||||
openDataSources
|
toggledDatasources,
|
||||||
|
searchTerm
|
||||||
)
|
)
|
||||||
$: openDataSource = enrichedDataSources.find(x => x.open)
|
|
||||||
$: {
|
|
||||||
// Ensure the open datasource is always actually open
|
|
||||||
if (openDataSource) {
|
|
||||||
openNode(openDataSource)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const enrichDatasources = (
|
|
||||||
datasources,
|
|
||||||
params,
|
|
||||||
isActive,
|
|
||||||
tables,
|
|
||||||
queries,
|
|
||||||
views,
|
|
||||||
viewsV2,
|
|
||||||
openDataSources
|
|
||||||
) => {
|
|
||||||
if (!datasources?.list?.length) {
|
|
||||||
return []
|
|
||||||
}
|
|
||||||
return datasources.list.map(datasource => {
|
|
||||||
const selected =
|
|
||||||
isActive("./datasource") &&
|
|
||||||
datasources.selectedDatasourceId === datasource._id
|
|
||||||
const open = openDataSources.includes(datasource._id)
|
|
||||||
const containsSelected = containsActiveEntity(
|
|
||||||
datasource,
|
|
||||||
params,
|
|
||||||
isActive,
|
|
||||||
tables,
|
|
||||||
queries,
|
|
||||||
views,
|
|
||||||
viewsV2
|
|
||||||
)
|
|
||||||
const onlySource = datasources.list.length === 1
|
|
||||||
return {
|
|
||||||
...datasource,
|
|
||||||
selected,
|
|
||||||
containsSelected,
|
|
||||||
open: selected || open || containsSelected || onlySource,
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
function selectDatasource(datasource) {
|
function selectDatasource(datasource) {
|
||||||
openNode(datasource)
|
openNode(datasource)
|
||||||
|
@ -91,102 +52,42 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function closeNode(datasource) {
|
|
||||||
openDataSources = openDataSources.filter(id => datasource._id !== id)
|
|
||||||
}
|
|
||||||
|
|
||||||
function openNode(datasource) {
|
function openNode(datasource) {
|
||||||
if (!openDataSources.includes(datasource._id)) {
|
toggledDatasources[datasource._id] = true
|
||||||
openDataSources = [...openDataSources, datasource._id]
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function toggleNode(datasource) {
|
function toggleNode(datasource) {
|
||||||
const isOpen = openDataSources.includes(datasource._id)
|
toggledDatasources[datasource._id] = !datasource.open
|
||||||
if (isOpen) {
|
|
||||||
closeNode(datasource)
|
|
||||||
} else {
|
|
||||||
openNode(datasource)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const containsActiveEntity = (
|
const appUsersTableName = "App users"
|
||||||
datasource,
|
$: showAppUsersTable =
|
||||||
params,
|
!searchTerm ||
|
||||||
isActive,
|
appUsersTableName.toLowerCase().indexOf(searchTerm.toLowerCase()) > -1
|
||||||
tables,
|
|
||||||
queries,
|
|
||||||
views,
|
|
||||||
viewsV2
|
|
||||||
) => {
|
|
||||||
// Check for being on a datasource page
|
|
||||||
if (params.datasourceId === datasource._id) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check for hardcoded datasource edge cases
|
onMount(() => {
|
||||||
if (
|
if ($tables.selected) {
|
||||||
isActive("./datasource/bb_internal") &&
|
toggledDatasources[$tables.selected.sourceId] = true
|
||||||
datasource._id === "bb_internal"
|
|
||||||
) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
if (
|
|
||||||
isActive("./datasource/datasource_internal_bb_default") &&
|
|
||||||
datasource._id === "datasource_internal_bb_default"
|
|
||||||
) {
|
|
||||||
return true
|
|
||||||
}
|
}
|
||||||
|
})
|
||||||
|
|
||||||
// Check for a matching query
|
$: showNoResults =
|
||||||
if (params.queryId) {
|
searchTerm && !showAppUsersTable && !enrichedDataSources.find(ds => ds.show)
|
||||||
const query = queries.list?.find(q => q._id === params.queryId)
|
|
||||||
return datasource._id === query?.datasourceId
|
|
||||||
}
|
|
||||||
|
|
||||||
// If there are no entities it can't contain anything
|
|
||||||
if (!datasource.entities) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get a list of table options
|
|
||||||
let options = datasource.entities
|
|
||||||
if (!Array.isArray(options)) {
|
|
||||||
options = Object.values(options)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check for a matching table
|
|
||||||
if (params.tableId) {
|
|
||||||
const selectedTable = tables.selected?._id
|
|
||||||
return options.find(x => x._id === selectedTable) != null
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check for a matching view
|
|
||||||
const selectedView = views.selected?.name
|
|
||||||
const viewTable = options.find(table => {
|
|
||||||
return table.views?.[selectedView] != null
|
|
||||||
})
|
|
||||||
if (viewTable) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check for a matching viewV2
|
|
||||||
const viewV2Table = options.find(x => x._id === viewsV2.selected?.tableId)
|
|
||||||
return viewV2Table != null
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if $database?._id}
|
{#if $database?._id}
|
||||||
<div class="hierarchy-items-container">
|
<div class="hierarchy-items-container">
|
||||||
<NavItem
|
{#if showAppUsersTable}
|
||||||
icon="UserGroup"
|
<NavItem
|
||||||
text="App users"
|
icon="UserGroup"
|
||||||
selected={$isActive("./table/:tableId") &&
|
text={appUsersTableName}
|
||||||
$tables.selected?._id === TableNames.USERS}
|
selected={$isActive("./table/:tableId") &&
|
||||||
on:click={() => selectTable(TableNames.USERS)}
|
$tables.selected?._id === TableNames.USERS}
|
||||||
selectedBy={$userSelectedResourceMap[TableNames.USERS]}
|
on:click={() => selectTable(TableNames.USERS)}
|
||||||
/>
|
selectedBy={$userSelectedResourceMap[TableNames.USERS]}
|
||||||
{#each enrichedDataSources as datasource}
|
/>
|
||||||
|
{/if}
|
||||||
|
{#each enrichedDataSources.filter(ds => ds.show) as datasource}
|
||||||
<NavItem
|
<NavItem
|
||||||
border
|
border
|
||||||
text={datasource.name}
|
text={datasource.name}
|
||||||
|
@ -210,8 +111,8 @@
|
||||||
</NavItem>
|
</NavItem>
|
||||||
|
|
||||||
{#if datasource.open}
|
{#if datasource.open}
|
||||||
<TableNavigator sourceId={datasource._id} {selectTable} />
|
<TableNavigator tables={datasource.tables} {selectTable} />
|
||||||
{#each $queries.list.filter(query => query.datasourceId === datasource._id) as query}
|
{#each datasource.queries as query}
|
||||||
<NavItem
|
<NavItem
|
||||||
indentLevel={1}
|
indentLevel={1}
|
||||||
icon="SQLQuery"
|
icon="SQLQuery"
|
||||||
|
@ -228,6 +129,13 @@
|
||||||
{/each}
|
{/each}
|
||||||
{/if}
|
{/if}
|
||||||
{/each}
|
{/each}
|
||||||
|
{#if showNoResults}
|
||||||
|
<Layout paddingY="none" paddingX="L">
|
||||||
|
<div class="no-results">
|
||||||
|
There aren't any datasources matching that name
|
||||||
|
</div>
|
||||||
|
</Layout>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
|
@ -240,4 +148,8 @@
|
||||||
place-items: center;
|
place-items: center;
|
||||||
flex: 0 0 24px;
|
flex: 0 0 24px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.no-results {
|
||||||
|
color: var(--spectrum-global-color-gray-600);
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -0,0 +1,181 @@
|
||||||
|
import { TableNames } from "constants"
|
||||||
|
|
||||||
|
const showDatasourceOpen = ({
|
||||||
|
selected,
|
||||||
|
containsSelected,
|
||||||
|
dsToggledStatus,
|
||||||
|
searchTerm,
|
||||||
|
onlyOneSource,
|
||||||
|
}) => {
|
||||||
|
// We want to display all the ds expanded while filtering ds
|
||||||
|
if (searchTerm) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the toggle status has been a value
|
||||||
|
if (dsToggledStatus !== undefined) {
|
||||||
|
return dsToggledStatus
|
||||||
|
}
|
||||||
|
|
||||||
|
if (onlyOneSource) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return selected || containsSelected
|
||||||
|
}
|
||||||
|
|
||||||
|
const containsActiveEntity = (
|
||||||
|
datasource,
|
||||||
|
params,
|
||||||
|
isActive,
|
||||||
|
tables,
|
||||||
|
queries,
|
||||||
|
views,
|
||||||
|
viewsV2
|
||||||
|
) => {
|
||||||
|
// Check for being on a datasource page
|
||||||
|
if (params.datasourceId === datasource._id) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for hardcoded datasource edge cases
|
||||||
|
if (
|
||||||
|
isActive("./datasource/bb_internal") &&
|
||||||
|
datasource._id === "bb_internal"
|
||||||
|
) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
isActive("./datasource/datasource_internal_bb_default") &&
|
||||||
|
datasource._id === "datasource_internal_bb_default"
|
||||||
|
) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for a matching query
|
||||||
|
if (params.queryId) {
|
||||||
|
const query = queries.list?.find(q => q._id === params.queryId)
|
||||||
|
return datasource._id === query?.datasourceId
|
||||||
|
}
|
||||||
|
|
||||||
|
// If there are no entities it can't contain anything
|
||||||
|
if (!datasource.entities) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get a list of table options
|
||||||
|
let options = datasource.entities
|
||||||
|
if (!Array.isArray(options)) {
|
||||||
|
options = Object.values(options)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for a matching table
|
||||||
|
if (params.tableId) {
|
||||||
|
const selectedTable = tables.selected?._id
|
||||||
|
return options.find(x => x._id === selectedTable) != null
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for a matching view
|
||||||
|
const selectedView = views.selected?.name
|
||||||
|
const viewTable = options.find(table => {
|
||||||
|
return table.views?.[selectedView] != null
|
||||||
|
})
|
||||||
|
if (viewTable) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for a matching viewV2
|
||||||
|
const viewV2Table = options.find(x => x._id === viewsV2.selected?.tableId)
|
||||||
|
return viewV2Table != null
|
||||||
|
}
|
||||||
|
|
||||||
|
export const enrichDatasources = (
|
||||||
|
datasources,
|
||||||
|
params,
|
||||||
|
isActive,
|
||||||
|
tables,
|
||||||
|
queries,
|
||||||
|
views,
|
||||||
|
viewsV2,
|
||||||
|
toggledDatasources,
|
||||||
|
searchTerm
|
||||||
|
) => {
|
||||||
|
if (!datasources?.list?.length) {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
const onlySource = datasources.list.length === 1
|
||||||
|
return datasources.list.map(datasource => {
|
||||||
|
const selected =
|
||||||
|
isActive("./datasource") &&
|
||||||
|
datasources.selectedDatasourceId === datasource._id
|
||||||
|
const containsSelected = containsActiveEntity(
|
||||||
|
datasource,
|
||||||
|
params,
|
||||||
|
isActive,
|
||||||
|
tables,
|
||||||
|
queries,
|
||||||
|
views,
|
||||||
|
viewsV2
|
||||||
|
)
|
||||||
|
|
||||||
|
const dsTables = tables.list.filter(
|
||||||
|
table =>
|
||||||
|
table.sourceId === datasource._id && table._id !== TableNames.USERS
|
||||||
|
)
|
||||||
|
const dsQueries = queries.list.filter(
|
||||||
|
query => query.datasourceId === datasource._id
|
||||||
|
)
|
||||||
|
|
||||||
|
const open = showDatasourceOpen({
|
||||||
|
selected,
|
||||||
|
containsSelected,
|
||||||
|
dsToggledStatus: toggledDatasources[datasource._id],
|
||||||
|
searchTerm,
|
||||||
|
onlyOneSource: onlySource,
|
||||||
|
})
|
||||||
|
|
||||||
|
const visibleDsQueries = dsQueries.filter(
|
||||||
|
q =>
|
||||||
|
!searchTerm ||
|
||||||
|
q.name?.toLowerCase()?.indexOf(searchTerm.toLowerCase()) > -1
|
||||||
|
)
|
||||||
|
|
||||||
|
const visibleDsTables = dsTables
|
||||||
|
.map(t => ({
|
||||||
|
...t,
|
||||||
|
views: !searchTerm
|
||||||
|
? t.views
|
||||||
|
: Object.keys(t.views || {})
|
||||||
|
.filter(
|
||||||
|
viewName =>
|
||||||
|
viewName.toLowerCase().indexOf(searchTerm.toLowerCase()) > -1
|
||||||
|
)
|
||||||
|
.reduce(
|
||||||
|
(acc, viewName) => ({ ...acc, [viewName]: t.views[viewName] }),
|
||||||
|
{}
|
||||||
|
),
|
||||||
|
}))
|
||||||
|
.filter(
|
||||||
|
table =>
|
||||||
|
!searchTerm ||
|
||||||
|
table.name?.toLowerCase()?.indexOf(searchTerm.toLowerCase()) > -1 ||
|
||||||
|
Object.keys(table.views).length
|
||||||
|
)
|
||||||
|
|
||||||
|
const show = !!(
|
||||||
|
!searchTerm ||
|
||||||
|
visibleDsQueries.length ||
|
||||||
|
visibleDsTables.length
|
||||||
|
)
|
||||||
|
return {
|
||||||
|
...datasource,
|
||||||
|
selected,
|
||||||
|
containsSelected,
|
||||||
|
open,
|
||||||
|
queries: visibleDsQueries,
|
||||||
|
tables: visibleDsTables,
|
||||||
|
show,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
|
@ -1,8 +1,7 @@
|
||||||
<script>
|
<script>
|
||||||
import { get } from "svelte/store"
|
import { get } from "svelte/store"
|
||||||
import { datasources, integrations } from "stores/backend"
|
import { datasources, integrations } from "stores/backend"
|
||||||
import { notifications } from "@budibase/bbui"
|
import { notifications, Input, ModalContent, Modal } from "@budibase/bbui"
|
||||||
import { Input, ModalContent, Modal } from "@budibase/bbui"
|
|
||||||
import { integrationForDatasource } from "stores/selectors"
|
import { integrationForDatasource } from "stores/selectors"
|
||||||
|
|
||||||
let error = ""
|
let error = ""
|
||||||
|
|
|
@ -1,8 +1,7 @@
|
||||||
<script>
|
<script>
|
||||||
import { goto } from "@roxi/routify"
|
import { goto } from "@roxi/routify"
|
||||||
import { datasources } from "stores/backend"
|
import { datasources } from "stores/backend"
|
||||||
import { notifications } from "@budibase/bbui"
|
import { notifications, ActionMenu, MenuItem, Icon } from "@budibase/bbui"
|
||||||
import { ActionMenu, MenuItem, Icon } from "@budibase/bbui"
|
|
||||||
import ConfirmDialog from "components/common/ConfirmDialog.svelte"
|
import ConfirmDialog from "components/common/ConfirmDialog.svelte"
|
||||||
import UpdateDatasourceModal from "components/backend/DatasourceNavigator/modals/UpdateDatasourceModal.svelte"
|
import UpdateDatasourceModal from "components/backend/DatasourceNavigator/modals/UpdateDatasourceModal.svelte"
|
||||||
import { BUDIBASE_DATASOURCE_TYPE } from "constants/backend"
|
import { BUDIBASE_DATASOURCE_TYPE } from "constants/backend"
|
||||||
|
|
|
@ -0,0 +1,219 @@
|
||||||
|
import { enrichDatasources } from "../datasourceUtils"
|
||||||
|
|
||||||
|
describe("datasourceUtils", () => {
|
||||||
|
describe("enrichDatasources", () => {
|
||||||
|
it.each([
|
||||||
|
["undefined", undefined],
|
||||||
|
["undefined list", {}],
|
||||||
|
["empty list", { list: [] }],
|
||||||
|
])("%s datasources will return an empty list", datasources => {
|
||||||
|
const result = enrichDatasources(datasources)
|
||||||
|
|
||||||
|
expect(result).toEqual([])
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("filtering", () => {
|
||||||
|
const internalTables = {
|
||||||
|
_id: "datasource_internal_bb_default",
|
||||||
|
name: "Sample Data",
|
||||||
|
}
|
||||||
|
|
||||||
|
const pgDatasource = {
|
||||||
|
_id: "pg_ds",
|
||||||
|
name: "PostgreSQL local",
|
||||||
|
}
|
||||||
|
|
||||||
|
const mysqlDatasource = {
|
||||||
|
_id: "mysql_ds",
|
||||||
|
name: "My SQL local",
|
||||||
|
}
|
||||||
|
|
||||||
|
const tables = [
|
||||||
|
...[
|
||||||
|
{
|
||||||
|
_id: "ta_bb_employee",
|
||||||
|
name: "Employees",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
_id: "ta_bb_expenses",
|
||||||
|
name: "Expenses",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
_id: "ta_bb_expenses_2",
|
||||||
|
name: "Expenses 2",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
_id: "ta_bb_inventory",
|
||||||
|
name: "Inventory",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
_id: "ta_bb_jobs",
|
||||||
|
name: "Jobs",
|
||||||
|
},
|
||||||
|
].map(t => ({
|
||||||
|
...t,
|
||||||
|
sourceId: internalTables._id,
|
||||||
|
})),
|
||||||
|
...[
|
||||||
|
{
|
||||||
|
_id: "pg_ds-external_inventory",
|
||||||
|
name: "External Inventory",
|
||||||
|
views: {
|
||||||
|
"External Inventory first view": {
|
||||||
|
name: "External Inventory first view",
|
||||||
|
id: "pg_ds_view_1",
|
||||||
|
},
|
||||||
|
"External Inventory second view": {
|
||||||
|
name: "External Inventory second view",
|
||||||
|
id: "pg_ds_view_2",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
_id: "pg_ds-another_table",
|
||||||
|
name: "Another table",
|
||||||
|
views: {
|
||||||
|
view1: {
|
||||||
|
id: "pg_ds-another_table-view1",
|
||||||
|
name: "view1",
|
||||||
|
},
|
||||||
|
["View 2"]: {
|
||||||
|
id: "pg_ds-another_table-view2",
|
||||||
|
name: "View 2",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
_id: "pg_ds_table2",
|
||||||
|
name: "table2",
|
||||||
|
views: {
|
||||||
|
"new 2": {
|
||||||
|
name: "new 2",
|
||||||
|
id: "pg_ds_table2_new_2",
|
||||||
|
},
|
||||||
|
new: {
|
||||||
|
name: "new",
|
||||||
|
id: "pg_ds_table2_new_",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
].map(t => ({
|
||||||
|
...t,
|
||||||
|
sourceId: pgDatasource._id,
|
||||||
|
})),
|
||||||
|
...[
|
||||||
|
{
|
||||||
|
_id: "mysql_ds-mysql_table",
|
||||||
|
name: "MySQL table",
|
||||||
|
},
|
||||||
|
].map(t => ({
|
||||||
|
...t,
|
||||||
|
sourceId: mysqlDatasource._id,
|
||||||
|
})),
|
||||||
|
]
|
||||||
|
|
||||||
|
const datasources = {
|
||||||
|
list: [internalTables, pgDatasource, mysqlDatasource],
|
||||||
|
}
|
||||||
|
const isActive = vi.fn().mockReturnValue(true)
|
||||||
|
|
||||||
|
it("without a search term, all datasources are returned", () => {
|
||||||
|
const searchTerm = ""
|
||||||
|
|
||||||
|
const result = enrichDatasources(
|
||||||
|
datasources,
|
||||||
|
{},
|
||||||
|
isActive,
|
||||||
|
{ list: [] },
|
||||||
|
{ list: [] },
|
||||||
|
{ list: [] },
|
||||||
|
{ list: [] },
|
||||||
|
{},
|
||||||
|
searchTerm
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(result).toEqual(
|
||||||
|
datasources.list.map(d =>
|
||||||
|
expect.objectContaining({
|
||||||
|
_id: d._id,
|
||||||
|
show: true,
|
||||||
|
})
|
||||||
|
)
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("given a valid search term, all tables are correctly filtered", () => {
|
||||||
|
const searchTerm = "ex"
|
||||||
|
|
||||||
|
const result = enrichDatasources(
|
||||||
|
datasources,
|
||||||
|
{},
|
||||||
|
isActive,
|
||||||
|
{ list: tables },
|
||||||
|
{ list: [] },
|
||||||
|
{ list: [] },
|
||||||
|
{ list: [] },
|
||||||
|
{},
|
||||||
|
searchTerm
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(result).toEqual([
|
||||||
|
expect.objectContaining({
|
||||||
|
_id: internalTables._id,
|
||||||
|
show: true,
|
||||||
|
tables: [
|
||||||
|
expect.objectContaining({ _id: "ta_bb_expenses" }),
|
||||||
|
expect.objectContaining({ _id: "ta_bb_expenses_2" }),
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
expect.objectContaining({
|
||||||
|
_id: pgDatasource._id,
|
||||||
|
show: true,
|
||||||
|
tables: [
|
||||||
|
expect.objectContaining({ _id: "pg_ds-external_inventory" }),
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
expect.objectContaining({
|
||||||
|
_id: mysqlDatasource._id,
|
||||||
|
show: false,
|
||||||
|
tables: [],
|
||||||
|
}),
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
|
it("given a non matching search term, all entities are empty", () => {
|
||||||
|
const searchTerm = "non matching"
|
||||||
|
|
||||||
|
const result = enrichDatasources(
|
||||||
|
datasources,
|
||||||
|
{},
|
||||||
|
isActive,
|
||||||
|
{ list: tables },
|
||||||
|
{ list: [] },
|
||||||
|
{ list: [] },
|
||||||
|
{ list: [] },
|
||||||
|
{},
|
||||||
|
searchTerm
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(result).toEqual([
|
||||||
|
expect.objectContaining({
|
||||||
|
_id: internalTables._id,
|
||||||
|
show: false,
|
||||||
|
tables: [],
|
||||||
|
}),
|
||||||
|
expect.objectContaining({
|
||||||
|
_id: pgDatasource._id,
|
||||||
|
show: false,
|
||||||
|
tables: [],
|
||||||
|
}),
|
||||||
|
expect.objectContaining({
|
||||||
|
_id: mysqlDatasource._id,
|
||||||
|
show: false,
|
||||||
|
tables: [],
|
||||||
|
}),
|
||||||
|
])
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
|
@ -1,5 +1,8 @@
|
||||||
<script>
|
<script>
|
||||||
import { RelationshipType } from "constants/backend"
|
import {
|
||||||
|
RelationshipType,
|
||||||
|
PrettyRelationshipDefinitions,
|
||||||
|
} from "constants/backend"
|
||||||
import {
|
import {
|
||||||
keepOpen,
|
keepOpen,
|
||||||
Button,
|
Button,
|
||||||
|
@ -8,13 +11,12 @@
|
||||||
Select,
|
Select,
|
||||||
Detail,
|
Detail,
|
||||||
Body,
|
Body,
|
||||||
|
Helpers,
|
||||||
} from "@budibase/bbui"
|
} from "@budibase/bbui"
|
||||||
import { tables } from "stores/backend"
|
import { tables } from "stores/backend"
|
||||||
import { Helpers } from "@budibase/bbui"
|
|
||||||
import { RelationshipErrorChecker } from "./relationshipErrors"
|
import { RelationshipErrorChecker } from "./relationshipErrors"
|
||||||
import { onMount } from "svelte"
|
import { onMount } from "svelte"
|
||||||
import RelationshipSelector from "components/common/RelationshipSelector.svelte"
|
import RelationshipSelector from "components/common/RelationshipSelector.svelte"
|
||||||
import { PrettyRelationshipDefinitions } from "constants/backend"
|
|
||||||
|
|
||||||
export let save
|
export let save
|
||||||
export let datasource
|
export let datasource
|
||||||
|
|
|
@ -1,5 +1,10 @@
|
||||||
<script>
|
<script>
|
||||||
import { tables, views, viewsV2, database } from "stores/backend"
|
import {
|
||||||
|
tables as tablesStore,
|
||||||
|
views,
|
||||||
|
viewsV2,
|
||||||
|
database,
|
||||||
|
} from "stores/backend"
|
||||||
import { TableNames } from "constants"
|
import { TableNames } from "constants"
|
||||||
import EditTablePopover from "./popovers/EditTablePopover.svelte"
|
import EditTablePopover from "./popovers/EditTablePopover.svelte"
|
||||||
import EditViewPopover from "./popovers/EditViewPopover.svelte"
|
import EditViewPopover from "./popovers/EditViewPopover.svelte"
|
||||||
|
@ -7,14 +12,10 @@
|
||||||
import { goto, isActive } from "@roxi/routify"
|
import { goto, isActive } from "@roxi/routify"
|
||||||
import { userSelectedResourceMap } from "builderStore"
|
import { userSelectedResourceMap } from "builderStore"
|
||||||
|
|
||||||
export let sourceId
|
export let tables
|
||||||
export let selectTable
|
export let selectTable
|
||||||
|
|
||||||
$: sortedTables = $tables.list
|
$: sortedTables = tables.sort(alphabetical)
|
||||||
.filter(
|
|
||||||
table => table.sourceId === sourceId && table._id !== TableNames.USERS
|
|
||||||
)
|
|
||||||
.sort(alphabetical)
|
|
||||||
|
|
||||||
const alphabetical = (a, b) => {
|
const alphabetical = (a, b) => {
|
||||||
return a.name?.toLowerCase() > b.name?.toLowerCase() ? 1 : -1
|
return a.name?.toLowerCase() > b.name?.toLowerCase() ? 1 : -1
|
||||||
|
@ -37,7 +38,7 @@
|
||||||
icon={table._id === TableNames.USERS ? "UserGroup" : "Table"}
|
icon={table._id === TableNames.USERS ? "UserGroup" : "Table"}
|
||||||
text={table.name}
|
text={table.name}
|
||||||
selected={$isActive("./table/:tableId") &&
|
selected={$isActive("./table/:tableId") &&
|
||||||
$tables.selected?._id === table._id}
|
$tablesStore.selected?._id === table._id}
|
||||||
on:click={() => selectTable(table._id)}
|
on:click={() => selectTable(table._id)}
|
||||||
selectedBy={$userSelectedResourceMap[table._id]}
|
selectedBy={$userSelectedResourceMap[table._id]}
|
||||||
>
|
>
|
||||||
|
|
|
@ -1,9 +1,13 @@
|
||||||
<script>
|
<script>
|
||||||
import { goto, url } from "@roxi/routify"
|
import { goto, url } from "@roxi/routify"
|
||||||
import { tables } from "stores/backend"
|
import { tables, datasources } from "stores/backend"
|
||||||
import { notifications } from "@budibase/bbui"
|
import {
|
||||||
import { Input, Label, ModalContent, Layout } from "@budibase/bbui"
|
notifications,
|
||||||
import { datasources } from "stores/backend"
|
Input,
|
||||||
|
Label,
|
||||||
|
ModalContent,
|
||||||
|
Layout,
|
||||||
|
} from "@budibase/bbui"
|
||||||
import TableDataImport from "../TableDataImport.svelte"
|
import TableDataImport from "../TableDataImport.svelte"
|
||||||
import {
|
import {
|
||||||
BUDIBASE_INTERNAL_DB_ID,
|
BUDIBASE_INTERNAL_DB_ID,
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
<script>
|
<script>
|
||||||
import { Helpers } from "@budibase/bbui"
|
import { Helpers } from "@budibase/bbui"
|
||||||
|
|
||||||
export let size
|
export let size
|
||||||
export let svgHtml
|
export let svgHtml
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,149 @@
|
||||||
|
<script>
|
||||||
|
import { tick } from "svelte"
|
||||||
|
import { Icon, Body } from "@budibase/bbui"
|
||||||
|
import { keyUtils } from "helpers/keyUtils"
|
||||||
|
|
||||||
|
export let title
|
||||||
|
export let placeholder
|
||||||
|
export let value
|
||||||
|
export let onAdd
|
||||||
|
export let search
|
||||||
|
|
||||||
|
let searchInput
|
||||||
|
|
||||||
|
const openSearch = async () => {
|
||||||
|
search = true
|
||||||
|
await tick()
|
||||||
|
searchInput.focus()
|
||||||
|
}
|
||||||
|
|
||||||
|
const closeSearch = async () => {
|
||||||
|
search = false
|
||||||
|
value = ""
|
||||||
|
}
|
||||||
|
|
||||||
|
const onKeyDown = e => {
|
||||||
|
if (e.key === "Escape") {
|
||||||
|
closeSearch()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleAddButton = () => {
|
||||||
|
if (search) {
|
||||||
|
closeSearch()
|
||||||
|
} else {
|
||||||
|
onAdd()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<svelte:window on:keydown={onKeyDown} />
|
||||||
|
|
||||||
|
<div class="header" class:search>
|
||||||
|
<input
|
||||||
|
readonly={!search}
|
||||||
|
bind:value
|
||||||
|
bind:this={searchInput}
|
||||||
|
class="searchBox"
|
||||||
|
class:hide={!search}
|
||||||
|
{placeholder}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div class="title" class:hide={search}>
|
||||||
|
<Body size="S">{title}</Body>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
on:click={openSearch}
|
||||||
|
on:keydown={keyUtils.handleEnter(openSearch)}
|
||||||
|
class="searchButton"
|
||||||
|
class:hide={search}
|
||||||
|
>
|
||||||
|
<Icon size="S" name="Search" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
on:click={handleAddButton}
|
||||||
|
on:keydown={keyUtils.handleEnter(handleAddButton)}
|
||||||
|
class="addButton"
|
||||||
|
class:rotate={search}
|
||||||
|
>
|
||||||
|
<Icon name="Add" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.search {
|
||||||
|
transition: height 300ms ease-out;
|
||||||
|
max-height: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
flex-shrink: 0;
|
||||||
|
flex-direction: row;
|
||||||
|
position: relative;
|
||||||
|
height: 50px;
|
||||||
|
width: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
border-bottom: 2px solid transparent;
|
||||||
|
transition: border-bottom 130ms ease-out;
|
||||||
|
gap: var(--spacing-l);
|
||||||
|
}
|
||||||
|
|
||||||
|
.searchBox {
|
||||||
|
font-family: var(--font-sans);
|
||||||
|
color: var(--ink);
|
||||||
|
background-color: transparent;
|
||||||
|
border: none;
|
||||||
|
font-size: var(--spectrum-alias-font-size-default);
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
.searchBox:focus {
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
.searchBox::placeholder {
|
||||||
|
color: var(--spectrum-global-color-gray-600);
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
height: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
|
flex: 1;
|
||||||
|
opacity: 1;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.searchButton {
|
||||||
|
color: var(--grey-7);
|
||||||
|
cursor: pointer;
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
.searchButton:hover {
|
||||||
|
color: var(--ink);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hide {
|
||||||
|
opacity: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.addButton {
|
||||||
|
display: flex;
|
||||||
|
transition: transform 300ms ease-out;
|
||||||
|
color: var(--grey-7);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.addButton:hover {
|
||||||
|
color: var(--ink);
|
||||||
|
}
|
||||||
|
|
||||||
|
.rotate {
|
||||||
|
transform: rotate(45deg);
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -189,6 +189,7 @@
|
||||||
flex: 0 0 20px;
|
flex: 0 0 20px;
|
||||||
pointer-events: all;
|
pointer-events: all;
|
||||||
order: 0;
|
order: 0;
|
||||||
|
transition: transform 100ms linear;
|
||||||
}
|
}
|
||||||
.icon.arrow.absolute {
|
.icon.arrow.absolute {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
|
|
|
@ -20,73 +20,91 @@
|
||||||
export let allowedRoles = null
|
export let allowedRoles = null
|
||||||
export let allowCreator = false
|
export let allowCreator = false
|
||||||
export let fancySelect = false
|
export let fancySelect = false
|
||||||
|
export let labelPrefix = null
|
||||||
|
|
||||||
const dispatch = createEventDispatcher()
|
const dispatch = createEventDispatcher()
|
||||||
const RemoveID = "remove"
|
const RemoveID = "remove"
|
||||||
|
|
||||||
|
$: enrichLabel = label => (labelPrefix ? `${labelPrefix} ${label}` : label)
|
||||||
$: options = getOptions(
|
$: options = getOptions(
|
||||||
$roles,
|
$roles,
|
||||||
allowPublic,
|
allowPublic,
|
||||||
allowRemove,
|
allowRemove,
|
||||||
allowedRoles,
|
allowedRoles,
|
||||||
allowCreator
|
allowCreator,
|
||||||
|
enrichLabel
|
||||||
)
|
)
|
||||||
|
|
||||||
const getOptions = (
|
const getOptions = (
|
||||||
roles,
|
roles,
|
||||||
allowPublic,
|
allowPublic,
|
||||||
allowRemove,
|
allowRemove,
|
||||||
allowedRoles,
|
allowedRoles,
|
||||||
allowCreator
|
allowCreator,
|
||||||
|
enrichLabel
|
||||||
) => {
|
) => {
|
||||||
|
// Use roles whitelist if specified
|
||||||
if (allowedRoles?.length) {
|
if (allowedRoles?.length) {
|
||||||
const filteredRoles = roles.filter(role =>
|
let options = roles
|
||||||
allowedRoles.includes(role._id)
|
.filter(role => allowedRoles.includes(role._id))
|
||||||
)
|
.map(role => ({
|
||||||
return [
|
name: enrichLabel(role.name),
|
||||||
...filteredRoles,
|
_id: role._id,
|
||||||
...(allowedRoles.includes(Constants.Roles.CREATOR)
|
}))
|
||||||
? [{ _id: Constants.Roles.CREATOR, name: "Creator", enabled: false }]
|
if (allowedRoles.includes(Constants.Roles.CREATOR)) {
|
||||||
: []),
|
options.push({
|
||||||
]
|
|
||||||
}
|
|
||||||
let newRoles = [...roles]
|
|
||||||
|
|
||||||
if (allowCreator) {
|
|
||||||
newRoles = [
|
|
||||||
{
|
|
||||||
_id: Constants.Roles.CREATOR,
|
_id: Constants.Roles.CREATOR,
|
||||||
name: "Creator",
|
name: "Can edit",
|
||||||
tag:
|
enabled: false,
|
||||||
!$licensing.perAppBuildersEnabled &&
|
})
|
||||||
capitalise(Constants.PlanType.BUSINESS),
|
}
|
||||||
},
|
return options
|
||||||
...newRoles,
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Allow all core roles
|
||||||
|
let options = roles.map(role => ({
|
||||||
|
name: enrichLabel(role.name),
|
||||||
|
_id: role._id,
|
||||||
|
}))
|
||||||
|
|
||||||
|
// Add creator if required
|
||||||
|
if (allowCreator) {
|
||||||
|
options.unshift({
|
||||||
|
_id: Constants.Roles.CREATOR,
|
||||||
|
name: "Can edit",
|
||||||
|
tag:
|
||||||
|
!$licensing.perAppBuildersEnabled &&
|
||||||
|
capitalise(Constants.PlanType.BUSINESS),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add remove option if required
|
||||||
if (allowRemove) {
|
if (allowRemove) {
|
||||||
newRoles = [
|
options.push({
|
||||||
...newRoles,
|
_id: RemoveID,
|
||||||
{
|
name: "Remove",
|
||||||
_id: RemoveID,
|
})
|
||||||
name: "Remove",
|
|
||||||
},
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
if (allowPublic) {
|
|
||||||
return newRoles
|
// Remove public if not allowed
|
||||||
|
if (!allowPublic) {
|
||||||
|
options = options.filter(role => role._id !== Constants.Roles.PUBLIC)
|
||||||
}
|
}
|
||||||
return newRoles.filter(role => role._id !== Constants.Roles.PUBLIC)
|
|
||||||
|
return options
|
||||||
}
|
}
|
||||||
|
|
||||||
const getColor = role => {
|
const getColor = role => {
|
||||||
if (allowRemove && role._id === RemoveID) {
|
// Creator and remove options have no colors
|
||||||
|
if (role._id === Constants.Roles.CREATOR || role._id === RemoveID) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
return RoleUtils.getRoleColour(role._id)
|
return RoleUtils.getRoleColour(role._id)
|
||||||
}
|
}
|
||||||
|
|
||||||
const getIcon = role => {
|
const getIcon = role => {
|
||||||
if (allowRemove && role._id === RemoveID) {
|
// Only remove option has an icon
|
||||||
|
if (role._id === RemoveID) {
|
||||||
return "Close"
|
return "Close"
|
||||||
}
|
}
|
||||||
return null
|
return null
|
||||||
|
|
|
@ -9,18 +9,18 @@
|
||||||
Heading,
|
Heading,
|
||||||
Icon,
|
Icon,
|
||||||
} from "@budibase/bbui"
|
} from "@budibase/bbui"
|
||||||
import { createEventDispatcher, onMount } from "svelte"
|
import { createEventDispatcher, onMount, getContext } from "svelte"
|
||||||
import {
|
import {
|
||||||
isValid,
|
isValid,
|
||||||
decodeJSBinding,
|
decodeJSBinding,
|
||||||
encodeJSBinding,
|
encodeJSBinding,
|
||||||
|
convertToJS,
|
||||||
} from "@budibase/string-templates"
|
} from "@budibase/string-templates"
|
||||||
import {
|
import {
|
||||||
readableToRuntimeBinding,
|
readableToRuntimeBinding,
|
||||||
runtimeToReadableBinding,
|
runtimeToReadableBinding,
|
||||||
} from "builderStore/dataBinding"
|
} from "builderStore/dataBinding"
|
||||||
|
|
||||||
import { convertToJS } from "@budibase/string-templates"
|
|
||||||
import { admin } from "stores/portal"
|
import { admin } from "stores/portal"
|
||||||
import CodeEditor from "../CodeEditor/CodeEditor.svelte"
|
import CodeEditor from "../CodeEditor/CodeEditor.svelte"
|
||||||
import {
|
import {
|
||||||
|
@ -32,7 +32,6 @@
|
||||||
hbInsert,
|
hbInsert,
|
||||||
jsInsert,
|
jsInsert,
|
||||||
} from "../CodeEditor"
|
} from "../CodeEditor"
|
||||||
import { getContext } from "svelte"
|
|
||||||
import BindingPicker from "./BindingPicker.svelte"
|
import BindingPicker from "./BindingPicker.svelte"
|
||||||
|
|
||||||
const dispatch = createEventDispatcher()
|
const dispatch = createEventDispatcher()
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
<script>
|
<script>
|
||||||
import { capitalise } from "helpers"
|
import { capitalise } from "helpers"
|
||||||
|
|
||||||
export let value
|
export let value
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
<script>
|
<script>
|
||||||
import dayjs from "dayjs"
|
import dayjs from "dayjs"
|
||||||
|
|
||||||
export let value
|
export let value
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
|
@ -20,7 +20,12 @@
|
||||||
import analytics, { Events, EventSource } from "analytics"
|
import analytics, { Events, EventSource } from "analytics"
|
||||||
import { API } from "api"
|
import { API } from "api"
|
||||||
import { apps } from "stores/portal"
|
import { apps } from "stores/portal"
|
||||||
import { deploymentStore, store, isOnlyUser } from "builderStore"
|
import {
|
||||||
|
deploymentStore,
|
||||||
|
store,
|
||||||
|
isOnlyUser,
|
||||||
|
sortedScreens,
|
||||||
|
} from "builderStore"
|
||||||
import TourWrap from "components/portal/onboarding/TourWrap.svelte"
|
import TourWrap from "components/portal/onboarding/TourWrap.svelte"
|
||||||
import { TOUR_STEP_KEYS } from "components/portal/onboarding/tours.js"
|
import { TOUR_STEP_KEYS } from "components/portal/onboarding/tours.js"
|
||||||
import { goto } from "@roxi/routify"
|
import { goto } from "@roxi/routify"
|
||||||
|
@ -48,7 +53,7 @@
|
||||||
$store.upgradableVersion &&
|
$store.upgradableVersion &&
|
||||||
$store.version &&
|
$store.version &&
|
||||||
$store.upgradableVersion !== $store.version
|
$store.upgradableVersion !== $store.version
|
||||||
$: canPublish = !publishing && loaded
|
$: canPublish = !publishing && loaded && $sortedScreens.length > 0
|
||||||
$: lastDeployed = getLastDeployedString($deploymentStore)
|
$: lastDeployed = getLastDeployedString($deploymentStore)
|
||||||
|
|
||||||
const initialiseApp = async () => {
|
const initialiseApp = async () => {
|
||||||
|
@ -175,7 +180,12 @@
|
||||||
|
|
||||||
<div class="app-action-button preview">
|
<div class="app-action-button preview">
|
||||||
<div class="app-action">
|
<div class="app-action">
|
||||||
<ActionButton quiet icon="PlayCircle" on:click={previewApp}>
|
<ActionButton
|
||||||
|
disabled={$sortedScreens.length === 0}
|
||||||
|
quiet
|
||||||
|
icon="PlayCircle"
|
||||||
|
on:click={previewApp}
|
||||||
|
>
|
||||||
Preview
|
Preview
|
||||||
</ActionButton>
|
</ActionButton>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -11,6 +11,7 @@
|
||||||
export let onClickCloseButton
|
export let onClickCloseButton
|
||||||
export let borderLeft = false
|
export let borderLeft = false
|
||||||
export let borderRight = false
|
export let borderRight = false
|
||||||
|
export let borderBottomHeader = true
|
||||||
export let wide = false
|
export let wide = false
|
||||||
export let extraWide = false
|
export let extraWide = false
|
||||||
export let closeButtonIcon = "Close"
|
export let closeButtonIcon = "Close"
|
||||||
|
@ -27,7 +28,12 @@
|
||||||
class:borderLeft
|
class:borderLeft
|
||||||
class:borderRight
|
class:borderRight
|
||||||
>
|
>
|
||||||
<div class="header" class:noHeaderBorder class:custom={customHeaderContent}>
|
<div
|
||||||
|
class="header"
|
||||||
|
class:custom={customHeaderContent}
|
||||||
|
class:borderBottom={borderBottomHeader}
|
||||||
|
class:noHeaderBorder
|
||||||
|
>
|
||||||
{#if showBackButton}
|
{#if showBackButton}
|
||||||
<Icon name="ArrowLeft" hoverable on:click={onClickBackButton} />
|
<Icon name="ArrowLeft" hoverable on:click={onClickBackButton} />
|
||||||
{/if}
|
{/if}
|
||||||
|
@ -95,13 +101,15 @@
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding: 0 var(--spacing-l);
|
padding: 0 var(--spacing-l);
|
||||||
border-bottom: var(--border-light);
|
|
||||||
gap: var(--spacing-m);
|
gap: var(--spacing-m);
|
||||||
}
|
}
|
||||||
|
|
||||||
.noHeaderBorder {
|
.noHeaderBorder {
|
||||||
border-bottom: none !important;
|
border-bottom: none !important;
|
||||||
}
|
}
|
||||||
|
.header.borderBottom {
|
||||||
|
border-bottom: var(--border-light);
|
||||||
|
}
|
||||||
.title {
|
.title {
|
||||||
flex: 1 1 auto;
|
flex: 1 1 auto;
|
||||||
width: 0;
|
width: 0;
|
||||||
|
|
|
@ -20,7 +20,7 @@ import ValidationEditor from "./controls/ValidationEditor/ValidationEditor.svelt
|
||||||
import DrawerBindableInput from "components/common/bindings/DrawerBindableInput.svelte"
|
import DrawerBindableInput from "components/common/bindings/DrawerBindableInput.svelte"
|
||||||
import ColumnEditor from "./controls/ColumnEditor/ColumnEditor.svelte"
|
import ColumnEditor from "./controls/ColumnEditor/ColumnEditor.svelte"
|
||||||
import BasicColumnEditor from "./controls/ColumnEditor/BasicColumnEditor.svelte"
|
import BasicColumnEditor from "./controls/ColumnEditor/BasicColumnEditor.svelte"
|
||||||
import GridColumnEditor from "./controls/ColumnEditor/GridColumnEditor.svelte"
|
import GridColumnEditor from "./controls/GridColumnConfiguration/GridColumnConfiguration.svelte"
|
||||||
import BarButtonList from "./controls/BarButtonList.svelte"
|
import BarButtonList from "./controls/BarButtonList.svelte"
|
||||||
import FieldConfiguration from "./controls/FieldConfiguration/FieldConfiguration.svelte"
|
import FieldConfiguration from "./controls/FieldConfiguration/FieldConfiguration.svelte"
|
||||||
import ButtonConfiguration from "./controls/ButtonConfiguration/ButtonConfiguration.svelte"
|
import ButtonConfiguration from "./controls/ButtonConfiguration/ButtonConfiguration.svelte"
|
||||||
|
@ -28,6 +28,7 @@ import RelationshipFilterEditor from "./controls/RelationshipFilterEditor.svelte
|
||||||
|
|
||||||
const componentMap = {
|
const componentMap = {
|
||||||
text: DrawerBindableInput,
|
text: DrawerBindableInput,
|
||||||
|
plainText: Input,
|
||||||
select: Select,
|
select: Select,
|
||||||
radio: RadioGroup,
|
radio: RadioGroup,
|
||||||
dataSource: DataSourceSelect,
|
dataSource: DataSourceSelect,
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
<script>
|
<script>
|
||||||
import { ActionButton, Button, Drawer } from "@budibase/bbui"
|
import { ActionButton, Button, Drawer, notifications } from "@budibase/bbui"
|
||||||
import { createEventDispatcher } from "svelte"
|
import { createEventDispatcher } from "svelte"
|
||||||
import { notifications } from "@budibase/bbui"
|
|
||||||
import ButtonActionDrawer from "./ButtonActionDrawer.svelte"
|
import ButtonActionDrawer from "./ButtonActionDrawer.svelte"
|
||||||
import { cloneDeep } from "lodash/fp"
|
import { cloneDeep } from "lodash/fp"
|
||||||
|
|
||||||
|
|
|
@ -1,17 +1,19 @@
|
||||||
<script>
|
<script>
|
||||||
import { Select, Label } from "@budibase/bbui"
|
import { Select, Label } from "@budibase/bbui"
|
||||||
import { currentAsset, store } from "builderStore"
|
import { currentAsset, store } from "builderStore"
|
||||||
import { getActionProviderComponents } from "builderStore/dataBinding"
|
import { getActionProviders } from "builderStore/dataBinding"
|
||||||
import { onMount } from "svelte"
|
import { onMount } from "svelte"
|
||||||
import DrawerBindableInput from "components/common/bindings/DrawerBindableInput.svelte"
|
import DrawerBindableInput from "components/common/bindings/DrawerBindableInput.svelte"
|
||||||
|
|
||||||
export let parameters
|
export let parameters
|
||||||
export let bindings = []
|
export let bindings = []
|
||||||
|
export let nested
|
||||||
|
|
||||||
$: actionProviders = getActionProviderComponents(
|
$: actionProviders = getActionProviders(
|
||||||
$currentAsset,
|
$currentAsset,
|
||||||
$store.selectedComponentId,
|
$store.selectedComponentId,
|
||||||
"ChangeFormStep"
|
"ChangeFormStep",
|
||||||
|
{ includeSelf: nested }
|
||||||
)
|
)
|
||||||
|
|
||||||
const typeOptions = [
|
const typeOptions = [
|
||||||
|
@ -46,8 +48,8 @@
|
||||||
placeholder={null}
|
placeholder={null}
|
||||||
bind:value={parameters.componentId}
|
bind:value={parameters.componentId}
|
||||||
options={actionProviders}
|
options={actionProviders}
|
||||||
getOptionLabel={x => x._instanceName}
|
getOptionLabel={x => x.readableBinding}
|
||||||
getOptionValue={x => x._id}
|
getOptionValue={x => x.runtimeBinding}
|
||||||
/>
|
/>
|
||||||
<Label small>Step</Label>
|
<Label small>Step</Label>
|
||||||
<Select bind:value={parameters.type} options={typeOptions} />
|
<Select bind:value={parameters.type} options={typeOptions} />
|
||||||
|
|
|
@ -1,14 +1,16 @@
|
||||||
<script>
|
<script>
|
||||||
import { Select, Label } from "@budibase/bbui"
|
import { Select, Label } from "@budibase/bbui"
|
||||||
import { currentAsset, store } from "builderStore"
|
import { currentAsset, store } from "builderStore"
|
||||||
import { getActionProviderComponents } from "builderStore/dataBinding"
|
import { getActionProviders } from "builderStore/dataBinding"
|
||||||
|
|
||||||
export let parameters
|
export let parameters
|
||||||
|
export let nested
|
||||||
|
|
||||||
$: actionProviders = getActionProviderComponents(
|
$: actionProviders = getActionProviders(
|
||||||
$currentAsset,
|
$currentAsset,
|
||||||
$store.selectedComponentId,
|
$store.selectedComponentId,
|
||||||
"ClearForm"
|
"ClearForm",
|
||||||
|
{ includeSelf: nested }
|
||||||
)
|
)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
@ -17,8 +19,8 @@
|
||||||
<Select
|
<Select
|
||||||
bind:value={parameters.componentId}
|
bind:value={parameters.componentId}
|
||||||
options={actionProviders}
|
options={actionProviders}
|
||||||
getOptionLabel={x => x._instanceName}
|
getOptionLabel={x => x.readableBinding}
|
||||||
getOptionValue={x => x._id}
|
getOptionValue={x => x.runtimeBinding}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
import { Select, Body } from "@budibase/bbui"
|
import { Select, Body } from "@budibase/bbui"
|
||||||
import { onMount } from "svelte"
|
import { onMount } from "svelte"
|
||||||
import DrawerBindableInput from "components/common/bindings/DrawerBindableInput.svelte"
|
import DrawerBindableInput from "components/common/bindings/DrawerBindableInput.svelte"
|
||||||
|
|
||||||
export let parameters
|
export let parameters
|
||||||
export let bindings
|
export let bindings
|
||||||
|
|
||||||
|
|
|
@ -2,27 +2,20 @@
|
||||||
import { Select, Label, Body, Checkbox, Input } from "@budibase/bbui"
|
import { Select, Label, Body, Checkbox, Input } from "@budibase/bbui"
|
||||||
import { store, currentAsset } from "builderStore"
|
import { store, currentAsset } from "builderStore"
|
||||||
import { tables, viewsV2 } from "stores/backend"
|
import { tables, viewsV2 } from "stores/backend"
|
||||||
import {
|
import { getSchemaForDatasourcePlus } from "builderStore/dataBinding"
|
||||||
getContextProviderComponents,
|
|
||||||
getSchemaForDatasourcePlus,
|
|
||||||
} from "builderStore/dataBinding"
|
|
||||||
import SaveFields from "./SaveFields.svelte"
|
import SaveFields from "./SaveFields.svelte"
|
||||||
|
import { getDatasourceLikeProviders } from "components/design/settings/controls/ButtonActionEditor/actions/utils"
|
||||||
|
|
||||||
export let parameters
|
export let parameters
|
||||||
export let bindings = []
|
export let bindings = []
|
||||||
|
export let nested
|
||||||
|
|
||||||
$: formComponents = getContextProviderComponents(
|
$: providerOptions = getDatasourceLikeProviders({
|
||||||
$currentAsset,
|
asset: $currentAsset,
|
||||||
$store.selectedComponentId,
|
componentId: $store.selectedComponentId,
|
||||||
"form"
|
nested,
|
||||||
)
|
})
|
||||||
$: schemaComponents = getContextProviderComponents(
|
$: schemaFields = getSchemaFields(parameters?.tableId)
|
||||||
$currentAsset,
|
|
||||||
$store.selectedComponentId,
|
|
||||||
"schema"
|
|
||||||
)
|
|
||||||
$: providerOptions = getProviderOptions(formComponents, schemaComponents)
|
|
||||||
$: schemaFields = getSchemaFields($currentAsset, parameters?.tableId)
|
|
||||||
$: tableOptions = $tables.list.map(table => ({
|
$: tableOptions = $tables.list.map(table => ({
|
||||||
label: table.name,
|
label: table.name,
|
||||||
resourceId: table._id,
|
resourceId: table._id,
|
||||||
|
@ -33,44 +26,8 @@
|
||||||
}))
|
}))
|
||||||
$: options = [...(tableOptions || []), ...(viewOptions || [])]
|
$: options = [...(tableOptions || []), ...(viewOptions || [])]
|
||||||
|
|
||||||
// Gets a context definition of a certain type from a component definition
|
const getSchemaFields = resourceId => {
|
||||||
const extractComponentContext = (component, contextType) => {
|
const { schema } = getSchemaForDatasourcePlus(resourceId)
|
||||||
const def = store.actions.components.getDefinition(component?._component)
|
|
||||||
if (!def) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
const contexts = Array.isArray(def.context) ? def.context : [def.context]
|
|
||||||
return contexts.find(context => context?.type === contextType)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Gets options for valid context keys which provide valid data to submit
|
|
||||||
const getProviderOptions = (formComponents, schemaComponents) => {
|
|
||||||
const formContexts = formComponents.map(component => ({
|
|
||||||
component,
|
|
||||||
context: extractComponentContext(component, "form"),
|
|
||||||
}))
|
|
||||||
const schemaContexts = schemaComponents.map(component => ({
|
|
||||||
component,
|
|
||||||
context: extractComponentContext(component, "schema"),
|
|
||||||
}))
|
|
||||||
const allContexts = formContexts.concat(schemaContexts)
|
|
||||||
|
|
||||||
return allContexts.map(({ component, context }) => {
|
|
||||||
let runtimeBinding = component._id
|
|
||||||
if (context.suffix) {
|
|
||||||
runtimeBinding += `-${context.suffix}`
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
label: component._instanceName,
|
|
||||||
value: runtimeBinding,
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const getSchemaFields = (asset, tableId) => {
|
|
||||||
const { schema } = getSchemaForDatasourcePlus(tableId)
|
|
||||||
delete schema._id
|
|
||||||
delete schema._rev
|
|
||||||
return Object.values(schema || {})
|
return Object.values(schema || {})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,14 +1,16 @@
|
||||||
<script>
|
<script>
|
||||||
import { Select, Label } from "@budibase/bbui"
|
import { Select, Label } from "@budibase/bbui"
|
||||||
import { currentAsset, store } from "builderStore"
|
import { currentAsset, store } from "builderStore"
|
||||||
import { getActionProviderComponents } from "builderStore/dataBinding"
|
import { getActionProviders } from "builderStore/dataBinding"
|
||||||
|
|
||||||
export let parameters
|
export let parameters
|
||||||
|
export let nested
|
||||||
|
|
||||||
$: actionProviders = getActionProviderComponents(
|
$: actionProviders = getActionProviders(
|
||||||
$currentAsset,
|
$currentAsset,
|
||||||
$store.selectedComponentId,
|
$store.selectedComponentId,
|
||||||
"RefreshDatasource"
|
"RefreshDatasource",
|
||||||
|
{ includeSelf: nested }
|
||||||
)
|
)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
@ -17,8 +19,8 @@
|
||||||
<Select
|
<Select
|
||||||
bind:value={parameters.componentId}
|
bind:value={parameters.componentId}
|
||||||
options={actionProviders}
|
options={actionProviders}
|
||||||
getOptionLabel={x => x._instanceName}
|
getOptionLabel={x => x.readableBinding}
|
||||||
getOptionValue={x => x._id}
|
getOptionValue={x => x.runtimeBinding}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
@ -2,29 +2,19 @@
|
||||||
import { Select, Label, Body, Checkbox, Input } from "@budibase/bbui"
|
import { Select, Label, Body, Checkbox, Input } from "@budibase/bbui"
|
||||||
import { store, currentAsset } from "builderStore"
|
import { store, currentAsset } from "builderStore"
|
||||||
import { tables, viewsV2 } from "stores/backend"
|
import { tables, viewsV2 } from "stores/backend"
|
||||||
import {
|
import { getSchemaForDatasourcePlus } from "builderStore/dataBinding"
|
||||||
getContextProviderComponents,
|
|
||||||
getSchemaForDatasourcePlus,
|
|
||||||
} from "builderStore/dataBinding"
|
|
||||||
import SaveFields from "./SaveFields.svelte"
|
import SaveFields from "./SaveFields.svelte"
|
||||||
|
import { getDatasourceLikeProviders } from "components/design/settings/controls/ButtonActionEditor/actions/utils"
|
||||||
|
|
||||||
export let parameters
|
export let parameters
|
||||||
export let bindings = []
|
export let bindings = []
|
||||||
export let nested
|
export let nested
|
||||||
|
|
||||||
$: formComponents = getContextProviderComponents(
|
$: providerOptions = getDatasourceLikeProviders({
|
||||||
$currentAsset,
|
asset: $currentAsset,
|
||||||
$store.selectedComponentId,
|
componentId: $store.selectedComponentId,
|
||||||
"form",
|
nested,
|
||||||
{ includeSelf: nested }
|
})
|
||||||
)
|
|
||||||
$: schemaComponents = getContextProviderComponents(
|
|
||||||
$currentAsset,
|
|
||||||
$store.selectedComponentId,
|
|
||||||
"schema",
|
|
||||||
{ includeSelf: nested }
|
|
||||||
)
|
|
||||||
$: providerOptions = getProviderOptions(formComponents, schemaComponents)
|
|
||||||
$: schemaFields = getSchemaFields(parameters?.tableId)
|
$: schemaFields = getSchemaFields(parameters?.tableId)
|
||||||
$: tableOptions = $tables.list.map(table => ({
|
$: tableOptions = $tables.list.map(table => ({
|
||||||
label: table.name,
|
label: table.name,
|
||||||
|
@ -36,40 +26,6 @@
|
||||||
}))
|
}))
|
||||||
$: options = [...(tableOptions || []), ...(viewOptions || [])]
|
$: options = [...(tableOptions || []), ...(viewOptions || [])]
|
||||||
|
|
||||||
// Gets a context definition of a certain type from a component definition
|
|
||||||
const extractComponentContext = (component, contextType) => {
|
|
||||||
const def = store.actions.components.getDefinition(component?._component)
|
|
||||||
if (!def) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
const contexts = Array.isArray(def.context) ? def.context : [def.context]
|
|
||||||
return contexts.find(context => context?.type === contextType)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Gets options for valid context keys which provide valid data to submit
|
|
||||||
const getProviderOptions = (formComponents, schemaComponents) => {
|
|
||||||
const formContexts = formComponents.map(component => ({
|
|
||||||
component,
|
|
||||||
context: extractComponentContext(component, "form"),
|
|
||||||
}))
|
|
||||||
const schemaContexts = schemaComponents.map(component => ({
|
|
||||||
component,
|
|
||||||
context: extractComponentContext(component, "schema"),
|
|
||||||
}))
|
|
||||||
const allContexts = formContexts.concat(schemaContexts)
|
|
||||||
|
|
||||||
return allContexts.map(({ component, context }) => {
|
|
||||||
let runtimeBinding = component._id
|
|
||||||
if (context.suffix) {
|
|
||||||
runtimeBinding += `-${context.suffix}`
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
label: component._instanceName,
|
|
||||||
value: runtimeBinding,
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const getSchemaFields = resourceId => {
|
const getSchemaFields = resourceId => {
|
||||||
const { schema } = getSchemaForDatasourcePlus(resourceId)
|
const { schema } = getSchemaForDatasourcePlus(resourceId)
|
||||||
return Object.values(schema || {})
|
return Object.values(schema || {})
|
||||||
|
|
|
@ -1,22 +1,36 @@
|
||||||
<script>
|
<script>
|
||||||
import { currentAsset, store } from "builderStore"
|
import { currentAsset, store } from "builderStore"
|
||||||
import { Label, Combobox, Select } from "@budibase/bbui"
|
import { Label, Combobox, Select } from "@budibase/bbui"
|
||||||
import {
|
import { getActionProviders, buildFormSchema } from "builderStore/dataBinding"
|
||||||
getActionProviderComponents,
|
|
||||||
buildFormSchema,
|
|
||||||
} from "builderStore/dataBinding"
|
|
||||||
import { findComponent } from "builderStore/componentUtils"
|
import { findComponent } from "builderStore/componentUtils"
|
||||||
|
|
||||||
export let parameters
|
export let parameters
|
||||||
|
export let nested
|
||||||
|
|
||||||
$: formComponent = findComponent($currentAsset.props, parameters.componentId)
|
$: formComponent = getFormComponent(
|
||||||
|
$currentAsset.props,
|
||||||
|
parameters.componentId
|
||||||
|
)
|
||||||
$: formSchema = buildFormSchema(formComponent)
|
$: formSchema = buildFormSchema(formComponent)
|
||||||
$: fieldOptions = Object.keys(formSchema || {})
|
$: fieldOptions = Object.keys(formSchema || {})
|
||||||
$: actionProviders = getActionProviderComponents(
|
$: actionProviders = getActionProviders(
|
||||||
$currentAsset,
|
$currentAsset,
|
||||||
$store.selectedComponentId,
|
$store.selectedComponentId,
|
||||||
"ScrollTo"
|
"ScrollTo",
|
||||||
|
{ includeSelf: nested }
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const getFormComponent = (asset, id) => {
|
||||||
|
let component = findComponent(asset, id)
|
||||||
|
if (component) {
|
||||||
|
return component
|
||||||
|
}
|
||||||
|
// Check for block component IDs, and use the block itself instead
|
||||||
|
if (id?.includes("-")) {
|
||||||
|
return findComponent(asset, id.split("-")[0])
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="root">
|
<div class="root">
|
||||||
|
@ -24,8 +38,8 @@
|
||||||
<Select
|
<Select
|
||||||
bind:value={parameters.componentId}
|
bind:value={parameters.componentId}
|
||||||
options={actionProviders}
|
options={actionProviders}
|
||||||
getOptionLabel={x => x._instanceName}
|
getOptionLabel={x => x.readableBinding}
|
||||||
getOptionValue={x => x._id}
|
getOptionValue={x => x.runtimeBinding}
|
||||||
/>
|
/>
|
||||||
<Label small>Field</Label>
|
<Label small>Field</Label>
|
||||||
<Combobox bind:value={parameters.field} options={fieldOptions} />
|
<Combobox bind:value={parameters.field} options={fieldOptions} />
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue