Merge remote-tracking branch 'origin/master' into feature/multistep-form-block
This commit is contained in:
commit
1091ba0c86
|
@ -99,11 +99,6 @@ jobs:
|
||||||
else
|
else
|
||||||
yarn test --ignore=@budibase/worker --ignore=@budibase/server --ignore=@budibase/pro
|
yarn test --ignore=@budibase/worker --ignore=@budibase/server --ignore=@budibase/pro
|
||||||
fi
|
fi
|
||||||
- uses: codecov/codecov-action@v3
|
|
||||||
with:
|
|
||||||
token: ${{ secrets.CODECOV_TOKEN }} # not required for public repos
|
|
||||||
name: codecov-umbrella
|
|
||||||
verbose: true
|
|
||||||
|
|
||||||
test-worker:
|
test-worker:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
@ -129,12 +124,6 @@ jobs:
|
||||||
yarn test --scope=@budibase/worker
|
yarn test --scope=@budibase/worker
|
||||||
fi
|
fi
|
||||||
|
|
||||||
- uses: codecov/codecov-action@v3
|
|
||||||
with:
|
|
||||||
token: ${{ secrets.CODECOV_TOKEN || github.token }} # not required for public repos
|
|
||||||
name: codecov-umbrella
|
|
||||||
verbose: true
|
|
||||||
|
|
||||||
test-server:
|
test-server:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
|
@ -159,12 +148,6 @@ jobs:
|
||||||
yarn test --scope=@budibase/server
|
yarn test --scope=@budibase/server
|
||||||
fi
|
fi
|
||||||
|
|
||||||
- uses: codecov/codecov-action@v3
|
|
||||||
with:
|
|
||||||
token: ${{ secrets.CODECOV_TOKEN || github.token }} # not required for public repos
|
|
||||||
name: codecov-umbrella
|
|
||||||
verbose: true
|
|
||||||
|
|
||||||
test-pro:
|
test-pro:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'Budibase/budibase'
|
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'Budibase/budibase'
|
||||||
|
|
|
@ -7,8 +7,8 @@ metadata:
|
||||||
kubernetes.io/ingress.class: alb
|
kubernetes.io/ingress.class: alb
|
||||||
alb.ingress.kubernetes.io/scheme: internet-facing
|
alb.ingress.kubernetes.io/scheme: internet-facing
|
||||||
alb.ingress.kubernetes.io/target-type: ip
|
alb.ingress.kubernetes.io/target-type: ip
|
||||||
alb.ingress.kubernetes.io/success-codes: 200,301
|
alb.ingress.kubernetes.io/success-codes: '200'
|
||||||
alb.ingress.kubernetes.io/healthcheck-path: /
|
alb.ingress.kubernetes.io/healthcheck-path: '/health'
|
||||||
{{- if .Values.ingress.certificateArn }}
|
{{- if .Values.ingress.certificateArn }}
|
||||||
alb.ingress.kubernetes.io/actions.ssl-redirect: '{"Type": "redirect", "RedirectConfig": { "Protocol": "HTTPS", "Port": "443", "StatusCode": "HTTP_301"}}'
|
alb.ingress.kubernetes.io/actions.ssl-redirect: '{"Type": "redirect", "RedirectConfig": { "Protocol": "HTTPS", "Port": "443", "StatusCode": "HTTP_301"}}'
|
||||||
alb.ingress.kubernetes.io/listen-ports: '[{"HTTP": 80}, {"HTTPS":443}]'
|
alb.ingress.kubernetes.io/listen-ports: '[{"HTTP": 80}, {"HTTPS":443}]'
|
||||||
|
|
|
@ -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,73 @@
|
||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
|
|
||||||
DATA_DIR=${DATA_DIR:-/data}
|
DATA_DIR=${DATA_DIR:-/data}
|
||||||
|
|
||||||
mkdir -p ${DATA_DIR}
|
mkdir -p ${DATA_DIR}
|
||||||
mkdir -p ${DATA_DIR}/couch/{dbs,views}
|
mkdir -p ${DATA_DIR}/couch/{dbs,views}
|
||||||
mkdir -p ${DATA_DIR}/search
|
mkdir -p ${DATA_DIR}/search
|
||||||
chown -R couchdb:couchdb ${DATA_DIR}/couch
|
chown -R couchdb:couchdb ${DATA_DIR}/couch
|
||||||
/build-target-paths.sh
|
|
||||||
|
echo ${TARGETBUILD} > /buildtarget.txt
|
||||||
|
if [[ "${TARGETBUILD}" = "aas" ]]; then
|
||||||
|
# Azure AppService uses /home for persistent data & SSH on port 2222
|
||||||
|
DATA_DIR="${DATA_DIR:-/home}"
|
||||||
|
WEBSITES_ENABLE_APP_SERVICE_STORAGE=true
|
||||||
|
mkdir -p $DATA_DIR/{search,minio,couch}
|
||||||
|
mkdir -p $DATA_DIR/couch/{dbs,views}
|
||||||
|
chown -R couchdb:couchdb $DATA_DIR/couch/
|
||||||
|
apt update
|
||||||
|
apt-get install -y openssh-server
|
||||||
|
echo "root:Docker!" | chpasswd
|
||||||
|
mkdir -p /tmp
|
||||||
|
chmod +x /tmp/ssh_setup.sh \
|
||||||
|
&& (sleep 1;/tmp/ssh_setup.sh 2>&1 > /dev/null)
|
||||||
|
cp /etc/sshd_config /etc/ssh/sshd_config
|
||||||
|
/etc/init.d/ssh restart
|
||||||
|
sed -i "s#DATA_DIR#/home#g" /opt/clouseau/clouseau.ini
|
||||||
|
sed -i "s#DATA_DIR#/home#g" /opt/couchdb/etc/local.ini
|
||||||
|
elif [[ "${TARGETBUILD}" = "single" ]]; then
|
||||||
|
# In the single image build, the Dockerfile specifies /data as a volume
|
||||||
|
# mount, so we use that for all persistent data.
|
||||||
|
sed -i "s#DATA_DIR#/data#g" /opt/clouseau/clouseau.ini
|
||||||
|
sed -i "s#DATA_DIR#/data#g" /opt/couchdb/etc/local.ini
|
||||||
|
elif [[ -n $KUBERNETES_SERVICE_HOST ]]; then
|
||||||
|
# In Kubernetes the directory /opt/couchdb/data has a persistent volume
|
||||||
|
# mount for storing database data.
|
||||||
|
sed -i "s#DATA_DIR#/opt/couchdb/data#g" /opt/clouseau/clouseau.ini
|
||||||
|
|
||||||
|
# We remove the database_dir and view_index_dir settings from the local.ini
|
||||||
|
# in Kubernetes because it will default to /opt/couchdb/data which is what
|
||||||
|
# our Helm chart was using prior to us switching to using our own CouchDB
|
||||||
|
# image.
|
||||||
|
sed -i "s#^database_dir.*\$##g" /opt/couchdb/etc/local.ini
|
||||||
|
sed -i "s#^view_index_dir.*\$##g" /opt/couchdb/etc/local.ini
|
||||||
|
|
||||||
|
# We remove the -name setting from the vm.args file in Kubernetes because
|
||||||
|
# it will default to the pod FQDN, which is what's required for clustering
|
||||||
|
# to work.
|
||||||
|
sed -i "s/^-name .*$//g" /opt/couchdb/etc/vm.args
|
||||||
|
else
|
||||||
|
# For all other builds, we use /data for persistent data.
|
||||||
|
sed -i "s#DATA_DIR#/data#g" /opt/clouseau/clouseau.ini
|
||||||
|
sed -i "s#DATA_DIR#/data#g" /opt/couchdb/etc/local.ini
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Start Clouseau. Budibase won't function correctly without Clouseau running, it
|
||||||
|
# powers the search API endpoints which are used to do all sorts, including
|
||||||
|
# populating app grids.
|
||||||
/opt/clouseau/bin/clouseau > /dev/stdout 2>&1 &
|
/opt/clouseau/bin/clouseau > /dev/stdout 2>&1 &
|
||||||
|
|
||||||
|
# Start CouchDB.
|
||||||
/docker-entrypoint.sh /opt/couchdb/bin/couchdb &
|
/docker-entrypoint.sh /opt/couchdb/bin/couchdb &
|
||||||
sleep 10
|
|
||||||
|
# Wati for CouchDB to start up.
|
||||||
|
while [[ $(curl -s -w "%{http_code}\n" http://localhost:5984/_up -o /dev/null) -ne 200 ]]; do
|
||||||
|
echo 'Waiting for CouchDB to start...';
|
||||||
|
sleep 5;
|
||||||
|
done
|
||||||
|
|
||||||
|
# CouchDB needs the `_users` and `_replicator` databases to exist before it will
|
||||||
|
# function correctly, so we create them here.
|
||||||
curl -X PUT http://${COUCHDB_USER}:${COUCHDB_PASSWORD}@localhost:5984/_users
|
curl -X PUT http://${COUCHDB_USER}:${COUCHDB_PASSWORD}@localhost:5984/_users
|
||||||
curl -X PUT http://${COUCHDB_USER}:${COUCHDB_PASSWORD}@localhost:5984/_replicator
|
curl -X PUT http://${COUCHDB_USER}:${COUCHDB_PASSWORD}@localhost:5984/_replicator
|
||||||
sleep infinity
|
sleep infinity
|
|
@ -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,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"]
|
|
|
@ -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.15",
|
"version": "2.13.23",
|
||||||
"npmClient": "yarn",
|
"npmClient": "yarn",
|
||||||
"packages": [
|
"packages": [
|
||||||
"packages/*"
|
"packages/*"
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
* @Budibase/backend
|
|
@ -260,12 +260,12 @@ export async function listAllObjects(bucketName: string, path: string) {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generate a presigned url with a default TTL of 1 hour
|
* Generate a presigned url with a default TTL of 36 hours
|
||||||
*/
|
*/
|
||||||
export function getPresignedUrl(
|
export function getPresignedUrl(
|
||||||
bucketName: string,
|
bucketName: string,
|
||||||
key: string,
|
key: string,
|
||||||
durationSeconds: number = 3600
|
durationSeconds: number = 129600
|
||||||
) {
|
) {
|
||||||
const objectStore = ObjectStore(bucketName, { presigning: true })
|
const objectStore = ObjectStore(bucketName, { presigning: true })
|
||||||
const params = {
|
const params = {
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -146,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) {
|
||||||
|
@ -209,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
|
||||||
|
|
|
@ -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 }
|
||||||
|
|
||||||
|
@ -336,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 })
|
||||||
|
}
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
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"
|
||||||
|
|
|
@ -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}
|
||||||
|
|
|
@ -18,6 +18,7 @@
|
||||||
checked={value}
|
checked={value}
|
||||||
{disabled}
|
{disabled}
|
||||||
on:change={onChange}
|
on:change={onChange}
|
||||||
|
on:click
|
||||||
{id}
|
{id}
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
class="spectrum-Switch-input"
|
class="spectrum-Switch-input"
|
||||||
|
|
|
@ -20,7 +20,7 @@
|
||||||
let focus = false
|
let focus = false
|
||||||
|
|
||||||
const updateValue = newValue => {
|
const updateValue = newValue => {
|
||||||
if (readonly) {
|
if (readonly || disabled) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (type === "number") {
|
if (type === "number") {
|
||||||
|
@ -31,14 +31,14 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
const onFocus = () => {
|
const onFocus = () => {
|
||||||
if (readonly) {
|
if (readonly || disabled) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
focus = true
|
focus = true
|
||||||
}
|
}
|
||||||
|
|
||||||
const onBlur = event => {
|
const onBlur = event => {
|
||||||
if (readonly) {
|
if (readonly || disabled) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
focus = false
|
focus = false
|
||||||
|
@ -46,14 +46,14 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
const onInput = event => {
|
const onInput = event => {
|
||||||
if (readonly || !updateOnChange) {
|
if (readonly || !updateOnChange || disabled) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
updateValue(event.target.value)
|
updateValue(event.target.value)
|
||||||
}
|
}
|
||||||
|
|
||||||
const updateValueOnEnter = event => {
|
const updateValueOnEnter = event => {
|
||||||
if (readonly) {
|
if (readonly || disabled) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (event.key === "Enter") {
|
if (event.key === "Enter") {
|
||||||
|
@ -69,6 +69,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
|
if (disabled) return
|
||||||
focus = autofocus
|
focus = autofocus
|
||||||
if (focus) field.focus()
|
if (focus) field.focus()
|
||||||
})
|
})
|
||||||
|
@ -108,4 +109,16 @@
|
||||||
.spectrum-Textfield {
|
.spectrum-Textfield {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
input::placeholder {
|
||||||
|
color: var(--grey-7);
|
||||||
|
}
|
||||||
|
|
||||||
|
input:hover::placeholder {
|
||||||
|
color: var(--grey-7) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
input:focus::placeholder {
|
||||||
|
color: var(--grey-7) !important;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -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}
|
||||||
|
|
|
@ -19,5 +19,5 @@
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Field {helpText} {label} {labelPosition} {error}>
|
<Field {helpText} {label} {labelPosition} {error}>
|
||||||
<Switch {error} {disabled} {text} {value} on:change={onChange} />
|
<Switch {error} {disabled} {text} {value} on:change={onChange} on:click />
|
||||||
</Field>
|
</Field>
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import { store } from "./index"
|
import { store } from "./index"
|
||||||
|
import { get } from "svelte/store"
|
||||||
import { Helpers } from "@budibase/bbui"
|
import { Helpers } from "@budibase/bbui"
|
||||||
import {
|
import {
|
||||||
decodeJSBinding,
|
decodeJSBinding,
|
||||||
|
@ -238,6 +239,10 @@ export const makeComponentUnique = component => {
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getComponentText = component => {
|
export const getComponentText = component => {
|
||||||
|
if (component == null) {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
if (component?._instanceName) {
|
if (component?._instanceName) {
|
||||||
return component._instanceName
|
return component._instanceName
|
||||||
}
|
}
|
||||||
|
@ -246,3 +251,16 @@ export const getComponentText = component => {
|
||||||
"component"
|
"component"
|
||||||
return capitalise(type)
|
return capitalise(type)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const getComponentName = component => {
|
||||||
|
if (component == null) {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
const components = get(store)?.components || {}
|
||||||
|
const componentDefinition = components[component._component] || {}
|
||||||
|
const name =
|
||||||
|
componentDefinition.friendlyName || componentDefinition.name || ""
|
||||||
|
|
||||||
|
return name
|
||||||
|
}
|
||||||
|
|
|
@ -29,6 +29,12 @@ const CAPTURE_VAR_INSIDE_TEMPLATE = /{{([^}]+)}}/g
|
||||||
const CAPTURE_VAR_INSIDE_JS = /\$\("([^")]+)"\)/g
|
const CAPTURE_VAR_INSIDE_JS = /\$\("([^")]+)"\)/g
|
||||||
const CAPTURE_HBS_TEMPLATE = /{{[\S\s]*?}}/g
|
const CAPTURE_HBS_TEMPLATE = /{{[\S\s]*?}}/g
|
||||||
|
|
||||||
|
const UpdateReferenceAction = {
|
||||||
|
ADD: "add",
|
||||||
|
DELETE: "delete",
|
||||||
|
MOVE: "move",
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets all bindable data context fields and instance fields.
|
* Gets all bindable data context fields and instance fields.
|
||||||
*/
|
*/
|
||||||
|
@ -1226,3 +1232,81 @@ export const runtimeToReadableBinding = (
|
||||||
"readableBinding"
|
"readableBinding"
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Used to update binding references for automation or action steps
|
||||||
|
*
|
||||||
|
* @param obj - The object to be updated
|
||||||
|
* @param originalIndex - The original index of the step being moved. Not applicable to add/delete.
|
||||||
|
* @param modifiedIndex - The new index of the step being modified
|
||||||
|
* @param action - Used to determine if a step is being added, deleted or moved
|
||||||
|
* @param label - The binding text that describes the steps
|
||||||
|
*/
|
||||||
|
export const updateReferencesInObject = ({
|
||||||
|
obj,
|
||||||
|
modifiedIndex,
|
||||||
|
action,
|
||||||
|
label,
|
||||||
|
originalIndex,
|
||||||
|
}) => {
|
||||||
|
const stepIndexRegex = new RegExp(`{{\\s*${label}\\.(\\d+)\\.`, "g")
|
||||||
|
const updateActionStep = (str, index, replaceWith) =>
|
||||||
|
str.replace(`{{ ${label}.${index}.`, `{{ ${label}.${replaceWith}.`)
|
||||||
|
for (const key in obj) {
|
||||||
|
if (typeof obj[key] === "string") {
|
||||||
|
let matches
|
||||||
|
while ((matches = stepIndexRegex.exec(obj[key])) !== null) {
|
||||||
|
const referencedStep = parseInt(matches[1])
|
||||||
|
if (
|
||||||
|
action === UpdateReferenceAction.ADD &&
|
||||||
|
referencedStep >= modifiedIndex
|
||||||
|
) {
|
||||||
|
obj[key] = updateActionStep(
|
||||||
|
obj[key],
|
||||||
|
referencedStep,
|
||||||
|
referencedStep + 1
|
||||||
|
)
|
||||||
|
} else if (
|
||||||
|
action === UpdateReferenceAction.DELETE &&
|
||||||
|
referencedStep > modifiedIndex
|
||||||
|
) {
|
||||||
|
obj[key] = updateActionStep(
|
||||||
|
obj[key],
|
||||||
|
referencedStep,
|
||||||
|
referencedStep - 1
|
||||||
|
)
|
||||||
|
} else if (action === UpdateReferenceAction.MOVE) {
|
||||||
|
if (referencedStep === originalIndex) {
|
||||||
|
obj[key] = updateActionStep(obj[key], referencedStep, modifiedIndex)
|
||||||
|
} else if (
|
||||||
|
modifiedIndex <= referencedStep &&
|
||||||
|
modifiedIndex < originalIndex
|
||||||
|
) {
|
||||||
|
obj[key] = updateActionStep(
|
||||||
|
obj[key],
|
||||||
|
referencedStep,
|
||||||
|
referencedStep + 1
|
||||||
|
)
|
||||||
|
} else if (
|
||||||
|
modifiedIndex >= referencedStep &&
|
||||||
|
modifiedIndex > originalIndex
|
||||||
|
) {
|
||||||
|
obj[key] = updateActionStep(
|
||||||
|
obj[key],
|
||||||
|
referencedStep,
|
||||||
|
referencedStep - 1
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (typeof obj[key] === "object" && obj[key] !== null) {
|
||||||
|
updateReferencesInObject({
|
||||||
|
obj: obj[key],
|
||||||
|
modifiedIndex,
|
||||||
|
action,
|
||||||
|
label,
|
||||||
|
originalIndex,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -4,7 +4,7 @@ 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, get } from "svelte/store"
|
import { derived, 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"
|
||||||
|
@ -146,5 +146,3 @@ export const userSelectedResourceMap = derived(userStore, $userStore => {
|
||||||
export const isOnlyUser = derived(userStore, $userStore => {
|
export const isOnlyUser = derived(userStore, $userStore => {
|
||||||
return $userStore.length < 2
|
return $userStore.length < 2
|
||||||
})
|
})
|
||||||
|
|
||||||
export const screensHeight = writable("210px")
|
|
||||||
|
|
|
@ -4,6 +4,7 @@ import { cloneDeep } from "lodash/fp"
|
||||||
import { generate } from "shortid"
|
import { generate } from "shortid"
|
||||||
import { selectedAutomation } from "builderStore"
|
import { selectedAutomation } from "builderStore"
|
||||||
import { notifications } from "@budibase/bbui"
|
import { notifications } from "@budibase/bbui"
|
||||||
|
import { updateReferencesInObject } from "builderStore/dataBinding"
|
||||||
|
|
||||||
const initialAutomationState = {
|
const initialAutomationState = {
|
||||||
automations: [],
|
automations: [],
|
||||||
|
@ -22,34 +23,14 @@ export const getAutomationStore = () => {
|
||||||
return store
|
return store
|
||||||
}
|
}
|
||||||
|
|
||||||
const updateReferencesInObject = (obj, modifiedIndex, action) => {
|
|
||||||
const regex = /{{\s*steps\.(\d+)\./g
|
|
||||||
for (const key in obj) {
|
|
||||||
if (typeof obj[key] === "string") {
|
|
||||||
let matches
|
|
||||||
while ((matches = regex.exec(obj[key])) !== null) {
|
|
||||||
const referencedStep = parseInt(matches[1])
|
|
||||||
if (action === "add" && referencedStep >= modifiedIndex) {
|
|
||||||
obj[key] = obj[key].replace(
|
|
||||||
`{{ steps.${referencedStep}.`,
|
|
||||||
`{{ steps.${referencedStep + 1}.`
|
|
||||||
)
|
|
||||||
} else if (action === "delete" && referencedStep > modifiedIndex) {
|
|
||||||
obj[key] = obj[key].replace(
|
|
||||||
`{{ steps.${referencedStep}.`,
|
|
||||||
`{{ steps.${referencedStep - 1}.`
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if (typeof obj[key] === "object" && obj[key] !== null) {
|
|
||||||
updateReferencesInObject(obj[key], modifiedIndex, action)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const updateStepReferences = (steps, modifiedIndex, action) => {
|
const updateStepReferences = (steps, modifiedIndex, action) => {
|
||||||
steps.forEach(step => {
|
steps.forEach(step => {
|
||||||
updateReferencesInObject(step.inputs, modifiedIndex, action)
|
updateReferencesInObject({
|
||||||
|
obj: step.inputs,
|
||||||
|
modifiedIndex,
|
||||||
|
action,
|
||||||
|
label: "steps",
|
||||||
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -2,6 +2,7 @@ import { expect, describe, it, vi } from "vitest"
|
||||||
import {
|
import {
|
||||||
runtimeToReadableBinding,
|
runtimeToReadableBinding,
|
||||||
readableToRuntimeBinding,
|
readableToRuntimeBinding,
|
||||||
|
updateReferencesInObject,
|
||||||
} from "../dataBinding"
|
} from "../dataBinding"
|
||||||
|
|
||||||
vi.mock("@budibase/frontend-core")
|
vi.mock("@budibase/frontend-core")
|
||||||
|
@ -84,3 +85,461 @@ describe("readableToRuntimeBinding", () => {
|
||||||
).toEqual(`Hello {{ [user].[firstName] }}! The count is {{ count }}.`)
|
).toEqual(`Hello {{ [user].[firstName] }}! The count is {{ count }}.`)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe("updateReferencesInObject", () => {
|
||||||
|
it("should increment steps in sequence on 'add'", () => {
|
||||||
|
let obj = [
|
||||||
|
{
|
||||||
|
id: "a0",
|
||||||
|
parameters: {
|
||||||
|
text: "Alpha",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "a1",
|
||||||
|
parameters: {
|
||||||
|
text: "Apple",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "b2",
|
||||||
|
parameters: {
|
||||||
|
text: "Banana {{ actions.1.row }}",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "c3",
|
||||||
|
parameters: {
|
||||||
|
text: "Carrot {{ actions.1.row }}",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "d4",
|
||||||
|
parameters: {
|
||||||
|
text: "Dog {{ actions.3.row }}",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "e5",
|
||||||
|
parameters: {
|
||||||
|
text: "Eagle {{ actions.4.row }}",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
updateReferencesInObject({
|
||||||
|
obj,
|
||||||
|
modifiedIndex: 0,
|
||||||
|
action: "add",
|
||||||
|
label: "actions",
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(obj).toEqual([
|
||||||
|
{
|
||||||
|
id: "a0",
|
||||||
|
parameters: {
|
||||||
|
text: "Alpha",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "a1",
|
||||||
|
parameters: {
|
||||||
|
text: "Apple",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "b2",
|
||||||
|
parameters: {
|
||||||
|
text: "Banana {{ actions.2.row }}",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "c3",
|
||||||
|
parameters: {
|
||||||
|
text: "Carrot {{ actions.2.row }}",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "d4",
|
||||||
|
parameters: {
|
||||||
|
text: "Dog {{ actions.4.row }}",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "e5",
|
||||||
|
parameters: {
|
||||||
|
text: "Eagle {{ actions.5.row }}",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should decrement steps in sequence on 'delete'", () => {
|
||||||
|
let obj = [
|
||||||
|
{
|
||||||
|
id: "a1",
|
||||||
|
parameters: {
|
||||||
|
text: "Apple",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "b2",
|
||||||
|
parameters: {
|
||||||
|
text: "Banana {{ actions.1.row }}",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "d4",
|
||||||
|
parameters: {
|
||||||
|
text: "Dog {{ actions.3.row }}",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "e5",
|
||||||
|
parameters: {
|
||||||
|
text: "Eagle {{ actions.4.row }}",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
updateReferencesInObject({
|
||||||
|
obj,
|
||||||
|
modifiedIndex: 2,
|
||||||
|
action: "delete",
|
||||||
|
label: "actions",
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(obj).toEqual([
|
||||||
|
{
|
||||||
|
id: "a1",
|
||||||
|
parameters: {
|
||||||
|
text: "Apple",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "b2",
|
||||||
|
parameters: {
|
||||||
|
text: "Banana {{ actions.1.row }}",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "d4",
|
||||||
|
parameters: {
|
||||||
|
text: "Dog {{ actions.2.row }}",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "e5",
|
||||||
|
parameters: {
|
||||||
|
text: "Eagle {{ actions.3.row }}",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should handle on 'move' to a lower index", () => {
|
||||||
|
let obj = [
|
||||||
|
{
|
||||||
|
id: "a1",
|
||||||
|
parameters: {
|
||||||
|
text: "Apple",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "b2",
|
||||||
|
parameters: {
|
||||||
|
text: "Banana {{ actions.0.row }}",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "e5",
|
||||||
|
parameters: {
|
||||||
|
text: "Eagle {{ actions.3.row }}",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "c3",
|
||||||
|
parameters: {
|
||||||
|
text: "Carrot {{ actions.0.row }}",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "d4",
|
||||||
|
parameters: {
|
||||||
|
text: "Dog {{ actions.2.row }}",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
updateReferencesInObject({
|
||||||
|
obj,
|
||||||
|
modifiedIndex: 2,
|
||||||
|
action: "move",
|
||||||
|
label: "actions",
|
||||||
|
originalIndex: 4,
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(obj).toEqual([
|
||||||
|
{
|
||||||
|
id: "a1",
|
||||||
|
parameters: {
|
||||||
|
text: "Apple",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "b2",
|
||||||
|
parameters: {
|
||||||
|
text: "Banana {{ actions.0.row }}",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "e5",
|
||||||
|
parameters: {
|
||||||
|
text: "Eagle {{ actions.4.row }}",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "c3",
|
||||||
|
parameters: {
|
||||||
|
text: "Carrot {{ actions.0.row }}",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "d4",
|
||||||
|
parameters: {
|
||||||
|
text: "Dog {{ actions.3.row }}",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should handle on 'move' to a higher index", () => {
|
||||||
|
let obj = [
|
||||||
|
{
|
||||||
|
id: "b2",
|
||||||
|
parameters: {
|
||||||
|
text: "Banana {{ actions.0.row }}",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "c3",
|
||||||
|
parameters: {
|
||||||
|
text: "Carrot {{ actions.0.row }}",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "a1",
|
||||||
|
parameters: {
|
||||||
|
text: "Apple",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "d4",
|
||||||
|
parameters: {
|
||||||
|
text: "Dog {{ actions.2.row }}",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "e5",
|
||||||
|
parameters: {
|
||||||
|
text: "Eagle {{ actions.3.row }}",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
updateReferencesInObject({
|
||||||
|
obj,
|
||||||
|
modifiedIndex: 2,
|
||||||
|
action: "move",
|
||||||
|
label: "actions",
|
||||||
|
originalIndex: 0,
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(obj).toEqual([
|
||||||
|
{
|
||||||
|
id: "b2",
|
||||||
|
parameters: {
|
||||||
|
text: "Banana {{ actions.2.row }}",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "c3",
|
||||||
|
parameters: {
|
||||||
|
text: "Carrot {{ actions.2.row }}",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "a1",
|
||||||
|
parameters: {
|
||||||
|
text: "Apple",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "d4",
|
||||||
|
parameters: {
|
||||||
|
text: "Dog {{ actions.1.row }}",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "e5",
|
||||||
|
parameters: {
|
||||||
|
text: "Eagle {{ actions.3.row }}",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should handle on 'move' of action being referenced, dragged to a higher index", () => {
|
||||||
|
let obj = [
|
||||||
|
{
|
||||||
|
"##eventHandlerType": "Validate Form",
|
||||||
|
id: "cCD0Dwcnq",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"##eventHandlerType": "Close Screen Modal",
|
||||||
|
id: "3fbbIOfN0H",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"##eventHandlerType": "Save Row",
|
||||||
|
parameters: {
|
||||||
|
tableId: "ta_bb_employee",
|
||||||
|
},
|
||||||
|
id: "aehg5cTmhR",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"##eventHandlerType": "Close Side Panel",
|
||||||
|
id: "mzkpf86cxo",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"##eventHandlerType": "Navigate To",
|
||||||
|
id: "h0uDFeJa8A",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
parameters: {
|
||||||
|
autoDismiss: true,
|
||||||
|
type: "success",
|
||||||
|
message: "{{ actions.1.row }}",
|
||||||
|
},
|
||||||
|
"##eventHandlerType": "Show Notification",
|
||||||
|
id: "JEI5lAyJZ",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
updateReferencesInObject({
|
||||||
|
obj,
|
||||||
|
modifiedIndex: 2,
|
||||||
|
action: "move",
|
||||||
|
label: "actions",
|
||||||
|
originalIndex: 1,
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(obj).toEqual([
|
||||||
|
{
|
||||||
|
"##eventHandlerType": "Validate Form",
|
||||||
|
id: "cCD0Dwcnq",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"##eventHandlerType": "Close Screen Modal",
|
||||||
|
id: "3fbbIOfN0H",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"##eventHandlerType": "Save Row",
|
||||||
|
parameters: {
|
||||||
|
tableId: "ta_bb_employee",
|
||||||
|
},
|
||||||
|
id: "aehg5cTmhR",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"##eventHandlerType": "Close Side Panel",
|
||||||
|
id: "mzkpf86cxo",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"##eventHandlerType": "Navigate To",
|
||||||
|
id: "h0uDFeJa8A",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
parameters: {
|
||||||
|
autoDismiss: true,
|
||||||
|
type: "success",
|
||||||
|
message: "{{ actions.2.row }}",
|
||||||
|
},
|
||||||
|
"##eventHandlerType": "Show Notification",
|
||||||
|
id: "JEI5lAyJZ",
|
||||||
|
},
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should handle on 'move' of action being referenced, dragged to a lower index", () => {
|
||||||
|
let obj = [
|
||||||
|
{
|
||||||
|
"##eventHandlerType": "Save Row",
|
||||||
|
parameters: {
|
||||||
|
tableId: "ta_bb_employee",
|
||||||
|
},
|
||||||
|
id: "aehg5cTmhR",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"##eventHandlerType": "Validate Form",
|
||||||
|
id: "cCD0Dwcnq",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"##eventHandlerType": "Close Screen Modal",
|
||||||
|
id: "3fbbIOfN0H",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"##eventHandlerType": "Close Side Panel",
|
||||||
|
id: "mzkpf86cxo",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"##eventHandlerType": "Navigate To",
|
||||||
|
id: "h0uDFeJa8A",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
parameters: {
|
||||||
|
autoDismiss: true,
|
||||||
|
type: "success",
|
||||||
|
message: "{{ actions.4.row }}",
|
||||||
|
},
|
||||||
|
"##eventHandlerType": "Show Notification",
|
||||||
|
id: "JEI5lAyJZ",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
updateReferencesInObject({
|
||||||
|
obj,
|
||||||
|
modifiedIndex: 0,
|
||||||
|
action: "move",
|
||||||
|
label: "actions",
|
||||||
|
originalIndex: 4,
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(obj).toEqual([
|
||||||
|
{
|
||||||
|
"##eventHandlerType": "Save Row",
|
||||||
|
parameters: {
|
||||||
|
tableId: "ta_bb_employee",
|
||||||
|
},
|
||||||
|
id: "aehg5cTmhR",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"##eventHandlerType": "Validate Form",
|
||||||
|
id: "cCD0Dwcnq",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"##eventHandlerType": "Close Screen Modal",
|
||||||
|
id: "3fbbIOfN0H",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"##eventHandlerType": "Close Side Panel",
|
||||||
|
id: "mzkpf86cxo",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"##eventHandlerType": "Navigate To",
|
||||||
|
id: "h0uDFeJa8A",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
parameters: {
|
||||||
|
autoDismiss: true,
|
||||||
|
type: "success",
|
||||||
|
message: "{{ actions.0.row }}",
|
||||||
|
},
|
||||||
|
"##eventHandlerType": "Show Notification",
|
||||||
|
id: "JEI5lAyJZ",
|
||||||
|
},
|
||||||
|
])
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
|
@ -64,7 +64,7 @@
|
||||||
</span>
|
</span>
|
||||||
{:else if schema.type === "link"}
|
{:else if schema.type === "link"}
|
||||||
<LinkedRowSelector
|
<LinkedRowSelector
|
||||||
bind:linkedRows={value[field]}
|
linkedRows={value[field]}
|
||||||
{schema}
|
{schema}
|
||||||
on:change={e => onChange(e, field)}
|
on:change={e => onChange(e, field)}
|
||||||
useLabel={false}
|
useLabel={false}
|
||||||
|
|
|
@ -22,7 +22,7 @@
|
||||||
<Select
|
<Select
|
||||||
on:change={onChange}
|
on:change={onChange}
|
||||||
bind:value
|
bind:value
|
||||||
options={filteredTables.filter(table => table._id !== TableNames.USERS)}
|
options={filteredTables}
|
||||||
getOptionLabel={table => table.name}
|
getOptionLabel={table => table.name}
|
||||||
getOptionValue={table => table._id}
|
getOptionValue={table => table._id}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -70,7 +70,12 @@
|
||||||
options={meta.constraints.inclusion}
|
options={meta.constraints.inclusion}
|
||||||
/>
|
/>
|
||||||
{:else if type === "link"}
|
{:else if type === "link"}
|
||||||
<LinkedRowSelector {error} bind:linkedRows={value} schema={meta} />
|
<LinkedRowSelector
|
||||||
|
{error}
|
||||||
|
linkedRows={value}
|
||||||
|
schema={meta}
|
||||||
|
on:change={e => (value = e.detail)}
|
||||||
|
/>
|
||||||
{:else if type === "longform"}
|
{:else if type === "longform"}
|
||||||
{#if meta.useRichText}
|
{#if meta.useRichText}
|
||||||
<RichTextField {error} {label} height="150px" bind:value />
|
<RichTextField {error} {label} height="150px" bind:value />
|
||||||
|
|
|
@ -56,12 +56,12 @@
|
||||||
/>
|
/>
|
||||||
{:else}
|
{:else}
|
||||||
<Multiselect
|
<Multiselect
|
||||||
bind:value={linkedIds}
|
value={linkedIds}
|
||||||
{label}
|
{label}
|
||||||
options={rows}
|
options={rows}
|
||||||
getOptionLabel={getPrettyName}
|
getOptionLabel={getPrettyName}
|
||||||
getOptionValue={row => row._id}
|
getOptionValue={row => row._id}
|
||||||
sort
|
sort
|
||||||
on:change={() => dispatch("change", linkedIds)}
|
on:change
|
||||||
/>
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
|
@ -1,10 +1,11 @@
|
||||||
<script>
|
<script>
|
||||||
import { Icon } from "@budibase/bbui"
|
import { AbsTooltip, Icon } from "@budibase/bbui"
|
||||||
import { createEventDispatcher, getContext } from "svelte"
|
import { createEventDispatcher, getContext } from "svelte"
|
||||||
import { helpers } from "@budibase/shared-core"
|
import { helpers } from "@budibase/shared-core"
|
||||||
import { UserAvatars } from "@budibase/frontend-core"
|
import { UserAvatars } from "@budibase/frontend-core"
|
||||||
|
|
||||||
export let icon
|
export let icon
|
||||||
|
export let iconTooltip
|
||||||
export let withArrow = false
|
export let withArrow = false
|
||||||
export let withActions = true
|
export let withActions = true
|
||||||
export let indentLevel = 0
|
export let indentLevel = 0
|
||||||
|
@ -77,7 +78,11 @@
|
||||||
{style}
|
{style}
|
||||||
{draggable}
|
{draggable}
|
||||||
>
|
>
|
||||||
<div class="nav-item-content" bind:this={contentRef}>
|
<div
|
||||||
|
class="nav-item-content"
|
||||||
|
bind:this={contentRef}
|
||||||
|
class:right={rightAlignIcon}
|
||||||
|
>
|
||||||
{#if withArrow}
|
{#if withArrow}
|
||||||
<div
|
<div
|
||||||
class:opened
|
class:opened
|
||||||
|
@ -98,7 +103,9 @@
|
||||||
</div>
|
</div>
|
||||||
{:else if icon}
|
{:else if icon}
|
||||||
<div class="icon" class:right={rightAlignIcon}>
|
<div class="icon" class:right={rightAlignIcon}>
|
||||||
<Icon color={iconColor} size="S" name={icon} />
|
<AbsTooltip type="info" position="right" text={iconTooltip}>
|
||||||
|
<Icon color={iconColor} size="S" name={icon} />
|
||||||
|
</AbsTooltip>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
<div class="text" title={showTooltip ? text : null}>
|
<div class="text" title={showTooltip ? text : null}>
|
||||||
|
@ -166,6 +173,11 @@
|
||||||
width: max-content;
|
width: max-content;
|
||||||
position: relative;
|
position: relative;
|
||||||
padding-left: var(--spacing-l);
|
padding-left: var(--spacing-l);
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-item-content.right {
|
||||||
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Needed to fully display the actions icon */
|
/* Needed to fully display the actions icon */
|
||||||
|
@ -264,6 +276,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.right {
|
.right {
|
||||||
|
margin-left: auto;
|
||||||
order: 10;
|
order: 10;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -0,0 +1,119 @@
|
||||||
|
const getResizeActions = (
|
||||||
|
cssProperty,
|
||||||
|
mouseMoveEventProperty,
|
||||||
|
elementProperty,
|
||||||
|
initialValue,
|
||||||
|
setValue = () => {}
|
||||||
|
) => {
|
||||||
|
let element = null
|
||||||
|
|
||||||
|
const elementAction = node => {
|
||||||
|
element = node
|
||||||
|
|
||||||
|
if (initialValue != null) {
|
||||||
|
element.style[cssProperty] = `${initialValue}px`
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
destroy() {
|
||||||
|
element = null
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const dragHandleAction = node => {
|
||||||
|
let startProperty = null
|
||||||
|
let startPosition = null
|
||||||
|
|
||||||
|
const handleMouseMove = e => {
|
||||||
|
e.preventDefault() // Prevent highlighting while dragging
|
||||||
|
const change = e[mouseMoveEventProperty] - startPosition
|
||||||
|
element.style[cssProperty] = `${startProperty + change}px`
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleMouseUp = e => {
|
||||||
|
e.preventDefault() // Prevent highlighting while dragging
|
||||||
|
window.removeEventListener("mousemove", handleMouseMove)
|
||||||
|
window.removeEventListener("mouseup", handleMouseUp)
|
||||||
|
|
||||||
|
element.style.removeProperty("transition") // remove temporary transition override
|
||||||
|
for (let item of document.getElementsByTagName("iframe")) {
|
||||||
|
item.style.removeProperty("pointer-events")
|
||||||
|
}
|
||||||
|
|
||||||
|
setValue(element[elementProperty])
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleMouseDown = e => {
|
||||||
|
if (e.detail > 1) {
|
||||||
|
// e.detail is the number of rapid clicks, so e.detail = 2 is
|
||||||
|
// a double click. We want to prevent default behaviour in
|
||||||
|
// this case as it highlights nearby selectable elements, which
|
||||||
|
// then interferes with the resizing mousemove.
|
||||||
|
// Putting this on the double click handler doesn't seem to
|
||||||
|
// work, so it must go here.
|
||||||
|
e.preventDefault()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
e.target.hasAttribute("disabled") &&
|
||||||
|
e.target.getAttribute("disabled") !== "false"
|
||||||
|
) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
element.style.transition = `${cssProperty} 0ms` // temporarily override any height transitions
|
||||||
|
|
||||||
|
// iframes swallow mouseup events if your cursor ends up over it during a drag, so make them
|
||||||
|
// temporarily non-interactive
|
||||||
|
for (let item of document.getElementsByTagName("iframe")) {
|
||||||
|
item.style.pointerEvents = "none"
|
||||||
|
}
|
||||||
|
|
||||||
|
startProperty = element[elementProperty]
|
||||||
|
startPosition = e[mouseMoveEventProperty]
|
||||||
|
|
||||||
|
window.addEventListener("mousemove", handleMouseMove)
|
||||||
|
window.addEventListener("mouseup", handleMouseUp)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleDoubleClick = () => {
|
||||||
|
element.style.removeProperty(cssProperty)
|
||||||
|
}
|
||||||
|
|
||||||
|
node.addEventListener("mousedown", handleMouseDown)
|
||||||
|
node.addEventListener("dblclick", handleDoubleClick)
|
||||||
|
|
||||||
|
return {
|
||||||
|
destroy() {
|
||||||
|
node.removeEventListener("mousedown", handleMouseDown)
|
||||||
|
node.removeEventListener("dblclick", handleDoubleClick)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return [elementAction, dragHandleAction]
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getVerticalResizeActions = (initialValue, setValue = () => {}) => {
|
||||||
|
return getResizeActions(
|
||||||
|
"height",
|
||||||
|
"pageY",
|
||||||
|
"clientHeight",
|
||||||
|
initialValue,
|
||||||
|
setValue
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getHorizontalResizeActions = (
|
||||||
|
initialValue,
|
||||||
|
setValue = () => {}
|
||||||
|
) => {
|
||||||
|
return getResizeActions(
|
||||||
|
"width",
|
||||||
|
"pageX",
|
||||||
|
"clientWidth",
|
||||||
|
initialValue,
|
||||||
|
setValue
|
||||||
|
)
|
||||||
|
}
|
|
@ -1,8 +1,9 @@
|
||||||
<script>
|
<script>
|
||||||
import { Icon, Body } from "@budibase/bbui"
|
import { AbsTooltip, Icon, Body } from "@budibase/bbui"
|
||||||
|
|
||||||
export let title
|
export let title
|
||||||
export let icon
|
export let icon
|
||||||
|
export let iconTooltip
|
||||||
export let showAddButton = false
|
export let showAddButton = false
|
||||||
export let showBackButton = false
|
export let showBackButton = false
|
||||||
export let showCloseButton = false
|
export let showCloseButton = false
|
||||||
|
@ -36,7 +37,9 @@
|
||||||
<Icon name="ArrowLeft" hoverable on:click={onClickBackButton} />
|
<Icon name="ArrowLeft" hoverable on:click={onClickBackButton} />
|
||||||
{/if}
|
{/if}
|
||||||
{#if icon}
|
{#if icon}
|
||||||
<Icon name={icon} />
|
<AbsTooltip type="info" text={iconTooltip}>
|
||||||
|
<Icon name={icon} />
|
||||||
|
</AbsTooltip>
|
||||||
{/if}
|
{/if}
|
||||||
<div class="title">
|
<div class="title">
|
||||||
{#if customTitleContent}
|
{#if customTitleContent}
|
||||||
|
@ -68,6 +71,7 @@
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.panel {
|
.panel {
|
||||||
|
min-width: 260px;
|
||||||
width: 260px;
|
width: 260px;
|
||||||
flex: 0 0 260px;
|
flex: 0 0 260px;
|
||||||
background: var(--background);
|
background: var(--background);
|
||||||
|
@ -85,6 +89,7 @@
|
||||||
border-right: var(--border-light);
|
border-right: var(--border-light);
|
||||||
}
|
}
|
||||||
.panel.wide {
|
.panel.wide {
|
||||||
|
min-width: 310px;
|
||||||
width: 310px;
|
width: 310px;
|
||||||
flex: 0 0 310px;
|
flex: 0 0 310px;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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"
|
||||||
|
@ -29,6 +29,7 @@ import FormStepConfiguration from "./controls/FormStepConfiguration.svelte"
|
||||||
|
|
||||||
const componentMap = {
|
const componentMap = {
|
||||||
text: DrawerBindableInput,
|
text: DrawerBindableInput,
|
||||||
|
plainText: Input,
|
||||||
select: Select,
|
select: Select,
|
||||||
radio: RadioGroup,
|
radio: RadioGroup,
|
||||||
dataSource: DataSourceSelect,
|
dataSource: DataSourceSelect,
|
||||||
|
|
|
@ -15,6 +15,7 @@
|
||||||
getEventContextBindings,
|
getEventContextBindings,
|
||||||
getActionBindings,
|
getActionBindings,
|
||||||
makeStateBinding,
|
makeStateBinding,
|
||||||
|
updateReferencesInObject,
|
||||||
} from "builderStore/dataBinding"
|
} from "builderStore/dataBinding"
|
||||||
import { cloneDeep } from "lodash/fp"
|
import { cloneDeep } from "lodash/fp"
|
||||||
|
|
||||||
|
@ -30,6 +31,7 @@
|
||||||
|
|
||||||
let actionQuery
|
let actionQuery
|
||||||
let selectedAction = actions?.length ? actions[0] : null
|
let selectedAction = actions?.length ? actions[0] : null
|
||||||
|
let originalActionIndex
|
||||||
|
|
||||||
const setUpdateActions = actions => {
|
const setUpdateActions = actions => {
|
||||||
return actions
|
return actions
|
||||||
|
@ -115,6 +117,14 @@
|
||||||
if (isSelected) {
|
if (isSelected) {
|
||||||
selectedAction = actions?.length ? actions[0] : null
|
selectedAction = actions?.length ? actions[0] : null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Update action binding references
|
||||||
|
updateReferencesInObject({
|
||||||
|
obj: actions,
|
||||||
|
modifiedIndex: index,
|
||||||
|
action: "delete",
|
||||||
|
label: "actions",
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const toggleActionList = () => {
|
const toggleActionList = () => {
|
||||||
|
@ -146,9 +156,29 @@
|
||||||
|
|
||||||
function handleDndConsider(e) {
|
function handleDndConsider(e) {
|
||||||
actions = e.detail.items
|
actions = e.detail.items
|
||||||
|
|
||||||
|
// set the initial index of the action being dragged
|
||||||
|
if (e.detail.info.trigger === "draggedEntered") {
|
||||||
|
originalActionIndex = actions.findIndex(
|
||||||
|
action => action.id === e.detail.info.id
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
function handleDndFinalize(e) {
|
function handleDndFinalize(e) {
|
||||||
actions = e.detail.items
|
actions = e.detail.items
|
||||||
|
|
||||||
|
// Update action binding references
|
||||||
|
updateReferencesInObject({
|
||||||
|
obj: actions,
|
||||||
|
modifiedIndex: actions.findIndex(
|
||||||
|
action => action.id === e.detail.info.id
|
||||||
|
),
|
||||||
|
action: "move",
|
||||||
|
label: "actions",
|
||||||
|
originalIndex: originalActionIndex,
|
||||||
|
})
|
||||||
|
|
||||||
|
originalActionIndex = -1
|
||||||
}
|
}
|
||||||
|
|
||||||
const getAllBindings = (actionBindings, eventContextBindings, actions) => {
|
const getAllBindings = (actionBindings, eventContextBindings, actions) => {
|
||||||
|
@ -289,7 +319,7 @@
|
||||||
</Layout>
|
</Layout>
|
||||||
<Layout noPadding>
|
<Layout noPadding>
|
||||||
{#if selectedActionComponent && !showAvailableActions}
|
{#if selectedActionComponent && !showAvailableActions}
|
||||||
{#key selectedAction.id}
|
{#key (selectedAction.id, originalActionIndex)}
|
||||||
<div class="selected-action-container">
|
<div class="selected-action-container">
|
||||||
<svelte:component
|
<svelte:component
|
||||||
this={selectedActionComponent}
|
this={selectedActionComponent}
|
||||||
|
|
|
@ -55,7 +55,10 @@
|
||||||
size="S"
|
size="S"
|
||||||
name="Close"
|
name="Close"
|
||||||
hoverable
|
hoverable
|
||||||
on:click={() => removeButton(item._id)}
|
on:click={e => {
|
||||||
|
e.stopPropagation()
|
||||||
|
removeButton(item._id)
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,10 +0,0 @@
|
||||||
<script>
|
|
||||||
import ColumnEditor from "./ColumnEditor.svelte"
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<ColumnEditor
|
|
||||||
{...$$props}
|
|
||||||
on:change
|
|
||||||
allowCellEditing={false}
|
|
||||||
allowReorder={false}
|
|
||||||
/>
|
|
|
@ -32,11 +32,14 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
const dispatch = createEventDispatcher()
|
const dispatch = createEventDispatcher()
|
||||||
const flipDurationMs = 150
|
|
||||||
|
|
||||||
let anchors = {}
|
let anchors = {}
|
||||||
let draggableItems = []
|
let draggableItems = []
|
||||||
|
|
||||||
|
// Used for controlling cursor behaviour in order to limit drag behaviour
|
||||||
|
// to the drag handle
|
||||||
|
let inactive = true
|
||||||
|
|
||||||
const buildDraggable = items => {
|
const buildDraggable = items => {
|
||||||
return items
|
return items
|
||||||
.map(item => {
|
.map(item => {
|
||||||
|
@ -64,6 +67,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleFinalize = e => {
|
const handleFinalize = e => {
|
||||||
|
inactive = true
|
||||||
updateRowOrder(e)
|
updateRowOrder(e)
|
||||||
dispatch("change", serialiseUpdate())
|
dispatch("change", serialiseUpdate())
|
||||||
}
|
}
|
||||||
|
@ -77,24 +81,36 @@
|
||||||
class="list-wrap"
|
class="list-wrap"
|
||||||
use:dndzone={{
|
use:dndzone={{
|
||||||
items: draggableItems,
|
items: draggableItems,
|
||||||
flipDurationMs,
|
|
||||||
dropTargetStyle: { outline: "none" },
|
dropTargetStyle: { outline: "none" },
|
||||||
dragDisabled: !draggable,
|
dragDisabled: !draggable || inactive,
|
||||||
}}
|
}}
|
||||||
on:finalize={handleFinalize}
|
on:finalize={handleFinalize}
|
||||||
on:consider={updateRowOrder}
|
on:consider={updateRowOrder}
|
||||||
>
|
>
|
||||||
{#each draggableItems as draggable (draggable.id)}
|
{#each draggableItems as draggableItem (draggableItem.id)}
|
||||||
<li
|
<li
|
||||||
|
on:click={() => {
|
||||||
|
get(store).actions.select(draggableItem.id)
|
||||||
|
}}
|
||||||
on:mousedown={() => {
|
on:mousedown={() => {
|
||||||
get(store).actions.select()
|
get(store).actions.select()
|
||||||
}}
|
}}
|
||||||
bind:this={anchors[draggable.id]}
|
bind:this={anchors[draggableItem.id]}
|
||||||
class:highlighted={draggable.id === $store.selected}
|
class:highlighted={draggableItem.id === $store.selected}
|
||||||
>
|
>
|
||||||
<div class="left-content">
|
<div class="left-content">
|
||||||
{#if showHandle}
|
{#if showHandle}
|
||||||
<div class="handle">
|
<div
|
||||||
|
class="handle"
|
||||||
|
aria-label="drag-handle"
|
||||||
|
style={!inactive ? "cursor:grabbing" : "cursor:grab"}
|
||||||
|
on:mousedown={() => {
|
||||||
|
inactive = false
|
||||||
|
}}
|
||||||
|
on:mouseup={() => {
|
||||||
|
inactive = true
|
||||||
|
}}
|
||||||
|
>
|
||||||
<DragHandle />
|
<DragHandle />
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
@ -102,8 +118,8 @@
|
||||||
<div class="right-content">
|
<div class="right-content">
|
||||||
<svelte:component
|
<svelte:component
|
||||||
this={listType}
|
this={listType}
|
||||||
anchor={anchors[draggable.item._id]}
|
anchor={anchors[draggableItem.item._id]}
|
||||||
item={draggable.item}
|
item={draggableItem.item}
|
||||||
{...listTypeProps}
|
{...listTypeProps}
|
||||||
on:change={onItemChanged}
|
on:change={onItemChanged}
|
||||||
/>
|
/>
|
||||||
|
@ -143,6 +159,7 @@
|
||||||
--spectrum-table-row-background-color-hover,
|
--spectrum-table-row-background-color-hover,
|
||||||
var(--spectrum-alias-highlight-hover)
|
var(--spectrum-alias-highlight-hover)
|
||||||
);
|
);
|
||||||
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
.list-wrap > li:first-child {
|
.list-wrap > li:first-child {
|
||||||
border-top-left-radius: 4px;
|
border-top-left-radius: 4px;
|
||||||
|
@ -165,6 +182,9 @@
|
||||||
display: flex;
|
display: flex;
|
||||||
height: var(--spectrum-global-dimension-size-150);
|
height: var(--spectrum-global-dimension-size-150);
|
||||||
}
|
}
|
||||||
|
.handle:hover {
|
||||||
|
cursor: grab;
|
||||||
|
}
|
||||||
.handle :global(svg) {
|
.handle :global(svg) {
|
||||||
fill: var(--spectrum-global-color-gray-500);
|
fill: var(--spectrum-global-color-gray-500);
|
||||||
margin-right: var(--spacing-m);
|
margin-right: var(--spacing-m);
|
||||||
|
|
|
@ -156,7 +156,7 @@
|
||||||
|
|
||||||
<div class="field-configuration">
|
<div class="field-configuration">
|
||||||
<div class="toggle-all">
|
<div class="toggle-all">
|
||||||
<span />
|
<span>Fields</span>
|
||||||
<Toggle
|
<Toggle
|
||||||
on:change={() => {
|
on:change={() => {
|
||||||
let update = fieldList.map(field => ({
|
let update = fieldList.map(field => ({
|
||||||
|
@ -186,6 +186,9 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
.field-configuration {
|
||||||
|
padding-top: 8px;
|
||||||
|
}
|
||||||
.field-configuration :global(.spectrum-ActionButton) {
|
.field-configuration :global(.spectrum-ActionButton) {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
@ -204,6 +207,5 @@
|
||||||
.toggle-all span {
|
.toggle-all span {
|
||||||
color: var(--spectrum-global-color-gray-700);
|
color: var(--spectrum-global-color-gray-700);
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
margin-left: calc(var(--spacing-s) - 1px);
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -58,7 +58,15 @@
|
||||||
<div class="field-label">{readableText}</div>
|
<div class="field-label">{readableText}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="list-item-right">
|
<div class="list-item-right">
|
||||||
<Toggle on:change={onToggle(item)} text="" value={item.active} thin />
|
<Toggle
|
||||||
|
on:change={onToggle(item)}
|
||||||
|
on:click={e => {
|
||||||
|
e.stopPropagation()
|
||||||
|
}}
|
||||||
|
text=""
|
||||||
|
value={item.active}
|
||||||
|
thin
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,91 @@
|
||||||
|
<script>
|
||||||
|
import EditComponentPopover from "../EditComponentPopover.svelte"
|
||||||
|
import { FieldTypeToComponentMap } from "../FieldConfiguration/utils"
|
||||||
|
import { Toggle, Icon } from "@budibase/bbui"
|
||||||
|
import { createEventDispatcher } from "svelte"
|
||||||
|
import { cloneDeep } from "lodash/fp"
|
||||||
|
import { store } from "builderStore"
|
||||||
|
|
||||||
|
export let item
|
||||||
|
export let anchor
|
||||||
|
|
||||||
|
const dispatch = createEventDispatcher()
|
||||||
|
const onToggle = item => {
|
||||||
|
return e => {
|
||||||
|
item.active = e.detail
|
||||||
|
dispatch("change", { ...cloneDeep(item), active: e.detail })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const parseSettings = settings => {
|
||||||
|
return settings
|
||||||
|
.filter(setting => setting.key !== "field")
|
||||||
|
.map(setting => {
|
||||||
|
return { ...setting, nested: true }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const getIcon = () => {
|
||||||
|
const component = `@budibase/standard-components/${
|
||||||
|
FieldTypeToComponentMap[item.columnType]
|
||||||
|
}`
|
||||||
|
return store.actions.components.getDefinition(component).icon
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="list-item-body">
|
||||||
|
<div class="list-item-left">
|
||||||
|
<EditComponentPopover
|
||||||
|
{anchor}
|
||||||
|
componentInstance={item}
|
||||||
|
{parseSettings}
|
||||||
|
on:change
|
||||||
|
>
|
||||||
|
<div slot="header" class="type-icon">
|
||||||
|
<Icon name={getIcon()} />
|
||||||
|
<span>{item.field}</span>
|
||||||
|
</div>
|
||||||
|
</EditComponentPopover>
|
||||||
|
<div class="field-label">{item.label || item.field}</div>
|
||||||
|
</div>
|
||||||
|
<div class="list-item-right">
|
||||||
|
<Toggle on:change={onToggle(item)} text="" value={item.active} thin />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.field-label {
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.list-item-body,
|
||||||
|
.list-item-left {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--spacing-m);
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
.list-item-right :global(div.spectrum-Switch) {
|
||||||
|
margin: 0px;
|
||||||
|
}
|
||||||
|
.list-item-body {
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
.type-icon {
|
||||||
|
display: flex;
|
||||||
|
gap: var(--spacing-m);
|
||||||
|
margin: var(--spacing-xl);
|
||||||
|
margin-bottom: 0px;
|
||||||
|
height: var(--spectrum-alias-item-height-m);
|
||||||
|
padding: 0px var(--spectrum-alias-item-padding-m);
|
||||||
|
border-width: var(--spectrum-actionbutton-border-size);
|
||||||
|
border-radius: var(--spectrum-alias-border-radius-regular);
|
||||||
|
border: 1px solid
|
||||||
|
var(
|
||||||
|
--spectrum-actionbutton-m-border-color,
|
||||||
|
var(--spectrum-alias-border-color)
|
||||||
|
);
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,107 @@
|
||||||
|
<script>
|
||||||
|
import {
|
||||||
|
getDatasourceForProvider,
|
||||||
|
getSchemaForDatasource,
|
||||||
|
} from "builderStore/dataBinding"
|
||||||
|
import { currentAsset, store } from "builderStore"
|
||||||
|
import DraggableList from "../DraggableList/DraggableList.svelte"
|
||||||
|
import { createEventDispatcher } from "svelte"
|
||||||
|
import FieldSetting from "./FieldSetting.svelte"
|
||||||
|
import PrimaryColumnFieldSetting from "./PrimaryColumnFieldSetting.svelte"
|
||||||
|
import getColumns from "./getColumns.js"
|
||||||
|
|
||||||
|
export let value
|
||||||
|
export let componentInstance
|
||||||
|
|
||||||
|
const dispatch = createEventDispatcher()
|
||||||
|
let primaryDisplayColumnAnchor
|
||||||
|
|
||||||
|
const handleChange = newValues => {
|
||||||
|
dispatch("change", newValues)
|
||||||
|
}
|
||||||
|
|
||||||
|
const getSchema = (asset, datasource) => {
|
||||||
|
const schema = getSchemaForDatasource(asset, datasource).schema
|
||||||
|
|
||||||
|
// Don't show ID and rev in tables
|
||||||
|
if (schema) {
|
||||||
|
delete schema._id
|
||||||
|
delete schema._rev
|
||||||
|
}
|
||||||
|
|
||||||
|
return schema
|
||||||
|
}
|
||||||
|
|
||||||
|
$: datasource = getDatasourceForProvider($currentAsset, componentInstance)
|
||||||
|
$: primaryDisplayColumnName = getSchemaForDatasource(
|
||||||
|
$currentAsset,
|
||||||
|
datasource
|
||||||
|
)?.table?.primaryDisplay
|
||||||
|
$: schema = getSchema(currentAsset, datasource)
|
||||||
|
$: columns = getColumns({
|
||||||
|
columns: value,
|
||||||
|
schema,
|
||||||
|
primaryDisplayColumnName,
|
||||||
|
onChange: handleChange,
|
||||||
|
createComponent: store.actions.components.createInstance,
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if columns.primary}
|
||||||
|
<div class="sticky-item">
|
||||||
|
<div bind:this={primaryDisplayColumnAnchor} class="sticky-item-inner">
|
||||||
|
<div class="right-content">
|
||||||
|
<PrimaryColumnFieldSetting
|
||||||
|
anchor={primaryDisplayColumnAnchor}
|
||||||
|
item={columns.primary}
|
||||||
|
on:change={e => columns.update(e.detail)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
<DraggableList
|
||||||
|
on:change={e => columns.updateSortable(e.detail)}
|
||||||
|
on:itemChange={e => columns.update(e.detail)}
|
||||||
|
items={columns.sortable}
|
||||||
|
listItemKey={"_id"}
|
||||||
|
listType={FieldSetting}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.right-content {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
margin-left: 17.5px;
|
||||||
|
}
|
||||||
|
.sticky-item {
|
||||||
|
list-style-type: none;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
width: 100%;
|
||||||
|
border-radius: 4px;
|
||||||
|
background-color: var(
|
||||||
|
--spectrum-table-background-color,
|
||||||
|
var(--spectrum-global-color-gray-50)
|
||||||
|
);
|
||||||
|
border: 1px solid
|
||||||
|
var(--spectrum-table-border-color, var(--spectrum-alias-border-color-mid));
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
.sticky-item-inner {
|
||||||
|
background-color: var(
|
||||||
|
--spectrum-table-background-color,
|
||||||
|
var(--spectrum-global-color-gray-50)
|
||||||
|
);
|
||||||
|
transition: background-color ease-in-out 130ms;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
border-bottom: 1px solid
|
||||||
|
var(--spectrum-table-border-color, var(--spectrum-alias-border-color-mid));
|
||||||
|
padding-left: var(--spacing-s);
|
||||||
|
padding-right: var(--spacing-s);
|
||||||
|
box-sizing: border-box;
|
||||||
|
border-radius: 4px;
|
||||||
|
border-bottom: 0;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,100 @@
|
||||||
|
<script>
|
||||||
|
import EditComponentPopover from "../EditComponentPopover.svelte"
|
||||||
|
import { Icon } from "@budibase/bbui"
|
||||||
|
import { setContext } from "svelte"
|
||||||
|
import { writable } from "svelte/store"
|
||||||
|
import { FieldTypeToComponentMap } from "../FieldConfiguration/utils"
|
||||||
|
import { store } from "builderStore"
|
||||||
|
|
||||||
|
export let item
|
||||||
|
export let anchor
|
||||||
|
|
||||||
|
let draggableStore = writable({
|
||||||
|
selected: null,
|
||||||
|
actions: {
|
||||||
|
select: id => {
|
||||||
|
draggableStore.update(state => ({
|
||||||
|
...state,
|
||||||
|
selected: id,
|
||||||
|
}))
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
setContext("draggable", draggableStore)
|
||||||
|
|
||||||
|
const parseSettings = settings => {
|
||||||
|
return settings
|
||||||
|
.filter(setting => setting.key !== "field")
|
||||||
|
.map(setting => {
|
||||||
|
return { ...setting, nested: true }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const getIcon = item => {
|
||||||
|
const component = `@budibase/standard-components/${
|
||||||
|
FieldTypeToComponentMap[item.columnType]
|
||||||
|
}`
|
||||||
|
return store.actions.components.getDefinition(component)?.icon
|
||||||
|
}
|
||||||
|
|
||||||
|
$: icon = getIcon(item)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="list-item-body">
|
||||||
|
<div class="list-item-left">
|
||||||
|
<EditComponentPopover
|
||||||
|
{anchor}
|
||||||
|
componentInstance={item}
|
||||||
|
{parseSettings}
|
||||||
|
on:change
|
||||||
|
>
|
||||||
|
<div slot="header" class="type-icon">
|
||||||
|
<Icon name={icon} />
|
||||||
|
<span>{item.field}</span>
|
||||||
|
</div>
|
||||||
|
</EditComponentPopover>
|
||||||
|
<div class="field-label">{item.label || item.field}</div>
|
||||||
|
</div>
|
||||||
|
<div title="The display column is always shown first" class="list-item-right">
|
||||||
|
<Icon name={"Info"} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.field-label {
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.list-item-body,
|
||||||
|
.list-item-left {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--spacing-m);
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
.list-item-right :global(svg) {
|
||||||
|
color: var(--grey-5);
|
||||||
|
padding: 7px 5px 7px 0;
|
||||||
|
}
|
||||||
|
.list-item-body {
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
.type-icon {
|
||||||
|
display: flex;
|
||||||
|
gap: var(--spacing-m);
|
||||||
|
margin: var(--spacing-xl);
|
||||||
|
margin-bottom: 0px;
|
||||||
|
height: var(--spectrum-alias-item-height-m);
|
||||||
|
padding: 0px var(--spectrum-alias-item-padding-m);
|
||||||
|
border-width: var(--spectrum-actionbutton-border-size);
|
||||||
|
border-radius: var(--spectrum-alias-border-radius-regular);
|
||||||
|
border: 1px solid
|
||||||
|
var(
|
||||||
|
--spectrum-actionbutton-m-border-color,
|
||||||
|
var(--spectrum-alias-border-color)
|
||||||
|
);
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,129 @@
|
||||||
|
const modernize = columns => {
|
||||||
|
if (!columns) {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
// If the first element has no active key then it's safe to assume all elements are in the old format
|
||||||
|
if (columns?.[0] && columns[0].active === undefined) {
|
||||||
|
return columns.map(column => ({
|
||||||
|
label: column.displayName,
|
||||||
|
field: column.name,
|
||||||
|
active: true,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
return columns
|
||||||
|
}
|
||||||
|
|
||||||
|
const removeInvalidAddMissing = (
|
||||||
|
columns = [],
|
||||||
|
defaultColumns,
|
||||||
|
primaryDisplayColumnName
|
||||||
|
) => {
|
||||||
|
const defaultColumnNames = defaultColumns.map(column => column.field)
|
||||||
|
const columnNames = columns.map(column => column.field)
|
||||||
|
|
||||||
|
const validColumns = columns.filter(column =>
|
||||||
|
defaultColumnNames.includes(column.field)
|
||||||
|
)
|
||||||
|
let missingColumns = defaultColumns.filter(
|
||||||
|
defaultColumn => !columnNames.includes(defaultColumn.field)
|
||||||
|
)
|
||||||
|
|
||||||
|
// If the user already has fields selected, any appended missing fields should be disabled by default
|
||||||
|
if (validColumns.length) {
|
||||||
|
missingColumns = missingColumns.map(field => ({ ...field, active: false }))
|
||||||
|
}
|
||||||
|
|
||||||
|
const combinedColumns = [...validColumns, ...missingColumns]
|
||||||
|
|
||||||
|
// Ensure the primary display column is always visible
|
||||||
|
const primaryDisplayIndex = combinedColumns.findIndex(
|
||||||
|
column => column.field === primaryDisplayColumnName
|
||||||
|
)
|
||||||
|
if (primaryDisplayIndex > -1) {
|
||||||
|
combinedColumns[primaryDisplayIndex].active = true
|
||||||
|
}
|
||||||
|
|
||||||
|
return combinedColumns
|
||||||
|
}
|
||||||
|
|
||||||
|
const getDefault = (schema = {}) => {
|
||||||
|
const defaultValues = Object.values(schema)
|
||||||
|
.filter(column => !column.nestedJSON)
|
||||||
|
.map(column => ({
|
||||||
|
label: column.name,
|
||||||
|
field: column.name,
|
||||||
|
active: column.visible ?? true,
|
||||||
|
order: column.visible ? column.order ?? -1 : Number.MAX_SAFE_INTEGER,
|
||||||
|
}))
|
||||||
|
|
||||||
|
defaultValues.sort((a, b) => a.order - b.order)
|
||||||
|
|
||||||
|
return defaultValues
|
||||||
|
}
|
||||||
|
|
||||||
|
const toGridFormat = draggableListColumns => {
|
||||||
|
return draggableListColumns.map(entry => ({
|
||||||
|
label: entry.label,
|
||||||
|
field: entry.field,
|
||||||
|
active: entry.active,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
const toDraggableListFormat = (gridFormatColumns, createComponent, schema) => {
|
||||||
|
return gridFormatColumns.map(column => {
|
||||||
|
return createComponent(
|
||||||
|
"@budibase/standard-components/labelfield",
|
||||||
|
{
|
||||||
|
_instanceName: column.field,
|
||||||
|
active: column.active,
|
||||||
|
field: column.field,
|
||||||
|
label: column.label,
|
||||||
|
columnType: schema[column.field].type,
|
||||||
|
},
|
||||||
|
{}
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const getColumns = ({
|
||||||
|
columns,
|
||||||
|
schema,
|
||||||
|
primaryDisplayColumnName,
|
||||||
|
onChange,
|
||||||
|
createComponent,
|
||||||
|
}) => {
|
||||||
|
const validatedColumns = removeInvalidAddMissing(
|
||||||
|
modernize(columns),
|
||||||
|
getDefault(schema),
|
||||||
|
primaryDisplayColumnName
|
||||||
|
)
|
||||||
|
const draggableList = toDraggableListFormat(
|
||||||
|
validatedColumns,
|
||||||
|
createComponent,
|
||||||
|
schema
|
||||||
|
)
|
||||||
|
const primary = draggableList.find(
|
||||||
|
entry => entry.field === primaryDisplayColumnName
|
||||||
|
)
|
||||||
|
const sortable = draggableList.filter(
|
||||||
|
entry => entry.field !== primaryDisplayColumnName
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
primary,
|
||||||
|
sortable,
|
||||||
|
updateSortable: newDraggableList => {
|
||||||
|
onChange(toGridFormat(newDraggableList.concat(primary)))
|
||||||
|
},
|
||||||
|
update: newEntry => {
|
||||||
|
const newDraggableList = draggableList.map(entry => {
|
||||||
|
return newEntry.field === entry.field ? newEntry : entry
|
||||||
|
})
|
||||||
|
|
||||||
|
onChange(toGridFormat(newDraggableList))
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default getColumns
|
|
@ -0,0 +1,374 @@
|
||||||
|
import { it, expect, describe, beforeEach, vi } from "vitest"
|
||||||
|
import getColumns from "./getColumns"
|
||||||
|
|
||||||
|
describe("getColumns", () => {
|
||||||
|
beforeEach(ctx => {
|
||||||
|
ctx.schema = {
|
||||||
|
one: { name: "one", visible: false, order: 0, type: "foo" },
|
||||||
|
two: { name: "two", visible: true, order: 1, type: "foo" },
|
||||||
|
three: { name: "three", visible: true, order: 2, type: "foo" },
|
||||||
|
four: { name: "four", visible: false, order: 3, type: "foo" },
|
||||||
|
five: {
|
||||||
|
name: "excluded",
|
||||||
|
visible: true,
|
||||||
|
order: 4,
|
||||||
|
type: "foo",
|
||||||
|
nestedJSON: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.primaryDisplayColumnName = "four"
|
||||||
|
ctx.onChange = vi.fn()
|
||||||
|
ctx.createComponent = (componentName, props) => {
|
||||||
|
return { componentName, ...props }
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("nested json fields", () => {
|
||||||
|
beforeEach(ctx => {
|
||||||
|
ctx.columns = getColumns({
|
||||||
|
columns: null,
|
||||||
|
schema: ctx.schema,
|
||||||
|
primaryDisplayColumnName: ctx.primaryDisplayColumnName,
|
||||||
|
onChange: ctx.onChange,
|
||||||
|
createComponent: ctx.createComponent,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it("does not return nested json fields, as the grid cannot display them", ctx => {
|
||||||
|
expect(ctx.columns.sortable).not.toContainEqual({
|
||||||
|
name: "excluded",
|
||||||
|
visible: true,
|
||||||
|
order: 4,
|
||||||
|
type: "foo",
|
||||||
|
nestedJSON: true,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("using the old grid column format", () => {
|
||||||
|
beforeEach(ctx => {
|
||||||
|
const oldGridFormatColumns = [
|
||||||
|
{ displayName: "three label", name: "three" },
|
||||||
|
{ displayName: "two label", name: "two" },
|
||||||
|
]
|
||||||
|
|
||||||
|
ctx.columns = getColumns({
|
||||||
|
columns: oldGridFormatColumns,
|
||||||
|
schema: ctx.schema,
|
||||||
|
primaryDisplayColumnName: ctx.primaryDisplayColumnName,
|
||||||
|
onChange: ctx.onChange,
|
||||||
|
createComponent: ctx.createComponent,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it("returns the selected and unselected fields in the modern format, respecting the original order", ctx => {
|
||||||
|
expect(ctx.columns.sortable).toEqual([
|
||||||
|
{
|
||||||
|
_instanceName: "three",
|
||||||
|
active: true,
|
||||||
|
columnType: "foo",
|
||||||
|
componentName: "@budibase/standard-components/labelfield",
|
||||||
|
field: "three",
|
||||||
|
label: "three label",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
_instanceName: "two",
|
||||||
|
active: true,
|
||||||
|
columnType: "foo",
|
||||||
|
componentName: "@budibase/standard-components/labelfield",
|
||||||
|
field: "two",
|
||||||
|
label: "two label",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
_instanceName: "one",
|
||||||
|
active: false,
|
||||||
|
columnType: "foo",
|
||||||
|
componentName: "@budibase/standard-components/labelfield",
|
||||||
|
field: "one",
|
||||||
|
label: "one",
|
||||||
|
},
|
||||||
|
])
|
||||||
|
|
||||||
|
expect(ctx.columns.primary).toEqual({
|
||||||
|
_instanceName: "four",
|
||||||
|
active: true,
|
||||||
|
columnType: "foo",
|
||||||
|
componentName: "@budibase/standard-components/labelfield",
|
||||||
|
field: "four",
|
||||||
|
label: "four",
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("default columns", () => {
|
||||||
|
beforeEach(ctx => {
|
||||||
|
ctx.columns = getColumns({
|
||||||
|
columns: undefined,
|
||||||
|
schema: ctx.schema,
|
||||||
|
primaryDisplayColumnName: ctx.primaryDisplayColumnName,
|
||||||
|
onChange: ctx.onChange,
|
||||||
|
createComponent: ctx.createComponent,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it("returns all columns, with non-hidden columns automatically selected", ctx => {
|
||||||
|
expect(ctx.columns.sortable).toEqual([
|
||||||
|
{
|
||||||
|
_instanceName: "two",
|
||||||
|
active: true,
|
||||||
|
columnType: "foo",
|
||||||
|
componentName: "@budibase/standard-components/labelfield",
|
||||||
|
field: "two",
|
||||||
|
label: "two",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
_instanceName: "three",
|
||||||
|
active: true,
|
||||||
|
columnType: "foo",
|
||||||
|
componentName: "@budibase/standard-components/labelfield",
|
||||||
|
field: "three",
|
||||||
|
label: "three",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
_instanceName: "one",
|
||||||
|
active: false,
|
||||||
|
columnType: "foo",
|
||||||
|
componentName: "@budibase/standard-components/labelfield",
|
||||||
|
field: "one",
|
||||||
|
label: "one",
|
||||||
|
},
|
||||||
|
])
|
||||||
|
|
||||||
|
expect(ctx.columns.primary).toEqual({
|
||||||
|
_instanceName: "four",
|
||||||
|
active: true,
|
||||||
|
columnType: "foo",
|
||||||
|
componentName: "@budibase/standard-components/labelfield",
|
||||||
|
field: "four",
|
||||||
|
label: "four",
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it("Unselected columns should be placed at the end", ctx => {
|
||||||
|
expect(ctx.columns.sortable[2].field).toEqual("one")
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("missing columns", () => {
|
||||||
|
beforeEach(ctx => {
|
||||||
|
const gridFormatColumns = [
|
||||||
|
{ label: "three label", field: "three", active: true },
|
||||||
|
]
|
||||||
|
|
||||||
|
ctx.columns = getColumns({
|
||||||
|
columns: gridFormatColumns,
|
||||||
|
schema: ctx.schema,
|
||||||
|
primaryDisplayColumnName: ctx.primaryDisplayColumnName,
|
||||||
|
onChange: ctx.onChange,
|
||||||
|
createComponent: ctx.createComponent,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it("returns all columns, including those missing from the initial data", ctx => {
|
||||||
|
expect(ctx.columns.sortable).toEqual([
|
||||||
|
{
|
||||||
|
_instanceName: "three",
|
||||||
|
active: true,
|
||||||
|
columnType: "foo",
|
||||||
|
componentName: "@budibase/standard-components/labelfield",
|
||||||
|
field: "three",
|
||||||
|
label: "three label",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
_instanceName: "two",
|
||||||
|
active: false,
|
||||||
|
columnType: "foo",
|
||||||
|
componentName: "@budibase/standard-components/labelfield",
|
||||||
|
field: "two",
|
||||||
|
label: "two",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
_instanceName: "one",
|
||||||
|
active: false,
|
||||||
|
columnType: "foo",
|
||||||
|
componentName: "@budibase/standard-components/labelfield",
|
||||||
|
field: "one",
|
||||||
|
label: "one",
|
||||||
|
},
|
||||||
|
])
|
||||||
|
|
||||||
|
expect(ctx.columns.primary).toEqual({
|
||||||
|
_instanceName: "four",
|
||||||
|
active: true,
|
||||||
|
columnType: "foo",
|
||||||
|
componentName: "@budibase/standard-components/labelfield",
|
||||||
|
field: "four",
|
||||||
|
label: "four",
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("invalid columns", () => {
|
||||||
|
beforeEach(ctx => {
|
||||||
|
const gridFormatColumns = [
|
||||||
|
{ label: "three label", field: "three", active: true },
|
||||||
|
{ label: "some nonsense", field: "some nonsense", active: true },
|
||||||
|
]
|
||||||
|
|
||||||
|
ctx.columns = getColumns({
|
||||||
|
columns: gridFormatColumns,
|
||||||
|
schema: ctx.schema,
|
||||||
|
primaryDisplayColumnName: ctx.primaryDisplayColumnName,
|
||||||
|
onChange: ctx.onChange,
|
||||||
|
createComponent: ctx.createComponent,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it("returns all valid columns, excluding those that aren't valid for the schema", ctx => {
|
||||||
|
expect(ctx.columns.sortable).toEqual([
|
||||||
|
{
|
||||||
|
_instanceName: "three",
|
||||||
|
active: true,
|
||||||
|
columnType: "foo",
|
||||||
|
componentName: "@budibase/standard-components/labelfield",
|
||||||
|
field: "three",
|
||||||
|
label: "three label",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
_instanceName: "two",
|
||||||
|
active: false,
|
||||||
|
columnType: "foo",
|
||||||
|
componentName: "@budibase/standard-components/labelfield",
|
||||||
|
field: "two",
|
||||||
|
label: "two",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
_instanceName: "one",
|
||||||
|
active: false,
|
||||||
|
columnType: "foo",
|
||||||
|
componentName: "@budibase/standard-components/labelfield",
|
||||||
|
field: "one",
|
||||||
|
label: "one",
|
||||||
|
},
|
||||||
|
])
|
||||||
|
|
||||||
|
expect(ctx.columns.primary).toEqual({
|
||||||
|
_instanceName: "four",
|
||||||
|
active: true,
|
||||||
|
columnType: "foo",
|
||||||
|
componentName: "@budibase/standard-components/labelfield",
|
||||||
|
field: "four",
|
||||||
|
label: "four",
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("methods", () => {
|
||||||
|
beforeEach(ctx => {
|
||||||
|
const { update, updateSortable } = getColumns({
|
||||||
|
columns: [],
|
||||||
|
schema: ctx.schema,
|
||||||
|
primaryDisplayColumnName: ctx.primaryDisplayColumnName,
|
||||||
|
onChange: ctx.onChange,
|
||||||
|
createComponent: ctx.createComponent,
|
||||||
|
})
|
||||||
|
|
||||||
|
ctx.update = update
|
||||||
|
ctx.updateSortable = updateSortable
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("update", () => {
|
||||||
|
beforeEach(ctx => {
|
||||||
|
ctx.update({
|
||||||
|
field: "one",
|
||||||
|
label: "a new label",
|
||||||
|
active: true,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it("calls the callback with the updated columns", ctx => {
|
||||||
|
expect(ctx.onChange).toHaveBeenCalledTimes(1)
|
||||||
|
expect(ctx.onChange).toHaveBeenCalledWith([
|
||||||
|
{
|
||||||
|
field: "two",
|
||||||
|
label: "two",
|
||||||
|
active: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: "three",
|
||||||
|
label: "three",
|
||||||
|
active: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: "one",
|
||||||
|
label: "a new label",
|
||||||
|
active: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: "four",
|
||||||
|
label: "four",
|
||||||
|
active: true,
|
||||||
|
},
|
||||||
|
])
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("updateSortable", () => {
|
||||||
|
beforeEach(ctx => {
|
||||||
|
ctx.updateSortable([
|
||||||
|
{
|
||||||
|
_instanceName: "three",
|
||||||
|
active: true,
|
||||||
|
columnType: "foo",
|
||||||
|
componentName: "@budibase/standard-components/labelfield",
|
||||||
|
field: "three",
|
||||||
|
label: "three",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
_instanceName: "one",
|
||||||
|
active: true,
|
||||||
|
columnType: "foo",
|
||||||
|
componentName: "@budibase/standard-components/labelfield",
|
||||||
|
field: "one",
|
||||||
|
label: "one",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
_instanceName: "two",
|
||||||
|
active: false,
|
||||||
|
columnType: "foo",
|
||||||
|
componentName: "@budibase/standard-components/labelfield",
|
||||||
|
field: "two",
|
||||||
|
label: "two",
|
||||||
|
},
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
|
it("calls the callback with the updated columns", ctx => {
|
||||||
|
expect(ctx.onChange).toHaveBeenCalledTimes(1)
|
||||||
|
expect(ctx.onChange).toHaveBeenCalledWith([
|
||||||
|
{
|
||||||
|
field: "three",
|
||||||
|
label: "three",
|
||||||
|
active: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: "one",
|
||||||
|
label: "one",
|
||||||
|
active: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: "two",
|
||||||
|
label: "two",
|
||||||
|
active: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: "four",
|
||||||
|
label: "four",
|
||||||
|
active: true,
|
||||||
|
},
|
||||||
|
])
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
|
@ -364,7 +364,10 @@
|
||||||
const payload = [
|
const payload = [
|
||||||
{
|
{
|
||||||
email: newUserEmail,
|
email: newUserEmail,
|
||||||
builder: { global: creationRoleType === Constants.BudibaseRoles.Admin },
|
builder: {
|
||||||
|
global: creationRoleType === Constants.BudibaseRoles.Admin,
|
||||||
|
creator: creationRoleType === Constants.BudibaseRoles.Creator,
|
||||||
|
},
|
||||||
admin: { global: creationRoleType === Constants.BudibaseRoles.Admin },
|
admin: { global: creationRoleType === Constants.BudibaseRoles.Admin },
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
@ -471,10 +474,6 @@
|
||||||
await users.removeAppBuilder(userId, prodAppId)
|
await users.removeAppBuilder(userId, prodAppId)
|
||||||
}
|
}
|
||||||
|
|
||||||
const addGroupAppBuilder = async groupId => {
|
|
||||||
await groups.actions.addGroupAppBuilder(groupId, prodAppId)
|
|
||||||
}
|
|
||||||
|
|
||||||
const removeGroupAppBuilder = async groupId => {
|
const removeGroupAppBuilder = async groupId => {
|
||||||
await groups.actions.removeGroupAppBuilder(groupId, prodAppId)
|
await groups.actions.removeGroupAppBuilder(groupId, prodAppId)
|
||||||
}
|
}
|
||||||
|
@ -495,14 +494,12 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
const getInviteRoleValue = invite => {
|
const getInviteRoleValue = invite => {
|
||||||
if (invite.info?.admin?.global && invite.info?.builder?.global) {
|
if (
|
||||||
return Constants.Roles.ADMIN
|
(invite.info?.admin?.global && invite.info?.builder?.global) ||
|
||||||
}
|
invite.info?.builder?.apps?.includes(prodAppId)
|
||||||
|
) {
|
||||||
if (invite.info?.builder?.apps?.includes(prodAppId)) {
|
|
||||||
return Constants.Roles.CREATOR
|
return Constants.Roles.CREATOR
|
||||||
}
|
}
|
||||||
|
|
||||||
return invite.info.apps?.[prodAppId]
|
return invite.info.apps?.[prodAppId]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -512,7 +509,7 @@
|
||||||
return `This user has been given ${role?.name} access from the ${user.group} group`
|
return `This user has been given ${role?.name} access from the ${user.group} group`
|
||||||
}
|
}
|
||||||
if (user.isAdminOrGlobalBuilder) {
|
if (user.isAdminOrGlobalBuilder) {
|
||||||
return "This user's role grants admin access to all apps"
|
return "Account admins can edit all apps"
|
||||||
}
|
}
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
@ -523,6 +520,18 @@
|
||||||
}
|
}
|
||||||
return user.role
|
return user.role
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const checkAppAccess = e => {
|
||||||
|
// Ensure we don't get into an invalid combo of tenant role and app access
|
||||||
|
if (
|
||||||
|
e.detail === Constants.BudibaseRoles.AppUser &&
|
||||||
|
creationAccessType === Constants.Roles.CREATOR
|
||||||
|
) {
|
||||||
|
creationAccessType = Constants.Roles.BASIC
|
||||||
|
} else if (e.detail === Constants.BudibaseRoles.Admin) {
|
||||||
|
creationAccessType = Constants.Roles.CREATOR
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svelte:window on:keydown={handleKeyDown} />
|
<svelte:window on:keydown={handleKeyDown} />
|
||||||
|
@ -650,8 +659,9 @@
|
||||||
autoWidth
|
autoWidth
|
||||||
align="right"
|
align="right"
|
||||||
allowedRoles={user.isAdminOrGlobalBuilder
|
allowedRoles={user.isAdminOrGlobalBuilder
|
||||||
? [Constants.Roles.ADMIN]
|
? [Constants.Roles.CREATOR]
|
||||||
: null}
|
: null}
|
||||||
|
labelPrefix="Can use as"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -695,19 +705,16 @@
|
||||||
allowRemove={group.role}
|
allowRemove={group.role}
|
||||||
allowPublic={false}
|
allowPublic={false}
|
||||||
quiet={true}
|
quiet={true}
|
||||||
allowCreator={true}
|
allowCreator={group.role === Constants.Roles.CREATOR}
|
||||||
on:change={e => {
|
on:change={e => {
|
||||||
if (e.detail === Constants.Roles.CREATOR) {
|
onUpdateGroup(group, e.detail)
|
||||||
addGroupAppBuilder(group._id)
|
|
||||||
} else {
|
|
||||||
onUpdateGroup(group, e.detail)
|
|
||||||
}
|
|
||||||
}}
|
}}
|
||||||
on:remove={() => {
|
on:remove={() => {
|
||||||
onUpdateGroup(group)
|
onUpdateGroup(group)
|
||||||
}}
|
}}
|
||||||
autoWidth
|
autoWidth
|
||||||
align="right"
|
align="right"
|
||||||
|
labelPrefix="Can use as"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -753,6 +760,7 @@
|
||||||
allowedRoles={user.isAdminOrGlobalBuilder
|
allowedRoles={user.isAdminOrGlobalBuilder
|
||||||
? [Constants.Roles.CREATOR]
|
? [Constants.Roles.CREATOR]
|
||||||
: null}
|
: null}
|
||||||
|
labelPrefix="Can use as"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -781,7 +789,7 @@
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
<Divider />
|
<Divider noMargin />
|
||||||
<div class="body">
|
<div class="body">
|
||||||
<Layout gap="L" noPadding>
|
<Layout gap="L" noPadding>
|
||||||
<div class="user-invite-form">
|
<div class="user-invite-form">
|
||||||
|
@ -804,31 +812,34 @@
|
||||||
<FancySelect
|
<FancySelect
|
||||||
bind:value={creationRoleType}
|
bind:value={creationRoleType}
|
||||||
options={sdk.users.isAdmin($auth.user)
|
options={sdk.users.isAdmin($auth.user)
|
||||||
? Constants.BudibaseRoleOptionsNew
|
? Constants.BudibaseRoleOptions
|
||||||
: Constants.BudibaseRoleOptionsNew.filter(
|
: Constants.BudibaseRoleOptions.filter(
|
||||||
option => option.value !== Constants.BudibaseRoles.Admin
|
option => option.value !== Constants.BudibaseRoles.Admin
|
||||||
)}
|
)}
|
||||||
label="Role"
|
label="Role"
|
||||||
|
on:change={checkAppAccess}
|
||||||
/>
|
/>
|
||||||
{#if creationRoleType !== Constants.BudibaseRoles.Admin}
|
<span class="role-wrap">
|
||||||
<RoleSelect
|
<RoleSelect
|
||||||
placeholder={false}
|
placeholder={false}
|
||||||
bind:value={creationAccessType}
|
bind:value={creationAccessType}
|
||||||
allowPublic={false}
|
allowPublic={false}
|
||||||
allowCreator={true}
|
allowCreator={creationRoleType !==
|
||||||
|
Constants.BudibaseRoles.AppUser}
|
||||||
quiet={true}
|
quiet={true}
|
||||||
autoWidth
|
autoWidth
|
||||||
align="right"
|
align="right"
|
||||||
fancySelect
|
fancySelect
|
||||||
|
allowedRoles={creationRoleType === Constants.BudibaseRoles.Admin
|
||||||
|
? [Constants.Roles.CREATOR]
|
||||||
|
: null}
|
||||||
|
footer={getRoleFooter({
|
||||||
|
isAdminOrGlobalBuilder:
|
||||||
|
creationRoleType === Constants.BudibaseRoles.Admin,
|
||||||
|
})}
|
||||||
/>
|
/>
|
||||||
{/if}
|
</span>
|
||||||
</FancyForm>
|
</FancyForm>
|
||||||
{#if creationRoleType === Constants.BudibaseRoles.Admin}
|
|
||||||
<div class="admin-info">
|
|
||||||
<Icon name="Info" />
|
|
||||||
Admins will get full access to all apps and settings
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
<span class="add-user">
|
<span class="add-user">
|
||||||
<Button
|
<Button
|
||||||
newStyles
|
newStyles
|
||||||
|
@ -847,6 +858,13 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
.role-wrap :global(.fancy-field:not(:last-of-type)) {
|
||||||
|
border-bottom-left-radius: 4px;
|
||||||
|
border-bottom-right-radius: 4px;
|
||||||
|
border-top-left-radius: 0;
|
||||||
|
border-top-right-radius: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.search :global(input) {
|
.search :global(input) {
|
||||||
padding-left: 0px;
|
padding-left: 0px;
|
||||||
}
|
}
|
||||||
|
@ -862,16 +880,6 @@
|
||||||
display: grid;
|
display: grid;
|
||||||
}
|
}
|
||||||
|
|
||||||
.admin-info {
|
|
||||||
margin-top: var(--spacing-xl);
|
|
||||||
padding: var(--spacing-l) var(--spacing-l) var(--spacing-l) var(--spacing-l);
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: var(--spacing-xl);
|
|
||||||
height: 30px;
|
|
||||||
background-color: var(--background-alt);
|
|
||||||
}
|
|
||||||
|
|
||||||
.underlined {
|
.underlined {
|
||||||
text-decoration: underline;
|
text-decoration: underline;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
@ -889,7 +897,6 @@
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: var(--spacing-s);
|
gap: var(--spacing-s);
|
||||||
width: 400px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.auth-entity-meta {
|
.auth-entity-meta {
|
||||||
|
@ -918,7 +925,7 @@
|
||||||
.auth-entity,
|
.auth-entity,
|
||||||
.auth-entity-header {
|
.auth-entity-header {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 1fr 110px;
|
grid-template-columns: 1fr 180px;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: var(--spacing-xl);
|
gap: var(--spacing-xl);
|
||||||
}
|
}
|
||||||
|
@ -949,7 +956,7 @@
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
overflow-x: hidden;
|
overflow-x: hidden;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
width: 400px;
|
width: 440px;
|
||||||
right: 0;
|
right: 0;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
box-shadow: 0 0 40px 10px rgba(0, 0, 0, 0.1);
|
box-shadow: 0 0 40px 10px rgba(0, 0, 0, 0.1);
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
<script>
|
<script>
|
||||||
import Panel from "components/design/Panel.svelte"
|
import Panel from "components/design/Panel.svelte"
|
||||||
import { store, selectedComponent, selectedScreen } from "builderStore"
|
import { store, selectedComponent, selectedScreen } from "builderStore"
|
||||||
import { getComponentText } from "builderStore/componentUtils"
|
import { getComponentName } from "builderStore/componentUtils"
|
||||||
import ComponentSettingsSection from "./ComponentSettingsSection.svelte"
|
import ComponentSettingsSection from "./ComponentSettingsSection.svelte"
|
||||||
import DesignSection from "./DesignSection.svelte"
|
import DesignSection from "./DesignSection.svelte"
|
||||||
import CustomStylesSection from "./CustomStylesSection.svelte"
|
import CustomStylesSection from "./CustomStylesSection.svelte"
|
||||||
|
@ -43,17 +43,25 @@
|
||||||
|
|
||||||
$: id = $selectedComponent?._id
|
$: id = $selectedComponent?._id
|
||||||
$: id, (section = tabs[0])
|
$: id, (section = tabs[0])
|
||||||
|
|
||||||
|
$: componentName = getComponentName(componentInstance)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if $selectedComponent}
|
{#if $selectedComponent}
|
||||||
{#key $selectedComponent._id}
|
{#key $selectedComponent._id}
|
||||||
<Panel {title} icon={componentDefinition?.icon} borderLeft wide>
|
<Panel
|
||||||
|
{title}
|
||||||
|
icon={componentDefinition?.icon}
|
||||||
|
iconTooltip={componentName}
|
||||||
|
borderLeft
|
||||||
|
wide
|
||||||
|
>
|
||||||
<span class="panel-title-content" slot="panel-title-content">
|
<span class="panel-title-content" slot="panel-title-content">
|
||||||
<input
|
<input
|
||||||
class="input"
|
class="input"
|
||||||
value={title}
|
value={title}
|
||||||
{title}
|
{title}
|
||||||
placeholder={getComponentText(componentInstance)}
|
placeholder={componentName}
|
||||||
on:keypress={e => {
|
on:keypress={e => {
|
||||||
if (e.key.toLowerCase() === "enter") {
|
if (e.key.toLowerCase() === "enter") {
|
||||||
e.target.blur()
|
e.target.blur()
|
||||||
|
|
|
@ -25,6 +25,7 @@
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.app-panel {
|
.app-panel {
|
||||||
|
min-width: 410px;
|
||||||
flex: 1 1 auto;
|
flex: 1 1 auto;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
|
@ -12,6 +12,7 @@
|
||||||
import {
|
import {
|
||||||
findComponentPath,
|
findComponentPath,
|
||||||
getComponentText,
|
getComponentText,
|
||||||
|
getComponentName,
|
||||||
} from "builderStore/componentUtils"
|
} from "builderStore/componentUtils"
|
||||||
import { get } from "svelte/store"
|
import { get } from "svelte/store"
|
||||||
import { dndStore } from "./dndStore"
|
import { dndStore } from "./dndStore"
|
||||||
|
@ -110,6 +111,7 @@
|
||||||
on:drop={onDrop}
|
on:drop={onDrop}
|
||||||
text={getComponentText(component)}
|
text={getComponentText(component)}
|
||||||
icon={getComponentIcon(component)}
|
icon={getComponentIcon(component)}
|
||||||
|
iconTooltip={getComponentName(component)}
|
||||||
withArrow={componentHasChildren(component)}
|
withArrow={componentHasChildren(component)}
|
||||||
indentLevel={level}
|
indentLevel={level}
|
||||||
selected={$store.selectedComponentId === component._id}
|
selected={$store.selectedComponentId === component._id}
|
||||||
|
|
|
@ -1,21 +1,55 @@
|
||||||
<script>
|
<script>
|
||||||
import ScreenList from "./ScreenList/index.svelte"
|
import ScreenList from "./ScreenList/index.svelte"
|
||||||
import ComponentList from "./ComponentList/index.svelte"
|
import ComponentList from "./ComponentList/index.svelte"
|
||||||
|
import { getHorizontalResizeActions } from "components/common/resizable"
|
||||||
|
|
||||||
|
const [resizable, resizableHandle] = getHorizontalResizeActions()
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="panel">
|
<div class="panel" use:resizable>
|
||||||
<ScreenList />
|
<div class="content">
|
||||||
<ComponentList />
|
<ScreenList />
|
||||||
|
<ComponentList />
|
||||||
|
</div>
|
||||||
|
<div class="divider">
|
||||||
|
<div class="dividerClickExtender" role="separator" use:resizableHandle />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.panel {
|
.panel {
|
||||||
|
display: flex;
|
||||||
|
min-width: 270px;
|
||||||
width: 310px;
|
width: 310px;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
border-right: var(--border-light);
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
overflow: hidden;
|
||||||
|
flex-grow: 1;
|
||||||
|
height: 100%;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
background: var(--background);
|
background: var(--background);
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.divider {
|
||||||
|
position: relative;
|
||||||
|
height: 100%;
|
||||||
|
width: 2px;
|
||||||
|
background: var(--spectrum-global-color-gray-200);
|
||||||
|
transition: background 130ms ease-out;
|
||||||
|
}
|
||||||
|
.divider:hover {
|
||||||
|
background: var(--spectrum-global-color-gray-300);
|
||||||
|
cursor: row-resize;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dividerClickExtender {
|
||||||
|
position: absolute;
|
||||||
|
cursor: col-resize;
|
||||||
|
height: 100%;
|
||||||
|
width: 12px;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -1,108 +1,50 @@
|
||||||
<script>
|
<script>
|
||||||
import { Layout } from "@budibase/bbui"
|
import { Layout } from "@budibase/bbui"
|
||||||
import {
|
import { store, sortedScreens, userSelectedResourceMap } from "builderStore"
|
||||||
store,
|
|
||||||
sortedScreens,
|
|
||||||
userSelectedResourceMap,
|
|
||||||
screensHeight,
|
|
||||||
} from "builderStore"
|
|
||||||
import NavItem from "components/common/NavItem.svelte"
|
import NavItem from "components/common/NavItem.svelte"
|
||||||
import RoleIndicator from "./RoleIndicator.svelte"
|
import RoleIndicator from "./RoleIndicator.svelte"
|
||||||
import DropdownMenu from "./DropdownMenu.svelte"
|
import DropdownMenu from "./DropdownMenu.svelte"
|
||||||
import { onMount } from "svelte"
|
|
||||||
import { goto } from "@roxi/routify"
|
import { goto } from "@roxi/routify"
|
||||||
|
import { getVerticalResizeActions } from "components/common/resizable"
|
||||||
import NavHeader from "components/common/NavHeader.svelte"
|
import NavHeader from "components/common/NavHeader.svelte"
|
||||||
|
|
||||||
let search = false
|
const [resizable, resizableHandle] = getVerticalResizeActions()
|
||||||
let resizing = false
|
|
||||||
let searchValue = ""
|
|
||||||
|
|
||||||
let container
|
let searching = false
|
||||||
|
let searchValue = ""
|
||||||
let screensContainer
|
let screensContainer
|
||||||
let scrolling = false
|
let scrolling = false
|
||||||
let previousHeight = null
|
|
||||||
let dragOffset
|
|
||||||
|
|
||||||
$: filteredScreens = getFilteredScreens($sortedScreens, searchValue)
|
$: filteredScreens = getFilteredScreens($sortedScreens, searchValue)
|
||||||
|
|
||||||
const sleep = ms => new Promise(resolve => setTimeout(resolve, ms))
|
const handleOpenSearch = async () => {
|
||||||
|
|
||||||
$: search ? openSearch() : closeSearch()
|
|
||||||
|
|
||||||
const openSearch = async () => {
|
|
||||||
screensContainer.scroll({ top: 0, behavior: "smooth" })
|
screensContainer.scroll({ top: 0, behavior: "smooth" })
|
||||||
previousHeight = $screensHeight
|
|
||||||
$screensHeight = "calc(100% + 1px)"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const closeSearch = async () => {
|
$: {
|
||||||
if (previousHeight) {
|
if (searching) {
|
||||||
// Restore previous height and wait for animation
|
handleOpenSearch()
|
||||||
$screensHeight = previousHeight
|
|
||||||
previousHeight = null
|
|
||||||
await sleep(300)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const getFilteredScreens = (screens, search) => {
|
const getFilteredScreens = (screens, searchValue) => {
|
||||||
return screens.filter(screen => {
|
return screens.filter(screen => {
|
||||||
return !search || screen.routing.route.includes(search)
|
return !searchValue || screen.routing.route.includes(searchValue)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleScroll = e => {
|
const handleScroll = e => {
|
||||||
scrolling = e.target.scrollTop !== 0
|
scrolling = e.target.scrollTop !== 0
|
||||||
}
|
}
|
||||||
|
|
||||||
const startResizing = e => {
|
|
||||||
// Reset the height store to match the true height
|
|
||||||
$screensHeight = `${container.getBoundingClientRect().height}px`
|
|
||||||
|
|
||||||
// Store an offset to easily compute new height when moving the mouse
|
|
||||||
dragOffset = parseInt($screensHeight) - e.clientY
|
|
||||||
|
|
||||||
// Add event listeners
|
|
||||||
resizing = true
|
|
||||||
document.addEventListener("mousemove", resize)
|
|
||||||
document.addEventListener("mouseup", stopResizing)
|
|
||||||
}
|
|
||||||
|
|
||||||
const resize = e => {
|
|
||||||
// Prevent negative heights as this screws with layout
|
|
||||||
const newHeight = Math.max(0, e.clientY + dragOffset)
|
|
||||||
if (newHeight == null || isNaN(newHeight)) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
$screensHeight = `${newHeight}px`
|
|
||||||
}
|
|
||||||
|
|
||||||
const stopResizing = () => {
|
|
||||||
resizing = false
|
|
||||||
document.removeEventListener("mousemove", resize)
|
|
||||||
}
|
|
||||||
|
|
||||||
onMount(() => {
|
|
||||||
// Ensure we aren't stuck at 100% height from leaving while searching
|
|
||||||
if ($screensHeight == null || isNaN(parseInt($screensHeight))) {
|
|
||||||
$screensHeight = "210px"
|
|
||||||
}
|
|
||||||
})
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svelte:window />
|
<div class="screens" class:searching use:resizable>
|
||||||
<div
|
|
||||||
class="screens"
|
|
||||||
class:search
|
|
||||||
class:resizing
|
|
||||||
style={`height:${$screensHeight};`}
|
|
||||||
bind:this={container}
|
|
||||||
>
|
|
||||||
<div class="header" class:scrolling>
|
<div class="header" class:scrolling>
|
||||||
<NavHeader
|
<NavHeader
|
||||||
title="Screens"
|
title="Screens"
|
||||||
placeholder="Search for screens"
|
placeholder="Search for screens"
|
||||||
bind:value={searchValue}
|
bind:value={searchValue}
|
||||||
bind:search
|
bind:search={searching}
|
||||||
onAdd={() => $goto("../new")}
|
onAdd={() => $goto("../new")}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
@ -110,6 +52,7 @@
|
||||||
{#if filteredScreens?.length}
|
{#if filteredScreens?.length}
|
||||||
{#each filteredScreens as screen (screen._id)}
|
{#each filteredScreens as screen (screen._id)}
|
||||||
<NavItem
|
<NavItem
|
||||||
|
scrollable
|
||||||
icon={screen.routing.homeScreen ? "Home" : null}
|
icon={screen.routing.homeScreen ? "Home" : null}
|
||||||
indentLevel={0}
|
indentLevel={0}
|
||||||
selected={$store.selectedScreenId === screen._id}
|
selected={$store.selectedScreenId === screen._id}
|
||||||
|
@ -135,9 +78,11 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
|
role="separator"
|
||||||
|
disabled={searching}
|
||||||
class="divider"
|
class="divider"
|
||||||
on:mousedown={startResizing}
|
class:disabled={searching}
|
||||||
on:dblclick={() => screensHeight.set("210px")}
|
use:resizableHandle
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -148,14 +93,12 @@
|
||||||
min-height: 147px;
|
min-height: 147px;
|
||||||
max-height: calc(100% - 147px);
|
max-height: calc(100% - 147px);
|
||||||
position: relative;
|
position: relative;
|
||||||
transition: height 300ms ease-out;
|
transition: height 300ms ease-out, max-height 300ms ease-out;
|
||||||
|
height: 210px;
|
||||||
}
|
}
|
||||||
.screens.search {
|
.screens.searching {
|
||||||
max-height: none;
|
max-height: 100%;
|
||||||
}
|
height: 100% !important;
|
||||||
.screens.resizing {
|
|
||||||
user-select: none;
|
|
||||||
cursor: row-resize;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.header {
|
.header {
|
||||||
|
@ -177,9 +120,6 @@
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
}
|
}
|
||||||
.screens.resizing .content {
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.screens :global(.nav-item) {
|
.screens :global(.nav-item) {
|
||||||
padding-right: 8px !important;
|
padding-right: 8px !important;
|
||||||
|
@ -217,4 +157,10 @@
|
||||||
.divider:hover:after {
|
.divider:hover:after {
|
||||||
background: var(--spectrum-global-color-gray-300);
|
background: var(--spectrum-global-color-gray-300);
|
||||||
}
|
}
|
||||||
|
.divider.disabled {
|
||||||
|
cursor: auto;
|
||||||
|
}
|
||||||
|
.divider.disabled:after {
|
||||||
|
background: var(--spectrum-global-color-gray-200);
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -40,6 +40,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.content {
|
.content {
|
||||||
|
width: 100vw;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
justify-content: flex-start;
|
justify-content: flex-start;
|
||||||
|
|
|
@ -4,8 +4,6 @@
|
||||||
import { url, isActive } from "@roxi/routify"
|
import { url, isActive } from "@roxi/routify"
|
||||||
import DeleteModal from "components/deploy/DeleteModal.svelte"
|
import DeleteModal from "components/deploy/DeleteModal.svelte"
|
||||||
import { isOnlyUser } from "builderStore"
|
import { isOnlyUser } from "builderStore"
|
||||||
import { auth } from "stores/portal"
|
|
||||||
import { sdk } from "@budibase/shared-core"
|
|
||||||
|
|
||||||
let deleteModal
|
let deleteModal
|
||||||
</script>
|
</script>
|
||||||
|
@ -46,24 +44,22 @@
|
||||||
url={$url("./version")}
|
url={$url("./version")}
|
||||||
active={$isActive("./version")}
|
active={$isActive("./version")}
|
||||||
/>
|
/>
|
||||||
{#if sdk.users.isGlobalBuilder($auth.user)}
|
<div class="delete-action">
|
||||||
<div class="delete-action">
|
<AbsTooltip
|
||||||
<AbsTooltip
|
position={TooltipPosition.Bottom}
|
||||||
position={TooltipPosition.Bottom}
|
text={$isOnlyUser
|
||||||
text={$isOnlyUser
|
? null
|
||||||
? null
|
: "Unavailable - another user is editing this app"}
|
||||||
: "Unavailable - another user is editing this app"}
|
>
|
||||||
>
|
<SideNavItem
|
||||||
<SideNavItem
|
text="Delete app"
|
||||||
text="Delete app"
|
disabled={!$isOnlyUser}
|
||||||
disabled={!$isOnlyUser}
|
on:click={() => {
|
||||||
on:click={() => {
|
deleteModal.show()
|
||||||
deleteModal.show()
|
}}
|
||||||
}}
|
/>
|
||||||
/>
|
</AbsTooltip>
|
||||||
</AbsTooltip>
|
</div>
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
</SideNav>
|
</SideNav>
|
||||||
<slot />
|
<slot />
|
||||||
</Content>
|
</Content>
|
||||||
|
|
|
@ -16,7 +16,7 @@
|
||||||
let activeTab = "Apps"
|
let activeTab = "Apps"
|
||||||
|
|
||||||
$: $url(), updateActiveTab($menu)
|
$: $url(), updateActiveTab($menu)
|
||||||
$: isOnboarding = !$apps.length && sdk.users.isGlobalBuilder($auth.user)
|
$: isOnboarding = !$apps.length && sdk.users.hasBuilderPermissions($auth.user)
|
||||||
|
|
||||||
const updateActiveTab = menu => {
|
const updateActiveTab = menu => {
|
||||||
for (let entry of menu) {
|
for (let entry of menu) {
|
||||||
|
|
|
@ -14,7 +14,7 @@
|
||||||
import PortalSideBar from "./_components/PortalSideBar.svelte"
|
import PortalSideBar from "./_components/PortalSideBar.svelte"
|
||||||
|
|
||||||
// Don't block loading if we've already hydrated state
|
// Don't block loading if we've already hydrated state
|
||||||
let loaded = $apps.length != null
|
let loaded = !!$apps?.length
|
||||||
|
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
try {
|
try {
|
||||||
|
@ -34,7 +34,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
// Go to new app page if no apps exists
|
// Go to new app page if no apps exists
|
||||||
if (!$apps.length && sdk.users.isGlobalBuilder($auth.user)) {
|
if (!$apps.length && sdk.users.hasBuilderPermissions($auth.user)) {
|
||||||
$redirect("./onboarding")
|
$redirect("./onboarding")
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
<script>
|
<script>
|
||||||
import {
|
import {
|
||||||
|
banner,
|
||||||
Heading,
|
Heading,
|
||||||
Layout,
|
Layout,
|
||||||
Button,
|
Button,
|
||||||
|
@ -10,6 +11,7 @@
|
||||||
Notification,
|
Notification,
|
||||||
Body,
|
Body,
|
||||||
Search,
|
Search,
|
||||||
|
BANNER_TYPES,
|
||||||
} from "@budibase/bbui"
|
} from "@budibase/bbui"
|
||||||
import Spinner from "components/common/Spinner.svelte"
|
import Spinner from "components/common/Spinner.svelte"
|
||||||
import CreateAppModal from "components/start/CreateAppModal.svelte"
|
import CreateAppModal from "components/start/CreateAppModal.svelte"
|
||||||
|
@ -198,6 +200,20 @@
|
||||||
if (usersLimitLockAction) {
|
if (usersLimitLockAction) {
|
||||||
usersLimitLockAction()
|
usersLimitLockAction()
|
||||||
}
|
}
|
||||||
|
if (!$admin.isDev) {
|
||||||
|
await banner.show({
|
||||||
|
messages: [
|
||||||
|
{
|
||||||
|
message:
|
||||||
|
"We've updated our pricing - see our website to learn more.",
|
||||||
|
type: BANNER_TYPES.NEUTRAL,
|
||||||
|
extraButtonText: "Learn More",
|
||||||
|
extraButtonAction: () =>
|
||||||
|
window.open("https://budibase.com/pricing"),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
notifications.error("Error getting init info")
|
notifications.error("Error getting init info")
|
||||||
}
|
}
|
||||||
|
@ -237,7 +253,7 @@
|
||||||
{#if enrichedApps.length}
|
{#if enrichedApps.length}
|
||||||
<Layout noPadding gap="L">
|
<Layout noPadding gap="L">
|
||||||
<div class="title">
|
<div class="title">
|
||||||
{#if $auth.user && sdk.users.isGlobalBuilder($auth.user)}
|
{#if $auth.user && sdk.users.canCreateApps($auth.user)}
|
||||||
<div class="buttons">
|
<div class="buttons">
|
||||||
<Button
|
<Button
|
||||||
size="M"
|
size="M"
|
||||||
|
|
|
@ -52,7 +52,7 @@
|
||||||
goToApp()
|
goToApp()
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
loading = false
|
loading = false
|
||||||
notifications.error("There was a problem creating your app")
|
notifications.error(e.message || "There was a problem creating your app")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -55,6 +55,7 @@
|
||||||
},
|
},
|
||||||
role: {
|
role: {
|
||||||
width: "1fr",
|
width: "1fr",
|
||||||
|
displayName: "Access",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
const customGroupTableRenderers = [
|
const customGroupTableRenderers = [
|
||||||
|
@ -98,7 +99,7 @@
|
||||||
return y._id === userId
|
return y._id === userId
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
$: globalRole = sdk.users.isAdmin(user) ? "admin" : "appUser"
|
$: globalRole = users.getUserRole(user)
|
||||||
|
|
||||||
const getAvailableApps = (appList, privileged, roles) => {
|
const getAvailableApps = (appList, privileged, roles) => {
|
||||||
let availableApps = appList.slice()
|
let availableApps = appList.slice()
|
||||||
|
@ -177,12 +178,21 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
async function updateUserRole({ detail }) {
|
async function updateUserRole({ detail }) {
|
||||||
if (detail === "developer") {
|
if (detail === Constants.BudibaseRoles.Developer) {
|
||||||
toggleFlags({ admin: { global: false }, builder: { global: true } })
|
toggleFlags({ admin: { global: false }, builder: { global: true } })
|
||||||
} else if (detail === "admin") {
|
} else if (detail === Constants.BudibaseRoles.Admin) {
|
||||||
toggleFlags({ admin: { global: true }, builder: { global: true } })
|
toggleFlags({ admin: { global: true }, builder: { global: true } })
|
||||||
} else if (detail === "appUser") {
|
} else if (detail === Constants.BudibaseRoles.AppUser) {
|
||||||
toggleFlags({ admin: { global: false }, builder: { global: false } })
|
toggleFlags({ admin: { global: false }, builder: { global: false } })
|
||||||
|
} else if (detail === Constants.BudibaseRoles.Creator) {
|
||||||
|
toggleFlags({
|
||||||
|
admin: { global: false },
|
||||||
|
builder: {
|
||||||
|
global: false,
|
||||||
|
creator: true,
|
||||||
|
apps: user?.builder?.apps || [],
|
||||||
|
},
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -295,6 +305,7 @@
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<Label size="L">Role</Label>
|
<Label size="L">Role</Label>
|
||||||
<Select
|
<Select
|
||||||
|
placeholder={null}
|
||||||
disabled={!sdk.users.isAdmin($auth.user)}
|
disabled={!sdk.users.isAdmin($auth.user)}
|
||||||
value={globalRole}
|
value={globalRole}
|
||||||
options={Constants.BudibaseRoleOptions}
|
options={Constants.BudibaseRoleOptions}
|
||||||
|
|
|
@ -29,7 +29,6 @@
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
$: hasError = userData.find(x => x.error != null)
|
$: hasError = userData.find(x => x.error != null)
|
||||||
|
|
||||||
$: userCount = $licensing.userCount + userData.length
|
$: userCount = $licensing.userCount + userData.length
|
||||||
$: reached = licensing.usersLimitReached(userCount)
|
$: reached = licensing.usersLimitReached(userCount)
|
||||||
$: exceeded = licensing.usersLimitExceeded(userCount)
|
$: exceeded = licensing.usersLimitExceeded(userCount)
|
||||||
|
@ -98,7 +97,7 @@
|
||||||
align-items: center;
|
align-items: center;
|
||||||
flex-direction: row;"
|
flex-direction: row;"
|
||||||
>
|
>
|
||||||
<div style="width: 90%">
|
<div style="flex: 1 1 auto;">
|
||||||
<InputDropdown
|
<InputDropdown
|
||||||
inputType="email"
|
inputType="email"
|
||||||
bind:inputValue={input.email}
|
bind:inputValue={input.email}
|
||||||
|
|
|
@ -14,6 +14,10 @@
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<StatusLight square color={RoleUtils.getRoleColour(value)}>
|
{#if value === Constants.Roles.CREATOR}
|
||||||
{getRoleLabel(value)}
|
Can edit
|
||||||
</StatusLight>
|
{:else}
|
||||||
|
<StatusLight square color={RoleUtils.getRoleColour(value)}>
|
||||||
|
Can use as {getRoleLabel(value)}
|
||||||
|
</StatusLight>
|
||||||
|
{/if}
|
||||||
|
|
|
@ -15,6 +15,7 @@
|
||||||
const BYTES_IN_MB = 1000000
|
const BYTES_IN_MB = 1000000
|
||||||
const FILE_SIZE_LIMIT = BYTES_IN_MB * 5
|
const FILE_SIZE_LIMIT = BYTES_IN_MB * 5
|
||||||
const MAX_USERS_UPLOAD_LIMIT = 1000
|
const MAX_USERS_UPLOAD_LIMIT = 1000
|
||||||
|
|
||||||
export let createUsersFromCsv
|
export let createUsersFromCsv
|
||||||
|
|
||||||
let files = []
|
let files = []
|
||||||
|
@ -22,13 +23,16 @@
|
||||||
let userEmails = []
|
let userEmails = []
|
||||||
let userGroups = []
|
let userGroups = []
|
||||||
let usersRole = null
|
let usersRole = null
|
||||||
$: invalidEmails = []
|
|
||||||
|
|
||||||
|
$: invalidEmails = []
|
||||||
$: userCount = $licensing.userCount + userEmails.length
|
$: userCount = $licensing.userCount + userEmails.length
|
||||||
$: exceed = licensing.usersLimitExceeded(userCount)
|
$: exceed = licensing.usersLimitExceeded(userCount)
|
||||||
|
|
||||||
$: importDisabled =
|
$: importDisabled =
|
||||||
!userEmails.length || !validEmails(userEmails) || !usersRole || exceed
|
!userEmails.length || !validEmails(userEmails) || !usersRole || exceed
|
||||||
|
$: roleOptions = Constants.BudibaseRoleOptions.map(option => ({
|
||||||
|
...option,
|
||||||
|
label: `${option.label} - ${option.subtitle}`,
|
||||||
|
}))
|
||||||
|
|
||||||
const validEmails = userEmails => {
|
const validEmails = userEmails => {
|
||||||
if ($admin.cloud && userEmails.length > MAX_USERS_UPLOAD_LIMIT) {
|
if ($admin.cloud && userEmails.length > MAX_USERS_UPLOAD_LIMIT) {
|
||||||
|
@ -100,10 +104,7 @@
|
||||||
users. Upgrade your plan to add more users
|
users. Upgrade your plan to add more users
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
<RadioGroup
|
<RadioGroup bind:value={usersRole} options={roleOptions} />
|
||||||
bind:value={usersRole}
|
|
||||||
options={Constants.BuilderRoleDescriptions}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{#if $licensing.groupsEnabled}
|
{#if $licensing.groupsEnabled}
|
||||||
<Multiselect
|
<Multiselect
|
||||||
|
|
|
@ -4,17 +4,11 @@
|
||||||
|
|
||||||
export let row
|
export let row
|
||||||
|
|
||||||
const TooltipMap = {
|
$: role = Constants.BudibaseRoleOptions.find(
|
||||||
appUser: "Only has access to assigned apps",
|
|
||||||
developer: "Access to the app builder",
|
|
||||||
admin: "Full access",
|
|
||||||
}
|
|
||||||
|
|
||||||
$: role = Constants.BudibaseRoleOptionsOld.find(
|
|
||||||
x => x.value === users.getUserRole(row)
|
x => x.value === users.getUserRole(row)
|
||||||
)
|
)
|
||||||
$: value = role?.label || "Not available"
|
$: value = role?.label || "Not available"
|
||||||
$: tooltip = TooltipMap[role?.value] || ""
|
$: tooltip = role?.subtitle || ""
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div on:click|stopPropagation title={tooltip}>
|
<div on:click|stopPropagation title={tooltip}>
|
||||||
|
|
|
@ -82,6 +82,7 @@
|
||||||
minWidth: "200px",
|
minWidth: "200px",
|
||||||
},
|
},
|
||||||
role: {
|
role: {
|
||||||
|
displayName: "Access",
|
||||||
sortable: false,
|
sortable: false,
|
||||||
width: "1fr",
|
width: "1fr",
|
||||||
},
|
},
|
||||||
|
@ -171,6 +172,7 @@
|
||||||
const payload = userData?.users?.map(user => ({
|
const payload = userData?.users?.map(user => ({
|
||||||
email: user.email,
|
email: user.email,
|
||||||
builder: user.role === Constants.BudibaseRoles.Developer,
|
builder: user.role === Constants.BudibaseRoles.Developer,
|
||||||
|
creator: user.role === Constants.BudibaseRoles.Creator,
|
||||||
admin: user.role === Constants.BudibaseRoles.Admin,
|
admin: user.role === Constants.BudibaseRoles.Admin,
|
||||||
groups: userData.groups,
|
groups: userData.groups,
|
||||||
}))
|
}))
|
||||||
|
@ -189,18 +191,18 @@
|
||||||
|
|
||||||
for (const user of userData?.users ?? []) {
|
for (const user of userData?.users ?? []) {
|
||||||
const { email } = user
|
const { email } = user
|
||||||
|
|
||||||
if (
|
if (
|
||||||
newUsers.find(x => x.email === email) ||
|
newUsers.find(x => x.email === email) ||
|
||||||
currentUserEmails.includes(email)
|
currentUserEmails.includes(email)
|
||||||
)
|
) {
|
||||||
continue
|
continue
|
||||||
|
}
|
||||||
newUsers.push(user)
|
newUsers.push(user)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!newUsers.length)
|
if (!newUsers.length) {
|
||||||
notifications.info("Duplicated! There is no new users to add.")
|
notifications.info("Duplicated! There is no new users to add.")
|
||||||
|
}
|
||||||
return { ...userData, users: newUsers }
|
return { ...userData, users: newUsers }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -265,7 +267,6 @@
|
||||||
try {
|
try {
|
||||||
await groups.actions.init()
|
await groups.actions.init()
|
||||||
groupsLoaded = true
|
groupsLoaded = true
|
||||||
|
|
||||||
pendingInvites = await users.getInvites()
|
pendingInvites = await users.getInvites()
|
||||||
invitesLoaded = true
|
invitesLoaded = true
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|
|
@ -3,6 +3,7 @@ import { API } from "api"
|
||||||
import { update } from "lodash"
|
import { update } from "lodash"
|
||||||
import { licensing } from "."
|
import { licensing } from "."
|
||||||
import { sdk } from "@budibase/shared-core"
|
import { sdk } from "@budibase/shared-core"
|
||||||
|
import { Constants } from "@budibase/frontend-core"
|
||||||
|
|
||||||
export function createUsersStore() {
|
export function createUsersStore() {
|
||||||
const { subscribe, set } = writable({})
|
const { subscribe, set } = writable({})
|
||||||
|
@ -77,6 +78,9 @@ export function createUsersStore() {
|
||||||
case "developer":
|
case "developer":
|
||||||
body.builder = { global: true }
|
body.builder = { global: true }
|
||||||
break
|
break
|
||||||
|
case "creator":
|
||||||
|
body.builder = { creator: true, global: false }
|
||||||
|
break
|
||||||
case "admin":
|
case "admin":
|
||||||
body.admin = { global: true }
|
body.admin = { global: true }
|
||||||
body.builder = { global: true }
|
body.builder = { global: true }
|
||||||
|
@ -120,12 +124,18 @@ export function createUsersStore() {
|
||||||
return await API.removeAppBuilder({ userId, appId })
|
return await API.removeAppBuilder({ userId, appId })
|
||||||
}
|
}
|
||||||
|
|
||||||
const getUserRole = user =>
|
const getUserRole = user => {
|
||||||
sdk.users.isAdmin(user)
|
if (sdk.users.isAdmin(user)) {
|
||||||
? "admin"
|
return Constants.BudibaseRoles.Admin
|
||||||
: sdk.users.isBuilder(user)
|
} else if (sdk.users.isBuilder(user)) {
|
||||||
? "developer"
|
return Constants.BudibaseRoles.Developer
|
||||||
: "appUser"
|
} else if (sdk.users.hasCreatorPermissions(user)) {
|
||||||
|
return Constants.BudibaseRoles.Creator
|
||||||
|
} else {
|
||||||
|
return Constants.BudibaseRoles.AppUser
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const refreshUsage =
|
const refreshUsage =
|
||||||
fn =>
|
fn =>
|
||||||
async (...args) => {
|
async (...args) => {
|
||||||
|
|
|
@ -2698,6 +2698,22 @@
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"labelfield": {
|
||||||
|
"name": "Text Field",
|
||||||
|
"icon": "Text",
|
||||||
|
"editable": true,
|
||||||
|
"size": {
|
||||||
|
"width": 400,
|
||||||
|
"height": 32
|
||||||
|
},
|
||||||
|
"settings": [
|
||||||
|
{
|
||||||
|
"type": "plainText",
|
||||||
|
"label": "Label",
|
||||||
|
"key": "label"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
"stringfield": {
|
"stringfield": {
|
||||||
"name": "Text Field",
|
"name": "Text Field",
|
||||||
"icon": "Text",
|
"icon": "Text",
|
||||||
|
@ -6071,18 +6087,6 @@
|
||||||
"options": ["Create", "Update", "View"],
|
"options": ["Create", "Update", "View"],
|
||||||
"defaultValue": "Create"
|
"defaultValue": "Create"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"type": "text",
|
|
||||||
"label": "Title",
|
|
||||||
"key": "title",
|
|
||||||
"nested": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "text",
|
|
||||||
"label": "Description",
|
|
||||||
"key": "description",
|
|
||||||
"nested": true
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"section": true,
|
"section": true,
|
||||||
"dependsOn": {
|
"dependsOn": {
|
||||||
|
@ -6090,7 +6094,7 @@
|
||||||
"value": "Create",
|
"value": "Create",
|
||||||
"invert": true
|
"invert": true
|
||||||
},
|
},
|
||||||
"name": "Row details",
|
"name": "Row ID",
|
||||||
"info": "<a href='https://docs.budibase.com/docs/form-block' target='_blank'>How to pass a row ID using bindings</a>",
|
"info": "<a href='https://docs.budibase.com/docs/form-block' target='_blank'>How to pass a row ID using bindings</a>",
|
||||||
"settings": [
|
"settings": [
|
||||||
{
|
{
|
||||||
|
@ -6110,8 +6114,20 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"section": true,
|
"section": true,
|
||||||
"name": "Fields",
|
"name": "Details",
|
||||||
"settings": [
|
"settings": [
|
||||||
|
{
|
||||||
|
"type": "text",
|
||||||
|
"label": "Title",
|
||||||
|
"key": "title",
|
||||||
|
"nested": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "text",
|
||||||
|
"label": "Description",
|
||||||
|
"key": "description",
|
||||||
|
"nested": true
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"type": "fieldConfiguration",
|
"type": "fieldConfiguration",
|
||||||
"key": "fields",
|
"key": "fields",
|
||||||
|
@ -6339,19 +6355,6 @@
|
||||||
"key": "table",
|
"key": "table",
|
||||||
"required": true
|
"required": true
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"type": "columns/grid",
|
|
||||||
"label": "Columns",
|
|
||||||
"key": "columns",
|
|
||||||
"dependsOn": [
|
|
||||||
"table",
|
|
||||||
{
|
|
||||||
"setting": "table.type",
|
|
||||||
"value": "custom",
|
|
||||||
"invert": true
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"type": "filter",
|
"type": "filter",
|
||||||
"label": "Filtering",
|
"label": "Filtering",
|
||||||
|
@ -6448,6 +6451,18 @@
|
||||||
"key": "stripeRows",
|
"key": "stripeRows",
|
||||||
"defaultValue": false
|
"defaultValue": false
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"section": true,
|
||||||
|
"name": "Columns",
|
||||||
|
"settings": [
|
||||||
|
{
|
||||||
|
"type": "columns/grid",
|
||||||
|
"key": "columns",
|
||||||
|
"nested": true,
|
||||||
|
"resetOn": "table"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"section": true,
|
"section": true,
|
||||||
"name": "Buttons",
|
"name": "Buttons",
|
||||||
|
|
|
@ -19,6 +19,22 @@
|
||||||
export let onRowClick = null
|
export let onRowClick = null
|
||||||
export let buttons = null
|
export let buttons = null
|
||||||
|
|
||||||
|
// parses columns to fix older formats
|
||||||
|
const getParsedColumns = columns => {
|
||||||
|
// If the first element has an active key all elements should be in the new format
|
||||||
|
if (columns?.length && columns[0]?.active !== undefined) {
|
||||||
|
return columns
|
||||||
|
}
|
||||||
|
|
||||||
|
return columns?.map(column => ({
|
||||||
|
label: column.displayName || column.name,
|
||||||
|
field: column.name,
|
||||||
|
active: true,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
$: parsedColumns = getParsedColumns(columns)
|
||||||
|
|
||||||
const context = getContext("context")
|
const context = getContext("context")
|
||||||
const component = getContext("component")
|
const component = getContext("component")
|
||||||
const {
|
const {
|
||||||
|
@ -33,16 +49,17 @@
|
||||||
|
|
||||||
let grid
|
let grid
|
||||||
|
|
||||||
$: columnWhitelist = columns?.map(col => col.name)
|
$: columnWhitelist = parsedColumns
|
||||||
$: schemaOverrides = getSchemaOverrides(columns)
|
?.filter(col => col.active)
|
||||||
|
?.map(col => col.field)
|
||||||
|
$: schemaOverrides = getSchemaOverrides(parsedColumns)
|
||||||
$: enrichedButtons = enrichButtons(buttons)
|
$: enrichedButtons = enrichButtons(buttons)
|
||||||
|
|
||||||
const getSchemaOverrides = columns => {
|
const getSchemaOverrides = columns => {
|
||||||
let overrides = {}
|
let overrides = {}
|
||||||
columns?.forEach(column => {
|
columns?.forEach(column => {
|
||||||
overrides[column.name] = {
|
overrides[column.field] = {
|
||||||
displayName: column.displayName || column.name,
|
displayName: column.label,
|
||||||
visible: true,
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
return overrides
|
return overrides
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
<script>
|
<script>
|
||||||
import { CoreSelect, CoreMultiselect } from "@budibase/bbui"
|
import { CoreSelect, CoreMultiselect } from "@budibase/bbui"
|
||||||
import { fetchData } from "@budibase/frontend-core"
|
import { fetchData, Utils } from "@budibase/frontend-core"
|
||||||
import { getContext } from "svelte"
|
import { getContext } from "svelte"
|
||||||
import Field from "./Field.svelte"
|
import Field from "./Field.svelte"
|
||||||
import { FieldTypes } from "../../../constants"
|
import { FieldTypes } from "../../../constants"
|
||||||
|
@ -108,7 +108,7 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$: fetchRows(searchTerm, primaryDisplay, defaultValue)
|
$: debouncedFetchRows(searchTerm, primaryDisplay, defaultValue)
|
||||||
|
|
||||||
const fetchRows = async (searchTerm, primaryDisplay, defaultVal) => {
|
const fetchRows = async (searchTerm, primaryDisplay, defaultVal) => {
|
||||||
const allRowsFetched =
|
const allRowsFetched =
|
||||||
|
@ -124,10 +124,22 @@
|
||||||
query: { equal: { _id: defaultVal } },
|
query: { equal: { _id: defaultVal } },
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Ensure we match all filters, rather than any
|
||||||
|
const baseFilter = (filter || []).filter(x => x.operator !== "allOr")
|
||||||
await fetch.update({
|
await fetch.update({
|
||||||
query: { string: { [primaryDisplay]: searchTerm } },
|
filter: [
|
||||||
|
...baseFilter,
|
||||||
|
{
|
||||||
|
// Use a big numeric prefix to avoid clashing with an existing filter
|
||||||
|
field: `999:${primaryDisplay}`,
|
||||||
|
operator: "string",
|
||||||
|
value: searchTerm,
|
||||||
|
},
|
||||||
|
],
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
const debouncedFetchRows = Utils.debounce(fetchRows, 250)
|
||||||
|
|
||||||
const flatten = values => {
|
const flatten = values => {
|
||||||
if (!values) {
|
if (!values) {
|
||||||
|
|
|
@ -214,15 +214,23 @@ export const buildUserEndpoints = API => ({
|
||||||
inviteUsers: async users => {
|
inviteUsers: async users => {
|
||||||
return await API.post({
|
return await API.post({
|
||||||
url: "/api/global/users/multi/invite",
|
url: "/api/global/users/multi/invite",
|
||||||
body: users.map(user => ({
|
body: users.map(user => {
|
||||||
email: user.email,
|
let builder = undefined
|
||||||
userInfo: {
|
if (user.admin || user.builder) {
|
||||||
admin: user.admin ? { global: true } : undefined,
|
builder = { global: true }
|
||||||
builder: user.admin || user.builder ? { global: true } : undefined,
|
} else if (user.creator) {
|
||||||
userGroups: user.groups,
|
builder = { creator: true }
|
||||||
roles: user.apps ? user.apps : undefined,
|
}
|
||||||
},
|
return {
|
||||||
})),
|
email: user.email,
|
||||||
|
userInfo: {
|
||||||
|
admin: user.admin ? { global: true } : undefined,
|
||||||
|
builder,
|
||||||
|
userGroups: user.groups,
|
||||||
|
roles: user.apps ? user.apps : undefined,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}),
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
|
@ -55,11 +55,20 @@ export const deriveStores = context => {
|
||||||
|
|
||||||
// Apply whitelist if specified
|
// Apply whitelist if specified
|
||||||
if ($columnWhitelist?.length) {
|
if ($columnWhitelist?.length) {
|
||||||
Object.keys(enrichedSchema).forEach(key => {
|
const sortedColumns = {}
|
||||||
if (!$columnWhitelist.includes(key)) {
|
|
||||||
delete enrichedSchema[key]
|
$columnWhitelist.forEach((columnKey, idx) => {
|
||||||
|
const enrichedColumn = enrichedSchema[columnKey]
|
||||||
|
if (enrichedColumn) {
|
||||||
|
sortedColumns[columnKey] = {
|
||||||
|
...enrichedColumn,
|
||||||
|
order: idx,
|
||||||
|
visible: true,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
return sortedColumns
|
||||||
}
|
}
|
||||||
|
|
||||||
return enrichedSchema
|
return enrichedSchema
|
||||||
|
|
|
@ -20,42 +20,31 @@ export const TableNames = {
|
||||||
export const BudibaseRoles = {
|
export const BudibaseRoles = {
|
||||||
AppUser: "appUser",
|
AppUser: "appUser",
|
||||||
Developer: "developer",
|
Developer: "developer",
|
||||||
|
Creator: "creator",
|
||||||
Admin: "admin",
|
Admin: "admin",
|
||||||
}
|
}
|
||||||
|
|
||||||
export const BudibaseRoleOptionsOld = [
|
export const BudibaseRoleOptionsOld = [
|
||||||
{ label: "Developer", value: BudibaseRoles.Developer },
|
{
|
||||||
{ label: "Member", value: BudibaseRoles.AppUser },
|
label: "Developer",
|
||||||
{ label: "Admin", value: BudibaseRoles.Admin },
|
value: BudibaseRoles.Developer,
|
||||||
|
},
|
||||||
]
|
]
|
||||||
export const BudibaseRoleOptions = [
|
export const BudibaseRoleOptions = [
|
||||||
{ label: "Member", value: BudibaseRoles.AppUser },
|
|
||||||
{ label: "Admin", value: BudibaseRoles.Admin },
|
|
||||||
]
|
|
||||||
|
|
||||||
export const BudibaseRoleOptionsNew = [
|
|
||||||
{
|
{
|
||||||
label: "Admin",
|
label: "Account admin",
|
||||||
value: "admin",
|
value: BudibaseRoles.Admin,
|
||||||
subtitle: "Has full access to all apps and settings in your account",
|
subtitle: "Has full access to all apps and settings in your account",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "Member",
|
label: "Creator",
|
||||||
value: "appUser",
|
value: BudibaseRoles.Creator,
|
||||||
subtitle: "Can only view apps they have access to",
|
subtitle: "Can create and edit apps they have access to",
|
||||||
},
|
},
|
||||||
]
|
|
||||||
|
|
||||||
export const BuilderRoleDescriptions = [
|
|
||||||
{
|
{
|
||||||
|
label: "App user",
|
||||||
value: BudibaseRoles.AppUser,
|
value: BudibaseRoles.AppUser,
|
||||||
icon: "User",
|
subtitle: "Can only use published apps they have access to",
|
||||||
label: "App user - Only has access to published apps",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
value: BudibaseRoles.Admin,
|
|
||||||
icon: "Draw",
|
|
||||||
label: "Admin - Full access",
|
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
|
@ -1 +1 @@
|
||||||
Subproject commit 5e3d59fc4060fd44b14b2599269c207753d4e5be
|
Subproject commit 1037b032d49244678204704d1bca779a29e395eb
|
|
@ -0,0 +1 @@
|
||||||
|
* @Budibase/backend
|
|
@ -18,12 +18,12 @@ ENV TOP_LEVEL_PATH=/
|
||||||
|
|
||||||
# handle node-gyp
|
# handle node-gyp
|
||||||
RUN apt-get update \
|
RUN apt-get update \
|
||||||
&& apt-get install -y --no-install-recommends g++ make python3
|
&& apt-get install -y --no-install-recommends g++ make python3 jq
|
||||||
RUN yarn global add pm2
|
RUN yarn global add pm2
|
||||||
|
|
||||||
# Install client for oracle datasource
|
# Install client for oracle datasource
|
||||||
RUN apt-get install unzip libaio1
|
RUN apt-get install unzip libaio1
|
||||||
COPY scripts/integrations/oracle/ scripts/integrations/oracle/
|
COPY packages/server/scripts/integrations/oracle/ scripts/integrations/oracle/
|
||||||
RUN /bin/bash -e scripts/integrations/oracle/instantclient/linux/x86-64/install.sh
|
RUN /bin/bash -e scripts/integrations/oracle/instantclient/linux/x86-64/install.sh
|
||||||
|
|
||||||
# Install postgres client for pg_dump utils
|
# Install postgres client for pg_dump utils
|
||||||
|
@ -35,18 +35,42 @@ RUN apt update && apt upgrade -y \
|
||||||
&& apt install postgresql-client-15 -y \
|
&& apt install postgresql-client-15 -y \
|
||||||
&& apt remove software-properties-common apt-transport-https curl gpg -y
|
&& apt remove software-properties-common apt-transport-https curl gpg -y
|
||||||
|
|
||||||
|
WORKDIR /
|
||||||
|
|
||||||
COPY package.json .
|
COPY scripts/removeWorkspaceDependencies.sh scripts/removeWorkspaceDependencies.sh
|
||||||
COPY dist/yarn.lock .
|
RUN chmod +x ./scripts/removeWorkspaceDependencies.sh
|
||||||
RUN yarn install --production=true --network-timeout 1000000 \
|
|
||||||
|
|
||||||
|
WORKDIR /string-templates
|
||||||
|
COPY packages/string-templates/package.json package.json
|
||||||
|
RUN ../scripts/removeWorkspaceDependencies.sh package.json
|
||||||
|
RUN --mount=type=cache,target=/root/.yarn YARN_CACHE_FOLDER=/root/.yarn yarn install --production=true --network-timeout 1000000
|
||||||
|
COPY packages/string-templates .
|
||||||
|
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
COPY packages/server/package.json .
|
||||||
|
COPY packages/server/dist/yarn.lock .
|
||||||
|
RUN cd ../string-templates && yarn link && cd - && yarn link @budibase/string-templates
|
||||||
|
|
||||||
|
COPY scripts/removeWorkspaceDependencies.sh scripts/removeWorkspaceDependencies.sh
|
||||||
|
RUN chmod +x ./scripts/removeWorkspaceDependencies.sh
|
||||||
|
RUN ./scripts/removeWorkspaceDependencies.sh package.json
|
||||||
|
|
||||||
|
RUN --mount=type=cache,target=/root/.yarn YARN_CACHE_FOLDER=/root/.yarn yarn install --production=true --network-timeout 1000000 \
|
||||||
# Remove unneeded data from file system to reduce image size
|
# Remove unneeded data from file system to reduce image size
|
||||||
&& yarn cache clean && apt-get remove -y --purge --auto-remove g++ make python \
|
&& yarn cache clean && apt-get remove -y --purge --auto-remove g++ make python jq \
|
||||||
&& rm -rf /tmp/* /root/.node-gyp /usr/local/lib/node_modules/npm/node_modules/node-gyp
|
&& rm -rf /tmp/* /root/.node-gyp /usr/local/lib/node_modules/npm/node_modules/node-gyp
|
||||||
|
|
||||||
COPY dist/ dist/
|
COPY packages/server/dist/ dist/
|
||||||
COPY docker_run.sh .
|
COPY packages/server/docker_run.sh .
|
||||||
COPY builder/ builder/
|
COPY packages/server/builder/ builder/
|
||||||
COPY client/ client/
|
COPY packages/server/client/ client/
|
||||||
|
|
||||||
|
ARG BUDIBASE_VERSION
|
||||||
|
# Ensuring the version argument is sent
|
||||||
|
RUN test -n "$BUDIBASE_VERSION"
|
||||||
|
ENV BUDIBASE_VERSION=$BUDIBASE_VERSION
|
||||||
|
|
||||||
EXPOSE 4001
|
EXPOSE 4001
|
||||||
|
|
||||||
|
|
|
@ -1,84 +0,0 @@
|
||||||
FROM node:18-slim
|
|
||||||
|
|
||||||
LABEL com.centurylinklabs.watchtower.lifecycle.pre-check="scripts/watchtower-hooks/pre-check.sh"
|
|
||||||
LABEL com.centurylinklabs.watchtower.lifecycle.pre-update="scripts/watchtower-hooks/pre-update.sh"
|
|
||||||
LABEL com.centurylinklabs.watchtower.lifecycle.post-update="scripts/watchtower-hooks/post-update.sh"
|
|
||||||
LABEL com.centurylinklabs.watchtower.lifecycle.post-check="scripts/watchtower-hooks/post-check.sh"
|
|
||||||
|
|
||||||
WORKDIR /app
|
|
||||||
|
|
||||||
ENV PORT=4001
|
|
||||||
ENV COUCH_DB_URL=https://couchdb.budi.live:5984
|
|
||||||
ENV BUDIBASE_ENVIRONMENT=PRODUCTION
|
|
||||||
ENV SERVICE=app-service
|
|
||||||
ENV POSTHOG_TOKEN=phc_bIjZL7oh2GEUd2vqvTBH8WvrX0fWTFQMs6H5KQxiUxU
|
|
||||||
ENV TENANT_FEATURE_FLAGS=*:LICENSING,*:USER_GROUPS,*:ONBOARDING_TOUR
|
|
||||||
ENV ACCOUNT_PORTAL_URL=https://account.budibase.app
|
|
||||||
ENV TOP_LEVEL_PATH=/
|
|
||||||
|
|
||||||
# handle node-gyp
|
|
||||||
RUN apt-get update \
|
|
||||||
&& apt-get install -y --no-install-recommends g++ make python3 jq
|
|
||||||
RUN yarn global add pm2
|
|
||||||
|
|
||||||
# Install client for oracle datasource
|
|
||||||
RUN apt-get install unzip libaio1
|
|
||||||
COPY packages/server/scripts/integrations/oracle/ scripts/integrations/oracle/
|
|
||||||
RUN /bin/bash -e scripts/integrations/oracle/instantclient/linux/x86-64/install.sh
|
|
||||||
|
|
||||||
# Install postgres client for pg_dump utils
|
|
||||||
RUN apt update && apt upgrade -y \
|
|
||||||
&& apt install software-properties-common apt-transport-https curl gpg -y \
|
|
||||||
&& 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 curl gpg -y
|
|
||||||
|
|
||||||
WORKDIR /
|
|
||||||
|
|
||||||
COPY scripts/removeWorkspaceDependencies.sh scripts/removeWorkspaceDependencies.sh
|
|
||||||
RUN chmod +x ./scripts/removeWorkspaceDependencies.sh
|
|
||||||
|
|
||||||
|
|
||||||
WORKDIR /string-templates
|
|
||||||
COPY packages/string-templates/package.json package.json
|
|
||||||
RUN ../scripts/removeWorkspaceDependencies.sh package.json
|
|
||||||
RUN --mount=type=cache,target=/root/.yarn YARN_CACHE_FOLDER=/root/.yarn yarn install --production=true --network-timeout 1000000
|
|
||||||
COPY packages/string-templates .
|
|
||||||
|
|
||||||
|
|
||||||
WORKDIR /app
|
|
||||||
COPY packages/server/package.json .
|
|
||||||
COPY packages/server/dist/yarn.lock .
|
|
||||||
RUN cd ../string-templates && yarn link && cd - && yarn link @budibase/string-templates
|
|
||||||
|
|
||||||
COPY scripts/removeWorkspaceDependencies.sh scripts/removeWorkspaceDependencies.sh
|
|
||||||
RUN chmod +x ./scripts/removeWorkspaceDependencies.sh
|
|
||||||
RUN ./scripts/removeWorkspaceDependencies.sh package.json
|
|
||||||
|
|
||||||
RUN --mount=type=cache,target=/root/.yarn YARN_CACHE_FOLDER=/root/.yarn yarn install --production=true --network-timeout 1000000 \
|
|
||||||
# Remove unneeded data from file system to reduce image size
|
|
||||||
&& yarn cache clean && apt-get remove -y --purge --auto-remove g++ make python jq \
|
|
||||||
&& rm -rf /tmp/* /root/.node-gyp /usr/local/lib/node_modules/npm/node_modules/node-gyp
|
|
||||||
|
|
||||||
COPY packages/server/dist/ dist/
|
|
||||||
COPY packages/server/docker_run.sh .
|
|
||||||
COPY packages/server/builder/ builder/
|
|
||||||
COPY packages/server/client/ client/
|
|
||||||
|
|
||||||
ARG BUDIBASE_VERSION
|
|
||||||
# Ensuring the version argument is sent
|
|
||||||
RUN test -n "$BUDIBASE_VERSION"
|
|
||||||
ENV BUDIBASE_VERSION=$BUDIBASE_VERSION
|
|
||||||
|
|
||||||
EXPOSE 4001
|
|
||||||
|
|
||||||
# have to add node environment production after install
|
|
||||||
# due to this causing yarn to stop installing dev dependencies
|
|
||||||
# which are actually needed to get this environment up and running
|
|
||||||
ENV NODE_ENV=production
|
|
||||||
ENV CLUSTER_MODE=${CLUSTER_MODE}
|
|
||||||
ENV TOP_LEVEL_PATH=/app
|
|
||||||
|
|
||||||
CMD ["./docker_run.sh"]
|
|
|
@ -51,6 +51,7 @@ import {
|
||||||
import { BASE_LAYOUT_PROP_IDS } from "../../constants/layouts"
|
import { BASE_LAYOUT_PROP_IDS } from "../../constants/layouts"
|
||||||
import sdk from "../../sdk"
|
import sdk from "../../sdk"
|
||||||
import { builderSocket } from "../../websockets"
|
import { builderSocket } from "../../websockets"
|
||||||
|
import { sdk as sharedCoreSDK } from "@budibase/shared-core"
|
||||||
|
|
||||||
// utility function, need to do away with this
|
// utility function, need to do away with this
|
||||||
async function getLayouts() {
|
async function getLayouts() {
|
||||||
|
@ -394,6 +395,12 @@ async function appPostCreate(ctx: UserCtx, app: App) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If the user is a creator, we need to give them access to the new app
|
||||||
|
if (sharedCoreSDK.users.hasCreatorPermissions(ctx.user)) {
|
||||||
|
const user = await users.UserDB.getUser(ctx.user._id!)
|
||||||
|
await users.addAppBuilder(user, app.appId)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function create(ctx: UserCtx) {
|
export async function create(ctx: UserCtx) {
|
||||||
|
|
|
@ -16,7 +16,7 @@ router
|
||||||
)
|
)
|
||||||
.post(
|
.post(
|
||||||
"/api/applications",
|
"/api/applications",
|
||||||
authorized(permissions.GLOBAL_BUILDER),
|
authorized(permissions.CREATOR),
|
||||||
applicationValidator(),
|
applicationValidator(),
|
||||||
controller.create
|
controller.create
|
||||||
)
|
)
|
||||||
|
|
|
@ -5,7 +5,7 @@ import {
|
||||||
roles,
|
roles,
|
||||||
users,
|
users,
|
||||||
} from "@budibase/backend-core"
|
} from "@budibase/backend-core"
|
||||||
import { PermissionLevel, PermissionType, Role, UserCtx } from "@budibase/types"
|
import { PermissionLevel, PermissionType, UserCtx } from "@budibase/types"
|
||||||
import builderMiddleware from "./builder"
|
import builderMiddleware from "./builder"
|
||||||
import { isWebhookEndpoint } from "./utils"
|
import { isWebhookEndpoint } from "./utils"
|
||||||
import { paramResource } from "./resourceId"
|
import { paramResource } from "./resourceId"
|
||||||
|
@ -31,13 +31,20 @@ const checkAuthorized = async (
|
||||||
) => {
|
) => {
|
||||||
const appId = context.getAppId()
|
const appId = context.getAppId()
|
||||||
const isGlobalBuilderApi = permType === PermissionType.GLOBAL_BUILDER
|
const isGlobalBuilderApi = permType === PermissionType.GLOBAL_BUILDER
|
||||||
|
const isCreatorApi = permType === PermissionType.CREATOR
|
||||||
const isBuilderApi = permType === PermissionType.BUILDER
|
const isBuilderApi = permType === PermissionType.BUILDER
|
||||||
const globalBuilder = users.isGlobalBuilder(ctx.user)
|
const isGlobalBuilder = users.isGlobalBuilder(ctx.user)
|
||||||
let isBuilder = appId
|
const isCreator = users.isCreator(ctx.user)
|
||||||
|
const isBuilder = appId
|
||||||
? users.isBuilder(ctx.user, appId)
|
? users.isBuilder(ctx.user, appId)
|
||||||
: users.hasBuilderPermissions(ctx.user)
|
: users.hasBuilderPermissions(ctx.user)
|
||||||
// check if this is a builder api and the user is not a builder
|
|
||||||
if ((isGlobalBuilderApi && !globalBuilder) || (isBuilderApi && !isBuilder)) {
|
// check api permission type against user
|
||||||
|
if (
|
||||||
|
(isGlobalBuilderApi && !isGlobalBuilder) ||
|
||||||
|
(isCreatorApi && !isCreator) ||
|
||||||
|
(isBuilderApi && !isBuilder)
|
||||||
|
) {
|
||||||
return ctx.throw(403, "Not Authorized")
|
return ctx.throw(403, "Not Authorized")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -148,6 +155,7 @@ const authorized =
|
||||||
// to find API endpoints which are builder focused
|
// to find API endpoints which are builder focused
|
||||||
if (
|
if (
|
||||||
permType === PermissionType.BUILDER ||
|
permType === PermissionType.BUILDER ||
|
||||||
|
permType === PermissionType.CREATOR ||
|
||||||
permType === PermissionType.GLOBAL_BUILDER
|
permType === PermissionType.GLOBAL_BUILDER
|
||||||
) {
|
) {
|
||||||
await builderMiddleware(ctx)
|
await builderMiddleware(ctx)
|
||||||
|
|
|
@ -133,9 +133,14 @@ export async function exportRows(
|
||||||
|
|
||||||
let result = await search({ tableId, query: requestQuery, sort, sortOrder })
|
let result = await search({ tableId, query: requestQuery, sort, sortOrder })
|
||||||
let rows: Row[] = []
|
let rows: Row[] = []
|
||||||
|
let headers
|
||||||
|
|
||||||
|
if (!tableName) {
|
||||||
|
throw new HTTPError("Could not find table name.", 400)
|
||||||
|
}
|
||||||
|
const schema = datasource.entities[tableName].schema
|
||||||
|
|
||||||
// Filter data to only specified columns if required
|
// Filter data to only specified columns if required
|
||||||
|
|
||||||
if (columns && columns.length) {
|
if (columns && columns.length) {
|
||||||
for (let i = 0; i < result.rows.length; i++) {
|
for (let i = 0; i < result.rows.length; i++) {
|
||||||
rows[i] = {}
|
rows[i] = {}
|
||||||
|
@ -143,22 +148,17 @@ export async function exportRows(
|
||||||
rows[i][column] = result.rows[i][column]
|
rows[i][column] = result.rows[i][column]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
headers = columns
|
||||||
} else {
|
} else {
|
||||||
rows = result.rows
|
rows = result.rows
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!tableName) {
|
|
||||||
throw new HTTPError("Could not find table name.", 400)
|
|
||||||
}
|
|
||||||
const schema = datasource.entities[tableName].schema
|
|
||||||
let exportRows = cleanExportRows(rows, schema, format, columns)
|
let exportRows = cleanExportRows(rows, schema, format, columns)
|
||||||
|
|
||||||
let headers = Object.keys(schema)
|
|
||||||
|
|
||||||
let content: string
|
let content: string
|
||||||
switch (format) {
|
switch (format) {
|
||||||
case exporters.Format.CSV:
|
case exporters.Format.CSV:
|
||||||
content = exporters.csv(headers, exportRows)
|
content = exporters.csv(headers ?? Object.keys(schema), exportRows)
|
||||||
break
|
break
|
||||||
case exporters.Format.JSON:
|
case exporters.Format.JSON:
|
||||||
content = exporters.json(exportRows)
|
content = exporters.json(exportRows)
|
||||||
|
|
|
@ -110,7 +110,7 @@ export async function exportRows(
|
||||||
|
|
||||||
let rows: Row[] = []
|
let rows: Row[] = []
|
||||||
let schema = table.schema
|
let schema = table.schema
|
||||||
|
let headers
|
||||||
// Filter data to only specified columns if required
|
// Filter data to only specified columns if required
|
||||||
if (columns && columns.length) {
|
if (columns && columns.length) {
|
||||||
for (let i = 0; i < result.length; i++) {
|
for (let i = 0; i < result.length; i++) {
|
||||||
|
@ -119,6 +119,7 @@ export async function exportRows(
|
||||||
rows[i][column] = result[i][column]
|
rows[i][column] = result[i][column]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
headers = columns
|
||||||
} else {
|
} else {
|
||||||
rows = result
|
rows = result
|
||||||
}
|
}
|
||||||
|
@ -127,7 +128,7 @@ export async function exportRows(
|
||||||
if (format === Format.CSV) {
|
if (format === Format.CSV) {
|
||||||
return {
|
return {
|
||||||
fileName: "export.csv",
|
fileName: "export.csv",
|
||||||
content: csv(Object.keys(rows[0]), exportRows),
|
content: csv(headers ?? Object.keys(rows[0]), exportRows),
|
||||||
}
|
}
|
||||||
} else if (format === Format.JSON) {
|
} else if (format === Format.JSON) {
|
||||||
return {
|
return {
|
||||||
|
|
|
@ -18,7 +18,6 @@ jest.mock("../../../utilities/rowProcessor", () => ({
|
||||||
|
|
||||||
jest.mock("../../../api/controllers/view/exporters", () => ({
|
jest.mock("../../../api/controllers/view/exporters", () => ({
|
||||||
...jest.requireActual("../../../api/controllers/view/exporters"),
|
...jest.requireActual("../../../api/controllers/view/exporters"),
|
||||||
csv: jest.fn(),
|
|
||||||
Format: {
|
Format: {
|
||||||
CSV: "csv",
|
CSV: "csv",
|
||||||
},
|
},
|
||||||
|
@ -102,5 +101,32 @@ describe("external row sdk", () => {
|
||||||
new HTTPError("Could not find table name.", 400)
|
new HTTPError("Could not find table name.", 400)
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it("should only export specified columns", async () => {
|
||||||
|
mockDatasourcesGet.mockImplementation(async () => ({
|
||||||
|
entities: {
|
||||||
|
tablename: {
|
||||||
|
schema: {
|
||||||
|
name: {},
|
||||||
|
age: {},
|
||||||
|
dob: {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
const headers = ["name", "dob"]
|
||||||
|
|
||||||
|
const result = await exportRows({
|
||||||
|
tableId: "datasource__tablename",
|
||||||
|
format: Format.CSV,
|
||||||
|
query: {},
|
||||||
|
columns: headers,
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(result).toEqual({
|
||||||
|
fileName: "export.csv",
|
||||||
|
content: `"name","dob"`,
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
|
@ -17,7 +17,8 @@
|
||||||
"@budibase/backend-core": ["../backend-core/src"],
|
"@budibase/backend-core": ["../backend-core/src"],
|
||||||
"@budibase/backend-core/*": ["../backend-core/*"],
|
"@budibase/backend-core/*": ["../backend-core/*"],
|
||||||
"@budibase/shared-core": ["../shared-core/src"],
|
"@budibase/shared-core": ["../shared-core/src"],
|
||||||
"@budibase/pro": ["../pro/src"]
|
"@budibase/pro": ["../pro/src"],
|
||||||
|
"@budibase/string-templates": ["../string-templates/src"]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"include": ["src/**/*"],
|
"include": ["src/**/*"],
|
||||||
|
|
|
@ -315,7 +315,7 @@ export const runLuceneQuery = (docs: any[], query?: SearchQuery) => {
|
||||||
new Date(docValue).getTime() > new Date(testValue.high).getTime()
|
new Date(docValue).getTime() > new Date(testValue.high).getTime()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
throw "Cannot perform range filter - invalid type."
|
return false
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -25,6 +25,10 @@ export function isGlobalBuilder(user: User | ContextUser): boolean {
|
||||||
return (isBuilder(user) && !hasAppBuilderPermissions(user)) || isAdmin(user)
|
return (isBuilder(user) && !hasAppBuilderPermissions(user)) || isAdmin(user)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function canCreateApps(user: User | ContextUser): boolean {
|
||||||
|
return isGlobalBuilder(user) || hasCreatorPermissions(user)
|
||||||
|
}
|
||||||
|
|
||||||
// alias for hasAdminPermission, currently do the same thing
|
// alias for hasAdminPermission, currently do the same thing
|
||||||
// in future whether someone has admin permissions and whether they are
|
// in future whether someone has admin permissions and whether they are
|
||||||
// an admin for a specific resource could be separated
|
// an admin for a specific resource could be separated
|
||||||
|
@ -66,7 +70,7 @@ export function hasAppCreatorPermissions(user?: User | ContextUser): boolean {
|
||||||
return _.flow(
|
return _.flow(
|
||||||
_.get("roles"),
|
_.get("roles"),
|
||||||
_.values,
|
_.values,
|
||||||
_.find(x => ["CREATOR", "ADMIN"].includes(x)),
|
_.find(x => x === "CREATOR"),
|
||||||
x => !!x
|
x => !!x
|
||||||
)(user)
|
)(user)
|
||||||
}
|
}
|
||||||
|
@ -76,7 +80,11 @@ export function hasBuilderPermissions(user?: User | ContextUser): boolean {
|
||||||
if (!user) {
|
if (!user) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
return user.builder?.global || hasAppBuilderPermissions(user)
|
return (
|
||||||
|
user.builder?.global ||
|
||||||
|
hasAppBuilderPermissions(user) ||
|
||||||
|
hasCreatorPermissions(user)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// checks if a user is capable of being an admin
|
// checks if a user is capable of being an admin
|
||||||
|
@ -87,13 +95,21 @@ export function hasAdminPermissions(user?: User | ContextUser): boolean {
|
||||||
return !!user.admin?.global
|
return !!user.admin?.global
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function hasCreatorPermissions(user?: User | ContextUser): boolean {
|
||||||
|
if (!user) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return !!user.builder?.creator
|
||||||
|
}
|
||||||
|
|
||||||
export function isCreator(user?: User | ContextUser): boolean {
|
export function isCreator(user?: User | ContextUser): boolean {
|
||||||
if (!user) {
|
if (!user) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
isGlobalBuilder(user) ||
|
isGlobalBuilder(user!) ||
|
||||||
hasAdminPermissions(user) ||
|
hasAdminPermissions(user) ||
|
||||||
|
hasCreatorPermissions(user) ||
|
||||||
hasAppBuilderPermissions(user) ||
|
hasAppBuilderPermissions(user) ||
|
||||||
hasAppCreatorPermissions(user)
|
hasAppCreatorPermissions(user)
|
||||||
)
|
)
|
||||||
|
|
|
@ -130,32 +130,28 @@ describe("runLuceneQuery", () => {
|
||||||
expect(runLuceneQuery(docs, query).map(row => row.order_id)).toEqual([2])
|
expect(runLuceneQuery(docs, query).map(row => row.order_id)).toEqual([2])
|
||||||
})
|
})
|
||||||
|
|
||||||
it("should throw an error is an invalid doc value is passed into a range filter", async () => {
|
it("should return return all docs if an invalid doc value is passed into a range filter", async () => {
|
||||||
|
const docs = [
|
||||||
|
{
|
||||||
|
order_id: 4,
|
||||||
|
customer_id: 1758,
|
||||||
|
order_status: 5,
|
||||||
|
order_date: "{{ Binding.INVALID }}",
|
||||||
|
required_date: "2017-03-05T00:00:00.000Z",
|
||||||
|
shipped_date: "2017-03-03T00:00:00.000Z",
|
||||||
|
store_id: 2,
|
||||||
|
staff_id: 7,
|
||||||
|
description: undefined,
|
||||||
|
label: "",
|
||||||
|
},
|
||||||
|
]
|
||||||
const query = buildQuery("range", {
|
const query = buildQuery("range", {
|
||||||
order_date: {
|
order_date: {
|
||||||
low: "2016-01-04T00:00:00.000Z",
|
low: "2016-01-04T00:00:00.000Z",
|
||||||
high: "2016-01-11T00:00:00.000Z",
|
high: "2016-01-11T00:00:00.000Z",
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
expect(() =>
|
expect(runLuceneQuery(docs, query)).toEqual(docs)
|
||||||
runLuceneQuery(
|
|
||||||
[
|
|
||||||
{
|
|
||||||
order_id: 4,
|
|
||||||
customer_id: 1758,
|
|
||||||
order_status: 5,
|
|
||||||
order_date: "INVALID",
|
|
||||||
required_date: "2017-03-05T00:00:00.000Z",
|
|
||||||
shipped_date: "2017-03-03T00:00:00.000Z",
|
|
||||||
store_id: 2,
|
|
||||||
staff_id: 7,
|
|
||||||
description: undefined,
|
|
||||||
label: "",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
query
|
|
||||||
)
|
|
||||||
).toThrowError("Cannot perform range filter - invalid type.")
|
|
||||||
})
|
})
|
||||||
|
|
||||||
it("should return rows with matches on empty filter", () => {
|
it("should return rows with matches on empty filter", () => {
|
||||||
|
|
|
@ -44,6 +44,7 @@ export interface User extends Document {
|
||||||
builder?: {
|
builder?: {
|
||||||
global?: boolean
|
global?: boolean
|
||||||
apps?: string[]
|
apps?: string[]
|
||||||
|
creator?: boolean
|
||||||
}
|
}
|
||||||
admin?: {
|
admin?: {
|
||||||
global: boolean
|
global: boolean
|
||||||
|
|
|
@ -13,6 +13,7 @@ export enum PermissionType {
|
||||||
AUTOMATION = "automation",
|
AUTOMATION = "automation",
|
||||||
WEBHOOK = "webhook",
|
WEBHOOK = "webhook",
|
||||||
BUILDER = "builder",
|
BUILDER = "builder",
|
||||||
|
CREATOR = "creator",
|
||||||
GLOBAL_BUILDER = "globalBuilder",
|
GLOBAL_BUILDER = "globalBuilder",
|
||||||
QUERY = "query",
|
QUERY = "query",
|
||||||
VIEW = "view",
|
VIEW = "view",
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
* @Budibase/backend
|
|
@ -5,22 +5,38 @@ LABEL com.centurylinklabs.watchtower.lifecycle.pre-update="scripts/watchtower-ho
|
||||||
LABEL com.centurylinklabs.watchtower.lifecycle.post-update="scripts/watchtower-hooks/post-update.sh"
|
LABEL com.centurylinklabs.watchtower.lifecycle.post-update="scripts/watchtower-hooks/post-update.sh"
|
||||||
LABEL com.centurylinklabs.watchtower.lifecycle.post-check="scripts/watchtower-hooks/post-check.sh"
|
LABEL com.centurylinklabs.watchtower.lifecycle.post-check="scripts/watchtower-hooks/post-check.sh"
|
||||||
|
|
||||||
WORKDIR /app
|
|
||||||
|
|
||||||
# handle node-gyp
|
# handle node-gyp
|
||||||
RUN apk add --no-cache --virtual .gyp python3 make g++
|
RUN apk add --no-cache --virtual .gyp python3 make g++ jq
|
||||||
RUN yarn global add pm2
|
RUN yarn global add pm2
|
||||||
|
|
||||||
|
WORKDIR /
|
||||||
|
|
||||||
COPY package.json .
|
COPY scripts/removeWorkspaceDependencies.sh scripts/removeWorkspaceDependencies.sh
|
||||||
COPY dist/yarn.lock .
|
RUN chmod +x ./scripts/removeWorkspaceDependencies.sh
|
||||||
RUN yarn install --production=true --network-timeout 1000000
|
|
||||||
|
|
||||||
|
WORKDIR /string-templates
|
||||||
|
COPY packages/string-templates/package.json package.json
|
||||||
|
RUN ../scripts/removeWorkspaceDependencies.sh package.json
|
||||||
|
RUN --mount=type=cache,target=/root/.yarn YARN_CACHE_FOLDER=/root/.yarn yarn install --production=true --network-timeout 1000000
|
||||||
|
COPY packages/string-templates .
|
||||||
|
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
COPY packages/worker/package.json .
|
||||||
|
COPY packages/worker/dist/yarn.lock .
|
||||||
|
RUN cd ../string-templates && yarn link && cd - && yarn link @budibase/string-templates
|
||||||
|
|
||||||
|
RUN ../scripts/removeWorkspaceDependencies.sh package.json
|
||||||
|
|
||||||
|
RUN --mount=type=cache,target=/root/.yarn YARN_CACHE_FOLDER=/root/.yarn yarn install --production=true --network-timeout 1000000
|
||||||
# Remove unneeded data from file system to reduce image size
|
# Remove unneeded data from file system to reduce image size
|
||||||
RUN apk del .gyp \
|
RUN apk del .gyp \
|
||||||
&& yarn cache clean
|
&& yarn cache clean
|
||||||
|
|
||||||
COPY dist/ dist/
|
COPY packages/worker/dist/ dist/
|
||||||
COPY docker_run.sh .
|
COPY packages/worker/docker_run.sh .
|
||||||
|
|
||||||
EXPOSE 4001
|
EXPOSE 4001
|
||||||
|
|
||||||
|
@ -34,4 +50,9 @@ ENV POSTHOG_TOKEN=phc_bIjZL7oh2GEUd2vqvTBH8WvrX0fWTFQMs6H5KQxiUxU
|
||||||
ENV TENANT_FEATURE_FLAGS=*:LICENSING,*:USER_GROUPS,*:ONBOARDING_TOUR
|
ENV TENANT_FEATURE_FLAGS=*:LICENSING,*:USER_GROUPS,*:ONBOARDING_TOUR
|
||||||
ENV ACCOUNT_PORTAL_URL=https://account.budibase.app
|
ENV ACCOUNT_PORTAL_URL=https://account.budibase.app
|
||||||
|
|
||||||
|
ARG BUDIBASE_VERSION
|
||||||
|
# Ensuring the version argument is sent
|
||||||
|
RUN test -n "$BUDIBASE_VERSION"
|
||||||
|
ENV BUDIBASE_VERSION=$BUDIBASE_VERSION
|
||||||
|
|
||||||
CMD ["./docker_run.sh"]
|
CMD ["./docker_run.sh"]
|
||||||
|
|
|
@ -1,58 +0,0 @@
|
||||||
FROM node:18-alpine
|
|
||||||
|
|
||||||
LABEL com.centurylinklabs.watchtower.lifecycle.pre-check="scripts/watchtower-hooks/pre-check.sh"
|
|
||||||
LABEL com.centurylinklabs.watchtower.lifecycle.pre-update="scripts/watchtower-hooks/pre-update.sh"
|
|
||||||
LABEL com.centurylinklabs.watchtower.lifecycle.post-update="scripts/watchtower-hooks/post-update.sh"
|
|
||||||
LABEL com.centurylinklabs.watchtower.lifecycle.post-check="scripts/watchtower-hooks/post-check.sh"
|
|
||||||
|
|
||||||
|
|
||||||
# handle node-gyp
|
|
||||||
RUN apk add --no-cache --virtual .gyp python3 make g++ jq
|
|
||||||
RUN yarn global add pm2
|
|
||||||
|
|
||||||
WORKDIR /
|
|
||||||
|
|
||||||
COPY scripts/removeWorkspaceDependencies.sh scripts/removeWorkspaceDependencies.sh
|
|
||||||
RUN chmod +x ./scripts/removeWorkspaceDependencies.sh
|
|
||||||
|
|
||||||
|
|
||||||
WORKDIR /string-templates
|
|
||||||
COPY packages/string-templates/package.json package.json
|
|
||||||
RUN ../scripts/removeWorkspaceDependencies.sh package.json
|
|
||||||
RUN --mount=type=cache,target=/root/.yarn YARN_CACHE_FOLDER=/root/.yarn yarn install --production=true --network-timeout 1000000
|
|
||||||
COPY packages/string-templates .
|
|
||||||
|
|
||||||
|
|
||||||
WORKDIR /app
|
|
||||||
COPY packages/worker/package.json .
|
|
||||||
COPY packages/worker/dist/yarn.lock .
|
|
||||||
RUN cd ../string-templates && yarn link && cd - && yarn link @budibase/string-templates
|
|
||||||
|
|
||||||
RUN ../scripts/removeWorkspaceDependencies.sh package.json
|
|
||||||
|
|
||||||
RUN --mount=type=cache,target=/root/.yarn YARN_CACHE_FOLDER=/root/.yarn yarn install --production=true --network-timeout 1000000
|
|
||||||
# Remove unneeded data from file system to reduce image size
|
|
||||||
RUN apk del .gyp \
|
|
||||||
&& yarn cache clean
|
|
||||||
|
|
||||||
COPY packages/worker/dist/ dist/
|
|
||||||
COPY packages/worker/docker_run.sh .
|
|
||||||
|
|
||||||
EXPOSE 4001
|
|
||||||
|
|
||||||
# have to add node environment production after install
|
|
||||||
# due to this causing yarn to stop installing dev dependencies
|
|
||||||
# which are actually needed to get this environment up and running
|
|
||||||
ENV NODE_ENV=production
|
|
||||||
ENV CLUSTER_MODE=${CLUSTER_MODE}
|
|
||||||
ENV SERVICE=worker-service
|
|
||||||
ENV POSTHOG_TOKEN=phc_bIjZL7oh2GEUd2vqvTBH8WvrX0fWTFQMs6H5KQxiUxU
|
|
||||||
ENV TENANT_FEATURE_FLAGS=*:LICENSING,*:USER_GROUPS,*:ONBOARDING_TOUR
|
|
||||||
ENV ACCOUNT_PORTAL_URL=https://account.budibase.app
|
|
||||||
|
|
||||||
ARG BUDIBASE_VERSION
|
|
||||||
# Ensuring the version argument is sent
|
|
||||||
RUN test -n "$BUDIBASE_VERSION"
|
|
||||||
ENV BUDIBASE_VERSION=$BUDIBASE_VERSION
|
|
||||||
|
|
||||||
CMD ["./docker_run.sh"]
|
|
|
@ -51,10 +51,22 @@ export async function removeAppRole(ctx: Ctx) {
|
||||||
const users = await sdk.users.db.allUsers()
|
const users = await sdk.users.db.allUsers()
|
||||||
const bulk = []
|
const bulk = []
|
||||||
const cacheInvalidations = []
|
const cacheInvalidations = []
|
||||||
|
const prodAppId = dbCore.getProdAppID(appId)
|
||||||
for (let user of users) {
|
for (let user of users) {
|
||||||
if (user.roles[appId]) {
|
let updated = false
|
||||||
cacheInvalidations.push(cache.user.invalidateUser(user._id))
|
if (user.roles[prodAppId]) {
|
||||||
delete user.roles[appId]
|
cacheInvalidations.push(cache.user.invalidateUser(user._id!))
|
||||||
|
delete user.roles[prodAppId]
|
||||||
|
updated = true
|
||||||
|
}
|
||||||
|
if (user.builder && Array.isArray(user.builder?.apps)) {
|
||||||
|
const idx = user.builder.apps.indexOf(prodAppId)
|
||||||
|
if (idx !== -1) {
|
||||||
|
user.builder.apps.splice(idx, 1)
|
||||||
|
updated = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (updated) {
|
||||||
bulk.push(user)
|
bulk.push(user)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
yarn build --scope @budibase/server --scope @budibase/worker
|
yarn build --scope @budibase/server --scope @budibase/worker
|
||||||
version=$(./scripts/getCurrentVersion.sh)
|
version=$(./scripts/getCurrentVersion.sh)
|
||||||
docker build -f hosting/single/Dockerfile.v2 -t budibase:latest --build-arg BUDIBASE_VERSION=$version .
|
docker build -f hosting/single/Dockerfile -t budibase:latest --build-arg BUDIBASE_VERSION=$version .
|
||||||
|
|
Loading…
Reference in New Issue