Merge branch 'master' into BUDI-8312
This commit is contained in:
commit
5a6f42cb28
|
@ -92,7 +92,8 @@
|
||||||
// differs to external, but the API is broadly the same
|
// differs to external, but the API is broadly the same
|
||||||
"jest/no-conditional-expect": "off",
|
"jest/no-conditional-expect": "off",
|
||||||
// have to turn this off to allow function overloading in typescript
|
// have to turn this off to allow function overloading in typescript
|
||||||
"no-dupe-class-members": "off"
|
"no-dupe-class-members": "off",
|
||||||
|
"no-redeclare": "off"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
|
@ -73,9 +73,9 @@ jobs:
|
||||||
- name: Check types
|
- name: Check types
|
||||||
run: |
|
run: |
|
||||||
if ${{ env.USE_NX_AFFECTED }}; then
|
if ${{ env.USE_NX_AFFECTED }}; then
|
||||||
yarn check:types --since=${{ env.NX_BASE_BRANCH }}
|
yarn check:types --since=${{ env.NX_BASE_BRANCH }} --ignore @budibase/account-portal-server
|
||||||
else
|
else
|
||||||
yarn check:types
|
yarn check:types --ignore @budibase/account-portal-server
|
||||||
fi
|
fi
|
||||||
|
|
||||||
helm-lint:
|
helm-lint:
|
||||||
|
@ -226,10 +226,11 @@ jobs:
|
||||||
if: ${{ steps.get_pro_commits.outputs.base_commit_excluding_merges != '' }}
|
if: ${{ steps.get_pro_commits.outputs.base_commit_excluding_merges != '' }}
|
||||||
run: |
|
run: |
|
||||||
cd packages/pro
|
cd packages/pro
|
||||||
|
base_commit='${{ steps.get_pro_commits.outputs.base_commit }}'
|
||||||
base_commit_excluding_merges='${{ steps.get_pro_commits.outputs.base_commit_excluding_merges }}'
|
base_commit_excluding_merges='${{ steps.get_pro_commits.outputs.base_commit_excluding_merges }}'
|
||||||
pro_commit='${{ steps.get_pro_commits.outputs.pro_commit }}'
|
pro_commit='${{ steps.get_pro_commits.outputs.pro_commit }}'
|
||||||
|
|
||||||
any_commit=$(git log --no-merges $base_commit_excluding_merges...$pro_commit)
|
any_commit=$(git log --no-merges $base_commit...$pro_commit)
|
||||||
|
|
||||||
if [ -n "$any_commit" ]; then
|
if [ -n "$any_commit" ]; then
|
||||||
echo $any_commit
|
echo $any_commit
|
||||||
|
|
|
@ -9,7 +9,7 @@ on:
|
||||||
jobs:
|
jobs:
|
||||||
ensure-is-master-tag:
|
ensure-is-master-tag:
|
||||||
name: Ensure is a master tag
|
name: Ensure is a master tag
|
||||||
runs-on: qa-arc-runner-set
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout monorepo
|
- name: Checkout monorepo
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
|
|
@ -0,0 +1,38 @@
|
||||||
|
name: PR labeler
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: ${{ github.workflow }}-${{ github.event.pull_request.number }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
types: [opened, synchronize]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
size-labeler:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: codelytv/pr-size-labeler@v1
|
||||||
|
with:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
xs_max_size: "10"
|
||||||
|
s_max_size: "100"
|
||||||
|
m_max_size: "500"
|
||||||
|
l_max_size: "1000"
|
||||||
|
fail_if_xl: "false"
|
||||||
|
files_to_ignore: "yarn.lock"
|
||||||
|
|
||||||
|
team-labeler:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
if: ${{ github.event.action == 'opened' }}
|
||||||
|
steps:
|
||||||
|
- uses: rodrigoarias/auto-label-per-user@v1.0.0
|
||||||
|
with:
|
||||||
|
git-token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
user-team-map: |
|
||||||
|
{
|
||||||
|
"adrinr": "firestorm",
|
||||||
|
"samwho": "firestorm",
|
||||||
|
"PClmnt": "firestorm",
|
||||||
|
"mike12345567": "firestorm"
|
||||||
|
}
|
|
@ -8,6 +8,8 @@ bb-airgapped.tar.gz
|
||||||
packages/server/build/oldClientVersions/**/*
|
packages/server/build/oldClientVersions/**/*
|
||||||
packages/builder/src/components/deploy/clientVersions.json
|
packages/builder/src/components/deploy/clientVersions.json
|
||||||
|
|
||||||
|
packages/server/src/integrations/tests/utils/*.lock
|
||||||
|
|
||||||
# Logs
|
# Logs
|
||||||
logs
|
logs
|
||||||
*.log
|
*.log
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
dependencies:
|
dependencies:
|
||||||
- name: couchdb
|
- name: couchdb
|
||||||
repository: https://apache.github.io/couchdb-helm
|
repository: https://apache.github.io/couchdb-helm
|
||||||
version: 4.3.0
|
version: 4.5.6
|
||||||
digest: sha256:94449a7f195b186f5af33ec5aa66d58b36bede240fae710f021ca87837b30606
|
digest: sha256:405f098633e632d6f4e140175f156ed4f02918b0d89193f1b66c9cbea211d6c9
|
||||||
generated: "2023-11-20T17:43:02.777596Z"
|
generated: "2024-06-05T14:41:05.979052+01:00"
|
||||||
|
|
|
@ -17,6 +17,6 @@ version: 0.0.0
|
||||||
appVersion: 0.0.0
|
appVersion: 0.0.0
|
||||||
dependencies:
|
dependencies:
|
||||||
- name: couchdb
|
- name: couchdb
|
||||||
version: 4.3.0
|
version: 4.5.6
|
||||||
repository: https://apache.github.io/couchdb-helm
|
repository: https://apache.github.io/couchdb-helm
|
||||||
condition: services.couchdb.enabled
|
condition: services.couchdb.enabled
|
||||||
|
|
|
@ -112,7 +112,9 @@ $ helm install --create-namespace --namespace budibase budibase . -f values.yaml
|
||||||
| awsAlbIngress.enabled | bool | `false` | Whether to create an ALB Ingress resource pointing to the Budibase proxy. Requires the AWS ALB Ingress Controller. |
|
| awsAlbIngress.enabled | bool | `false` | Whether to create an ALB Ingress resource pointing to the Budibase proxy. Requires the AWS ALB Ingress Controller. |
|
||||||
| couchdb.clusterSize | int | `1` | The number of replicas to run in the CouchDB cluster. We set this to 1 by default to make things simpler, but you can set it to 3 if you need a high-availability CouchDB cluster. |
|
| couchdb.clusterSize | int | `1` | The number of replicas to run in the CouchDB cluster. We set this to 1 by default to make things simpler, but you can set it to 3 if you need a high-availability CouchDB cluster. |
|
||||||
| couchdb.couchdbConfig.couchdb.uuid | string | `"budibase-couchdb"` | Unique identifier for this CouchDB server instance. You shouldn't need to change this. |
|
| couchdb.couchdbConfig.couchdb.uuid | string | `"budibase-couchdb"` | Unique identifier for this CouchDB server instance. You shouldn't need to change this. |
|
||||||
|
| couchdb.extraPorts[0] | object | `{"containerPort":4984,"name":"sqs"}` | Extra ports to expose on the CouchDB service. We expose the SQS port by default, but you can add more ports here if you need to. |
|
||||||
| couchdb.image | object | `{}` | We use a custom CouchDB image for running Budibase and we don't support using any other CouchDB image. You shouldn't change this, and if you do we can't guarantee that Budibase will work. |
|
| couchdb.image | object | `{}` | We use a custom CouchDB image for running Budibase and we don't support using any other CouchDB image. You shouldn't change this, and if you do we can't guarantee that Budibase will work. |
|
||||||
|
| couchdb.service.extraPorts[0] | object | `{"name":"sqs","port":4984,"protocol":"TCP","targetPort":4984}` | Extra ports to expose on the CouchDB service. We expose the SQS port by default, but you can add more ports here if you need to. |
|
||||||
| globals.apiEncryptionKey | string | `""` | Used for encrypting API keys and environment variables when stored in the database. You don't need to set this if `createSecrets` is true. |
|
| globals.apiEncryptionKey | string | `""` | Used for encrypting API keys and environment variables when stored in the database. You don't need to set this if `createSecrets` is true. |
|
||||||
| globals.appVersion | string | `""` | The version of Budibase to deploy. Defaults to what's specified by {{ .Chart.AppVersion }}. Ends up being used as the image version tag for the apps, proxy, and worker images. |
|
| globals.appVersion | string | `""` | The version of Budibase to deploy. Defaults to what's specified by {{ .Chart.AppVersion }}. Ends up being used as the image version tag for the apps, proxy, and worker images. |
|
||||||
| globals.automationMaxIterations | string | `"200"` | The maximum number of iterations allows for an automation loop step. You can read more about looping here: <https://docs.budibase.com/docs/looping>. |
|
| globals.automationMaxIterations | string | `"200"` | The maximum number of iterations allows for an automation loop step. You can read more about looping here: <https://docs.budibase.com/docs/looping>. |
|
||||||
|
@ -135,6 +137,8 @@ $ helm install --create-namespace --namespace budibase budibase . -f values.yaml
|
||||||
| globals.smtp.password | string | `""` | The password to use when authenticating with your SMTP server. |
|
| globals.smtp.password | string | `""` | The password to use when authenticating with your SMTP server. |
|
||||||
| globals.smtp.port | string | `"587"` | The port of your SMTP server. |
|
| globals.smtp.port | string | `"587"` | The port of your SMTP server. |
|
||||||
| globals.smtp.user | string | `""` | The username to use when authenticating with your SMTP server. |
|
| globals.smtp.user | string | `""` | The username to use when authenticating with your SMTP server. |
|
||||||
|
| globals.sqs.enabled | bool | `false` | Whether to use the CouchDB "structured query service" or not. This is disabled by default for now, but will become the default in a future release. |
|
||||||
|
| globals.tempBucketName | string | `""` | |
|
||||||
| globals.tenantFeatureFlags | string | `"*:LICENSING,*:USER_GROUPS,*:ONBOARDING_TOUR"` | Sets what feature flags are enabled and for which tenants. Should not ordinarily need to be changed. |
|
| globals.tenantFeatureFlags | string | `"*:LICENSING,*:USER_GROUPS,*:ONBOARDING_TOUR"` | Sets what feature flags are enabled and for which tenants. Should not ordinarily need to be changed. |
|
||||||
| imagePullSecrets | list | `[]` | Passed to all pods created by this chart. Should not ordinarily need to be changed. |
|
| imagePullSecrets | list | `[]` | Passed to all pods created by this chart. Should not ordinarily need to be changed. |
|
||||||
| ingress.className | string | `""` | What ingress class to use. |
|
| ingress.className | string | `""` | What ingress class to use. |
|
||||||
|
@ -152,6 +156,7 @@ $ helm install --create-namespace --namespace budibase budibase . -f values.yaml
|
||||||
| services.apps.autoscaling.targetCPUUtilizationPercentage | int | `80` | Target CPU utilization percentage for the apps service. Note that for autoscaling to work, you will need to have metrics-server configured, and resources set for the apps pods. |
|
| services.apps.autoscaling.targetCPUUtilizationPercentage | int | `80` | Target CPU utilization percentage for the apps service. Note that for autoscaling to work, you will need to have metrics-server configured, and resources set for the apps pods. |
|
||||||
| services.apps.extraContainers | list | `[]` | Additional containers to be added to the apps pod. |
|
| services.apps.extraContainers | list | `[]` | Additional containers to be added to the apps pod. |
|
||||||
| services.apps.extraEnv | list | `[]` | Extra environment variables to set for apps pods. Takes a list of name=value pairs. |
|
| services.apps.extraEnv | list | `[]` | Extra environment variables to set for apps pods. Takes a list of name=value pairs. |
|
||||||
|
| services.apps.extraEnvFromSecret | list | `[]` | Name of the K8s Secret in the same namespace which contains the extra environment variables. This can be used to avoid storing sensitive information in the values.yaml file. |
|
||||||
| services.apps.extraVolumeMounts | list | `[]` | Additional volumeMounts to the main apps container. |
|
| services.apps.extraVolumeMounts | list | `[]` | Additional volumeMounts to the main apps container. |
|
||||||
| services.apps.extraVolumes | list | `[]` | Additional volumes to the apps pod. |
|
| services.apps.extraVolumes | list | `[]` | Additional volumes to the apps pod. |
|
||||||
| services.apps.httpLogging | int | `1` | Whether or not to log HTTP requests to the apps service. |
|
| services.apps.httpLogging | int | `1` | Whether or not to log HTTP requests to the apps service. |
|
||||||
|
@ -168,6 +173,7 @@ $ helm install --create-namespace --namespace budibase budibase . -f values.yaml
|
||||||
| services.automationWorkers.enabled | bool | `true` | Whether or not to enable the automation worker service. If you disable this, automations will be processed by the apps service. |
|
| services.automationWorkers.enabled | bool | `true` | Whether or not to enable the automation worker service. If you disable this, automations will be processed by the apps service. |
|
||||||
| services.automationWorkers.extraContainers | list | `[]` | Additional containers to be added to the automationWorkers pod. |
|
| services.automationWorkers.extraContainers | list | `[]` | Additional containers to be added to the automationWorkers pod. |
|
||||||
| services.automationWorkers.extraEnv | list | `[]` | Extra environment variables to set for automation worker pods. Takes a list of name=value pairs. |
|
| services.automationWorkers.extraEnv | list | `[]` | Extra environment variables to set for automation worker pods. Takes a list of name=value pairs. |
|
||||||
|
| services.automationWorkers.extraEnvFromSecret | list | `[]` | Name of the K8s Secret in the same namespace which contains the extra environment variables. This can be used to avoid storing sensitive information in the values.yaml file. |
|
||||||
| services.automationWorkers.extraVolumeMounts | list | `[]` | Additional volumeMounts to the main automationWorkers container. |
|
| services.automationWorkers.extraVolumeMounts | list | `[]` | Additional volumeMounts to the main automationWorkers container. |
|
||||||
| services.automationWorkers.extraVolumes | list | `[]` | Additional volumes to the automationWorkers pod. |
|
| services.automationWorkers.extraVolumes | list | `[]` | Additional volumes to the automationWorkers pod. |
|
||||||
| services.automationWorkers.livenessProbe | object | HTTP health checks. | Liveness probe configuration for automation worker pods. You shouldn't need to change this, but if you want to you can find more information here: <https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-startup-probes/> |
|
| services.automationWorkers.livenessProbe | object | HTTP health checks. | Liveness probe configuration for automation worker pods. You shouldn't need to change this, but if you want to you can find more information here: <https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-startup-probes/> |
|
||||||
|
@ -195,7 +201,7 @@ $ helm install --create-namespace --namespace budibase budibase . -f values.yaml
|
||||||
| services.objectStore.region | string | `""` | AWS_REGION if using S3 |
|
| services.objectStore.region | string | `""` | AWS_REGION if using S3 |
|
||||||
| services.objectStore.resources | object | `{}` | The resources to use for Minio pods. See <https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/> for more information on how to set these. |
|
| services.objectStore.resources | object | `{}` | The resources to use for Minio pods. See <https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/> for more information on how to set these. |
|
||||||
| services.objectStore.secretKey | string | `""` | AWS_SECRET_ACCESS_KEY if using S3 |
|
| services.objectStore.secretKey | string | `""` | AWS_SECRET_ACCESS_KEY if using S3 |
|
||||||
| services.objectStore.storage | string | `"100Mi"` | How much storage to give Minio in its PersistentVolumeClaim. |
|
| services.objectStore.storage | string | `"2Gi"` | How much storage to give Minio in its PersistentVolumeClaim. |
|
||||||
| services.objectStore.storageClass | string | `""` | If defined, storageClassName: <storageClass> If set to "-", storageClassName: "", which disables dynamic provisioning If undefined (the default) or set to null, no storageClassName spec is set, choosing the default provisioner. |
|
| services.objectStore.storageClass | string | `""` | If defined, storageClassName: <storageClass> If set to "-", storageClassName: "", which disables dynamic provisioning If undefined (the default) or set to null, no storageClassName spec is set, choosing the default provisioner. |
|
||||||
| services.objectStore.url | string | `"http://minio-service:9000"` | URL to use for object storage. Only change this if you're using an external object store, such as S3. Remember to set `minio: false` if you do this. |
|
| services.objectStore.url | string | `"http://minio-service:9000"` | URL to use for object storage. Only change this if you're using an external object store, such as S3. Remember to set `minio: false` if you do this. |
|
||||||
| services.proxy.autoscaling.enabled | bool | `false` | Whether to enable horizontal pod autoscaling for the proxy service. |
|
| services.proxy.autoscaling.enabled | bool | `false` | Whether to enable horizontal pod autoscaling for the proxy service. |
|
||||||
|
@ -227,6 +233,7 @@ $ helm install --create-namespace --namespace budibase budibase . -f values.yaml
|
||||||
| services.worker.autoscaling.targetCPUUtilizationPercentage | int | `80` | Target CPU utilization percentage for the worker service. Note that for autoscaling to work, you will need to have metrics-server configured, and resources set for the worker pods. |
|
| services.worker.autoscaling.targetCPUUtilizationPercentage | int | `80` | Target CPU utilization percentage for the worker service. Note that for autoscaling to work, you will need to have metrics-server configured, and resources set for the worker pods. |
|
||||||
| services.worker.extraContainers | list | `[]` | Additional containers to be added to the worker pod. |
|
| services.worker.extraContainers | list | `[]` | Additional containers to be added to the worker pod. |
|
||||||
| services.worker.extraEnv | list | `[]` | Extra environment variables to set for worker pods. Takes a list of name=value pairs. |
|
| services.worker.extraEnv | list | `[]` | Extra environment variables to set for worker pods. Takes a list of name=value pairs. |
|
||||||
|
| services.worker.extraEnvFromSecret | list | `[]` | Name of the K8s Secret in the same namespace which contains the extra environment variables. This can be used to avoid storing sensitive information in the values.yaml file. |
|
||||||
| services.worker.extraVolumeMounts | list | `[]` | Additional volumeMounts to the main worker container. |
|
| services.worker.extraVolumeMounts | list | `[]` | Additional volumeMounts to the main worker container. |
|
||||||
| services.worker.extraVolumes | list | `[]` | Additional volumes to the worker pod. |
|
| services.worker.extraVolumes | list | `[]` | Additional volumes to the worker pod. |
|
||||||
| services.worker.httpLogging | int | `1` | Whether or not to log HTTP requests to the worker service. |
|
| services.worker.httpLogging | int | `1` | Whether or not to log HTTP requests to the worker service. |
|
||||||
|
|
Binary file not shown.
Binary file not shown.
|
@ -42,6 +42,14 @@ 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 }}
|
||||||
|
{{ if .Values.globals.sqs.enabled }}
|
||||||
|
- name: COUCH_DB_SQL_URL
|
||||||
|
{{ if .Values.globals.sqs.url }}
|
||||||
|
value: {{ .Values.globals.sqs.url }}
|
||||||
|
{{ else }}
|
||||||
|
value: http://{{ .Release.Name }}-svc-couchdb:{{ .Values.globals.sqs.port }}
|
||||||
|
{{ end }}
|
||||||
|
{{ end }}
|
||||||
{{ if .Values.services.couchdb.enabled }}
|
{{ if .Values.services.couchdb.enabled }}
|
||||||
- name: COUCH_DB_USER
|
- name: COUCH_DB_USER
|
||||||
valueFrom:
|
valueFrom:
|
||||||
|
@ -198,10 +206,21 @@ spec:
|
||||||
- name: APP_FEATURES
|
- name: APP_FEATURES
|
||||||
value: "api"
|
value: "api"
|
||||||
{{- end }}
|
{{- end }}
|
||||||
|
{{- if .Values.globals.sqs.enabled }}
|
||||||
|
- name: SQS_SEARCH_ENABLE
|
||||||
|
value: "true"
|
||||||
|
{{- end }}
|
||||||
{{- range .Values.services.apps.extraEnv }}
|
{{- range .Values.services.apps.extraEnv }}
|
||||||
- name: {{ .name }}
|
- name: {{ .name }}
|
||||||
value: {{ .value | quote }}
|
value: {{ .value | quote }}
|
||||||
{{- end }}
|
{{- end }}
|
||||||
|
{{- range .Values.services.apps.extraEnvFromSecret}}
|
||||||
|
- name: {{ .name }}
|
||||||
|
valueFrom:
|
||||||
|
secretKeyRef:
|
||||||
|
name: {{ .secretName }}
|
||||||
|
key: {{ .secretKey | quote }}
|
||||||
|
{{- end}}
|
||||||
image: budibase/apps:{{ .Values.globals.appVersion | default .Chart.AppVersion }}
|
image: budibase/apps:{{ .Values.globals.appVersion | default .Chart.AppVersion }}
|
||||||
imagePullPolicy: Always
|
imagePullPolicy: Always
|
||||||
{{- if .Values.services.apps.startupProbe }}
|
{{- if .Values.services.apps.startupProbe }}
|
||||||
|
|
|
@ -201,6 +201,13 @@ spec:
|
||||||
- name: {{ .name }}
|
- name: {{ .name }}
|
||||||
value: {{ .value | quote }}
|
value: {{ .value | quote }}
|
||||||
{{- end }}
|
{{- end }}
|
||||||
|
{{- range .Values.services.automationWorkers.extraEnvFromSecret}}
|
||||||
|
- name: {{ .name }}
|
||||||
|
valueFrom:
|
||||||
|
secretKeyRef:
|
||||||
|
name: {{ .secretName }}
|
||||||
|
key: {{ .secretKey | quote }}
|
||||||
|
{{- end}}
|
||||||
|
|
||||||
image: budibase/apps:{{ .Values.globals.appVersion | default .Chart.AppVersion }}
|
image: budibase/apps:{{ .Values.globals.appVersion | default .Chart.AppVersion }}
|
||||||
imagePullPolicy: Always
|
imagePullPolicy: Always
|
||||||
|
@ -272,4 +279,4 @@ spec:
|
||||||
{{- toYaml .Values.services.automationWorkers.extraVolumes | nindent 8 }}
|
{{- toYaml .Values.services.automationWorkers.extraVolumes | nindent 8 }}
|
||||||
{{ end }}
|
{{ end }}
|
||||||
status: {}
|
status: {}
|
||||||
{{- end }}
|
{{- end }}
|
||||||
|
|
|
@ -56,6 +56,14 @@ 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 }}
|
||||||
|
{{ if .Values.globals.sqs.enabled }}
|
||||||
|
- name: COUCH_DB_SQL_URL
|
||||||
|
{{ if .Values.globals.sqs.url }}
|
||||||
|
value: {{ .Values.globals.sqs.url }}
|
||||||
|
{{ else }}
|
||||||
|
value: http://{{ .Release.Name }}-svc-couchdb:{{ .Values.globals.sqs.port }}
|
||||||
|
{{ end }}
|
||||||
|
{{ end }}
|
||||||
- name: API_ENCRYPTION_KEY
|
- name: API_ENCRYPTION_KEY
|
||||||
value: {{ .Values.globals.apiEncryptionKey | quote }}
|
value: {{ .Values.globals.apiEncryptionKey | quote }}
|
||||||
- name: HTTP_LOGGING
|
- name: HTTP_LOGGING
|
||||||
|
@ -184,10 +192,21 @@ spec:
|
||||||
- name: NODE_TLS_REJECT_UNAUTHORIZED
|
- name: NODE_TLS_REJECT_UNAUTHORIZED
|
||||||
value: {{ .Values.services.tlsRejectUnauthorized }}
|
value: {{ .Values.services.tlsRejectUnauthorized }}
|
||||||
{{ end }}
|
{{ end }}
|
||||||
|
{{- if .Values.globals.sqs.enabled }}
|
||||||
|
- name: SQS_SEARCH_ENABLE
|
||||||
|
value: "true"
|
||||||
|
{{- end }}
|
||||||
{{- range .Values.services.worker.extraEnv }}
|
{{- range .Values.services.worker.extraEnv }}
|
||||||
- name: {{ .name }}
|
- name: {{ .name }}
|
||||||
value: {{ .value | quote }}
|
value: {{ .value | quote }}
|
||||||
{{- end }}
|
{{- end }}
|
||||||
|
{{- range .Values.services.worker.extraEnvFromSecret}}
|
||||||
|
- name: {{ .name }}
|
||||||
|
valueFrom:
|
||||||
|
secretKeyRef:
|
||||||
|
name: {{ .secretName }}
|
||||||
|
key: {{ .secretKey | quote }}
|
||||||
|
{{- end}}
|
||||||
image: budibase/worker:{{ .Values.globals.appVersion | default .Chart.AppVersion }}
|
image: budibase/worker:{{ .Values.globals.appVersion | default .Chart.AppVersion }}
|
||||||
imagePullPolicy: Always
|
imagePullPolicy: Always
|
||||||
{{- if .Values.services.worker.startupProbe }}
|
{{- if .Values.services.worker.startupProbe }}
|
||||||
|
|
|
@ -138,6 +138,15 @@ globals:
|
||||||
# -- The password to use when authenticating with your SMTP server.
|
# -- The password to use when authenticating with your SMTP server.
|
||||||
password: ""
|
password: ""
|
||||||
|
|
||||||
|
sqs:
|
||||||
|
# -- Whether to use the CouchDB "structured query service" or not. This is disabled by
|
||||||
|
# default for now, but will become the default in a future release.
|
||||||
|
enabled: false
|
||||||
|
# @ignore
|
||||||
|
url: ""
|
||||||
|
# @ignore
|
||||||
|
port: "4984"
|
||||||
|
|
||||||
services:
|
services:
|
||||||
# -- The DNS suffix to use for service discovery. You only need to change this
|
# -- The DNS suffix to use for service discovery. You only need to change this
|
||||||
# if you've configured your cluster to use a different DNS suffix.
|
# if you've configured your cluster to use a different DNS suffix.
|
||||||
|
@ -240,6 +249,13 @@ services:
|
||||||
# -- Extra environment variables to set for apps pods. Takes a list of
|
# -- Extra environment variables to set for apps pods. Takes a list of
|
||||||
# name=value pairs.
|
# name=value pairs.
|
||||||
extraEnv: []
|
extraEnv: []
|
||||||
|
# -- Name of the K8s Secret in the same namespace which contains the extra environment variables.
|
||||||
|
# This can be used to avoid storing sensitive information in the values.yaml file.
|
||||||
|
extraEnvFromSecret: []
|
||||||
|
# - name: MY_SECRET_KEY
|
||||||
|
# secretName : my-secret
|
||||||
|
# secretKey: my-secret-key
|
||||||
|
|
||||||
# -- Startup probe configuration for apps pods. You shouldn't need to
|
# -- Startup probe configuration for apps pods. You shouldn't need to
|
||||||
# change this, but if you want to you can find more information here:
|
# change this, but if you want to you can find more information here:
|
||||||
# <https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-startup-probes/>
|
# <https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-startup-probes/>
|
||||||
|
@ -323,6 +339,13 @@ services:
|
||||||
# -- Extra environment variables to set for automation worker pods. Takes a list of
|
# -- Extra environment variables to set for automation worker pods. Takes a list of
|
||||||
# name=value pairs.
|
# name=value pairs.
|
||||||
extraEnv: []
|
extraEnv: []
|
||||||
|
# -- Name of the K8s Secret in the same namespace which contains the extra environment variables.
|
||||||
|
# This can be used to avoid storing sensitive information in the values.yaml file.
|
||||||
|
extraEnvFromSecret: []
|
||||||
|
# - name: MY_SECRET_KEY
|
||||||
|
# secretName : my-secret
|
||||||
|
# secretKey: my-secret-key
|
||||||
|
|
||||||
# -- Startup probe configuration for automation worker pods. You shouldn't
|
# -- Startup probe configuration for automation worker pods. You shouldn't
|
||||||
# need to change this, but if you want to you can find more information
|
# need to change this, but if you want to you can find more information
|
||||||
# here:
|
# here:
|
||||||
|
@ -408,6 +431,13 @@ services:
|
||||||
# -- Extra environment variables to set for worker pods. Takes a list of
|
# -- Extra environment variables to set for worker pods. Takes a list of
|
||||||
# name=value pairs.
|
# name=value pairs.
|
||||||
extraEnv: []
|
extraEnv: []
|
||||||
|
# -- Name of the K8s Secret in the same namespace which contains the extra environment variables.
|
||||||
|
# This can be used to avoid storing sensitive information in the values.yaml file.
|
||||||
|
extraEnvFromSecret: []
|
||||||
|
# - name: MY_SECRET_KEY
|
||||||
|
# secretName : my-secret
|
||||||
|
# secretKey: my-secret-key
|
||||||
|
|
||||||
# -- Startup probe configuration for worker pods. You shouldn't need to
|
# -- Startup probe configuration for worker pods. You shouldn't need to
|
||||||
# change this, but if you want to you can find more information here:
|
# change this, but if you want to you can find more information here:
|
||||||
# <https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-startup-probes/>
|
# <https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-startup-probes/>
|
||||||
|
@ -611,10 +641,25 @@ couchdb:
|
||||||
# @ignore
|
# @ignore
|
||||||
repository: budibase/couchdb
|
repository: budibase/couchdb
|
||||||
# @ignore
|
# @ignore
|
||||||
tag: v3.2.1
|
tag: v3.3.3
|
||||||
# @ignore
|
# @ignore
|
||||||
pullPolicy: Always
|
pullPolicy: Always
|
||||||
|
|
||||||
|
extraPorts:
|
||||||
|
# -- Extra ports to expose on the CouchDB service. We expose the SQS port
|
||||||
|
# by default, but you can add more ports here if you need to.
|
||||||
|
- name: sqs
|
||||||
|
containerPort: 4984
|
||||||
|
|
||||||
|
service:
|
||||||
|
extraPorts:
|
||||||
|
# -- Extra ports to expose on the CouchDB service. We expose the SQS port
|
||||||
|
# by default, but you can add more ports here if you need to.
|
||||||
|
- name: sqs
|
||||||
|
port: 4984
|
||||||
|
targetPort: 4984
|
||||||
|
protocol: TCP
|
||||||
|
|
||||||
# @ignore
|
# @ignore
|
||||||
# This should remain false. We ship Clouseau ourselves as part of the
|
# This should remain false. We ship Clouseau ourselves as part of the
|
||||||
# budibase/couchdb image, and it's not possible to disable it because it's a
|
# budibase/couchdb image, and it's not possible to disable it because it's a
|
||||||
|
|
|
@ -22,6 +22,6 @@
|
||||||
"@types/react": "17.0.39",
|
"@types/react": "17.0.39",
|
||||||
"eslint": "8.10.0",
|
"eslint": "8.10.0",
|
||||||
"eslint-config-next": "12.1.0",
|
"eslint-config-next": "12.1.0",
|
||||||
"typescript": "5.2.2"
|
"typescript": "5.5.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -333,11 +333,11 @@ brace-expansion@^1.1.7:
|
||||||
concat-map "0.0.1"
|
concat-map "0.0.1"
|
||||||
|
|
||||||
braces@^3.0.1, braces@~3.0.2:
|
braces@^3.0.1, braces@~3.0.2:
|
||||||
version "3.0.2"
|
version "3.0.3"
|
||||||
resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107"
|
resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.3.tgz#490332f40919452272d55a8480adc0c441358789"
|
||||||
integrity sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==
|
integrity sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==
|
||||||
dependencies:
|
dependencies:
|
||||||
fill-range "^7.0.1"
|
fill-range "^7.1.1"
|
||||||
|
|
||||||
bulma@^0.9.3:
|
bulma@^0.9.3:
|
||||||
version "0.9.3"
|
version "0.9.3"
|
||||||
|
@ -781,10 +781,10 @@ file-entry-cache@^6.0.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
flat-cache "^3.0.4"
|
flat-cache "^3.0.4"
|
||||||
|
|
||||||
fill-range@^7.0.1:
|
fill-range@^7.1.1:
|
||||||
version "7.0.1"
|
version "7.1.1"
|
||||||
resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.0.1.tgz#1919a6a7c75fe38b2c7c77e5198535da9acdda40"
|
resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.1.1.tgz#44265d3cac07e3ea7dc247516380643754a05292"
|
||||||
integrity sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==
|
integrity sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==
|
||||||
dependencies:
|
dependencies:
|
||||||
to-regex-range "^5.0.1"
|
to-regex-range "^5.0.1"
|
||||||
|
|
||||||
|
@ -1709,10 +1709,10 @@ type-fest@^0.20.2:
|
||||||
resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.20.2.tgz#1bf207f4b28f91583666cb5fbd327887301cd5f4"
|
resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.20.2.tgz#1bf207f4b28f91583666cb5fbd327887301cd5f4"
|
||||||
integrity sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==
|
integrity sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==
|
||||||
|
|
||||||
typescript@4.6.2:
|
typescript@5.2.2:
|
||||||
version "4.6.2"
|
version "5.2.2"
|
||||||
resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.6.2.tgz#fe12d2727b708f4eef40f51598b3398baa9611d4"
|
resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.2.2.tgz#5ebb5e5a5b75f085f22bc3f8460fba308310fa78"
|
||||||
integrity sha512-HM/hFigTBHZhLXshn9sN37H085+hQGeJHJ/X7LpBWLID/fbc2acUMfU+lGD98X81sKP+pFa9f0DZmCwB9GnbAg==
|
integrity sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w==
|
||||||
|
|
||||||
unbox-primitive@^1.0.1:
|
unbox-primitive@^1.0.1:
|
||||||
version "1.0.1"
|
version "1.0.1"
|
||||||
|
|
|
@ -74,6 +74,7 @@ http {
|
||||||
add_header X-Content-Type-Options nosniff always;
|
add_header X-Content-Type-Options nosniff always;
|
||||||
add_header X-XSS-Protection "1; mode=block" always;
|
add_header X-XSS-Protection "1; mode=block" always;
|
||||||
add_header Content-Security-Policy "${csp_default}; ${csp_script}; ${csp_style}; ${csp_object}; ${csp_base_uri}; ${csp_connect}; ${csp_font}; ${csp_frame}; ${csp_img}; ${csp_manifest}; ${csp_media}; ${csp_worker};" always;
|
add_header Content-Security-Policy "${csp_default}; ${csp_script}; ${csp_style}; ${csp_object}; ${csp_base_uri}; ${csp_connect}; ${csp_font}; ${csp_frame}; ${csp_img}; ${csp_manifest}; ${csp_media}; ${csp_worker};" always;
|
||||||
|
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;
|
||||||
|
|
||||||
# upstreams
|
# upstreams
|
||||||
set $apps ${APPS_UPSTREAM_URL};
|
set $apps ${APPS_UPSTREAM_URL};
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
{
|
{
|
||||||
"version": "2.27.5",
|
"version": "2.29.5",
|
||||||
"npmClient": "yarn",
|
"npmClient": "yarn",
|
||||||
"packages": [
|
"packages": [
|
||||||
"packages/*",
|
"packages/*",
|
||||||
|
|
|
@ -27,7 +27,7 @@
|
||||||
"proper-lockfile": "^4.1.2",
|
"proper-lockfile": "^4.1.2",
|
||||||
"svelte": "^4.2.10",
|
"svelte": "^4.2.10",
|
||||||
"svelte-eslint-parser": "^0.33.1",
|
"svelte-eslint-parser": "^0.33.1",
|
||||||
"typescript": "5.2.2",
|
"typescript": "5.5.2",
|
||||||
"typescript-eslint": "^7.3.1",
|
"typescript-eslint": "^7.3.1",
|
||||||
"yargs": "^17.7.2"
|
"yargs": "^17.7.2"
|
||||||
},
|
},
|
||||||
|
@ -37,10 +37,10 @@
|
||||||
"build": "NODE_OPTIONS=--max-old-space-size=1500 lerna run build --stream",
|
"build": "NODE_OPTIONS=--max-old-space-size=1500 lerna run build --stream",
|
||||||
"build:apps": "yarn build --scope @budibase/server --scope @budibase/worker",
|
"build:apps": "yarn build --scope @budibase/server --scope @budibase/worker",
|
||||||
"build:cli": "yarn build --scope @budibase/cli",
|
"build:cli": "yarn build --scope @budibase/cli",
|
||||||
"build:oss": "NODE_OPTIONS=--max-old-space-size=1500 lerna run build --stream --ignore @budibase/account-portal --ignore @budibase/account-portal-server --ignore @budibase/account-portal-ui",
|
"build:oss": "NODE_OPTIONS=--max-old-space-size=1500 lerna run build --stream --ignore @budibase/account-portal-server --ignore @budibase/account-portal-ui",
|
||||||
"build:account-portal": "NODE_OPTIONS=--max-old-space-size=1500 lerna run build --stream --scope @budibase/account-portal --scope @budibase/account-portal-server --scope @budibase/account-portal-ui",
|
"build:account-portal": "NODE_OPTIONS=--max-old-space-size=1500 lerna run build --stream --scope @budibase/account-portal-server --scope @budibase/account-portal-ui",
|
||||||
"build:dev": "lerna run --stream prebuild && yarn nx run-many --target=build --output-style=dynamic --watch --preserveWatchOutput",
|
"build:dev": "lerna run --stream prebuild && yarn nx run-many --target=build --output-style=dynamic --watch --preserveWatchOutput",
|
||||||
"check:types": "lerna run --concurrency 2 check:types",
|
"check:types": "lerna run --concurrency 2 check:types --ignore @budibase/account-portal-server",
|
||||||
"build:sdk": "lerna run --stream build:sdk",
|
"build:sdk": "lerna run --stream build:sdk",
|
||||||
"deps:circular": "madge packages/server/dist/index.js packages/worker/src/index.ts packages/backend-core/dist/src/index.js packages/cli/src/index.js --circular",
|
"deps:circular": "madge packages/server/dist/index.js packages/worker/src/index.ts packages/backend-core/dist/src/index.js packages/cli/src/index.js --circular",
|
||||||
"release": "lerna publish from-package --yes --force-publish --no-git-tag-version --no-push --no-git-reset",
|
"release": "lerna publish from-package --yes --force-publish --no-git-tag-version --no-push --no-git-reset",
|
||||||
|
|
|
@ -1 +1 @@
|
||||||
Subproject commit 39acfff42a063e5a8a7d58d36721ec3103e16348
|
Subproject commit ff16525b73c5751d344f5c161a682609c0a993f2
|
|
@ -16,7 +16,7 @@
|
||||||
"prepack": "cp package.json dist",
|
"prepack": "cp package.json dist",
|
||||||
"build": "tsc -p tsconfig.build.json --paths null && node ./scripts/build.js",
|
"build": "tsc -p tsconfig.build.json --paths null && node ./scripts/build.js",
|
||||||
"build:dev": "yarn prebuild && tsc --build --watch --preserveWatchOutput",
|
"build:dev": "yarn prebuild && tsc --build --watch --preserveWatchOutput",
|
||||||
"check:types": "tsc -p tsconfig.json --noEmit --paths null",
|
"check:types": "tsc -p tsconfig.json --noEmit --paths null --target es2020",
|
||||||
"test": "bash scripts/test.sh",
|
"test": "bash scripts/test.sh",
|
||||||
"test:watch": "jest --watchAll"
|
"test:watch": "jest --watchAll"
|
||||||
},
|
},
|
||||||
|
@ -79,7 +79,7 @@
|
||||||
"pouchdb-adapter-memory": "7.2.2",
|
"pouchdb-adapter-memory": "7.2.2",
|
||||||
"testcontainers": "^10.7.2",
|
"testcontainers": "^10.7.2",
|
||||||
"timekeeper": "2.2.0",
|
"timekeeper": "2.2.0",
|
||||||
"typescript": "5.2.2"
|
"typescript": "5.5.2"
|
||||||
},
|
},
|
||||||
"nx": {
|
"nx": {
|
||||||
"targets": {
|
"targets": {
|
||||||
|
|
|
@ -72,4 +72,4 @@ export const DEFAULT_JOBS_TABLE_ID = "ta_bb_jobs"
|
||||||
export const DEFAULT_INVENTORY_TABLE_ID = "ta_bb_inventory"
|
export const DEFAULT_INVENTORY_TABLE_ID = "ta_bb_inventory"
|
||||||
export const DEFAULT_EXPENSES_TABLE_ID = "ta_bb_expenses"
|
export const DEFAULT_EXPENSES_TABLE_ID = "ta_bb_expenses"
|
||||||
export const DEFAULT_EMPLOYEE_TABLE_ID = "ta_bb_employee"
|
export const DEFAULT_EMPLOYEE_TABLE_ID = "ta_bb_employee"
|
||||||
export const DEFAULT_BB_DATASOURCE_ID = "datasource_internal_bb_default"
|
export { DEFAULT_BB_DATASOURCE_ID } from "@budibase/shared-core"
|
||||||
|
|
|
@ -1,14 +1,5 @@
|
||||||
export const CONSTANT_INTERNAL_ROW_COLS = [
|
export {
|
||||||
"_id",
|
CONSTANT_INTERNAL_ROW_COLS,
|
||||||
"_rev",
|
CONSTANT_EXTERNAL_ROW_COLS,
|
||||||
"type",
|
isInternalColumnName,
|
||||||
"createdAt",
|
} from "@budibase/shared-core"
|
||||||
"updatedAt",
|
|
||||||
"tableId",
|
|
||||||
] as const
|
|
||||||
|
|
||||||
export const CONSTANT_EXTERNAL_ROW_COLS = ["_id", "_rev", "tableId"] as const
|
|
||||||
|
|
||||||
export function isInternalColumnName(name: string): boolean {
|
|
||||||
return (CONSTANT_INTERNAL_ROW_COLS as readonly string[]).includes(name)
|
|
||||||
}
|
|
||||||
|
|
|
@ -8,6 +8,7 @@ import {
|
||||||
DatabaseOpts,
|
DatabaseOpts,
|
||||||
DatabasePutOpts,
|
DatabasePutOpts,
|
||||||
DatabaseQueryOpts,
|
DatabaseQueryOpts,
|
||||||
|
DBError,
|
||||||
Document,
|
Document,
|
||||||
isDocument,
|
isDocument,
|
||||||
RowResponse,
|
RowResponse,
|
||||||
|
@ -41,7 +42,7 @@ function buildNano(couchInfo: { url: string; cookie: string }) {
|
||||||
|
|
||||||
type DBCall<T> = () => Promise<T>
|
type DBCall<T> = () => Promise<T>
|
||||||
|
|
||||||
class CouchDBError extends Error {
|
class CouchDBError extends Error implements DBError {
|
||||||
status: number
|
status: number
|
||||||
statusCode: number
|
statusCode: number
|
||||||
reason: string
|
reason: string
|
||||||
|
@ -328,7 +329,14 @@ export class DatabaseImpl implements Database {
|
||||||
async sqlDiskCleanup(): Promise<void> {
|
async sqlDiskCleanup(): Promise<void> {
|
||||||
const dbName = this.name
|
const dbName = this.name
|
||||||
const url = `/${dbName}/_cleanup`
|
const url = `/${dbName}/_cleanup`
|
||||||
return await this._sqlQuery<void>(url, "POST")
|
try {
|
||||||
|
await this._sqlQuery<void>(url, "POST")
|
||||||
|
} catch (err: any) {
|
||||||
|
// hack for now - SQS throws a 500 when there is nothing to clean-up
|
||||||
|
if (err.status !== 500) {
|
||||||
|
throw err
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// removes a document from sqlite
|
// removes a document from sqlite
|
||||||
|
@ -352,18 +360,15 @@ export class DatabaseImpl implements Database {
|
||||||
}
|
}
|
||||||
|
|
||||||
async destroy() {
|
async destroy() {
|
||||||
|
if (env.SQS_SEARCH_ENABLE && (await this.exists(SQLITE_DESIGN_DOC_ID))) {
|
||||||
|
// delete the design document, then run the cleanup operation
|
||||||
|
const definition = await this.get<SQLiteDefinition>(SQLITE_DESIGN_DOC_ID)
|
||||||
|
// remove all tables - save the definition then trigger a cleanup
|
||||||
|
definition.sql.tables = {}
|
||||||
|
await this.put(definition)
|
||||||
|
await this.sqlDiskCleanup()
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
if (env.SQS_SEARCH_ENABLE) {
|
|
||||||
// delete the design document, then run the cleanup operation
|
|
||||||
try {
|
|
||||||
const definition = await this.get<SQLiteDefinition>(
|
|
||||||
SQLITE_DESIGN_DOC_ID
|
|
||||||
)
|
|
||||||
await this.remove(SQLITE_DESIGN_DOC_ID, definition._rev)
|
|
||||||
} finally {
|
|
||||||
await this.sqlDiskCleanup()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return await this.nano().db.destroy(this.name)
|
return await this.nano().db.destroy(this.name)
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
// didn't exist, don't worry
|
// didn't exist, don't worry
|
||||||
|
|
|
@ -93,15 +93,21 @@ function isApps() {
|
||||||
return environment.SERVICE_TYPE === ServiceType.APPS
|
return environment.SERVICE_TYPE === ServiceType.APPS
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isQA() {
|
||||||
|
return environment.BUDIBASE_ENVIRONMENT === "QA"
|
||||||
|
}
|
||||||
|
|
||||||
const environment = {
|
const environment = {
|
||||||
isTest,
|
isTest,
|
||||||
isJest,
|
isJest,
|
||||||
isDev,
|
isDev,
|
||||||
isWorker,
|
isWorker,
|
||||||
isApps,
|
isApps,
|
||||||
|
isQA,
|
||||||
isProd: () => {
|
isProd: () => {
|
||||||
return !isDev()
|
return !isDev()
|
||||||
},
|
},
|
||||||
|
BUDIBASE_ENVIRONMENT: process.env.BUDIBASE_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,
|
JWT_SECRET_FALLBACK: process.env.JWT_SECRET_FALLBACK,
|
||||||
|
@ -120,6 +126,7 @@ const environment = {
|
||||||
REDIS_CLUSTERED: process.env.REDIS_CLUSTERED,
|
REDIS_CLUSTERED: process.env.REDIS_CLUSTERED,
|
||||||
MINIO_ACCESS_KEY: process.env.MINIO_ACCESS_KEY,
|
MINIO_ACCESS_KEY: process.env.MINIO_ACCESS_KEY,
|
||||||
MINIO_SECRET_KEY: process.env.MINIO_SECRET_KEY,
|
MINIO_SECRET_KEY: process.env.MINIO_SECRET_KEY,
|
||||||
|
AWS_SESSION_TOKEN: process.env.AWS_SESSION_TOKEN,
|
||||||
AWS_REGION: process.env.AWS_REGION,
|
AWS_REGION: process.env.AWS_REGION,
|
||||||
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,
|
||||||
|
|
|
@ -3,7 +3,8 @@ import { Ctx } from "@budibase/types"
|
||||||
|
|
||||||
function validate(
|
function validate(
|
||||||
schema: Joi.ObjectSchema | Joi.ArraySchema,
|
schema: Joi.ObjectSchema | Joi.ArraySchema,
|
||||||
property: string
|
property: string,
|
||||||
|
opts: { errorPrefix: string } = { errorPrefix: `Invalid ${property}` }
|
||||||
) {
|
) {
|
||||||
// Return a Koa middleware function
|
// Return a Koa middleware function
|
||||||
return (ctx: Ctx, next: any) => {
|
return (ctx: Ctx, next: any) => {
|
||||||
|
@ -29,16 +30,26 @@ function validate(
|
||||||
|
|
||||||
const { error } = schema.validate(params)
|
const { error } = schema.validate(params)
|
||||||
if (error) {
|
if (error) {
|
||||||
ctx.throw(400, `Invalid ${property} - ${error.message}`)
|
let message = error.message
|
||||||
|
if (opts.errorPrefix) {
|
||||||
|
message = `Invalid ${property} - ${message}`
|
||||||
|
}
|
||||||
|
ctx.throw(400, message)
|
||||||
}
|
}
|
||||||
return next()
|
return next()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function body(schema: Joi.ObjectSchema | Joi.ArraySchema) {
|
export function body(
|
||||||
return validate(schema, "body")
|
schema: Joi.ObjectSchema | Joi.ArraySchema,
|
||||||
|
opts?: { errorPrefix: string }
|
||||||
|
) {
|
||||||
|
return validate(schema, "body", opts)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function params(schema: Joi.ObjectSchema | Joi.ArraySchema) {
|
export function params(
|
||||||
return validate(schema, "params")
|
schema: Joi.ObjectSchema | Joi.ArraySchema,
|
||||||
|
opts?: { errorPrefix: string }
|
||||||
|
) {
|
||||||
|
return validate(schema, "params", opts)
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,6 +14,7 @@ import { v4 } from "uuid"
|
||||||
import { APP_PREFIX, APP_DEV_PREFIX } from "../db"
|
import { APP_PREFIX, APP_DEV_PREFIX } from "../db"
|
||||||
import fsp from "fs/promises"
|
import fsp from "fs/promises"
|
||||||
import { HeadObjectOutput } from "aws-sdk/clients/s3"
|
import { HeadObjectOutput } from "aws-sdk/clients/s3"
|
||||||
|
import { ReadableStream } from "stream/web"
|
||||||
|
|
||||||
const streamPipeline = promisify(stream.pipeline)
|
const streamPipeline = promisify(stream.pipeline)
|
||||||
// use this as a temporary store of buckets that are being created
|
// use this as a temporary store of buckets that are being created
|
||||||
|
@ -41,10 +42,7 @@ type UploadParams = BaseUploadParams & {
|
||||||
path?: string | PathLike
|
path?: string | PathLike
|
||||||
}
|
}
|
||||||
|
|
||||||
export type StreamTypes =
|
export type StreamTypes = ReadStream | NodeJS.ReadableStream
|
||||||
| ReadStream
|
|
||||||
| NodeJS.ReadableStream
|
|
||||||
| ReadableStream<Uint8Array>
|
|
||||||
|
|
||||||
export type StreamUploadParams = BaseUploadParams & {
|
export type StreamUploadParams = BaseUploadParams & {
|
||||||
stream?: StreamTypes
|
stream?: StreamTypes
|
||||||
|
@ -103,6 +101,11 @@ export function ObjectStore(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// for AWS Credentials using temporary session token
|
||||||
|
if (!env.MINIO_ENABLED && env.AWS_SESSION_TOKEN) {
|
||||||
|
config.sessionToken = env.AWS_SESSION_TOKEN
|
||||||
|
}
|
||||||
|
|
||||||
// custom S3 is in use i.e. minio
|
// custom S3 is in use i.e. minio
|
||||||
if (env.MINIO_URL) {
|
if (env.MINIO_URL) {
|
||||||
if (opts.presigning && env.MINIO_ENABLED) {
|
if (opts.presigning && env.MINIO_ENABLED) {
|
||||||
|
@ -222,6 +225,9 @@ export async function streamUpload({
|
||||||
extra,
|
extra,
|
||||||
ttl,
|
ttl,
|
||||||
}: StreamUploadParams) {
|
}: StreamUploadParams) {
|
||||||
|
if (!stream) {
|
||||||
|
throw new Error("Stream to upload is invalid/undefined")
|
||||||
|
}
|
||||||
const extension = filename.split(".").pop()
|
const extension = filename.split(".").pop()
|
||||||
const objectStore = ObjectStore(bucketName)
|
const objectStore = ObjectStore(bucketName)
|
||||||
const bucketCreated = await createBucketIfNotExists(objectStore, bucketName)
|
const bucketCreated = await createBucketIfNotExists(objectStore, bucketName)
|
||||||
|
@ -251,14 +257,27 @@ export async function streamUpload({
|
||||||
: CONTENT_TYPE_MAP.txt
|
: CONTENT_TYPE_MAP.txt
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const bucket = sanitizeBucket(bucketName),
|
||||||
|
objKey = sanitizeKey(filename)
|
||||||
const params = {
|
const params = {
|
||||||
Bucket: sanitizeBucket(bucketName),
|
Bucket: bucket,
|
||||||
Key: sanitizeKey(filename),
|
Key: objKey,
|
||||||
Body: stream,
|
Body: stream,
|
||||||
ContentType: contentType,
|
ContentType: contentType,
|
||||||
...extra,
|
...extra,
|
||||||
}
|
}
|
||||||
return objectStore.upload(params).promise()
|
|
||||||
|
const details = await objectStore.upload(params).promise()
|
||||||
|
const headDetails = await objectStore
|
||||||
|
.headObject({
|
||||||
|
Bucket: bucket,
|
||||||
|
Key: objKey,
|
||||||
|
})
|
||||||
|
.promise()
|
||||||
|
return {
|
||||||
|
...details,
|
||||||
|
ContentLength: headDetails.ContentLength,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -63,12 +63,12 @@ class InMemoryQueue implements Partial<Queue> {
|
||||||
* Same callback API as Bull, each callback passed to this will consume messages as they are
|
* Same callback API as Bull, each callback passed to this will consume messages as they are
|
||||||
* available. Please note this is a queue service, not a notification service, so each
|
* available. Please note this is a queue service, not a notification service, so each
|
||||||
* consumer will receive different messages.
|
* consumer will receive different messages.
|
||||||
* @param func The callback function which will return a "Job", the same
|
|
||||||
* as the Bull API, within this job the property "data" contains the JSON message. Please
|
* as the Bull API, within this job the property "data" contains the JSON message. Please
|
||||||
* note this is incredibly limited compared to Bull as in reality the Job would contain
|
* note this is incredibly limited compared to Bull as in reality the Job would contain
|
||||||
* a lot more information about the queue and current status of Bull cluster.
|
* a lot more information about the queue and current status of Bull cluster.
|
||||||
*/
|
*/
|
||||||
async process(func: any) {
|
async process(concurrencyOrFunc: number | any, func?: any) {
|
||||||
|
func = typeof concurrencyOrFunc === "number" ? func : concurrencyOrFunc
|
||||||
this._emitter.on("message", async () => {
|
this._emitter.on("message", async () => {
|
||||||
if (this._messages.length <= 0) {
|
if (this._messages.length <= 0) {
|
||||||
return
|
return
|
||||||
|
|
|
@ -21,6 +21,7 @@ let cleanupInterval: NodeJS.Timeout
|
||||||
async function cleanup() {
|
async function cleanup() {
|
||||||
for (let queue of QUEUES) {
|
for (let queue of QUEUES) {
|
||||||
await queue.clean(CLEANUP_PERIOD_MS, "completed")
|
await queue.clean(CLEANUP_PERIOD_MS, "completed")
|
||||||
|
await queue.clean(CLEANUP_PERIOD_MS, "failed")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
import { Knex, knex } from "knex"
|
import { Knex, knex } from "knex"
|
||||||
import * as dbCore from "../db"
|
import * as dbCore from "../db"
|
||||||
import {
|
import {
|
||||||
isIsoDateString,
|
|
||||||
isValidFilter,
|
|
||||||
getNativeSql,
|
getNativeSql,
|
||||||
isExternalTable,
|
isExternalTable,
|
||||||
|
isIsoDateString,
|
||||||
|
isValidFilter,
|
||||||
} from "./utils"
|
} from "./utils"
|
||||||
import { SqlStatements } from "./sqlStatements"
|
import { SqlStatements } from "./sqlStatements"
|
||||||
import SqlTableQueryBuilder from "./sqlTable"
|
import SqlTableQueryBuilder from "./sqlTable"
|
||||||
|
@ -12,21 +12,21 @@ import {
|
||||||
BBReferenceFieldMetadata,
|
BBReferenceFieldMetadata,
|
||||||
FieldSchema,
|
FieldSchema,
|
||||||
FieldType,
|
FieldType,
|
||||||
|
INTERNAL_TABLE_SOURCE_ID,
|
||||||
JsonFieldMetadata,
|
JsonFieldMetadata,
|
||||||
|
JsonTypes,
|
||||||
Operation,
|
Operation,
|
||||||
|
prefixed,
|
||||||
QueryJson,
|
QueryJson,
|
||||||
SqlQuery,
|
QueryOptions,
|
||||||
RelationshipsJson,
|
RelationshipsJson,
|
||||||
SearchFilters,
|
SearchFilters,
|
||||||
SortDirection,
|
SortOrder,
|
||||||
|
SqlClient,
|
||||||
|
SqlQuery,
|
||||||
SqlQueryBinding,
|
SqlQueryBinding,
|
||||||
Table,
|
Table,
|
||||||
TableSourceType,
|
TableSourceType,
|
||||||
INTERNAL_TABLE_SOURCE_ID,
|
|
||||||
SqlClient,
|
|
||||||
QueryOptions,
|
|
||||||
JsonTypes,
|
|
||||||
prefixed,
|
|
||||||
} from "@budibase/types"
|
} from "@budibase/types"
|
||||||
import environment from "../environment"
|
import environment from "../environment"
|
||||||
import { helpers } from "@budibase/shared-core"
|
import { helpers } from "@budibase/shared-core"
|
||||||
|
@ -114,7 +114,7 @@ function generateSelectStatement(
|
||||||
): (string | Knex.Raw)[] | "*" {
|
): (string | Knex.Raw)[] | "*" {
|
||||||
const { resource, meta } = json
|
const { resource, meta } = json
|
||||||
|
|
||||||
if (!resource) {
|
if (!resource || !resource.fields || resource.fields.length === 0) {
|
||||||
return "*"
|
return "*"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -184,7 +184,11 @@ class InternalBuilder {
|
||||||
query: Knex.QueryBuilder,
|
query: Knex.QueryBuilder,
|
||||||
filters: SearchFilters | undefined,
|
filters: SearchFilters | undefined,
|
||||||
table: Table,
|
table: Table,
|
||||||
opts: { aliases?: Record<string, string>; relationship?: boolean }
|
opts: {
|
||||||
|
aliases?: Record<string, string>
|
||||||
|
relationship?: boolean
|
||||||
|
columnPrefix?: string
|
||||||
|
}
|
||||||
): Knex.QueryBuilder {
|
): Knex.QueryBuilder {
|
||||||
if (!filters) {
|
if (!filters) {
|
||||||
return query
|
return query
|
||||||
|
@ -192,7 +196,10 @@ class InternalBuilder {
|
||||||
filters = parseFilters(filters)
|
filters = parseFilters(filters)
|
||||||
// if all or specified in filters, then everything is an or
|
// if all or specified in filters, then everything is an or
|
||||||
const allOr = filters.allOr
|
const allOr = filters.allOr
|
||||||
const sqlStatements = new SqlStatements(this.client, table, { allOr })
|
const sqlStatements = new SqlStatements(this.client, table, {
|
||||||
|
allOr,
|
||||||
|
columnPrefix: opts.columnPrefix,
|
||||||
|
})
|
||||||
const tableName =
|
const tableName =
|
||||||
this.client === SqlClient.SQL_LITE ? table._id! : table.name
|
this.client === SqlClient.SQL_LITE ? table._id! : table.name
|
||||||
|
|
||||||
|
@ -397,9 +404,9 @@ class InternalBuilder {
|
||||||
contains(filters.containsAny, true)
|
contains(filters.containsAny, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const tableRef = opts?.aliases?.[table._id!] || table._id
|
||||||
// when searching internal tables make sure long looking for rows
|
// when searching internal tables make sure long looking for rows
|
||||||
if (filters.documentType && !isExternalTable(table)) {
|
if (filters.documentType && !isExternalTable(table) && tableRef) {
|
||||||
const tableRef = opts?.aliases?.[table._id!] || table._id
|
|
||||||
// has to be its own option, must always be AND onto the search
|
// has to be its own option, must always be AND onto the search
|
||||||
query.andWhereLike(
|
query.andWhereLike(
|
||||||
`${tableRef}._id`,
|
`${tableRef}._id`,
|
||||||
|
@ -410,28 +417,50 @@ class InternalBuilder {
|
||||||
return query
|
return query
|
||||||
}
|
}
|
||||||
|
|
||||||
addSorting(query: Knex.QueryBuilder, json: QueryJson): Knex.QueryBuilder {
|
addDistinctCount(
|
||||||
let { sort, paginate } = json
|
query: Knex.QueryBuilder,
|
||||||
|
json: QueryJson
|
||||||
|
): Knex.QueryBuilder {
|
||||||
const table = json.meta.table
|
const table = json.meta.table
|
||||||
|
const primary = table.primary
|
||||||
|
const aliases = json.tableAliases
|
||||||
|
const aliased =
|
||||||
|
table.name && aliases?.[table.name] ? aliases[table.name] : table.name
|
||||||
|
if (!primary) {
|
||||||
|
throw new Error("SQL counting requires primary key to be supplied")
|
||||||
|
}
|
||||||
|
return query.countDistinct(`${aliased}.${primary[0]} as total`)
|
||||||
|
}
|
||||||
|
|
||||||
|
addSorting(query: Knex.QueryBuilder, json: QueryJson): Knex.QueryBuilder {
|
||||||
|
let { sort } = json
|
||||||
|
const table = json.meta.table
|
||||||
|
const primaryKey = table.primary
|
||||||
const tableName = getTableName(table)
|
const tableName = getTableName(table)
|
||||||
const aliases = json.tableAliases
|
const aliases = json.tableAliases
|
||||||
const aliased =
|
const aliased =
|
||||||
tableName && aliases?.[tableName] ? aliases[tableName] : table?.name
|
tableName && aliases?.[tableName] ? aliases[tableName] : table?.name
|
||||||
|
if (!Array.isArray(primaryKey)) {
|
||||||
|
throw new Error("Sorting requires primary key to be specified for table")
|
||||||
|
}
|
||||||
if (sort && Object.keys(sort || {}).length > 0) {
|
if (sort && Object.keys(sort || {}).length > 0) {
|
||||||
for (let [key, value] of Object.entries(sort)) {
|
for (let [key, value] of Object.entries(sort)) {
|
||||||
const direction =
|
const direction =
|
||||||
value.direction === SortDirection.ASCENDING ? "asc" : "desc"
|
value.direction === SortOrder.ASCENDING ? "asc" : "desc"
|
||||||
let nulls
|
let nulls
|
||||||
if (this.client === SqlClient.POSTGRES) {
|
if (this.client === SqlClient.POSTGRES) {
|
||||||
// All other clients already sort this as expected by default, and adding this to the rest of the clients is causing issues
|
// All other clients already sort this as expected by default, and adding this to the rest of the clients is causing issues
|
||||||
nulls = value.direction === SortDirection.ASCENDING ? "first" : "last"
|
nulls = value.direction === SortOrder.ASCENDING ? "first" : "last"
|
||||||
}
|
}
|
||||||
|
|
||||||
query = query.orderBy(`${aliased}.${key}`, direction, nulls)
|
query = query.orderBy(`${aliased}.${key}`, direction, nulls)
|
||||||
}
|
}
|
||||||
} else if (this.client === SqlClient.MS_SQL && paginate?.limit) {
|
}
|
||||||
// @ts-ignore
|
|
||||||
query = query.orderBy(`${aliased}.${table?.primary[0]}`)
|
// add sorting by the primary key if the result isn't already sorted by it,
|
||||||
|
// to make sure result is deterministic
|
||||||
|
if (!sort || sort[primaryKey[0]] === undefined) {
|
||||||
|
query = query.orderBy(`${aliased}.${primaryKey[0]}`)
|
||||||
}
|
}
|
||||||
return query
|
return query
|
||||||
}
|
}
|
||||||
|
@ -522,7 +551,7 @@ class InternalBuilder {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return query.limit(BASE_LIMIT)
|
return query
|
||||||
}
|
}
|
||||||
|
|
||||||
knexWithAlias(
|
knexWithAlias(
|
||||||
|
@ -533,13 +562,12 @@ class InternalBuilder {
|
||||||
const tableName = endpoint.entityId
|
const tableName = endpoint.entityId
|
||||||
const tableAlias = aliases?.[tableName]
|
const tableAlias = aliases?.[tableName]
|
||||||
|
|
||||||
const query = knex(
|
return knex(
|
||||||
this.tableNameWithSchema(tableName, {
|
this.tableNameWithSchema(tableName, {
|
||||||
alias: tableAlias,
|
alias: tableAlias,
|
||||||
schema: endpoint.schema,
|
schema: endpoint.schema,
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
return query
|
|
||||||
}
|
}
|
||||||
|
|
||||||
create(knex: Knex, json: QueryJson, opts: QueryOptions): Knex.QueryBuilder {
|
create(knex: Knex, json: QueryJson, opts: QueryOptions): Knex.QueryBuilder {
|
||||||
|
@ -571,52 +599,95 @@ class InternalBuilder {
|
||||||
return query.insert(parsedBody)
|
return query.insert(parsedBody)
|
||||||
}
|
}
|
||||||
|
|
||||||
read(knex: Knex, json: QueryJson, limit: number): Knex.QueryBuilder {
|
bulkUpsert(knex: Knex, json: QueryJson): Knex.QueryBuilder {
|
||||||
let { endpoint, resource, filters, paginate, relationships, tableAliases } =
|
const { endpoint, body } = json
|
||||||
json
|
let query = this.knexWithAlias(knex, endpoint)
|
||||||
|
if (!Array.isArray(body)) {
|
||||||
|
return query
|
||||||
|
}
|
||||||
|
const parsedBody = body.map(row => parseBody(row))
|
||||||
|
if (
|
||||||
|
this.client === SqlClient.POSTGRES ||
|
||||||
|
this.client === SqlClient.SQL_LITE ||
|
||||||
|
this.client === SqlClient.MY_SQL
|
||||||
|
) {
|
||||||
|
const primary = json.meta.table.primary
|
||||||
|
if (!primary) {
|
||||||
|
throw new Error("Primary key is required for upsert")
|
||||||
|
}
|
||||||
|
const ret = query.insert(parsedBody).onConflict(primary).merge()
|
||||||
|
return ret
|
||||||
|
} else if (this.client === SqlClient.MS_SQL) {
|
||||||
|
// No upsert or onConflict support in MSSQL yet, see:
|
||||||
|
// https://github.com/knex/knex/pull/6050
|
||||||
|
return query.insert(parsedBody)
|
||||||
|
}
|
||||||
|
return query.upsert(parsedBody)
|
||||||
|
}
|
||||||
|
|
||||||
|
read(
|
||||||
|
knex: Knex,
|
||||||
|
json: QueryJson,
|
||||||
|
opts: {
|
||||||
|
limits?: { base: number; query: number }
|
||||||
|
} = {}
|
||||||
|
): Knex.QueryBuilder {
|
||||||
|
let { endpoint, filters, paginate, relationships, tableAliases } = json
|
||||||
|
const { limits } = opts
|
||||||
|
const counting = endpoint.operation === Operation.COUNT
|
||||||
|
|
||||||
const tableName = endpoint.entityId
|
const tableName = endpoint.entityId
|
||||||
// select all if not specified
|
// start building the query
|
||||||
if (!resource) {
|
let query = this.knexWithAlias(knex, endpoint, tableAliases)
|
||||||
resource = { fields: [] }
|
|
||||||
}
|
|
||||||
let selectStatement: string | (string | Knex.Raw)[] = "*"
|
|
||||||
// handle select
|
|
||||||
if (resource.fields && resource.fields.length > 0) {
|
|
||||||
// select the resources as the format "table.columnName" - this is what is provided
|
|
||||||
// by the resource builder further up
|
|
||||||
selectStatement = generateSelectStatement(json, knex)
|
|
||||||
}
|
|
||||||
let foundLimit = limit || BASE_LIMIT
|
|
||||||
// handle pagination
|
// handle pagination
|
||||||
let foundOffset: number | null = null
|
let foundOffset: number | null = null
|
||||||
|
let foundLimit = limits?.query || limits?.base
|
||||||
if (paginate && paginate.page && paginate.limit) {
|
if (paginate && paginate.page && paginate.limit) {
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
const page = paginate.page <= 1 ? 0 : paginate.page - 1
|
const page = paginate.page <= 1 ? 0 : paginate.page - 1
|
||||||
const offset = page * paginate.limit
|
const offset = page * paginate.limit
|
||||||
foundLimit = paginate.limit
|
foundLimit = paginate.limit
|
||||||
foundOffset = offset
|
foundOffset = offset
|
||||||
|
} else if (paginate && paginate.offset && paginate.limit) {
|
||||||
|
foundLimit = paginate.limit
|
||||||
|
foundOffset = paginate.offset
|
||||||
} else if (paginate && paginate.limit) {
|
} else if (paginate && paginate.limit) {
|
||||||
foundLimit = paginate.limit
|
foundLimit = paginate.limit
|
||||||
}
|
}
|
||||||
// start building the query
|
// counting should not sort, limit or offset
|
||||||
let query = this.knexWithAlias(knex, endpoint, tableAliases)
|
if (!counting) {
|
||||||
query = query.limit(foundLimit)
|
// add the found limit if supplied
|
||||||
if (foundOffset) {
|
if (foundLimit != null) {
|
||||||
query = query.offset(foundOffset)
|
query = query.limit(foundLimit)
|
||||||
|
}
|
||||||
|
// add overall pagination
|
||||||
|
if (foundOffset != null) {
|
||||||
|
query = query.offset(foundOffset)
|
||||||
|
}
|
||||||
|
// add sorting to pre-query
|
||||||
|
// no point in sorting when counting
|
||||||
|
query = this.addSorting(query, json)
|
||||||
}
|
}
|
||||||
|
// add filters to the query (where)
|
||||||
query = this.addFilters(query, filters, json.meta.table, {
|
query = this.addFilters(query, filters, json.meta.table, {
|
||||||
|
columnPrefix: json.meta.columnPrefix,
|
||||||
aliases: tableAliases,
|
aliases: tableAliases,
|
||||||
})
|
})
|
||||||
|
|
||||||
// add sorting to pre-query
|
|
||||||
query = this.addSorting(query, json)
|
|
||||||
const alias = tableAliases?.[tableName] || tableName
|
const alias = tableAliases?.[tableName] || tableName
|
||||||
let preQuery = knex({
|
let preQuery: Knex.QueryBuilder = knex({
|
||||||
[alias]: query,
|
// the typescript definition for the knex constructor doesn't support this
|
||||||
} as any).select(selectStatement) as any
|
// syntax, but it is the only way to alias a pre-query result as part of
|
||||||
|
// a query - there is an alias dictionary type, but it assumes it can only
|
||||||
|
// be a table name, not a pre-query
|
||||||
|
[alias]: query as any,
|
||||||
|
})
|
||||||
|
// if counting, use distinct count, else select
|
||||||
|
preQuery = !counting
|
||||||
|
? preQuery.select(generateSelectStatement(json, knex))
|
||||||
|
: this.addDistinctCount(preQuery, json)
|
||||||
// have to add after as well (this breaks MS-SQL)
|
// have to add after as well (this breaks MS-SQL)
|
||||||
if (this.client !== SqlClient.MS_SQL) {
|
if (this.client !== SqlClient.MS_SQL && !counting) {
|
||||||
preQuery = this.addSorting(preQuery, json)
|
preQuery = this.addSorting(preQuery, json)
|
||||||
}
|
}
|
||||||
// handle joins
|
// handle joins
|
||||||
|
@ -627,7 +698,15 @@ class InternalBuilder {
|
||||||
endpoint.schema,
|
endpoint.schema,
|
||||||
tableAliases
|
tableAliases
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// add a base limit over the whole query
|
||||||
|
// if counting we can't set this limit
|
||||||
|
if (limits?.base) {
|
||||||
|
query = query.limit(limits.base)
|
||||||
|
}
|
||||||
|
|
||||||
return this.addFilters(query, filters, json.meta.table, {
|
return this.addFilters(query, filters, json.meta.table, {
|
||||||
|
columnPrefix: json.meta.columnPrefix,
|
||||||
relationship: true,
|
relationship: true,
|
||||||
aliases: tableAliases,
|
aliases: tableAliases,
|
||||||
})
|
})
|
||||||
|
@ -638,6 +717,7 @@ class InternalBuilder {
|
||||||
let query = this.knexWithAlias(knex, endpoint, tableAliases)
|
let query = this.knexWithAlias(knex, endpoint, tableAliases)
|
||||||
const parsedBody = parseBody(body)
|
const parsedBody = parseBody(body)
|
||||||
query = this.addFilters(query, filters, json.meta.table, {
|
query = this.addFilters(query, filters, json.meta.table, {
|
||||||
|
columnPrefix: json.meta.columnPrefix,
|
||||||
aliases: tableAliases,
|
aliases: tableAliases,
|
||||||
})
|
})
|
||||||
// mysql can't use returning
|
// mysql can't use returning
|
||||||
|
@ -652,6 +732,7 @@ class InternalBuilder {
|
||||||
const { endpoint, filters, tableAliases } = json
|
const { endpoint, filters, tableAliases } = json
|
||||||
let query = this.knexWithAlias(knex, endpoint, tableAliases)
|
let query = this.knexWithAlias(knex, endpoint, tableAliases)
|
||||||
query = this.addFilters(query, filters, json.meta.table, {
|
query = this.addFilters(query, filters, json.meta.table, {
|
||||||
|
columnPrefix: json.meta.columnPrefix,
|
||||||
aliases: tableAliases,
|
aliases: tableAliases,
|
||||||
})
|
})
|
||||||
// mysql can't use returning
|
// mysql can't use returning
|
||||||
|
@ -671,6 +752,19 @@ class SqlQueryBuilder extends SqlTableQueryBuilder {
|
||||||
this.limit = limit
|
this.limit = limit
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private convertToNative(query: Knex.QueryBuilder, opts: QueryOptions = {}) {
|
||||||
|
const sqlClient = this.getSqlClient()
|
||||||
|
if (opts?.disableBindings) {
|
||||||
|
return { sql: query.toString() }
|
||||||
|
} else {
|
||||||
|
let native = getNativeSql(query)
|
||||||
|
if (sqlClient === SqlClient.SQL_LITE) {
|
||||||
|
native = convertBooleans(native)
|
||||||
|
}
|
||||||
|
return native
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param json The JSON query DSL which is to be converted to SQL.
|
* @param json The JSON query DSL which is to be converted to SQL.
|
||||||
* @param opts extra options which are to be passed into the query builder, e.g. disableReturning
|
* @param opts extra options which are to be passed into the query builder, e.g. disableReturning
|
||||||
|
@ -694,7 +788,16 @@ class SqlQueryBuilder extends SqlTableQueryBuilder {
|
||||||
query = builder.create(client, json, opts)
|
query = builder.create(client, json, opts)
|
||||||
break
|
break
|
||||||
case Operation.READ:
|
case Operation.READ:
|
||||||
query = builder.read(client, json, this.limit)
|
query = builder.read(client, json, {
|
||||||
|
limits: {
|
||||||
|
query: this.limit,
|
||||||
|
base: BASE_LIMIT,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
break
|
||||||
|
case Operation.COUNT:
|
||||||
|
// read without any limits to count
|
||||||
|
query = builder.read(client, json)
|
||||||
break
|
break
|
||||||
case Operation.UPDATE:
|
case Operation.UPDATE:
|
||||||
query = builder.update(client, json, opts)
|
query = builder.update(client, json, opts)
|
||||||
|
@ -705,6 +808,9 @@ class SqlQueryBuilder extends SqlTableQueryBuilder {
|
||||||
case Operation.BULK_CREATE:
|
case Operation.BULK_CREATE:
|
||||||
query = builder.bulkCreate(client, json)
|
query = builder.bulkCreate(client, json)
|
||||||
break
|
break
|
||||||
|
case Operation.BULK_UPSERT:
|
||||||
|
query = builder.bulkUpsert(client, json)
|
||||||
|
break
|
||||||
case Operation.CREATE_TABLE:
|
case Operation.CREATE_TABLE:
|
||||||
case Operation.UPDATE_TABLE:
|
case Operation.UPDATE_TABLE:
|
||||||
case Operation.DELETE_TABLE:
|
case Operation.DELETE_TABLE:
|
||||||
|
@ -713,15 +819,7 @@ class SqlQueryBuilder extends SqlTableQueryBuilder {
|
||||||
throw `Operation type is not supported by SQL query builder`
|
throw `Operation type is not supported by SQL query builder`
|
||||||
}
|
}
|
||||||
|
|
||||||
if (opts?.disableBindings) {
|
return this.convertToNative(query, opts)
|
||||||
return { sql: query.toString() }
|
|
||||||
} else {
|
|
||||||
let native = getNativeSql(query)
|
|
||||||
if (sqlClient === SqlClient.SQL_LITE) {
|
|
||||||
native = convertBooleans(native)
|
|
||||||
}
|
|
||||||
return native
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async getReturningRow(queryFn: QueryFunction, json: QueryJson) {
|
async getReturningRow(queryFn: QueryFunction, json: QueryJson) {
|
||||||
|
@ -797,6 +895,9 @@ class SqlQueryBuilder extends SqlTableQueryBuilder {
|
||||||
await this.getReturningRow(queryFn, this.checkLookupKeys(id, json))
|
await this.getReturningRow(queryFn, this.checkLookupKeys(id, json))
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
if (operation === Operation.COUNT) {
|
||||||
|
return results
|
||||||
|
}
|
||||||
if (operation !== Operation.READ) {
|
if (operation !== Operation.READ) {
|
||||||
return row
|
return row
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,19 +5,27 @@ export class SqlStatements {
|
||||||
client: string
|
client: string
|
||||||
table: Table
|
table: Table
|
||||||
allOr: boolean | undefined
|
allOr: boolean | undefined
|
||||||
|
columnPrefix: string | undefined
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
client: string,
|
client: string,
|
||||||
table: Table,
|
table: Table,
|
||||||
{ allOr }: { allOr?: boolean } = {}
|
{ allOr, columnPrefix }: { allOr?: boolean; columnPrefix?: string } = {}
|
||||||
) {
|
) {
|
||||||
this.client = client
|
this.client = client
|
||||||
this.table = table
|
this.table = table
|
||||||
this.allOr = allOr
|
this.allOr = allOr
|
||||||
|
this.columnPrefix = columnPrefix
|
||||||
}
|
}
|
||||||
|
|
||||||
getField(key: string): FieldSchema | undefined {
|
getField(key: string): FieldSchema | undefined {
|
||||||
const fieldName = key.split(".")[1]
|
const fieldName = key.split(".")[1]
|
||||||
return this.table.schema[fieldName]
|
let found = this.table.schema[fieldName]
|
||||||
|
if (!found && this.columnPrefix) {
|
||||||
|
const prefixRemovedFieldName = fieldName.replace(this.columnPrefix, "")
|
||||||
|
found = this.table.schema[prefixRemovedFieldName]
|
||||||
|
}
|
||||||
|
return found
|
||||||
}
|
}
|
||||||
|
|
||||||
between(
|
between(
|
||||||
|
|
|
@ -109,8 +109,10 @@ function generateSchema(
|
||||||
const { tableName } = breakExternalTableId(column.tableId)
|
const { tableName } = breakExternalTableId(column.tableId)
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
const relatedTable = tables[tableName]
|
const relatedTable = tables[tableName]
|
||||||
if (!relatedTable) {
|
if (!relatedTable || !relatedTable.primary) {
|
||||||
throw new Error("Referenced table doesn't exist")
|
throw new Error(
|
||||||
|
"Referenced table doesn't exist or has no primary keys"
|
||||||
|
)
|
||||||
}
|
}
|
||||||
const relatedPrimary = relatedTable.primary[0]
|
const relatedPrimary = relatedTable.primary[0]
|
||||||
const externalType = relatedTable.schema[relatedPrimary].externalType
|
const externalType = relatedTable.schema[relatedPrimary].externalType
|
||||||
|
|
|
@ -55,10 +55,7 @@ export function buildExternalTableId(datasourceId: string, tableName: string) {
|
||||||
return `${datasourceId}${DOUBLE_SEPARATOR}${tableName}`
|
return `${datasourceId}${DOUBLE_SEPARATOR}${tableName}`
|
||||||
}
|
}
|
||||||
|
|
||||||
export function breakExternalTableId(tableId: string | undefined) {
|
export function breakExternalTableId(tableId: string) {
|
||||||
if (!tableId) {
|
|
||||||
return {}
|
|
||||||
}
|
|
||||||
const parts = tableId.split(DOUBLE_SEPARATOR)
|
const parts = tableId.split(DOUBLE_SEPARATOR)
|
||||||
let datasourceId = parts.shift()
|
let datasourceId = parts.shift()
|
||||||
// if they need joined
|
// if they need joined
|
||||||
|
@ -67,6 +64,9 @@ export function breakExternalTableId(tableId: string | undefined) {
|
||||||
if (tableName.includes(ENCODED_SPACE)) {
|
if (tableName.includes(ENCODED_SPACE)) {
|
||||||
tableName = decodeURIComponent(tableName)
|
tableName = decodeURIComponent(tableName)
|
||||||
}
|
}
|
||||||
|
if (!datasourceId || !tableName) {
|
||||||
|
throw new Error("Unable to get datasource/table name from table ID")
|
||||||
|
}
|
||||||
return { datasourceId, tableName }
|
return { datasourceId, tableName }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,21 @@
|
||||||
import { getDB } from "../db/db"
|
import { getDB } from "../db/db"
|
||||||
import { getGlobalDBName } from "../context"
|
import { getGlobalDBName } from "../context"
|
||||||
|
import { TenantInfo } from "@budibase/types"
|
||||||
|
|
||||||
export function getTenantDB(tenantId: string) {
|
export function getTenantDB(tenantId: string) {
|
||||||
return getDB(getGlobalDBName(tenantId))
|
return getDB(getGlobalDBName(tenantId))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function saveTenantInfo(tenantInfo: TenantInfo) {
|
||||||
|
const db = getTenantDB(tenantInfo.tenantId)
|
||||||
|
// save the tenant info to db
|
||||||
|
return db.put({
|
||||||
|
_id: "tenant_info",
|
||||||
|
...tenantInfo,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getTenantInfo(tenantId: string): Promise<TenantInfo> {
|
||||||
|
const db = getTenantDB(tenantId)
|
||||||
|
return db.get("tenant_info")
|
||||||
|
}
|
||||||
|
|
|
@ -24,7 +24,6 @@ export const account = (partial: Partial<Account> = {}): Account => {
|
||||||
createdAt: Date.now(),
|
createdAt: Date.now(),
|
||||||
verified: true,
|
verified: true,
|
||||||
verificationSent: true,
|
verificationSent: true,
|
||||||
tier: "FREE", // DEPRECATED
|
|
||||||
authType: AuthType.PASSWORD,
|
authType: AuthType.PASSWORD,
|
||||||
name: generator.name(),
|
name: generator.name(),
|
||||||
size: "10+",
|
size: "10+",
|
||||||
|
|
|
@ -162,6 +162,7 @@
|
||||||
max-height: 100%;
|
max-height: 100%;
|
||||||
}
|
}
|
||||||
.modal-inner-wrapper {
|
.modal-inner-wrapper {
|
||||||
|
padding: 40px;
|
||||||
flex: 1 1 auto;
|
flex: 1 1 auto;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
|
@ -176,7 +177,6 @@
|
||||||
border: 2px solid var(--spectrum-global-color-gray-200);
|
border: 2px solid var(--spectrum-global-color-gray-200);
|
||||||
overflow: visible;
|
overflow: visible;
|
||||||
max-height: none;
|
max-height: none;
|
||||||
margin: 40px 0;
|
|
||||||
transform: none;
|
transform: none;
|
||||||
--spectrum-dialog-confirm-border-radius: var(
|
--spectrum-dialog-confirm-border-radius: var(
|
||||||
--spectrum-global-dimension-size-100
|
--spectrum-global-dimension-size-100
|
||||||
|
|
|
@ -15,6 +15,9 @@
|
||||||
Checkbox,
|
Checkbox,
|
||||||
DatePicker,
|
DatePicker,
|
||||||
DrawerContent,
|
DrawerContent,
|
||||||
|
Toggle,
|
||||||
|
Icon,
|
||||||
|
Divider,
|
||||||
} from "@budibase/bbui"
|
} from "@budibase/bbui"
|
||||||
import CreateWebhookModal from "components/automation/Shared/CreateWebhookModal.svelte"
|
import CreateWebhookModal from "components/automation/Shared/CreateWebhookModal.svelte"
|
||||||
import { automationStore, selectedAutomation, tables } from "stores/builder"
|
import { automationStore, selectedAutomation, tables } from "stores/builder"
|
||||||
|
@ -40,7 +43,7 @@
|
||||||
EditorModes,
|
EditorModes,
|
||||||
} from "components/common/CodeEditor"
|
} from "components/common/CodeEditor"
|
||||||
import FilterBuilder from "components/design/settings/controls/FilterEditor/FilterBuilder.svelte"
|
import FilterBuilder from "components/design/settings/controls/FilterEditor/FilterBuilder.svelte"
|
||||||
import { LuceneUtils, Utils } from "@budibase/frontend-core"
|
import { QueryUtils, Utils, search } from "@budibase/frontend-core"
|
||||||
import {
|
import {
|
||||||
getSchemaForDatasourcePlus,
|
getSchemaForDatasourcePlus,
|
||||||
getEnvironmentBindings,
|
getEnvironmentBindings,
|
||||||
|
@ -72,7 +75,11 @@
|
||||||
$: schema = getSchemaForDatasourcePlus(tableId, {
|
$: schema = getSchemaForDatasourcePlus(tableId, {
|
||||||
searchableSchema: true,
|
searchableSchema: true,
|
||||||
}).schema
|
}).schema
|
||||||
$: schemaFields = Object.values(schema || {})
|
$: schemaFields = search.getFields(
|
||||||
|
$tables.list,
|
||||||
|
Object.values(schema || {}),
|
||||||
|
{ allowLinks: true }
|
||||||
|
)
|
||||||
$: queryLimit = tableId?.includes("datasource") ? "∞" : "1000"
|
$: queryLimit = tableId?.includes("datasource") ? "∞" : "1000"
|
||||||
$: isTrigger = block?.type === "TRIGGER"
|
$: isTrigger = block?.type === "TRIGGER"
|
||||||
$: isUpdateRow = stepId === ActionStepID.UPDATE_ROW
|
$: isUpdateRow = stepId === ActionStepID.UPDATE_ROW
|
||||||
|
@ -88,6 +95,8 @@
|
||||||
? [hbAutocomplete([...bindingsToCompletions(bindings, codeMode)])]
|
? [hbAutocomplete([...bindingsToCompletions(bindings, codeMode)])]
|
||||||
: []
|
: []
|
||||||
|
|
||||||
|
let testDataRowVisibility = {}
|
||||||
|
|
||||||
const getInputData = (testData, blockInputs) => {
|
const getInputData = (testData, blockInputs) => {
|
||||||
// Test data is not cloned for reactivity
|
// Test data is not cloned for reactivity
|
||||||
let newInputData = testData || cloneDeep(blockInputs)
|
let newInputData = testData || cloneDeep(blockInputs)
|
||||||
|
@ -118,7 +127,6 @@
|
||||||
searchableSchema: true,
|
searchableSchema: true,
|
||||||
}).schema
|
}).schema
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (isTestModal) {
|
if (isTestModal) {
|
||||||
let newTestData = { schema }
|
let newTestData = { schema }
|
||||||
|
@ -196,7 +204,8 @@
|
||||||
(automation.trigger?.event === "row:update" ||
|
(automation.trigger?.event === "row:update" ||
|
||||||
automation.trigger?.event === "row:save")
|
automation.trigger?.event === "row:save")
|
||||||
) {
|
) {
|
||||||
if (name !== "id" && name !== "revision") return `trigger.row.${name}`
|
let noRowKeywordBindings = ["id", "revision", "oldRow"]
|
||||||
|
if (!noRowKeywordBindings.includes(name)) return `trigger.row.${name}`
|
||||||
}
|
}
|
||||||
/* End special cases for generating custom schemas based on triggers */
|
/* End special cases for generating custom schemas based on triggers */
|
||||||
|
|
||||||
|
@ -343,7 +352,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
function saveFilters(key) {
|
function saveFilters(key) {
|
||||||
const filters = LuceneUtils.buildLuceneQuery(tempFilters)
|
const filters = QueryUtils.buildQuery(tempFilters)
|
||||||
const defKey = `${key}-def`
|
const defKey = `${key}-def`
|
||||||
onChange({ detail: filters }, key)
|
onChange({ detail: filters }, key)
|
||||||
// need to store the builder definition in the automation
|
// need to store the builder definition in the automation
|
||||||
|
@ -372,7 +381,11 @@
|
||||||
|
|
||||||
function getFieldLabel(key, value) {
|
function getFieldLabel(key, value) {
|
||||||
const requiredSuffix = requiredProperties.includes(key) ? "*" : ""
|
const requiredSuffix = requiredProperties.includes(key) ? "*" : ""
|
||||||
return `${value.title || (key === "row" ? "Table" : key)} ${requiredSuffix}`
|
return `${value.title || (key === "row" ? "Row" : key)} ${requiredSuffix}`
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleTestDataRowVisibility(key) {
|
||||||
|
testDataRowVisibility[key] = !testDataRowVisibility[key]
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleAttachmentParams(keyValueObj) {
|
function handleAttachmentParams(keyValueObj) {
|
||||||
|
@ -385,6 +398,16 @@
|
||||||
return params
|
return params
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function toggleAttachmentBinding(e, key) {
|
||||||
|
onChange(
|
||||||
|
{
|
||||||
|
detail: "",
|
||||||
|
},
|
||||||
|
key
|
||||||
|
)
|
||||||
|
onChange({ detail: { useAttachmentBinding: e.detail } }, "meta")
|
||||||
|
}
|
||||||
|
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
try {
|
try {
|
||||||
await environment.loadVariables()
|
await environment.loadVariables()
|
||||||
|
@ -462,27 +485,64 @@
|
||||||
<div class="label-wrapper">
|
<div class="label-wrapper">
|
||||||
<Label>{label}</Label>
|
<Label>{label}</Label>
|
||||||
</div>
|
</div>
|
||||||
<div class="attachment-field-width">
|
<div class="toggle-container">
|
||||||
<KeyValueBuilder
|
<Toggle
|
||||||
on:change={e =>
|
value={inputData?.meta?.useAttachmentBinding}
|
||||||
onChange(
|
text={"Use bindings"}
|
||||||
{
|
size={"XS"}
|
||||||
detail: e.detail.map(({ name, value }) => ({
|
on:change={e => toggleAttachmentBinding(e, key)}
|
||||||
url: name,
|
|
||||||
filename: value,
|
|
||||||
})),
|
|
||||||
},
|
|
||||||
key
|
|
||||||
)}
|
|
||||||
object={handleAttachmentParams(inputData[key])}
|
|
||||||
allowJS
|
|
||||||
{bindings}
|
|
||||||
keyBindings
|
|
||||||
customButtonText={"Add attachment"}
|
|
||||||
keyPlaceholder={"URL"}
|
|
||||||
valuePlaceholder={"Filename"}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="attachment-field-width">
|
||||||
|
{#if !inputData?.meta?.useAttachmentBinding}
|
||||||
|
<KeyValueBuilder
|
||||||
|
on:change={e =>
|
||||||
|
onChange(
|
||||||
|
{
|
||||||
|
detail: e.detail.map(({ name, value }) => ({
|
||||||
|
url: name,
|
||||||
|
filename: value,
|
||||||
|
})),
|
||||||
|
},
|
||||||
|
key
|
||||||
|
)}
|
||||||
|
object={handleAttachmentParams(inputData[key])}
|
||||||
|
allowJS
|
||||||
|
{bindings}
|
||||||
|
keyBindings
|
||||||
|
customButtonText={"Add attachment"}
|
||||||
|
keyPlaceholder={"URL"}
|
||||||
|
valuePlaceholder={"Filename"}
|
||||||
|
/>
|
||||||
|
{:else if isTestModal}
|
||||||
|
<ModalBindableInput
|
||||||
|
title={value.title || label}
|
||||||
|
value={inputData[key]}
|
||||||
|
panel={AutomationBindingPanel}
|
||||||
|
type={value.customType}
|
||||||
|
on:change={e => onChange(e, key)}
|
||||||
|
{bindings}
|
||||||
|
updateOnChange={false}
|
||||||
|
/>
|
||||||
|
{:else}
|
||||||
|
<div class="test">
|
||||||
|
<DrawerBindableInput
|
||||||
|
title={value.title ?? label}
|
||||||
|
panel={AutomationBindingPanel}
|
||||||
|
type={value.customType}
|
||||||
|
value={inputData[key]}
|
||||||
|
on:change={e => onChange(e, key)}
|
||||||
|
{bindings}
|
||||||
|
updateOnChange={false}
|
||||||
|
placeholder={value.customType === "queryLimit"
|
||||||
|
? queryLimit
|
||||||
|
: ""}
|
||||||
|
drawerLeft="260px"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{:else if value.customType === "filters"}
|
{:else if value.customType === "filters"}
|
||||||
<ActionButton on:click={drawer.show}>Define filters</ActionButton>
|
<ActionButton on:click={drawer.show}>Define filters</ActionButton>
|
||||||
|
@ -560,20 +620,48 @@
|
||||||
on:change={e => onChange(e, key)}
|
on:change={e => onChange(e, key)}
|
||||||
/>
|
/>
|
||||||
{:else if value.customType === "row"}
|
{:else if value.customType === "row"}
|
||||||
<RowSelector
|
{#if isTestModal}
|
||||||
value={inputData[key]}
|
<div class="align-horizontally">
|
||||||
meta={inputData["meta"] || {}}
|
<Icon
|
||||||
on:change={e => {
|
name={testDataRowVisibility[key] ? "Remove" : "Add"}
|
||||||
if (e.detail?.key) {
|
hoverable
|
||||||
onChange(e, e.detail.key)
|
on:click={() => toggleTestDataRowVisibility(key)}
|
||||||
} else {
|
/>
|
||||||
onChange(e, key)
|
<Label size="XL">{label}</Label>
|
||||||
}
|
</div>
|
||||||
}}
|
{#if testDataRowVisibility[key]}
|
||||||
{bindings}
|
<RowSelector
|
||||||
{isTestModal}
|
value={inputData[key]}
|
||||||
{isUpdateRow}
|
meta={inputData["meta"] || {}}
|
||||||
/>
|
on:change={e => {
|
||||||
|
if (e.detail?.key) {
|
||||||
|
onChange(e, e.detail.key)
|
||||||
|
} else {
|
||||||
|
onChange(e, key)
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
{bindings}
|
||||||
|
{isTestModal}
|
||||||
|
{isUpdateRow}
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
|
<Divider />
|
||||||
|
{:else}
|
||||||
|
<RowSelector
|
||||||
|
value={inputData[key]}
|
||||||
|
meta={inputData["meta"] || {}}
|
||||||
|
on:change={e => {
|
||||||
|
if (e.detail?.key) {
|
||||||
|
onChange(e, e.detail.key)
|
||||||
|
} else {
|
||||||
|
onChange(e, key)
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
{bindings}
|
||||||
|
{isTestModal}
|
||||||
|
{isUpdateRow}
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
{:else if value.customType === "webhookUrl"}
|
{:else if value.customType === "webhookUrl"}
|
||||||
<WebhookDisplay
|
<WebhookDisplay
|
||||||
on:change={e => onChange(e, key)}
|
on:change={e => onChange(e, key)}
|
||||||
|
@ -689,6 +777,12 @@
|
||||||
width: 320px;
|
width: 320px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.align-horizontally {
|
||||||
|
display: flex;
|
||||||
|
gap: var(--spacing-s);
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
.fields {
|
.fields {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|
|
@ -10,12 +10,12 @@
|
||||||
import { TableNames } from "constants"
|
import { TableNames } from "constants"
|
||||||
|
|
||||||
const dispatch = createEventDispatcher()
|
const dispatch = createEventDispatcher()
|
||||||
|
|
||||||
export let value
|
export let value
|
||||||
export let meta
|
export let meta
|
||||||
export let bindings
|
export let bindings
|
||||||
export let isTestModal
|
export let isTestModal
|
||||||
export let isUpdateRow
|
export let isUpdateRow
|
||||||
|
|
||||||
$: parsedBindings = bindings.map(binding => {
|
$: parsedBindings = bindings.map(binding => {
|
||||||
let clone = Object.assign({}, binding)
|
let clone = Object.assign({}, binding)
|
||||||
clone.icon = "ShareAndroid"
|
clone.icon = "ShareAndroid"
|
||||||
|
@ -94,17 +94,22 @@
|
||||||
dispatch("change", newValue)
|
dispatch("change", newValue)
|
||||||
}
|
}
|
||||||
|
|
||||||
const onChangeSetting = (e, field) => {
|
const onChangeSetting = (field, key, value) => {
|
||||||
let fields = {}
|
let newField = {}
|
||||||
fields[field] = {
|
newField[field] = {
|
||||||
clearRelationships: e.detail,
|
[key]: value,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let updatedFields = {
|
||||||
|
...meta?.fields,
|
||||||
|
...newField,
|
||||||
|
}
|
||||||
|
|
||||||
dispatch("change", {
|
dispatch("change", {
|
||||||
key: "meta",
|
key: "meta",
|
||||||
fields,
|
fields: updatedFields,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ensure any nullish tableId values get set to empty string so
|
// Ensure any nullish tableId values get set to empty string so
|
||||||
// that the select works
|
// that the select works
|
||||||
$: if (value?.tableId == null) value = { tableId: "" }
|
$: if (value?.tableId == null) value = { tableId: "" }
|
||||||
|
@ -157,6 +162,9 @@
|
||||||
bindings={parsedBindings}
|
bindings={parsedBindings}
|
||||||
{value}
|
{value}
|
||||||
{onChange}
|
{onChange}
|
||||||
|
useAttachmentBinding={meta?.fields?.[field]
|
||||||
|
?.useAttachmentBinding}
|
||||||
|
{onChangeSetting}
|
||||||
/>
|
/>
|
||||||
</DrawerBindableSlot>
|
</DrawerBindableSlot>
|
||||||
{/if}
|
{/if}
|
||||||
|
@ -167,7 +175,8 @@
|
||||||
value={meta.fields?.[field]?.clearRelationships}
|
value={meta.fields?.[field]?.clearRelationships}
|
||||||
text={"Clear relationships if empty?"}
|
text={"Clear relationships if empty?"}
|
||||||
size={"S"}
|
size={"S"}
|
||||||
on:change={e => onChangeSetting(e, field)}
|
on:change={e =>
|
||||||
|
onChangeSetting(field, "clearRelationships", e.detail)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
|
@ -1,5 +1,11 @@
|
||||||
<script>
|
<script>
|
||||||
import { Select, DatePicker, Multiselect, TextArea } from "@budibase/bbui"
|
import {
|
||||||
|
Select,
|
||||||
|
DatePicker,
|
||||||
|
Multiselect,
|
||||||
|
TextArea,
|
||||||
|
Toggle,
|
||||||
|
} from "@budibase/bbui"
|
||||||
import { FieldType } from "@budibase/types"
|
import { FieldType } from "@budibase/types"
|
||||||
import LinkedRowSelector from "components/common/LinkedRowSelector.svelte"
|
import LinkedRowSelector from "components/common/LinkedRowSelector.svelte"
|
||||||
import DrawerBindableInput from "../../common/bindings/DrawerBindableInput.svelte"
|
import DrawerBindableInput from "../../common/bindings/DrawerBindableInput.svelte"
|
||||||
|
@ -14,6 +20,8 @@
|
||||||
export let value
|
export let value
|
||||||
export let bindings
|
export let bindings
|
||||||
export let isTestModal
|
export let isTestModal
|
||||||
|
export let useAttachmentBinding
|
||||||
|
export let onChangeSetting
|
||||||
|
|
||||||
$: parsedBindings = bindings.map(binding => {
|
$: parsedBindings = bindings.map(binding => {
|
||||||
let clone = Object.assign({}, binding)
|
let clone = Object.assign({}, binding)
|
||||||
|
@ -27,6 +35,8 @@
|
||||||
FieldType.SIGNATURE_SINGLE,
|
FieldType.SIGNATURE_SINGLE,
|
||||||
]
|
]
|
||||||
|
|
||||||
|
let previousBindingState = useAttachmentBinding
|
||||||
|
|
||||||
function schemaHasOptions(schema) {
|
function schemaHasOptions(schema) {
|
||||||
return !!schema.constraints?.inclusion?.length
|
return !!schema.constraints?.inclusion?.length
|
||||||
}
|
}
|
||||||
|
@ -34,13 +44,6 @@
|
||||||
function handleAttachmentParams(keyValueObj) {
|
function handleAttachmentParams(keyValueObj) {
|
||||||
let params = {}
|
let params = {}
|
||||||
|
|
||||||
if (
|
|
||||||
(schema.type === FieldType.ATTACHMENT_SINGLE ||
|
|
||||||
schema.type === FieldType.SIGNATURE_SINGLE) &&
|
|
||||||
Object.keys(keyValueObj).length === 0
|
|
||||||
) {
|
|
||||||
return []
|
|
||||||
}
|
|
||||||
if (!Array.isArray(keyValueObj) && keyValueObj) {
|
if (!Array.isArray(keyValueObj) && keyValueObj) {
|
||||||
keyValueObj = [keyValueObj]
|
keyValueObj = [keyValueObj]
|
||||||
}
|
}
|
||||||
|
@ -52,6 +55,26 @@
|
||||||
}
|
}
|
||||||
return params
|
return params
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function handleToggleChange(toggleField, event) {
|
||||||
|
if (event.detail === true) {
|
||||||
|
value[toggleField] = []
|
||||||
|
} else {
|
||||||
|
value[toggleField] = ""
|
||||||
|
}
|
||||||
|
previousBindingState = event.detail
|
||||||
|
onChangeSetting(toggleField, "useAttachmentBinding", event.detail)
|
||||||
|
onChange({ detail: value[toggleField] }, toggleField)
|
||||||
|
}
|
||||||
|
|
||||||
|
$: if (useAttachmentBinding !== previousBindingState) {
|
||||||
|
if (useAttachmentBinding) {
|
||||||
|
value[field] = []
|
||||||
|
} else {
|
||||||
|
value[field] = ""
|
||||||
|
}
|
||||||
|
previousBindingState = useAttachmentBinding
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if schemaHasOptions(schema) && schema.type !== "array"}
|
{#if schemaHasOptions(schema) && schema.type !== "array"}
|
||||||
|
@ -108,38 +131,65 @@
|
||||||
useLabel={false}
|
useLabel={false}
|
||||||
/>
|
/>
|
||||||
{:else if attachmentTypes.includes(schema.type)}
|
{:else if attachmentTypes.includes(schema.type)}
|
||||||
<div class="attachment-field-spacinng">
|
<div class="attachment-field-container">
|
||||||
<KeyValueBuilder
|
<div class="toggle-container">
|
||||||
on:change={e =>
|
<Toggle
|
||||||
onChange(
|
value={useAttachmentBinding}
|
||||||
{
|
text={"Use bindings"}
|
||||||
detail:
|
size={"XS"}
|
||||||
schema.type === FieldType.ATTACHMENT_SINGLE ||
|
on:change={e => handleToggleChange(field, e)}
|
||||||
schema.type === FieldType.SIGNATURE_SINGLE
|
/>
|
||||||
? e.detail.length > 0
|
</div>
|
||||||
? {
|
{#if !useAttachmentBinding}
|
||||||
url: e.detail[0].name,
|
<div class="attachment-field-spacing">
|
||||||
filename: e.detail[0].value,
|
<KeyValueBuilder
|
||||||
}
|
on:change={async e => {
|
||||||
: {}
|
onChange(
|
||||||
: e.detail.map(({ name, value }) => ({
|
{
|
||||||
url: name,
|
detail:
|
||||||
filename: value,
|
schema.type === FieldType.ATTACHMENT_SINGLE ||
|
||||||
})),
|
schema.type === FieldType.SIGNATURE_SINGLE
|
||||||
},
|
? e.detail.length > 0
|
||||||
field
|
? {
|
||||||
)}
|
url: e.detail[0].name,
|
||||||
object={handleAttachmentParams(value[field])}
|
filename: e.detail[0].value,
|
||||||
allowJS
|
}
|
||||||
{bindings}
|
: {}
|
||||||
keyBindings
|
: e.detail.map(({ name, value }) => ({
|
||||||
customButtonText={"Add attachment"}
|
url: name,
|
||||||
keyPlaceholder={"URL"}
|
filename: value,
|
||||||
valuePlaceholder={"Filename"}
|
})),
|
||||||
actionButtonDisabled={(schema.type === FieldType.ATTACHMENT_SINGLE ||
|
},
|
||||||
schema.type === FieldType.SIGNATURE) &&
|
field
|
||||||
Object.keys(value[field]).length >= 1}
|
)
|
||||||
/>
|
}}
|
||||||
|
object={handleAttachmentParams(value[field])}
|
||||||
|
allowJS
|
||||||
|
{bindings}
|
||||||
|
keyBindings
|
||||||
|
customButtonText={"Add attachment"}
|
||||||
|
keyPlaceholder={"URL"}
|
||||||
|
valuePlaceholder={"Filename"}
|
||||||
|
actionButtonDisabled={(schema.type === FieldType.ATTACHMENT_SINGLE ||
|
||||||
|
schema.type === FieldType.SIGNATURE) &&
|
||||||
|
Object.keys(value[field]).length >= 1}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
<div class="json-input-spacing">
|
||||||
|
<svelte:component
|
||||||
|
this={isTestModal ? ModalBindableInput : DrawerBindableInput}
|
||||||
|
panel={AutomationBindingPanel}
|
||||||
|
value={value[field]}
|
||||||
|
on:change={e => onChange(e, field)}
|
||||||
|
type="string"
|
||||||
|
bindings={parsedBindings}
|
||||||
|
allowJS={true}
|
||||||
|
updateOnChange={false}
|
||||||
|
title={schema.name}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
{:else if ["string", "number", "bigint", "barcodeqr", "array"].includes(schema.type)}
|
{:else if ["string", "number", "bigint", "barcodeqr", "array"].includes(schema.type)}
|
||||||
<svelte:component
|
<svelte:component
|
||||||
|
@ -156,7 +206,8 @@
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.attachment-field-spacinng {
|
.attachment-field-spacing,
|
||||||
|
.json-input-spacing {
|
||||||
margin-top: var(--spacing-s);
|
margin-top: var(--spacing-s);
|
||||||
margin-bottom: var(--spacing-l);
|
margin-bottom: var(--spacing-l);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
<script>
|
<script>
|
||||||
import { viewsV2 } from "stores/builder"
|
import { viewsV2 } from "stores/builder"
|
||||||
import { admin } from "stores/portal"
|
import { admin, licensing } from "stores/portal"
|
||||||
import { Grid } from "@budibase/frontend-core"
|
import { Grid } from "@budibase/frontend-core"
|
||||||
import { API } from "api"
|
import { API } from "api"
|
||||||
import GridCreateEditRowModal from "components/backend/DataTable/modals/grid/GridCreateEditRowModal.svelte"
|
import GridCreateEditRowModal from "components/backend/DataTable/modals/grid/GridCreateEditRowModal.svelte"
|
||||||
|
@ -28,6 +28,7 @@
|
||||||
showAvatars={false}
|
showAvatars={false}
|
||||||
on:updatedatasource={handleGridViewUpdate}
|
on:updatedatasource={handleGridViewUpdate}
|
||||||
isCloud={$admin.cloud}
|
isCloud={$admin.cloud}
|
||||||
|
allowViewReadonlyColumns={$licensing.isViewReadonlyColumnsEnabled}
|
||||||
>
|
>
|
||||||
<svelte:fragment slot="filter">
|
<svelte:fragment slot="filter">
|
||||||
<GridFilterButton />
|
<GridFilterButton />
|
||||||
|
|
|
@ -17,6 +17,8 @@
|
||||||
SWITCHABLE_TYPES,
|
SWITCHABLE_TYPES,
|
||||||
ValidColumnNameRegex,
|
ValidColumnNameRegex,
|
||||||
helpers,
|
helpers,
|
||||||
|
CONSTANT_INTERNAL_ROW_COLS,
|
||||||
|
CONSTANT_EXTERNAL_ROW_COLS,
|
||||||
} from "@budibase/shared-core"
|
} from "@budibase/shared-core"
|
||||||
import { createEventDispatcher, getContext, onMount } from "svelte"
|
import { createEventDispatcher, getContext, onMount } from "svelte"
|
||||||
import { cloneDeep } from "lodash/fp"
|
import { cloneDeep } from "lodash/fp"
|
||||||
|
@ -52,7 +54,6 @@
|
||||||
const DATE_TYPE = FieldType.DATETIME
|
const DATE_TYPE = FieldType.DATETIME
|
||||||
|
|
||||||
const dispatch = createEventDispatcher()
|
const dispatch = createEventDispatcher()
|
||||||
const PROHIBITED_COLUMN_NAMES = ["type", "_id", "_rev", "tableId"]
|
|
||||||
const { dispatch: gridDispatch, rows } = getContext("grid")
|
const { dispatch: gridDispatch, rows } = getContext("grid")
|
||||||
|
|
||||||
export let field
|
export let field
|
||||||
|
@ -334,7 +335,7 @@
|
||||||
// Add in defaults and initial definition
|
// Add in defaults and initial definition
|
||||||
const definition = fieldDefinitions[type?.toUpperCase()]
|
const definition = fieldDefinitions[type?.toUpperCase()]
|
||||||
if (definition?.constraints) {
|
if (definition?.constraints) {
|
||||||
editableColumn.constraints = definition.constraints
|
editableColumn.constraints = cloneDeep(definition.constraints)
|
||||||
}
|
}
|
||||||
|
|
||||||
editableColumn.type = definition.type
|
editableColumn.type = definition.type
|
||||||
|
@ -487,20 +488,23 @@
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
const newError = {}
|
const newError = {}
|
||||||
|
const prohibited = externalTable
|
||||||
|
? CONSTANT_EXTERNAL_ROW_COLS
|
||||||
|
: CONSTANT_INTERNAL_ROW_COLS
|
||||||
if (!externalTable && fieldInfo.name?.startsWith("_")) {
|
if (!externalTable && fieldInfo.name?.startsWith("_")) {
|
||||||
newError.name = `Column name cannot start with an underscore.`
|
newError.name = `Column name cannot start with an underscore.`
|
||||||
} else if (fieldInfo.name && !fieldInfo.name.match(ValidColumnNameRegex)) {
|
} else if (fieldInfo.name && !fieldInfo.name.match(ValidColumnNameRegex)) {
|
||||||
newError.name = `Illegal character; must be alpha-numeric.`
|
newError.name = `Illegal character; must be alpha-numeric.`
|
||||||
} else if (PROHIBITED_COLUMN_NAMES.some(name => fieldInfo.name === name)) {
|
} else if (prohibited.some(name => fieldInfo?.name === name)) {
|
||||||
newError.name = `${PROHIBITED_COLUMN_NAMES.join(
|
newError.name = `${prohibited.join(
|
||||||
", "
|
", "
|
||||||
)} are not allowed as column names`
|
)} are not allowed as column names - case insensitive.`
|
||||||
} else if (inUse($tables.selected, fieldInfo.name, originalName)) {
|
} else if (inUse($tables.selected, fieldInfo.name, originalName)) {
|
||||||
newError.name = `Column name already in use.`
|
newError.name = `Column name already in use.`
|
||||||
}
|
}
|
||||||
|
|
||||||
if (fieldInfo.type === FieldType.AUTO && !fieldInfo.subtype) {
|
if (fieldInfo.type === FieldType.AUTO && !fieldInfo.subtype) {
|
||||||
newError.subtype = `Auto Column requires a type`
|
newError.subtype = `Auto Column requires a type.`
|
||||||
}
|
}
|
||||||
|
|
||||||
if (fieldInfo.fieldName && fieldInfo.tableId) {
|
if (fieldInfo.fieldName && fieldInfo.tableId) {
|
||||||
|
|
|
@ -8,7 +8,7 @@
|
||||||
} from "@budibase/bbui"
|
} from "@budibase/bbui"
|
||||||
import download from "downloadjs"
|
import download from "downloadjs"
|
||||||
import { API } from "api"
|
import { API } from "api"
|
||||||
import { LuceneUtils } from "@budibase/frontend-core"
|
import { QueryUtils } from "@budibase/frontend-core"
|
||||||
import { utils } from "@budibase/shared-core"
|
import { utils } from "@budibase/shared-core"
|
||||||
import { ROW_EXPORT_FORMATS } from "constants/backend"
|
import { ROW_EXPORT_FORMATS } from "constants/backend"
|
||||||
|
|
||||||
|
@ -49,7 +49,7 @@
|
||||||
exportFormat = Array.isArray(options) ? options[0]?.key : []
|
exportFormat = Array.isArray(options) ? options[0]?.key : []
|
||||||
}
|
}
|
||||||
|
|
||||||
$: luceneFilter = LuceneUtils.buildLuceneQuery(appliedFilters)
|
$: query = QueryUtils.buildQuery(appliedFilters)
|
||||||
$: exportOpDisplay = buildExportOpDisplay(
|
$: exportOpDisplay = buildExportOpDisplay(
|
||||||
sorting,
|
sorting,
|
||||||
filterDisplay,
|
filterDisplay,
|
||||||
|
@ -139,7 +139,7 @@
|
||||||
tableId: view,
|
tableId: view,
|
||||||
format: exportFormat,
|
format: exportFormat,
|
||||||
search: {
|
search: {
|
||||||
query: luceneFilter,
|
query,
|
||||||
sort: sorting?.sortColumn,
|
sort: sorting?.sortColumn,
|
||||||
sortOrder: sorting?.sortOrder,
|
sortOrder: sorting?.sortOrder,
|
||||||
paginate: false,
|
paginate: false,
|
||||||
|
|
|
@ -1,9 +1,14 @@
|
||||||
<script>
|
<script>
|
||||||
import { FieldType, BBReferenceFieldSubType } from "@budibase/types"
|
import {
|
||||||
|
FieldType,
|
||||||
|
BBReferenceFieldSubType,
|
||||||
|
SourceName,
|
||||||
|
} from "@budibase/types"
|
||||||
import { Select, Toggle, Multiselect } from "@budibase/bbui"
|
import { Select, Toggle, Multiselect } from "@budibase/bbui"
|
||||||
import { DB_TYPE_INTERNAL } from "constants/backend"
|
import { DB_TYPE_INTERNAL } from "constants/backend"
|
||||||
import { API } from "api"
|
import { API } from "api"
|
||||||
import { parseFile } from "./utils"
|
import { parseFile } from "./utils"
|
||||||
|
import { tables, datasources } from "stores/builder"
|
||||||
|
|
||||||
let error = null
|
let error = null
|
||||||
let fileName = null
|
let fileName = null
|
||||||
|
@ -80,6 +85,9 @@
|
||||||
schema = fetchSchema(tableId)
|
schema = fetchSchema(tableId)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$: table = $tables.list.find(table => table._id === tableId)
|
||||||
|
$: datasource = $datasources.list.find(ds => ds._id === table?.sourceId)
|
||||||
|
|
||||||
async function fetchSchema(tableId) {
|
async function fetchSchema(tableId) {
|
||||||
try {
|
try {
|
||||||
const definition = await API.fetchTableDefinition(tableId)
|
const definition = await API.fetchTableDefinition(tableId)
|
||||||
|
@ -185,20 +193,25 @@
|
||||||
</div>
|
</div>
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
{#if tableType === DB_TYPE_INTERNAL}
|
<br />
|
||||||
<br />
|
<!-- SQL Server doesn't yet support overwriting rows by existing keys -->
|
||||||
|
{#if datasource?.source !== SourceName.SQL_SERVER}
|
||||||
<Toggle
|
<Toggle
|
||||||
bind:value={updateExistingRows}
|
bind:value={updateExistingRows}
|
||||||
on:change={() => (identifierFields = [])}
|
on:change={() => (identifierFields = [])}
|
||||||
thin
|
thin
|
||||||
text="Update existing rows"
|
text="Update existing rows"
|
||||||
/>
|
/>
|
||||||
{#if updateExistingRows}
|
{/if}
|
||||||
|
{#if updateExistingRows}
|
||||||
|
{#if tableType === DB_TYPE_INTERNAL}
|
||||||
<Multiselect
|
<Multiselect
|
||||||
label="Identifier field(s)"
|
label="Identifier field(s)"
|
||||||
options={Object.keys(validation)}
|
options={Object.keys(validation)}
|
||||||
bind:value={identifierFields}
|
bind:value={identifierFields}
|
||||||
/>
|
/>
|
||||||
|
{:else}
|
||||||
|
<p>Rows will be updated based on the table's primary key.</p>
|
||||||
{/if}
|
{/if}
|
||||||
{/if}
|
{/if}
|
||||||
{#if invalidColumns.length > 0}
|
{#if invalidColumns.length > 0}
|
||||||
|
|
|
@ -38,4 +38,5 @@
|
||||||
{processFiles}
|
{processFiles}
|
||||||
handleFileTooLarge={$admin.cloud ? handleFileTooLarge : null}
|
handleFileTooLarge={$admin.cloud ? handleFileTooLarge : null}
|
||||||
{fileSizeLimit}
|
{fileSizeLimit}
|
||||||
|
on:change
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -0,0 +1,8 @@
|
||||||
|
<div class="root">This action doesn't require any settings.</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.root {
|
||||||
|
max-width: 400px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -53,6 +53,12 @@
|
||||||
placeholder="Are you sure you want to delete?"
|
placeholder="Are you sure you want to delete?"
|
||||||
bind:value={parameters.confirmText}
|
bind:value={parameters.confirmText}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<Label small>Confirm Text</Label>
|
||||||
|
<Input placeholder="Confirm" bind:value={parameters.confirmButtonText} />
|
||||||
|
|
||||||
|
<Label small>Cancel Text</Label>
|
||||||
|
<Input placeholder="Cancel" bind:value={parameters.cancelButtonText} />
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -83,6 +83,12 @@
|
||||||
placeholder="Are you sure you want to duplicate this row?"
|
placeholder="Are you sure you want to duplicate this row?"
|
||||||
bind:value={parameters.confirmText}
|
bind:value={parameters.confirmText}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<Label small>Confirm Text</Label>
|
||||||
|
<Input placeholder="Confirm" bind:value={parameters.confirmButtonText} />
|
||||||
|
|
||||||
|
<Label small>Cancel Text</Label>
|
||||||
|
<Input placeholder="Cancel" bind:value={parameters.cancelButtonText} />
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
@ -74,6 +74,18 @@
|
||||||
placeholder="Are you sure you want to execute this query?"
|
placeholder="Are you sure you want to execute this query?"
|
||||||
bind:value={parameters.confirmText}
|
bind:value={parameters.confirmText}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<Input
|
||||||
|
label="Confirm Text"
|
||||||
|
placeholder="Confirm"
|
||||||
|
bind:value={parameters.confirmButtonText}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Input
|
||||||
|
label="Cancel Text"
|
||||||
|
placeholder="Cancel"
|
||||||
|
bind:value={parameters.cancelButtonText}
|
||||||
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if query?.parameters?.length > 0}
|
{#if query?.parameters?.length > 0}
|
||||||
|
|
|
@ -0,0 +1,36 @@
|
||||||
|
<script>
|
||||||
|
import { Select, Label } from "@budibase/bbui"
|
||||||
|
import { selectedScreen } from "stores/builder"
|
||||||
|
import { findAllMatchingComponents } from "helpers/components"
|
||||||
|
|
||||||
|
export let parameters
|
||||||
|
|
||||||
|
$: modalOptions = getModalOptions($selectedScreen)
|
||||||
|
|
||||||
|
const getModalOptions = screen => {
|
||||||
|
const modalComponents = findAllMatchingComponents(screen.props, component =>
|
||||||
|
component._component.endsWith("/modal")
|
||||||
|
)
|
||||||
|
return modalComponents.map(modal => ({
|
||||||
|
label: modal._instanceName,
|
||||||
|
value: modal._id,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="root">
|
||||||
|
<Label small>Modal</Label>
|
||||||
|
<Select bind:value={parameters.id} options={modalOptions} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.root {
|
||||||
|
display: grid;
|
||||||
|
column-gap: var(--spacing-l);
|
||||||
|
row-gap: var(--spacing-s);
|
||||||
|
grid-template-columns: 60px 1fr;
|
||||||
|
align-items: center;
|
||||||
|
max-width: 400px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -18,7 +18,7 @@
|
||||||
<div class="params">
|
<div class="params">
|
||||||
<Label small>Title</Label>
|
<Label small>Title</Label>
|
||||||
<DrawerBindableInput
|
<DrawerBindableInput
|
||||||
placeholder="Title"
|
placeholder="Prompt User"
|
||||||
value={parameters.customTitleText}
|
value={parameters.customTitleText}
|
||||||
on:change={e => (parameters.customTitleText = e.detail)}
|
on:change={e => (parameters.customTitleText = e.detail)}
|
||||||
{bindings}
|
{bindings}
|
||||||
|
@ -30,6 +30,22 @@
|
||||||
on:change={e => (parameters.confirmText = e.detail)}
|
on:change={e => (parameters.confirmText = e.detail)}
|
||||||
{bindings}
|
{bindings}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<Label small>Confirm Text</Label>
|
||||||
|
<DrawerBindableInput
|
||||||
|
placeholder="Confirm"
|
||||||
|
value={parameters.confirmButtonText}
|
||||||
|
on:change={e => (parameters.confirmButtonText = e.detail)}
|
||||||
|
{bindings}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Label small>Cancel Text</Label>
|
||||||
|
<DrawerBindableInput
|
||||||
|
placeholder="Cancel"
|
||||||
|
value={parameters.cancelButtonText}
|
||||||
|
on:change={e => (parameters.cancelButtonText = e.detail)}
|
||||||
|
{bindings}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
@ -80,6 +80,12 @@
|
||||||
placeholder="Are you sure you want to save this row?"
|
placeholder="Are you sure you want to save this row?"
|
||||||
bind:value={parameters.confirmText}
|
bind:value={parameters.confirmText}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<Label small>Confirm Text</Label>
|
||||||
|
<Input placeholder="Confirm" bind:value={parameters.confirmButtonText} />
|
||||||
|
|
||||||
|
<Label small>Cancel Text</Label>
|
||||||
|
<Input placeholder="Cancel" bind:value={parameters.cancelButtonText} />
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
@ -25,6 +25,8 @@
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
const MAX_DURATION = 120000 // Maximum duration in milliseconds (2 minutes)
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
if (!parameters.type) {
|
if (!parameters.type) {
|
||||||
parameters.type = "success"
|
parameters.type = "success"
|
||||||
|
@ -33,6 +35,14 @@
|
||||||
parameters.autoDismiss = true
|
parameters.autoDismiss = true
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
function handleDurationChange(event) {
|
||||||
|
let newDuration = event.detail
|
||||||
|
if (newDuration > MAX_DURATION) {
|
||||||
|
newDuration = MAX_DURATION
|
||||||
|
}
|
||||||
|
parameters.duration = newDuration
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="root">
|
<div class="root">
|
||||||
|
@ -47,6 +57,16 @@
|
||||||
/>
|
/>
|
||||||
<Label />
|
<Label />
|
||||||
<Checkbox text="Auto dismiss" bind:value={parameters.autoDismiss} />
|
<Checkbox text="Auto dismiss" bind:value={parameters.autoDismiss} />
|
||||||
|
{#if parameters.autoDismiss}
|
||||||
|
<Label>Duration (ms)</Label>
|
||||||
|
<DrawerBindableInput
|
||||||
|
title="Duration"
|
||||||
|
{bindings}
|
||||||
|
value={parameters.duration}
|
||||||
|
placeholder="3000"
|
||||||
|
on:change={handleDurationChange}
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
|
|
@ -21,5 +21,7 @@ export { default as ShowNotification } from "./ShowNotification.svelte"
|
||||||
export { default as PromptUser } from "./PromptUser.svelte"
|
export { default as PromptUser } from "./PromptUser.svelte"
|
||||||
export { default as OpenSidePanel } from "./OpenSidePanel.svelte"
|
export { default as OpenSidePanel } from "./OpenSidePanel.svelte"
|
||||||
export { default as CloseSidePanel } from "./CloseSidePanel.svelte"
|
export { default as CloseSidePanel } from "./CloseSidePanel.svelte"
|
||||||
|
export { default as OpenModal } from "./OpenModal.svelte"
|
||||||
|
export { default as CloseModal } from "./CloseModal.svelte"
|
||||||
export { default as ClearRowSelection } from "./ClearRowSelection.svelte"
|
export { default as ClearRowSelection } from "./ClearRowSelection.svelte"
|
||||||
export { default as DownloadFile } from "./DownloadFile.svelte"
|
export { default as DownloadFile } from "./DownloadFile.svelte"
|
||||||
|
|
|
@ -157,6 +157,18 @@
|
||||||
"component": "CloseSidePanel",
|
"component": "CloseSidePanel",
|
||||||
"dependsOnFeature": "sidePanel"
|
"dependsOnFeature": "sidePanel"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "Open Modal",
|
||||||
|
"type": "application",
|
||||||
|
"component": "OpenModal",
|
||||||
|
"dependsOnFeature": "modal"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Close Modal",
|
||||||
|
"type": "application",
|
||||||
|
"component": "CloseModal",
|
||||||
|
"dependsOnFeature": "modal"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "Clear Row Selection",
|
"name": "Clear Row Selection",
|
||||||
"type": "data",
|
"type": "data",
|
||||||
|
|
|
@ -29,7 +29,7 @@
|
||||||
on:click={() => onSelect(data)}
|
on:click={() => onSelect(data)}
|
||||||
>
|
>
|
||||||
<span class="spectrum-Menu-itemLabel">
|
<span class="spectrum-Menu-itemLabel">
|
||||||
{data.label}
|
{data.datasource?.name ? `${data.datasource.name} - ` : ""}{data.label}
|
||||||
</span>
|
</span>
|
||||||
<svg
|
<svg
|
||||||
class="spectrum-Icon spectrum-UIIcon-Checkmark100 spectrum-Menu-checkmark spectrum-Menu-itemIcon"
|
class="spectrum-Icon spectrum-UIIcon-Checkmark100 spectrum-Menu-checkmark spectrum-Menu-itemIcon"
|
||||||
|
|
|
@ -55,6 +55,9 @@
|
||||||
label: m.name,
|
label: m.name,
|
||||||
tableId: m._id,
|
tableId: m._id,
|
||||||
type: "table",
|
type: "table",
|
||||||
|
datasource: $datasources.list.find(
|
||||||
|
ds => ds._id === m.sourceId || m.datasourceId
|
||||||
|
),
|
||||||
}))
|
}))
|
||||||
$: viewsV1 = $viewsStore.list.map(view => ({
|
$: viewsV1 = $viewsStore.list.map(view => ({
|
||||||
...view,
|
...view,
|
||||||
|
|
|
@ -18,14 +18,11 @@
|
||||||
import subjects from "./subjects"
|
import subjects from "./subjects"
|
||||||
import { appStore } from "stores/builder"
|
import { appStore } from "stores/builder"
|
||||||
|
|
||||||
export let explanation
|
|
||||||
export let columnIcon
|
|
||||||
export let columnType
|
|
||||||
export let columnName
|
|
||||||
|
|
||||||
export let tableHref = () => {}
|
export let tableHref = () => {}
|
||||||
|
|
||||||
export let schema
|
export let schema
|
||||||
|
export let name
|
||||||
|
export let explanation
|
||||||
|
export let componentName
|
||||||
|
|
||||||
$: explanationWithPresets = getExplanationWithPresets(
|
$: explanationWithPresets = getExplanationWithPresets(
|
||||||
explanation,
|
explanation,
|
||||||
|
@ -54,14 +51,8 @@
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div bind:this={root} class="tooltipContents">
|
<div bind:this={root} class="tooltipContents">
|
||||||
<Column
|
<Column {name} {schema} {tableHref} {setExplanationSubject} />
|
||||||
{columnName}
|
<Support {componentName} {support} {setExplanationSubject} />
|
||||||
{columnIcon}
|
|
||||||
{columnType}
|
|
||||||
{tableHref}
|
|
||||||
{setExplanationSubject}
|
|
||||||
/>
|
|
||||||
<Support {support} {setExplanationSubject} />
|
|
||||||
{#if messages.includes(messageConstants.stringAsNumber)}
|
{#if messages.includes(messageConstants.stringAsNumber)}
|
||||||
<StringAsNumber {setExplanationSubject} />
|
<StringAsNumber {setExplanationSubject} />
|
||||||
{/if}
|
{/if}
|
||||||
|
@ -84,7 +75,7 @@
|
||||||
|
|
||||||
{#if detailsModalSubject !== subjects.none}
|
{#if detailsModalSubject !== subjects.none}
|
||||||
<DetailsModal
|
<DetailsModal
|
||||||
{columnName}
|
columnName={name}
|
||||||
anchor={root}
|
anchor={root}
|
||||||
{schema}
|
{schema}
|
||||||
subject={detailsModalSubject}
|
subject={detailsModalSubject}
|
||||||
|
|
|
@ -1,69 +1,124 @@
|
||||||
<script>
|
<script>
|
||||||
import {
|
import { Line, InfoWord, DocumentationLink, Text } from "../typography"
|
||||||
Line,
|
import { FieldType } from "@budibase/types"
|
||||||
InfoWord,
|
import { FIELDS } from "constants/backend"
|
||||||
DocumentationLink,
|
|
||||||
Text,
|
|
||||||
Period,
|
|
||||||
} from "../typography"
|
|
||||||
import subjects from "../subjects"
|
import subjects from "../subjects"
|
||||||
|
|
||||||
export let columnName
|
export let schema
|
||||||
export let columnIcon
|
export let name
|
||||||
export let columnType
|
|
||||||
export let tableHref
|
export let tableHref
|
||||||
export let setExplanationSubject
|
export let setExplanationSubject
|
||||||
|
|
||||||
|
const getTypeName = schema => {
|
||||||
|
const fieldDefinition = Object.values(FIELDS).find(
|
||||||
|
f => f.type === schema?.type
|
||||||
|
)
|
||||||
|
|
||||||
|
if (schema?.type === "jsonarray") {
|
||||||
|
return "JSON Array"
|
||||||
|
}
|
||||||
|
if (schema?.type === "options") {
|
||||||
|
return "Options"
|
||||||
|
}
|
||||||
|
|
||||||
|
return fieldDefinition?.name || schema?.type || "Unknown"
|
||||||
|
}
|
||||||
|
|
||||||
|
const getTypeIcon = schema => {
|
||||||
|
const fieldDefinition = Object.values(FIELDS).find(
|
||||||
|
f => f.type === schema?.type
|
||||||
|
)
|
||||||
|
|
||||||
|
if (schema?.type === "jsonarray") {
|
||||||
|
return "BracketsSquare"
|
||||||
|
}
|
||||||
|
|
||||||
|
return fieldDefinition?.icon || "Circle"
|
||||||
|
}
|
||||||
|
|
||||||
const getDocLink = columnType => {
|
const getDocLink = columnType => {
|
||||||
if (columnType === "Number") {
|
if (columnType === FieldType.NUMBER) {
|
||||||
return "https://docs.budibase.com/docs/number"
|
return "https://docs.budibase.com/docs/number"
|
||||||
}
|
}
|
||||||
if (columnType === "Text") {
|
if (columnType === FieldType.STRING) {
|
||||||
return "https://docs.budibase.com/docs/text"
|
return "https://docs.budibase.com/docs/text"
|
||||||
}
|
}
|
||||||
if (columnType === "Attachment") {
|
if (columnType === FieldType.LONGFORM) {
|
||||||
|
return "https://docs.budibase.com/docs/text"
|
||||||
|
}
|
||||||
|
if (columnType === FieldType.ATTACHMENT_SINGLE) {
|
||||||
return "https://docs.budibase.com/docs/attachments"
|
return "https://docs.budibase.com/docs/attachments"
|
||||||
}
|
}
|
||||||
if (columnType === "Multi-select") {
|
if (columnType === FieldType.ATTACHMENTS) {
|
||||||
|
// No distinct multi attachment docs, link to attachment instead
|
||||||
|
return "https://docs.budibase.com/docs/attachments"
|
||||||
|
}
|
||||||
|
if (columnType === FieldType.ARRAY) {
|
||||||
return "https://docs.budibase.com/docs/multi-select"
|
return "https://docs.budibase.com/docs/multi-select"
|
||||||
}
|
}
|
||||||
if (columnType === "JSON") {
|
if (columnType === FieldType.JSON) {
|
||||||
return "https://docs.budibase.com/docs/json"
|
return "https://docs.budibase.com/docs/json"
|
||||||
}
|
}
|
||||||
if (columnType === "Date/Time") {
|
if (columnType === "jsonarray") {
|
||||||
|
return "https://docs.budibase.com/docs/json"
|
||||||
|
}
|
||||||
|
if (columnType === FieldType.DATETIME) {
|
||||||
return "https://docs.budibase.com/docs/datetime"
|
return "https://docs.budibase.com/docs/datetime"
|
||||||
}
|
}
|
||||||
if (columnType === "User") {
|
if (columnType === FieldType.BB_REFERENCE_SINGLE) {
|
||||||
return "https://docs.budibase.com/docs/user"
|
return "https://docs.budibase.com/docs/users-1"
|
||||||
}
|
}
|
||||||
if (columnType === "QR") {
|
if (columnType === FieldType.BB_REFERENCE) {
|
||||||
|
return "https://docs.budibase.com/docs/users-1"
|
||||||
|
}
|
||||||
|
if (columnType === FieldType.BARCODEQR) {
|
||||||
return "https://docs.budibase.com/docs/barcodeqr"
|
return "https://docs.budibase.com/docs/barcodeqr"
|
||||||
}
|
}
|
||||||
if (columnType === "Relationship") {
|
if (columnType === FieldType.LINK) {
|
||||||
return "https://docs.budibase.com/docs/relationships"
|
return "https://docs.budibase.com/docs/relationships"
|
||||||
}
|
}
|
||||||
if (columnType === "Formula") {
|
if (columnType === FieldType.FORMULA) {
|
||||||
return "https://docs.budibase.com/docs/formula"
|
return "https://docs.budibase.com/docs/formula"
|
||||||
}
|
}
|
||||||
if (columnType === "Options") {
|
if (columnType === FieldType.OPTIONS) {
|
||||||
return "https://docs.budibase.com/docs/options"
|
return "https://docs.budibase.com/docs/options"
|
||||||
}
|
}
|
||||||
if (columnType === "BigInt") {
|
if (columnType === FieldType.BOOLEAN) {
|
||||||
// No BigInt docs
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
if (columnType === "Boolean") {
|
|
||||||
return "https://docs.budibase.com/docs/boolean-truefalse"
|
return "https://docs.budibase.com/docs/boolean-truefalse"
|
||||||
}
|
}
|
||||||
if (columnType === "Signature") {
|
if (columnType === FieldType.SIGNATURE_SINGLE) {
|
||||||
// No Signature docs
|
// No Signature docs
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
if (columnType === FieldType.BIGINT) {
|
||||||
|
// No BigInt docs
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
$: docLink = getDocLink(columnType)
|
// NOTE The correct indefinite article is based on the pronounciation of the word it precedes, not the spelling. So simply checking if the word begins with a vowel is not sufficient.
|
||||||
|
|
||||||
|
// e.g., `an honor`, `a user`
|
||||||
|
const getIndefiniteArticle = schema => {
|
||||||
|
const anTypes = [
|
||||||
|
FieldType.OPTIONS,
|
||||||
|
null, // `null` gets parsed as "unknown"
|
||||||
|
undefined, // `undefined` gets parsed as "unknown"
|
||||||
|
]
|
||||||
|
|
||||||
|
if (anTypes.includes(schema?.type)) {
|
||||||
|
return "an"
|
||||||
|
}
|
||||||
|
|
||||||
|
return "a"
|
||||||
|
}
|
||||||
|
|
||||||
|
$: columnTypeName = getTypeName(schema)
|
||||||
|
$: columnIcon = getTypeIcon(schema)
|
||||||
|
$: docLink = getDocLink(schema?.type)
|
||||||
|
$: indefiniteArticle = getIndefiniteArticle(schema)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Line noWrap>
|
<Line noWrap>
|
||||||
|
@ -71,14 +126,14 @@
|
||||||
on:mouseenter={() => setExplanationSubject(subjects.column)}
|
on:mouseenter={() => setExplanationSubject(subjects.column)}
|
||||||
on:mouseleave={() => setExplanationSubject(subjects.none)}
|
on:mouseleave={() => setExplanationSubject(subjects.none)}
|
||||||
href={tableHref}
|
href={tableHref}
|
||||||
text={columnName}
|
text={name}
|
||||||
/>
|
/>
|
||||||
<Text value=" is a " />
|
<Text value={` is ${indefiniteArticle} `} />
|
||||||
<DocumentationLink
|
<DocumentationLink
|
||||||
disabled={docLink === null}
|
disabled={docLink === null}
|
||||||
href={docLink}
|
href={docLink}
|
||||||
icon={columnIcon}
|
icon={columnIcon}
|
||||||
text={`${columnType} column`}
|
text={columnTypeName}
|
||||||
/>
|
/>
|
||||||
<Period />
|
<Text value=" column." />
|
||||||
</Line>
|
</Line>
|
||||||
|
|
|
@ -2,9 +2,16 @@
|
||||||
import { Line, InfoWord, DocumentationLink, Text } from "../typography"
|
import { Line, InfoWord, DocumentationLink, Text } from "../typography"
|
||||||
import subjects from "../subjects"
|
import subjects from "../subjects"
|
||||||
import * as explanation from "../explanation"
|
import * as explanation from "../explanation"
|
||||||
|
import { componentStore } from "stores/builder"
|
||||||
|
|
||||||
export let setExplanationSubject
|
export let setExplanationSubject
|
||||||
export let support
|
export let support
|
||||||
|
export let componentName
|
||||||
|
|
||||||
|
const getComponentDefinition = componentName => {
|
||||||
|
const components = $componentStore.components || {}
|
||||||
|
return components[componentName] || null
|
||||||
|
}
|
||||||
|
|
||||||
const getIcon = support => {
|
const getIcon = support => {
|
||||||
if (support === explanation.support.unsupported) {
|
if (support === explanation.support.unsupported) {
|
||||||
|
@ -39,21 +46,24 @@
|
||||||
$: icon = getIcon(support)
|
$: icon = getIcon(support)
|
||||||
$: color = getColor(support)
|
$: color = getColor(support)
|
||||||
$: text = getText(support)
|
$: text = getText(support)
|
||||||
|
$: componentDefinition = getComponentDefinition(componentName)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Line>
|
{#if componentDefinition}
|
||||||
<InfoWord
|
<Line>
|
||||||
on:mouseenter={() => setExplanationSubject(subjects.support)}
|
<InfoWord
|
||||||
on:mouseleave={() => setExplanationSubject(subjects.none)}
|
on:mouseenter={() => setExplanationSubject(subjects.support)}
|
||||||
{icon}
|
on:mouseleave={() => setExplanationSubject(subjects.none)}
|
||||||
{color}
|
{icon}
|
||||||
{text}
|
{color}
|
||||||
/>
|
{text}
|
||||||
<Text value=" with this " />
|
/>
|
||||||
<DocumentationLink
|
<Text value=" with this " />
|
||||||
href="https://docs.budibase.com/docs/chart"
|
<DocumentationLink
|
||||||
icon="GraphPie"
|
href={componentDefinition.documentationLink}
|
||||||
text="Chart component"
|
icon={componentDefinition.icon}
|
||||||
/>
|
text={componentDefinition.name}
|
||||||
<Text value=" input." />
|
/>
|
||||||
</Line>
|
<Text value=" input." />
|
||||||
|
</Line>
|
||||||
|
{/if}
|
||||||
|
|
|
@ -6,8 +6,6 @@
|
||||||
import { Explanation } from "./Explanation"
|
import { Explanation } from "./Explanation"
|
||||||
import { debounce } from "lodash"
|
import { debounce } from "lodash"
|
||||||
import { params } from "@roxi/routify"
|
import { params } from "@roxi/routify"
|
||||||
import { Constants } from "@budibase/frontend-core"
|
|
||||||
import { FIELDS } from "constants/backend"
|
|
||||||
|
|
||||||
export let componentInstance = {}
|
export let componentInstance = {}
|
||||||
export let value = ""
|
export let value = ""
|
||||||
|
@ -60,35 +58,6 @@
|
||||||
const onOptionMouseleave = e => {
|
const onOptionMouseleave = e => {
|
||||||
updateTooltip(e, null)
|
updateTooltip(e, null)
|
||||||
}
|
}
|
||||||
const getOptionIcon = optionKey => {
|
|
||||||
const option = schema[optionKey]
|
|
||||||
if (!option) return ""
|
|
||||||
|
|
||||||
if (option.autocolumn) {
|
|
||||||
return "MagicWand"
|
|
||||||
}
|
|
||||||
const { type, subtype } = option
|
|
||||||
|
|
||||||
const result =
|
|
||||||
typeof Constants.TypeIconMap[type] === "object" && subtype
|
|
||||||
? Constants.TypeIconMap[type][subtype]
|
|
||||||
: Constants.TypeIconMap[type]
|
|
||||||
|
|
||||||
return result || "Text"
|
|
||||||
}
|
|
||||||
|
|
||||||
const getOptionIconTooltip = optionKey => {
|
|
||||||
const option = schema[optionKey]
|
|
||||||
|
|
||||||
const type = option?.type
|
|
||||||
const field = Object.values(FIELDS).find(f => f.type === type)
|
|
||||||
|
|
||||||
if (field) {
|
|
||||||
return field.name
|
|
||||||
}
|
|
||||||
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Select
|
<Select
|
||||||
|
@ -109,10 +78,9 @@
|
||||||
<Explanation
|
<Explanation
|
||||||
tableHref={`/builder/app/${$params.application}/data/table/${datasource?.tableId}`}
|
tableHref={`/builder/app/${$params.application}/data/table/${datasource?.tableId}`}
|
||||||
schema={schema[currentOption]}
|
schema={schema[currentOption]}
|
||||||
columnIcon={getOptionIcon(currentOption)}
|
name={currentOption}
|
||||||
columnName={currentOption}
|
|
||||||
columnType={getOptionIconTooltip(currentOption)}
|
|
||||||
{explanation}
|
{explanation}
|
||||||
|
componentName={componentInstance._component}
|
||||||
/>
|
/>
|
||||||
</ContextTooltip>
|
</ContextTooltip>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
|
@ -9,7 +9,8 @@
|
||||||
import { createEventDispatcher } from "svelte"
|
import { createEventDispatcher } from "svelte"
|
||||||
import { getDatasourceForProvider, getSchemaForDatasource } from "dataBinding"
|
import { getDatasourceForProvider, getSchemaForDatasource } from "dataBinding"
|
||||||
import FilterBuilder from "./FilterBuilder.svelte"
|
import FilterBuilder from "./FilterBuilder.svelte"
|
||||||
import { selectedScreen } from "stores/builder"
|
import { tables, selectedScreen } from "stores/builder"
|
||||||
|
import { search } from "@budibase/frontend-core"
|
||||||
|
|
||||||
const dispatch = createEventDispatcher()
|
const dispatch = createEventDispatcher()
|
||||||
|
|
||||||
|
@ -23,7 +24,11 @@
|
||||||
$: tempValue = value
|
$: tempValue = value
|
||||||
$: datasource = getDatasourceForProvider($selectedScreen, componentInstance)
|
$: datasource = getDatasourceForProvider($selectedScreen, componentInstance)
|
||||||
$: dsSchema = getSchemaForDatasource($selectedScreen, datasource)?.schema
|
$: dsSchema = getSchemaForDatasource($selectedScreen, datasource)?.schema
|
||||||
$: schemaFields = Object.values(schema || dsSchema || {})
|
$: schemaFields = search.getFields(
|
||||||
|
$tables.list,
|
||||||
|
Object.values(schema || dsSchema || {}),
|
||||||
|
{ allowLinks: true }
|
||||||
|
)
|
||||||
$: text = getText(value?.filter(filter => filter.field))
|
$: text = getText(value?.filter(filter => filter.field))
|
||||||
|
|
||||||
async function saveFilter() {
|
async function saveFilter() {
|
||||||
|
|
|
@ -4,10 +4,8 @@
|
||||||
import { selectedScreen } from "stores/builder"
|
import { selectedScreen } from "stores/builder"
|
||||||
import { createEventDispatcher } from "svelte"
|
import { createEventDispatcher } from "svelte"
|
||||||
import { Explanation } from "./Explanation"
|
import { Explanation } from "./Explanation"
|
||||||
import { FIELDS } from "constants/backend"
|
|
||||||
import { params } from "@roxi/routify"
|
import { params } from "@roxi/routify"
|
||||||
import { debounce } from "lodash"
|
import { debounce } from "lodash"
|
||||||
import { Constants } from "@budibase/frontend-core"
|
|
||||||
|
|
||||||
export let componentInstance = {}
|
export let componentInstance = {}
|
||||||
export let value = ""
|
export let value = ""
|
||||||
|
@ -37,40 +35,6 @@
|
||||||
dispatch("change", boundValue)
|
dispatch("change", boundValue)
|
||||||
}
|
}
|
||||||
|
|
||||||
const getOptionIcon = optionKey => {
|
|
||||||
const option = schema[optionKey]
|
|
||||||
if (!option) return ""
|
|
||||||
|
|
||||||
if (option.autocolumn) {
|
|
||||||
return "MagicWand"
|
|
||||||
}
|
|
||||||
const { type, subtype } = option
|
|
||||||
|
|
||||||
const result =
|
|
||||||
typeof Constants.TypeIconMap[type] === "object" && subtype
|
|
||||||
? Constants.TypeIconMap[type][subtype]
|
|
||||||
: Constants.TypeIconMap[type]
|
|
||||||
|
|
||||||
return result || "Text"
|
|
||||||
}
|
|
||||||
|
|
||||||
const getOptionIconTooltip = optionKey => {
|
|
||||||
const option = schema[optionKey]
|
|
||||||
|
|
||||||
const type = option?.type
|
|
||||||
const field = Object.values(FIELDS).find(f => f.type === type)
|
|
||||||
|
|
||||||
if (field) {
|
|
||||||
return field.name
|
|
||||||
} else if (type === "jsonarray") {
|
|
||||||
// `jsonarray` isn't present in the above FIELDS constant
|
|
||||||
|
|
||||||
return "JSON Array"
|
|
||||||
}
|
|
||||||
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
const updateTooltip = debounce((e, option) => {
|
const updateTooltip = debounce((e, option) => {
|
||||||
if (option == null) {
|
if (option == null) {
|
||||||
contextTooltipVisible = false
|
contextTooltipVisible = false
|
||||||
|
@ -110,10 +74,9 @@
|
||||||
<Explanation
|
<Explanation
|
||||||
tableHref={`/builder/app/${$params.application}/data/table/${datasource?.tableId}`}
|
tableHref={`/builder/app/${$params.application}/data/table/${datasource?.tableId}`}
|
||||||
schema={schema[currentOption]}
|
schema={schema[currentOption]}
|
||||||
columnIcon={getOptionIcon(currentOption)}
|
name={currentOption}
|
||||||
columnName={currentOption}
|
|
||||||
columnType={getOptionIconTooltip(currentOption)}
|
|
||||||
{explanation}
|
{explanation}
|
||||||
|
componentName={componentInstance._component}
|
||||||
/>
|
/>
|
||||||
</ContextTooltip>
|
</ContextTooltip>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
|
@ -233,9 +233,9 @@
|
||||||
response.info = response.info || { code: 200 }
|
response.info = response.info || { code: 200 }
|
||||||
// if existing schema, copy over what it is
|
// if existing schema, copy over what it is
|
||||||
if (schema) {
|
if (schema) {
|
||||||
for (let [name, field] of Object.entries(schema)) {
|
for (let [name, field] of Object.entries(response.schema)) {
|
||||||
if (response.schema[name]) {
|
if (!schema[name]) {
|
||||||
response.schema[name] = field
|
schema[name] = field
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -30,6 +30,7 @@ import ActionDefinitions from "components/design/settings/controls/ButtonActionE
|
||||||
import { environment, licensing } from "stores/portal"
|
import { environment, licensing } from "stores/portal"
|
||||||
import { convertOldFieldFormat } from "components/design/settings/controls/FieldConfiguration/utils"
|
import { convertOldFieldFormat } from "components/design/settings/controls/FieldConfiguration/utils"
|
||||||
import { FIELDS } from "constants/backend"
|
import { FIELDS } from "constants/backend"
|
||||||
|
import { FieldType } from "@budibase/types"
|
||||||
|
|
||||||
const { ContextScopes } = Constants
|
const { ContextScopes } = Constants
|
||||||
|
|
||||||
|
@ -555,6 +556,9 @@ const getComponentBindingCategory = (component, context, def) => {
|
||||||
export const getUserBindings = () => {
|
export const getUserBindings = () => {
|
||||||
let bindings = []
|
let bindings = []
|
||||||
const { schema } = getSchemaForDatasourcePlus(TableNames.USERS)
|
const { schema } = getSchemaForDatasourcePlus(TableNames.USERS)
|
||||||
|
// add props that are not in the user metadata table schema
|
||||||
|
// but will be there for logged-in user
|
||||||
|
schema["globalId"] = { type: FieldType.STRING }
|
||||||
const keys = Object.keys(schema).sort()
|
const keys = Object.keys(schema).sort()
|
||||||
const safeUser = makePropSafe("user")
|
const safeUser = makePropSafe("user")
|
||||||
|
|
||||||
|
@ -728,7 +732,7 @@ const getRoleBindings = () => {
|
||||||
return (get(rolesStore) || []).map(role => {
|
return (get(rolesStore) || []).map(role => {
|
||||||
return {
|
return {
|
||||||
type: "context",
|
type: "context",
|
||||||
runtimeBinding: `trim "${role._id}"`,
|
runtimeBinding: `'${role._id}'`,
|
||||||
readableBinding: `Role.${role.name}`,
|
readableBinding: `Role.${role.name}`,
|
||||||
category: "Role",
|
category: "Role",
|
||||||
icon: "UserGroup",
|
icon: "UserGroup",
|
||||||
|
|
|
@ -70,7 +70,7 @@
|
||||||
<input
|
<input
|
||||||
class="input"
|
class="input"
|
||||||
value={title}
|
value={title}
|
||||||
{title}
|
title={componentName}
|
||||||
placeholder={componentName}
|
placeholder={componentName}
|
||||||
on:keypress={e => {
|
on:keypress={e => {
|
||||||
if (e.key.toLowerCase() === "enter") {
|
if (e.key.toLowerCase() === "enter") {
|
||||||
|
@ -158,7 +158,32 @@
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
|
position: relative;
|
||||||
|
padding: 5px;
|
||||||
|
right: 6px;
|
||||||
|
border: 1px solid transparent;
|
||||||
|
border-radius: 3px;
|
||||||
|
transition: 150ms background-color, 150ms border-color, 150ms color;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.input:hover,
|
||||||
|
.input:focus {
|
||||||
|
cursor: text;
|
||||||
|
background-color: var(
|
||||||
|
--spectrum-textfield-m-background-color,
|
||||||
|
var(--spectrum-global-color-gray-50)
|
||||||
|
);
|
||||||
|
border: 1px solid white;
|
||||||
|
border-color: var(
|
||||||
|
--spectrum-textfield-m-border-color,
|
||||||
|
var(--spectrum-alias-border-color)
|
||||||
|
);
|
||||||
|
color: var(
|
||||||
|
--spectrum-textfield-m-text-color,
|
||||||
|
var(--spectrum-alias-text-color)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
.panel-title-content {
|
.panel-title-content {
|
||||||
display: contents;
|
display: contents;
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,7 +12,7 @@
|
||||||
import { dndzone } from "svelte-dnd-action"
|
import { dndzone } from "svelte-dnd-action"
|
||||||
import { generate } from "shortid"
|
import { generate } from "shortid"
|
||||||
import DrawerBindableInput from "components/common/bindings/DrawerBindableInput.svelte"
|
import DrawerBindableInput from "components/common/bindings/DrawerBindableInput.svelte"
|
||||||
import { LuceneUtils, Constants } from "@budibase/frontend-core"
|
import { QueryUtils, Constants } from "@budibase/frontend-core"
|
||||||
import { selectedComponent, componentStore } from "stores/builder"
|
import { selectedComponent, componentStore } from "stores/builder"
|
||||||
import { getComponentForSetting } from "components/design/settings/componentSettings"
|
import { getComponentForSetting } from "components/design/settings/componentSettings"
|
||||||
import PropertyControl from "components/design/settings/controls/PropertyControl.svelte"
|
import PropertyControl from "components/design/settings/controls/PropertyControl.svelte"
|
||||||
|
@ -119,7 +119,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
const getOperatorOptions = condition => {
|
const getOperatorOptions = condition => {
|
||||||
return LuceneUtils.getValidOperatorsForType({ type: condition.valueType })
|
return QueryUtils.getValidOperatorsForType({ type: condition.valueType })
|
||||||
}
|
}
|
||||||
|
|
||||||
const onOperatorChange = (condition, newOperator) => {
|
const onOperatorChange = (condition, newOperator) => {
|
||||||
|
@ -138,7 +138,7 @@
|
||||||
condition.referenceValue = null
|
condition.referenceValue = null
|
||||||
|
|
||||||
// Ensure a valid operator is set
|
// Ensure a valid operator is set
|
||||||
const validOperators = LuceneUtils.getValidOperatorsForType({
|
const validOperators = QueryUtils.getValidOperatorsForType({
|
||||||
type: newType,
|
type: newType,
|
||||||
}).map(x => x.value)
|
}).map(x => x.value)
|
||||||
if (!validOperators.includes(condition.operator)) {
|
if (!validOperators.includes(condition.operator)) {
|
||||||
|
|
|
@ -59,7 +59,14 @@
|
||||||
// Build up list of illegal children from ancestors
|
// Build up list of illegal children from ancestors
|
||||||
let illegalChildren = definition.illegalChildren || []
|
let illegalChildren = definition.illegalChildren || []
|
||||||
path.forEach(ancestor => {
|
path.forEach(ancestor => {
|
||||||
if (ancestor._component === `@budibase/standard-components/sidepanel`) {
|
// Sidepanels and modals can be nested anywhere in the component tree, but really they are always rendered at the top level.
|
||||||
|
// Because of this, it doesn't make sense to carry over any parent illegal children to them, so the array is reset here.
|
||||||
|
if (
|
||||||
|
[
|
||||||
|
"@budibase/standard-components/sidepanel",
|
||||||
|
"@budibase/standard-components/modal",
|
||||||
|
].includes(ancestor._component)
|
||||||
|
) {
|
||||||
illegalChildren = []
|
illegalChildren = []
|
||||||
}
|
}
|
||||||
const def = componentStore.getDefinition(ancestor._component)
|
const def = componentStore.getDefinition(ancestor._component)
|
||||||
|
|
|
@ -14,7 +14,7 @@
|
||||||
{
|
{
|
||||||
"name": "Layout",
|
"name": "Layout",
|
||||||
"icon": "ClassicGridView",
|
"icon": "ClassicGridView",
|
||||||
"children": ["container", "section", "sidepanel"]
|
"children": ["container", "section", "sidepanel", "modal"]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "Data",
|
"name": "Data",
|
||||||
|
|
|
@ -33,7 +33,8 @@
|
||||||
</Body>
|
</Body>
|
||||||
</Layout>
|
</Layout>
|
||||||
<Button
|
<Button
|
||||||
on:click={() => (window.location = "https://docs.budibase.com")}
|
on:click={() =>
|
||||||
|
(window.location = "https://docs.budibase.com/docs/migrations")}
|
||||||
>Migration guide</Button
|
>Migration guide</Button
|
||||||
>
|
>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
|
@ -60,6 +60,7 @@
|
||||||
userLimitReachedModal
|
userLimitReachedModal
|
||||||
let searchEmail = undefined
|
let searchEmail = undefined
|
||||||
let selectedRows = []
|
let selectedRows = []
|
||||||
|
let selectedInvites = []
|
||||||
let bulkSaveResponse
|
let bulkSaveResponse
|
||||||
let customRenderers = [
|
let customRenderers = [
|
||||||
{ column: "email", component: EmailTableRenderer },
|
{ column: "email", component: EmailTableRenderer },
|
||||||
|
@ -123,7 +124,7 @@
|
||||||
return {}
|
return {}
|
||||||
}
|
}
|
||||||
let pendingSchema = JSON.parse(JSON.stringify(tblSchema))
|
let pendingSchema = JSON.parse(JSON.stringify(tblSchema))
|
||||||
pendingSchema.email.displayName = "Pending Invites"
|
pendingSchema.email.displayName = "Pending Users"
|
||||||
return pendingSchema
|
return pendingSchema
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -132,6 +133,7 @@
|
||||||
const { admin, builder, userGroups, apps } = invite.info
|
const { admin, builder, userGroups, apps } = invite.info
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
_id: invite.code,
|
||||||
email: invite.email,
|
email: invite.email,
|
||||||
builder,
|
builder,
|
||||||
admin,
|
admin,
|
||||||
|
@ -260,9 +262,26 @@
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
await users.bulkDelete(ids)
|
if (ids.length > 0) {
|
||||||
notifications.success(`Successfully deleted ${selectedRows.length} rows`)
|
await users.bulkDelete(ids)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (selectedInvites.length > 0) {
|
||||||
|
await users.removeInvites(
|
||||||
|
selectedInvites.map(invite => ({
|
||||||
|
code: invite._id,
|
||||||
|
}))
|
||||||
|
)
|
||||||
|
pendingInvites = await users.getInvites()
|
||||||
|
}
|
||||||
|
|
||||||
|
notifications.success(
|
||||||
|
`Successfully deleted ${
|
||||||
|
selectedRows.length + selectedInvites.length
|
||||||
|
} users`
|
||||||
|
)
|
||||||
selectedRows = []
|
selectedRows = []
|
||||||
|
selectedInvites = []
|
||||||
await fetch.refresh()
|
await fetch.refresh()
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
notifications.error("Error deleting users")
|
notifications.error("Error deleting users")
|
||||||
|
@ -328,15 +347,15 @@
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
<div class="controls-right">
|
<div class="controls-right">
|
||||||
<Search bind:value={searchEmail} placeholder="Search" />
|
{#if selectedRows.length > 0 || selectedInvites.length > 0}
|
||||||
{#if selectedRows.length > 0}
|
|
||||||
<DeleteRowsButton
|
<DeleteRowsButton
|
||||||
item="user"
|
item="user"
|
||||||
on:updaterows
|
on:updaterows
|
||||||
{selectedRows}
|
selectedRows={[...selectedRows, ...selectedInvites]}
|
||||||
deleteRows={deleteUsers}
|
deleteRows={deleteUsers}
|
||||||
/>
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
|
<Search bind:value={searchEmail} placeholder="Search" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Table
|
<Table
|
||||||
|
@ -362,10 +381,12 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Table
|
<Table
|
||||||
|
bind:selectedRows={selectedInvites}
|
||||||
schema={pendingSchema}
|
schema={pendingSchema}
|
||||||
data={parsedInvites}
|
data={parsedInvites}
|
||||||
allowEditColumns={false}
|
allowEditColumns={false}
|
||||||
allowEditRows={false}
|
allowEditRows={false}
|
||||||
|
allowSelectRows={!readonly}
|
||||||
{customRenderers}
|
{customRenderers}
|
||||||
loading={!invitesLoaded}
|
loading={!invitesLoaded}
|
||||||
allowClickRows={false}
|
allowClickRows={false}
|
||||||
|
|
|
@ -125,7 +125,14 @@ export class ScreenStore extends BudiStore {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (type === "@budibase/standard-components/sidepanel") {
|
// Sidepanels and modals can be nested anywhere in the component tree, but really they are always rendered at the top level.
|
||||||
|
// Because of this, it doesn't make sense to carry over any parent illegal children to them, so the array is reset here.
|
||||||
|
if (
|
||||||
|
[
|
||||||
|
"@budibase/standard-components/sidepanel",
|
||||||
|
"@budibase/standard-components/modal",
|
||||||
|
].includes(type)
|
||||||
|
) {
|
||||||
illegalChildren = []
|
illegalChildren = []
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -138,6 +138,11 @@ export const createLicensingStore = () => {
|
||||||
const isViewPermissionsEnabled = license.features.includes(
|
const isViewPermissionsEnabled = license.features.includes(
|
||||||
Constants.Features.VIEW_PERMISSIONS
|
Constants.Features.VIEW_PERMISSIONS
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const isViewReadonlyColumnsEnabled = license.features.includes(
|
||||||
|
Constants.Features.VIEW_READONLY_COLUMNS
|
||||||
|
)
|
||||||
|
|
||||||
store.update(state => {
|
store.update(state => {
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
|
@ -157,6 +162,7 @@ export const createLicensingStore = () => {
|
||||||
triggerAutomationRunEnabled,
|
triggerAutomationRunEnabled,
|
||||||
isViewPermissionsEnabled,
|
isViewPermissionsEnabled,
|
||||||
perAppBuildersEnabled,
|
perAppBuildersEnabled,
|
||||||
|
isViewReadonlyColumnsEnabled,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|
|
@ -38,6 +38,10 @@ export function createUsersStore() {
|
||||||
return API.inviteUsers(payload)
|
return API.inviteUsers(payload)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function removeInvites(payload) {
|
||||||
|
return API.removeUserInvites(payload)
|
||||||
|
}
|
||||||
|
|
||||||
async function acceptInvite(inviteCode, password, firstName, lastName) {
|
async function acceptInvite(inviteCode, password, firstName, lastName) {
|
||||||
return API.acceptInvite({
|
return API.acceptInvite({
|
||||||
inviteCode,
|
inviteCode,
|
||||||
|
@ -154,6 +158,7 @@ export function createUsersStore() {
|
||||||
onboard,
|
onboard,
|
||||||
fetchInvite,
|
fetchInvite,
|
||||||
getInvites,
|
getInvites,
|
||||||
|
removeInvites,
|
||||||
updateInvite,
|
updateInvite,
|
||||||
getUserCountByApp,
|
getUserCountByApp,
|
||||||
addAppBuilder,
|
addAppBuilder,
|
||||||
|
|
|
@ -11,7 +11,7 @@
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"tsc": "node ../../scripts/build.js",
|
"tsc": "node ../../scripts/build.js",
|
||||||
"build": "yarn tsc",
|
"build": "yarn tsc",
|
||||||
"check:types": "tsc -p tsconfig.json --noEmit --paths null",
|
"check:types": "tsc -p tsconfig.json --noEmit --paths null --target es2020",
|
||||||
"start": "ts-node ./src/index.ts"
|
"start": "ts-node ./src/index.ts"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
@ -32,7 +32,7 @@
|
||||||
"pouchdb": "7.3.0",
|
"pouchdb": "7.3.0",
|
||||||
"pouchdb-replication-stream": "1.2.9",
|
"pouchdb-replication-stream": "1.2.9",
|
||||||
"randomstring": "1.1.5",
|
"randomstring": "1.1.5",
|
||||||
"tar": "6.1.15",
|
"tar": "6.2.1",
|
||||||
"yaml": "^2.1.1"
|
"yaml": "^2.1.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
@ -40,6 +40,6 @@
|
||||||
"@types/node-fetch": "2.6.4",
|
"@types/node-fetch": "2.6.4",
|
||||||
"@types/pouchdb": "^6.4.0",
|
"@types/pouchdb": "^6.4.0",
|
||||||
"ts-node": "10.8.1",
|
"ts-node": "10.8.1",
|
||||||
"typescript": "5.2.2"
|
"typescript": "5.5.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,6 +11,7 @@
|
||||||
"continueIfAction": true,
|
"continueIfAction": true,
|
||||||
"showNotificationAction": true,
|
"showNotificationAction": true,
|
||||||
"sidePanel": true,
|
"sidePanel": true,
|
||||||
|
"modal": true,
|
||||||
"skeletonLoader": true
|
"skeletonLoader": true
|
||||||
},
|
},
|
||||||
"typeSupportPresets": {
|
"typeSupportPresets": {
|
||||||
|
@ -22,17 +23,21 @@
|
||||||
{ "type": "bigint", "message": "stringAsNumber" },
|
{ "type": "bigint", "message": "stringAsNumber" },
|
||||||
{ "type": "options", "message": "stringAsNumber" },
|
{ "type": "options", "message": "stringAsNumber" },
|
||||||
{ "type": "formula", "message": "stringAsNumber" },
|
{ "type": "formula", "message": "stringAsNumber" },
|
||||||
{ "type": "datetime", "message": "dateAsNumber"}
|
{ "type": "datetime", "message": "dateAsNumber" }
|
||||||
],
|
],
|
||||||
"unsupported": [
|
"unsupported": [{ "type": "json", "message": "jsonPrimitivesOnly" }]
|
||||||
{ "type": "json", "message": "jsonPrimitivesOnly" }
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
"stringLike": {
|
"stringLike": {
|
||||||
"supported": ["string", "number", "bigint", "options", "longform", "boolean", "datetime"],
|
"supported": [
|
||||||
"unsupported": [
|
"string",
|
||||||
{ "type": "json", "message": "jsonPrimitivesOnly" }
|
"number",
|
||||||
]
|
"bigint",
|
||||||
|
"options",
|
||||||
|
"longform",
|
||||||
|
"boolean",
|
||||||
|
"datetime"
|
||||||
|
],
|
||||||
|
"unsupported": [{ "type": "json", "message": "jsonPrimitivesOnly" }]
|
||||||
},
|
},
|
||||||
"datetimeLike": {
|
"datetimeLike": {
|
||||||
"supported": ["datetime"],
|
"supported": ["datetime"],
|
||||||
|
@ -42,11 +47,9 @@
|
||||||
{ "type": "options", "message": "stringAsDate" },
|
{ "type": "options", "message": "stringAsDate" },
|
||||||
{ "type": "formula", "message": "stringAsDate" },
|
{ "type": "formula", "message": "stringAsDate" },
|
||||||
{ "type": "bigint", "message": "stringAsDate" },
|
{ "type": "bigint", "message": "stringAsDate" },
|
||||||
{ "type": "number", "message": "numberAsDate"}
|
{ "type": "number", "message": "numberAsDate" }
|
||||||
],
|
],
|
||||||
"unsupported": [
|
"unsupported": [{ "type": "json", "message": "jsonPrimitivesOnly" }]
|
||||||
{ "type": "json", "message": "jsonPrimitivesOnly" }
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"layout": {
|
"layout": {
|
||||||
|
@ -5223,6 +5226,7 @@
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"chartblock": {
|
"chartblock": {
|
||||||
|
"documentationLink": "https://docs.budibase.com/docs/chart",
|
||||||
"block": true,
|
"block": true,
|
||||||
"name": "Chart Block",
|
"name": "Chart Block",
|
||||||
"icon": "GraphPie",
|
"icon": "GraphPie",
|
||||||
|
@ -6974,7 +6978,7 @@
|
||||||
"name": "Side Panel",
|
"name": "Side Panel",
|
||||||
"icon": "RailRight",
|
"icon": "RailRight",
|
||||||
"hasChildren": true,
|
"hasChildren": true,
|
||||||
"illegalChildren": ["section", "sidepanel"],
|
"illegalChildren": ["section", "sidepanel", "modal"],
|
||||||
"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.",
|
||||||
|
@ -6992,6 +6996,52 @@
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"modal": {
|
||||||
|
"name": "Modal",
|
||||||
|
"icon": "MBox",
|
||||||
|
"hasChildren": true,
|
||||||
|
"illegalChildren": ["section", "modal", "sidepanel"],
|
||||||
|
"showEmptyState": false,
|
||||||
|
"draggable": false,
|
||||||
|
"info": "Modals are hidden by default. They will only be revealed when triggered by the 'Open Modal' action.",
|
||||||
|
"settings": [
|
||||||
|
{
|
||||||
|
"type": "boolean",
|
||||||
|
"key": "ignoreClicksOutside",
|
||||||
|
"label": "Ignore clicks outside",
|
||||||
|
"defaultValue": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "event",
|
||||||
|
"key": "onClose",
|
||||||
|
"label": "On close"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "select",
|
||||||
|
"label": "Size",
|
||||||
|
"key": "size",
|
||||||
|
"defaultValue": "small",
|
||||||
|
"options": [
|
||||||
|
{
|
||||||
|
"label": "Small",
|
||||||
|
"value": "small"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "Medium",
|
||||||
|
"value": "medium"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "Large",
|
||||||
|
"value": "large"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "Fullscreen",
|
||||||
|
"value": "fullscreen"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
"rowexplorer": {
|
"rowexplorer": {
|
||||||
"block": true,
|
"block": true,
|
||||||
"name": "Row Explorer Block",
|
"name": "Row Explorer Block",
|
||||||
|
|
|
@ -19,6 +19,8 @@
|
||||||
devToolsStore,
|
devToolsStore,
|
||||||
devToolsEnabled,
|
devToolsEnabled,
|
||||||
environmentStore,
|
environmentStore,
|
||||||
|
sidePanelStore,
|
||||||
|
modalStore,
|
||||||
} from "stores"
|
} from "stores"
|
||||||
import NotificationDisplay from "components/overlay/NotificationDisplay.svelte"
|
import NotificationDisplay from "components/overlay/NotificationDisplay.svelte"
|
||||||
import ConfirmationDisplay from "components/overlay/ConfirmationDisplay.svelte"
|
import ConfirmationDisplay from "components/overlay/ConfirmationDisplay.svelte"
|
||||||
|
@ -102,6 +104,21 @@
|
||||||
embedded: !!$appStore.embedded,
|
embedded: !!$appStore.embedded,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
const handleHashChange = () => {
|
||||||
|
const { open: sidePanelOpen } = $sidePanelStore
|
||||||
|
if (sidePanelOpen) {
|
||||||
|
sidePanelStore.actions.close()
|
||||||
|
}
|
||||||
|
|
||||||
|
const { open: modalOpen } = $modalStore
|
||||||
|
if (modalOpen) {
|
||||||
|
modalStore.actions.close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
window.addEventListener("hashchange", handleHashChange)
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener("hashchange", handleHashChange)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
$: {
|
$: {
|
||||||
|
|
|
@ -5,8 +5,6 @@
|
||||||
const { styleable, builderStore } = getContext("sdk")
|
const { styleable, builderStore } = getContext("sdk")
|
||||||
const component = getContext("component")
|
const component = getContext("component")
|
||||||
|
|
||||||
let handlingOnClick = false
|
|
||||||
|
|
||||||
export let disabled = false
|
export let disabled = false
|
||||||
export let text = ""
|
export let text = ""
|
||||||
export let onClick
|
export let onClick
|
||||||
|
@ -19,17 +17,9 @@
|
||||||
// For internal use only for now - not defined in the manifest
|
// For internal use only for now - not defined in the manifest
|
||||||
export let active = false
|
export let active = false
|
||||||
|
|
||||||
const handleOnClick = async () => {
|
|
||||||
handlingOnClick = true
|
|
||||||
|
|
||||||
if (onClick) {
|
|
||||||
await onClick()
|
|
||||||
}
|
|
||||||
|
|
||||||
handlingOnClick = false
|
|
||||||
}
|
|
||||||
|
|
||||||
let node
|
let node
|
||||||
|
let touched = false
|
||||||
|
let handlingOnClick = false
|
||||||
|
|
||||||
$: $component.editing && node?.focus()
|
$: $component.editing && node?.focus()
|
||||||
$: componentText = getComponentText(text, $builderStore, $component)
|
$: componentText = getComponentText(text, $builderStore, $component)
|
||||||
|
@ -42,7 +32,18 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
const updateText = e => {
|
const updateText = e => {
|
||||||
builderStore.actions.updateProp("text", e.target.textContent)
|
if (touched) {
|
||||||
|
builderStore.actions.updateProp("text", e.target.textContent)
|
||||||
|
}
|
||||||
|
touched = false
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleOnClick = async () => {
|
||||||
|
handlingOnClick = true
|
||||||
|
if (onClick) {
|
||||||
|
await onClick()
|
||||||
|
}
|
||||||
|
handlingOnClick = false
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
@ -57,6 +58,7 @@
|
||||||
on:blur={$component.editing ? updateText : null}
|
on:blur={$component.editing ? updateText : null}
|
||||||
bind:this={node}
|
bind:this={node}
|
||||||
class:active
|
class:active
|
||||||
|
on:input={() => (touched = true)}
|
||||||
>
|
>
|
||||||
{#if icon}
|
{#if icon}
|
||||||
<i class="{icon} {size}" />
|
<i class="{icon} {size}" />
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
<script>
|
<script>
|
||||||
import { getContext } from "svelte"
|
import { getContext } from "svelte"
|
||||||
import { Pagination, ProgressCircle } from "@budibase/bbui"
|
import { Pagination, ProgressCircle } from "@budibase/bbui"
|
||||||
import { fetchData, LuceneUtils } from "@budibase/frontend-core"
|
import { fetchData, QueryUtils } from "@budibase/frontend-core"
|
||||||
|
|
||||||
export let dataSource
|
export let dataSource
|
||||||
export let filter
|
export let filter
|
||||||
|
@ -19,7 +19,7 @@
|
||||||
|
|
||||||
// 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
|
||||||
$: defaultQuery = LuceneUtils.buildLuceneQuery(filter)
|
$: defaultQuery = QueryUtils.buildQuery(filter)
|
||||||
$: query = extendQuery(defaultQuery, queryExtensions)
|
$: query = extendQuery(defaultQuery, queryExtensions)
|
||||||
$: fetch = createFetch(dataSource)
|
$: fetch = createFetch(dataSource)
|
||||||
$: fetch.update({
|
$: fetch.update({
|
||||||
|
|
|
@ -90,9 +90,11 @@
|
||||||
columns.forEach((column, idx) => {
|
columns.forEach((column, idx) => {
|
||||||
overrides[column.field] = {
|
overrides[column.field] = {
|
||||||
displayName: column.label,
|
displayName: column.label,
|
||||||
width: column.width,
|
|
||||||
order: idx,
|
order: idx,
|
||||||
}
|
}
|
||||||
|
if (column.width) {
|
||||||
|
overrides[column.field].width = column.width
|
||||||
|
}
|
||||||
})
|
})
|
||||||
return overrides
|
return overrides
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,6 +14,7 @@
|
||||||
export let size
|
export let size
|
||||||
|
|
||||||
let node
|
let node
|
||||||
|
let touched = false
|
||||||
|
|
||||||
$: $component.editing && node?.focus()
|
$: $component.editing && node?.focus()
|
||||||
$: placeholder = $builderStore.inBuilder && !text && !$component.editing
|
$: placeholder = $builderStore.inBuilder && !text && !$component.editing
|
||||||
|
@ -47,7 +48,10 @@
|
||||||
|
|
||||||
// Convert contenteditable HTML to text and save
|
// Convert contenteditable HTML to text and save
|
||||||
const updateText = e => {
|
const updateText = e => {
|
||||||
builderStore.actions.updateProp("text", e.target.textContent)
|
if (touched) {
|
||||||
|
builderStore.actions.updateProp("text", e.target.textContent)
|
||||||
|
}
|
||||||
|
touched = false
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
@ -62,6 +66,7 @@
|
||||||
class:underline
|
class:underline
|
||||||
class="spectrum-Heading {sizeClass} {alignClass}"
|
class="spectrum-Heading {sizeClass} {alignClass}"
|
||||||
on:blur={$component.editing ? updateText : null}
|
on:blur={$component.editing ? updateText : null}
|
||||||
|
on:input={() => (touched = true)}
|
||||||
>
|
>
|
||||||
{componentText}
|
{componentText}
|
||||||
</h1>
|
</h1>
|
||||||
|
|
|
@ -12,6 +12,7 @@
|
||||||
linkable,
|
linkable,
|
||||||
builderStore,
|
builderStore,
|
||||||
sidePanelStore,
|
sidePanelStore,
|
||||||
|
modalStore,
|
||||||
appStore,
|
appStore,
|
||||||
} = sdk
|
} = sdk
|
||||||
const context = getContext("context")
|
const context = getContext("context")
|
||||||
|
@ -77,6 +78,7 @@
|
||||||
!$builderStore.inBuilder &&
|
!$builderStore.inBuilder &&
|
||||||
$sidePanelStore.open &&
|
$sidePanelStore.open &&
|
||||||
!$sidePanelStore.ignoreClicksOutside
|
!$sidePanelStore.ignoreClicksOutside
|
||||||
|
|
||||||
$: screenId = $builderStore.inBuilder
|
$: screenId = $builderStore.inBuilder
|
||||||
? `${$builderStore.screen?._id}-screen`
|
? `${$builderStore.screen?._id}-screen`
|
||||||
: "screen"
|
: "screen"
|
||||||
|
@ -198,6 +200,7 @@
|
||||||
const handleClickLink = () => {
|
const handleClickLink = () => {
|
||||||
mobileOpen = false
|
mobileOpen = false
|
||||||
sidePanelStore.actions.close()
|
sidePanelStore.actions.close()
|
||||||
|
modalStore.actions.close()
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
<script>
|
<script>
|
||||||
import { getContext } from "svelte"
|
import { getContext } from "svelte"
|
||||||
|
|
||||||
const { linkable, styleable, builderStore, sidePanelStore } =
|
const { linkable, styleable, builderStore, sidePanelStore, modalStore } =
|
||||||
getContext("sdk")
|
getContext("sdk")
|
||||||
const component = getContext("component")
|
const component = getContext("component")
|
||||||
|
|
||||||
|
@ -16,6 +16,7 @@
|
||||||
export let size
|
export let size
|
||||||
|
|
||||||
let node
|
let node
|
||||||
|
let touched = false
|
||||||
|
|
||||||
$: $component.editing && node?.focus()
|
$: $component.editing && node?.focus()
|
||||||
$: externalLink = url && typeof url === "string" && !url.startsWith("/")
|
$: externalLink = url && typeof url === "string" && !url.startsWith("/")
|
||||||
|
@ -28,6 +29,11 @@
|
||||||
// overrides the color when it's passed as inline style.
|
// overrides the color when it's passed as inline style.
|
||||||
$: styles = enrichStyles($component.styles, color)
|
$: styles = enrichStyles($component.styles, color)
|
||||||
|
|
||||||
|
const handleUrlChange = () => {
|
||||||
|
sidePanelStore.actions.close()
|
||||||
|
modalStore.actions.close()
|
||||||
|
}
|
||||||
|
|
||||||
const getSanitizedUrl = (url, externalLink, newTab) => {
|
const getSanitizedUrl = (url, externalLink, newTab) => {
|
||||||
if (!url) {
|
if (!url) {
|
||||||
return externalLink || newTab ? "#/" : "/"
|
return externalLink || newTab ? "#/" : "/"
|
||||||
|
@ -62,7 +68,10 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
const updateText = e => {
|
const updateText = e => {
|
||||||
builderStore.actions.updateProp("text", e.target.textContent)
|
if (touched) {
|
||||||
|
builderStore.actions.updateProp("text", e.target.textContent)
|
||||||
|
}
|
||||||
|
touched = false
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
@ -76,6 +85,7 @@
|
||||||
class:underline
|
class:underline
|
||||||
class="align--{align || 'left'} size--{size || 'M'}"
|
class="align--{align || 'left'} size--{size || 'M'}"
|
||||||
on:blur={$component.editing ? updateText : null}
|
on:blur={$component.editing ? updateText : null}
|
||||||
|
on:input={() => (touched = true)}
|
||||||
>
|
>
|
||||||
{componentText}
|
{componentText}
|
||||||
</div>
|
</div>
|
||||||
|
@ -104,7 +114,7 @@
|
||||||
class:italic
|
class:italic
|
||||||
class:underline
|
class:underline
|
||||||
class="align--{align || 'left'} size--{size || 'M'}"
|
class="align--{align || 'left'} size--{size || 'M'}"
|
||||||
on:click={sidePanelStore.actions.close}
|
on:click={handleUrlChange}
|
||||||
>
|
>
|
||||||
{componentText}
|
{componentText}
|
||||||
</a>
|
</a>
|
||||||
|
|
|
@ -0,0 +1,141 @@
|
||||||
|
<script>
|
||||||
|
import { getContext } from "svelte"
|
||||||
|
import { Modal, Icon } from "@budibase/bbui"
|
||||||
|
|
||||||
|
const component = getContext("component")
|
||||||
|
const { styleable, modalStore, builderStore, dndIsDragging } =
|
||||||
|
getContext("sdk")
|
||||||
|
|
||||||
|
export let onClose
|
||||||
|
export let ignoreClicksOutside
|
||||||
|
export let size
|
||||||
|
let modal
|
||||||
|
|
||||||
|
// Open modal automatically in builder
|
||||||
|
$: {
|
||||||
|
if ($builderStore.inBuilder) {
|
||||||
|
if (
|
||||||
|
$component.inSelectedPath &&
|
||||||
|
$modalStore.contentId !== $component.id
|
||||||
|
) {
|
||||||
|
modalStore.actions.open($component.id)
|
||||||
|
} else if (
|
||||||
|
!$component.inSelectedPath &&
|
||||||
|
$modalStore.contentId === $component.id &&
|
||||||
|
!$dndIsDragging
|
||||||
|
) {
|
||||||
|
modalStore.actions.close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$: open = $modalStore.contentId === $component.id
|
||||||
|
|
||||||
|
const handleModalClose = async () => {
|
||||||
|
if (onClose) {
|
||||||
|
await onClose()
|
||||||
|
}
|
||||||
|
modalStore.actions.close()
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleOpen = (open, modal) => {
|
||||||
|
if (!modal) return
|
||||||
|
|
||||||
|
if (open) {
|
||||||
|
modal.show()
|
||||||
|
} else {
|
||||||
|
modal.hide()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$: handleOpen(open, modal)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<!-- Conditional displaying in the builder is necessary otherwise previews don't update properly upon component deletion -->
|
||||||
|
{#if !$builderStore.inBuilder || open}
|
||||||
|
<Modal
|
||||||
|
on:cancel={handleModalClose}
|
||||||
|
bind:this={modal}
|
||||||
|
disableCancel={$builderStore.inBuilder}
|
||||||
|
zIndex={2}
|
||||||
|
>
|
||||||
|
<div use:styleable={$component.styles} class={`modal-content ${size}`}>
|
||||||
|
<div class="modal-header">
|
||||||
|
<Icon
|
||||||
|
color="var(--spectrum-global-color-gray-800)"
|
||||||
|
name="Close"
|
||||||
|
hoverable
|
||||||
|
on:click={handleModalClose}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="modal-main">
|
||||||
|
<div class="modal-main-inner">
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.modal-content {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
max-width: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
|
padding: 12px 0px 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.small {
|
||||||
|
width: 400px;
|
||||||
|
min-height: 200px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.medium {
|
||||||
|
width: 600px;
|
||||||
|
min-height: 400px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.large {
|
||||||
|
width: 800px;
|
||||||
|
min-height: 600px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fullscreen {
|
||||||
|
width: calc(100vw - 80px);
|
||||||
|
min-height: calc(100vh - 80px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-header {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: flex-end;
|
||||||
|
flex-shrink: 0;
|
||||||
|
flex-grow: 0;
|
||||||
|
padding: 0 12px 12px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-main {
|
||||||
|
padding: 0 40px;
|
||||||
|
flex-grow: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-main :global(.component > *) {
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-main-inner {
|
||||||
|
flex-grow: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
word-break: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-main-inner:empty {
|
||||||
|
border-radius: 3px;
|
||||||
|
border: 2px dashed var(--spectrum-global-color-gray-400);
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -29,10 +29,6 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// $: {
|
|
||||||
|
|
||||||
// }
|
|
||||||
|
|
||||||
// Derive visibility
|
// Derive visibility
|
||||||
$: open = $sidePanelStore.contentId === $component.id
|
$: open = $sidePanelStore.contentId === $component.id
|
||||||
|
|
||||||
|
|
|
@ -13,6 +13,7 @@
|
||||||
export let size
|
export let size
|
||||||
|
|
||||||
let node
|
let node
|
||||||
|
let touched = false
|
||||||
|
|
||||||
$: $component.editing && node?.focus()
|
$: $component.editing && node?.focus()
|
||||||
$: placeholder = $builderStore.inBuilder && !text && !$component.editing
|
$: placeholder = $builderStore.inBuilder && !text && !$component.editing
|
||||||
|
@ -46,7 +47,10 @@
|
||||||
|
|
||||||
// Convert contenteditable HTML to text and save
|
// Convert contenteditable HTML to text and save
|
||||||
const updateText = e => {
|
const updateText = e => {
|
||||||
builderStore.actions.updateProp("text", e.target.textContent)
|
if (touched) {
|
||||||
|
builderStore.actions.updateProp("text", e.target.textContent)
|
||||||
|
}
|
||||||
|
touched = false
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
@ -61,6 +65,7 @@
|
||||||
class:underline
|
class:underline
|
||||||
class="spectrum-Body {sizeClass} {alignClass}"
|
class="spectrum-Body {sizeClass} {alignClass}"
|
||||||
on:blur={$component.editing ? updateText : null}
|
on:blur={$component.editing ? updateText : null}
|
||||||
|
on:input={() => (touched = true)}
|
||||||
>
|
>
|
||||||
{componentText}
|
{componentText}
|
||||||
</p>
|
</p>
|
||||||
|
|
|
@ -35,6 +35,7 @@
|
||||||
export let valueUnits
|
export let valueUnits
|
||||||
export let yAxisLabel
|
export let yAxisLabel
|
||||||
export let xAxisLabel
|
export let xAxisLabel
|
||||||
|
export let yAxisUnits
|
||||||
export let curve
|
export let curve
|
||||||
|
|
||||||
// Area
|
// Area
|
||||||
|
@ -85,6 +86,7 @@
|
||||||
valueUnits,
|
valueUnits,
|
||||||
yAxisLabel,
|
yAxisLabel,
|
||||||
xAxisLabel,
|
xAxisLabel,
|
||||||
|
yAxisUnits,
|
||||||
stacked,
|
stacked,
|
||||||
horizontal,
|
horizontal,
|
||||||
curve,
|
curve,
|
||||||
|
|
|
@ -68,6 +68,15 @@
|
||||||
maximum: schema?.constraints?.length?.maximum,
|
maximum: schema?.constraints?.length?.maximum,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
[FieldType.DATETIME]: (_field, schema) => {
|
||||||
|
const props = {
|
||||||
|
valueAsTimestamp: !schema?.timeOnly,
|
||||||
|
}
|
||||||
|
if (schema?.dateOnly) {
|
||||||
|
props.enableTime = false
|
||||||
|
}
|
||||||
|
return props
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
const fieldSchema = getFieldSchema(field)
|
const fieldSchema = getFieldSchema(field)
|
||||||
|
|
|
@ -31,41 +31,23 @@
|
||||||
|
|
||||||
let schema
|
let schema
|
||||||
|
|
||||||
$: formattedFields = convertOldFieldFormat(fields)
|
|
||||||
$: fieldsOrDefault = getDefaultFields(formattedFields, schema)
|
|
||||||
$: fetchSchema(dataSource)
|
$: fetchSchema(dataSource)
|
||||||
$: id = $component.id
|
$: id = $component.id
|
||||||
// We could simply spread $$props into the inner form and append our
|
$: formattedFields = convertOldFieldFormat(fields)
|
||||||
// additions, but that would create svelte warnings about unused props and
|
$: fieldsOrDefault = getDefaultFields(formattedFields, schema)
|
||||||
// make maintenance in future more confusing as we typically always have a
|
$: buttonsOrDefault =
|
||||||
// proper mapping of schema settings to component exports, without having to
|
buttons ||
|
||||||
// search multiple files
|
Utils.buildFormBlockButtonConfig({
|
||||||
$: innerProps = {
|
_id: id,
|
||||||
dataSource,
|
showDeleteButton,
|
||||||
actionUrl,
|
showSaveButton,
|
||||||
actionType,
|
saveButtonLabel,
|
||||||
size,
|
deleteButtonLabel,
|
||||||
disabled,
|
notificationOverride,
|
||||||
fields: fieldsOrDefault,
|
actionType,
|
||||||
title,
|
actionUrl,
|
||||||
description,
|
dataSource,
|
||||||
schema,
|
})
|
||||||
notificationOverride,
|
|
||||||
buttons:
|
|
||||||
buttons ||
|
|
||||||
Utils.buildFormBlockButtonConfig({
|
|
||||||
_id: id,
|
|
||||||
showDeleteButton,
|
|
||||||
showSaveButton,
|
|
||||||
saveButtonLabel,
|
|
||||||
deleteButtonLabel,
|
|
||||||
notificationOverride,
|
|
||||||
actionType,
|
|
||||||
actionUrl,
|
|
||||||
dataSource,
|
|
||||||
}),
|
|
||||||
buttonPosition: buttons ? buttonPosition : "top",
|
|
||||||
}
|
|
||||||
|
|
||||||
// Provide additional data context for live binding eval
|
// Provide additional data context for live binding eval
|
||||||
export const getAdditionalDataContext = () => {
|
export const getAdditionalDataContext = () => {
|
||||||
|
@ -123,5 +105,18 @@
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<FormBlockWrapper {actionType} {dataSource} {rowId} {noRowsMessage}>
|
<FormBlockWrapper {actionType} {dataSource} {rowId} {noRowsMessage}>
|
||||||
<InnerFormBlock {...innerProps} />
|
<InnerFormBlock
|
||||||
|
{dataSource}
|
||||||
|
{actionUrl}
|
||||||
|
{actionType}
|
||||||
|
{size}
|
||||||
|
{disabled}
|
||||||
|
fields={fieldsOrDefault}
|
||||||
|
{title}
|
||||||
|
{description}
|
||||||
|
{schema}
|
||||||
|
{notificationOverride}
|
||||||
|
buttons={buttonsOrDefault}
|
||||||
|
buttonPosition={buttons ? buttonPosition : "top"}
|
||||||
|
/>
|
||||||
</FormBlockWrapper>
|
</FormBlockWrapper>
|
||||||
|
|
|
@ -91,15 +91,13 @@
|
||||||
{#if description}
|
{#if description}
|
||||||
<BlockComponent type="text" props={{ text: description }} order={1} />
|
<BlockComponent type="text" props={{ text: description }} order={1} />
|
||||||
{/if}
|
{/if}
|
||||||
{#key fields}
|
<BlockComponent type="container">
|
||||||
<BlockComponent type="container">
|
<div class="form-block fields" class:mobile={$context.device.mobile}>
|
||||||
<div class="form-block fields" class:mobile={$context.device.mobile}>
|
{#each fields as field, idx}
|
||||||
{#each fields as field, idx}
|
<FormBlockComponent {field} {schema} order={idx} />
|
||||||
<FormBlockComponent {field} {schema} order={idx} />
|
{/each}
|
||||||
{/each}
|
</div>
|
||||||
</div>
|
</BlockComponent>
|
||||||
</BlockComponent>
|
|
||||||
{/key}
|
|
||||||
</BlockComponent>
|
</BlockComponent>
|
||||||
{#if buttonPosition === "bottom"}
|
{#if buttonPosition === "bottom"}
|
||||||
<BlockComponent
|
<BlockComponent
|
||||||
|
|
|
@ -74,7 +74,6 @@
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
xaxis: {
|
xaxis: {
|
||||||
type: labelType,
|
|
||||||
categories,
|
categories,
|
||||||
labels: {
|
labels: {
|
||||||
formatter: xAxisFormatter,
|
formatter: xAxisFormatter,
|
||||||
|
|
|
@ -72,7 +72,6 @@
|
||||||
},
|
},
|
||||||
// We can just always provide the categories to the xaxis and horizontal mode automatically handles "tranposing" the categories to the yaxis, but certain things like labels need to be manually put on a certain axis based on the selected mode. Titles do not need to be handled this way, they are exposed to the user as "X axis" and Y Axis" so flipping them would be confusing.
|
// We can just always provide the categories to the xaxis and horizontal mode automatically handles "tranposing" the categories to the yaxis, but certain things like labels need to be manually put on a certain axis based on the selected mode. Titles do not need to be handled this way, they are exposed to the user as "X axis" and Y Axis" so flipping them would be confusing.
|
||||||
xaxis: {
|
xaxis: {
|
||||||
type: labelType,
|
|
||||||
categories,
|
categories,
|
||||||
labels: {
|
labels: {
|
||||||
formatter: xAxisFormatter,
|
formatter: xAxisFormatter,
|
||||||
|
|
|
@ -66,7 +66,6 @@
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
xaxis: {
|
xaxis: {
|
||||||
type: labelType,
|
|
||||||
categories,
|
categories,
|
||||||
labels: {
|
labels: {
|
||||||
formatter: xAxisFormatter,
|
formatter: xAxisFormatter,
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
import { getContext, onDestroy } from "svelte"
|
import { getContext, onDestroy } from "svelte"
|
||||||
import { ModalContent, Modal } from "@budibase/bbui"
|
import { ModalContent, Modal } from "@budibase/bbui"
|
||||||
import FilterModal from "./FilterModal.svelte"
|
import FilterModal from "./FilterModal.svelte"
|
||||||
import { LuceneUtils } from "@budibase/frontend-core"
|
import { QueryUtils } from "@budibase/frontend-core"
|
||||||
import Button from "../Button.svelte"
|
import Button from "../Button.svelte"
|
||||||
|
|
||||||
export let dataProvider
|
export let dataProvider
|
||||||
|
@ -36,7 +36,7 @@
|
||||||
// Add query extension to data provider
|
// Add query extension to data provider
|
||||||
$: {
|
$: {
|
||||||
if (filters?.length) {
|
if (filters?.length) {
|
||||||
const queryExtension = LuceneUtils.buildLuceneQuery(filters)
|
const queryExtension = QueryUtils.buildQuery(filters)
|
||||||
addExtension?.($component.id, queryExtension)
|
addExtension?.($component.id, queryExtension)
|
||||||
} else {
|
} else {
|
||||||
removeExtension?.($component.id)
|
removeExtension?.($component.id)
|
||||||
|
|
|
@ -16,15 +16,37 @@
|
||||||
export let onChange
|
export let onChange
|
||||||
export let span
|
export let span
|
||||||
export let helpText = null
|
export let helpText = null
|
||||||
|
export let valueAsTimestamp = false
|
||||||
|
|
||||||
let fieldState
|
let fieldState
|
||||||
let fieldApi
|
let fieldApi
|
||||||
|
|
||||||
const handleChange = e => {
|
const handleChange = e => {
|
||||||
const changed = fieldApi.setValue(e.detail)
|
let value = e.detail
|
||||||
if (onChange && changed) {
|
if (timeOnly && valueAsTimestamp) {
|
||||||
onChange({ value: e.detail })
|
if (!isValidDate(value)) {
|
||||||
|
// Handle time only fields that are timestamps under the hood
|
||||||
|
value = timeToDateISOString(value)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const changed = fieldApi.setValue(value)
|
||||||
|
if (onChange && changed) {
|
||||||
|
onChange({ value })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const isValidDate = value => !isNaN(new Date(value))
|
||||||
|
|
||||||
|
const timeToDateISOString = value => {
|
||||||
|
let [hours, minutes] = value.split(":").map(Number)
|
||||||
|
|
||||||
|
const date = new Date()
|
||||||
|
date.setHours(hours)
|
||||||
|
date.setMinutes(minutes)
|
||||||
|
date.setSeconds(0)
|
||||||
|
date.setMilliseconds(0)
|
||||||
|
return date.toISOString()
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
|
@ -26,6 +26,10 @@
|
||||||
// Register field with form
|
// Register field with form
|
||||||
const formApi = formContext?.formApi
|
const formApi = formContext?.formApi
|
||||||
const labelPos = fieldGroupContext?.labelPosition || "above"
|
const labelPos = fieldGroupContext?.labelPosition || "above"
|
||||||
|
|
||||||
|
let touched = false
|
||||||
|
let labelNode
|
||||||
|
|
||||||
$: formStep = formStepContext ? $formStepContext || 1 : 1
|
$: formStep = formStepContext ? $formStepContext || 1 : 1
|
||||||
$: formField = formApi?.registerField(
|
$: formField = formApi?.registerField(
|
||||||
field,
|
field,
|
||||||
|
@ -36,14 +40,12 @@
|
||||||
validation,
|
validation,
|
||||||
formStep
|
formStep
|
||||||
)
|
)
|
||||||
|
|
||||||
$: schemaType =
|
$: schemaType =
|
||||||
fieldSchema?.type !== "formula" && fieldSchema?.type !== "bigint"
|
fieldSchema?.type !== "formula" && fieldSchema?.type !== "bigint"
|
||||||
? fieldSchema?.type
|
? fieldSchema?.type
|
||||||
: "string"
|
: "string"
|
||||||
|
|
||||||
// Focus label when editing
|
// Focus label when editing
|
||||||
let labelNode
|
|
||||||
$: $component.editing && labelNode?.focus()
|
$: $component.editing && labelNode?.focus()
|
||||||
|
|
||||||
// Update form properties in parent component on every store change
|
// Update form properties in parent component on every store change
|
||||||
|
@ -57,7 +59,10 @@
|
||||||
$: labelClass = labelPos === "above" ? "" : `spectrum-FieldLabel--${labelPos}`
|
$: labelClass = labelPos === "above" ? "" : `spectrum-FieldLabel--${labelPos}`
|
||||||
|
|
||||||
const updateLabel = e => {
|
const updateLabel = e => {
|
||||||
builderStore.actions.updateProp("label", e.target.textContent)
|
if (touched) {
|
||||||
|
builderStore.actions.updateProp("label", e.target.textContent)
|
||||||
|
}
|
||||||
|
touched = false
|
||||||
}
|
}
|
||||||
|
|
||||||
onDestroy(() => {
|
onDestroy(() => {
|
||||||
|
@ -79,6 +84,7 @@
|
||||||
bind:this={labelNode}
|
bind:this={labelNode}
|
||||||
contenteditable={$component.editing}
|
contenteditable={$component.editing}
|
||||||
on:blur={$component.editing ? updateLabel : null}
|
on:blur={$component.editing ? updateLabel : null}
|
||||||
|
on:input={() => (touched = true)}
|
||||||
class:hidden={!label}
|
class:hidden={!label}
|
||||||
class:readonly
|
class:readonly
|
||||||
for={fieldState?.fieldId}
|
for={fieldState?.fieldId}
|
||||||
|
|
|
@ -206,7 +206,7 @@
|
||||||
error: initialError,
|
error: initialError,
|
||||||
disabled:
|
disabled:
|
||||||
disabled || fieldDisabled || (isAutoColumn && !editAutoColumns),
|
disabled || fieldDisabled || (isAutoColumn && !editAutoColumns),
|
||||||
readonly: readonly || fieldReadOnly,
|
readonly: readonly || fieldReadOnly || schema?.[field]?.readonly,
|
||||||
defaultValue,
|
defaultValue,
|
||||||
validator,
|
validator,
|
||||||
lastUpdate: Date.now(),
|
lastUpdate: Date.now(),
|
||||||
|
|
|
@ -37,6 +37,7 @@ export { default as markdownviewer } from "./MarkdownViewer.svelte"
|
||||||
export { default as embeddedmap } from "./embedded-map/EmbeddedMap.svelte"
|
export { default as embeddedmap } from "./embedded-map/EmbeddedMap.svelte"
|
||||||
export { default as grid } from "./Grid.svelte"
|
export { default as grid } from "./Grid.svelte"
|
||||||
export { default as sidepanel } from "./SidePanel.svelte"
|
export { default as sidepanel } from "./SidePanel.svelte"
|
||||||
|
export { default as modal } from "./Modal.svelte"
|
||||||
export { default as gridblock } from "./GridBlock.svelte"
|
export { default as gridblock } from "./GridBlock.svelte"
|
||||||
export * from "./charts"
|
export * from "./charts"
|
||||||
export * from "./forms"
|
export * from "./forms"
|
||||||
|
|
|
@ -8,6 +8,8 @@
|
||||||
<ModalContent
|
<ModalContent
|
||||||
title={$confirmationStore.title}
|
title={$confirmationStore.title}
|
||||||
onConfirm={confirmationStore.actions.confirm}
|
onConfirm={confirmationStore.actions.confirm}
|
||||||
|
confirmText={$confirmationStore.confirmButtonText}
|
||||||
|
cancelText={$confirmationStore.cancelButtonText}
|
||||||
>
|
>
|
||||||
{$confirmationStore.text}
|
{$confirmationStore.text}
|
||||||
</ModalContent>
|
</ModalContent>
|
||||||
|
|
|
@ -57,7 +57,9 @@
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
nextState.indicators[idx].visible =
|
nextState.indicators[idx].visible =
|
||||||
nextState.indicators[idx].insideSidePanel || entries[0].isIntersecting
|
nextState.indicators[idx].insideModal ||
|
||||||
|
nextState.indicators[idx].insideSidePanel ||
|
||||||
|
entries[0].isIntersecting
|
||||||
if (++callbackCount === observers.length) {
|
if (++callbackCount === observers.length) {
|
||||||
state = nextState
|
state = nextState
|
||||||
updating = false
|
updating = false
|
||||||
|
@ -139,6 +141,7 @@
|
||||||
height: elBounds.height + 4,
|
height: elBounds.height + 4,
|
||||||
visible: false,
|
visible: false,
|
||||||
insideSidePanel: !!child.closest(".side-panel"),
|
insideSidePanel: !!child.closest(".side-panel"),
|
||||||
|
insideModal: !!child.closest(".modal-content"),
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -41,7 +41,7 @@
|
||||||
allSettings.push(setting)
|
allSettings.push(setting)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
return allSettings.filter(setting => setting.showInBar)
|
return allSettings.filter(setting => setting.showInBar && !setting.hidden)
|
||||||
}
|
}
|
||||||
|
|
||||||
const updatePosition = () => {
|
const updatePosition = () => {
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue