Merge branch 'develop' of github.com:Budibase/budibase into spreadsheet-integration
This commit is contained in:
commit
e0e5ca7a3c
190 changed files with 3628 additions and 3305 deletions
2
.github/ISSUE_TEMPLATE/bug_report.md
vendored
2
.github/ISSUE_TEMPLATE/bug_report.md
vendored
|
@ -2,7 +2,7 @@
|
||||||
name: Bug report
|
name: Bug report
|
||||||
about: Create a report to help us improve
|
about: Create a report to help us improve
|
||||||
title: ''
|
title: ''
|
||||||
labels: bug, linear
|
labels: bug
|
||||||
assignees: ''
|
assignees: ''
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
4
.github/workflows/budibase_ci.yml
vendored
4
.github/workflows/budibase_ci.yml
vendored
|
@ -56,11 +56,11 @@ jobs:
|
||||||
run: yarn install:pro $BRANCH $BASE_BRANCH
|
run: yarn install:pro $BRANCH $BASE_BRANCH
|
||||||
- run: yarn
|
- run: yarn
|
||||||
- run: yarn bootstrap
|
- run: yarn bootstrap
|
||||||
|
- run: yarn build:client
|
||||||
- run: yarn test
|
- run: yarn test
|
||||||
- uses: codecov/codecov-action@v1
|
- uses: codecov/codecov-action@v3
|
||||||
with:
|
with:
|
||||||
token: ${{ secrets.CODECOV_TOKEN }} # not required for public repos
|
token: ${{ secrets.CODECOV_TOKEN }} # not required for public repos
|
||||||
files: ./packages/server/coverage/clover.xml,./packages/worker/coverage/clover.xml,./packages/backend-core/coverage/clover.xml
|
|
||||||
name: codecov-umbrella
|
name: codecov-umbrella
|
||||||
verbose: true
|
verbose: true
|
||||||
|
|
||||||
|
|
23
.github/workflows/deploy-preprod.yml
vendored
23
.github/workflows/deploy-preprod.yml
vendored
|
@ -1,6 +1,10 @@
|
||||||
name: "deploy-preprod"
|
name: "deploy-preprod"
|
||||||
on:
|
on:
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
version:
|
||||||
|
description: Budibase release version. For example - 1.0.0
|
||||||
|
required: false
|
||||||
workflow_call:
|
workflow_call:
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
|
@ -8,10 +12,16 @@ jobs:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v2
|
||||||
- name: 'Get Previous tag'
|
|
||||||
id: previoustag
|
|
||||||
uses: "WyriHaximus/github-action-get-previous-tag@v1"
|
|
||||||
|
|
||||||
|
- name: Get the latest budibase release version
|
||||||
|
id: version
|
||||||
|
run: |
|
||||||
|
if [ -z "${{ github.event.inputs.version }}" ]; then
|
||||||
|
release_version=$(cat lerna.json | jq -r '.version')
|
||||||
|
else
|
||||||
|
release_version=${{ github.event.inputs.version }}
|
||||||
|
fi
|
||||||
|
echo "RELEASE_VERSION=$release_version" >> $GITHUB_ENV
|
||||||
- name: Configure AWS Credentials
|
- name: Configure AWS Credentials
|
||||||
uses: aws-actions/configure-aws-credentials@v1
|
uses: aws-actions/configure-aws-credentials@v1
|
||||||
with:
|
with:
|
||||||
|
@ -26,7 +36,6 @@ jobs:
|
||||||
-o values.preprod.yaml \
|
-o values.preprod.yaml \
|
||||||
-L https://api.github.com/repos/budibase/budibase-infra/contents/kubernetes/budibase-preprod/values.yaml
|
-L https://api.github.com/repos/budibase/budibase-infra/contents/kubernetes/budibase-preprod/values.yaml
|
||||||
wc -l values.preprod.yaml
|
wc -l values.preprod.yaml
|
||||||
|
|
||||||
- name: Deploy to Preprod Environment
|
- name: Deploy to Preprod Environment
|
||||||
uses: budibase/helm@v1.8.0
|
uses: budibase/helm@v1.8.0
|
||||||
with:
|
with:
|
||||||
|
@ -37,7 +46,7 @@ jobs:
|
||||||
helm: helm3
|
helm: helm3
|
||||||
values: |
|
values: |
|
||||||
globals:
|
globals:
|
||||||
appVersion: ${{ steps.previoustag.outputs.tag }}
|
appVersion: v${{ env.RELEASE_VERSION }}
|
||||||
ingress:
|
ingress:
|
||||||
enabled: true
|
enabled: true
|
||||||
nginx: true
|
nginx: true
|
||||||
|
@ -52,5 +61,5 @@ jobs:
|
||||||
uses: tsickert/discord-webhook@v4.0.0
|
uses: tsickert/discord-webhook@v4.0.0
|
||||||
with:
|
with:
|
||||||
webhook-url: ${{ secrets.PROD_DEPLOY_WEBHOOK_URL }}
|
webhook-url: ${{ secrets.PROD_DEPLOY_WEBHOOK_URL }}
|
||||||
content: "Preprod Deployment Complete: ${{ steps.previoustag.outputs.tag }} deployed to Budibase Pre-prod."
|
content: "Preprod Deployment Complete: ${{ env.RELEASE_VERSION }} deployed to Budibase Pre-prod."
|
||||||
embed-title: ${{ steps.previoustag.outputs.tag }}
|
embed-title: ${{ env.RELEASE_VERSION }}
|
||||||
|
|
21
.github/workflows/release-master.yml
vendored
21
.github/workflows/release-master.yml
vendored
|
@ -91,9 +91,11 @@ jobs:
|
||||||
uses: azure/setup-helm@v1
|
uses: azure/setup-helm@v1
|
||||||
id: helm-install
|
id: helm-install
|
||||||
|
|
||||||
- name: 'Get Previous tag'
|
- name: Get the latest budibase release version
|
||||||
id: previoustag
|
id: version
|
||||||
uses: "WyriHaximus/github-action-get-previous-tag@v1"
|
run: |
|
||||||
|
release_version=$(cat lerna.json | jq -r '.version')
|
||||||
|
echo "RELEASE_VERSION=$release_version" >> $GITHUB_ENV
|
||||||
|
|
||||||
# due to helm repo index issue: https://github.com/helm/helm/issues/7363
|
# due to helm repo index issue: https://github.com/helm/helm/issues/7363
|
||||||
# we need to create new package in a different dir, merge the index and move the package back
|
# we need to create new package in a different dir, merge the index and move the package back
|
||||||
|
@ -116,8 +118,6 @@ jobs:
|
||||||
git add -A
|
git add -A
|
||||||
git commit -m "Helm Release: ${{ env.RELEASE_VERSION }}"
|
git commit -m "Helm Release: ${{ env.RELEASE_VERSION }}"
|
||||||
git push
|
git push
|
||||||
env:
|
|
||||||
RELEASE_VERSION: ${{ steps.previoustag.outputs.tag }}
|
|
||||||
|
|
||||||
deploy-to-legacy-preprod-env:
|
deploy-to-legacy-preprod-env:
|
||||||
needs: [release-images]
|
needs: [release-images]
|
||||||
|
@ -130,13 +130,16 @@ jobs:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v2
|
||||||
- name: 'Get Previous tag'
|
|
||||||
id: previoustag
|
- name: Get the latest budibase release version
|
||||||
uses: "WyriHaximus/github-action-get-previous-tag@v1"
|
id: version
|
||||||
|
run: |
|
||||||
|
release_version=$(cat lerna.json | jq -r '.version')
|
||||||
|
echo "RELEASE_VERSION=$release_version" >> $GITHUB_ENV
|
||||||
|
|
||||||
- uses: passeidireto/trigger-external-workflow-action@main
|
- uses: passeidireto/trigger-external-workflow-action@main
|
||||||
env:
|
env:
|
||||||
PAYLOAD_VERSION: ${{ steps.previoustag.outputs.tag }}
|
PAYLOAD_VERSION: ${{ env.RELEASE_VERSION }}
|
||||||
with:
|
with:
|
||||||
repository: budibase/budibase-deploys
|
repository: budibase/budibase-deploys
|
||||||
event: budicloud-preprod-deploy
|
event: budicloud-preprod-deploy
|
||||||
|
|
|
@ -62,16 +62,22 @@ spec:
|
||||||
{{ end }}
|
{{ end }}
|
||||||
- name: ENABLE_ANALYTICS
|
- name: ENABLE_ANALYTICS
|
||||||
value: {{ .Values.globals.enableAnalytics | quote }}
|
value: {{ .Values.globals.enableAnalytics | quote }}
|
||||||
|
- name: API_ENCRYPTION_KEY
|
||||||
|
value: {{ .Values.globals.apiEncryptionKey | quote }}
|
||||||
- name: INTERNAL_API_KEY
|
- name: INTERNAL_API_KEY
|
||||||
valueFrom:
|
valueFrom:
|
||||||
secretKeyRef:
|
secretKeyRef:
|
||||||
name: {{ template "budibase.fullname" . }}
|
name: {{ template "budibase.fullname" . }}
|
||||||
key: internalApiKey
|
key: internalApiKey
|
||||||
|
- name: INTERNAL_API_KEY_FALLBACK
|
||||||
|
value: {{ .Values.globals.internalApiKeyFallback | quote }}
|
||||||
- name: JWT_SECRET
|
- name: JWT_SECRET
|
||||||
valueFrom:
|
valueFrom:
|
||||||
secretKeyRef:
|
secretKeyRef:
|
||||||
name: {{ template "budibase.fullname" . }}
|
name: {{ template "budibase.fullname" . }}
|
||||||
key: jwtSecret
|
key: jwtSecret
|
||||||
|
- name: JWT_SECRET_FALLBACK
|
||||||
|
value: {{ .Values.globals.jwtSecretFallback | quote }}
|
||||||
{{ if .Values.services.objectStore.region }}
|
{{ if .Values.services.objectStore.region }}
|
||||||
- name: AWS_REGION
|
- name: AWS_REGION
|
||||||
value: {{ .Values.services.objectStore.region }}
|
value: {{ .Values.services.objectStore.region }}
|
||||||
|
@ -125,9 +131,9 @@ spec:
|
||||||
- name: SELF_HOSTED
|
- name: SELF_HOSTED
|
||||||
value: {{ .Values.globals.selfHosted | quote }}
|
value: {{ .Values.globals.selfHosted | quote }}
|
||||||
- name: SENTRY_DSN
|
- name: SENTRY_DSN
|
||||||
value: {{ .Values.globals.sentryDSN }}
|
value: {{ .Values.globals.sentryDSN | quote }}
|
||||||
- name: POSTHOG_TOKEN
|
- name: POSTHOG_TOKEN
|
||||||
value: {{ .Values.globals.posthogToken }}
|
value: {{ .Values.globals.posthogToken | quote }}
|
||||||
- name: WORKER_URL
|
- name: WORKER_URL
|
||||||
value: http://worker-service:{{ .Values.services.worker.port }}
|
value: http://worker-service:{{ .Values.services.worker.port }}
|
||||||
- name: PLATFORM_URL
|
- name: PLATFORM_URL
|
||||||
|
@ -198,8 +204,6 @@ spec:
|
||||||
- name: GLOBAL_AGENT_NO_PROXY
|
- name: GLOBAL_AGENT_NO_PROXY
|
||||||
value: {{ .Values.globals.globalAgentNoProxy | quote }}
|
value: {{ .Values.globals.globalAgentNoProxy | quote }}
|
||||||
{{ end }}
|
{{ end }}
|
||||||
- name: CDN_URL
|
|
||||||
value: {{ .Values.globals.cdnUrl }}
|
|
||||||
{{ if .Values.services.tlsRejectUnauthorized }}
|
{{ if .Values.services.tlsRejectUnauthorized }}
|
||||||
- name: NODE_TLS_REJECT_UNAUTHORIZED
|
- name: NODE_TLS_REJECT_UNAUTHORIZED
|
||||||
value: {{ .Values.services.tlsRejectUnauthorized }}
|
value: {{ .Values.services.tlsRejectUnauthorized }}
|
||||||
|
@ -228,6 +232,9 @@ spec:
|
||||||
tolerations:
|
tolerations:
|
||||||
{{- toYaml . | nindent 8 }}
|
{{- toYaml . | nindent 8 }}
|
||||||
{{- end }}
|
{{- end }}
|
||||||
|
{{ if .Values.schedulerName }}
|
||||||
|
schedulerName: {{ .Values.schedulerName | quote }}
|
||||||
|
{{ end }}
|
||||||
{{ if .Values.imagePullSecrets }}
|
{{ if .Values.imagePullSecrets }}
|
||||||
imagePullSecrets:
|
imagePullSecrets:
|
||||||
{{- toYaml .Values.imagePullSecrets | nindent 6 }}
|
{{- toYaml .Values.imagePullSecrets | nindent 6 }}
|
||||||
|
|
|
@ -50,5 +50,8 @@ spec:
|
||||||
tolerations:
|
tolerations:
|
||||||
{{- toYaml . | nindent 8 }}
|
{{- toYaml . | nindent 8 }}
|
||||||
{{- end }}
|
{{- end }}
|
||||||
|
{{ if .Values.schedulerName }}
|
||||||
|
schedulerName: {{ .Values.schedulerName | quote }}
|
||||||
|
{{ end }}
|
||||||
status: {}
|
status: {}
|
||||||
{{- end }}
|
{{- end }}
|
||||||
|
|
|
@ -72,6 +72,9 @@ spec:
|
||||||
tolerations:
|
tolerations:
|
||||||
{{- toYaml . | nindent 8 }}
|
{{- toYaml . | nindent 8 }}
|
||||||
{{- end }}
|
{{- end }}
|
||||||
|
{{ if .Values.schedulerName }}
|
||||||
|
schedulerName: {{ .Values.schedulerName | quote }}
|
||||||
|
{{ end }}
|
||||||
{{ if .Values.imagePullSecrets }}
|
{{ if .Values.imagePullSecrets }}
|
||||||
imagePullSecrets:
|
imagePullSecrets:
|
||||||
{{- toYaml .Values.imagePullSecrets | nindent 6 }}
|
{{- toYaml .Values.imagePullSecrets | nindent 6 }}
|
||||||
|
|
|
@ -78,6 +78,9 @@ spec:
|
||||||
tolerations:
|
tolerations:
|
||||||
{{- toYaml . | nindent 8 }}
|
{{- toYaml . | nindent 8 }}
|
||||||
{{- end }}
|
{{- end }}
|
||||||
|
{{ if .Values.schedulerName }}
|
||||||
|
schedulerName: {{ .Values.schedulerName | quote }}
|
||||||
|
{{ end }}
|
||||||
{{ if .Values.imagePullSecrets }}
|
{{ if .Values.imagePullSecrets }}
|
||||||
imagePullSecrets:
|
imagePullSecrets:
|
||||||
{{- toYaml .Values.imagePullSecrets | nindent 6 }}
|
{{- toYaml .Values.imagePullSecrets | nindent 6 }}
|
||||||
|
|
|
@ -50,6 +50,9 @@ spec:
|
||||||
tolerations:
|
tolerations:
|
||||||
{{- toYaml . | nindent 8 }}
|
{{- toYaml . | nindent 8 }}
|
||||||
{{- end }}
|
{{- end }}
|
||||||
|
{{ if .Values.schedulerName }}
|
||||||
|
schedulerName: {{ .Values.schedulerName | quote }}
|
||||||
|
{{ end }}
|
||||||
{{ if .Values.imagePullSecrets }}
|
{{ if .Values.imagePullSecrets }}
|
||||||
imagePullSecrets:
|
imagePullSecrets:
|
||||||
{{- toYaml .Values.imagePullSecrets | nindent 6 }}
|
{{- toYaml .Values.imagePullSecrets | nindent 6 }}
|
||||||
|
|
|
@ -62,16 +62,22 @@ spec:
|
||||||
{{ else }}
|
{{ else }}
|
||||||
value: http://{{ .Release.Name }}-svc-couchdb:{{ .Values.services.couchdb.port }}
|
value: http://{{ .Release.Name }}-svc-couchdb:{{ .Values.services.couchdb.port }}
|
||||||
{{ end }}
|
{{ end }}
|
||||||
|
- name: API_ENCRYPTION_KEY
|
||||||
|
value: {{ .Values.globals.apiEncryptionKey | quote }}
|
||||||
- name: INTERNAL_API_KEY
|
- name: INTERNAL_API_KEY
|
||||||
valueFrom:
|
valueFrom:
|
||||||
secretKeyRef:
|
secretKeyRef:
|
||||||
name: {{ template "budibase.fullname" . }}
|
name: {{ template "budibase.fullname" . }}
|
||||||
key: internalApiKey
|
key: internalApiKey
|
||||||
|
- name: INTERNAL_API_KEY_FALLBACK
|
||||||
|
value: {{ .Values.globals.internalApiKeyFallback | quote }}
|
||||||
- name: JWT_SECRET
|
- name: JWT_SECRET
|
||||||
valueFrom:
|
valueFrom:
|
||||||
secretKeyRef:
|
secretKeyRef:
|
||||||
name: {{ template "budibase.fullname" . }}
|
name: {{ template "budibase.fullname" . }}
|
||||||
key: jwtSecret
|
key: jwtSecret
|
||||||
|
- name: JWT_SECRET_FALLBACK
|
||||||
|
value: {{ .Values.globals.jwtSecretFallback | quote }}
|
||||||
{{ if .Values.services.objectStore.region }}
|
{{ if .Values.services.objectStore.region }}
|
||||||
- name: AWS_REGION
|
- name: AWS_REGION
|
||||||
value: {{ .Values.services.objectStore.region }}
|
value: {{ .Values.services.objectStore.region }}
|
||||||
|
@ -188,8 +194,6 @@ spec:
|
||||||
- name: GLOBAL_AGENT_NO_PROXY
|
- name: GLOBAL_AGENT_NO_PROXY
|
||||||
value: {{ .Values.globals.globalAgentNoProxy | quote }}
|
value: {{ .Values.globals.globalAgentNoProxy | quote }}
|
||||||
{{ end }}
|
{{ end }}
|
||||||
- name: CDN_URL
|
|
||||||
value: {{ .Values.globals.cdnUrl }}
|
|
||||||
{{ if .Values.services.tlsRejectUnauthorized }}
|
{{ if .Values.services.tlsRejectUnauthorized }}
|
||||||
- name: NODE_TLS_REJECT_UNAUTHORIZED
|
- name: NODE_TLS_REJECT_UNAUTHORIZED
|
||||||
value: {{ .Values.services.tlsRejectUnauthorized }}
|
value: {{ .Values.services.tlsRejectUnauthorized }}
|
||||||
|
@ -218,6 +222,9 @@ spec:
|
||||||
tolerations:
|
tolerations:
|
||||||
{{- toYaml . | nindent 8 }}
|
{{- toYaml . | nindent 8 }}
|
||||||
{{- end }}
|
{{- end }}
|
||||||
|
{{ if .Values.schedulerName }}
|
||||||
|
schedulerName: {{ .Values.schedulerName | quote }}
|
||||||
|
{{ end }}
|
||||||
{{ if .Values.imagePullSecrets }}
|
{{ if .Values.imagePullSecrets }}
|
||||||
imagePullSecrets:
|
imagePullSecrets:
|
||||||
{{- toYaml .Values.imagePullSecrets | nindent 6 }}
|
{{- toYaml .Values.imagePullSecrets | nindent 6 }}
|
||||||
|
|
|
@ -96,9 +96,13 @@ globals:
|
||||||
createSecrets: true # creates an internal API key, JWT secrets and redis password for you
|
createSecrets: true # creates an internal API key, JWT secrets and redis password for you
|
||||||
|
|
||||||
# if createSecrets is set to false, you can hard-code your secrets here
|
# if createSecrets is set to false, you can hard-code your secrets here
|
||||||
|
apiEncryptionKey: ""
|
||||||
internalApiKey: ""
|
internalApiKey: ""
|
||||||
jwtSecret: ""
|
jwtSecret: ""
|
||||||
cdnUrl: ""
|
cdnUrl: ""
|
||||||
|
# fallback values used during live rotation
|
||||||
|
internalApiKeyFallback: ""
|
||||||
|
jwtSecretFallback: ""
|
||||||
|
|
||||||
smtp:
|
smtp:
|
||||||
enabled: false
|
enabled: false
|
||||||
|
|
|
@ -3,6 +3,7 @@ MAIN_PORT=10000
|
||||||
|
|
||||||
# This section contains all secrets pertaining to the system
|
# This section contains all secrets pertaining to the system
|
||||||
# These should be updated
|
# These should be updated
|
||||||
|
API_ENCRYPTION_KEY=testsecret
|
||||||
JWT_SECRET=testsecret
|
JWT_SECRET=testsecret
|
||||||
MINIO_ACCESS_KEY=budibase
|
MINIO_ACCESS_KEY=budibase
|
||||||
MINIO_SECRET_KEY=budibase
|
MINIO_SECRET_KEY=budibase
|
||||||
|
|
|
@ -17,6 +17,7 @@ services:
|
||||||
INTERNAL_API_KEY: ${INTERNAL_API_KEY}
|
INTERNAL_API_KEY: ${INTERNAL_API_KEY}
|
||||||
BUDIBASE_ENVIRONMENT: ${BUDIBASE_ENVIRONMENT}
|
BUDIBASE_ENVIRONMENT: ${BUDIBASE_ENVIRONMENT}
|
||||||
PORT: 4002
|
PORT: 4002
|
||||||
|
API_ENCRYPTION_KEY: ${API_ENCRYPTION_KEY}
|
||||||
JWT_SECRET: ${JWT_SECRET}
|
JWT_SECRET: ${JWT_SECRET}
|
||||||
LOG_LEVEL: info
|
LOG_LEVEL: info
|
||||||
SENTRY_DSN: https://a34ae347621946bf8acded18e5b7d4b8@o420233.ingest.sentry.io/5338131
|
SENTRY_DSN: https://a34ae347621946bf8acded18e5b7d4b8@o420233.ingest.sentry.io/5338131
|
||||||
|
@ -40,6 +41,7 @@ services:
|
||||||
SELF_HOSTED: 1
|
SELF_HOSTED: 1
|
||||||
PORT: 4003
|
PORT: 4003
|
||||||
CLUSTER_PORT: ${MAIN_PORT}
|
CLUSTER_PORT: ${MAIN_PORT}
|
||||||
|
API_ENCRYPTION_KEY: ${API_ENCRYPTION_KEY}
|
||||||
JWT_SECRET: ${JWT_SECRET}
|
JWT_SECRET: ${JWT_SECRET}
|
||||||
MINIO_ACCESS_KEY: ${MINIO_ACCESS_KEY}
|
MINIO_ACCESS_KEY: ${MINIO_ACCESS_KEY}
|
||||||
MINIO_SECRET_KEY: ${MINIO_SECRET_KEY}
|
MINIO_SECRET_KEY: ${MINIO_SECRET_KEY}
|
||||||
|
|
|
@ -3,6 +3,7 @@ MAIN_PORT=10000
|
||||||
|
|
||||||
# This section contains all secrets pertaining to the system
|
# This section contains all secrets pertaining to the system
|
||||||
# These should be updated
|
# These should be updated
|
||||||
|
API_ENCRYPTION_KEY=testsecret
|
||||||
JWT_SECRET=testsecret
|
JWT_SECRET=testsecret
|
||||||
MINIO_ACCESS_KEY=budibase
|
MINIO_ACCESS_KEY=budibase
|
||||||
MINIO_SECRET_KEY=budibase
|
MINIO_SECRET_KEY=budibase
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
{
|
{
|
||||||
"version": "2.4.12-alpha.3",
|
"version": "2.4.27-alpha.9",
|
||||||
"npmClient": "yarn",
|
"npmClient": "yarn",
|
||||||
"packages": [
|
"packages": [
|
||||||
"packages/*"
|
"packages/*"
|
||||||
|
|
|
@ -25,6 +25,7 @@
|
||||||
"setup": "node ./hosting/scripts/setup.js && yarn && yarn bootstrap && yarn build && yarn dev",
|
"setup": "node ./hosting/scripts/setup.js && yarn && yarn bootstrap && yarn build && yarn dev",
|
||||||
"bootstrap": "lerna bootstrap && lerna link && ./scripts/link-dependencies.sh",
|
"bootstrap": "lerna bootstrap && lerna link && ./scripts/link-dependencies.sh",
|
||||||
"build": "lerna run build",
|
"build": "lerna run build",
|
||||||
|
"build:client": "lerna run build --ignore @budibase/backend-core --ignore @budibase/worker --ignore @budibase/server --ignore @budibase/builder --ignore @budibase/cli --ignore @budibase/sdk",
|
||||||
"build:dev": "lerna run prebuild && tsc --build --watch --preserveWatchOutput",
|
"build:dev": "lerna run prebuild && tsc --build --watch --preserveWatchOutput",
|
||||||
"build:backend": "lerna run build --ignore @budibase/client --ignore @budibase/bbui --ignore @budibase/builder --ignore @budibase/cli",
|
"build:backend": "lerna run build --ignore @budibase/client --ignore @budibase/bbui --ignore @budibase/builder --ignore @budibase/cli",
|
||||||
"build:sdk": "lerna run build:sdk",
|
"build:sdk": "lerna run build:sdk",
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@budibase/backend-core",
|
"name": "@budibase/backend-core",
|
||||||
"version": "2.4.12-alpha.3",
|
"version": "2.4.27-alpha.9",
|
||||||
"description": "Budibase backend core libraries used in server and worker",
|
"description": "Budibase backend core libraries used in server and worker",
|
||||||
"main": "dist/src/index.js",
|
"main": "dist/src/index.js",
|
||||||
"types": "dist/src/index.d.ts",
|
"types": "dist/src/index.d.ts",
|
||||||
|
@ -24,7 +24,7 @@
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@budibase/nano": "10.1.2",
|
"@budibase/nano": "10.1.2",
|
||||||
"@budibase/pouchdb-replication-stream": "1.2.10",
|
"@budibase/pouchdb-replication-stream": "1.2.10",
|
||||||
"@budibase/types": "2.4.12-alpha.3",
|
"@budibase/types": "2.4.27-alpha.9",
|
||||||
"@shopify/jest-koa-mocks": "5.0.1",
|
"@shopify/jest-koa-mocks": "5.0.1",
|
||||||
"@techpass/passport-openidconnect": "0.3.2",
|
"@techpass/passport-openidconnect": "0.3.2",
|
||||||
"aws-cloudfront-sign": "2.2.0",
|
"aws-cloudfront-sign": "2.2.0",
|
||||||
|
|
|
@ -3,8 +3,8 @@
|
||||||
if [[ -n $CI ]]
|
if [[ -n $CI ]]
|
||||||
then
|
then
|
||||||
# --runInBand performs better in ci where resources are limited
|
# --runInBand performs better in ci where resources are limited
|
||||||
echo "jest --coverage --runInBand"
|
echo "jest --coverage --runInBand --forceExit"
|
||||||
jest --coverage --runInBand
|
jest --coverage --runInBand --forceExit
|
||||||
else
|
else
|
||||||
# --maxWorkers performs better in development
|
# --maxWorkers performs better in development
|
||||||
echo "jest --coverage"
|
echo "jest --coverage"
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
const _passport = require("koa-passport")
|
const _passport = require("koa-passport")
|
||||||
const LocalStrategy = require("passport-local").Strategy
|
const LocalStrategy = require("passport-local").Strategy
|
||||||
const JwtStrategy = require("passport-jwt").Strategy
|
|
||||||
import { getGlobalDB } from "../context"
|
import { getGlobalDB } from "../context"
|
||||||
import { Cookie } from "../constants"
|
import { Cookie } from "../constants"
|
||||||
import { getSessionsForUser, invalidateSessions } from "../security/sessions"
|
import { getSessionsForUser, invalidateSessions } from "../security/sessions"
|
||||||
|
@ -8,7 +7,6 @@ import {
|
||||||
authenticated,
|
authenticated,
|
||||||
csrf,
|
csrf,
|
||||||
google,
|
google,
|
||||||
jwt as jwtPassport,
|
|
||||||
local,
|
local,
|
||||||
oidc,
|
oidc,
|
||||||
tenancy,
|
tenancy,
|
||||||
|
@ -21,14 +19,11 @@ import {
|
||||||
OIDCInnerConfig,
|
OIDCInnerConfig,
|
||||||
PlatformLogoutOpts,
|
PlatformLogoutOpts,
|
||||||
SSOProviderType,
|
SSOProviderType,
|
||||||
User,
|
|
||||||
} from "@budibase/types"
|
} from "@budibase/types"
|
||||||
import { logAlert } from "../logging"
|
|
||||||
import * as events from "../events"
|
import * as events from "../events"
|
||||||
import * as configs from "../configs"
|
import * as configs from "../configs"
|
||||||
import { clearCookie, getCookie } from "../utils"
|
import { clearCookie, getCookie } from "../utils"
|
||||||
import { ssoSaveUserNoOp } from "../middleware/passport/sso/sso"
|
import { ssoSaveUserNoOp } from "../middleware/passport/sso/sso"
|
||||||
import env from "../environment"
|
|
||||||
|
|
||||||
const refresh = require("passport-oauth2-refresh")
|
const refresh = require("passport-oauth2-refresh")
|
||||||
export {
|
export {
|
||||||
|
@ -51,25 +46,6 @@ export const jwt = require("jsonwebtoken")
|
||||||
|
|
||||||
// Strategies
|
// Strategies
|
||||||
_passport.use(new LocalStrategy(local.options, local.authenticate))
|
_passport.use(new LocalStrategy(local.options, local.authenticate))
|
||||||
if (jwtPassport.options.secretOrKey) {
|
|
||||||
_passport.use(new JwtStrategy(jwtPassport.options, jwtPassport.authenticate))
|
|
||||||
} else if (!env.DISABLE_JWT_WARNING) {
|
|
||||||
logAlert("No JWT Secret supplied, cannot configure JWT strategy")
|
|
||||||
}
|
|
||||||
|
|
||||||
_passport.serializeUser((user: User, done: any) => done(null, user))
|
|
||||||
|
|
||||||
_passport.deserializeUser(async (user: User, done: any) => {
|
|
||||||
const db = getGlobalDB()
|
|
||||||
|
|
||||||
try {
|
|
||||||
const dbUser = await db.get(user._id)
|
|
||||||
return done(null, dbUser)
|
|
||||||
} catch (err) {
|
|
||||||
console.error(`User not found`, err)
|
|
||||||
return done(null, false, { message: "User not found" })
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
async function refreshOIDCAccessToken(
|
async function refreshOIDCAccessToken(
|
||||||
chosenConfig: OIDCInnerConfig,
|
chosenConfig: OIDCInnerConfig,
|
||||||
|
|
|
@ -199,6 +199,10 @@ export class QueryBuilder<T> {
|
||||||
return this
|
return this
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setAllOr() {
|
||||||
|
this.query.allOr = true
|
||||||
|
}
|
||||||
|
|
||||||
handleSpaces(input: string) {
|
handleSpaces(input: string) {
|
||||||
if (this.noEscaping) {
|
if (this.noEscaping) {
|
||||||
return input
|
return input
|
||||||
|
@ -236,6 +240,36 @@ export class QueryBuilder<T> {
|
||||||
return value
|
return value
|
||||||
}
|
}
|
||||||
|
|
||||||
|
isMultiCondition() {
|
||||||
|
let count = 0
|
||||||
|
for (let filters of Object.values(this.query)) {
|
||||||
|
// not contains is one massive filter in allOr mode
|
||||||
|
if (typeof filters === "object") {
|
||||||
|
count += Object.keys(filters).length
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return count > 1
|
||||||
|
}
|
||||||
|
|
||||||
|
compressFilters(filters: Record<string, string[]>) {
|
||||||
|
const compressed: typeof filters = {}
|
||||||
|
for (let key of Object.keys(filters)) {
|
||||||
|
const finalKey = removeKeyNumbering(key)
|
||||||
|
if (compressed[finalKey]) {
|
||||||
|
compressed[finalKey] = compressed[finalKey].concat(filters[key])
|
||||||
|
} else {
|
||||||
|
compressed[finalKey] = filters[key]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// add prefixes back
|
||||||
|
const final: typeof filters = {}
|
||||||
|
let count = 1
|
||||||
|
for (let [key, value] of Object.entries(compressed)) {
|
||||||
|
final[`${count++}:${key}`] = value
|
||||||
|
}
|
||||||
|
return final
|
||||||
|
}
|
||||||
|
|
||||||
buildSearchQuery() {
|
buildSearchQuery() {
|
||||||
const builder = this
|
const builder = this
|
||||||
let allOr = this.query && this.query.allOr
|
let allOr = this.query && this.query.allOr
|
||||||
|
@ -272,9 +306,9 @@ export class QueryBuilder<T> {
|
||||||
}
|
}
|
||||||
|
|
||||||
const notContains = (key: string, value: any) => {
|
const notContains = (key: string, value: any) => {
|
||||||
// @ts-ignore
|
const allPrefix = allOr ? "*:* AND " : ""
|
||||||
const allPrefix = allOr === "" ? "*:* AND" : ""
|
const mode = allOr ? "AND" : undefined
|
||||||
return allPrefix + "NOT " + contains(key, value)
|
return allPrefix + "NOT " + contains(key, value, mode)
|
||||||
}
|
}
|
||||||
|
|
||||||
const containsAny = (key: string, value: any) => {
|
const containsAny = (key: string, value: any) => {
|
||||||
|
@ -299,21 +333,32 @@ export class QueryBuilder<T> {
|
||||||
return `${key}:(${orStatement})`
|
return `${key}:(${orStatement})`
|
||||||
}
|
}
|
||||||
|
|
||||||
function build(structure: any, queryFn: any) {
|
function build(
|
||||||
|
structure: any,
|
||||||
|
queryFn: (key: string, value: any) => string | null,
|
||||||
|
opts?: { returnBuilt?: boolean; mode?: string }
|
||||||
|
) {
|
||||||
|
let built = ""
|
||||||
for (let [key, value] of Object.entries(structure)) {
|
for (let [key, value] of Object.entries(structure)) {
|
||||||
// check for new format - remove numbering if needed
|
// check for new format - remove numbering if needed
|
||||||
key = removeKeyNumbering(key)
|
key = removeKeyNumbering(key)
|
||||||
key = builder.preprocess(builder.handleSpaces(key), {
|
key = builder.preprocess(builder.handleSpaces(key), {
|
||||||
escape: true,
|
escape: true,
|
||||||
})
|
})
|
||||||
const expression = queryFn(key, value)
|
let expression = queryFn(key, value)
|
||||||
if (expression == null) {
|
if (expression == null) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if (query.length > 0) {
|
if (built.length > 0 || query.length > 0) {
|
||||||
query += ` ${allOr ? "OR" : "AND"} `
|
const mode = opts?.mode ? opts.mode : allOr ? "OR" : "AND"
|
||||||
|
built += ` ${mode} `
|
||||||
}
|
}
|
||||||
query += expression
|
built += expression
|
||||||
|
}
|
||||||
|
if (opts?.returnBuilt) {
|
||||||
|
return built
|
||||||
|
} else {
|
||||||
|
query += built
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -384,14 +429,14 @@ export class QueryBuilder<T> {
|
||||||
build(this.query.contains, contains)
|
build(this.query.contains, contains)
|
||||||
}
|
}
|
||||||
if (this.query.notContains) {
|
if (this.query.notContains) {
|
||||||
build(this.query.notContains, notContains)
|
build(this.compressFilters(this.query.notContains), notContains)
|
||||||
}
|
}
|
||||||
if (this.query.containsAny) {
|
if (this.query.containsAny) {
|
||||||
build(this.query.containsAny, containsAny)
|
build(this.query.containsAny, containsAny)
|
||||||
}
|
}
|
||||||
// make sure table ID is always added as an AND
|
// make sure table ID is always added as an AND
|
||||||
if (tableId) {
|
if (tableId) {
|
||||||
query = `(${query})`
|
query = this.isMultiCondition() ? `(${query})` : query
|
||||||
allOr = false
|
allOr = false
|
||||||
build({ tableId }, equal)
|
build({ tableId }, equal)
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,9 +6,13 @@ import { QueryBuilder, paginatedSearch, fullSearch } from "../lucene"
|
||||||
const INDEX_NAME = "main"
|
const INDEX_NAME = "main"
|
||||||
|
|
||||||
const index = `function(doc) {
|
const index = `function(doc) {
|
||||||
let props = ["property", "number"]
|
let props = ["property", "number", "array"]
|
||||||
for (let key of props) {
|
for (let key of props) {
|
||||||
if (doc[key]) {
|
if (Array.isArray(doc[key])) {
|
||||||
|
for (let val of doc[key]) {
|
||||||
|
index(key, val)
|
||||||
|
}
|
||||||
|
} else if (doc[key]) {
|
||||||
index(key, doc[key])
|
index(key, doc[key])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -21,9 +25,14 @@ describe("lucene", () => {
|
||||||
dbName = `db-${newid()}`
|
dbName = `db-${newid()}`
|
||||||
// create the DB for testing
|
// create the DB for testing
|
||||||
db = getDB(dbName)
|
db = getDB(dbName)
|
||||||
await db.put({ _id: newid(), property: "word" })
|
await db.put({ _id: newid(), property: "word", array: ["1", "4"] })
|
||||||
await db.put({ _id: newid(), property: "word2" })
|
await db.put({ _id: newid(), property: "word2", array: ["3", "1"] })
|
||||||
await db.put({ _id: newid(), property: "word3", number: 1 })
|
await db.put({
|
||||||
|
_id: newid(),
|
||||||
|
property: "word3",
|
||||||
|
number: 1,
|
||||||
|
array: ["1", "2"],
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
it("should be able to create a lucene index", async () => {
|
it("should be able to create a lucene index", async () => {
|
||||||
|
@ -118,6 +127,15 @@ describe("lucene", () => {
|
||||||
const resp = await builder.run()
|
const resp = await builder.run()
|
||||||
expect(resp.rows.length).toBe(2)
|
expect(resp.rows.length).toBe(2)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it("should be able to perform an or not contains search", async () => {
|
||||||
|
const builder = new QueryBuilder(dbName, INDEX_NAME)
|
||||||
|
builder.addNotContains("array", ["1"])
|
||||||
|
builder.addNotContains("array", ["2"])
|
||||||
|
builder.setAllOr()
|
||||||
|
const resp = await builder.run()
|
||||||
|
expect(resp.rows.length).toBe(2)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe("paginated search", () => {
|
describe("paginated search", () => {
|
||||||
|
|
|
@ -30,6 +30,12 @@ const DefaultBucketName = {
|
||||||
|
|
||||||
const selfHosted = !!parseInt(process.env.SELF_HOSTED || "")
|
const selfHosted = !!parseInt(process.env.SELF_HOSTED || "")
|
||||||
|
|
||||||
|
function getAPIEncryptionKey() {
|
||||||
|
return process.env.API_ENCRYPTION_KEY
|
||||||
|
? process.env.API_ENCRYPTION_KEY
|
||||||
|
: process.env.JWT_SECRET // fallback to the JWT_SECRET used historically
|
||||||
|
}
|
||||||
|
|
||||||
const environment = {
|
const environment = {
|
||||||
isTest,
|
isTest,
|
||||||
isJest,
|
isJest,
|
||||||
|
@ -39,7 +45,9 @@ const environment = {
|
||||||
},
|
},
|
||||||
JS_BCRYPT: process.env.JS_BCRYPT,
|
JS_BCRYPT: process.env.JS_BCRYPT,
|
||||||
JWT_SECRET: process.env.JWT_SECRET,
|
JWT_SECRET: process.env.JWT_SECRET,
|
||||||
|
JWT_SECRET_FALLBACK: process.env.JWT_SECRET_FALLBACK,
|
||||||
ENCRYPTION_KEY: process.env.ENCRYPTION_KEY,
|
ENCRYPTION_KEY: process.env.ENCRYPTION_KEY,
|
||||||
|
API_ENCRYPTION_KEY: getAPIEncryptionKey(),
|
||||||
COUCH_DB_URL: process.env.COUCH_DB_URL || "http://localhost:4005",
|
COUCH_DB_URL: process.env.COUCH_DB_URL || "http://localhost:4005",
|
||||||
COUCH_DB_USERNAME: process.env.COUCH_DB_USER,
|
COUCH_DB_USERNAME: process.env.COUCH_DB_USER,
|
||||||
COUCH_DB_PASSWORD: process.env.COUCH_DB_PASSWORD,
|
COUCH_DB_PASSWORD: process.env.COUCH_DB_PASSWORD,
|
||||||
|
@ -55,6 +63,7 @@ const environment = {
|
||||||
MINIO_URL: process.env.MINIO_URL,
|
MINIO_URL: process.env.MINIO_URL,
|
||||||
MINIO_ENABLED: process.env.MINIO_ENABLED || 1,
|
MINIO_ENABLED: process.env.MINIO_ENABLED || 1,
|
||||||
INTERNAL_API_KEY: process.env.INTERNAL_API_KEY,
|
INTERNAL_API_KEY: process.env.INTERNAL_API_KEY,
|
||||||
|
INTERNAL_API_KEY_FALLBACK: process.env.INTERNAL_API_KEY_FALLBACK,
|
||||||
MULTI_TENANCY: process.env.MULTI_TENANCY,
|
MULTI_TENANCY: process.env.MULTI_TENANCY,
|
||||||
ACCOUNT_PORTAL_URL:
|
ACCOUNT_PORTAL_URL:
|
||||||
process.env.ACCOUNT_PORTAL_URL || "https://account.budibase.app",
|
process.env.ACCOUNT_PORTAL_URL || "https://account.budibase.app",
|
||||||
|
|
|
@ -1,10 +0,0 @@
|
||||||
export class BudibaseError extends Error {
|
|
||||||
code: string
|
|
||||||
type: string
|
|
||||||
|
|
||||||
constructor(message: string, code: string, type: string) {
|
|
||||||
super(message)
|
|
||||||
this.code = code
|
|
||||||
this.type = type
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,37 +1,99 @@
|
||||||
import * as licensing from "./licensing"
|
// BASE
|
||||||
|
|
||||||
// combine all error codes into single object
|
export abstract class BudibaseError extends Error {
|
||||||
|
code: string
|
||||||
|
|
||||||
export const codes = {
|
constructor(message: string, code: ErrorCode) {
|
||||||
...licensing.codes,
|
super(message)
|
||||||
|
this.code = code
|
||||||
|
}
|
||||||
|
|
||||||
|
protected getPublicError?(): any
|
||||||
}
|
}
|
||||||
|
|
||||||
// combine all error types
|
// ERROR HANDLING
|
||||||
export const types = [licensing.type]
|
|
||||||
|
|
||||||
// combine all error contexts
|
export enum ErrorCode {
|
||||||
const context = {
|
USAGE_LIMIT_EXCEEDED = "usage_limit_exceeded",
|
||||||
...licensing.context,
|
FEATURE_DISABLED = "feature_disabled",
|
||||||
|
INVALID_API_KEY = "invalid_api_key",
|
||||||
|
HTTP = "http",
|
||||||
}
|
}
|
||||||
|
|
||||||
// derive a public error message using codes, types and any custom contexts
|
/**
|
||||||
|
* For the given error, build the public representation that is safe
|
||||||
|
* to be exposed over an api.
|
||||||
|
*/
|
||||||
export const getPublicError = (err: any) => {
|
export const getPublicError = (err: any) => {
|
||||||
let error
|
let error
|
||||||
if (err.code || err.type) {
|
if (err.code) {
|
||||||
// add generic error information
|
// add generic error information
|
||||||
error = {
|
error = {
|
||||||
code: err.code,
|
code: err.code,
|
||||||
type: err.type,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (err.code && context[err.code]) {
|
if (err.getPublicError) {
|
||||||
error = {
|
error = {
|
||||||
...error,
|
...error,
|
||||||
// get any additional context from this error
|
// get any additional context from this error
|
||||||
...context[err.code](err),
|
...err.getPublicError(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return error
|
return error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// HTTP
|
||||||
|
|
||||||
|
export class HTTPError extends BudibaseError {
|
||||||
|
status: number
|
||||||
|
|
||||||
|
constructor(message: string, httpStatus: number, code = ErrorCode.HTTP) {
|
||||||
|
super(message, code)
|
||||||
|
this.status = httpStatus
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// LICENSING
|
||||||
|
|
||||||
|
export class UsageLimitError extends HTTPError {
|
||||||
|
limitName: string
|
||||||
|
|
||||||
|
constructor(message: string, limitName: string) {
|
||||||
|
super(message, 400, ErrorCode.USAGE_LIMIT_EXCEEDED)
|
||||||
|
this.limitName = limitName
|
||||||
|
}
|
||||||
|
|
||||||
|
getPublicError() {
|
||||||
|
return {
|
||||||
|
limitName: this.limitName,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class FeatureDisabledError extends HTTPError {
|
||||||
|
featureName: string
|
||||||
|
|
||||||
|
constructor(message: string, featureName: string) {
|
||||||
|
super(message, 400, ErrorCode.FEATURE_DISABLED)
|
||||||
|
this.featureName = featureName
|
||||||
|
}
|
||||||
|
|
||||||
|
getPublicError() {
|
||||||
|
return {
|
||||||
|
featureName: this.featureName,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// AUTH
|
||||||
|
|
||||||
|
export class InvalidAPIKeyError extends BudibaseError {
|
||||||
|
constructor() {
|
||||||
|
super(
|
||||||
|
"Invalid API key - may need re-generated, or user doesn't exist",
|
||||||
|
ErrorCode.INVALID_API_KEY
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -1,7 +0,0 @@
|
||||||
import { BudibaseError } from "./base"
|
|
||||||
|
|
||||||
export class GenericError extends BudibaseError {
|
|
||||||
constructor(message: string, code: string, type: string) {
|
|
||||||
super(message, code, type ? type : "generic")
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,15 +0,0 @@
|
||||||
import { GenericError } from "./generic"
|
|
||||||
|
|
||||||
export class HTTPError extends GenericError {
|
|
||||||
status: number
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
message: string,
|
|
||||||
httpStatus: number,
|
|
||||||
code = "http",
|
|
||||||
type = "generic"
|
|
||||||
) {
|
|
||||||
super(message, code, type)
|
|
||||||
this.status = httpStatus
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,3 +1 @@
|
||||||
export * from "./errors"
|
export * from "./errors"
|
||||||
export { UsageLimitError, FeatureDisabledError } from "./licensing"
|
|
||||||
export { HTTPError } from "./http"
|
|
||||||
|
|
|
@ -1,39 +0,0 @@
|
||||||
import { HTTPError } from "./http"
|
|
||||||
|
|
||||||
export const type = "license_error"
|
|
||||||
|
|
||||||
export const codes = {
|
|
||||||
USAGE_LIMIT_EXCEEDED: "usage_limit_exceeded",
|
|
||||||
FEATURE_DISABLED: "feature_disabled",
|
|
||||||
}
|
|
||||||
|
|
||||||
export const context = {
|
|
||||||
[codes.USAGE_LIMIT_EXCEEDED]: (err: any) => {
|
|
||||||
return {
|
|
||||||
limitName: err.limitName,
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[codes.FEATURE_DISABLED]: (err: any) => {
|
|
||||||
return {
|
|
||||||
featureName: err.featureName,
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
export class UsageLimitError extends HTTPError {
|
|
||||||
limitName: string
|
|
||||||
|
|
||||||
constructor(message: string, limitName: string) {
|
|
||||||
super(message, 400, codes.USAGE_LIMIT_EXCEEDED, type)
|
|
||||||
this.limitName = limitName
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export class FeatureDisabledError extends HTTPError {
|
|
||||||
featureName: string
|
|
||||||
|
|
||||||
constructor(message: string, featureName: string) {
|
|
||||||
super(message, 400, codes.FEATURE_DISABLED, type)
|
|
||||||
this.featureName = featureName
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -24,6 +24,7 @@ export * as redis from "./redis"
|
||||||
export * as locks from "./redis/redlockImpl"
|
export * as locks from "./redis/redlockImpl"
|
||||||
export * as utils from "./utils"
|
export * as utils from "./utils"
|
||||||
export * as errors from "./errors"
|
export * as errors from "./errors"
|
||||||
|
export * as timers from "./timers"
|
||||||
export { default as env } from "./environment"
|
export { default as env } from "./environment"
|
||||||
export { SearchParams } from "./db"
|
export { SearchParams } from "./db"
|
||||||
// Add context to tenancy for backwards compatibility
|
// Add context to tenancy for backwards compatibility
|
||||||
|
|
|
@ -1,5 +1,10 @@
|
||||||
import { Cookie, Header } from "../constants"
|
import { Cookie, Header } from "../constants"
|
||||||
import { getCookie, clearCookie, openJwt } from "../utils"
|
import {
|
||||||
|
getCookie,
|
||||||
|
clearCookie,
|
||||||
|
openJwt,
|
||||||
|
isValidInternalAPIKey,
|
||||||
|
} from "../utils"
|
||||||
import { getUser } from "../cache/user"
|
import { getUser } from "../cache/user"
|
||||||
import { getSession, updateSessionTTL } from "../security/sessions"
|
import { getSession, updateSessionTTL } from "../security/sessions"
|
||||||
import { buildMatcherRegex, matches } from "./matchers"
|
import { buildMatcherRegex, matches } from "./matchers"
|
||||||
|
@ -9,6 +14,7 @@ import { decrypt } from "../security/encryption"
|
||||||
import * as identity from "../context/identity"
|
import * as identity from "../context/identity"
|
||||||
import env from "../environment"
|
import env from "../environment"
|
||||||
import { Ctx, EndpointMatcher } from "@budibase/types"
|
import { Ctx, EndpointMatcher } from "@budibase/types"
|
||||||
|
import { InvalidAPIKeyError, ErrorCode } from "../errors"
|
||||||
|
|
||||||
const ONE_MINUTE = env.SESSION_UPDATE_PERIOD
|
const ONE_MINUTE = env.SESSION_UPDATE_PERIOD
|
||||||
? parseInt(env.SESSION_UPDATE_PERIOD)
|
? parseInt(env.SESSION_UPDATE_PERIOD)
|
||||||
|
@ -35,28 +41,35 @@ function finalise(ctx: any, opts: FinaliseOpts = {}) {
|
||||||
}
|
}
|
||||||
|
|
||||||
async function checkApiKey(apiKey: string, populateUser?: Function) {
|
async function checkApiKey(apiKey: string, populateUser?: Function) {
|
||||||
if (apiKey === env.INTERNAL_API_KEY) {
|
// check both the primary and the fallback internal api keys
|
||||||
|
// this allows for rotation
|
||||||
|
if (isValidInternalAPIKey(apiKey)) {
|
||||||
return { valid: true }
|
return { valid: true }
|
||||||
}
|
}
|
||||||
const decrypted = decrypt(apiKey)
|
const decrypted = decrypt(apiKey)
|
||||||
const tenantId = decrypted.split(SEPARATOR)[0]
|
const tenantId = decrypted.split(SEPARATOR)[0]
|
||||||
return doInTenant(tenantId, async () => {
|
return doInTenant(tenantId, async () => {
|
||||||
const db = getGlobalDB()
|
let userId
|
||||||
// api key is encrypted in the database
|
try {
|
||||||
const userId = (await queryGlobalView(
|
const db = getGlobalDB()
|
||||||
ViewName.BY_API_KEY,
|
// api key is encrypted in the database
|
||||||
{
|
userId = (await queryGlobalView(
|
||||||
key: apiKey,
|
ViewName.BY_API_KEY,
|
||||||
},
|
{
|
||||||
db
|
key: apiKey,
|
||||||
)) as string
|
},
|
||||||
|
db
|
||||||
|
)) as string
|
||||||
|
} catch (err) {
|
||||||
|
userId = undefined
|
||||||
|
}
|
||||||
if (userId) {
|
if (userId) {
|
||||||
return {
|
return {
|
||||||
valid: true,
|
valid: true,
|
||||||
user: await getUser(userId, tenantId, populateUser),
|
user: await getUser(userId, tenantId, populateUser),
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
throw "Invalid API key"
|
throw new InvalidAPIKeyError()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -157,8 +170,10 @@ export default function (
|
||||||
console.error(`Auth Error: ${err.message}`)
|
console.error(`Auth Error: ${err.message}`)
|
||||||
console.error(err)
|
console.error(err)
|
||||||
// invalid token, clear the cookie
|
// invalid token, clear the cookie
|
||||||
if (err && err.name === "JsonWebTokenError") {
|
if (err?.name === "JsonWebTokenError") {
|
||||||
clearCookie(ctx, Cookie.Auth)
|
clearCookie(ctx, Cookie.Auth)
|
||||||
|
} else if (err?.code === ErrorCode.INVALID_API_KEY) {
|
||||||
|
ctx.throw(403, err.message)
|
||||||
}
|
}
|
||||||
// allow configuring for public access
|
// allow configuring for public access
|
||||||
if ((opts && opts.publicAllowed) || publicEndpoint) {
|
if ((opts && opts.publicAllowed) || publicEndpoint) {
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
export * as jwt from "./passport/jwt"
|
|
||||||
export * as local from "./passport/local"
|
export * as local from "./passport/local"
|
||||||
export * as google from "./passport/sso/google"
|
export * as google from "./passport/sso/google"
|
||||||
export * as oidc from "./passport/sso/oidc"
|
export * as oidc from "./passport/sso/oidc"
|
||||||
|
|
|
@ -1,13 +1,21 @@
|
||||||
import env from "../environment"
|
|
||||||
import { Header } from "../constants"
|
import { Header } from "../constants"
|
||||||
import { BBContext } from "@budibase/types"
|
import { BBContext } from "@budibase/types"
|
||||||
|
import { isValidInternalAPIKey } from "../utils"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* API Key only endpoint.
|
* API Key only endpoint.
|
||||||
*/
|
*/
|
||||||
export default async (ctx: BBContext, next: any) => {
|
export default async (ctx: BBContext, next: any) => {
|
||||||
const apiKey = ctx.request.headers[Header.API_KEY]
|
const apiKey = ctx.request.headers[Header.API_KEY]
|
||||||
if (apiKey !== env.INTERNAL_API_KEY) {
|
if (!apiKey) {
|
||||||
|
ctx.throw(403, "Unauthorized")
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Array.isArray(apiKey)) {
|
||||||
|
ctx.throw(403, "Unauthorized")
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isValidInternalAPIKey(apiKey)) {
|
||||||
ctx.throw(403, "Unauthorized")
|
ctx.throw(403, "Unauthorized")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,19 +0,0 @@
|
||||||
import { Cookie } from "../../constants"
|
|
||||||
import env from "../../environment"
|
|
||||||
import { authError } from "./utils"
|
|
||||||
import { BBContext } from "@budibase/types"
|
|
||||||
|
|
||||||
export const options = {
|
|
||||||
secretOrKey: env.JWT_SECRET,
|
|
||||||
jwtFromRequest: function (ctx: BBContext) {
|
|
||||||
return ctx.cookies.get(Cookie.Auth)
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function authenticate(jwt: Function, done: Function) {
|
|
||||||
try {
|
|
||||||
return done(null, jwt)
|
|
||||||
} catch (err) {
|
|
||||||
return authError(done, "JWT invalid", err)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -4,6 +4,7 @@ import { JobQueue } from "./constants"
|
||||||
import InMemoryQueue from "./inMemoryQueue"
|
import InMemoryQueue from "./inMemoryQueue"
|
||||||
import BullQueue from "bull"
|
import BullQueue from "bull"
|
||||||
import { addListeners, StalledFn } from "./listeners"
|
import { addListeners, StalledFn } from "./listeners"
|
||||||
|
import * as timers from "../timers"
|
||||||
|
|
||||||
const CLEANUP_PERIOD_MS = 60 * 1000
|
const CLEANUP_PERIOD_MS = 60 * 1000
|
||||||
let QUEUES: BullQueue.Queue[] | InMemoryQueue[] = []
|
let QUEUES: BullQueue.Queue[] | InMemoryQueue[] = []
|
||||||
|
@ -29,8 +30,8 @@ export function createQueue<T>(
|
||||||
}
|
}
|
||||||
addListeners(queue, jobQueue, opts?.removeStalledCb)
|
addListeners(queue, jobQueue, opts?.removeStalledCb)
|
||||||
QUEUES.push(queue)
|
QUEUES.push(queue)
|
||||||
if (!cleanupInterval) {
|
if (!cleanupInterval && !env.isTest()) {
|
||||||
cleanupInterval = setInterval(cleanup, CLEANUP_PERIOD_MS)
|
cleanupInterval = timers.set(cleanup, CLEANUP_PERIOD_MS)
|
||||||
// fire off an initial cleanup
|
// fire off an initial cleanup
|
||||||
cleanup().catch(err => {
|
cleanup().catch(err => {
|
||||||
console.error(`Unable to cleanup automation queue initially - ${err}`)
|
console.error(`Unable to cleanup automation queue initially - ${err}`)
|
||||||
|
@ -41,7 +42,7 @@ export function createQueue<T>(
|
||||||
|
|
||||||
export async function shutdown() {
|
export async function shutdown() {
|
||||||
if (cleanupInterval) {
|
if (cleanupInterval) {
|
||||||
clearInterval(cleanupInterval)
|
timers.clear(cleanupInterval)
|
||||||
}
|
}
|
||||||
if (QUEUES.length) {
|
if (QUEUES.length) {
|
||||||
for (let queue of QUEUES) {
|
for (let queue of QUEUES) {
|
||||||
|
|
|
@ -8,6 +8,7 @@ import {
|
||||||
SEPARATOR,
|
SEPARATOR,
|
||||||
SelectableDatabase,
|
SelectableDatabase,
|
||||||
} from "./utils"
|
} from "./utils"
|
||||||
|
import * as timers from "../timers"
|
||||||
|
|
||||||
const RETRY_PERIOD_MS = 2000
|
const RETRY_PERIOD_MS = 2000
|
||||||
const STARTUP_TIMEOUT_MS = 5000
|
const STARTUP_TIMEOUT_MS = 5000
|
||||||
|
@ -117,9 +118,9 @@ function waitForConnection(selectDb: number = DEFAULT_SELECT_DB) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// check if the connection is ready
|
// check if the connection is ready
|
||||||
const interval = setInterval(() => {
|
const interval = timers.set(() => {
|
||||||
if (CONNECTED) {
|
if (CONNECTED) {
|
||||||
clearInterval(interval)
|
timers.clear(interval)
|
||||||
resolve("")
|
resolve("")
|
||||||
}
|
}
|
||||||
}, 500)
|
}, 500)
|
||||||
|
|
|
@ -8,7 +8,7 @@ const RANDOM_BYTES = 16
|
||||||
const STRETCH_LENGTH = 32
|
const STRETCH_LENGTH = 32
|
||||||
|
|
||||||
export enum SecretOption {
|
export enum SecretOption {
|
||||||
JWT = "jwt",
|
API = "api",
|
||||||
ENCRYPTION = "encryption",
|
ENCRYPTION = "encryption",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -19,10 +19,10 @@ function getSecret(secretOption: SecretOption): string {
|
||||||
secret = env.ENCRYPTION_KEY
|
secret = env.ENCRYPTION_KEY
|
||||||
secretName = "ENCRYPTION_KEY"
|
secretName = "ENCRYPTION_KEY"
|
||||||
break
|
break
|
||||||
case SecretOption.JWT:
|
case SecretOption.API:
|
||||||
default:
|
default:
|
||||||
secret = env.JWT_SECRET
|
secret = env.API_ENCRYPTION_KEY
|
||||||
secretName = "JWT_SECRET"
|
secretName = "API_ENCRYPTION_KEY"
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
if (!secret) {
|
if (!secret) {
|
||||||
|
@ -37,7 +37,7 @@ function stretchString(string: string, salt: Buffer) {
|
||||||
|
|
||||||
export function encrypt(
|
export function encrypt(
|
||||||
input: string,
|
input: string,
|
||||||
secretOption: SecretOption = SecretOption.JWT
|
secretOption: SecretOption = SecretOption.API
|
||||||
) {
|
) {
|
||||||
const salt = crypto.randomBytes(RANDOM_BYTES)
|
const salt = crypto.randomBytes(RANDOM_BYTES)
|
||||||
const stretched = stretchString(getSecret(secretOption), salt)
|
const stretched = stretchString(getSecret(secretOption), salt)
|
||||||
|
@ -50,7 +50,7 @@ export function encrypt(
|
||||||
|
|
||||||
export function decrypt(
|
export function decrypt(
|
||||||
input: string,
|
input: string,
|
||||||
secretOption: SecretOption = SecretOption.JWT
|
secretOption: SecretOption = SecretOption.API
|
||||||
) {
|
) {
|
||||||
const [salt, encrypted] = input.split(SEPARATOR)
|
const [salt, encrypted] = input.split(SEPARATOR)
|
||||||
const saltBuffer = Buffer.from(salt, "hex")
|
const saltBuffer = Buffer.from(salt, "hex")
|
||||||
|
|
1
packages/backend-core/src/timers/index.ts
Normal file
1
packages/backend-core/src/timers/index.ts
Normal file
|
@ -0,0 +1 @@
|
||||||
|
export * from "./timers"
|
22
packages/backend-core/src/timers/timers.ts
Normal file
22
packages/backend-core/src/timers/timers.ts
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
let intervals: NodeJS.Timeout[] = []
|
||||||
|
|
||||||
|
export function set(callback: () => any, period: number) {
|
||||||
|
const interval = setInterval(callback, period)
|
||||||
|
intervals.push(interval)
|
||||||
|
return interval
|
||||||
|
}
|
||||||
|
|
||||||
|
export function clear(interval: NodeJS.Timeout) {
|
||||||
|
const idx = intervals.indexOf(interval)
|
||||||
|
if (idx !== -1) {
|
||||||
|
intervals.splice(idx, 1)
|
||||||
|
}
|
||||||
|
clearInterval(interval)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function cleanup() {
|
||||||
|
for (let interval of intervals) {
|
||||||
|
clearInterval(interval)
|
||||||
|
}
|
||||||
|
intervals = []
|
||||||
|
}
|
|
@ -1,5 +1,4 @@
|
||||||
import { getAllApps, queryGlobalView } from "../db"
|
import { getAllApps, queryGlobalView } from "../db"
|
||||||
import { options } from "../middleware/passport/jwt"
|
|
||||||
import {
|
import {
|
||||||
Header,
|
Header,
|
||||||
MAX_VALID_DATE,
|
MAX_VALID_DATE,
|
||||||
|
@ -133,7 +132,30 @@ export function openJwt(token: string) {
|
||||||
if (!token) {
|
if (!token) {
|
||||||
return token
|
return token
|
||||||
}
|
}
|
||||||
return jwt.verify(token, options.secretOrKey)
|
try {
|
||||||
|
return jwt.verify(token, env.JWT_SECRET)
|
||||||
|
} catch (e) {
|
||||||
|
if (env.JWT_SECRET_FALLBACK) {
|
||||||
|
// fallback to enable rotation
|
||||||
|
return jwt.verify(token, env.JWT_SECRET_FALLBACK)
|
||||||
|
} else {
|
||||||
|
throw e
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isValidInternalAPIKey(apiKey: string) {
|
||||||
|
if (env.INTERNAL_API_KEY && env.INTERNAL_API_KEY === apiKey) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
// fallback to enable rotation
|
||||||
|
if (
|
||||||
|
env.INTERNAL_API_KEY_FALLBACK &&
|
||||||
|
env.INTERNAL_API_KEY_FALLBACK === apiKey
|
||||||
|
) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -165,7 +187,7 @@ export function setCookie(
|
||||||
opts = { sign: true }
|
opts = { sign: true }
|
||||||
) {
|
) {
|
||||||
if (value && opts && opts.sign) {
|
if (value && opts && opts.sign) {
|
||||||
value = jwt.sign(value, options.secretOrKey)
|
value = jwt.sign(value, env.JWT_SECRET)
|
||||||
}
|
}
|
||||||
|
|
||||||
const config: SetOption = {
|
const config: SetOption = {
|
||||||
|
|
|
@ -4,3 +4,4 @@ process.env.NODE_ENV = "jest"
|
||||||
process.env.MOCK_REDIS = "1"
|
process.env.MOCK_REDIS = "1"
|
||||||
process.env.LOG_LEVEL = process.env.LOG_LEVEL || "error"
|
process.env.LOG_LEVEL = process.env.LOG_LEVEL || "error"
|
||||||
process.env.ENABLE_4XX_HTTP_LOGGING = "0"
|
process.env.ENABLE_4XX_HTTP_LOGGING = "0"
|
||||||
|
process.env.REDIS_PASSWORD = "budibase"
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import "./logging"
|
import "./logging"
|
||||||
import env from "../src/environment"
|
import env from "../src/environment"
|
||||||
|
import { cleanup } from "../src/timers"
|
||||||
import { mocks, testContainerUtils } from "./utilities"
|
import { mocks, testContainerUtils } from "./utilities"
|
||||||
|
|
||||||
// must explicitly enable fetch mock
|
// must explicitly enable fetch mock
|
||||||
|
@ -21,3 +22,7 @@ if (!process.env.CI) {
|
||||||
}
|
}
|
||||||
|
|
||||||
testContainerUtils.setupEnv(env)
|
testContainerUtils.setupEnv(env)
|
||||||
|
|
||||||
|
afterAll(() => {
|
||||||
|
cleanup()
|
||||||
|
})
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"name": "@budibase/bbui",
|
"name": "@budibase/bbui",
|
||||||
"description": "A UI solution used in the different Budibase projects.",
|
"description": "A UI solution used in the different Budibase projects.",
|
||||||
"version": "2.4.12-alpha.3",
|
"version": "2.4.27-alpha.9",
|
||||||
"license": "MPL-2.0",
|
"license": "MPL-2.0",
|
||||||
"svelte": "src/index.js",
|
"svelte": "src/index.js",
|
||||||
"module": "dist/bbui.es.js",
|
"module": "dist/bbui.es.js",
|
||||||
|
@ -38,8 +38,8 @@
|
||||||
],
|
],
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@adobe/spectrum-css-workflow-icons": "1.2.1",
|
"@adobe/spectrum-css-workflow-icons": "1.2.1",
|
||||||
"@budibase/shared-core": "2.4.12-alpha.3",
|
"@budibase/shared-core": "2.4.27-alpha.9",
|
||||||
"@budibase/string-templates": "2.4.12-alpha.3",
|
"@budibase/string-templates": "2.4.27-alpha.9",
|
||||||
"@spectrum-css/accordion": "3.0.24",
|
"@spectrum-css/accordion": "3.0.24",
|
||||||
"@spectrum-css/actionbutton": "1.0.1",
|
"@spectrum-css/actionbutton": "1.0.1",
|
||||||
"@spectrum-css/actiongroup": "1.0.1",
|
"@spectrum-css/actiongroup": "1.0.1",
|
||||||
|
|
115
packages/bbui/src/Form/Core/File.svelte
Normal file
115
packages/bbui/src/Form/Core/File.svelte
Normal file
|
@ -0,0 +1,115 @@
|
||||||
|
<script>
|
||||||
|
import ActionButton from "../../ActionButton/ActionButton.svelte"
|
||||||
|
import { uuid } from "../../helpers"
|
||||||
|
import Icon from "../../Icon/Icon.svelte"
|
||||||
|
import { createEventDispatcher } from "svelte"
|
||||||
|
|
||||||
|
export let value = null
|
||||||
|
export let title = "Upload file"
|
||||||
|
export let disabled = false
|
||||||
|
export let allowClear = null
|
||||||
|
export let extensions = null
|
||||||
|
export let handleFileTooLarge = null
|
||||||
|
export let fileSizeLimit = BYTES_IN_MB * 20
|
||||||
|
export let id = null
|
||||||
|
export let previewUrl = null
|
||||||
|
|
||||||
|
const fieldId = id || uuid()
|
||||||
|
const BYTES_IN_KB = 1000
|
||||||
|
const BYTES_IN_MB = 1000000
|
||||||
|
|
||||||
|
const dispatch = createEventDispatcher()
|
||||||
|
|
||||||
|
let fileInput
|
||||||
|
|
||||||
|
$: inputAccept = Array.isArray(extensions) ? extensions.join(",") : "*"
|
||||||
|
|
||||||
|
async function processFile(targetFile) {
|
||||||
|
if (handleFileTooLarge && targetFile?.size >= fileSizeLimit) {
|
||||||
|
handleFileTooLarge(targetFile)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
dispatch("change", targetFile)
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleFile(evt) {
|
||||||
|
processFile(evt.target.files[0])
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearFile() {
|
||||||
|
dispatch("change", null)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<input
|
||||||
|
id={fieldId}
|
||||||
|
{disabled}
|
||||||
|
type="file"
|
||||||
|
accept={inputAccept}
|
||||||
|
bind:this={fileInput}
|
||||||
|
on:change={handleFile}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div class="field">
|
||||||
|
{#if value}
|
||||||
|
<div class="file-view">
|
||||||
|
{#if previewUrl}
|
||||||
|
<img class="preview" alt="" src={previewUrl} />
|
||||||
|
{/if}
|
||||||
|
<div class="filename">{value.name}</div>
|
||||||
|
{#if value.size}
|
||||||
|
<div class="filesize">
|
||||||
|
{#if value.size <= BYTES_IN_MB}
|
||||||
|
{`${value.size / BYTES_IN_KB} KB`}
|
||||||
|
{:else}
|
||||||
|
{`${value.size / BYTES_IN_MB} MB`}
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
{#if !disabled || (allowClear === true && disabled)}
|
||||||
|
<div class="delete-button" on:click={clearFile}>
|
||||||
|
<Icon name="Close" size="XS" />
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
<ActionButton {disabled} on:click={fileInput.click()}>{title}</ActionButton>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.field {
|
||||||
|
display: flex;
|
||||||
|
gap: var(--spacing-m);
|
||||||
|
}
|
||||||
|
.file-view {
|
||||||
|
display: flex;
|
||||||
|
gap: var(--spacing-l);
|
||||||
|
align-items: center;
|
||||||
|
border: 1px solid var(--spectrum-alias-border-color);
|
||||||
|
border-radius: var(--spectrum-global-dimension-size-50);
|
||||||
|
padding: 0px var(--spectrum-alias-item-padding-m);
|
||||||
|
}
|
||||||
|
input[type="file"] {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.delete-button {
|
||||||
|
transition: all 0.3s;
|
||||||
|
margin-left: 10px;
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
.delete-button:hover {
|
||||||
|
cursor: pointer;
|
||||||
|
color: var(--red);
|
||||||
|
}
|
||||||
|
.filesize {
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
.filename {
|
||||||
|
overflow: hidden;
|
||||||
|
white-space: nowrap;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
.preview {
|
||||||
|
height: 1.5em;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -13,3 +13,4 @@ export { default as CoreDropzone } from "./Dropzone.svelte"
|
||||||
export { default as CoreStepper } from "./Stepper.svelte"
|
export { default as CoreStepper } from "./Stepper.svelte"
|
||||||
export { default as CoreRichTextField } from "./RichTextField.svelte"
|
export { default as CoreRichTextField } from "./RichTextField.svelte"
|
||||||
export { default as CoreSlider } from "./Slider.svelte"
|
export { default as CoreSlider } from "./Slider.svelte"
|
||||||
|
export { default as CoreFile } from "./File.svelte"
|
||||||
|
|
37
packages/bbui/src/Form/File.svelte
Normal file
37
packages/bbui/src/Form/File.svelte
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
<script>
|
||||||
|
import Field from "./Field.svelte"
|
||||||
|
import { CoreFile } from "./Core"
|
||||||
|
import { createEventDispatcher } from "svelte"
|
||||||
|
|
||||||
|
export let label = null
|
||||||
|
export let labelPosition = "above"
|
||||||
|
export let disabled = false
|
||||||
|
export let allowClear = null
|
||||||
|
export let handleFileTooLarge = () => {}
|
||||||
|
export let previewUrl = null
|
||||||
|
export let extensions = null
|
||||||
|
export let error = null
|
||||||
|
export let title = null
|
||||||
|
export let value = null
|
||||||
|
export let tooltip = null
|
||||||
|
|
||||||
|
const dispatch = createEventDispatcher()
|
||||||
|
const onChange = e => {
|
||||||
|
value = e.detail
|
||||||
|
dispatch("change", e.detail)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Field {label} {labelPosition} {error} {tooltip}>
|
||||||
|
<CoreFile
|
||||||
|
{error}
|
||||||
|
{disabled}
|
||||||
|
{allowClear}
|
||||||
|
{title}
|
||||||
|
{value}
|
||||||
|
{previewUrl}
|
||||||
|
{handleFileTooLarge}
|
||||||
|
{extensions}
|
||||||
|
on:change={onChange}
|
||||||
|
/>
|
||||||
|
</Field>
|
|
@ -1,56 +0,0 @@
|
||||||
<div class="skeleton">
|
|
||||||
<div class="children">
|
|
||||||
<slot />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
.skeleton {
|
|
||||||
height: 100%;
|
|
||||||
width: 100%;
|
|
||||||
opacity: 0;
|
|
||||||
background-color: var(--spectrum-global-color-gray-200) !important;
|
|
||||||
border-radius: 7px;
|
|
||||||
overflow: hidden;
|
|
||||||
position: relative;
|
|
||||||
animation: fadeIn 130ms ease 0s 1 normal forwards;
|
|
||||||
}
|
|
||||||
|
|
||||||
.children {
|
|
||||||
pointer-events: none;
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.skeleton::after {
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
right: 0;
|
|
||||||
bottom: 0;
|
|
||||||
left: 0;
|
|
||||||
transform: translateX(-100%);
|
|
||||||
background-image: linear-gradient(
|
|
||||||
90deg,
|
|
||||||
rgba(255, 255, 255, 0) 0,
|
|
||||||
rgba(255, 255, 255, 0.15) 20%,
|
|
||||||
rgba(255, 255, 255, 0.3) 60%,
|
|
||||||
rgba(255, 255, 255, 0)
|
|
||||||
);
|
|
||||||
animation: shimmer 2s infinite;
|
|
||||||
content: "";
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes fadeIn {
|
|
||||||
0% {
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
100% {
|
|
||||||
opacity: 0.75;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes shimmer {
|
|
||||||
100% {
|
|
||||||
transform: translateX(100%);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
|
@ -4,7 +4,6 @@ import "./bbui.css"
|
||||||
import "@spectrum-css/icon/dist/index-vars.css"
|
import "@spectrum-css/icon/dist/index-vars.css"
|
||||||
|
|
||||||
// Components
|
// Components
|
||||||
export { default as Skeleton } from "./Skeleton/Skeleton.svelte"
|
|
||||||
export { default as Input } from "./Form/Input.svelte"
|
export { default as Input } from "./Form/Input.svelte"
|
||||||
export { default as Stepper } from "./Form/Stepper.svelte"
|
export { default as Stepper } from "./Form/Stepper.svelte"
|
||||||
export { default as TextArea } from "./Form/TextArea.svelte"
|
export { default as TextArea } from "./Form/TextArea.svelte"
|
||||||
|
@ -78,6 +77,7 @@ export { default as IconSideNav } from "./IconSideNav/IconSideNav.svelte"
|
||||||
export { default as IconSideNavItem } from "./IconSideNav/IconSideNavItem.svelte"
|
export { default as IconSideNavItem } from "./IconSideNav/IconSideNavItem.svelte"
|
||||||
export { default as Slider } from "./Form/Slider.svelte"
|
export { default as Slider } from "./Form/Slider.svelte"
|
||||||
export { default as Accordion } from "./Accordion/Accordion.svelte"
|
export { default as Accordion } from "./Accordion/Accordion.svelte"
|
||||||
|
export { default as File } from "./Form/File.svelte"
|
||||||
|
|
||||||
// Renderers
|
// Renderers
|
||||||
export { default as BoldRenderer } from "./Table/BoldRenderer.svelte"
|
export { default as BoldRenderer } from "./Table/BoldRenderer.svelte"
|
||||||
|
|
|
@ -1,17 +1,17 @@
|
||||||
<!doctype html>
|
<!doctype html>
|
||||||
<html class="spectrum spectrum--medium spectrum--darkest" lang="en" dir="ltr">
|
<html class="spectrum spectrum--medium spectrum--darkest" lang="en" dir="ltr">
|
||||||
|
|
||||||
<head>
|
<head>
|
||||||
<meta charset='utf8'>
|
<meta charset='utf8'>
|
||||||
<meta name='viewport' content='width=device-width'>
|
<meta name='viewport' content='width=device-width'>
|
||||||
<title>Budibase</title>
|
<title>Budibase</title>
|
||||||
<link rel='icon' href='/src/favicon.png'>
|
|
||||||
<link rel="preconnect" href="https://fonts.gstatic.com" />
|
<link rel="preconnect" href="https://fonts.gstatic.com" />
|
||||||
<link
|
<link href="https://fonts.googleapis.com/css2?family=Source+Sans+Pro:wght@400;600;700&display=swap"
|
||||||
href="https://fonts.googleapis.com/css2?family=Source+Sans+Pro:wght@400;600;700&display=swap"
|
rel="stylesheet" />
|
||||||
rel="stylesheet"
|
|
||||||
/>
|
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body id="app">
|
<body id="app">
|
||||||
<script type="module" src='/src/main.js'></script>
|
<script type="module" src='/src/main.js'></script>
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
</html>
|
</html>
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@budibase/builder",
|
"name": "@budibase/builder",
|
||||||
"version": "2.4.12-alpha.3",
|
"version": "2.4.27-alpha.9",
|
||||||
"license": "GPL-3.0",
|
"license": "GPL-3.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
@ -58,11 +58,11 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@budibase/bbui": "2.4.12-alpha.3",
|
"@budibase/bbui": "2.4.27-alpha.9",
|
||||||
"@budibase/client": "2.4.12-alpha.3",
|
"@budibase/client": "2.4.27-alpha.9",
|
||||||
"@budibase/frontend-core": "2.4.12-alpha.3",
|
"@budibase/frontend-core": "2.4.27-alpha.9",
|
||||||
"@budibase/shared-core": "2.4.12-alpha.3",
|
"@budibase/shared-core": "2.4.27-alpha.9",
|
||||||
"@budibase/string-templates": "2.4.12-alpha.3",
|
"@budibase/string-templates": "2.4.27-alpha.9",
|
||||||
"@fortawesome/fontawesome-svg-core": "^6.2.1",
|
"@fortawesome/fontawesome-svg-core": "^6.2.1",
|
||||||
"@fortawesome/free-brands-svg-icons": "^6.2.1",
|
"@fortawesome/free-brands-svg-icons": "^6.2.1",
|
||||||
"@fortawesome/free-solid-svg-icons": "^6.2.1",
|
"@fortawesome/free-solid-svg-icons": "^6.2.1",
|
||||||
|
|
|
@ -308,7 +308,7 @@
|
||||||
{ name: "Auto Column", type: AUTO_TYPE },
|
{ name: "Auto Column", type: AUTO_TYPE },
|
||||||
]
|
]
|
||||||
} else {
|
} else {
|
||||||
return [
|
let fields = [
|
||||||
FIELDS.STRING,
|
FIELDS.STRING,
|
||||||
FIELDS.BARCODEQR,
|
FIELDS.BARCODEQR,
|
||||||
FIELDS.LONGFORM,
|
FIELDS.LONGFORM,
|
||||||
|
@ -316,10 +316,13 @@
|
||||||
FIELDS.DATETIME,
|
FIELDS.DATETIME,
|
||||||
FIELDS.NUMBER,
|
FIELDS.NUMBER,
|
||||||
FIELDS.BOOLEAN,
|
FIELDS.BOOLEAN,
|
||||||
FIELDS.ARRAY,
|
|
||||||
FIELDS.FORMULA,
|
FIELDS.FORMULA,
|
||||||
FIELDS.LINK,
|
|
||||||
]
|
]
|
||||||
|
// no-sql or a spreadsheet
|
||||||
|
if (!external || table.sql) {
|
||||||
|
fields = [...fields, FIELDS.LINK, FIELDS.ARRAY]
|
||||||
|
}
|
||||||
|
return fields
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -35,7 +35,9 @@
|
||||||
await datasources.fetch()
|
await datasources.fetch()
|
||||||
$goto(`../../table/${table._id}`)
|
$goto(`../../table/${table._id}`)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
notifications.error("Error saving table")
|
notifications.error(
|
||||||
|
`Error saving table - ${error?.message || "unknown error"}`
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -74,6 +74,14 @@
|
||||||
}
|
}
|
||||||
return capitalise(name)
|
return capitalise(name)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getDisplayError(error, configKey) {
|
||||||
|
return error?.replace(
|
||||||
|
new RegExp(`${configKey}`, "i"),
|
||||||
|
getDisplayName(configKey)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
function getFieldGroupKeys(fieldGroup) {
|
function getFieldGroupKeys(fieldGroup) {
|
||||||
return Object.entries(schema[fieldGroup].fields || {})
|
return Object.entries(schema[fieldGroup].fields || {})
|
||||||
.filter(el => filter(el))
|
.filter(el => filter(el))
|
||||||
|
@ -147,7 +155,7 @@
|
||||||
type={schema[configKey].type}
|
type={schema[configKey].type}
|
||||||
on:change
|
on:change
|
||||||
bind:value={config[configKey]}
|
bind:value={config[configKey]}
|
||||||
error={$validation.errors[configKey]}
|
error={getDisplayError($validation.errors[configKey], configKey)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{:else if schema[configKey].type === "fieldGroup"}
|
{:else if schema[configKey].type === "fieldGroup"}
|
||||||
|
@ -180,7 +188,7 @@
|
||||||
type={configKey === "port" ? "string" : schema[configKey].type}
|
type={configKey === "port" ? "string" : schema[configKey].type}
|
||||||
on:change
|
on:change
|
||||||
bind:value={config[configKey]}
|
bind:value={config[configKey]}
|
||||||
error={$validation.errors[configKey]}
|
error={getDisplayError($validation.errors[configKey], configKey)}
|
||||||
environmentVariablesEnabled={$licensing.environmentVariablesEnabled}
|
environmentVariablesEnabled={$licensing.environmentVariablesEnabled}
|
||||||
{handleUpgradePanel}
|
{handleUpgradePanel}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -1,15 +1,22 @@
|
||||||
<script>
|
<script>
|
||||||
import { ModalContent, Body, Layout } from "@budibase/bbui"
|
import { ModalContent, Body, Layout, Link } from "@budibase/bbui"
|
||||||
import { IntegrationNames } from "constants/backend"
|
import { IntegrationNames } from "constants/backend"
|
||||||
import cloneDeep from "lodash/cloneDeepWith"
|
import cloneDeep from "lodash/cloneDeepWith"
|
||||||
import GoogleButton from "../_components/GoogleButton.svelte"
|
import GoogleButton from "../_components/GoogleButton.svelte"
|
||||||
import { saveDatasource as save } from "builderStore/datasource"
|
import { saveDatasource as save } from "builderStore/datasource"
|
||||||
|
import { organisation } from "stores/portal"
|
||||||
|
import { onMount } from "svelte"
|
||||||
|
|
||||||
export let integration
|
export let integration
|
||||||
export let modal
|
export let modal
|
||||||
|
|
||||||
// kill the reference so the input isn't saved
|
// kill the reference so the input isn't saved
|
||||||
let datasource = cloneDeep(integration)
|
let datasource = cloneDeep(integration)
|
||||||
|
$: isGoogleConfigured = !!$organisation.google
|
||||||
|
|
||||||
|
onMount(async () => {
|
||||||
|
await organisation.init()
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<ModalContent
|
<ModalContent
|
||||||
|
@ -18,12 +25,21 @@
|
||||||
cancelText="Back"
|
cancelText="Back"
|
||||||
size="L"
|
size="L"
|
||||||
>
|
>
|
||||||
<Layout noPadding>
|
<!-- check true and false directly, don't render until flag is set -->
|
||||||
<Body size="XS"
|
{#if isGoogleConfigured === true}
|
||||||
>Authenticate with your google account to use the {IntegrationNames[
|
<Layout noPadding>
|
||||||
datasource.type
|
<Body size="S"
|
||||||
]} integration.</Body
|
>Authenticate with your google account to use the {IntegrationNames[
|
||||||
|
datasource.type
|
||||||
|
]} integration.</Body
|
||||||
|
>
|
||||||
|
</Layout>
|
||||||
|
<GoogleButton preAuthStep={() => save(datasource, true)} />
|
||||||
|
{:else if isGoogleConfigured === false}
|
||||||
|
<Body size="S"
|
||||||
|
>Google authentication is not enabled, please complete Google SSO
|
||||||
|
configuration.</Body
|
||||||
>
|
>
|
||||||
</Layout>
|
<Link href="/builder/portal/settings/auth">Configure Google SSO</Link>
|
||||||
<GoogleButton preAuthStep={() => save(datasource, true)} />
|
{/if}
|
||||||
</ModalContent>
|
</ModalContent>
|
||||||
|
|
|
@ -15,20 +15,12 @@
|
||||||
$: tourKey = $store.tourKey
|
$: tourKey = $store.tourKey
|
||||||
$: tourStepKey = $store.tourStepKey
|
$: tourStepKey = $store.tourStepKey
|
||||||
|
|
||||||
const initTour = targetKey => {
|
const updateTourStep = (targetStepKey, tourKey) => {
|
||||||
if (!targetKey) {
|
if (!tourKey) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
tourSteps = [...TOURS[targetKey]]
|
|
||||||
tourStepIdx = 0
|
|
||||||
tourStep = { ...tourSteps[tourStepIdx] }
|
|
||||||
}
|
|
||||||
|
|
||||||
$: initTour(tourKey)
|
|
||||||
|
|
||||||
const updateTourStep = targetStepKey => {
|
|
||||||
if (!tourSteps?.length) {
|
if (!tourSteps?.length) {
|
||||||
return
|
tourSteps = [...TOURS[tourKey]]
|
||||||
}
|
}
|
||||||
tourStepIdx = getCurrentStepIdx(tourSteps, targetStepKey)
|
tourStepIdx = getCurrentStepIdx(tourSteps, targetStepKey)
|
||||||
lastStep = tourStepIdx + 1 == tourSteps.length
|
lastStep = tourStepIdx + 1 == tourSteps.length
|
||||||
|
@ -36,7 +28,7 @@
|
||||||
tourStep.onLoad()
|
tourStep.onLoad()
|
||||||
}
|
}
|
||||||
|
|
||||||
$: updateTourStep(tourStepKey)
|
$: updateTourStep(tourStepKey, tourKey)
|
||||||
|
|
||||||
const showPopover = (tourStep, tourNodes, popover) => {
|
const showPopover = (tourStep, tourNodes, popover) => {
|
||||||
if (!tourStep) {
|
if (!tourStep) {
|
||||||
|
|
|
@ -8,20 +8,28 @@
|
||||||
|
|
||||||
let currentTourStep
|
let currentTourStep
|
||||||
let ready = false
|
let ready = false
|
||||||
|
let registered = false
|
||||||
let handler
|
let handler
|
||||||
|
|
||||||
|
const registerTourNode = (tourKey, stepKey) => {
|
||||||
|
if (ready && !registered && tourKey) {
|
||||||
|
currentTourStep = TOURS[tourKey].find(step => step.id === stepKey)
|
||||||
|
if (!currentTourStep) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const elem = document.querySelector(currentTourStep.query)
|
||||||
|
handler = tourHandler(elem, stepKey)
|
||||||
|
registered = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$: tourKeyWatch = $store.tourKey
|
||||||
|
$: registerTourNode(tourKeyWatch, tourStepKey, ready)
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
if (!$store.tourKey) return
|
|
||||||
|
|
||||||
currentTourStep = TOURS[$store.tourKey].find(
|
|
||||||
step => step.id === tourStepKey
|
|
||||||
)
|
|
||||||
if (!currentTourStep) return
|
|
||||||
|
|
||||||
const elem = document.querySelector(currentTourStep.query)
|
|
||||||
handler = tourHandler(elem, tourStepKey)
|
|
||||||
ready = true
|
ready = true
|
||||||
})
|
})
|
||||||
|
|
||||||
onDestroy(() => {
|
onDestroy(() => {
|
||||||
if (handler) {
|
if (handler) {
|
||||||
handler.destroy()
|
handler.destroy()
|
||||||
|
|
32
packages/builder/src/pages/builder/Branding.svelte
Normal file
32
packages/builder/src/pages/builder/Branding.svelte
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
<script>
|
||||||
|
import { organisation, auth } from "stores/portal"
|
||||||
|
import { onMount } from "svelte"
|
||||||
|
|
||||||
|
let loaded = false
|
||||||
|
|
||||||
|
$: platformTitleText = $organisation.platformTitle
|
||||||
|
$: platformTitle =
|
||||||
|
!$auth.user && platformTitleText ? platformTitleText : "Budibase"
|
||||||
|
|
||||||
|
$: faviconUrl = $organisation.faviconUrl || "https://i.imgur.com/Xhdt1YP.png"
|
||||||
|
|
||||||
|
onMount(async () => {
|
||||||
|
await organisation.init()
|
||||||
|
loaded = true
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<!--
|
||||||
|
In order to update the org elements, an update will have to be made to clear them.
|
||||||
|
-->
|
||||||
|
|
||||||
|
<svelte:head>
|
||||||
|
<title>{platformTitle}</title>
|
||||||
|
|
||||||
|
{#if loaded && !$auth.user && faviconUrl}
|
||||||
|
<link rel="icon" href={faviconUrl} />
|
||||||
|
{:else}
|
||||||
|
<!-- A default must be set or the browser defaults to favicon.ico behaviour -->
|
||||||
|
<link rel="icon" href={"https://i.imgur.com/Xhdt1YP.png"} />
|
||||||
|
{/if}
|
||||||
|
</svelte:head>
|
|
@ -4,6 +4,7 @@
|
||||||
import { onMount } from "svelte"
|
import { onMount } from "svelte"
|
||||||
import { CookieUtils, Constants } from "@budibase/frontend-core"
|
import { CookieUtils, Constants } from "@budibase/frontend-core"
|
||||||
import { API } from "api"
|
import { API } from "api"
|
||||||
|
import Branding from "./Branding.svelte"
|
||||||
|
|
||||||
let loaded = false
|
let loaded = false
|
||||||
|
|
||||||
|
@ -146,6 +147,9 @@
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<!--Portal branding overrides -->
|
||||||
|
<Branding />
|
||||||
|
|
||||||
{#if loaded}
|
{#if loaded}
|
||||||
<slot />
|
<slot />
|
||||||
{/if}
|
{/if}
|
||||||
|
|
|
@ -182,12 +182,13 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleKeyDown = e => {
|
const handleKeyDown = e => {
|
||||||
if (e.key === "Tab") {
|
if (e.key === "Tab" || e.key === "ArrowDown" || e.key === "ArrowUp") {
|
||||||
// Cycle selected components on tab press
|
// Cycle selected components on tab press
|
||||||
if (selectedIndex == null) {
|
if (selectedIndex == null) {
|
||||||
selectedIndex = 0
|
selectedIndex = 0
|
||||||
} else {
|
} else {
|
||||||
selectedIndex = (selectedIndex + 1) % componentList.length
|
const direction = e.key === "ArrowUp" ? -1 : 1
|
||||||
|
selectedIndex = (selectedIndex + direction) % componentList.length
|
||||||
}
|
}
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
e.stopPropagation()
|
e.stopPropagation()
|
||||||
|
|
|
@ -30,7 +30,7 @@
|
||||||
async function login() {
|
async function login() {
|
||||||
form.validate()
|
form.validate()
|
||||||
if (Object.keys(errors).length > 0) {
|
if (Object.keys(errors).length > 0) {
|
||||||
console.log("errors")
|
console.log("errors", errors)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
|
@ -64,99 +64,106 @@
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svelte:window on:keydown={handleKeydown} />
|
<svelte:window on:keydown={handleKeydown} />
|
||||||
|
{#if loaded}
|
||||||
<TestimonialPage>
|
<TestimonialPage enabled={$organisation.testimonialsEnabled}>
|
||||||
<Layout gap="L" noPadding>
|
<Layout gap="L" noPadding>
|
||||||
<Layout justifyItems="center" noPadding>
|
<Layout justifyItems="center" noPadding>
|
||||||
{#if loaded}
|
{#if loaded}
|
||||||
<img alt="logo" src={$organisation.logoUrl || Logo} />
|
<img alt="logo" src={$organisation.logoUrl || Logo} />
|
||||||
{/if}
|
{/if}
|
||||||
<Heading size="M">Log in to Budibase</Heading>
|
<Heading size="M">
|
||||||
</Layout>
|
{$organisation.loginHeading || "Log in to Budibase"}
|
||||||
<Layout gap="S" noPadding>
|
</Heading>
|
||||||
{#if loaded && ($organisation.google || $organisation.oidc)}
|
</Layout>
|
||||||
<FancyForm>
|
<Layout gap="S" noPadding>
|
||||||
<OIDCButton oidcIcon={$oidc.logo} oidcName={$oidc.name} />
|
{#if loaded && ($organisation.google || $organisation.oidc)}
|
||||||
<GoogleButton />
|
<FancyForm>
|
||||||
</FancyForm>
|
<OIDCButton oidcIcon={$oidc.logo} oidcName={$oidc.name} />
|
||||||
{/if}
|
<GoogleButton />
|
||||||
|
</FancyForm>
|
||||||
|
{/if}
|
||||||
|
{#if !$organisation.isSSOEnforced}
|
||||||
|
<Divider />
|
||||||
|
<FancyForm bind:this={form}>
|
||||||
|
<FancyInput
|
||||||
|
label="Your work email"
|
||||||
|
value={formData.username}
|
||||||
|
on:change={e => {
|
||||||
|
formData = {
|
||||||
|
...formData,
|
||||||
|
username: e.detail,
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
validate={() => {
|
||||||
|
let fieldError = {
|
||||||
|
username: !formData.username
|
||||||
|
? "Please enter a valid email"
|
||||||
|
: undefined,
|
||||||
|
}
|
||||||
|
errors = handleError({ ...errors, ...fieldError })
|
||||||
|
}}
|
||||||
|
error={errors.username}
|
||||||
|
/>
|
||||||
|
<FancyInput
|
||||||
|
label="Password"
|
||||||
|
value={formData.password}
|
||||||
|
type="password"
|
||||||
|
on:change={e => {
|
||||||
|
formData = {
|
||||||
|
...formData,
|
||||||
|
password: e.detail,
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
validate={() => {
|
||||||
|
let fieldError = {
|
||||||
|
password: !formData.password
|
||||||
|
? "Please enter your password"
|
||||||
|
: undefined,
|
||||||
|
}
|
||||||
|
errors = handleError({ ...errors, ...fieldError })
|
||||||
|
}}
|
||||||
|
error={errors.password}
|
||||||
|
/>
|
||||||
|
</FancyForm>
|
||||||
|
{/if}
|
||||||
|
</Layout>
|
||||||
{#if !$organisation.isSSOEnforced}
|
{#if !$organisation.isSSOEnforced}
|
||||||
<Divider />
|
<Layout gap="XS" noPadding justifyItems="center">
|
||||||
<FancyForm bind:this={form}>
|
<Button
|
||||||
<FancyInput
|
size="L"
|
||||||
label="Your work email"
|
cta
|
||||||
value={formData.username}
|
disabled={Object.keys(errors).length > 0}
|
||||||
on:change={e => {
|
on:click={login}
|
||||||
formData = {
|
>
|
||||||
...formData,
|
{$organisation.loginButton || `Log in to ${company}`}
|
||||||
username: e.detail,
|
</Button>
|
||||||
}
|
</Layout>
|
||||||
}}
|
<Layout gap="XS" noPadding justifyItems="center">
|
||||||
validate={() => {
|
<div class="user-actions">
|
||||||
let fieldError = {
|
<ActionButton size="L" quiet on:click={() => $goto("./forgot")}>
|
||||||
username: !formData.username
|
Forgot password?
|
||||||
? "Please enter a valid email"
|
</ActionButton>
|
||||||
: undefined,
|
</div>
|
||||||
}
|
</Layout>
|
||||||
errors = handleError({ ...errors, ...fieldError })
|
{/if}
|
||||||
}}
|
|
||||||
error={errors.username}
|
{#if cloud}
|
||||||
/>
|
<Body size="xs" textAlign="center">
|
||||||
<FancyInput
|
By using Budibase Cloud
|
||||||
label="Password"
|
<br />
|
||||||
value={formData.password}
|
you are agreeing to our
|
||||||
type="password"
|
<Link
|
||||||
on:change={e => {
|
href="https://budibase.com/eula"
|
||||||
formData = {
|
target="_blank"
|
||||||
...formData,
|
secondary={true}
|
||||||
password: e.detail,
|
>
|
||||||
}
|
License Agreement
|
||||||
}}
|
</Link>
|
||||||
validate={() => {
|
</Body>
|
||||||
let fieldError = {
|
|
||||||
password: !formData.password
|
|
||||||
? "Please enter your password"
|
|
||||||
: undefined,
|
|
||||||
}
|
|
||||||
errors = handleError({ ...errors, ...fieldError })
|
|
||||||
}}
|
|
||||||
error={errors.password}
|
|
||||||
/>
|
|
||||||
</FancyForm>
|
|
||||||
{/if}
|
{/if}
|
||||||
</Layout>
|
</Layout>
|
||||||
{#if !$organisation.isSSOEnforced}
|
</TestimonialPage>
|
||||||
<Layout gap="XS" noPadding justifyItems="center">
|
{/if}
|
||||||
<Button
|
|
||||||
size="L"
|
|
||||||
cta
|
|
||||||
disabled={Object.keys(errors).length > 0}
|
|
||||||
on:click={login}
|
|
||||||
>
|
|
||||||
Log in to {company}
|
|
||||||
</Button>
|
|
||||||
</Layout>
|
|
||||||
<Layout gap="XS" noPadding justifyItems="center">
|
|
||||||
<div class="user-actions">
|
|
||||||
<ActionButton size="L" quiet on:click={() => $goto("./forgot")}>
|
|
||||||
Forgot password?
|
|
||||||
</ActionButton>
|
|
||||||
</div>
|
|
||||||
</Layout>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
{#if cloud}
|
|
||||||
<Body size="xs" textAlign="center">
|
|
||||||
By using Budibase Cloud
|
|
||||||
<br />
|
|
||||||
you are agreeing to our
|
|
||||||
<Link href="https://budibase.com/eula" target="_blank" secondary={true}>
|
|
||||||
License Agreement
|
|
||||||
</Link>
|
|
||||||
</Body>
|
|
||||||
{/if}
|
|
||||||
</Layout>
|
|
||||||
</TestimonialPage>
|
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.user-actions {
|
.user-actions {
|
||||||
|
|
|
@ -47,8 +47,9 @@
|
||||||
$: googleCallbackTooltip = $admin.cloud
|
$: googleCallbackTooltip = $admin.cloud
|
||||||
? null
|
? null
|
||||||
: googleCallbackReadonly
|
: googleCallbackReadonly
|
||||||
? "Vist the organisation page to update the platform URL"
|
? "Visit the organisation page to update the platform URL"
|
||||||
: "Leave blank to use the default callback URL"
|
: "Leave blank to use the default callback URL"
|
||||||
|
$: googleSheetsCallbackUrl = `${$organisation.platformUrl}/api/global/auth/datasource/google/callback`
|
||||||
|
|
||||||
$: GoogleConfigFields = {
|
$: GoogleConfigFields = {
|
||||||
Google: [
|
Google: [
|
||||||
|
@ -62,6 +63,14 @@
|
||||||
placeholder: $organisation.googleCallbackUrl,
|
placeholder: $organisation.googleCallbackUrl,
|
||||||
copyButton: true,
|
copyButton: true,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "sheetsURL",
|
||||||
|
label: "Sheets URL",
|
||||||
|
readonly: googleCallbackReadonly,
|
||||||
|
tooltip: googleCallbackTooltip,
|
||||||
|
placeholder: googleSheetsCallbackUrl,
|
||||||
|
copyButton: true,
|
||||||
|
},
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -396,7 +405,11 @@
|
||||||
</Heading>
|
</Heading>
|
||||||
<Body size="S">
|
<Body size="S">
|
||||||
To allow users to authenticate using their Google accounts, fill out the
|
To allow users to authenticate using their Google accounts, fill out the
|
||||||
fields below.
|
fields below. Read the <Link
|
||||||
|
size="M"
|
||||||
|
href={"https://docs.budibase.com/docs/sso-with-google"}
|
||||||
|
>documentation</Link
|
||||||
|
> for more information.
|
||||||
</Body>
|
</Body>
|
||||||
</Layout>
|
</Layout>
|
||||||
<Layout gap="XS" noPadding>
|
<Layout gap="XS" noPadding>
|
||||||
|
|
|
@ -0,0 +1,446 @@
|
||||||
|
<script>
|
||||||
|
import {
|
||||||
|
Layout,
|
||||||
|
Heading,
|
||||||
|
Body,
|
||||||
|
Divider,
|
||||||
|
File,
|
||||||
|
notifications,
|
||||||
|
Tags,
|
||||||
|
Tag,
|
||||||
|
Button,
|
||||||
|
Toggle,
|
||||||
|
Input,
|
||||||
|
Label,
|
||||||
|
TextArea,
|
||||||
|
} from "@budibase/bbui"
|
||||||
|
import { auth, organisation, licensing, admin } from "stores/portal"
|
||||||
|
import { API } from "api"
|
||||||
|
import { onMount } from "svelte"
|
||||||
|
import { goto } from "@roxi/routify"
|
||||||
|
|
||||||
|
const imageExtensions = [
|
||||||
|
".png",
|
||||||
|
".tiff",
|
||||||
|
".gif",
|
||||||
|
".raw",
|
||||||
|
".jpg",
|
||||||
|
".jpeg",
|
||||||
|
".svg",
|
||||||
|
".bmp",
|
||||||
|
".jfif",
|
||||||
|
]
|
||||||
|
|
||||||
|
const faviconExtensions = [".png", ".ico", ".gif"]
|
||||||
|
|
||||||
|
let mounted = false
|
||||||
|
let saving = false
|
||||||
|
|
||||||
|
let logoFile = null
|
||||||
|
let logoPreview = null
|
||||||
|
let faviconFile = null
|
||||||
|
let faviconPreview = null
|
||||||
|
|
||||||
|
let config = {}
|
||||||
|
let updated = false
|
||||||
|
|
||||||
|
$: onConfigUpdate(config, mounted)
|
||||||
|
$: init = Object.keys(config).length > 0
|
||||||
|
|
||||||
|
$: isCloud = $admin.cloud
|
||||||
|
$: brandingEnabled = $licensing.brandingEnabled
|
||||||
|
|
||||||
|
const onConfigUpdate = () => {
|
||||||
|
if (!mounted || updated || !init) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
updated = true
|
||||||
|
}
|
||||||
|
|
||||||
|
$: logo = config.logoUrl
|
||||||
|
? { url: config.logoUrl, type: "image", name: "Logo" }
|
||||||
|
: null
|
||||||
|
|
||||||
|
$: favicon = config.faviconUrl
|
||||||
|
? { url: config.faviconUrl, type: "image", name: "Favicon" }
|
||||||
|
: null
|
||||||
|
|
||||||
|
const previewUrl = async localFile => {
|
||||||
|
if (!localFile) {
|
||||||
|
return Promise.resolve(null)
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Promise(resolve => {
|
||||||
|
let reader = new FileReader()
|
||||||
|
try {
|
||||||
|
reader.onload = e => {
|
||||||
|
resolve({
|
||||||
|
result: e.target.result,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
reader.readAsDataURL(localFile)
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error)
|
||||||
|
resolve(null)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
$: previewUrl(logoFile).then(response => {
|
||||||
|
if (response) {
|
||||||
|
logoPreview = response.result
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
$: previewUrl(faviconFile).then(response => {
|
||||||
|
if (response) {
|
||||||
|
faviconPreview = response.result
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
async function uploadLogo(file) {
|
||||||
|
let response = {}
|
||||||
|
try {
|
||||||
|
let data = new FormData()
|
||||||
|
data.append("file", file)
|
||||||
|
response = await API.uploadLogo(data)
|
||||||
|
} catch (error) {
|
||||||
|
notifications.error("Error uploading logo")
|
||||||
|
}
|
||||||
|
return response
|
||||||
|
}
|
||||||
|
|
||||||
|
async function uploadFavicon(file) {
|
||||||
|
let response = {}
|
||||||
|
try {
|
||||||
|
let data = new FormData()
|
||||||
|
data.append("file", file)
|
||||||
|
response = await API.uploadFavicon(data)
|
||||||
|
} catch (error) {
|
||||||
|
notifications.error("Error uploading favicon")
|
||||||
|
}
|
||||||
|
return response
|
||||||
|
}
|
||||||
|
|
||||||
|
async function saveConfig() {
|
||||||
|
saving = true
|
||||||
|
|
||||||
|
if (logoFile) {
|
||||||
|
const logoResp = await uploadLogo(logoFile)
|
||||||
|
if (logoResp.url) {
|
||||||
|
config = {
|
||||||
|
...config,
|
||||||
|
logoUrl: logoResp.url,
|
||||||
|
}
|
||||||
|
logoFile = null
|
||||||
|
logoPreview = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (faviconFile) {
|
||||||
|
const faviconResp = await uploadFavicon(faviconFile)
|
||||||
|
if (faviconResp.url) {
|
||||||
|
config = {
|
||||||
|
...config,
|
||||||
|
faviconUrl: faviconResp.url,
|
||||||
|
}
|
||||||
|
faviconFile = null
|
||||||
|
faviconPreview = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Trim
|
||||||
|
const userStrings = [
|
||||||
|
"metaTitle",
|
||||||
|
"platformTitle",
|
||||||
|
"loginButton",
|
||||||
|
"loginHeading",
|
||||||
|
"metaDescription",
|
||||||
|
"metaImageUrl",
|
||||||
|
]
|
||||||
|
|
||||||
|
const trimmed = userStrings.reduce((acc, fieldName) => {
|
||||||
|
acc[fieldName] = config[fieldName] ? config[fieldName].trim() : undefined
|
||||||
|
return acc
|
||||||
|
}, {})
|
||||||
|
|
||||||
|
config = {
|
||||||
|
...config,
|
||||||
|
...trimmed,
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Update settings
|
||||||
|
await organisation.save(config)
|
||||||
|
await organisation.init()
|
||||||
|
notifications.success("Branding settings updated")
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Branding updated failed", e)
|
||||||
|
notifications.error("Branding updated failed")
|
||||||
|
}
|
||||||
|
updated = false
|
||||||
|
saving = false
|
||||||
|
}
|
||||||
|
|
||||||
|
onMount(async () => {
|
||||||
|
await organisation.init()
|
||||||
|
|
||||||
|
config = {
|
||||||
|
faviconUrl: $organisation.faviconUrl,
|
||||||
|
logoUrl: $organisation.logoUrl,
|
||||||
|
platformTitle: $organisation.platformTitle,
|
||||||
|
emailBrandingEnabled: $organisation.emailBrandingEnabled,
|
||||||
|
loginHeading: $organisation.loginHeading,
|
||||||
|
loginButton: $organisation.loginButton,
|
||||||
|
testimonialsEnabled: $organisation.testimonialsEnabled,
|
||||||
|
metaDescription: $organisation.metaDescription,
|
||||||
|
metaImageUrl: $organisation.metaImageUrl,
|
||||||
|
metaTitle: $organisation.metaTitle,
|
||||||
|
}
|
||||||
|
mounted = true
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if $auth.isAdmin && mounted}
|
||||||
|
<Layout noPadding>
|
||||||
|
<Layout gap="XS" noPadding>
|
||||||
|
<div class="title">
|
||||||
|
<Heading size="M">Branding</Heading>
|
||||||
|
{#if !isCloud && !brandingEnabled}
|
||||||
|
<Tags>
|
||||||
|
<Tag icon="LockClosed">Business</Tag>
|
||||||
|
</Tags>
|
||||||
|
{/if}
|
||||||
|
{#if isCloud && !brandingEnabled}
|
||||||
|
<Tags>
|
||||||
|
<Tag icon="LockClosed">Pro</Tag>
|
||||||
|
</Tags>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
<Body>Remove all Budibase branding and use your own.</Body>
|
||||||
|
</Layout>
|
||||||
|
<Divider />
|
||||||
|
<div class="branding fields">
|
||||||
|
<div class="field">
|
||||||
|
<Label size="L">Logo</Label>
|
||||||
|
<File
|
||||||
|
title="Upload image"
|
||||||
|
handleFileTooLarge={() => {
|
||||||
|
notifications.warn("File too large. 20mb limit")
|
||||||
|
}}
|
||||||
|
extensions={imageExtensions}
|
||||||
|
previewUrl={logoPreview || logo?.url}
|
||||||
|
on:change={e => {
|
||||||
|
let clone = { ...config }
|
||||||
|
if (e.detail) {
|
||||||
|
logoFile = e.detail
|
||||||
|
logoPreview = null
|
||||||
|
} else {
|
||||||
|
logoFile = null
|
||||||
|
clone.logoUrl = ""
|
||||||
|
}
|
||||||
|
config = clone
|
||||||
|
}}
|
||||||
|
value={logoFile || logo}
|
||||||
|
disabled={!brandingEnabled || saving}
|
||||||
|
allowClear={true}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="field">
|
||||||
|
<Label size="L">Favicon</Label>
|
||||||
|
<File
|
||||||
|
title="Upload image"
|
||||||
|
handleFileTooLarge={() => {
|
||||||
|
notifications.warn("File too large. 20mb limit")
|
||||||
|
}}
|
||||||
|
extensions={faviconExtensions}
|
||||||
|
previewUrl={faviconPreview || favicon?.url}
|
||||||
|
on:change={e => {
|
||||||
|
let clone = { ...config }
|
||||||
|
if (e.detail) {
|
||||||
|
faviconFile = e.detail
|
||||||
|
faviconPreview = null
|
||||||
|
} else {
|
||||||
|
clone.faviconUrl = ""
|
||||||
|
}
|
||||||
|
config = clone
|
||||||
|
}}
|
||||||
|
value={faviconFile || favicon}
|
||||||
|
disabled={!brandingEnabled || saving}
|
||||||
|
allowClear={true}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{#if !isCloud}
|
||||||
|
<div class="field">
|
||||||
|
<Label size="L">Title</Label>
|
||||||
|
<Input
|
||||||
|
on:change={e => {
|
||||||
|
let clone = { ...config }
|
||||||
|
clone.platformTitle = e.detail ? e.detail : ""
|
||||||
|
config = clone
|
||||||
|
}}
|
||||||
|
value={config.platformTitle || ""}
|
||||||
|
disabled={!brandingEnabled || saving}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
<div>
|
||||||
|
<Toggle
|
||||||
|
text={"Remove Budibase brand from emails"}
|
||||||
|
on:change={e => {
|
||||||
|
let clone = { ...config }
|
||||||
|
clone.emailBrandingEnabled = !e.detail
|
||||||
|
config = clone
|
||||||
|
}}
|
||||||
|
value={!config.emailBrandingEnabled}
|
||||||
|
disabled={!brandingEnabled || saving}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{#if !isCloud}
|
||||||
|
<Divider />
|
||||||
|
<Layout gap="XS" noPadding>
|
||||||
|
<Heading size="S">Login page</Heading>
|
||||||
|
<Body />
|
||||||
|
</Layout>
|
||||||
|
<div class="login">
|
||||||
|
<div class="fields">
|
||||||
|
<div class="field">
|
||||||
|
<Label size="L">Header</Label>
|
||||||
|
<Input
|
||||||
|
on:change={e => {
|
||||||
|
let clone = { ...config }
|
||||||
|
clone.loginHeading = e.detail ? e.detail : ""
|
||||||
|
config = clone
|
||||||
|
}}
|
||||||
|
value={config.loginHeading || ""}
|
||||||
|
disabled={!brandingEnabled || saving}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="field">
|
||||||
|
<Label size="L">Button</Label>
|
||||||
|
<Input
|
||||||
|
on:change={e => {
|
||||||
|
let clone = { ...config }
|
||||||
|
clone.loginButton = e.detail ? e.detail : ""
|
||||||
|
config = clone
|
||||||
|
}}
|
||||||
|
value={config.loginButton || ""}
|
||||||
|
disabled={!brandingEnabled || saving}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Toggle
|
||||||
|
text={"Remove customer testimonials"}
|
||||||
|
on:change={e => {
|
||||||
|
let clone = { ...config }
|
||||||
|
clone.testimonialsEnabled = !e.detail
|
||||||
|
config = clone
|
||||||
|
}}
|
||||||
|
value={!config.testimonialsEnabled}
|
||||||
|
disabled={!brandingEnabled || saving}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
<Divider />
|
||||||
|
<Layout gap="XS" noPadding>
|
||||||
|
<Heading size="S">Application previews</Heading>
|
||||||
|
<Body>Customise the meta tags on your app preview</Body>
|
||||||
|
</Layout>
|
||||||
|
<div class="app-previews">
|
||||||
|
<div class="fields">
|
||||||
|
<div class="field">
|
||||||
|
<Label size="L">Image URL</Label>
|
||||||
|
<Input
|
||||||
|
on:change={e => {
|
||||||
|
let clone = { ...config }
|
||||||
|
clone.metaImageUrl = e.detail ? e.detail : ""
|
||||||
|
config = clone
|
||||||
|
}}
|
||||||
|
value={config.metaImageUrl}
|
||||||
|
disabled={!brandingEnabled || saving}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<Label size="L">Title</Label>
|
||||||
|
<Input
|
||||||
|
on:change={e => {
|
||||||
|
let clone = { ...config }
|
||||||
|
clone.metaTitle = e.detail ? e.detail : ""
|
||||||
|
config = clone
|
||||||
|
}}
|
||||||
|
value={config.metaTitle}
|
||||||
|
disabled={!brandingEnabled || saving}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<Label size="L">Description</Label>
|
||||||
|
<TextArea
|
||||||
|
on:change={e => {
|
||||||
|
let clone = { ...config }
|
||||||
|
clone.metaDescription = e.detail ? e.detail : ""
|
||||||
|
config = clone
|
||||||
|
}}
|
||||||
|
value={config.metaDescription}
|
||||||
|
disabled={!brandingEnabled || saving}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="buttons">
|
||||||
|
{#if !brandingEnabled}
|
||||||
|
<Button
|
||||||
|
on:click={() => {
|
||||||
|
if (isCloud && $auth?.user?.accountPortalAccess) {
|
||||||
|
window.open($admin.accountPortalUrl + "/portal/upgrade", "_blank")
|
||||||
|
} else if ($auth.isAdmin) {
|
||||||
|
$goto("/builder/portal/account/upgrade")
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
secondary
|
||||||
|
disabled={saving}
|
||||||
|
>
|
||||||
|
Upgrade
|
||||||
|
</Button>
|
||||||
|
{/if}
|
||||||
|
<Button on:click={saveConfig} cta disabled={saving || !updated || !init}>
|
||||||
|
Save
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</Layout>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.buttons {
|
||||||
|
display: flex;
|
||||||
|
gap: var(--spacing-m);
|
||||||
|
}
|
||||||
|
.title {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: flex-start;
|
||||||
|
gap: var(--spacing-m);
|
||||||
|
}
|
||||||
|
|
||||||
|
.branding,
|
||||||
|
.login {
|
||||||
|
width: 70%;
|
||||||
|
max-width: 70%;
|
||||||
|
}
|
||||||
|
.fields {
|
||||||
|
display: grid;
|
||||||
|
grid-gap: var(--spacing-m);
|
||||||
|
}
|
||||||
|
.field {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 80px auto;
|
||||||
|
grid-gap: var(--spacing-l);
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -7,12 +7,10 @@
|
||||||
Divider,
|
Divider,
|
||||||
Label,
|
Label,
|
||||||
Input,
|
Input,
|
||||||
Dropzone,
|
|
||||||
notifications,
|
notifications,
|
||||||
Toggle,
|
Toggle,
|
||||||
} from "@budibase/bbui"
|
} from "@budibase/bbui"
|
||||||
import { auth, organisation, admin } from "stores/portal"
|
import { auth, organisation, admin } from "stores/portal"
|
||||||
import { API } from "api"
|
|
||||||
import { writable } from "svelte/store"
|
import { writable } from "svelte/store"
|
||||||
import { redirect } from "@roxi/routify"
|
import { redirect } from "@roxi/routify"
|
||||||
|
|
||||||
|
@ -28,32 +26,14 @@
|
||||||
company: $organisation.company,
|
company: $organisation.company,
|
||||||
platformUrl: $organisation.platformUrl,
|
platformUrl: $organisation.platformUrl,
|
||||||
analyticsEnabled: $organisation.analyticsEnabled,
|
analyticsEnabled: $organisation.analyticsEnabled,
|
||||||
logo: $organisation.logoUrl
|
|
||||||
? { url: $organisation.logoUrl, type: "image", name: "Logo" }
|
|
||||||
: null,
|
|
||||||
})
|
})
|
||||||
let loading = false
|
|
||||||
|
|
||||||
async function uploadLogo(file) {
|
let loading = false
|
||||||
try {
|
|
||||||
let data = new FormData()
|
|
||||||
data.append("file", file)
|
|
||||||
await API.uploadLogo(data)
|
|
||||||
} catch (error) {
|
|
||||||
notifications.error("Error uploading logo")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function saveConfig() {
|
async function saveConfig() {
|
||||||
loading = true
|
loading = true
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Upload logo if required
|
|
||||||
if ($values.logo && !$values.logo.url) {
|
|
||||||
await uploadLogo($values.logo)
|
|
||||||
await organisation.init()
|
|
||||||
}
|
|
||||||
|
|
||||||
const config = {
|
const config = {
|
||||||
isSSOEnforced: $values.isSSOEnforced,
|
isSSOEnforced: $values.isSSOEnforced,
|
||||||
company: $values.company ?? "",
|
company: $values.company ?? "",
|
||||||
|
@ -61,11 +41,6 @@
|
||||||
analyticsEnabled: $values.analyticsEnabled,
|
analyticsEnabled: $values.analyticsEnabled,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove logo if required
|
|
||||||
if (!$values.logo) {
|
|
||||||
config.logoUrl = ""
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update settings
|
// Update settings
|
||||||
await organisation.save(config)
|
await organisation.save(config)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
@ -87,21 +62,7 @@
|
||||||
<Label size="L">Org. name</Label>
|
<Label size="L">Org. name</Label>
|
||||||
<Input thin bind:value={$values.company} />
|
<Input thin bind:value={$values.company} />
|
||||||
</div>
|
</div>
|
||||||
<div class="field logo">
|
|
||||||
<Label size="L">Logo</Label>
|
|
||||||
<div class="file">
|
|
||||||
<Dropzone
|
|
||||||
value={[$values.logo]}
|
|
||||||
on:change={e => {
|
|
||||||
if (!e.detail || e.detail.length === 0) {
|
|
||||||
$values.logo = null
|
|
||||||
} else {
|
|
||||||
$values.logo = e.detail[0]
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{#if !$admin.cloud}
|
{#if !$admin.cloud}
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<Label
|
<Label
|
||||||
|
@ -137,10 +98,4 @@
|
||||||
grid-gap: var(--spacing-l);
|
grid-gap: var(--spacing-l);
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
.file {
|
|
||||||
max-width: 30ch;
|
|
||||||
}
|
|
||||||
.logo {
|
|
||||||
align-items: start;
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -13,9 +13,11 @@ export const createLicensingStore = () => {
|
||||||
license: undefined,
|
license: undefined,
|
||||||
isFreePlan: true,
|
isFreePlan: true,
|
||||||
isEnterprisePlan: true,
|
isEnterprisePlan: true,
|
||||||
|
isBusinessPlan: true,
|
||||||
// features
|
// features
|
||||||
groupsEnabled: false,
|
groupsEnabled: false,
|
||||||
backupsEnabled: false,
|
backupsEnabled: false,
|
||||||
|
brandingEnabled: false,
|
||||||
// the currently used quotas from the db
|
// the currently used quotas from the db
|
||||||
quotaUsage: undefined,
|
quotaUsage: undefined,
|
||||||
// derived quota metrics for percentages used
|
// derived quota metrics for percentages used
|
||||||
|
@ -57,6 +59,7 @@ export const createLicensingStore = () => {
|
||||||
const planType = license?.plan.type
|
const planType = license?.plan.type
|
||||||
const isEnterprisePlan = planType === Constants.PlanType.ENTERPRISE
|
const isEnterprisePlan = planType === Constants.PlanType.ENTERPRISE
|
||||||
const isFreePlan = planType === Constants.PlanType.FREE
|
const isFreePlan = planType === Constants.PlanType.FREE
|
||||||
|
const isBusinessPlan = planType === Constants.PlanType.BUSINESS
|
||||||
const groupsEnabled = license.features.includes(
|
const groupsEnabled = license.features.includes(
|
||||||
Constants.Features.USER_GROUPS
|
Constants.Features.USER_GROUPS
|
||||||
)
|
)
|
||||||
|
@ -69,7 +72,9 @@ export const createLicensingStore = () => {
|
||||||
const enforceableSSO = license.features.includes(
|
const enforceableSSO = license.features.includes(
|
||||||
Constants.Features.ENFORCEABLE_SSO
|
Constants.Features.ENFORCEABLE_SSO
|
||||||
)
|
)
|
||||||
|
const brandingEnabled = license.features.includes(
|
||||||
|
Constants.Features.BRANDING
|
||||||
|
)
|
||||||
const auditLogsEnabled = license.features.includes(
|
const auditLogsEnabled = license.features.includes(
|
||||||
Constants.Features.AUDIT_LOGS
|
Constants.Features.AUDIT_LOGS
|
||||||
)
|
)
|
||||||
|
@ -79,8 +84,10 @@ export const createLicensingStore = () => {
|
||||||
license,
|
license,
|
||||||
isEnterprisePlan,
|
isEnterprisePlan,
|
||||||
isFreePlan,
|
isFreePlan,
|
||||||
|
isBusinessPlan,
|
||||||
groupsEnabled,
|
groupsEnabled,
|
||||||
backupsEnabled,
|
backupsEnabled,
|
||||||
|
brandingEnabled,
|
||||||
environmentVariablesEnabled,
|
environmentVariablesEnabled,
|
||||||
auditLogsEnabled,
|
auditLogsEnabled,
|
||||||
enforceableSSO,
|
enforceableSSO,
|
||||||
|
|
|
@ -50,6 +50,10 @@ export const menu = derived([admin, auth], ([$admin, $auth]) => {
|
||||||
title: "Organisation",
|
title: "Organisation",
|
||||||
href: "/builder/portal/settings/organisation",
|
href: "/builder/portal/settings/organisation",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
title: "Branding",
|
||||||
|
href: "/builder/portal/settings/branding",
|
||||||
|
},
|
||||||
{
|
{
|
||||||
title: "Environment",
|
title: "Environment",
|
||||||
href: "/builder/portal/settings/environment",
|
href: "/builder/portal/settings/environment",
|
||||||
|
|
|
@ -6,6 +6,15 @@ import _ from "lodash"
|
||||||
const DEFAULT_CONFIG = {
|
const DEFAULT_CONFIG = {
|
||||||
platformUrl: "",
|
platformUrl: "",
|
||||||
logoUrl: undefined,
|
logoUrl: undefined,
|
||||||
|
faviconUrl: undefined,
|
||||||
|
emailBrandingEnabled: true,
|
||||||
|
testimonialsEnabled: true,
|
||||||
|
platformTitle: "Budibase",
|
||||||
|
loginHeading: undefined,
|
||||||
|
loginButton: undefined,
|
||||||
|
metaDescription: undefined,
|
||||||
|
metaImageUrl: undefined,
|
||||||
|
metaTitle: undefined,
|
||||||
docsUrl: undefined,
|
docsUrl: undefined,
|
||||||
company: "Budibase",
|
company: "Budibase",
|
||||||
oidc: undefined,
|
oidc: undefined,
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@budibase/cli",
|
"name": "@budibase/cli",
|
||||||
"version": "2.4.12-alpha.3",
|
"version": "2.4.27-alpha.9",
|
||||||
"description": "Budibase CLI, for developers, self hosting and migrations.",
|
"description": "Budibase CLI, for developers, self hosting and migrations.",
|
||||||
"main": "dist/index.js",
|
"main": "dist/index.js",
|
||||||
"bin": {
|
"bin": {
|
||||||
|
@ -29,9 +29,9 @@
|
||||||
"outputPath": "build"
|
"outputPath": "build"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@budibase/backend-core": "2.4.12-alpha.3",
|
"@budibase/backend-core": "2.4.27-alpha.9",
|
||||||
"@budibase/string-templates": "2.4.12-alpha.3",
|
"@budibase/string-templates": "2.4.27-alpha.9",
|
||||||
"@budibase/types": "2.4.12-alpha.3",
|
"@budibase/types": "2.4.27-alpha.9",
|
||||||
"axios": "0.21.2",
|
"axios": "0.21.2",
|
||||||
"chalk": "4.1.0",
|
"chalk": "4.1.0",
|
||||||
"cli-progress": "3.11.2",
|
"cli-progress": "3.11.2",
|
||||||
|
|
|
@ -13,6 +13,7 @@ export const ENV_PATH = path.resolve("./.env")
|
||||||
|
|
||||||
function getSecrets(opts = { single: false }) {
|
function getSecrets(opts = { single: false }) {
|
||||||
const secrets = [
|
const secrets = [
|
||||||
|
"API_ENCRYPTION_KEY",
|
||||||
"JWT_SECRET",
|
"JWT_SECRET",
|
||||||
"MINIO_ACCESS_KEY",
|
"MINIO_ACCESS_KEY",
|
||||||
"MINIO_SECRET_KEY",
|
"MINIO_SECRET_KEY",
|
||||||
|
|
|
@ -4,6 +4,8 @@ import {
|
||||||
downloadDockerCompose,
|
downloadDockerCompose,
|
||||||
handleError,
|
handleError,
|
||||||
getServices,
|
getServices,
|
||||||
|
getServiceImage,
|
||||||
|
setServiceImage,
|
||||||
} from "./utils"
|
} from "./utils"
|
||||||
import { confirmation } from "../questions"
|
import { confirmation } from "../questions"
|
||||||
import compose from "docker-compose"
|
import compose from "docker-compose"
|
||||||
|
@ -23,7 +25,11 @@ export async function update() {
|
||||||
!isSingle &&
|
!isSingle &&
|
||||||
(await confirmation("Do you wish to update you docker-compose.yaml?"))
|
(await confirmation("Do you wish to update you docker-compose.yaml?"))
|
||||||
) {
|
) {
|
||||||
|
// get current MinIO image
|
||||||
|
const image = await getServiceImage("minio")
|
||||||
await downloadDockerCompose()
|
await downloadDockerCompose()
|
||||||
|
// replace MinIO image
|
||||||
|
setServiceImage("minio", image)
|
||||||
}
|
}
|
||||||
await handleError(async () => {
|
await handleError(async () => {
|
||||||
const status = await compose.ps()
|
const status = await compose.ps()
|
||||||
|
|
|
@ -9,10 +9,44 @@ const ERROR_FILE = "docker-error.log"
|
||||||
const COMPOSE_URL =
|
const COMPOSE_URL =
|
||||||
"https://raw.githubusercontent.com/Budibase/budibase/master/hosting/docker-compose.yaml"
|
"https://raw.githubusercontent.com/Budibase/budibase/master/hosting/docker-compose.yaml"
|
||||||
|
|
||||||
export async function downloadDockerCompose() {
|
function composeFilename() {
|
||||||
const fileName = COMPOSE_URL.split("/").slice(-1)[0]
|
return COMPOSE_URL.split("/").slice(-1)[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getServiceImage(service: string) {
|
||||||
|
const filename = composeFilename()
|
||||||
try {
|
try {
|
||||||
await downloadFile(COMPOSE_URL, `./${fileName}`)
|
const { services } = getServices(filename)
|
||||||
|
const serviceKey = Object.keys(services).find(name =>
|
||||||
|
name.includes(service)
|
||||||
|
)
|
||||||
|
if (serviceKey) {
|
||||||
|
return services[serviceKey].image
|
||||||
|
} else {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setServiceImage(service: string, image: string) {
|
||||||
|
const filename = composeFilename()
|
||||||
|
if (!fs.existsSync(filename)) {
|
||||||
|
throw new Error(
|
||||||
|
`File ${filename} not found, cannot update ${service} image.`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
const current = getServiceImage(service)!
|
||||||
|
let contents = fs.readFileSync(filename, "utf8")
|
||||||
|
contents = contents.replace(`image: ${current}`, `image: ${image}`)
|
||||||
|
fs.writeFileSync(filename, contents)
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function downloadDockerCompose() {
|
||||||
|
const filename = composeFilename()
|
||||||
|
try {
|
||||||
|
await downloadFile(COMPOSE_URL, `./${filename}`)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(error(`Failed to retrieve compose file - ${err}`))
|
console.error(error(`Failed to retrieve compose file - ${err}`))
|
||||||
}
|
}
|
||||||
|
@ -49,6 +83,9 @@ export async function handleError(func: Function) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getServices(path: string) {
|
export function getServices(path: string) {
|
||||||
|
if (!fs.existsSync(path)) {
|
||||||
|
throw new Error(`No yaml found at path: ${path}`)
|
||||||
|
}
|
||||||
const dockerYaml = fs.readFileSync(path, "utf8")
|
const dockerYaml = fs.readFileSync(path, "utf8")
|
||||||
const parsedYaml = yaml.parse(dockerYaml)
|
const parsedYaml = yaml.parse(dockerYaml)
|
||||||
return { yaml: parsedYaml, services: parsedYaml.services }
|
return { yaml: parsedYaml, services: parsedYaml.services }
|
||||||
|
|
|
@ -239,9 +239,9 @@
|
||||||
"@hapi/hoek" "^9.0.0"
|
"@hapi/hoek" "^9.0.0"
|
||||||
|
|
||||||
"@sideway/formula@^3.0.0":
|
"@sideway/formula@^3.0.0":
|
||||||
version "3.0.0"
|
version "3.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/@sideway/formula/-/formula-3.0.0.tgz#fe158aee32e6bd5de85044be615bc08478a0a13c"
|
resolved "https://registry.yarnpkg.com/@sideway/formula/-/formula-3.0.1.tgz#80fcbcbaf7ce031e0ef2dd29b1bfc7c3f583611f"
|
||||||
integrity sha512-vHe7wZ4NOXVfkoRb8T5otiENVlT7a3IAiw7H5M2+GO+9CDgcVUUsX1zalAztCmwyOr2RUTGJdgB+ZvSVqmdHmg==
|
integrity sha512-/poHZJJVjx3L+zVD6g9KgHfYnb443oi7wLu/XKojDviHy6HOEOA6z1Trk5aR1dGcmPenJEgb2sK2I80LeS3MIg==
|
||||||
|
|
||||||
"@sideway/pinpoint@^2.0.0":
|
"@sideway/pinpoint@^2.0.0":
|
||||||
version "2.0.0"
|
version "2.0.0"
|
||||||
|
|
|
@ -17,10 +17,7 @@
|
||||||
"description": "This component is specific only to layouts",
|
"description": "This component is specific only to layouts",
|
||||||
"icon": "Sandbox",
|
"icon": "Sandbox",
|
||||||
"hasChildren": true,
|
"hasChildren": true,
|
||||||
"styles": [
|
"styles": ["padding", "background"],
|
||||||
"padding",
|
|
||||||
"background"
|
|
||||||
],
|
|
||||||
"settings": [
|
"settings": [
|
||||||
{
|
{
|
||||||
"type": "text",
|
"type": "text",
|
||||||
|
@ -36,23 +33,14 @@
|
||||||
"type": "select",
|
"type": "select",
|
||||||
"label": "Navigation",
|
"label": "Navigation",
|
||||||
"key": "navigation",
|
"key": "navigation",
|
||||||
"options": [
|
"options": ["Top", "Left", "None"],
|
||||||
"Top",
|
|
||||||
"Left",
|
|
||||||
"None"
|
|
||||||
],
|
|
||||||
"defaultValue": "Top"
|
"defaultValue": "Top"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "select",
|
"type": "select",
|
||||||
"label": "Width",
|
"label": "Width",
|
||||||
"key": "width",
|
"key": "width",
|
||||||
"options": [
|
"options": ["Small", "Medium", "Large", "Max"],
|
||||||
"Small",
|
|
||||||
"Medium",
|
|
||||||
"Large",
|
|
||||||
"Max"
|
|
||||||
],
|
|
||||||
"defaultValue": "Large"
|
"defaultValue": "Large"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -89,13 +77,7 @@
|
||||||
"width": 400,
|
"width": 400,
|
||||||
"height": 200
|
"height": 200
|
||||||
},
|
},
|
||||||
"styles": [
|
"styles": ["padding", "size", "background", "border", "shadow"],
|
||||||
"padding",
|
|
||||||
"size",
|
|
||||||
"background",
|
|
||||||
"border",
|
|
||||||
"shadow"
|
|
||||||
],
|
|
||||||
"settings": [
|
"settings": [
|
||||||
{
|
{
|
||||||
"type": "select",
|
"type": "select",
|
||||||
|
@ -255,9 +237,7 @@
|
||||||
"description": "Add a section to your application",
|
"description": "Add a section to your application",
|
||||||
"icon": "ColumnTwoB",
|
"icon": "ColumnTwoB",
|
||||||
"hasChildren": true,
|
"hasChildren": true,
|
||||||
"illegalChildren": [
|
"illegalChildren": ["section"],
|
||||||
"section"
|
|
||||||
],
|
|
||||||
"showEmptyState": false,
|
"showEmptyState": false,
|
||||||
"size": {
|
"size": {
|
||||||
"width": 400,
|
"width": 400,
|
||||||
|
@ -376,9 +356,7 @@
|
||||||
"name": "Divider",
|
"name": "Divider",
|
||||||
"description": "A basic divider",
|
"description": "A basic divider",
|
||||||
"icon": "Separator",
|
"icon": "Separator",
|
||||||
"illegalChildren": [
|
"illegalChildren": ["section"],
|
||||||
"section"
|
|
||||||
],
|
|
||||||
"size": {
|
"size": {
|
||||||
"width": 400,
|
"width": 400,
|
||||||
"height": 10
|
"height": 10
|
||||||
|
@ -415,9 +393,7 @@
|
||||||
"name": "Repeater",
|
"name": "Repeater",
|
||||||
"description": "A configurable data list that attaches to your backend tables.",
|
"description": "A configurable data list that attaches to your backend tables.",
|
||||||
"icon": "JourneyData",
|
"icon": "JourneyData",
|
||||||
"illegalChildren": [
|
"illegalChildren": ["section"],
|
||||||
"section"
|
|
||||||
],
|
|
||||||
"hasChildren": true,
|
"hasChildren": true,
|
||||||
"size": {
|
"size": {
|
||||||
"width": 400,
|
"width": 400,
|
||||||
|
@ -574,9 +550,7 @@
|
||||||
"name": "Stacked List",
|
"name": "Stacked List",
|
||||||
"icon": "TaskList",
|
"icon": "TaskList",
|
||||||
"description": "A basic card component that can contain content and actions.",
|
"description": "A basic card component that can contain content and actions.",
|
||||||
"illegalChildren": [
|
"illegalChildren": ["section"],
|
||||||
"section"
|
|
||||||
],
|
|
||||||
"settings": [
|
"settings": [
|
||||||
{
|
{
|
||||||
"type": "text",
|
"type": "text",
|
||||||
|
@ -606,9 +580,7 @@
|
||||||
"name": "Vertical Card",
|
"name": "Vertical Card",
|
||||||
"description": "A basic card component that can contain content and actions.",
|
"description": "A basic card component that can contain content and actions.",
|
||||||
"icon": "ViewColumn",
|
"icon": "ViewColumn",
|
||||||
"illegalChildren": [
|
"illegalChildren": ["section"],
|
||||||
"section"
|
|
||||||
],
|
|
||||||
"settings": [
|
"settings": [
|
||||||
{
|
{
|
||||||
"type": "text",
|
"type": "text",
|
||||||
|
@ -652,24 +624,14 @@
|
||||||
"type": "select",
|
"type": "select",
|
||||||
"label": "Image Height",
|
"label": "Image Height",
|
||||||
"key": "imageHeight",
|
"key": "imageHeight",
|
||||||
"options": [
|
"options": ["auto", "12rem", "16rem", "20rem", "24rem"],
|
||||||
"auto",
|
|
||||||
"12rem",
|
|
||||||
"16rem",
|
|
||||||
"20rem",
|
|
||||||
"24rem"
|
|
||||||
],
|
|
||||||
"defaultValue": "auto"
|
"defaultValue": "auto"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "select",
|
"type": "select",
|
||||||
"label": "Card Width",
|
"label": "Card Width",
|
||||||
"key": "cardWidth",
|
"key": "cardWidth",
|
||||||
"options": [
|
"options": ["16rem", "20rem", "24rem"],
|
||||||
"16rem",
|
|
||||||
"20rem",
|
|
||||||
"24rem"
|
|
||||||
],
|
|
||||||
"defaultValue": "20rem"
|
"defaultValue": "20rem"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
@ -678,9 +640,7 @@
|
||||||
"name": "Paragraph",
|
"name": "Paragraph",
|
||||||
"description": "A component for displaying paragraph text.",
|
"description": "A component for displaying paragraph text.",
|
||||||
"icon": "TextParagraph",
|
"icon": "TextParagraph",
|
||||||
"illegalChildren": [
|
"illegalChildren": ["section"],
|
||||||
"section"
|
|
||||||
],
|
|
||||||
"editable": true,
|
"editable": true,
|
||||||
"size": {
|
"size": {
|
||||||
"width": 400,
|
"width": 400,
|
||||||
|
@ -803,9 +763,7 @@
|
||||||
"name": "Headline",
|
"name": "Headline",
|
||||||
"icon": "TextBold",
|
"icon": "TextBold",
|
||||||
"description": "A component for displaying heading text",
|
"description": "A component for displaying heading text",
|
||||||
"illegalChildren": [
|
"illegalChildren": ["section"],
|
||||||
"section"
|
|
||||||
],
|
|
||||||
"editable": true,
|
"editable": true,
|
||||||
"size": {
|
"size": {
|
||||||
"width": 400,
|
"width": 400,
|
||||||
|
@ -982,9 +940,7 @@
|
||||||
"name": "Image",
|
"name": "Image",
|
||||||
"description": "A basic component for displaying images",
|
"description": "A basic component for displaying images",
|
||||||
"icon": "Image",
|
"icon": "Image",
|
||||||
"styles": [
|
"styles": ["size"],
|
||||||
"size"
|
|
||||||
],
|
|
||||||
"size": {
|
"size": {
|
||||||
"width": 400,
|
"width": 400,
|
||||||
"height": 300
|
"height": 300
|
||||||
|
@ -1002,9 +958,8 @@
|
||||||
"name": "Background Image",
|
"name": "Background Image",
|
||||||
"description": "A background image",
|
"description": "A background image",
|
||||||
"icon": "Images",
|
"icon": "Images",
|
||||||
"styles": [
|
"hasChildren": true,
|
||||||
"size"
|
"styles": ["size"],
|
||||||
],
|
|
||||||
"size": {
|
"size": {
|
||||||
"width": 400,
|
"width": 400,
|
||||||
"height": 300
|
"height": 300
|
||||||
|
@ -1162,9 +1117,7 @@
|
||||||
"name": "Nav Bar",
|
"name": "Nav Bar",
|
||||||
"description": "A component for handling the navigation within your app.",
|
"description": "A component for handling the navigation within your app.",
|
||||||
"icon": "BreadcrumbNavigation",
|
"icon": "BreadcrumbNavigation",
|
||||||
"illegalChildren": [
|
"illegalChildren": ["section"],
|
||||||
"section"
|
|
||||||
],
|
|
||||||
"hasChildren": true,
|
"hasChildren": true,
|
||||||
"settings": [
|
"settings": [
|
||||||
{
|
{
|
||||||
|
@ -1365,25 +1318,14 @@
|
||||||
"type": "select",
|
"type": "select",
|
||||||
"label": "Image Width",
|
"label": "Image Width",
|
||||||
"key": "imageWidth",
|
"key": "imageWidth",
|
||||||
"options": [
|
"options": ["auto", "8rem", "12rem", "16rem"],
|
||||||
"auto",
|
|
||||||
"8rem",
|
|
||||||
"12rem",
|
|
||||||
"16rem"
|
|
||||||
],
|
|
||||||
"defaultValue": "8rem"
|
"defaultValue": "8rem"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "select",
|
"type": "select",
|
||||||
"label": "Image Height",
|
"label": "Image Height",
|
||||||
"key": "imageHeight",
|
"key": "imageHeight",
|
||||||
"options": [
|
"options": ["auto", "8rem", "12rem", "16rem", "auto"],
|
||||||
"auto",
|
|
||||||
"8rem",
|
|
||||||
"12rem",
|
|
||||||
"16rem",
|
|
||||||
"auto"
|
|
||||||
],
|
|
||||||
"defaultValue": "auto"
|
"defaultValue": "auto"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
@ -1424,9 +1366,7 @@
|
||||||
"name": "Embed",
|
"name": "Embed",
|
||||||
"icon": "Code",
|
"icon": "Code",
|
||||||
"description": "Embed content from 3rd party sources",
|
"description": "Embed content from 3rd party sources",
|
||||||
"styles": [
|
"styles": ["size"],
|
||||||
"size"
|
|
||||||
],
|
|
||||||
"size": {
|
"size": {
|
||||||
"width": 400,
|
"width": 400,
|
||||||
"height": 100
|
"height": 100
|
||||||
|
@ -1478,11 +1418,7 @@
|
||||||
"type": "select",
|
"type": "select",
|
||||||
"label": "Format",
|
"label": "Format",
|
||||||
"key": "yAxisUnits",
|
"key": "yAxisUnits",
|
||||||
"options": [
|
"options": ["Default", "Thousands", "Millions"],
|
||||||
"Default",
|
|
||||||
"Thousands",
|
|
||||||
"Millions"
|
|
||||||
],
|
|
||||||
"defaultValue": "Default"
|
"defaultValue": "Default"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -1640,11 +1576,7 @@
|
||||||
"type": "select",
|
"type": "select",
|
||||||
"label": "Format",
|
"label": "Format",
|
||||||
"key": "yAxisUnits",
|
"key": "yAxisUnits",
|
||||||
"options": [
|
"options": ["Default", "Thousands", "Millions"],
|
||||||
"Default",
|
|
||||||
"Thousands",
|
|
||||||
"Millions"
|
|
||||||
],
|
|
||||||
"defaultValue": "Default"
|
"defaultValue": "Default"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -1736,11 +1668,7 @@
|
||||||
"type": "select",
|
"type": "select",
|
||||||
"label": "Curve",
|
"label": "Curve",
|
||||||
"key": "curve",
|
"key": "curve",
|
||||||
"options": [
|
"options": ["Smooth", "Straight", "Stepline"],
|
||||||
"Smooth",
|
|
||||||
"Straight",
|
|
||||||
"Stepline"
|
|
||||||
],
|
|
||||||
"defaultValue": "Smooth"
|
"defaultValue": "Smooth"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -1801,11 +1729,7 @@
|
||||||
"type": "select",
|
"type": "select",
|
||||||
"label": "Format",
|
"label": "Format",
|
||||||
"key": "yAxisUnits",
|
"key": "yAxisUnits",
|
||||||
"options": [
|
"options": ["Default", "Thousands", "Millions"],
|
||||||
"Default",
|
|
||||||
"Thousands",
|
|
||||||
"Millions"
|
|
||||||
],
|
|
||||||
"defaultValue": "Default"
|
"defaultValue": "Default"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -1897,11 +1821,7 @@
|
||||||
"type": "select",
|
"type": "select",
|
||||||
"label": "Curve",
|
"label": "Curve",
|
||||||
"key": "curve",
|
"key": "curve",
|
||||||
"options": [
|
"options": ["Smooth", "Straight", "Stepline"],
|
||||||
"Smooth",
|
|
||||||
"Straight",
|
|
||||||
"Stepline"
|
|
||||||
],
|
|
||||||
"defaultValue": "Smooth"
|
"defaultValue": "Smooth"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -2253,11 +2173,7 @@
|
||||||
"type": "select",
|
"type": "select",
|
||||||
"label": "Format",
|
"label": "Format",
|
||||||
"key": "yAxisUnits",
|
"key": "yAxisUnits",
|
||||||
"options": [
|
"options": ["Default", "Thousands", "Millions"],
|
||||||
"Default",
|
|
||||||
"Thousands",
|
|
||||||
"Millions"
|
|
||||||
],
|
|
||||||
"defaultValue": "Default"
|
"defaultValue": "Default"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -2293,19 +2209,14 @@
|
||||||
"name": "Form",
|
"name": "Form",
|
||||||
"icon": "Form",
|
"icon": "Form",
|
||||||
"hasChildren": true,
|
"hasChildren": true,
|
||||||
"illegalChildren": [
|
"illegalChildren": ["section", "form"],
|
||||||
"section",
|
|
||||||
"form"
|
|
||||||
],
|
|
||||||
"actions": [
|
"actions": [
|
||||||
"ValidateForm",
|
"ValidateForm",
|
||||||
"ClearForm",
|
"ClearForm",
|
||||||
"ChangeFormStep",
|
"ChangeFormStep",
|
||||||
"UpdateFieldValue"
|
"UpdateFieldValue"
|
||||||
],
|
],
|
||||||
"styles": [
|
"styles": ["size"],
|
||||||
"size"
|
|
||||||
],
|
|
||||||
"size": {
|
"size": {
|
||||||
"width": 400,
|
"width": 400,
|
||||||
"height": 400
|
"height": 400
|
||||||
|
@ -2315,10 +2226,7 @@
|
||||||
"type": "select",
|
"type": "select",
|
||||||
"label": "Type",
|
"label": "Type",
|
||||||
"key": "actionType",
|
"key": "actionType",
|
||||||
"options": [
|
"options": ["Create", "Update"],
|
||||||
"Create",
|
|
||||||
"Update"
|
|
||||||
],
|
|
||||||
"defaultValue": "Create"
|
"defaultValue": "Create"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -2388,14 +2296,8 @@
|
||||||
"name": "Form Step",
|
"name": "Form Step",
|
||||||
"icon": "AssetsAdded",
|
"icon": "AssetsAdded",
|
||||||
"hasChildren": true,
|
"hasChildren": true,
|
||||||
"illegalChildren": [
|
"illegalChildren": ["section", "form", "form step"],
|
||||||
"section",
|
"styles": ["size"],
|
||||||
"form",
|
|
||||||
"form step"
|
|
||||||
],
|
|
||||||
"styles": [
|
|
||||||
"size"
|
|
||||||
],
|
|
||||||
"size": {
|
"size": {
|
||||||
"width": 400,
|
"width": 400,
|
||||||
"height": 400
|
"height": 400
|
||||||
|
@ -2413,12 +2315,8 @@
|
||||||
"fieldgroup": {
|
"fieldgroup": {
|
||||||
"name": "Field Group",
|
"name": "Field Group",
|
||||||
"icon": "Group",
|
"icon": "Group",
|
||||||
"illegalChildren": [
|
"illegalChildren": ["section"],
|
||||||
"section"
|
"styles": ["size"],
|
||||||
],
|
|
||||||
"styles": [
|
|
||||||
"size"
|
|
||||||
],
|
|
||||||
"hasChildren": true,
|
"hasChildren": true,
|
||||||
"size": {
|
"size": {
|
||||||
"width": 400,
|
"width": 400,
|
||||||
|
@ -2448,12 +2346,9 @@
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"stringfield": {
|
"stringfield": {
|
||||||
"skeleton": false,
|
|
||||||
"name": "Text Field",
|
"name": "Text Field",
|
||||||
"icon": "Text",
|
"icon": "Text",
|
||||||
"styles": [
|
"styles": ["size"],
|
||||||
"size"
|
|
||||||
],
|
|
||||||
"editable": true,
|
"editable": true,
|
||||||
"size": {
|
"size": {
|
||||||
"width": 400,
|
"width": 400,
|
||||||
|
@ -2540,12 +2435,9 @@
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"numberfield": {
|
"numberfield": {
|
||||||
"skeleton": false,
|
|
||||||
"name": "Number Field",
|
"name": "Number Field",
|
||||||
"icon": "123",
|
"icon": "123",
|
||||||
"styles": [
|
"styles": ["size"],
|
||||||
"size"
|
|
||||||
],
|
|
||||||
"editable": true,
|
"editable": true,
|
||||||
"size": {
|
"size": {
|
||||||
"width": 400,
|
"width": 400,
|
||||||
|
@ -2598,12 +2490,9 @@
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"passwordfield": {
|
"passwordfield": {
|
||||||
"skeleton": false,
|
|
||||||
"name": "Password Field",
|
"name": "Password Field",
|
||||||
"icon": "LockClosed",
|
"icon": "LockClosed",
|
||||||
"styles": [
|
"styles": ["size"],
|
||||||
"size"
|
|
||||||
],
|
|
||||||
"editable": true,
|
"editable": true,
|
||||||
"size": {
|
"size": {
|
||||||
"width": 400,
|
"width": 400,
|
||||||
|
@ -2656,12 +2545,9 @@
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"optionsfield": {
|
"optionsfield": {
|
||||||
"skeleton": false,
|
|
||||||
"name": "Options Picker",
|
"name": "Options Picker",
|
||||||
"icon": "Menu",
|
"icon": "Menu",
|
||||||
"styles": [
|
"styles": ["size"],
|
||||||
"size"
|
|
||||||
],
|
|
||||||
"editable": true,
|
"editable": true,
|
||||||
"size": {
|
"size": {
|
||||||
"width": 400,
|
"width": 400,
|
||||||
|
@ -2825,12 +2711,9 @@
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"multifieldselect": {
|
"multifieldselect": {
|
||||||
"skeleton": false,
|
|
||||||
"name": "Multi-select Picker",
|
"name": "Multi-select Picker",
|
||||||
"icon": "ViewList",
|
"icon": "ViewList",
|
||||||
"styles": [
|
"styles": ["size"],
|
||||||
"size"
|
|
||||||
],
|
|
||||||
"editable": true,
|
"editable": true,
|
||||||
"size": {
|
"size": {
|
||||||
"width": 400,
|
"width": 400,
|
||||||
|
@ -2988,7 +2871,6 @@
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"booleanfield": {
|
"booleanfield": {
|
||||||
"skeleton": false,
|
|
||||||
"name": "Checkbox",
|
"name": "Checkbox",
|
||||||
"icon": "SelectBox",
|
"icon": "SelectBox",
|
||||||
"editable": true,
|
"editable": true,
|
||||||
|
@ -3067,12 +2949,9 @@
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"longformfield": {
|
"longformfield": {
|
||||||
"skeleton": false,
|
|
||||||
"name": "Long Form Field",
|
"name": "Long Form Field",
|
||||||
"icon": "TextAlignLeft",
|
"icon": "TextAlignLeft",
|
||||||
"styles": [
|
"styles": ["size"],
|
||||||
"size"
|
|
||||||
],
|
|
||||||
"editable": true,
|
"editable": true,
|
||||||
"size": {
|
"size": {
|
||||||
"width": 400,
|
"width": 400,
|
||||||
|
@ -3147,12 +3026,9 @@
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"datetimefield": {
|
"datetimefield": {
|
||||||
"skeleton": false,
|
|
||||||
"name": "Date Picker",
|
"name": "Date Picker",
|
||||||
"icon": "Date",
|
"icon": "Date",
|
||||||
"styles": [
|
"styles": ["size"],
|
||||||
"size"
|
|
||||||
],
|
|
||||||
"editable": true,
|
"editable": true,
|
||||||
"size": {
|
"size": {
|
||||||
"width": 400,
|
"width": 400,
|
||||||
|
@ -3229,12 +3105,9 @@
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"codescanner": {
|
"codescanner": {
|
||||||
"skeleton": false,
|
|
||||||
"name": "Barcode/QR Scanner",
|
"name": "Barcode/QR Scanner",
|
||||||
"icon": "Camera",
|
"icon": "Camera",
|
||||||
"styles": [
|
"styles": ["size"],
|
||||||
"size"
|
|
||||||
],
|
|
||||||
"size": {
|
"size": {
|
||||||
"width": 400,
|
"width": 400,
|
||||||
"height": 50
|
"height": 50
|
||||||
|
@ -3283,9 +3156,7 @@
|
||||||
"embeddedmap": {
|
"embeddedmap": {
|
||||||
"name": "Embedded Map",
|
"name": "Embedded Map",
|
||||||
"icon": "Location",
|
"icon": "Location",
|
||||||
"styles": [
|
"styles": ["size"],
|
||||||
"size"
|
|
||||||
],
|
|
||||||
"draggable": false,
|
"draggable": false,
|
||||||
"size": {
|
"size": {
|
||||||
"width": 400,
|
"width": 400,
|
||||||
|
@ -3395,12 +3266,9 @@
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"attachmentfield": {
|
"attachmentfield": {
|
||||||
"skeleton": false,
|
|
||||||
"name": "Attachment",
|
"name": "Attachment",
|
||||||
"icon": "Attach",
|
"icon": "Attach",
|
||||||
"styles": [
|
"styles": ["size"],
|
||||||
"size"
|
|
||||||
],
|
|
||||||
"editable": true,
|
"editable": true,
|
||||||
"size": {
|
"size": {
|
||||||
"width": 400,
|
"width": 400,
|
||||||
|
@ -3460,12 +3328,9 @@
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"relationshipfield": {
|
"relationshipfield": {
|
||||||
"skeleton": false,
|
|
||||||
"name": "Relationship Picker",
|
"name": "Relationship Picker",
|
||||||
"icon": "TaskList",
|
"icon": "TaskList",
|
||||||
"styles": [
|
"styles": ["size"],
|
||||||
"size"
|
|
||||||
],
|
|
||||||
"editable": true,
|
"editable": true,
|
||||||
"size": {
|
"size": {
|
||||||
"width": 400,
|
"width": 400,
|
||||||
|
@ -3524,12 +3389,9 @@
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"jsonfield": {
|
"jsonfield": {
|
||||||
"skeleton": false,
|
|
||||||
"name": "JSON Field",
|
"name": "JSON Field",
|
||||||
"icon": "Brackets",
|
"icon": "Brackets",
|
||||||
"styles": [
|
"styles": ["size"],
|
||||||
"size"
|
|
||||||
],
|
|
||||||
"editable": true,
|
"editable": true,
|
||||||
"size": {
|
"size": {
|
||||||
"width": 400,
|
"width": 400,
|
||||||
|
@ -3579,9 +3441,7 @@
|
||||||
"s3upload": {
|
"s3upload": {
|
||||||
"name": "S3 File Upload",
|
"name": "S3 File Upload",
|
||||||
"icon": "UploadToCloud",
|
"icon": "UploadToCloud",
|
||||||
"styles": [
|
"styles": ["size"],
|
||||||
"size"
|
|
||||||
],
|
|
||||||
"editable": true,
|
"editable": true,
|
||||||
"size": {
|
"size": {
|
||||||
"width": 400,
|
"width": 400,
|
||||||
|
@ -3642,13 +3502,9 @@
|
||||||
"dataprovider": {
|
"dataprovider": {
|
||||||
"name": "Data Provider",
|
"name": "Data Provider",
|
||||||
"icon": "Data",
|
"icon": "Data",
|
||||||
"illegalChildren": [
|
"illegalChildren": ["section"],
|
||||||
"section"
|
|
||||||
],
|
|
||||||
"hasChildren": true,
|
"hasChildren": true,
|
||||||
"actions": [
|
"actions": ["RefreshDatasource"],
|
||||||
"RefreshDatasource"
|
|
||||||
],
|
|
||||||
"size": {
|
"size": {
|
||||||
"width": 400,
|
"width": 400,
|
||||||
"height": 100
|
"height": 100
|
||||||
|
@ -3674,10 +3530,7 @@
|
||||||
"type": "select",
|
"type": "select",
|
||||||
"label": "Sort Order",
|
"label": "Sort Order",
|
||||||
"key": "sortOrder",
|
"key": "sortOrder",
|
||||||
"options": [
|
"options": ["Ascending", "Descending"],
|
||||||
"Ascending",
|
|
||||||
"Descending"
|
|
||||||
],
|
|
||||||
"defaultValue": "Ascending"
|
"defaultValue": "Ascending"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -3726,12 +3579,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"table": {
|
"table": {
|
||||||
"skeleton": false,
|
|
||||||
"name": "Table",
|
"name": "Table",
|
||||||
"icon": "Table",
|
"icon": "Table",
|
||||||
"illegalChildren": [
|
"illegalChildren": ["section"],
|
||||||
"section"
|
|
||||||
],
|
|
||||||
"hasChildren": true,
|
"hasChildren": true,
|
||||||
"showEmptyState": false,
|
"showEmptyState": false,
|
||||||
"size": {
|
"size": {
|
||||||
|
@ -3815,9 +3665,7 @@
|
||||||
"daterangepicker": {
|
"daterangepicker": {
|
||||||
"name": "Date Range",
|
"name": "Date Range",
|
||||||
"icon": "Calendar",
|
"icon": "Calendar",
|
||||||
"styles": [
|
"styles": ["size"],
|
||||||
"size"
|
|
||||||
],
|
|
||||||
"hasChildren": false,
|
"hasChildren": false,
|
||||||
"size": {
|
"size": {
|
||||||
"width": 200,
|
"width": 200,
|
||||||
|
@ -3856,9 +3704,7 @@
|
||||||
"spectrumcard": {
|
"spectrumcard": {
|
||||||
"name": "Card",
|
"name": "Card",
|
||||||
"icon": "PersonalizationField",
|
"icon": "PersonalizationField",
|
||||||
"styles": [
|
"styles": ["size"],
|
||||||
"size"
|
|
||||||
],
|
|
||||||
"size": {
|
"size": {
|
||||||
"width": 300,
|
"width": 300,
|
||||||
"height": 120
|
"height": 120
|
||||||
|
@ -4031,10 +3877,7 @@
|
||||||
"type": "select",
|
"type": "select",
|
||||||
"label": "Sort Order",
|
"label": "Sort Order",
|
||||||
"key": "sortOrder",
|
"key": "sortOrder",
|
||||||
"options": [
|
"options": ["Ascending", "Descending"],
|
||||||
"Ascending",
|
|
||||||
"Descending"
|
|
||||||
],
|
|
||||||
"defaultValue": "Ascending"
|
"defaultValue": "Ascending"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -4213,11 +4056,7 @@
|
||||||
"type": "select",
|
"type": "select",
|
||||||
"label": "Format",
|
"label": "Format",
|
||||||
"key": "yAxisUnits",
|
"key": "yAxisUnits",
|
||||||
"options": [
|
"options": ["Default", "Thousands", "Millions"],
|
||||||
"Default",
|
|
||||||
"Thousands",
|
|
||||||
"Millions"
|
|
||||||
],
|
|
||||||
"defaultValue": "Default"
|
"defaultValue": "Default"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -4271,11 +4110,7 @@
|
||||||
"type": "select",
|
"type": "select",
|
||||||
"label": "Format",
|
"label": "Format",
|
||||||
"key": "yAxisUnits",
|
"key": "yAxisUnits",
|
||||||
"options": [
|
"options": ["Default", "Thousands", "Millions"],
|
||||||
"Default",
|
|
||||||
"Thousands",
|
|
||||||
"Millions"
|
|
||||||
],
|
|
||||||
"defaultValue": "Default"
|
"defaultValue": "Default"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -4292,11 +4127,7 @@
|
||||||
"type": "select",
|
"type": "select",
|
||||||
"label": "Curve",
|
"label": "Curve",
|
||||||
"key": "curve",
|
"key": "curve",
|
||||||
"options": [
|
"options": ["Smooth", "Straight", "Stepline"],
|
||||||
"Smooth",
|
|
||||||
"Straight",
|
|
||||||
"Stepline"
|
|
||||||
],
|
|
||||||
"defaultValue": "Smooth"
|
"defaultValue": "Smooth"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
@ -4328,11 +4159,7 @@
|
||||||
"type": "select",
|
"type": "select",
|
||||||
"label": "Format",
|
"label": "Format",
|
||||||
"key": "yAxisUnits",
|
"key": "yAxisUnits",
|
||||||
"options": [
|
"options": ["Default", "Thousands", "Millions"],
|
||||||
"Default",
|
|
||||||
"Thousands",
|
|
||||||
"Millions"
|
|
||||||
],
|
|
||||||
"defaultValue": "Default"
|
"defaultValue": "Default"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -4349,11 +4176,7 @@
|
||||||
"type": "select",
|
"type": "select",
|
||||||
"label": "Curve",
|
"label": "Curve",
|
||||||
"key": "curve",
|
"key": "curve",
|
||||||
"options": [
|
"options": ["Smooth", "Straight", "Stepline"],
|
||||||
"Smooth",
|
|
||||||
"Straight",
|
|
||||||
"Stepline"
|
|
||||||
],
|
|
||||||
"defaultValue": "Smooth"
|
"defaultValue": "Smooth"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -4418,11 +4241,7 @@
|
||||||
"type": "select",
|
"type": "select",
|
||||||
"label": "Format",
|
"label": "Format",
|
||||||
"key": "yAxisUnits",
|
"key": "yAxisUnits",
|
||||||
"options": [
|
"options": ["Default", "Thousands", "Millions"],
|
||||||
"Default",
|
|
||||||
"Thousands",
|
|
||||||
"Millions"
|
|
||||||
],
|
|
||||||
"defaultValue": "Default"
|
"defaultValue": "Default"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -4443,9 +4262,7 @@
|
||||||
"block": true,
|
"block": true,
|
||||||
"name": "Table block",
|
"name": "Table block",
|
||||||
"icon": "Table",
|
"icon": "Table",
|
||||||
"styles": [
|
"styles": ["size"],
|
||||||
"size"
|
|
||||||
],
|
|
||||||
"size": {
|
"size": {
|
||||||
"width": 600,
|
"width": 600,
|
||||||
"height": 400
|
"height": 400
|
||||||
|
@ -4483,10 +4300,7 @@
|
||||||
"type": "select",
|
"type": "select",
|
||||||
"label": "Sort Order",
|
"label": "Sort Order",
|
||||||
"key": "sortOrder",
|
"key": "sortOrder",
|
||||||
"options": [
|
"options": ["Ascending", "Descending"],
|
||||||
"Ascending",
|
|
||||||
"Descending"
|
|
||||||
],
|
|
||||||
"defaultValue": "Ascending"
|
"defaultValue": "Ascending"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -4638,9 +4452,7 @@
|
||||||
"block": true,
|
"block": true,
|
||||||
"name": "Cards block",
|
"name": "Cards block",
|
||||||
"icon": "PersonalizationField",
|
"icon": "PersonalizationField",
|
||||||
"styles": [
|
"styles": ["size"],
|
||||||
"size"
|
|
||||||
],
|
|
||||||
"size": {
|
"size": {
|
||||||
"width": 600,
|
"width": 600,
|
||||||
"height": 400
|
"height": 400
|
||||||
|
@ -4679,10 +4491,7 @@
|
||||||
"type": "select",
|
"type": "select",
|
||||||
"label": "Sort Order",
|
"label": "Sort Order",
|
||||||
"key": "sortOrder",
|
"key": "sortOrder",
|
||||||
"options": [
|
"options": ["Ascending", "Descending"],
|
||||||
"Ascending",
|
|
||||||
"Descending"
|
|
||||||
],
|
|
||||||
"defaultValue": "Descending"
|
"defaultValue": "Descending"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -4816,9 +4625,7 @@
|
||||||
"block": true,
|
"block": true,
|
||||||
"name": "Repeater block",
|
"name": "Repeater block",
|
||||||
"icon": "ViewList",
|
"icon": "ViewList",
|
||||||
"illegalChildren": [
|
"illegalChildren": ["section"],
|
||||||
"section"
|
|
||||||
],
|
|
||||||
"hasChildren": true,
|
"hasChildren": true,
|
||||||
"size": {
|
"size": {
|
||||||
"width": 400,
|
"width": 400,
|
||||||
|
@ -4846,10 +4653,7 @@
|
||||||
"type": "select",
|
"type": "select",
|
||||||
"label": "Sort Order",
|
"label": "Sort Order",
|
||||||
"key": "sortOrder",
|
"key": "sortOrder",
|
||||||
"options": [
|
"options": ["Ascending", "Descending"],
|
||||||
"Ascending",
|
|
||||||
"Descending"
|
|
||||||
],
|
|
||||||
"defaultValue": "Descending"
|
"defaultValue": "Descending"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -5044,9 +4848,7 @@
|
||||||
"markdownviewer": {
|
"markdownviewer": {
|
||||||
"name": "Markdown Viewer",
|
"name": "Markdown Viewer",
|
||||||
"icon": "Preview",
|
"icon": "Preview",
|
||||||
"styles": [
|
"styles": ["size"],
|
||||||
"size"
|
|
||||||
],
|
|
||||||
"size": {
|
"size": {
|
||||||
"width": 400,
|
"width": 400,
|
||||||
"height": 100
|
"height": 100
|
||||||
|
@ -5063,9 +4865,7 @@
|
||||||
"formblock": {
|
"formblock": {
|
||||||
"name": "Form Block",
|
"name": "Form Block",
|
||||||
"icon": "Form",
|
"icon": "Form",
|
||||||
"styles": [
|
"styles": ["size"],
|
||||||
"size"
|
|
||||||
],
|
|
||||||
"block": true,
|
"block": true,
|
||||||
"info": "Form blocks are only compatible with internal or SQL tables",
|
"info": "Form blocks are only compatible with internal or SQL tables",
|
||||||
"size": {
|
"size": {
|
||||||
|
@ -5077,11 +4877,7 @@
|
||||||
"type": "select",
|
"type": "select",
|
||||||
"label": "Type",
|
"label": "Type",
|
||||||
"key": "actionType",
|
"key": "actionType",
|
||||||
"options": [
|
"options": ["Create", "Update", "View"],
|
||||||
"Create",
|
|
||||||
"Update",
|
|
||||||
"View"
|
|
||||||
],
|
|
||||||
"defaultValue": "Create"
|
"defaultValue": "Create"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -5215,10 +5011,7 @@
|
||||||
"name": "Side Panel",
|
"name": "Side Panel",
|
||||||
"icon": "RailRight",
|
"icon": "RailRight",
|
||||||
"hasChildren": true,
|
"hasChildren": true,
|
||||||
"illegalChildren": [
|
"illegalChildren": ["section", "sidepanel"],
|
||||||
"section",
|
|
||||||
"sidepanel"
|
|
||||||
],
|
|
||||||
"showEmptyState": false,
|
"showEmptyState": false,
|
||||||
"draggable": false,
|
"draggable": false,
|
||||||
"info": "Side panels are hidden by default. They will only be revealed when triggered by the 'Open Side Panel' action."
|
"info": "Side panels are hidden by default. They will only be revealed when triggered by the 'Open Side Panel' action."
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@budibase/client",
|
"name": "@budibase/client",
|
||||||
"version": "2.4.12-alpha.3",
|
"version": "2.4.27-alpha.9",
|
||||||
"license": "MPL-2.0",
|
"license": "MPL-2.0",
|
||||||
"module": "dist/budibase-client.js",
|
"module": "dist/budibase-client.js",
|
||||||
"main": "dist/budibase-client.js",
|
"main": "dist/budibase-client.js",
|
||||||
|
@ -19,11 +19,11 @@
|
||||||
"dev:builder": "rollup -cw"
|
"dev:builder": "rollup -cw"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@budibase/bbui": "2.4.12-alpha.3",
|
"@budibase/bbui": "2.4.27-alpha.9",
|
||||||
"@budibase/frontend-core": "2.4.12-alpha.3",
|
"@budibase/frontend-core": "2.4.27-alpha.9",
|
||||||
"@budibase/shared-core": "2.4.12-alpha.3",
|
"@budibase/shared-core": "2.4.27-alpha.9",
|
||||||
"@budibase/string-templates": "2.4.12-alpha.3",
|
"@budibase/string-templates": "2.4.27-alpha.9",
|
||||||
"@budibase/types": "2.4.12-alpha.3",
|
"@budibase/types": "2.4.27-alpha.9",
|
||||||
"@spectrum-css/button": "^3.0.3",
|
"@spectrum-css/button": "^3.0.3",
|
||||||
"@spectrum-css/card": "^3.0.3",
|
"@spectrum-css/card": "^3.0.3",
|
||||||
"@spectrum-css/divider": "^1.0.3",
|
"@spectrum-css/divider": "^1.0.3",
|
||||||
|
|
|
@ -24,7 +24,6 @@
|
||||||
// to render this part of the block, taking advantage of binding enrichment
|
// to render this part of the block, taking advantage of binding enrichment
|
||||||
$: id = `${block.id}-${context ?? rand}`
|
$: id = `${block.id}-${context ?? rand}`
|
||||||
$: instance = {
|
$: instance = {
|
||||||
_blockElementHasChildren: $$slots?.default ?? false,
|
|
||||||
_component: `@budibase/standard-components/${type}`,
|
_component: `@budibase/standard-components/${type}`,
|
||||||
_id: id,
|
_id: id,
|
||||||
_instanceName: name || type[0].toUpperCase() + type.slice(1),
|
_instanceName: name || type[0].toUpperCase() + type.slice(1),
|
||||||
|
|
|
@ -29,7 +29,6 @@
|
||||||
import Placeholder from "components/app/Placeholder.svelte"
|
import Placeholder from "components/app/Placeholder.svelte"
|
||||||
import ScreenPlaceholder from "components/app/ScreenPlaceholder.svelte"
|
import ScreenPlaceholder from "components/app/ScreenPlaceholder.svelte"
|
||||||
import ComponentPlaceholder from "components/app/ComponentPlaceholder.svelte"
|
import ComponentPlaceholder from "components/app/ComponentPlaceholder.svelte"
|
||||||
import Skeleton from "components/app/Skeleton.svelte"
|
|
||||||
|
|
||||||
export let instance = {}
|
export let instance = {}
|
||||||
export let isLayout = false
|
export let isLayout = false
|
||||||
|
@ -39,7 +38,6 @@
|
||||||
|
|
||||||
// Get parent contexts
|
// Get parent contexts
|
||||||
const context = getContext("context")
|
const context = getContext("context")
|
||||||
const loading = getContext("loading")
|
|
||||||
const insideScreenslot = !!getContext("screenslot")
|
const insideScreenslot = !!getContext("screenslot")
|
||||||
|
|
||||||
// Create component context
|
// Create component context
|
||||||
|
@ -172,15 +170,6 @@
|
||||||
$: pad = pad || (interactive && hasChildren && inDndPath)
|
$: pad = pad || (interactive && hasChildren && inDndPath)
|
||||||
$: $dndIsDragging, (pad = false)
|
$: $dndIsDragging, (pad = false)
|
||||||
|
|
||||||
// Determine whether we should render a skeleton loader for this component
|
|
||||||
$: showSkeleton =
|
|
||||||
$loading &&
|
|
||||||
definition?.name !== "Screenslot" &&
|
|
||||||
children.length === 0 &&
|
|
||||||
!instance._blockElementHasChildren &&
|
|
||||||
!definition?.block &&
|
|
||||||
definition?.skeleton !== false
|
|
||||||
|
|
||||||
// Update component context
|
// Update component context
|
||||||
$: store.set({
|
$: store.set({
|
||||||
id,
|
id,
|
||||||
|
@ -507,12 +496,7 @@
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if showSkeleton}
|
{#if constructor && initialSettings && (visible || inSelectedPath) && !builderHidden}
|
||||||
<Skeleton
|
|
||||||
height={initialSettings?.height || definition?.size?.height || 0}
|
|
||||||
width={initialSettings?.width || definition?.size?.width || 0}
|
|
||||||
/>
|
|
||||||
{:else if constructor && initialSettings && (visible || inSelectedPath) && !builderHidden}
|
|
||||||
<!-- The ID is used as a class because getElementsByClassName is O(1) -->
|
<!-- The ID is used as a class because getElementsByClassName is O(1) -->
|
||||||
<!-- and the performance matters for the selection indicators -->
|
<!-- and the performance matters for the selection indicators -->
|
||||||
<div
|
<div
|
||||||
|
@ -530,23 +514,25 @@
|
||||||
data-icon={icon}
|
data-icon={icon}
|
||||||
data-parent={parent}
|
data-parent={parent}
|
||||||
>
|
>
|
||||||
<svelte:component this={constructor} bind:this={ref} {...initialSettings}>
|
{#if hasMissingRequiredSettings}
|
||||||
{#if hasMissingRequiredSettings}
|
<ComponentPlaceholder />
|
||||||
<ComponentPlaceholder />
|
{:else}
|
||||||
{:else if children.length}
|
<svelte:component this={constructor} bind:this={ref} {...initialSettings}>
|
||||||
{#each children as child (child._id)}
|
{#if children.length}
|
||||||
<svelte:self instance={child} parent={id} />
|
{#each children as child (child._id)}
|
||||||
{/each}
|
<svelte:self instance={child} parent={id} />
|
||||||
{:else if emptyState}
|
{/each}
|
||||||
{#if isScreen}
|
{:else if emptyState}
|
||||||
<ScreenPlaceholder />
|
{#if isScreen}
|
||||||
{:else}
|
<ScreenPlaceholder />
|
||||||
<Placeholder />
|
{:else}
|
||||||
|
<Placeholder />
|
||||||
|
{/if}
|
||||||
|
{:else if isBlock}
|
||||||
|
<slot />
|
||||||
{/if}
|
{/if}
|
||||||
{:else if isBlock}
|
</svelte:component>
|
||||||
<slot />
|
{/if}
|
||||||
{/if}
|
|
||||||
</svelte:component>
|
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
<script>
|
<script>
|
||||||
import { writable } from "svelte/store"
|
|
||||||
import { setContext, getContext, onMount } from "svelte"
|
import { setContext, getContext, onMount } from "svelte"
|
||||||
import Router, { querystring } from "svelte-spa-router"
|
import Router, { querystring } from "svelte-spa-router"
|
||||||
import { routeStore, stateStore } from "stores"
|
import { routeStore, stateStore } from "stores"
|
||||||
|
@ -10,9 +9,6 @@
|
||||||
const component = getContext("component")
|
const component = getContext("component")
|
||||||
setContext("screenslot", true)
|
setContext("screenslot", true)
|
||||||
|
|
||||||
const loading = writable(false)
|
|
||||||
setContext("loading", loading)
|
|
||||||
|
|
||||||
// Only wrap this as an array to take advantage of svelte keying,
|
// Only wrap this as an array to take advantage of svelte keying,
|
||||||
// to ensure the svelte-spa-router is fully remounted when route config
|
// to ensure the svelte-spa-router is fully remounted when route config
|
||||||
// changes
|
// changes
|
||||||
|
|
|
@ -21,7 +21,9 @@
|
||||||
|
|
||||||
{#if url}
|
{#if url}
|
||||||
<div class="outer" use:styleable={$component.styles}>
|
<div class="outer" use:styleable={$component.styles}>
|
||||||
<div class="inner" {style} />
|
<div class="inner" {style}>
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{:else if $builderStore.inBuilder}
|
{:else if $builderStore.inBuilder}
|
||||||
<div
|
<div
|
||||||
|
|
|
@ -3,12 +3,13 @@
|
||||||
import { builderStore } from "stores"
|
import { builderStore } from "stores"
|
||||||
|
|
||||||
const component = getContext("component")
|
const component = getContext("component")
|
||||||
|
const { styleable } = getContext("sdk")
|
||||||
|
|
||||||
$: requiredSetting = $component.missingRequiredSettings?.[0]
|
$: requiredSetting = $component.missingRequiredSettings?.[0]
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if $builderStore.inBuilder && requiredSetting}
|
{#if $builderStore.inBuilder && requiredSetting}
|
||||||
<div class="component-placeholder">
|
<div class="component-placeholder" use:styleable={$component.styles}>
|
||||||
<span>
|
<span>
|
||||||
Add the <mark>{requiredSetting.label}</mark> setting to start using your component
|
Add the <mark>{requiredSetting.label}</mark> setting to start using your component
|
||||||
-
|
-
|
||||||
|
@ -32,7 +33,7 @@
|
||||||
}
|
}
|
||||||
.component-placeholder mark {
|
.component-placeholder mark {
|
||||||
background-color: var(--spectrum-global-color-gray-400);
|
background-color: var(--spectrum-global-color-gray-400);
|
||||||
padding: 0 2px;
|
padding: 0 4px;
|
||||||
border-radius: 2px;
|
border-radius: 2px;
|
||||||
}
|
}
|
||||||
.component-placeholder .spectrum-Link {
|
.component-placeholder .spectrum-Link {
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
<script>
|
<script>
|
||||||
import { writable } from "svelte/store"
|
import { getContext } from "svelte"
|
||||||
import { setContext, getContext } from "svelte"
|
import { Pagination, ProgressCircle } from "@budibase/bbui"
|
||||||
import { Pagination } from "@budibase/bbui"
|
|
||||||
import { fetchData, LuceneUtils } from "@budibase/frontend-core"
|
import { fetchData, LuceneUtils } from "@budibase/frontend-core"
|
||||||
|
|
||||||
export let dataSource
|
export let dataSource
|
||||||
|
@ -14,11 +13,6 @@
|
||||||
const { styleable, Provider, ActionTypes, API } = getContext("sdk")
|
const { styleable, Provider, ActionTypes, API } = getContext("sdk")
|
||||||
const component = getContext("component")
|
const component = getContext("component")
|
||||||
|
|
||||||
// Update loading state
|
|
||||||
const parentLoading = getContext("loading")
|
|
||||||
const loading = writable(true)
|
|
||||||
setContext("loading", loading)
|
|
||||||
|
|
||||||
// We need to manage our lucene query manually as we want to allow components
|
// We need to manage our lucene query manually as we want to allow components
|
||||||
// to extend it
|
// to extend it
|
||||||
let queryExtensions = {}
|
let queryExtensions = {}
|
||||||
|
@ -26,8 +20,8 @@
|
||||||
$: query = extendQuery(defaultQuery, queryExtensions)
|
$: query = extendQuery(defaultQuery, queryExtensions)
|
||||||
|
|
||||||
// Fetch data and refresh when needed
|
// Fetch data and refresh when needed
|
||||||
$: fetch = createFetch(dataSource, $parentLoading)
|
$: fetch = createFetch(dataSource)
|
||||||
$: updateFetch({
|
$: fetch.update({
|
||||||
query,
|
query,
|
||||||
sortColumn,
|
sortColumn,
|
||||||
sortOrder,
|
sortOrder,
|
||||||
|
@ -35,9 +29,6 @@
|
||||||
paginate,
|
paginate,
|
||||||
})
|
})
|
||||||
|
|
||||||
// Keep loading context updated
|
|
||||||
$: loading.set($parentLoading || !$fetch.loaded)
|
|
||||||
|
|
||||||
// Build our action context
|
// Build our action context
|
||||||
$: actions = [
|
$: actions = [
|
||||||
{
|
{
|
||||||
|
@ -89,18 +80,7 @@
|
||||||
limit,
|
limit,
|
||||||
}
|
}
|
||||||
|
|
||||||
const createFetch = (datasource, parentLoading) => {
|
const createFetch = datasource => {
|
||||||
// Return a dummy fetch if parent is still loading. We do this so that we
|
|
||||||
// can still properly subscribe to a valid fetch object and check all
|
|
||||||
// properties, but we want to avoid fetching the real data until all parents
|
|
||||||
// have finished loading.
|
|
||||||
// This logic is only needed due to skeleton loaders, as previously we
|
|
||||||
// simply blocked component rendering until data was ready.
|
|
||||||
if (parentLoading) {
|
|
||||||
return fetchData({ API })
|
|
||||||
}
|
|
||||||
|
|
||||||
// Otherwise return the real thing
|
|
||||||
return fetchData({
|
return fetchData({
|
||||||
API,
|
API,
|
||||||
datasource,
|
datasource,
|
||||||
|
@ -114,14 +94,6 @@
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const updateFetch = opts => {
|
|
||||||
// Only update fetch if parents have stopped loading. Otherwise we will
|
|
||||||
// trigger a fetch of the real data before parents are ready.
|
|
||||||
if (!$parentLoading) {
|
|
||||||
fetch.update(opts)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const addQueryExtension = (key, extension) => {
|
const addQueryExtension = (key, extension) => {
|
||||||
if (!key || !extension) {
|
if (!key || !extension) {
|
||||||
return
|
return
|
||||||
|
@ -155,17 +127,23 @@
|
||||||
|
|
||||||
<div use:styleable={$component.styles} class="container">
|
<div use:styleable={$component.styles} class="container">
|
||||||
<Provider {actions} data={dataContext}>
|
<Provider {actions} data={dataContext}>
|
||||||
<slot />
|
{#if !$fetch.loaded}
|
||||||
{#if paginate && $fetch.supportsPagination}
|
<div class="loading">
|
||||||
<div class="pagination">
|
<ProgressCircle />
|
||||||
<Pagination
|
|
||||||
page={$fetch.pageNumber + 1}
|
|
||||||
hasPrevPage={$fetch.hasPrevPage}
|
|
||||||
hasNextPage={$fetch.hasNextPage}
|
|
||||||
goToPrevPage={fetch.prevPage}
|
|
||||||
goToNextPage={fetch.nextPage}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
|
{:else}
|
||||||
|
<slot />
|
||||||
|
{#if paginate && $fetch.supportsPagination}
|
||||||
|
<div class="pagination">
|
||||||
|
<Pagination
|
||||||
|
page={$fetch.pageNumber + 1}
|
||||||
|
hasPrevPage={$fetch.hasPrevPage}
|
||||||
|
hasNextPage={$fetch.hasNextPage}
|
||||||
|
goToPrevPage={fetch.prevPage}
|
||||||
|
goToNextPage={fetch.nextPage}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
{/if}
|
{/if}
|
||||||
</Provider>
|
</Provider>
|
||||||
</div>
|
</div>
|
||||||
|
@ -177,6 +155,13 @@
|
||||||
justify-content: flex-start;
|
justify-content: flex-start;
|
||||||
align-items: stretch;
|
align-items: stretch;
|
||||||
}
|
}
|
||||||
|
.loading {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
height: 100px;
|
||||||
|
}
|
||||||
.pagination {
|
.pagination {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
|
|
|
@ -12,25 +12,22 @@
|
||||||
|
|
||||||
const { Provider } = getContext("sdk")
|
const { Provider } = getContext("sdk")
|
||||||
const component = getContext("component")
|
const component = getContext("component")
|
||||||
const loading = getContext("loading")
|
|
||||||
|
|
||||||
// If the parent DataProvider is loading, fill the rows array with a number of empty objects corresponding to the DataProvider's page size; this allows skeleton loader components to be rendered further down the tree.
|
$: rows = dataProvider?.rows ?? []
|
||||||
$: rows = $loading
|
$: loaded = dataProvider?.loaded ?? true
|
||||||
? new Array(dataProvider.limit > 20 ? 20 : dataProvider.limit).fill({})
|
|
||||||
: dataProvider?.rows
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Container {direction} {hAlign} {vAlign} {gap} wrap>
|
<Container {direction} {hAlign} {vAlign} {gap} wrap>
|
||||||
{#if $component.empty}
|
{#if $component.empty}
|
||||||
<Placeholder />
|
<Placeholder />
|
||||||
{:else if !$loading && rows.length === 0}
|
{:else if rows.length > 0}
|
||||||
<div class="noRows"><i class="ri-list-check-2" />{noRowsMessage}</div>
|
|
||||||
{:else}
|
|
||||||
{#each rows as row, index}
|
{#each rows as row, index}
|
||||||
<Provider data={{ ...row, index }}>
|
<Provider data={{ ...row, index }}>
|
||||||
<slot />
|
<slot />
|
||||||
</Provider>
|
</Provider>
|
||||||
{/each}
|
{/each}
|
||||||
|
{:else if loaded && noRowsMessage}
|
||||||
|
<div class="noRows"><i class="ri-list-check-2" />{noRowsMessage}</div>
|
||||||
{/if}
|
{/if}
|
||||||
</Container>
|
</Container>
|
||||||
|
|
||||||
|
|
|
@ -1,31 +0,0 @@
|
||||||
<script>
|
|
||||||
import { getContext } from "svelte"
|
|
||||||
import { Skeleton } from "@budibase/bbui"
|
|
||||||
|
|
||||||
const { styleable } = getContext("sdk")
|
|
||||||
const component = getContext("component")
|
|
||||||
|
|
||||||
export let height
|
|
||||||
export let width
|
|
||||||
|
|
||||||
let styles
|
|
||||||
|
|
||||||
$: {
|
|
||||||
styles = JSON.parse(JSON.stringify($component.styles))
|
|
||||||
|
|
||||||
if (!styles.normal.height && height) {
|
|
||||||
// The height and width props provided to this component can either be numbers or strings set by users (ex. '100%', '100px', '100'). A string of '100' wouldn't be a valid CSS property, but some of our components respect that input, so we need to handle it here also, hence the `!isNaN` check.
|
|
||||||
styles.normal.height = !isNaN(height) ? `${height}px` : height
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!styles.normal.width && width) {
|
|
||||||
styles.normal.width = !isNaN(width) ? `${width}px` : width
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<div use:styleable={styles}>
|
|
||||||
<Skeleton>
|
|
||||||
<slot />
|
|
||||||
</Skeleton>
|
|
||||||
</div>
|
|
|
@ -37,6 +37,7 @@
|
||||||
let repeaterId
|
let repeaterId
|
||||||
let schema
|
let schema
|
||||||
let enrichedSearchColumns
|
let enrichedSearchColumns
|
||||||
|
let schemaLoaded = false
|
||||||
|
|
||||||
$: fetchSchema(dataSource)
|
$: fetchSchema(dataSource)
|
||||||
$: enrichSearchColumns(searchColumns, schema).then(
|
$: enrichSearchColumns(searchColumns, schema).then(
|
||||||
|
@ -77,135 +78,138 @@
|
||||||
enrichRelationships: true,
|
enrichRelationships: true,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
schemaLoaded = true
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Block>
|
{#if schemaLoaded}
|
||||||
<BlockComponent
|
<Block>
|
||||||
type="form"
|
<BlockComponent
|
||||||
bind:id={formId}
|
type="form"
|
||||||
props={{ dataSource, disableValidation: true }}
|
bind:id={formId}
|
||||||
>
|
props={{ dataSource, disableValidation: true }}
|
||||||
{#if title || enrichedSearchColumns?.length || showTitleButton}
|
>
|
||||||
<BlockComponent
|
{#if title || enrichedSearchColumns?.length || showTitleButton}
|
||||||
type="container"
|
|
||||||
props={{
|
|
||||||
direction: "row",
|
|
||||||
hAlign: "stretch",
|
|
||||||
vAlign: "middle",
|
|
||||||
gap: "M",
|
|
||||||
wrap: true,
|
|
||||||
}}
|
|
||||||
styles={{
|
|
||||||
normal: {
|
|
||||||
"margin-bottom": "20px",
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
order={0}
|
|
||||||
>
|
|
||||||
<BlockComponent
|
|
||||||
type="heading"
|
|
||||||
props={{
|
|
||||||
text: title,
|
|
||||||
}}
|
|
||||||
order={0}
|
|
||||||
/>
|
|
||||||
<BlockComponent
|
<BlockComponent
|
||||||
type="container"
|
type="container"
|
||||||
props={{
|
props={{
|
||||||
direction: "row",
|
direction: "row",
|
||||||
hAlign: "left",
|
hAlign: "stretch",
|
||||||
vAlign: "middle",
|
vAlign: "middle",
|
||||||
gap: "M",
|
gap: "M",
|
||||||
wrap: true,
|
wrap: true,
|
||||||
}}
|
}}
|
||||||
order={1}
|
|
||||||
>
|
|
||||||
{#if enrichedSearchColumns?.length}
|
|
||||||
{#each enrichedSearchColumns as column, idx}
|
|
||||||
<BlockComponent
|
|
||||||
type={column.componentType}
|
|
||||||
props={{
|
|
||||||
field: column.name,
|
|
||||||
placeholder: column.name,
|
|
||||||
text: column.name,
|
|
||||||
autoWidth: true,
|
|
||||||
}}
|
|
||||||
order={idx}
|
|
||||||
styles={{
|
|
||||||
normal: {
|
|
||||||
width: "192px",
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
{/each}
|
|
||||||
{/if}
|
|
||||||
{#if showTitleButton}
|
|
||||||
<BlockComponent
|
|
||||||
type="button"
|
|
||||||
props={{
|
|
||||||
onClick: titleButtonAction,
|
|
||||||
text: titleButtonText,
|
|
||||||
type: "cta",
|
|
||||||
}}
|
|
||||||
order={enrichedSearchColumns?.length ?? 0}
|
|
||||||
/>
|
|
||||||
{/if}
|
|
||||||
</BlockComponent>
|
|
||||||
</BlockComponent>
|
|
||||||
{/if}
|
|
||||||
<BlockComponent
|
|
||||||
type="dataprovider"
|
|
||||||
bind:id={dataProviderId}
|
|
||||||
props={{
|
|
||||||
dataSource,
|
|
||||||
filter: enrichedFilter,
|
|
||||||
sortColumn,
|
|
||||||
sortOrder,
|
|
||||||
paginate,
|
|
||||||
limit,
|
|
||||||
}}
|
|
||||||
order={1}
|
|
||||||
>
|
|
||||||
<BlockComponent
|
|
||||||
type="repeater"
|
|
||||||
bind:id={repeaterId}
|
|
||||||
context="repeater"
|
|
||||||
props={{
|
|
||||||
dataProvider: `{{ literal ${safe(dataProviderId)} }}`,
|
|
||||||
direction: "row",
|
|
||||||
hAlign: "stretch",
|
|
||||||
vAlign: "top",
|
|
||||||
gap: "M",
|
|
||||||
noRowsMessage: "No rows found",
|
|
||||||
}}
|
|
||||||
styles={{
|
|
||||||
custom: `display: grid;\ngrid-template-columns: repeat(auto-fill, minmax(min(${cardWidth}px, 100%), 1fr));`,
|
|
||||||
}}
|
|
||||||
order={0}
|
|
||||||
>
|
|
||||||
<BlockComponent
|
|
||||||
type="spectrumcard"
|
|
||||||
props={{
|
|
||||||
title: cardTitle,
|
|
||||||
subtitle: cardSubtitle,
|
|
||||||
description: cardDescription,
|
|
||||||
imageURL: cardImageURL,
|
|
||||||
horizontal: cardHorizontal,
|
|
||||||
showButton: showCardButton,
|
|
||||||
buttonText: cardButtonText,
|
|
||||||
buttonOnClick: cardButtonOnClick,
|
|
||||||
linkURL: fullCardURL,
|
|
||||||
linkPeek: cardPeek,
|
|
||||||
}}
|
|
||||||
styles={{
|
styles={{
|
||||||
normal: {
|
normal: {
|
||||||
width: "auto",
|
"margin-bottom": "20px",
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
order={0}
|
order={0}
|
||||||
/>
|
>
|
||||||
|
<BlockComponent
|
||||||
|
type="heading"
|
||||||
|
props={{
|
||||||
|
text: title,
|
||||||
|
}}
|
||||||
|
order={0}
|
||||||
|
/>
|
||||||
|
<BlockComponent
|
||||||
|
type="container"
|
||||||
|
props={{
|
||||||
|
direction: "row",
|
||||||
|
hAlign: "left",
|
||||||
|
vAlign: "middle",
|
||||||
|
gap: "M",
|
||||||
|
wrap: true,
|
||||||
|
}}
|
||||||
|
order={1}
|
||||||
|
>
|
||||||
|
{#if enrichedSearchColumns?.length}
|
||||||
|
{#each enrichedSearchColumns as column, idx}
|
||||||
|
<BlockComponent
|
||||||
|
type={column.componentType}
|
||||||
|
props={{
|
||||||
|
field: column.name,
|
||||||
|
placeholder: column.name,
|
||||||
|
text: column.name,
|
||||||
|
autoWidth: true,
|
||||||
|
}}
|
||||||
|
order={idx}
|
||||||
|
styles={{
|
||||||
|
normal: {
|
||||||
|
width: "192px",
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{/each}
|
||||||
|
{/if}
|
||||||
|
{#if showTitleButton}
|
||||||
|
<BlockComponent
|
||||||
|
type="button"
|
||||||
|
props={{
|
||||||
|
onClick: titleButtonAction,
|
||||||
|
text: titleButtonText,
|
||||||
|
type: "cta",
|
||||||
|
}}
|
||||||
|
order={enrichedSearchColumns?.length ?? 0}
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
|
</BlockComponent>
|
||||||
|
</BlockComponent>
|
||||||
|
{/if}
|
||||||
|
<BlockComponent
|
||||||
|
type="dataprovider"
|
||||||
|
bind:id={dataProviderId}
|
||||||
|
props={{
|
||||||
|
dataSource,
|
||||||
|
filter: enrichedFilter,
|
||||||
|
sortColumn,
|
||||||
|
sortOrder,
|
||||||
|
paginate,
|
||||||
|
limit,
|
||||||
|
}}
|
||||||
|
order={1}
|
||||||
|
>
|
||||||
|
<BlockComponent
|
||||||
|
type="repeater"
|
||||||
|
bind:id={repeaterId}
|
||||||
|
context="repeater"
|
||||||
|
props={{
|
||||||
|
dataProvider: `{{ literal ${safe(dataProviderId)} }}`,
|
||||||
|
direction: "row",
|
||||||
|
hAlign: "stretch",
|
||||||
|
vAlign: "top",
|
||||||
|
gap: "M",
|
||||||
|
noRowsMessage: "No rows found",
|
||||||
|
}}
|
||||||
|
styles={{
|
||||||
|
custom: `display: grid;\ngrid-template-columns: repeat(auto-fill, minmax(min(${cardWidth}px, 100%), 1fr));`,
|
||||||
|
}}
|
||||||
|
order={0}
|
||||||
|
>
|
||||||
|
<BlockComponent
|
||||||
|
type="spectrumcard"
|
||||||
|
props={{
|
||||||
|
title: cardTitle,
|
||||||
|
subtitle: cardSubtitle,
|
||||||
|
description: cardDescription,
|
||||||
|
imageURL: cardImageURL,
|
||||||
|
horizontal: cardHorizontal,
|
||||||
|
showButton: showCardButton,
|
||||||
|
buttonText: cardButtonText,
|
||||||
|
buttonOnClick: cardButtonOnClick,
|
||||||
|
linkURL: fullCardURL,
|
||||||
|
linkPeek: cardPeek,
|
||||||
|
}}
|
||||||
|
styles={{
|
||||||
|
normal: {
|
||||||
|
width: "auto",
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
order={0}
|
||||||
|
/>
|
||||||
|
</BlockComponent>
|
||||||
</BlockComponent>
|
</BlockComponent>
|
||||||
</BlockComponent>
|
</BlockComponent>
|
||||||
</BlockComponent>
|
</Block>
|
||||||
</Block>
|
{/if}
|
||||||
|
|
|
@ -37,6 +37,7 @@
|
||||||
let schema
|
let schema
|
||||||
let primaryDisplay
|
let primaryDisplay
|
||||||
let enrichedSearchColumns
|
let enrichedSearchColumns
|
||||||
|
let schemaLoaded = false
|
||||||
|
|
||||||
$: fetchSchema(dataSource)
|
$: fetchSchema(dataSource)
|
||||||
$: enrichSearchColumns(searchColumns, schema).then(
|
$: enrichSearchColumns(searchColumns, schema).then(
|
||||||
|
@ -91,6 +92,7 @@
|
||||||
enrichRelationships: true,
|
enrichRelationships: true,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
schemaLoaded = true
|
||||||
}
|
}
|
||||||
|
|
||||||
const getNormalFields = schema => {
|
const getNormalFields = schema => {
|
||||||
|
@ -114,160 +116,162 @@
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Block>
|
{#if schemaLoaded}
|
||||||
<BlockComponent
|
<Block>
|
||||||
type="form"
|
<BlockComponent
|
||||||
bind:id={formId}
|
type="form"
|
||||||
props={{
|
bind:id={formId}
|
||||||
dataSource,
|
props={{
|
||||||
disableValidation: true,
|
dataSource,
|
||||||
editAutoColumns: true,
|
disableValidation: true,
|
||||||
size,
|
editAutoColumns: true,
|
||||||
}}
|
size,
|
||||||
>
|
}}
|
||||||
{#if title || enrichedSearchColumns?.length || showTitleButton}
|
>
|
||||||
<BlockComponent
|
{#if title || enrichedSearchColumns?.length || showTitleButton}
|
||||||
type="container"
|
|
||||||
props={{
|
|
||||||
direction: "row",
|
|
||||||
hAlign: "stretch",
|
|
||||||
vAlign: "middle",
|
|
||||||
gap: "M",
|
|
||||||
wrap: true,
|
|
||||||
}}
|
|
||||||
styles={{
|
|
||||||
normal: {
|
|
||||||
"margin-bottom": "20px",
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
order={0}
|
|
||||||
>
|
|
||||||
<BlockComponent
|
|
||||||
type="heading"
|
|
||||||
props={{
|
|
||||||
text: title,
|
|
||||||
}}
|
|
||||||
order={0}
|
|
||||||
/>
|
|
||||||
<BlockComponent
|
<BlockComponent
|
||||||
type="container"
|
type="container"
|
||||||
props={{
|
props={{
|
||||||
direction: "row",
|
direction: "row",
|
||||||
hAlign: "left",
|
hAlign: "stretch",
|
||||||
vAlign: "center",
|
vAlign: "middle",
|
||||||
gap: "M",
|
gap: "M",
|
||||||
wrap: true,
|
wrap: true,
|
||||||
}}
|
}}
|
||||||
order={1}
|
styles={{
|
||||||
|
normal: {
|
||||||
|
"margin-bottom": "20px",
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
order={0}
|
||||||
>
|
>
|
||||||
{#if enrichedSearchColumns?.length}
|
<BlockComponent
|
||||||
{#each enrichedSearchColumns as column, idx}
|
type="heading"
|
||||||
|
props={{
|
||||||
|
text: title,
|
||||||
|
}}
|
||||||
|
order={0}
|
||||||
|
/>
|
||||||
|
<BlockComponent
|
||||||
|
type="container"
|
||||||
|
props={{
|
||||||
|
direction: "row",
|
||||||
|
hAlign: "left",
|
||||||
|
vAlign: "center",
|
||||||
|
gap: "M",
|
||||||
|
wrap: true,
|
||||||
|
}}
|
||||||
|
order={1}
|
||||||
|
>
|
||||||
|
{#if enrichedSearchColumns?.length}
|
||||||
|
{#each enrichedSearchColumns as column, idx}
|
||||||
|
<BlockComponent
|
||||||
|
type={column.componentType}
|
||||||
|
props={{
|
||||||
|
field: column.name,
|
||||||
|
placeholder: column.name,
|
||||||
|
text: column.name,
|
||||||
|
autoWidth: true,
|
||||||
|
}}
|
||||||
|
styles={{
|
||||||
|
normal: {
|
||||||
|
width: "192px",
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
order={idx}
|
||||||
|
/>
|
||||||
|
{/each}
|
||||||
|
{/if}
|
||||||
|
{#if showTitleButton}
|
||||||
<BlockComponent
|
<BlockComponent
|
||||||
type={column.componentType}
|
type="button"
|
||||||
props={{
|
props={{
|
||||||
field: column.name,
|
onClick: buttonClickActions,
|
||||||
placeholder: column.name,
|
text: titleButtonText,
|
||||||
text: column.name,
|
type: "cta",
|
||||||
autoWidth: true,
|
|
||||||
}}
|
}}
|
||||||
styles={{
|
order={enrichedSearchColumns?.length ?? 0}
|
||||||
normal: {
|
|
||||||
width: "192px",
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
order={idx}
|
|
||||||
/>
|
/>
|
||||||
{/each}
|
{/if}
|
||||||
{/if}
|
</BlockComponent>
|
||||||
{#if showTitleButton}
|
|
||||||
<BlockComponent
|
|
||||||
type="button"
|
|
||||||
props={{
|
|
||||||
onClick: buttonClickActions,
|
|
||||||
text: titleButtonText,
|
|
||||||
type: "cta",
|
|
||||||
}}
|
|
||||||
order={enrichedSearchColumns?.length ?? 0}
|
|
||||||
/>
|
|
||||||
{/if}
|
|
||||||
</BlockComponent>
|
</BlockComponent>
|
||||||
</BlockComponent>
|
{/if}
|
||||||
{/if}
|
|
||||||
<BlockComponent
|
|
||||||
type="dataprovider"
|
|
||||||
bind:id={dataProviderId}
|
|
||||||
props={{
|
|
||||||
dataSource,
|
|
||||||
filter: enrichedFilter,
|
|
||||||
sortColumn: sortColumn || primaryDisplay,
|
|
||||||
sortOrder,
|
|
||||||
paginate,
|
|
||||||
limit: rowCount,
|
|
||||||
}}
|
|
||||||
order={1}
|
|
||||||
>
|
|
||||||
<BlockComponent
|
<BlockComponent
|
||||||
type="table"
|
type="dataprovider"
|
||||||
context="table"
|
bind:id={dataProviderId}
|
||||||
props={{
|
props={{
|
||||||
dataProvider: `{{ literal ${safe(dataProviderId)} }}`,
|
dataSource,
|
||||||
columns: tableColumns,
|
filter: enrichedFilter,
|
||||||
rowCount,
|
sortColumn: sortColumn || primaryDisplay,
|
||||||
quiet,
|
sortOrder,
|
||||||
compact,
|
paginate,
|
||||||
allowSelectRows,
|
limit: rowCount,
|
||||||
size,
|
|
||||||
onClick: rowClickActions,
|
|
||||||
}}
|
}}
|
||||||
/>
|
order={1}
|
||||||
|
>
|
||||||
|
<BlockComponent
|
||||||
|
type="table"
|
||||||
|
context="table"
|
||||||
|
props={{
|
||||||
|
dataProvider: `{{ literal ${safe(dataProviderId)} }}`,
|
||||||
|
columns: tableColumns,
|
||||||
|
rowCount,
|
||||||
|
quiet,
|
||||||
|
compact,
|
||||||
|
allowSelectRows,
|
||||||
|
size,
|
||||||
|
onClick: rowClickActions,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</BlockComponent>
|
||||||
|
{#if clickBehaviour === "details"}
|
||||||
|
<BlockComponent
|
||||||
|
name="Details side panel"
|
||||||
|
type="sidepanel"
|
||||||
|
bind:id={detailsSidePanelId}
|
||||||
|
context="details-side-panel"
|
||||||
|
order={2}
|
||||||
|
>
|
||||||
|
<BlockComponent
|
||||||
|
name="Details form block"
|
||||||
|
type="formblock"
|
||||||
|
bind:id={detailsFormBlockId}
|
||||||
|
props={{
|
||||||
|
dataSource,
|
||||||
|
showSaveButton: true,
|
||||||
|
showDeleteButton: true,
|
||||||
|
actionType: "Update",
|
||||||
|
rowId: `{{ ${safe("state")}.${safe(stateKey)} }}`,
|
||||||
|
fields: normalFields,
|
||||||
|
title: editTitle,
|
||||||
|
labelPosition: "left",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</BlockComponent>
|
||||||
|
{/if}
|
||||||
|
{#if showTitleButton && titleButtonClickBehaviour === "new"}
|
||||||
|
<BlockComponent
|
||||||
|
name="New row side panel"
|
||||||
|
type="sidepanel"
|
||||||
|
bind:id={newRowSidePanelId}
|
||||||
|
context="new-side-panel"
|
||||||
|
order={3}
|
||||||
|
>
|
||||||
|
<BlockComponent
|
||||||
|
name="New row form block"
|
||||||
|
type="formblock"
|
||||||
|
props={{
|
||||||
|
dataSource,
|
||||||
|
showSaveButton: true,
|
||||||
|
showDeleteButton: false,
|
||||||
|
actionType: "Create",
|
||||||
|
fields: normalFields,
|
||||||
|
title: "Create Row",
|
||||||
|
labelPosition: "left",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</BlockComponent>
|
||||||
|
{/if}
|
||||||
</BlockComponent>
|
</BlockComponent>
|
||||||
{#if clickBehaviour === "details"}
|
</Block>
|
||||||
<BlockComponent
|
{/if}
|
||||||
name="Details side panel"
|
|
||||||
type="sidepanel"
|
|
||||||
bind:id={detailsSidePanelId}
|
|
||||||
context="details-side-panel"
|
|
||||||
order={2}
|
|
||||||
>
|
|
||||||
<BlockComponent
|
|
||||||
name="Details form block"
|
|
||||||
type="formblock"
|
|
||||||
bind:id={detailsFormBlockId}
|
|
||||||
props={{
|
|
||||||
dataSource,
|
|
||||||
showSaveButton: true,
|
|
||||||
showDeleteButton: true,
|
|
||||||
actionType: "Update",
|
|
||||||
rowId: `{{ ${safe("state")}.${safe(stateKey)} }}`,
|
|
||||||
fields: normalFields,
|
|
||||||
title: editTitle,
|
|
||||||
labelPosition: "left",
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</BlockComponent>
|
|
||||||
{/if}
|
|
||||||
{#if showTitleButton && titleButtonClickBehaviour === "new"}
|
|
||||||
<BlockComponent
|
|
||||||
name="New row side panel"
|
|
||||||
type="sidepanel"
|
|
||||||
bind:id={newRowSidePanelId}
|
|
||||||
context="new-side-panel"
|
|
||||||
order={3}
|
|
||||||
>
|
|
||||||
<BlockComponent
|
|
||||||
name="New row form block"
|
|
||||||
type="formblock"
|
|
||||||
props={{
|
|
||||||
dataSource,
|
|
||||||
showSaveButton: true,
|
|
||||||
showDeleteButton: false,
|
|
||||||
actionType: "Create",
|
|
||||||
fields: normalFields,
|
|
||||||
title: "Create Row",
|
|
||||||
labelPosition: "left",
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</BlockComponent>
|
|
||||||
{/if}
|
|
||||||
</BlockComponent>
|
|
||||||
</Block>
|
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
<script>
|
<script>
|
||||||
import Placeholder from "../Placeholder.svelte"
|
import Placeholder from "../Placeholder.svelte"
|
||||||
import FieldGroupFallback from "./FieldGroupFallback.svelte"
|
import FieldGroupFallback from "./FieldGroupFallback.svelte"
|
||||||
import Skeleton from "../Skeleton.svelte"
|
|
||||||
import { getContext, onDestroy } from "svelte"
|
import { getContext, onDestroy } from "svelte"
|
||||||
|
|
||||||
export let label
|
export let label
|
||||||
|
@ -54,8 +53,6 @@
|
||||||
builderStore.actions.updateProp("label", e.target.textContent)
|
builderStore.actions.updateProp("label", e.target.textContent)
|
||||||
}
|
}
|
||||||
|
|
||||||
const loading = getContext("loading")
|
|
||||||
|
|
||||||
onDestroy(() => {
|
onDestroy(() => {
|
||||||
fieldApi?.deregister()
|
fieldApi?.deregister()
|
||||||
unsubscribe?.()
|
unsubscribe?.()
|
||||||
|
@ -79,10 +76,6 @@
|
||||||
<div class="spectrum-Form-itemField">
|
<div class="spectrum-Form-itemField">
|
||||||
{#if !formContext}
|
{#if !formContext}
|
||||||
<Placeholder text="Form components need to be wrapped in a form" />
|
<Placeholder text="Form components need to be wrapped in a form" />
|
||||||
{:else if $loading}
|
|
||||||
<Skeleton>
|
|
||||||
<slot />
|
|
||||||
</Skeleton>
|
|
||||||
{:else if !fieldState}
|
{:else if !fieldState}
|
||||||
<Placeholder />
|
<Placeholder />
|
||||||
{:else if schemaType && schemaType !== type && !["options", "longform"].includes(type)}
|
{:else if schemaType && schemaType !== type && !["options", "longform"].includes(type)}
|
||||||
|
|
|
@ -1,8 +1,7 @@
|
||||||
<script>
|
<script>
|
||||||
import { getContext, setContext } from "svelte"
|
import { getContext } from "svelte"
|
||||||
import InnerForm from "./InnerForm.svelte"
|
import InnerForm from "./InnerForm.svelte"
|
||||||
import { Helpers } from "@budibase/bbui"
|
import { Helpers } from "@budibase/bbui"
|
||||||
import { writable } from "svelte/store"
|
|
||||||
|
|
||||||
export let dataSource
|
export let dataSource
|
||||||
export let theme
|
export let theme
|
||||||
|
@ -21,11 +20,6 @@
|
||||||
const context = getContext("context")
|
const context = getContext("context")
|
||||||
const { API, fetchDatasourceSchema } = getContext("sdk")
|
const { API, fetchDatasourceSchema } = getContext("sdk")
|
||||||
|
|
||||||
// Forms also use loading context as they require loading a schema
|
|
||||||
const parentLoading = getContext("loading")
|
|
||||||
const loading = writable(true)
|
|
||||||
setContext("loading", loading)
|
|
||||||
|
|
||||||
let loaded = false
|
let loaded = false
|
||||||
let schema
|
let schema
|
||||||
let table
|
let table
|
||||||
|
@ -36,7 +30,6 @@
|
||||||
$: resetKey = Helpers.hashString(
|
$: resetKey = Helpers.hashString(
|
||||||
schemaKey + JSON.stringify(initialValues) + disabled
|
schemaKey + JSON.stringify(initialValues) + disabled
|
||||||
)
|
)
|
||||||
$: loading.set($parentLoading || !loaded)
|
|
||||||
|
|
||||||
// Returns the closes data context which isn't a built in context
|
// Returns the closes data context which isn't a built in context
|
||||||
const getInitialValues = (type, dataSource, context) => {
|
const getInitialValues = (type, dataSource, context) => {
|
||||||
|
@ -86,19 +79,21 @@
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#key resetKey}
|
{#if loaded}
|
||||||
<InnerForm
|
{#key resetKey}
|
||||||
{dataSource}
|
<InnerForm
|
||||||
{theme}
|
{dataSource}
|
||||||
{size}
|
{theme}
|
||||||
{disabled}
|
{size}
|
||||||
{actionType}
|
{disabled}
|
||||||
{schema}
|
{actionType}
|
||||||
{table}
|
{schema}
|
||||||
{initialValues}
|
{table}
|
||||||
{disableValidation}
|
{initialValues}
|
||||||
{editAutoColumns}
|
{disableValidation}
|
||||||
>
|
{editAutoColumns}
|
||||||
<slot />
|
>
|
||||||
</InnerForm>
|
<slot />
|
||||||
{/key}
|
</InnerForm>
|
||||||
|
{/key}
|
||||||
|
{/if}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
<script>
|
<script>
|
||||||
import { getContext } from "svelte"
|
import { getContext } from "svelte"
|
||||||
import { Table, Skeleton } from "@budibase/bbui"
|
import { Table } from "@budibase/bbui"
|
||||||
import SlotRenderer from "./SlotRenderer.svelte"
|
import SlotRenderer from "./SlotRenderer.svelte"
|
||||||
import { UnsortableTypes } from "../../../constants"
|
import { UnsortableTypes } from "../../../constants"
|
||||||
import { onDestroy } from "svelte"
|
import { onDestroy } from "svelte"
|
||||||
|
@ -14,7 +14,6 @@
|
||||||
export let compact
|
export let compact
|
||||||
export let onClick
|
export let onClick
|
||||||
|
|
||||||
const loading = getContext("loading")
|
|
||||||
const component = getContext("component")
|
const component = getContext("component")
|
||||||
const { styleable, getAction, ActionTypes, rowSelectionStore } =
|
const { styleable, getAction, ActionTypes, rowSelectionStore } =
|
||||||
getContext("sdk")
|
getContext("sdk")
|
||||||
|
@ -29,6 +28,7 @@
|
||||||
let selectedRows = []
|
let selectedRows = []
|
||||||
|
|
||||||
$: hasChildren = $component.children
|
$: hasChildren = $component.children
|
||||||
|
$: loading = dataProvider?.loading ?? false
|
||||||
$: data = dataProvider?.rows || []
|
$: data = dataProvider?.rows || []
|
||||||
$: fullSchema = dataProvider?.schema ?? {}
|
$: fullSchema = dataProvider?.schema ?? {}
|
||||||
$: fields = getFields(fullSchema, columns, false)
|
$: fields = getFields(fullSchema, columns, false)
|
||||||
|
@ -130,7 +130,7 @@
|
||||||
<Table
|
<Table
|
||||||
{data}
|
{data}
|
||||||
{schema}
|
{schema}
|
||||||
loading={$loading}
|
{loading}
|
||||||
{rowCount}
|
{rowCount}
|
||||||
{quiet}
|
{quiet}
|
||||||
{compact}
|
{compact}
|
||||||
|
@ -145,9 +145,6 @@
|
||||||
on:sort={onSort}
|
on:sort={onSort}
|
||||||
on:click={handleClick}
|
on:click={handleClick}
|
||||||
>
|
>
|
||||||
<div class="skeleton" slot="loadingIndicator">
|
|
||||||
<Skeleton />
|
|
||||||
</div>
|
|
||||||
<slot />
|
<slot />
|
||||||
</Table>
|
</Table>
|
||||||
{#if allowSelectRows && selectedRows.length}
|
{#if allowSelectRows && selectedRows.length}
|
||||||
|
@ -161,12 +158,6 @@
|
||||||
div {
|
div {
|
||||||
background-color: var(--spectrum-alias-background-color-secondary);
|
background-color: var(--spectrum-alias-background-color-secondary);
|
||||||
}
|
}
|
||||||
|
|
||||||
.skeleton {
|
|
||||||
height: 100%;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.row-count {
|
.row-count {
|
||||||
margin-top: var(--spacing-l);
|
margin-top: var(--spacing-l);
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,6 +16,7 @@ export { rowSelectionStore } from "./rowSelection.js"
|
||||||
export { blockStore } from "./blocks.js"
|
export { blockStore } from "./blocks.js"
|
||||||
export { environmentStore } from "./environment"
|
export { environmentStore } from "./environment"
|
||||||
export { eventStore } from "./events.js"
|
export { eventStore } from "./events.js"
|
||||||
|
export { orgStore } from "./org.js"
|
||||||
export {
|
export {
|
||||||
dndStore,
|
dndStore,
|
||||||
dndIndex,
|
dndIndex,
|
||||||
|
|
|
@ -1,7 +1,9 @@
|
||||||
import { routeStore } from "./routes"
|
import { routeStore } from "./routes"
|
||||||
import { appStore } from "./app"
|
import { appStore } from "./app"
|
||||||
|
import { orgStore } from "./org"
|
||||||
|
|
||||||
export async function initialise() {
|
export async function initialise() {
|
||||||
await routeStore.actions.fetchRoutes()
|
await routeStore.actions.fetchRoutes()
|
||||||
await appStore.actions.fetchAppDefinition()
|
await appStore.actions.fetchAppDefinition()
|
||||||
|
await orgStore.actions.init()
|
||||||
}
|
}
|
||||||
|
|
29
packages/client/src/stores/org.js
Normal file
29
packages/client/src/stores/org.js
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
import { API } from "api"
|
||||||
|
import { writable, get } from "svelte/store"
|
||||||
|
import { appStore } from "./app"
|
||||||
|
|
||||||
|
const createOrgStore = () => {
|
||||||
|
const store = writable(null)
|
||||||
|
|
||||||
|
const { subscribe, set } = store
|
||||||
|
|
||||||
|
async function init() {
|
||||||
|
const tenantId = get(appStore).application?.tenantId
|
||||||
|
if (!tenantId) return
|
||||||
|
try {
|
||||||
|
const settingsConfigDoc = await API.getTenantConfig(tenantId)
|
||||||
|
set({ logoUrl: settingsConfigDoc.config.logoUrl })
|
||||||
|
} catch (e) {
|
||||||
|
console.log("Could not init org ", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
subscribe,
|
||||||
|
actions: {
|
||||||
|
init,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const orgStore = createOrgStore()
|
|
@ -2,6 +2,7 @@ import { derived } from "svelte/store"
|
||||||
import { routeStore } from "./routes"
|
import { routeStore } from "./routes"
|
||||||
import { builderStore } from "./builder"
|
import { builderStore } from "./builder"
|
||||||
import { appStore } from "./app"
|
import { appStore } from "./app"
|
||||||
|
import { orgStore } from "./org"
|
||||||
import { dndIndex, dndParent, dndIsNewComponent, dndBounds } from "./dnd.js"
|
import { dndIndex, dndParent, dndIsNewComponent, dndBounds } from "./dnd.js"
|
||||||
import { RoleUtils } from "@budibase/frontend-core"
|
import { RoleUtils } from "@budibase/frontend-core"
|
||||||
import { findComponentById, findComponentParent } from "../utils/components.js"
|
import { findComponentById, findComponentParent } from "../utils/components.js"
|
||||||
|
@ -14,6 +15,7 @@ const createScreenStore = () => {
|
||||||
appStore,
|
appStore,
|
||||||
routeStore,
|
routeStore,
|
||||||
builderStore,
|
builderStore,
|
||||||
|
orgStore,
|
||||||
dndParent,
|
dndParent,
|
||||||
dndIndex,
|
dndIndex,
|
||||||
dndIsNewComponent,
|
dndIsNewComponent,
|
||||||
|
@ -23,6 +25,7 @@ const createScreenStore = () => {
|
||||||
$appStore,
|
$appStore,
|
||||||
$routeStore,
|
$routeStore,
|
||||||
$builderStore,
|
$builderStore,
|
||||||
|
$orgStore,
|
||||||
$dndParent,
|
$dndParent,
|
||||||
$dndIndex,
|
$dndIndex,
|
||||||
$dndIsNewComponent,
|
$dndIsNewComponent,
|
||||||
|
@ -146,6 +149,11 @@ const createScreenStore = () => {
|
||||||
if (!navigationSettings.title && !navigationSettings.hideTitle) {
|
if (!navigationSettings.title && !navigationSettings.hideTitle) {
|
||||||
navigationSettings.title = $appStore.application?.name
|
navigationSettings.title = $appStore.application?.name
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Default to the org logo
|
||||||
|
if (!navigationSettings.logoUrl) {
|
||||||
|
navigationSettings.logoUrl = $orgStore?.logoUrl
|
||||||
|
}
|
||||||
}
|
}
|
||||||
activeLayout = {
|
activeLayout = {
|
||||||
_id: "layout",
|
_id: "layout",
|
||||||
|
|
|
@ -23,11 +23,6 @@
|
||||||
chalk "^2.0.0"
|
chalk "^2.0.0"
|
||||||
js-tokens "^4.0.0"
|
js-tokens "^4.0.0"
|
||||||
|
|
||||||
"@budibase/types@2.4.8-alpha.4":
|
|
||||||
version "2.4.8-alpha.4"
|
|
||||||
resolved "https://registry.yarnpkg.com/@budibase/types/-/types-2.4.8-alpha.4.tgz#4e6dec50eef381994432ef4d08587a9a7156dd84"
|
|
||||||
integrity sha512-aiHHOvsDLHQ2OFmLgaSUttQwSuaPBqF1lbyyCkEJIbbl/qo9EPNZGl+AkB7wo12U5HdqWhr9OpFL12EqkcD4GA==
|
|
||||||
|
|
||||||
"@jridgewell/gen-mapping@^0.3.0":
|
"@jridgewell/gen-mapping@^0.3.0":
|
||||||
version "0.3.2"
|
version "0.3.2"
|
||||||
resolved "https://registry.yarnpkg.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.2.tgz#c1aedc61e853f2bb9f5dfe6d4442d3b565b253b9"
|
resolved "https://registry.yarnpkg.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.2.tgz#c1aedc61e853f2bb9f5dfe6d4442d3b565b253b9"
|
||||||
|
|
|
@ -1,13 +1,13 @@
|
||||||
{
|
{
|
||||||
"name": "@budibase/frontend-core",
|
"name": "@budibase/frontend-core",
|
||||||
"version": "2.4.12-alpha.3",
|
"version": "2.4.27-alpha.9",
|
||||||
"description": "Budibase frontend core libraries used in builder and client",
|
"description": "Budibase frontend core libraries used in builder and client",
|
||||||
"author": "Budibase",
|
"author": "Budibase",
|
||||||
"license": "MPL-2.0",
|
"license": "MPL-2.0",
|
||||||
"svelte": "src/index.js",
|
"svelte": "src/index.js",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@budibase/bbui": "2.4.12-alpha.3",
|
"@budibase/bbui": "2.4.27-alpha.9",
|
||||||
"@budibase/shared-core": "2.4.12-alpha.3",
|
"@budibase/shared-core": "2.4.27-alpha.9",
|
||||||
"dayjs": "^1.11.7",
|
"dayjs": "^1.11.7",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"socket.io-client": "^4.6.1",
|
"socket.io-client": "^4.6.1",
|
||||||
|
|
|
@ -73,6 +73,18 @@ export const buildConfigEndpoints = API => ({
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates the company favicon for the environment.
|
||||||
|
* @param data the favicon form data
|
||||||
|
*/
|
||||||
|
uploadFavicon: async data => {
|
||||||
|
return await API.post({
|
||||||
|
url: "/api/global/configs/upload/settings/faviconUrl",
|
||||||
|
body: data,
|
||||||
|
json: false,
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Uploads a logo for an OIDC provider.
|
* Uploads a logo for an OIDC provider.
|
||||||
* @param name the name of the OIDC provider
|
* @param name the name of the OIDC provider
|
||||||
|
|
|
@ -5,6 +5,8 @@
|
||||||
import Covanta from "../../assets/covanta.png"
|
import Covanta from "../../assets/covanta.png"
|
||||||
import Schnellecke from "../../assets/schnellecke.png"
|
import Schnellecke from "../../assets/schnellecke.png"
|
||||||
|
|
||||||
|
export let enabled = true
|
||||||
|
|
||||||
const testimonials = [
|
const testimonials = [
|
||||||
{
|
{
|
||||||
text: "Budibase was the only solution that checked all the boxes for Covanta. Covanta expects to realize $3.2MM in savings due to the elimination of redundant data entry.",
|
text: "Budibase was the only solution that checked all the boxes for Covanta. Covanta expects to realize $3.2MM in savings due to the elimination of redundant data entry.",
|
||||||
|
@ -33,23 +35,25 @@
|
||||||
|
|
||||||
<SplitPage>
|
<SplitPage>
|
||||||
<slot />
|
<slot />
|
||||||
<div class="wrapper" slot="right">
|
<div class:wrapper={enabled} slot="right">
|
||||||
<div class="testimonial">
|
{#if enabled}
|
||||||
<Layout noPadding gap="S">
|
<div class="testimonial">
|
||||||
<img
|
<Layout noPadding gap="S">
|
||||||
width={testimonial.imageSize}
|
<img
|
||||||
alt="a-happy-budibase-user"
|
width={testimonial.imageSize}
|
||||||
src={testimonial.image}
|
alt="a-happy-budibase-user"
|
||||||
/>
|
src={testimonial.image}
|
||||||
<div class="text">
|
/>
|
||||||
"{testimonial.text}"
|
<div class="text">
|
||||||
</div>
|
"{testimonial.text}"
|
||||||
<div class="author">
|
</div>
|
||||||
<div class="name">{testimonial.name}</div>
|
<div class="author">
|
||||||
<div class="company">{testimonial.role}</div>
|
<div class="name">{testimonial.name}</div>
|
||||||
</div>
|
<div class="company">{testimonial.role}</div>
|
||||||
</Layout>
|
</div>
|
||||||
</div>
|
</Layout>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</SplitPage>
|
</SplitPage>
|
||||||
|
|
||||||
|
|
|
@ -68,6 +68,7 @@ export const Features = {
|
||||||
ENVIRONMENT_VARIABLES: "environmentVariables",
|
ENVIRONMENT_VARIABLES: "environmentVariables",
|
||||||
AUDIT_LOGS: "auditLogs",
|
AUDIT_LOGS: "auditLogs",
|
||||||
ENFORCEABLE_SSO: "enforceableSSO",
|
ENFORCEABLE_SSO: "enforceableSSO",
|
||||||
|
BRANDING: "branding",
|
||||||
}
|
}
|
||||||
|
|
||||||
// Role IDs
|
// Role IDs
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@budibase/sdk",
|
"name": "@budibase/sdk",
|
||||||
"version": "2.4.12-alpha.3",
|
"version": "2.4.27-alpha.9",
|
||||||
"description": "Budibase Public API SDK",
|
"description": "Budibase Public API SDK",
|
||||||
"author": "Budibase",
|
"author": "Budibase",
|
||||||
"license": "MPL-2.0",
|
"license": "MPL-2.0",
|
||||||
|
|
|
@ -4,6 +4,7 @@ module FetchMock {
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
const fetch = jest.requireActual("node-fetch")
|
const fetch = jest.requireActual("node-fetch")
|
||||||
let failCount = 0
|
let failCount = 0
|
||||||
|
let mockSearch = false
|
||||||
|
|
||||||
const func = async (url: any, opts: any) => {
|
const func = async (url: any, opts: any) => {
|
||||||
function json(body: any, status = 200) {
|
function json(body: any, status = 200) {
|
||||||
|
@ -69,7 +70,7 @@ module FetchMock {
|
||||||
},
|
},
|
||||||
404
|
404
|
||||||
)
|
)
|
||||||
} else if (url.includes("_search")) {
|
} else if (mockSearch && url.includes("_search")) {
|
||||||
const body = opts.body
|
const body = opts.body
|
||||||
const parts = body.split("tableId:")
|
const parts = body.split("tableId:")
|
||||||
let tableId
|
let tableId
|
||||||
|
@ -192,5 +193,9 @@ module FetchMock {
|
||||||
|
|
||||||
func.Headers = fetch.Headers
|
func.Headers = fetch.Headers
|
||||||
|
|
||||||
|
func.mockSearch = () => {
|
||||||
|
mockSearch = true
|
||||||
|
}
|
||||||
|
|
||||||
module.exports = func
|
module.exports = func
|
||||||
}
|
}
|
||||||
|
|
|
@ -43,6 +43,7 @@ const config: Config.InitialOptions = {
|
||||||
"../backend-core/src/**/*.{js,ts}",
|
"../backend-core/src/**/*.{js,ts}",
|
||||||
// The use of coverage with couchdb view functions breaks tests
|
// The use of coverage with couchdb view functions breaks tests
|
||||||
"!src/db/views/staticViews.*",
|
"!src/db/views/staticViews.*",
|
||||||
|
"!src/**/*.spec.{js,ts}",
|
||||||
],
|
],
|
||||||
coverageReporters: ["lcov", "json", "clover"],
|
coverageReporters: ["lcov", "json", "clover"],
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"name": "@budibase/server",
|
"name": "@budibase/server",
|
||||||
"email": "hi@budibase.com",
|
"email": "hi@budibase.com",
|
||||||
"version": "2.4.12-alpha.3",
|
"version": "2.4.27-alpha.9",
|
||||||
"description": "Budibase Web Server",
|
"description": "Budibase Web Server",
|
||||||
"main": "src/index.ts",
|
"main": "src/index.ts",
|
||||||
"repository": {
|
"repository": {
|
||||||
|
@ -14,7 +14,8 @@
|
||||||
"build:dev": "yarn prebuild && tsc --build --watch --preserveWatchOutput",
|
"build:dev": "yarn prebuild && tsc --build --watch --preserveWatchOutput",
|
||||||
"debug": "yarn build && node --expose-gc --inspect=9222 dist/index.js",
|
"debug": "yarn build && node --expose-gc --inspect=9222 dist/index.js",
|
||||||
"postbuild": "copyfiles -u 1 src/**/*.svelte dist/ && copyfiles -u 1 src/**/*.hbs dist/ && copyfiles -u 1 src/**/*.json dist/",
|
"postbuild": "copyfiles -u 1 src/**/*.svelte dist/ && copyfiles -u 1 src/**/*.hbs dist/ && copyfiles -u 1 src/**/*.json dist/",
|
||||||
"test": "bash scripts/test.sh",
|
"test": "NODE_OPTIONS=\"--max-old-space-size=4096\" bash scripts/test.sh",
|
||||||
|
"test:memory": "jest --maxWorkers=2 --logHeapUsage --forceExit",
|
||||||
"test:watch": "jest --watch",
|
"test:watch": "jest --watch",
|
||||||
"predocker": "copyfiles -f ../client/dist/budibase-client.js ../client/manifest.json client",
|
"predocker": "copyfiles -f ../client/dist/budibase-client.js ../client/manifest.json client",
|
||||||
"build:docker": "yarn run predocker && docker build . -t app-service --label version=$BUDIBASE_RELEASE_VERSION",
|
"build:docker": "yarn run predocker && docker build . -t app-service --label version=$BUDIBASE_RELEASE_VERSION",
|
||||||
|
@ -43,12 +44,12 @@
|
||||||
"license": "GPL-3.0",
|
"license": "GPL-3.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@apidevtools/swagger-parser": "10.0.3",
|
"@apidevtools/swagger-parser": "10.0.3",
|
||||||
"@budibase/backend-core": "2.4.12-alpha.3",
|
"@budibase/backend-core": "2.4.27-alpha.9",
|
||||||
"@budibase/client": "2.4.12-alpha.3",
|
"@budibase/client": "2.4.27-alpha.9",
|
||||||
"@budibase/pro": "2.4.12-alpha.3",
|
"@budibase/pro": "2.4.27-alpha.9",
|
||||||
"@budibase/shared-core": "2.4.12-alpha.3",
|
"@budibase/shared-core": "2.4.27-alpha.9",
|
||||||
"@budibase/string-templates": "2.4.12-alpha.3",
|
"@budibase/string-templates": "2.4.27-alpha.9",
|
||||||
"@budibase/types": "2.4.12-alpha.3",
|
"@budibase/types": "2.4.27-alpha.9",
|
||||||
"@bull-board/api": "3.7.0",
|
"@bull-board/api": "3.7.0",
|
||||||
"@bull-board/koa": "3.9.4",
|
"@bull-board/koa": "3.9.4",
|
||||||
"@elastic/elasticsearch": "7.10.0",
|
"@elastic/elasticsearch": "7.10.0",
|
||||||
|
@ -126,7 +127,7 @@
|
||||||
"@babel/core": "7.17.4",
|
"@babel/core": "7.17.4",
|
||||||
"@babel/preset-env": "7.16.11",
|
"@babel/preset-env": "7.16.11",
|
||||||
"@budibase/standard-components": "^0.9.139",
|
"@budibase/standard-components": "^0.9.139",
|
||||||
"@jest/test-sequencer": "24.9.0",
|
"@jest/test-sequencer": "29.5.0",
|
||||||
"@swc/core": "^1.3.25",
|
"@swc/core": "^1.3.25",
|
||||||
"@swc/jest": "^0.2.24",
|
"@swc/jest": "^0.2.24",
|
||||||
"@trendyol/jest-testcontainers": "^2.1.1",
|
"@trendyol/jest-testcontainers": "^2.1.1",
|
||||||
|
@ -135,7 +136,7 @@
|
||||||
"@types/global-agent": "2.1.1",
|
"@types/global-agent": "2.1.1",
|
||||||
"@types/google-spreadsheet": "3.1.5",
|
"@types/google-spreadsheet": "3.1.5",
|
||||||
"@types/ioredis": "4.28.10",
|
"@types/ioredis": "4.28.10",
|
||||||
"@types/jest": "27.5.1",
|
"@types/jest": "29.5.0",
|
||||||
"@types/koa": "2.13.4",
|
"@types/koa": "2.13.4",
|
||||||
"@types/koa__router": "8.0.8",
|
"@types/koa__router": "8.0.8",
|
||||||
"@types/lodash": "4.14.180",
|
"@types/lodash": "4.14.180",
|
||||||
|
@ -155,7 +156,7 @@
|
||||||
"eslint": "6.8.0",
|
"eslint": "6.8.0",
|
||||||
"ioredis-mock": "7.2.0",
|
"ioredis-mock": "7.2.0",
|
||||||
"is-wsl": "2.2.0",
|
"is-wsl": "2.2.0",
|
||||||
"jest": "28.1.1",
|
"jest": "29.5.0",
|
||||||
"jest-openapi": "0.14.2",
|
"jest-openapi": "0.14.2",
|
||||||
"jest-serial-runner": "^1.2.1",
|
"jest-serial-runner": "^1.2.1",
|
||||||
"nodemon": "2.0.15",
|
"nodemon": "2.0.15",
|
||||||
|
@ -167,7 +168,7 @@
|
||||||
"supertest": "6.2.2",
|
"supertest": "6.2.2",
|
||||||
"swagger-jsdoc": "6.1.0",
|
"swagger-jsdoc": "6.1.0",
|
||||||
"timekeeper": "2.2.0",
|
"timekeeper": "2.2.0",
|
||||||
"ts-jest": "28.0.4",
|
"ts-jest": "29.0.5",
|
||||||
"ts-node": "10.8.1",
|
"ts-node": "10.8.1",
|
||||||
"tsconfig-paths": "4.0.0",
|
"tsconfig-paths": "4.0.0",
|
||||||
"typescript": "4.7.3",
|
"typescript": "4.7.3",
|
||||||
|
|
|
@ -3,8 +3,8 @@
|
||||||
if [[ -n $CI ]]
|
if [[ -n $CI ]]
|
||||||
then
|
then
|
||||||
# --runInBand performs better in ci where resources are limited
|
# --runInBand performs better in ci where resources are limited
|
||||||
echo "jest --coverage --runInBand"
|
echo "jest --coverage --runInBand --forceExit"
|
||||||
jest --coverage --runInBand
|
jest --coverage --runInBand --forceExit
|
||||||
else
|
else
|
||||||
# --maxWorkers performs better in development
|
# --maxWorkers performs better in development
|
||||||
echo "jest --coverage --maxWorkers=2"
|
echo "jest --coverage --maxWorkers=2"
|
||||||
|
|
|
@ -20,10 +20,10 @@ import {
|
||||||
cache,
|
cache,
|
||||||
tenancy,
|
tenancy,
|
||||||
context,
|
context,
|
||||||
errors,
|
|
||||||
events,
|
events,
|
||||||
migrations,
|
migrations,
|
||||||
objectStore,
|
objectStore,
|
||||||
|
ErrorCode,
|
||||||
} from "@budibase/backend-core"
|
} from "@budibase/backend-core"
|
||||||
import { USERS_TABLE_SCHEMA } from "../../constants"
|
import { USERS_TABLE_SCHEMA } from "../../constants"
|
||||||
import { buildDefaultDocs } from "../../db/defaultData/datasource_bb_default"
|
import { buildDefaultDocs } from "../../db/defaultData/datasource_bb_default"
|
||||||
|
@ -44,7 +44,6 @@ import {
|
||||||
Layout,
|
Layout,
|
||||||
Screen,
|
Screen,
|
||||||
MigrationType,
|
MigrationType,
|
||||||
BBContext,
|
|
||||||
Database,
|
Database,
|
||||||
UserCtx,
|
UserCtx,
|
||||||
} from "@budibase/types"
|
} from "@budibase/types"
|
||||||
|
@ -74,14 +73,14 @@ async function getScreens() {
|
||||||
).rows.map((row: any) => row.doc)
|
).rows.map((row: any) => row.doc)
|
||||||
}
|
}
|
||||||
|
|
||||||
function getUserRoleId(ctx: BBContext) {
|
function getUserRoleId(ctx: UserCtx) {
|
||||||
return !ctx.user?.role || !ctx.user.role._id
|
return !ctx.user?.role || !ctx.user.role._id
|
||||||
? roles.BUILTIN_ROLE_IDS.PUBLIC
|
? roles.BUILTIN_ROLE_IDS.PUBLIC
|
||||||
: ctx.user.role._id
|
: ctx.user.role._id
|
||||||
}
|
}
|
||||||
|
|
||||||
function checkAppUrl(
|
function checkAppUrl(
|
||||||
ctx: BBContext,
|
ctx: UserCtx,
|
||||||
apps: App[],
|
apps: App[],
|
||||||
url: string,
|
url: string,
|
||||||
currentAppId?: string
|
currentAppId?: string
|
||||||
|
@ -95,7 +94,7 @@ function checkAppUrl(
|
||||||
}
|
}
|
||||||
|
|
||||||
function checkAppName(
|
function checkAppName(
|
||||||
ctx: BBContext,
|
ctx: UserCtx,
|
||||||
apps: App[],
|
apps: App[],
|
||||||
name: string,
|
name: string,
|
||||||
currentAppId?: string
|
currentAppId?: string
|
||||||
|
@ -160,7 +159,7 @@ async function addDefaultTables(db: Database) {
|
||||||
await db.bulkDocs([...defaultDbDocs])
|
await db.bulkDocs([...defaultDbDocs])
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function fetch(ctx: BBContext) {
|
export async function fetch(ctx: UserCtx) {
|
||||||
const dev = ctx.query && ctx.query.status === AppStatus.DEV
|
const dev = ctx.query && ctx.query.status === AppStatus.DEV
|
||||||
const all = ctx.query && ctx.query.status === AppStatus.ALL
|
const all = ctx.query && ctx.query.status === AppStatus.ALL
|
||||||
const apps = (await dbCore.getAllApps({ dev, all })) as App[]
|
const apps = (await dbCore.getAllApps({ dev, all })) as App[]
|
||||||
|
@ -185,7 +184,7 @@ export async function fetch(ctx: BBContext) {
|
||||||
ctx.body = await checkAppMetadata(apps)
|
ctx.body = await checkAppMetadata(apps)
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function fetchAppDefinition(ctx: BBContext) {
|
export async function fetchAppDefinition(ctx: UserCtx) {
|
||||||
const layouts = await getLayouts()
|
const layouts = await getLayouts()
|
||||||
const userRoleId = getUserRoleId(ctx)
|
const userRoleId = getUserRoleId(ctx)
|
||||||
const accessController = new roles.AccessController()
|
const accessController = new roles.AccessController()
|
||||||
|
@ -231,7 +230,7 @@ export async function fetchAppPackage(ctx: UserCtx) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function performAppCreate(ctx: BBContext) {
|
async function performAppCreate(ctx: UserCtx) {
|
||||||
const apps = (await dbCore.getAllApps({ dev: true })) as App[]
|
const apps = (await dbCore.getAllApps({ dev: true })) as App[]
|
||||||
const name = ctx.request.body.name,
|
const name = ctx.request.body.name,
|
||||||
possibleUrl = ctx.request.body.url
|
possibleUrl = ctx.request.body.url
|
||||||
|
@ -360,7 +359,7 @@ async function creationEvents(request: any, app: App) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function appPostCreate(ctx: BBContext, app: App) {
|
async function appPostCreate(ctx: UserCtx, app: App) {
|
||||||
const tenantId = tenancy.getTenantId()
|
const tenantId = tenancy.getTenantId()
|
||||||
await migrations.backPopulateMigrations({
|
await migrations.backPopulateMigrations({
|
||||||
type: MigrationType.APP,
|
type: MigrationType.APP,
|
||||||
|
@ -378,7 +377,7 @@ async function appPostCreate(ctx: BBContext, app: App) {
|
||||||
return quotas.addRows(rowCount)
|
return quotas.addRows(rowCount)
|
||||||
})
|
})
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
if (err.code && err.code === errors.codes.USAGE_LIMIT_EXCEEDED) {
|
if (err.code && err.code === ErrorCode.USAGE_LIMIT_EXCEEDED) {
|
||||||
// this import resulted in row usage exceeding the quota
|
// this import resulted in row usage exceeding the quota
|
||||||
// delete the app
|
// delete the app
|
||||||
// skip pre and post-steps as no rows have been added to quotas yet
|
// skip pre and post-steps as no rows have been added to quotas yet
|
||||||
|
@ -391,7 +390,7 @@ async function appPostCreate(ctx: BBContext, app: App) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function create(ctx: BBContext) {
|
export async function create(ctx: UserCtx) {
|
||||||
const newApplication = await quotas.addApp(() => performAppCreate(ctx))
|
const newApplication = await quotas.addApp(() => performAppCreate(ctx))
|
||||||
await appPostCreate(ctx, newApplication)
|
await appPostCreate(ctx, newApplication)
|
||||||
await cache.bustCache(cache.CacheKey.CHECKLIST)
|
await cache.bustCache(cache.CacheKey.CHECKLIST)
|
||||||
|
@ -401,7 +400,7 @@ export async function create(ctx: BBContext) {
|
||||||
|
|
||||||
// This endpoint currently operates as a PATCH rather than a PUT
|
// This endpoint currently operates as a PATCH rather than a PUT
|
||||||
// Thus name and url fields are handled only if present
|
// Thus name and url fields are handled only if present
|
||||||
export async function update(ctx: BBContext) {
|
export async function update(ctx: UserCtx) {
|
||||||
const apps = (await dbCore.getAllApps({ dev: true })) as App[]
|
const apps = (await dbCore.getAllApps({ dev: true })) as App[]
|
||||||
// validation
|
// validation
|
||||||
const name = ctx.request.body.name,
|
const name = ctx.request.body.name,
|
||||||
|
@ -421,7 +420,7 @@ export async function update(ctx: BBContext) {
|
||||||
ctx.body = app
|
ctx.body = app
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function updateClient(ctx: BBContext) {
|
export async function updateClient(ctx: UserCtx) {
|
||||||
// Get current app version
|
// Get current app version
|
||||||
const db = context.getAppDB()
|
const db = context.getAppDB()
|
||||||
const application = await db.get(DocumentType.APP_METADATA)
|
const application = await db.get(DocumentType.APP_METADATA)
|
||||||
|
@ -445,7 +444,7 @@ export async function updateClient(ctx: BBContext) {
|
||||||
ctx.body = app
|
ctx.body = app
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function revertClient(ctx: BBContext) {
|
export async function revertClient(ctx: UserCtx) {
|
||||||
// Check app can be reverted
|
// Check app can be reverted
|
||||||
const db = context.getAppDB()
|
const db = context.getAppDB()
|
||||||
const application = await db.get(DocumentType.APP_METADATA)
|
const application = await db.get(DocumentType.APP_METADATA)
|
||||||
|
@ -471,7 +470,7 @@ export async function revertClient(ctx: BBContext) {
|
||||||
ctx.body = app
|
ctx.body = app
|
||||||
}
|
}
|
||||||
|
|
||||||
const unpublishApp = async (ctx: any) => {
|
async function unpublishApp(ctx: UserCtx) {
|
||||||
let appId = ctx.params.appId
|
let appId = ctx.params.appId
|
||||||
appId = dbCore.getProdAppID(appId)
|
appId = dbCore.getProdAppID(appId)
|
||||||
|
|
||||||
|
@ -487,7 +486,7 @@ const unpublishApp = async (ctx: any) => {
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
async function destroyApp(ctx: BBContext) {
|
async function destroyApp(ctx: UserCtx) {
|
||||||
let appId = ctx.params.appId
|
let appId = ctx.params.appId
|
||||||
appId = dbCore.getProdAppID(appId)
|
appId = dbCore.getProdAppID(appId)
|
||||||
const devAppId = dbCore.getDevAppID(appId)
|
const devAppId = dbCore.getDevAppID(appId)
|
||||||
|
@ -515,12 +514,12 @@ async function destroyApp(ctx: BBContext) {
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
async function preDestroyApp(ctx: BBContext) {
|
async function preDestroyApp(ctx: UserCtx) {
|
||||||
const { rows } = await getUniqueRows([ctx.params.appId])
|
const { rows } = await getUniqueRows([ctx.params.appId])
|
||||||
ctx.rowCount = rows.length
|
ctx.rowCount = rows.length
|
||||||
}
|
}
|
||||||
|
|
||||||
async function postDestroyApp(ctx: BBContext) {
|
async function postDestroyApp(ctx: UserCtx) {
|
||||||
const rowCount = ctx.rowCount
|
const rowCount = ctx.rowCount
|
||||||
await groups.cleanupApp(ctx.params.appId)
|
await groups.cleanupApp(ctx.params.appId)
|
||||||
if (rowCount) {
|
if (rowCount) {
|
||||||
|
@ -528,7 +527,7 @@ async function postDestroyApp(ctx: BBContext) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function destroy(ctx: BBContext) {
|
export async function destroy(ctx: UserCtx) {
|
||||||
await preDestroyApp(ctx)
|
await preDestroyApp(ctx)
|
||||||
const result = await destroyApp(ctx)
|
const result = await destroyApp(ctx)
|
||||||
await postDestroyApp(ctx)
|
await postDestroyApp(ctx)
|
||||||
|
@ -536,7 +535,7 @@ export async function destroy(ctx: BBContext) {
|
||||||
ctx.body = result
|
ctx.body = result
|
||||||
}
|
}
|
||||||
|
|
||||||
export const unpublish = async (ctx: BBContext) => {
|
export async function unpublish(ctx: UserCtx) {
|
||||||
const prodAppId = dbCore.getProdAppID(ctx.params.appId)
|
const prodAppId = dbCore.getProdAppID(ctx.params.appId)
|
||||||
const dbExists = await dbCore.dbExists(prodAppId)
|
const dbExists = await dbCore.dbExists(prodAppId)
|
||||||
|
|
||||||
|
@ -551,7 +550,7 @@ export const unpublish = async (ctx: BBContext) => {
|
||||||
ctx.status = 204
|
ctx.status = 204
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function sync(ctx: BBContext) {
|
export async function sync(ctx: UserCtx) {
|
||||||
const appId = ctx.params.appId
|
const appId = ctx.params.appId
|
||||||
try {
|
try {
|
||||||
ctx.body = await sdk.applications.syncApp(appId)
|
ctx.body = await sdk.applications.syncApp(appId)
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue