Merge branch 'master' of github.com:Budibase/budibase into grid-layout-expansion
|
@ -43,6 +43,7 @@
|
||||||
},
|
},
|
||||||
"rules": {
|
"rules": {
|
||||||
"no-unused-vars": "off",
|
"no-unused-vars": "off",
|
||||||
|
"local-rules/no-barrel-imports": "error",
|
||||||
"local-rules/no-budibase-imports": "error",
|
"local-rules/no-budibase-imports": "error",
|
||||||
"local-rules/no-console-error": "error",
|
"local-rules/no-console-error": "error",
|
||||||
"@typescript-eslint/no-unused-vars": [
|
"@typescript-eslint/no-unused-vars": [
|
||||||
|
|
|
@ -139,7 +139,7 @@ $ helm install --create-namespace --namespace budibase budibase . -f values.yaml
|
||||||
| 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.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.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 | `` | 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. |
|
||||||
| ingress.enabled | bool | `true` | Whether to create an Ingress resource pointing to the Budibase proxy. |
|
| ingress.enabled | bool | `true` | Whether to create an Ingress resource pointing to the Budibase proxy. |
|
||||||
|
|
|
@ -65,7 +65,10 @@ spec:
|
||||||
- name: ENABLE_ANALYTICS
|
- name: ENABLE_ANALYTICS
|
||||||
value: {{ .Values.globals.enableAnalytics | quote }}
|
value: {{ .Values.globals.enableAnalytics | quote }}
|
||||||
- name: API_ENCRYPTION_KEY
|
- name: API_ENCRYPTION_KEY
|
||||||
value: {{ .Values.globals.apiEncryptionKey | quote }}
|
valueFrom:
|
||||||
|
secretKeyRef:
|
||||||
|
name: {{ template "budibase.fullname" . }}
|
||||||
|
key: apiEncryptionKey
|
||||||
- name: HTTP_LOGGING
|
- name: HTTP_LOGGING
|
||||||
value: {{ .Values.services.apps.httpLogging | quote }}
|
value: {{ .Values.services.apps.httpLogging | quote }}
|
||||||
- name: INTERNAL_API_KEY
|
- name: INTERNAL_API_KEY
|
||||||
|
@ -161,7 +164,10 @@ spec:
|
||||||
- name: TENANT_FEATURE_FLAGS
|
- name: TENANT_FEATURE_FLAGS
|
||||||
value: {{ .Values.globals.tenantFeatureFlags | quote }}
|
value: {{ .Values.globals.tenantFeatureFlags | quote }}
|
||||||
- name: ENCRYPTION_KEY
|
- name: ENCRYPTION_KEY
|
||||||
value: {{ .Values.globals.bbEncryptionKey | quote }}
|
valueFrom:
|
||||||
|
secretKeyRef:
|
||||||
|
name: {{ template "budibase.fullname" . }}
|
||||||
|
key: bbEncryptionKey
|
||||||
{{ if .Values.globals.bbAdminUserEmail }}
|
{{ if .Values.globals.bbAdminUserEmail }}
|
||||||
- name: BB_ADMIN_USER_EMAIL
|
- name: BB_ADMIN_USER_EMAIL
|
||||||
value: {{ .Values.globals.bbAdminUserEmail | quote }}
|
value: {{ .Values.globals.bbAdminUserEmail | quote }}
|
||||||
|
@ -221,7 +227,7 @@ spec:
|
||||||
name: {{ .secretName }}
|
name: {{ .secretName }}
|
||||||
key: {{ .secretKey | quote }}
|
key: {{ .secretKey | quote }}
|
||||||
{{- end}}
|
{{- end}}
|
||||||
image: budibase/apps:{{ .Values.globals.appVersion | default .Chart.AppVersion }}
|
image: {{ .Values.globals.dockerRegistry }}budibase/apps:{{ .Values.globals.appVersion | default .Chart.AppVersion }}
|
||||||
imagePullPolicy: Always
|
imagePullPolicy: Always
|
||||||
{{- if .Values.services.apps.startupProbe }}
|
{{- if .Values.services.apps.startupProbe }}
|
||||||
{{- with .Values.services.apps.startupProbe }}
|
{{- with .Values.services.apps.startupProbe }}
|
||||||
|
|
|
@ -58,7 +58,10 @@ spec:
|
||||||
- name: ENABLE_ANALYTICS
|
- name: ENABLE_ANALYTICS
|
||||||
value: {{ .Values.globals.enableAnalytics | quote }}
|
value: {{ .Values.globals.enableAnalytics | quote }}
|
||||||
- name: API_ENCRYPTION_KEY
|
- name: API_ENCRYPTION_KEY
|
||||||
value: {{ .Values.globals.apiEncryptionKey | quote }}
|
valueFrom:
|
||||||
|
secretKeyRef:
|
||||||
|
name: {{ template "budibase.fullname" . }}
|
||||||
|
key: apiEncryptionKey
|
||||||
- name: HTTP_LOGGING
|
- name: HTTP_LOGGING
|
||||||
value: {{ .Values.services.automationWorkers.httpLogging | quote }}
|
value: {{ .Values.services.automationWorkers.httpLogging | quote }}
|
||||||
- name: INTERNAL_API_KEY
|
- name: INTERNAL_API_KEY
|
||||||
|
@ -154,7 +157,10 @@ spec:
|
||||||
- name: TENANT_FEATURE_FLAGS
|
- name: TENANT_FEATURE_FLAGS
|
||||||
value: {{ .Values.globals.tenantFeatureFlags | quote }}
|
value: {{ .Values.globals.tenantFeatureFlags | quote }}
|
||||||
- name: ENCRYPTION_KEY
|
- name: ENCRYPTION_KEY
|
||||||
value: {{ .Values.globals.bbEncryptionKey | quote }}
|
valueFrom:
|
||||||
|
secretKeyRef:
|
||||||
|
name: {{ template "budibase.fullname" . }}
|
||||||
|
key: bbEncryptionKey
|
||||||
{{ if .Values.globals.bbAdminUserEmail }}
|
{{ if .Values.globals.bbAdminUserEmail }}
|
||||||
- name: BB_ADMIN_USER_EMAIL
|
- name: BB_ADMIN_USER_EMAIL
|
||||||
value: {{ .Values.globals.bbAdminUserEmail | quote }}
|
value: {{ .Values.globals.bbAdminUserEmail | quote }}
|
||||||
|
@ -209,7 +215,7 @@ spec:
|
||||||
key: {{ .secretKey | quote }}
|
key: {{ .secretKey | quote }}
|
||||||
{{- end}}
|
{{- end}}
|
||||||
|
|
||||||
image: budibase/apps:{{ .Values.globals.appVersion | default .Chart.AppVersion }}
|
image: {{ .Values.globals.dockerRegistry }}budibase/apps:{{ .Values.globals.appVersion | default .Chart.AppVersion }}
|
||||||
imagePullPolicy: Always
|
imagePullPolicy: Always
|
||||||
{{- if .Values.services.automationWorkers.startupProbe }}
|
{{- if .Values.services.automationWorkers.startupProbe }}
|
||||||
{{- with .Values.services.automationWorkers.startupProbe }}
|
{{- with .Values.services.automationWorkers.startupProbe }}
|
||||||
|
|
|
@ -35,7 +35,7 @@ spec:
|
||||||
name: {{ template "budibase.fullname" . }}
|
name: {{ template "budibase.fullname" . }}
|
||||||
key: objectStoreSecret
|
key: objectStoreSecret
|
||||||
|
|
||||||
image: minio/minio
|
image: {{ .Values.globals.dockerRegistry }}minio/minio
|
||||||
imagePullPolicy: ""
|
imagePullPolicy: ""
|
||||||
livenessProbe:
|
livenessProbe:
|
||||||
httpGet:
|
httpGet:
|
||||||
|
|
|
@ -32,7 +32,7 @@ spec:
|
||||||
{{ end }}
|
{{ end }}
|
||||||
spec:
|
spec:
|
||||||
containers:
|
containers:
|
||||||
- image: budibase/proxy:{{ .Values.globals.appVersion | default .Chart.AppVersion }}
|
- image: {{ .Values.globals.dockerRegistry }}budibase/proxy:{{ .Values.globals.appVersion | default .Chart.AppVersion }}
|
||||||
imagePullPolicy: Always
|
imagePullPolicy: Always
|
||||||
name: proxy-service
|
name: proxy-service
|
||||||
{{- if .Values.services.proxy.startupProbe }}
|
{{- if .Values.services.proxy.startupProbe }}
|
||||||
|
|
|
@ -22,7 +22,7 @@ spec:
|
||||||
- redis-server
|
- redis-server
|
||||||
- --requirepass
|
- --requirepass
|
||||||
- {{ .Values.services.redis.password }}
|
- {{ .Values.services.redis.password }}
|
||||||
image: {{ .Values.services.redis.image }}
|
image: {{ .Values.globals.dockerRegistry }}{{ .Values.services.redis.image }}
|
||||||
imagePullPolicy: ""
|
imagePullPolicy: ""
|
||||||
name: redis-service
|
name: redis-service
|
||||||
ports:
|
ports:
|
||||||
|
|
|
@ -16,10 +16,14 @@ data:
|
||||||
jwtSecret: {{ index $existingSecret.data "jwtSecret" }}
|
jwtSecret: {{ index $existingSecret.data "jwtSecret" }}
|
||||||
objectStoreAccess: {{ index $existingSecret.data "objectStoreAccess" }}
|
objectStoreAccess: {{ index $existingSecret.data "objectStoreAccess" }}
|
||||||
objectStoreSecret: {{ index $existingSecret.data "objectStoreSecret" }}
|
objectStoreSecret: {{ index $existingSecret.data "objectStoreSecret" }}
|
||||||
|
bbEncryptionKey: {{ index $existingSecret.data "bbEncryptionKey" }}
|
||||||
|
apiEncryptionKey: {{ index $existingSecret.data "apiEncryptionKey" }}
|
||||||
{{- else }}
|
{{- else }}
|
||||||
internalApiKey: {{ template "budibase.defaultsecret" .Values.globals.internalApiKey }}
|
internalApiKey: {{ template "budibase.defaultsecret" .Values.globals.internalApiKey }}
|
||||||
jwtSecret: {{ template "budibase.defaultsecret" .Values.globals.jwtSecret }}
|
jwtSecret: {{ template "budibase.defaultsecret" .Values.globals.jwtSecret }}
|
||||||
objectStoreAccess: {{ template "budibase.defaultsecret" .Values.services.objectStore.accessKey }}
|
objectStoreAccess: {{ template "budibase.defaultsecret" .Values.services.objectStore.accessKey }}
|
||||||
objectStoreSecret: {{ template "budibase.defaultsecret" .Values.services.objectStore.secretKey }}
|
objectStoreSecret: {{ template "budibase.defaultsecret" .Values.services.objectStore.secretKey }}
|
||||||
|
bbEncryptionKey: {{ template "budibase.defaultsecret" .Values.globals.bbEncryptionKey }}
|
||||||
|
apiEncryptionKey: {{ template "budibase.defaultsecret" .Values.globals.apiEncryptionKey }}
|
||||||
{{- end }}
|
{{- end }}
|
||||||
{{- end }}
|
{{- end }}
|
||||||
|
|
|
@ -65,7 +65,10 @@ spec:
|
||||||
{{ end }}
|
{{ end }}
|
||||||
{{ end }}
|
{{ end }}
|
||||||
- name: API_ENCRYPTION_KEY
|
- name: API_ENCRYPTION_KEY
|
||||||
value: {{ .Values.globals.apiEncryptionKey | quote }}
|
valueFrom:
|
||||||
|
secretKeyRef:
|
||||||
|
name: {{ template "budibase.fullname" . }}
|
||||||
|
key: apiEncryptionKey
|
||||||
- name: HTTP_LOGGING
|
- name: HTTP_LOGGING
|
||||||
value: {{ .Values.services.worker.httpLogging | quote }}
|
value: {{ .Values.services.worker.httpLogging | quote }}
|
||||||
- name: INTERNAL_API_KEY
|
- name: INTERNAL_API_KEY
|
||||||
|
@ -167,7 +170,10 @@ spec:
|
||||||
- name: TENANT_FEATURE_FLAGS
|
- name: TENANT_FEATURE_FLAGS
|
||||||
value: {{ .Values.globals.tenantFeatureFlags | quote }}
|
value: {{ .Values.globals.tenantFeatureFlags | quote }}
|
||||||
- name: ENCRYPTION_KEY
|
- name: ENCRYPTION_KEY
|
||||||
value: {{ .Values.globals.bbEncryptionKey | quote }}
|
valueFrom:
|
||||||
|
secretKeyRef:
|
||||||
|
name: {{ template "budibase.fullname" . }}
|
||||||
|
key: bbEncryptionKey
|
||||||
{{ if .Values.globals.datadogApmEnabled }}
|
{{ if .Values.globals.datadogApmEnabled }}
|
||||||
- name: DD_LOGS_INJECTION
|
- name: DD_LOGS_INJECTION
|
||||||
value: {{ .Values.globals.datadogApmEnabled | quote }}
|
value: {{ .Values.globals.datadogApmEnabled | quote }}
|
||||||
|
@ -207,7 +213,7 @@ spec:
|
||||||
name: {{ .secretName }}
|
name: {{ .secretName }}
|
||||||
key: {{ .secretKey | quote }}
|
key: {{ .secretKey | quote }}
|
||||||
{{- end}}
|
{{- end}}
|
||||||
image: budibase/worker:{{ .Values.globals.appVersion | default .Chart.AppVersion }}
|
image: {{ .Values.globals.dockerRegistry }}budibase/worker:{{ .Values.globals.appVersion | default .Chart.AppVersion }}
|
||||||
imagePullPolicy: Always
|
imagePullPolicy: Always
|
||||||
{{- if .Values.services.worker.startupProbe }}
|
{{- if .Values.services.worker.startupProbe }}
|
||||||
{{- with .Values.services.worker.startupProbe }}
|
{{- with .Values.services.worker.startupProbe }}
|
||||||
|
|
|
@ -62,7 +62,7 @@ globals:
|
||||||
budibaseEnv: PRODUCTION
|
budibaseEnv: PRODUCTION
|
||||||
# -- Sets what feature flags are enabled and for which tenants. Should not ordinarily need to be
|
# -- Sets what feature flags are enabled and for which tenants. Should not ordinarily need to be
|
||||||
# changed.
|
# changed.
|
||||||
tenantFeatureFlags: "*:LICENSING,*:USER_GROUPS,*:ONBOARDING_TOUR"
|
tenantFeatureFlags: ""
|
||||||
# -- Whether to enable analytics or not. You can read more about our analytics here:
|
# -- Whether to enable analytics or not. You can read more about our analytics here:
|
||||||
# <https://docs.budibase.com/docs/analytics>.
|
# <https://docs.budibase.com/docs/analytics>.
|
||||||
enableAnalytics: "1"
|
enableAnalytics: "1"
|
||||||
|
|
|
@ -1,6 +1,17 @@
|
||||||
|
const path = require("path")
|
||||||
|
|
||||||
|
const makeBarrelPath = finalPath => {
|
||||||
|
return path.resolve(__dirname, "..", finalPath)
|
||||||
|
}
|
||||||
|
const backendCoreBarrelPaths = [
|
||||||
|
makeBarrelPath(path.join("packages", "backend-core", "src", "index.ts")),
|
||||||
|
makeBarrelPath(path.join("packages", "backend-core", "src")),
|
||||||
|
makeBarrelPath(path.join("packages", "backend-core")),
|
||||||
|
]
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
"no-console-error": {
|
"no-console-error": {
|
||||||
create: function(context) {
|
create: function (context) {
|
||||||
return {
|
return {
|
||||||
CallExpression(node) {
|
CallExpression(node) {
|
||||||
if (
|
if (
|
||||||
|
@ -13,11 +24,12 @@ module.exports = {
|
||||||
) {
|
) {
|
||||||
context.report({
|
context.report({
|
||||||
node,
|
node,
|
||||||
message: 'Using console.error(err) on its own is not allowed. Either provide context to the error (console.error(msg, err)) or throw it.',
|
message:
|
||||||
|
"Using console.error(err) on its own is not allowed. Either provide context to the error (console.error(msg, err)) or throw it.",
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
};
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"no-budibase-imports": {
|
"no-budibase-imports": {
|
||||||
|
@ -106,4 +118,42 @@ module.exports = {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
"no-barrel-imports": {
|
||||||
|
meta: {
|
||||||
|
type: "problem",
|
||||||
|
docs: {
|
||||||
|
description:
|
||||||
|
"Disallow imports from the top-level backend-core barrel file",
|
||||||
|
category: "Best Practices",
|
||||||
|
recommended: false,
|
||||||
|
},
|
||||||
|
schema: [], // no options
|
||||||
|
messages: {
|
||||||
|
noBarrelImport:
|
||||||
|
"Avoid importing from the top-level barrel file 'backend-core/src/index.ts'. Import directly from the specific module instead.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
create(context) {
|
||||||
|
return {
|
||||||
|
ImportDeclaration(node) {
|
||||||
|
const importPath = node.source.value
|
||||||
|
const importFullPath = path.resolve(
|
||||||
|
context.getFilename(),
|
||||||
|
"..",
|
||||||
|
importPath
|
||||||
|
)
|
||||||
|
|
||||||
|
if (backendCoreBarrelPaths.includes(importFullPath)) {
|
||||||
|
context.report({
|
||||||
|
node,
|
||||||
|
messageId: "noBarrelImport",
|
||||||
|
data: {
|
||||||
|
importFullPath,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,7 +10,6 @@ declare -a DOCKER_VARS=("APP_PORT" "APPS_URL" "ARCHITECTURE" "BUDIBASE_ENVIRONME
|
||||||
[[ -z "${MINIO_URL}" ]] && [[ -z "${USE_S3}" ]] && export MINIO_URL=http://127.0.0.1:9000
|
[[ -z "${MINIO_URL}" ]] && [[ -z "${USE_S3}" ]] && export MINIO_URL=http://127.0.0.1:9000
|
||||||
[[ -z "${NODE_ENV}" ]] && export NODE_ENV=production
|
[[ -z "${NODE_ENV}" ]] && export NODE_ENV=production
|
||||||
[[ -z "${POSTHOG_TOKEN}" ]] && export POSTHOG_TOKEN=phc_bIjZL7oh2GEUd2vqvTBH8WvrX0fWTFQMs6H5KQxiUxU
|
[[ -z "${POSTHOG_TOKEN}" ]] && export POSTHOG_TOKEN=phc_bIjZL7oh2GEUd2vqvTBH8WvrX0fWTFQMs6H5KQxiUxU
|
||||||
[[ -z "${TENANT_FEATURE_FLAGS}" ]] && export TENANT_FEATURE_FLAGS="*:LICENSING,*:USER_GROUPS,*:ONBOARDING_TOUR"
|
|
||||||
[[ -z "${ACCOUNT_PORTAL_URL}" ]] && export ACCOUNT_PORTAL_URL=https://account.budibase.app
|
[[ -z "${ACCOUNT_PORTAL_URL}" ]] && export ACCOUNT_PORTAL_URL=https://account.budibase.app
|
||||||
[[ -z "${REDIS_URL}" ]] && export REDIS_URL=127.0.0.1:6379
|
[[ -z "${REDIS_URL}" ]] && export REDIS_URL=127.0.0.1:6379
|
||||||
[[ -z "${SELF_HOSTED}" ]] && export SELF_HOSTED=1
|
[[ -z "${SELF_HOSTED}" ]] && export SELF_HOSTED=1
|
||||||
|
@ -45,8 +44,7 @@ fi
|
||||||
# randomise any unset environment variables
|
# randomise any unset environment variables
|
||||||
for ENV_VAR in "${ENV_VARS[@]}"
|
for ENV_VAR in "${ENV_VARS[@]}"
|
||||||
do
|
do
|
||||||
temp=$(eval "echo \$$ENV_VAR")
|
if [[ -z "${!ENV_VAR}" ]]; then
|
||||||
if [[ -z "${temp}" ]]; then
|
|
||||||
eval "export $ENV_VAR=$(uuidgen | sed -e 's/-//g')"
|
eval "export $ENV_VAR=$(uuidgen | sed -e 's/-//g')"
|
||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"$schema": "node_modules/lerna/schemas/lerna-schema.json",
|
"$schema": "node_modules/lerna/schemas/lerna-schema.json",
|
||||||
"version": "2.30.3",
|
"version": "2.30.7",
|
||||||
"npmClient": "yarn",
|
"npmClient": "yarn",
|
||||||
"packages": [
|
"packages": [
|
||||||
"packages/*",
|
"packages/*",
|
||||||
|
|
|
@ -2,7 +2,7 @@ import { testEnv } from "../../../tests/extra"
|
||||||
import * as context from "../"
|
import * as context from "../"
|
||||||
import { DEFAULT_TENANT_ID } from "../../constants"
|
import { DEFAULT_TENANT_ID } from "../../constants"
|
||||||
import { structures } from "../../../tests"
|
import { structures } from "../../../tests"
|
||||||
import { db } from "../.."
|
import * as db from "../../db"
|
||||||
import Context from "../Context"
|
import Context from "../Context"
|
||||||
import { ContextMap } from "../types"
|
import { ContextMap } from "../types"
|
||||||
import { IdentityType } from "@budibase/types"
|
import { IdentityType } from "@budibase/types"
|
||||||
|
|
|
@ -159,8 +159,9 @@ export class FlagSet<V extends Flag<any>, T extends { [key: string]: V }> {
|
||||||
specificallySetFalse.add(feature)
|
specificallySetFalse.add(feature)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ignore unknown flags
|
||||||
if (!this.isFlagName(feature)) {
|
if (!this.isFlagName(feature)) {
|
||||||
throw new Error(`Feature: ${feature} is not an allowed option`)
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
if (typeof flagValues[feature] !== "boolean") {
|
if (typeof flagValues[feature] !== "boolean") {
|
||||||
|
@ -169,7 +170,7 @@ export class FlagSet<V extends Flag<any>, T extends { [key: string]: V }> {
|
||||||
|
|
||||||
// @ts-expect-error - TS does not like you writing into a generic type,
|
// @ts-expect-error - TS does not like you writing into a generic type,
|
||||||
// but we know that it's okay in this case because it's just an object.
|
// but we know that it's okay in this case because it's just an object.
|
||||||
flagValues[feature] = value
|
flagValues[feature as keyof FlagValues] = value
|
||||||
tags[`flags.${feature}.source`] = "environment"
|
tags[`flags.${feature}.source`] = "environment"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -265,9 +266,5 @@ export class FlagSet<V extends Flag<any>, T extends { [key: string]: V }> {
|
||||||
// All of the machinery in this file is to make sure that flags have their
|
// All of the machinery in this file is to make sure that flags have their
|
||||||
// default values set correctly and their types flow through the system.
|
// default values set correctly and their types flow through the system.
|
||||||
export const flags = new FlagSet({
|
export const flags = new FlagSet({
|
||||||
LICENSING: Flag.boolean(false),
|
|
||||||
GOOGLE_SHEETS: Flag.boolean(false),
|
|
||||||
USER_GROUPS: Flag.boolean(false),
|
|
||||||
ONBOARDING_TOUR: Flag.boolean(false),
|
|
||||||
DEFAULT_VALUES: Flag.boolean(false),
|
DEFAULT_VALUES: Flag.boolean(false),
|
||||||
})
|
})
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { IdentityContext, IdentityType, UserCtx } from "@budibase/types"
|
import { IdentityContext, IdentityType, UserCtx } from "@budibase/types"
|
||||||
import { Flag, FlagSet, FlagValues, init, shutdown } from "../"
|
import { Flag, FlagSet, FlagValues, init, shutdown } from "../"
|
||||||
import { context } from "../.."
|
import * as context from "../../context"
|
||||||
import environment, { withEnv } from "../../environment"
|
import environment, { withEnv } from "../../environment"
|
||||||
import nodeFetch from "node-fetch"
|
import nodeFetch from "node-fetch"
|
||||||
import nock from "nock"
|
import nock from "nock"
|
||||||
|
@ -67,9 +67,9 @@ describe("feature flags", () => {
|
||||||
expected: flags.defaults(),
|
expected: flags.defaults(),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
it: "should fail when an environment flag is not recognised",
|
it: "should ignore unknown feature flags",
|
||||||
environmentFlags: "default:TEST_BOOLEAN,default:FOO",
|
environmentFlags: "default:TEST_BOOLEAN,default:FOO",
|
||||||
errorMessage: "Feature: FOO is not an allowed option",
|
expected: { TEST_BOOLEAN: true },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
it: "should be able to read boolean flags from PostHog",
|
it: "should be able to read boolean flags from PostHog",
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { GenericContainer, StartedTestContainer } from "testcontainers"
|
import { GenericContainer, StartedTestContainer } from "testcontainers"
|
||||||
import { generator, structures } from "../../../tests"
|
import { generator, structures } from "../../../tests"
|
||||||
import RedisWrapper, { closeAll } from "../redis"
|
import RedisWrapper, { closeAll } from "../redis"
|
||||||
import { env } from "../.."
|
import env from "../../environment"
|
||||||
import { randomUUID } from "crypto"
|
import { randomUUID } from "crypto"
|
||||||
|
|
||||||
jest.setTimeout(30000)
|
jest.setTimeout(30000)
|
||||||
|
|
|
@ -46,7 +46,7 @@
|
||||||
import { RowUtils } from "@budibase/frontend-core"
|
import { RowUtils } from "@budibase/frontend-core"
|
||||||
import ServerBindingPanel from "components/common/bindings/ServerBindingPanel.svelte"
|
import ServerBindingPanel from "components/common/bindings/ServerBindingPanel.svelte"
|
||||||
import OptionsEditor from "./OptionsEditor.svelte"
|
import OptionsEditor from "./OptionsEditor.svelte"
|
||||||
import { isEnabled, TENANT_FEATURE_FLAGS } from "helpers/featureFlags"
|
import { isEnabled } from "helpers/featureFlags"
|
||||||
|
|
||||||
const AUTO_TYPE = FieldType.AUTO
|
const AUTO_TYPE = FieldType.AUTO
|
||||||
const FORMULA_TYPE = FieldType.FORMULA
|
const FORMULA_TYPE = FieldType.FORMULA
|
||||||
|
@ -168,8 +168,7 @@
|
||||||
$: canBeDisplay =
|
$: canBeDisplay =
|
||||||
canBeDisplayColumn(editableColumn.type) && !editableColumn.autocolumn
|
canBeDisplayColumn(editableColumn.type) && !editableColumn.autocolumn
|
||||||
$: canHaveDefault =
|
$: canHaveDefault =
|
||||||
isEnabled(TENANT_FEATURE_FLAGS.DEFAULT_VALUES) &&
|
isEnabled("DEFAULT_VALUES") && canHaveDefaultColumn(editableColumn.type)
|
||||||
canHaveDefaultColumn(editableColumn.type)
|
|
||||||
$: canBeRequired =
|
$: canBeRequired =
|
||||||
editableColumn?.type !== LINK_TYPE &&
|
editableColumn?.type !== LINK_TYPE &&
|
||||||
!uneditable &&
|
!uneditable &&
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
<script>
|
<script>
|
||||||
import FontAwesomeIcon from "./FontAwesomeIcon.svelte"
|
import FontAwesomeIcon from "./FontAwesomeIcon.svelte"
|
||||||
import { Popover, Heading, Body } from "@budibase/bbui"
|
import { Popover, Heading, Body } from "@budibase/bbui"
|
||||||
import { isEnabled, TENANT_FEATURE_FLAGS } from "helpers/featureFlags"
|
|
||||||
import { licensing } from "stores/portal"
|
import { licensing } from "stores/portal"
|
||||||
import { isPremiumOrAbove } from "helpers/planTitle"
|
import { isPremiumOrAbove } from "helpers/planTitle"
|
||||||
import { ChangelogURL } from "constants"
|
import { ChangelogURL } from "constants"
|
||||||
|
@ -62,31 +61,26 @@
|
||||||
<Body size="S">Budibase University</Body>
|
<Body size="S">Budibase University</Body>
|
||||||
</a>
|
</a>
|
||||||
<div class="divider" />
|
<div class="divider" />
|
||||||
{#if isEnabled(TENANT_FEATURE_FLAGS.LICENSING)}
|
<a
|
||||||
<a
|
href={premiumOrAboveLicense
|
||||||
href={premiumOrAboveLicense
|
? "mailto:support@budibase.com"
|
||||||
? "mailto:support@budibase.com"
|
: "/builder/portal/account/usage"}
|
||||||
: "/builder/portal/account/usage"}
|
>
|
||||||
>
|
<div class="premiumLinkContent" class:disabled={!premiumOrAboveLicense}>
|
||||||
<div
|
<div class="icon">
|
||||||
class="premiumLinkContent"
|
<FontAwesomeIcon name="fa-solid fa-envelope" />
|
||||||
class:disabled={!premiumOrAboveLicense}
|
|
||||||
>
|
|
||||||
<div class="icon">
|
|
||||||
<FontAwesomeIcon name="fa-solid fa-envelope" />
|
|
||||||
</div>
|
|
||||||
<Body size="S">Email support</Body>
|
|
||||||
</div>
|
</div>
|
||||||
{#if !premiumOrAboveLicense}
|
<Body size="S">Email support</Body>
|
||||||
<div class="premiumBadge">
|
</div>
|
||||||
<div class="icon">
|
{#if !premiumOrAboveLicense}
|
||||||
<FontAwesomeIcon name="fa-solid fa-lock" />
|
<div class="premiumBadge">
|
||||||
</div>
|
<div class="icon">
|
||||||
<Body size="XS">Premium</Body>
|
<FontAwesomeIcon name="fa-solid fa-lock" />
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
<Body size="XS">Premium</Body>
|
||||||
</a>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
</a>
|
||||||
</nav>
|
</nav>
|
||||||
</Popover>
|
</Popover>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -6,8 +6,8 @@
|
||||||
|
|
||||||
export let onConfirm
|
export let onConfirm
|
||||||
export let onCancel
|
export let onCancel
|
||||||
export let screenUrl
|
export let route
|
||||||
export let screenRole
|
export let role
|
||||||
export let confirmText = "Continue"
|
export let confirmText = "Continue"
|
||||||
|
|
||||||
const appPrefix = "/app"
|
const appPrefix = "/app"
|
||||||
|
@ -15,17 +15,17 @@
|
||||||
let error
|
let error
|
||||||
let modal
|
let modal
|
||||||
|
|
||||||
$: appUrl = screenUrl
|
$: appUrl = route
|
||||||
? `${window.location.origin}${appPrefix}${screenUrl}`
|
? `${window.location.origin}${appPrefix}${route}`
|
||||||
: `${window.location.origin}${appPrefix}`
|
: `${window.location.origin}${appPrefix}`
|
||||||
|
|
||||||
const routeChanged = event => {
|
const routeChanged = event => {
|
||||||
if (!event.detail.startsWith("/")) {
|
if (!event.detail.startsWith("/")) {
|
||||||
screenUrl = "/" + event.detail
|
route = "/" + event.detail
|
||||||
}
|
}
|
||||||
touched = true
|
touched = true
|
||||||
screenUrl = sanitizeUrl(screenUrl)
|
route = sanitizeUrl(route)
|
||||||
if (routeExists(screenUrl)) {
|
if (routeExists(route)) {
|
||||||
error = "This URL is already taken for this access role"
|
error = "This URL is already taken for this access role"
|
||||||
} else {
|
} else {
|
||||||
error = null
|
error = null
|
||||||
|
@ -33,19 +33,19 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
const routeExists = url => {
|
const routeExists = url => {
|
||||||
if (!screenRole) {
|
if (!role) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
return get(screenStore).screens.some(
|
return get(screenStore).screens.some(
|
||||||
screen =>
|
screen =>
|
||||||
screen.routing.route.toLowerCase() === url.toLowerCase() &&
|
screen.routing.route.toLowerCase() === url.toLowerCase() &&
|
||||||
screen.routing.roleId === screenRole
|
screen.routing.roleId === role
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const confirmScreenDetails = async () => {
|
const confirmScreenDetails = async () => {
|
||||||
await onConfirm({
|
await onConfirm({
|
||||||
screenUrl,
|
route,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
@ -58,13 +58,13 @@
|
||||||
onConfirm={confirmScreenDetails}
|
onConfirm={confirmScreenDetails}
|
||||||
{onCancel}
|
{onCancel}
|
||||||
cancelText={"Back"}
|
cancelText={"Back"}
|
||||||
disabled={!screenUrl || error || !touched}
|
disabled={!route || error || !touched}
|
||||||
>
|
>
|
||||||
<form on:submit|preventDefault={() => modal.confirm()}>
|
<form on:submit|preventDefault={() => modal.confirm()}>
|
||||||
<Input
|
<Input
|
||||||
label="Enter a URL for the new screen"
|
label="Enter a URL for the new screen"
|
||||||
{error}
|
{error}
|
||||||
bind:value={screenUrl}
|
bind:value={route}
|
||||||
on:change={routeChanged}
|
on:change={routeChanged}
|
||||||
/>
|
/>
|
||||||
<div class="app-server" title={appUrl}>
|
<div class="app-server" title={appUrl}>
|
||||||
|
|
|
@ -34,6 +34,7 @@
|
||||||
import ClientBindingPanel from "components/common/bindings/ClientBindingPanel.svelte"
|
import ClientBindingPanel from "components/common/bindings/ClientBindingPanel.svelte"
|
||||||
import DataSourceCategory from "components/design/settings/controls/DataSourceSelect/DataSourceCategory.svelte"
|
import DataSourceCategory from "components/design/settings/controls/DataSourceSelect/DataSourceCategory.svelte"
|
||||||
import { API } from "api"
|
import { API } from "api"
|
||||||
|
import { datasourceSelect as format } from "helpers/data/format"
|
||||||
|
|
||||||
export let value = {}
|
export let value = {}
|
||||||
export let otherSources
|
export let otherSources
|
||||||
|
@ -51,24 +52,15 @@
|
||||||
let modal
|
let modal
|
||||||
|
|
||||||
$: text = value?.label ?? "Choose an option"
|
$: text = value?.label ?? "Choose an option"
|
||||||
$: tables = $tablesStore.list.map(m => ({
|
$: tables = $tablesStore.list.map(table =>
|
||||||
label: m.name,
|
format.table(table, $datasources.list)
|
||||||
tableId: m._id,
|
)
|
||||||
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,
|
||||||
label: view.name,
|
label: view.name,
|
||||||
type: "view",
|
type: "view",
|
||||||
}))
|
}))
|
||||||
$: viewsV2 = $viewsV2Store.list.map(view => ({
|
$: viewsV2 = $viewsV2Store.list.map(format.viewV2)
|
||||||
...view,
|
|
||||||
label: view.name,
|
|
||||||
type: "viewV2",
|
|
||||||
}))
|
|
||||||
$: views = [...(viewsV1 || []), ...(viewsV2 || [])]
|
$: views = [...(viewsV1 || []), ...(viewsV2 || [])]
|
||||||
$: queries = $queriesStore.list
|
$: queries = $queriesStore.list
|
||||||
.filter(q => showAllQueries || q.queryVerb === "read" || q.readable)
|
.filter(q => showAllQueries || q.queryVerb === "read" || q.readable)
|
||||||
|
|
|
@ -2,24 +2,14 @@
|
||||||
import { Select } from "@budibase/bbui"
|
import { Select } from "@budibase/bbui"
|
||||||
import { createEventDispatcher, onMount } from "svelte"
|
import { createEventDispatcher, onMount } from "svelte"
|
||||||
import { tables as tablesStore, viewsV2 } from "stores/builder"
|
import { tables as tablesStore, viewsV2 } from "stores/builder"
|
||||||
|
import { tableSelect as format } from "helpers/data/format"
|
||||||
|
|
||||||
export let value
|
export let value
|
||||||
|
|
||||||
const dispatch = createEventDispatcher()
|
const dispatch = createEventDispatcher()
|
||||||
|
|
||||||
$: tables = $tablesStore.list.map(table => ({
|
$: tables = $tablesStore.list.map(format.table)
|
||||||
type: "table",
|
$: views = $viewsV2.list.map(format.viewV2)
|
||||||
label: table.name,
|
|
||||||
tableId: table._id,
|
|
||||||
resourceId: table._id,
|
|
||||||
}))
|
|
||||||
$: views = $viewsV2.list.map(view => ({
|
|
||||||
type: "viewV2",
|
|
||||||
id: view.id,
|
|
||||||
label: view.name,
|
|
||||||
tableId: view.tableId,
|
|
||||||
resourceId: view.id,
|
|
||||||
}))
|
|
||||||
$: options = [...(tables || []), ...(views || [])]
|
$: options = [...(tables || []), ...(views || [])]
|
||||||
|
|
||||||
const onChange = e => {
|
const onChange = e => {
|
||||||
|
|
|
@ -7,7 +7,6 @@
|
||||||
import { ExpiringKeys } from "./constants"
|
import { ExpiringKeys } from "./constants"
|
||||||
import { getBanners } from "./licensingBanners"
|
import { getBanners } from "./licensingBanners"
|
||||||
import { banner } from "@budibase/bbui"
|
import { banner } from "@budibase/bbui"
|
||||||
import { TENANT_FEATURE_FLAGS, isEnabled } from "helpers/featureFlags"
|
|
||||||
|
|
||||||
const oneDayInSeconds = 86400
|
const oneDayInSeconds = 86400
|
||||||
|
|
||||||
|
@ -89,8 +88,7 @@
|
||||||
userLoaded &&
|
userLoaded &&
|
||||||
$licensing.usageMetrics &&
|
$licensing.usageMetrics &&
|
||||||
domLoaded &&
|
domLoaded &&
|
||||||
!licensingLoaded &&
|
!licensingLoaded
|
||||||
isEnabled(TENANT_FEATURE_FLAGS.LICENSING)
|
|
||||||
) {
|
) {
|
||||||
licensingLoaded = true
|
licensingLoaded = true
|
||||||
queuedModals = processModals()
|
queuedModals = processModals()
|
||||||
|
|
|
@ -0,0 +1,31 @@
|
||||||
|
export const datasourceSelect = {
|
||||||
|
table: (table, datasources) => ({
|
||||||
|
label: table.name,
|
||||||
|
tableId: table._id,
|
||||||
|
type: "table",
|
||||||
|
datasource: datasources.find(
|
||||||
|
datasource => datasource._id === table.sourceId || table.datasourceId
|
||||||
|
),
|
||||||
|
}),
|
||||||
|
viewV2: view => ({
|
||||||
|
...view,
|
||||||
|
label: view.name,
|
||||||
|
type: "viewV2",
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
|
||||||
|
export const tableSelect = {
|
||||||
|
table: table => ({
|
||||||
|
type: "table",
|
||||||
|
label: table.name,
|
||||||
|
tableId: table._id,
|
||||||
|
resourceId: table._id,
|
||||||
|
}),
|
||||||
|
viewV2: view => ({
|
||||||
|
type: "viewV2",
|
||||||
|
id: view.id,
|
||||||
|
label: view.name,
|
||||||
|
tableId: view.tableId,
|
||||||
|
resourceId: view.id,
|
||||||
|
}),
|
||||||
|
}
|
|
@ -1,14 +1,6 @@
|
||||||
import { auth } from "../stores/portal"
|
import { auth } from "../stores/portal"
|
||||||
import { get } from "svelte/store"
|
import { get } from "svelte/store"
|
||||||
|
|
||||||
export const TENANT_FEATURE_FLAGS = {
|
|
||||||
LICENSING: "LICENSING",
|
|
||||||
USER_GROUPS: "USER_GROUPS",
|
|
||||||
ONBOARDING_TOUR: "ONBOARDING_TOUR",
|
|
||||||
GOOGLE_SHEETS: "GOOGLE_SHEETS",
|
|
||||||
DEFAULT_VALUES: "DEFAULT_VALUES",
|
|
||||||
}
|
|
||||||
|
|
||||||
export const isEnabled = featureFlag => {
|
export const isEnabled = featureFlag => {
|
||||||
const user = get(auth).user
|
const user = get(auth).user
|
||||||
return !!user?.flags?.[featureFlag]
|
return !!user?.flags?.[featureFlag]
|
||||||
|
|
|
@ -9,7 +9,6 @@
|
||||||
deploymentStore,
|
deploymentStore,
|
||||||
} from "stores/builder"
|
} from "stores/builder"
|
||||||
import { auth, appsStore } from "stores/portal"
|
import { auth, appsStore } from "stores/portal"
|
||||||
import { TENANT_FEATURE_FLAGS, isEnabled } from "helpers/featureFlags"
|
|
||||||
import {
|
import {
|
||||||
Icon,
|
Icon,
|
||||||
Tabs,
|
Tabs,
|
||||||
|
@ -90,16 +89,14 @@
|
||||||
|
|
||||||
const initTour = async () => {
|
const initTour = async () => {
|
||||||
// Check if onboarding is enabled.
|
// Check if onboarding is enabled.
|
||||||
if (isEnabled(TENANT_FEATURE_FLAGS.ONBOARDING_TOUR)) {
|
if (!$auth.user?.onboardedAt) {
|
||||||
if (!$auth.user?.onboardedAt) {
|
builderStore.startBuilderOnboarding()
|
||||||
builderStore.startBuilderOnboarding()
|
} else {
|
||||||
} else {
|
// Feature tour date
|
||||||
// Feature tour date
|
const release_date = new Date("2023-03-01T00:00:00.000Z")
|
||||||
const release_date = new Date("2023-03-01T00:00:00.000Z")
|
const onboarded = new Date($auth.user?.onboardedAt)
|
||||||
const onboarded = new Date($auth.user?.onboardedAt)
|
if (onboarded < release_date) {
|
||||||
if (onboarded < release_date) {
|
builderStore.setTour(TOUR_KEYS.FEATURE_ONBOARDING)
|
||||||
builderStore.setTour(TOUR_KEYS.FEATURE_ONBOARDING)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,7 +20,7 @@
|
||||||
let confirmDeleteDialog
|
let confirmDeleteDialog
|
||||||
let screenDetailsModal
|
let screenDetailsModal
|
||||||
|
|
||||||
const createDuplicateScreen = async ({ screenName, screenUrl }) => {
|
const createDuplicateScreen = async ({ route }) => {
|
||||||
// Create a dupe and ensure it is unique
|
// Create a dupe and ensure it is unique
|
||||||
let duplicateScreen = Helpers.cloneDeep(screen)
|
let duplicateScreen = Helpers.cloneDeep(screen)
|
||||||
delete duplicateScreen._id
|
delete duplicateScreen._id
|
||||||
|
@ -28,9 +28,8 @@
|
||||||
duplicateScreen.props = makeComponentUnique(duplicateScreen.props)
|
duplicateScreen.props = makeComponentUnique(duplicateScreen.props)
|
||||||
|
|
||||||
// Attach the new name and URL
|
// Attach the new name and URL
|
||||||
duplicateScreen.routing.route = sanitizeUrl(screenUrl)
|
duplicateScreen.routing.route = sanitizeUrl(route)
|
||||||
duplicateScreen.routing.homeScreen = false
|
duplicateScreen.routing.homeScreen = false
|
||||||
duplicateScreen.props._instanceName = screenName
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Create the screen
|
// Create the screen
|
||||||
|
@ -136,8 +135,8 @@
|
||||||
<Modal bind:this={screenDetailsModal}>
|
<Modal bind:this={screenDetailsModal}>
|
||||||
<ScreenDetailsModal
|
<ScreenDetailsModal
|
||||||
onConfirm={createDuplicateScreen}
|
onConfirm={createDuplicateScreen}
|
||||||
screenUrl={screen?.routing.route}
|
route={screen?.routing.route}
|
||||||
screenRole={screen?.routing.roleId}
|
role={screen?.routing.roleId}
|
||||||
confirmText="Duplicate"
|
confirmText="Duplicate"
|
||||||
/>
|
/>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
|
@ -1,8 +1,9 @@
|
||||||
<script>
|
<script>
|
||||||
import ScreenDetailsModal from "components/design/ScreenDetailsModal.svelte"
|
import ScreenDetailsModal from "components/design/ScreenDetailsModal.svelte"
|
||||||
import DatasourceModal from "./DatasourceModal.svelte"
|
import DatasourceModal from "./DatasourceModal.svelte"
|
||||||
import sanitizeUrl from "helpers/sanitizeUrl"
|
import TypeModal from "./TypeModal.svelte"
|
||||||
import FormTypeModal from "./FormTypeModal.svelte"
|
import tableTypes from "./tableTypes"
|
||||||
|
import formTypes from "./formTypes"
|
||||||
import { Modal, notifications } from "@budibase/bbui"
|
import { Modal, notifications } from "@budibase/bbui"
|
||||||
import {
|
import {
|
||||||
screenStore,
|
screenStore,
|
||||||
|
@ -11,14 +12,9 @@
|
||||||
builderStore,
|
builderStore,
|
||||||
} from "stores/builder"
|
} from "stores/builder"
|
||||||
import { auth } from "stores/portal"
|
import { auth } from "stores/portal"
|
||||||
import { get } from "svelte/store"
|
|
||||||
import { capitalise } from "helpers"
|
|
||||||
import { goto } from "@roxi/routify"
|
import { goto } from "@roxi/routify"
|
||||||
import { TOUR_KEYS } from "components/portal/onboarding/tours.js"
|
import { TOUR_KEYS } from "components/portal/onboarding/tours.js"
|
||||||
import blankScreen from "templates/blankScreen"
|
import * as screenTemplating from "templates/screenTemplating"
|
||||||
import formScreen from "templates/formScreen"
|
|
||||||
import gridScreen from "templates/gridScreen"
|
|
||||||
import gridDetailsScreen from "templates/gridDetailsScreen"
|
|
||||||
import { Roles } from "constants/backend"
|
import { Roles } from "constants/backend"
|
||||||
|
|
||||||
let mode
|
let mode
|
||||||
|
@ -26,16 +22,19 @@
|
||||||
let screenDetailsModal
|
let screenDetailsModal
|
||||||
let datasourceModal
|
let datasourceModal
|
||||||
let formTypeModal
|
let formTypeModal
|
||||||
|
let tableTypeModal
|
||||||
|
|
||||||
let selectedTablesAndViews = []
|
let selectedTablesAndViews = []
|
||||||
let permissions = {}
|
let permissions = {}
|
||||||
|
|
||||||
|
$: screens = $screenStore.screens
|
||||||
|
|
||||||
export const show = newMode => {
|
export const show = newMode => {
|
||||||
mode = newMode
|
mode = newMode
|
||||||
selectedTablesAndViews = []
|
selectedTablesAndViews = []
|
||||||
permissions = {}
|
permissions = {}
|
||||||
|
|
||||||
if (mode === "grid" || mode === "gridDetails" || mode === "form") {
|
if (mode === "table" || mode === "form") {
|
||||||
datasourceModal.show()
|
datasourceModal.show()
|
||||||
} else if (mode === "blank") {
|
} else if (mode === "blank") {
|
||||||
screenDetailsModal.show()
|
screenDetailsModal.show()
|
||||||
|
@ -44,136 +43,83 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const createScreen = async screen => {
|
const createScreen = async screenTemplate => {
|
||||||
try {
|
try {
|
||||||
// Check we aren't clashing with an existing URL
|
return await screenStore.save(screenTemplate)
|
||||||
if (hasExistingUrl(screen.routing.route, screen.routing.roleId)) {
|
|
||||||
let suffix = 2
|
|
||||||
let candidateUrl = makeCandidateUrl(screen, suffix)
|
|
||||||
while (hasExistingUrl(candidateUrl, screen.routing.roleId)) {
|
|
||||||
candidateUrl = makeCandidateUrl(screen, ++suffix)
|
|
||||||
}
|
|
||||||
screen.routing.route = candidateUrl
|
|
||||||
}
|
|
||||||
|
|
||||||
screen.routing.route = sanitizeUrl(screen.routing.route)
|
|
||||||
|
|
||||||
return await screenStore.save(screen)
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error)
|
console.error(error)
|
||||||
notifications.error("Error creating screens")
|
notifications.error("Error creating screens")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const addNavigationLink = async screen =>
|
const createScreens = async screenTemplates => {
|
||||||
await navigationStore.saveLink(
|
const newScreens = []
|
||||||
screen.routing.route,
|
|
||||||
capitalise(screen.routing.route.split("/")[1]),
|
|
||||||
screen.routing.roleId
|
|
||||||
)
|
|
||||||
|
|
||||||
// Checks if any screens exist in the store with the given route and
|
for (let screenTemplate of screenTemplates) {
|
||||||
// currently selected role
|
await addNavigationLink(
|
||||||
const hasExistingUrl = (url, screenAccessRole) => {
|
screenTemplate.data,
|
||||||
const screens = get(screenStore).screens.filter(
|
screenTemplate.navigationLinkLabel
|
||||||
s => s.routing.roleId === screenAccessRole
|
)
|
||||||
)
|
newScreens.push(await createScreen(screenTemplate.data))
|
||||||
return !!screens.find(s => s.routing?.route === url)
|
}
|
||||||
|
|
||||||
|
return newScreens
|
||||||
}
|
}
|
||||||
|
|
||||||
// Constructs a candidate URL for a new screen, appending a given suffix to the
|
const addNavigationLink = async (screen, linkLabel) => {
|
||||||
// screen's URL
|
if (linkLabel == null) return
|
||||||
// e.g. "/sales/:id" => "/sales-1/:id"
|
|
||||||
const makeCandidateUrl = (screen, suffix) => {
|
await navigationStore.saveLink(
|
||||||
let url = screen.routing?.route || ""
|
screen.routing.route,
|
||||||
if (url.startsWith("/")) {
|
linkLabel,
|
||||||
url = url.slice(1)
|
screen.routing.roleId
|
||||||
}
|
)
|
||||||
if (!url.includes("/")) {
|
|
||||||
return `/${url}-${suffix}`
|
|
||||||
} else {
|
|
||||||
const split = url.split("/")
|
|
||||||
return `/${split[0]}-${suffix}/${split.slice(1).join("/")}`
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const onSelectDatasources = async () => {
|
const onSelectDatasources = async () => {
|
||||||
if (mode === "form") {
|
if (mode === "form") {
|
||||||
formTypeModal.show()
|
formTypeModal.show()
|
||||||
} else if (mode === "grid") {
|
} else if (mode === "table") {
|
||||||
await createGridScreen()
|
tableTypeModal.show()
|
||||||
} else if (mode === "gridDetails") {
|
|
||||||
await createGridDetailsScreen()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const createBlankScreen = async ({ screenUrl }) => {
|
const createBlankScreen = async ({ route }) => {
|
||||||
const screenTemplate = blankScreen(screenUrl)
|
const screenTemplates = screenTemplating.blank({ route, screens })
|
||||||
const screen = await createScreen(screenTemplate)
|
|
||||||
await addNavigationLink(screenTemplate)
|
|
||||||
|
|
||||||
loadNewScreen(screen)
|
const newScreens = await createScreens(screenTemplates)
|
||||||
|
loadNewScreen(newScreens[0])
|
||||||
}
|
}
|
||||||
|
|
||||||
const createGridScreen = async () => {
|
const createTableScreen = async type => {
|
||||||
let firstScreen = null
|
const screenTemplates = selectedTablesAndViews.flatMap(tableOrView =>
|
||||||
|
screenTemplating.table({
|
||||||
for (let tableOrView of selectedTablesAndViews) {
|
screens,
|
||||||
const screenTemplate = gridScreen(
|
|
||||||
tableOrView,
|
tableOrView,
|
||||||
permissions[tableOrView.id]
|
type,
|
||||||
)
|
permissions: permissions[tableOrView.id],
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
const screen = await createScreen(screenTemplate)
|
const newScreens = await createScreens(screenTemplates)
|
||||||
await addNavigationLink(screen)
|
loadNewScreen(newScreens[0])
|
||||||
|
|
||||||
firstScreen ??= screen
|
|
||||||
}
|
|
||||||
|
|
||||||
loadNewScreen(firstScreen)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const createGridDetailsScreen = async () => {
|
const createFormScreen = async type => {
|
||||||
let firstScreen = null
|
const screenTemplates = selectedTablesAndViews.flatMap(tableOrView =>
|
||||||
|
screenTemplating.form({
|
||||||
for (let tableOrView of selectedTablesAndViews) {
|
screens,
|
||||||
const screenTemplate = gridDetailsScreen(
|
|
||||||
tableOrView,
|
tableOrView,
|
||||||
permissions[tableOrView.id]
|
type,
|
||||||
)
|
permissions: permissions[tableOrView.id],
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
const screen = await createScreen(screenTemplate)
|
const newScreens = await createScreens(screenTemplates)
|
||||||
await addNavigationLink(screen)
|
|
||||||
|
|
||||||
firstScreen ??= screen
|
if (type === "update" || type === "create") {
|
||||||
}
|
|
||||||
|
|
||||||
loadNewScreen(firstScreen)
|
|
||||||
}
|
|
||||||
|
|
||||||
const createFormScreen = async formType => {
|
|
||||||
let firstScreen = null
|
|
||||||
|
|
||||||
for (let tableOrView of selectedTablesAndViews) {
|
|
||||||
const screenTemplate = formScreen(
|
|
||||||
tableOrView,
|
|
||||||
formType,
|
|
||||||
permissions[tableOrView.id]
|
|
||||||
)
|
|
||||||
|
|
||||||
const screen = await createScreen(screenTemplate)
|
|
||||||
// Only add a navigation link for `Create`, as both `Update` and `View`
|
|
||||||
// require an `id` in their URL in order to function.
|
|
||||||
if (formType === "Create") {
|
|
||||||
await addNavigationLink(screen)
|
|
||||||
}
|
|
||||||
|
|
||||||
firstScreen ??= screen
|
|
||||||
}
|
|
||||||
|
|
||||||
if (formType === "Update" || formType === "Create") {
|
|
||||||
const associatedTour =
|
const associatedTour =
|
||||||
formType === "Update"
|
type === "update"
|
||||||
? TOUR_KEYS.BUILDER_FORM_VIEW_UPDATE
|
? TOUR_KEYS.BUILDER_FORM_VIEW_UPDATE
|
||||||
: TOUR_KEYS.BUILDER_FORM_CREATE
|
: TOUR_KEYS.BUILDER_FORM_CREATE
|
||||||
|
|
||||||
|
@ -183,7 +129,7 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
loadNewScreen(firstScreen)
|
loadNewScreen(newScreens[0])
|
||||||
}
|
}
|
||||||
|
|
||||||
const loadNewScreen = screen => {
|
const loadNewScreen = screen => {
|
||||||
|
@ -199,7 +145,11 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
const fetchPermission = resourceId => {
|
const fetchPermission = resourceId => {
|
||||||
permissions[resourceId] = { loading: true, read: null, write: null }
|
permissions[resourceId] = {
|
||||||
|
loading: true,
|
||||||
|
read: Roles.BASIC,
|
||||||
|
write: Roles.BASIC,
|
||||||
|
}
|
||||||
|
|
||||||
permissionsStore
|
permissionsStore
|
||||||
.forResource(resourceId)
|
.forResource(resourceId)
|
||||||
|
@ -218,8 +168,8 @@
|
||||||
if (permissions[resourceId]?.loading) {
|
if (permissions[resourceId]?.loading) {
|
||||||
permissions[resourceId] = {
|
permissions[resourceId] = {
|
||||||
loading: false,
|
loading: false,
|
||||||
read: Roles.PUBLIC,
|
read: Roles.BASIC,
|
||||||
write: Roles.PUBLIC,
|
write: Roles.BASIC,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
@ -250,18 +200,31 @@
|
||||||
<Modal bind:this={datasourceModal} autoFocus={false}>
|
<Modal bind:this={datasourceModal} autoFocus={false}>
|
||||||
<DatasourceModal
|
<DatasourceModal
|
||||||
{selectedTablesAndViews}
|
{selectedTablesAndViews}
|
||||||
{permissions}
|
|
||||||
onConfirm={onSelectDatasources}
|
onConfirm={onSelectDatasources}
|
||||||
on:toggle={handleTableOrViewToggle}
|
on:toggle={handleTableOrViewToggle}
|
||||||
/>
|
/>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
||||||
|
<Modal bind:this={tableTypeModal}>
|
||||||
|
<TypeModal
|
||||||
|
title="Choose how you want to manage rows"
|
||||||
|
types={tableTypes}
|
||||||
|
onConfirm={createTableScreen}
|
||||||
|
onCancel={() => {
|
||||||
|
tableTypeModal.hide()
|
||||||
|
datasourceModal.show()
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Modal>
|
||||||
|
|
||||||
<Modal bind:this={screenDetailsModal}>
|
<Modal bind:this={screenDetailsModal}>
|
||||||
<ScreenDetailsModal onConfirm={createBlankScreen} />
|
<ScreenDetailsModal onConfirm={createBlankScreen} />
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
||||||
<Modal bind:this={formTypeModal}>
|
<Modal bind:this={formTypeModal}>
|
||||||
<FormTypeModal
|
<TypeModal
|
||||||
|
title="Select form type"
|
||||||
|
types={formTypes}
|
||||||
onConfirm={createFormScreen}
|
onConfirm={createFormScreen}
|
||||||
onCancel={() => {
|
onCancel={() => {
|
||||||
formTypeModal.hide()
|
formTypeModal.hide()
|
||||||
|
|
|
@ -1,14 +1,14 @@
|
||||||
<script>
|
<script>
|
||||||
import { ModalContent, Layout, notifications, Body } from "@budibase/bbui"
|
import { Body, ModalContent, Layout, notifications } from "@budibase/bbui"
|
||||||
import { datasources as datasourcesStore } from "stores/builder"
|
import { datasources as datasourcesStore } from "stores/builder"
|
||||||
import ICONS from "components/backend/DatasourceNavigator/icons"
|
import ICONS from "components/backend/DatasourceNavigator/icons"
|
||||||
import { IntegrationNames } from "constants"
|
import { IntegrationNames } from "constants"
|
||||||
import { createEventDispatcher, onMount } from "svelte"
|
import { createEventDispatcher, onMount } from "svelte"
|
||||||
import TableOrViewOption from "./TableOrViewOption.svelte"
|
import TableOrViewOption from "./TableOrViewOption.svelte"
|
||||||
|
import * as format from "helpers/data/format"
|
||||||
|
|
||||||
export let onConfirm
|
export let onConfirm
|
||||||
export let selectedTablesAndViews
|
export let selectedTablesAndViews
|
||||||
export let permissions
|
|
||||||
|
|
||||||
const dispatch = createEventDispatcher()
|
const dispatch = createEventDispatcher()
|
||||||
|
|
||||||
|
@ -21,38 +21,37 @@
|
||||||
icon: "Remove",
|
icon: "Remove",
|
||||||
name: view.name,
|
name: view.name,
|
||||||
id: view.id,
|
id: view.id,
|
||||||
clientData: {
|
tableSelectFormat: format.tableSelect.viewV2(view),
|
||||||
...view,
|
datasourceSelectFormat: format.datasourceSelect.viewV2(view),
|
||||||
type: "viewV2",
|
|
||||||
label: view.name,
|
|
||||||
},
|
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
const getTablesAndViews = datasource => {
|
const getTablesAndViews = datasource => {
|
||||||
let tablesAndViews = []
|
let tablesAndViews = []
|
||||||
const rawTables = Array.isArray(datasource.entities)
|
const tables = Array.isArray(datasource.entities)
|
||||||
? datasource.entities
|
? datasource.entities
|
||||||
: Object.values(datasource.entities ?? {})
|
: Object.values(datasource.entities ?? {})
|
||||||
|
|
||||||
for (const rawTable of rawTables) {
|
for (const table of tables) {
|
||||||
if (rawTable._id === "ta_users") {
|
if (table._id === "ta_users") {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
const table = {
|
const formattedTable = {
|
||||||
icon: "Table",
|
icon: "Table",
|
||||||
name: rawTable.name,
|
name: table.name,
|
||||||
id: rawTable._id,
|
id: table._id,
|
||||||
clientData: {
|
tableSelectFormat: format.tableSelect.table(table),
|
||||||
...rawTable,
|
datasourceSelectFormat: format.datasourceSelect.table(
|
||||||
label: rawTable.name,
|
table,
|
||||||
tableId: rawTable._id,
|
$datasourcesStore.list
|
||||||
type: "table",
|
),
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
tablesAndViews = tablesAndViews.concat([table, ...getViews(rawTable)])
|
tablesAndViews = tablesAndViews.concat([
|
||||||
|
formattedTable,
|
||||||
|
...getViews(table),
|
||||||
|
])
|
||||||
}
|
}
|
||||||
|
|
||||||
return tablesAndViews
|
return tablesAndViews
|
||||||
|
@ -96,60 +95,76 @@
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<span>
|
<ModalContent
|
||||||
<ModalContent
|
title="Autogenerated screens"
|
||||||
title="Autogenerated screens"
|
confirmText="Next"
|
||||||
confirmText="Confirm"
|
cancelText="Cancel"
|
||||||
cancelText="Back"
|
{onConfirm}
|
||||||
{onConfirm}
|
disabled={!selectedTablesAndViews.length}
|
||||||
disabled={!selectedTablesAndViews.length}
|
size="L"
|
||||||
size="L"
|
>
|
||||||
>
|
<Body size="S">
|
||||||
<Body size="S">
|
Select which datasources you would like to use to create your screens
|
||||||
Select which datasources you would like to use to create your screens
|
</Body>
|
||||||
</Body>
|
<Layout noPadding gap="S">
|
||||||
<Layout noPadding gap="S">
|
{#each datasources as datasource}
|
||||||
{#each datasources as datasource}
|
<div class="datasource">
|
||||||
<div class="data-source-wrap">
|
<div class="header">
|
||||||
<div class="data-source-header">
|
<svelte:component
|
||||||
<svelte:component
|
this={datasource.iconComponent}
|
||||||
this={datasource.iconComponent}
|
height="18"
|
||||||
height="24"
|
width="18"
|
||||||
width="24"
|
/>
|
||||||
/>
|
<h2>{datasource.name}</h2>
|
||||||
<div class="data-source-name">{datasource.name}</div>
|
|
||||||
</div>
|
|
||||||
<!-- List all tables -->
|
|
||||||
{#each datasource.tablesAndViews as tableOrView}
|
|
||||||
{@const selected = selectedTablesAndViews.some(
|
|
||||||
selected => selected.id === tableOrView.id
|
|
||||||
)}
|
|
||||||
<TableOrViewOption
|
|
||||||
roles={permissions[tableOrView.id]}
|
|
||||||
on:click={() => toggleSelection(tableOrView)}
|
|
||||||
{selected}
|
|
||||||
{tableOrView}
|
|
||||||
/>
|
|
||||||
{/each}
|
|
||||||
</div>
|
</div>
|
||||||
{/each}
|
<!-- List all tables -->
|
||||||
</Layout>
|
{#each datasource.tablesAndViews as tableOrView}
|
||||||
</ModalContent>
|
{@const selected = selectedTablesAndViews.some(
|
||||||
</span>
|
selected => selected.id === tableOrView.id
|
||||||
|
)}
|
||||||
|
<TableOrViewOption
|
||||||
|
on:click={() => toggleSelection(tableOrView)}
|
||||||
|
{selected}
|
||||||
|
{tableOrView}
|
||||||
|
/>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
</Layout>
|
||||||
|
</ModalContent>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.data-source-wrap {
|
.datasource {
|
||||||
padding-bottom: var(--spectrum-alias-item-padding-s);
|
padding-bottom: 15px;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
grid-gap: var(--spacing-s);
|
grid-gap: var(--spacing-s);
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
}
|
}
|
||||||
.data-source-header {
|
|
||||||
|
.datasource:last-child {
|
||||||
|
padding-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: var(--spacing-m);
|
padding-bottom: var(--spacing-m);
|
||||||
padding-bottom: var(--spacing-xs);
|
}
|
||||||
|
|
||||||
|
.header :global(svg) {
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header h2 {
|
||||||
|
padding-top: 1px;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 400;
|
||||||
|
margin: 0;
|
||||||
|
margin-left: 10px;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -1,115 +0,0 @@
|
||||||
<script>
|
|
||||||
import { ModalContent, Layout, Body, Icon } from "@budibase/bbui"
|
|
||||||
|
|
||||||
let type = null
|
|
||||||
|
|
||||||
export let onCancel = () => {}
|
|
||||||
export let onConfirm = () => {}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<span>
|
|
||||||
<ModalContent
|
|
||||||
title="Select form type"
|
|
||||||
confirmText="Done"
|
|
||||||
cancelText="Back"
|
|
||||||
onConfirm={() => onConfirm(type)}
|
|
||||||
{onCancel}
|
|
||||||
disabled={!type}
|
|
||||||
size="L"
|
|
||||||
>
|
|
||||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
|
||||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
|
||||||
<Layout noPadding gap="S">
|
|
||||||
<div
|
|
||||||
class="form-type"
|
|
||||||
class:selected={type === "Create"}
|
|
||||||
on:click={() => (type = "Create")}
|
|
||||||
>
|
|
||||||
<div class="form-type-wrap">
|
|
||||||
<div class="form-type-content">
|
|
||||||
<Body noPadding>Create a new row</Body>
|
|
||||||
<Body size="S">
|
|
||||||
For capturing and storing new data from your users
|
|
||||||
</Body>
|
|
||||||
</div>
|
|
||||||
{#if type === "Create"}
|
|
||||||
<span class="form-type-check">
|
|
||||||
<Icon size="S" name="CheckmarkCircle" />
|
|
||||||
</span>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="form-type"
|
|
||||||
class:selected={type === "Update"}
|
|
||||||
on:click={() => (type = "Update")}
|
|
||||||
>
|
|
||||||
<div class="form-type-wrap">
|
|
||||||
<div class="form-type-content">
|
|
||||||
<Body noPadding>Update an existing row</Body>
|
|
||||||
<Body size="S">For viewing and updating existing data</Body>
|
|
||||||
</div>
|
|
||||||
{#if type === "Update"}
|
|
||||||
<span class="form-type-check">
|
|
||||||
<Icon size="S" name="CheckmarkCircle" />
|
|
||||||
</span>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="form-type"
|
|
||||||
class:selected={type === "View"}
|
|
||||||
on:click={() => (type = "View")}
|
|
||||||
>
|
|
||||||
<div class="form-type-wrap">
|
|
||||||
<div class="form-type-content">
|
|
||||||
<Body noPadding>View an existing row</Body>
|
|
||||||
<Body size="S">For a read only view of your data</Body>
|
|
||||||
</div>
|
|
||||||
{#if type === "View"}
|
|
||||||
<span class="form-type-check">
|
|
||||||
<Icon size="S" name="CheckmarkCircle" />
|
|
||||||
</span>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Layout>
|
|
||||||
</ModalContent>
|
|
||||||
</span>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
.form-type {
|
|
||||||
cursor: pointer;
|
|
||||||
gap: var(--spacing-s);
|
|
||||||
padding: var(--spacing-m) var(--spacing-xl);
|
|
||||||
background: var(--spectrum-alias-background-color-secondary);
|
|
||||||
transition: 0.3s all;
|
|
||||||
border: 1px solid var(--spectrum-global-color-gray-300);
|
|
||||||
border-radius: 4px;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
.selected,
|
|
||||||
.form-type:hover {
|
|
||||||
background: var(--spectrum-alias-background-color-tertiary);
|
|
||||||
}
|
|
||||||
.form-type-wrap {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
}
|
|
||||||
.form-type :global(p:nth-child(2)) {
|
|
||||||
color: var(--grey-6);
|
|
||||||
}
|
|
||||||
.form-type-check {
|
|
||||||
margin-left: auto;
|
|
||||||
}
|
|
||||||
.form-type-check :global(.spectrum-Icon) {
|
|
||||||
color: var(--spectrum-global-color-green-600);
|
|
||||||
}
|
|
||||||
.form-type-content {
|
|
||||||
gap: var(--spacing-s);
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
</style>
|
|
|
@ -1,49 +1,14 @@
|
||||||
<script>
|
<script>
|
||||||
import { Icon, AbsTooltip } from "@budibase/bbui"
|
import { Icon } from "@budibase/bbui"
|
||||||
import RoleIcon from "components/common/RoleIcon.svelte"
|
|
||||||
|
|
||||||
export let tableOrView
|
export let tableOrView
|
||||||
export let roles
|
|
||||||
export let selected = false
|
export let selected = false
|
||||||
|
|
||||||
$: hideRoles = roles == undefined || roles?.loading
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||||
<div role="button" tabindex="0" class="datasource" class:selected on:click>
|
<div role="button" tabindex="0" class="datasource" class:selected on:click>
|
||||||
<div class="content">
|
<Icon name={tableOrView.icon} />
|
||||||
<Icon name={tableOrView.icon} />
|
<span>{tableOrView.name}</span>
|
||||||
<span>{tableOrView.name}</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class:hideRoles class="roles">
|
|
||||||
<AbsTooltip
|
|
||||||
type="info"
|
|
||||||
text={`Screens that only read data will be generated with access "${roles?.read?.toLowerCase()}"`}
|
|
||||||
>
|
|
||||||
<div class="role">
|
|
||||||
<span>read</span>
|
|
||||||
<RoleIcon
|
|
||||||
size="XS"
|
|
||||||
id={roles?.read}
|
|
||||||
disabled={roles?.loading !== false}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</AbsTooltip>
|
|
||||||
<AbsTooltip
|
|
||||||
type="info"
|
|
||||||
text={`Screens that write data will be generated with access "${roles?.write?.toLowerCase()}"`}
|
|
||||||
>
|
|
||||||
<div class="role">
|
|
||||||
<span>write</span>
|
|
||||||
<RoleIcon
|
|
||||||
size="XS"
|
|
||||||
id={roles?.write}
|
|
||||||
disabled={roles?.loading !== false}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</AbsTooltip>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
@ -52,18 +17,8 @@
|
||||||
border: 1px solid var(--spectrum-global-color-gray-300);
|
border: 1px solid var(--spectrum-global-color-gray-300);
|
||||||
transition: 160ms all;
|
transition: 160ms all;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
user-select: none;
|
user-select: none;
|
||||||
background-color: var(--background);
|
|
||||||
}
|
|
||||||
|
|
||||||
.datasource :global(svg) {
|
|
||||||
transition: 160ms all;
|
|
||||||
color: var(--spectrum-global-color-gray-600);
|
|
||||||
}
|
|
||||||
|
|
||||||
.content {
|
|
||||||
padding: var(--spectrum-alias-item-padding-s);
|
padding: var(--spectrum-alias-item-padding-s);
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
@ -71,7 +26,12 @@
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.content span {
|
.datasource :global(svg) {
|
||||||
|
transition: 160ms all;
|
||||||
|
color: var(--spectrum-global-color-gray-600);
|
||||||
|
}
|
||||||
|
|
||||||
|
.datasource span {
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
|
@ -84,29 +44,4 @@
|
||||||
.selected {
|
.selected {
|
||||||
border: 1px solid var(--blue) !important;
|
border: 1px solid var(--blue) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.roles {
|
|
||||||
margin-left: auto;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: end;
|
|
||||||
padding-right: var(--spectrum-alias-item-padding-s);
|
|
||||||
opacity: 0.5;
|
|
||||||
transition: opacity 160ms;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hideRoles {
|
|
||||||
opacity: 0;
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.role {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.role span {
|
|
||||||
font-size: 11px;
|
|
||||||
margin-right: 5px;
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -0,0 +1,82 @@
|
||||||
|
<script>
|
||||||
|
import { ModalContent, Layout, Body } from "@budibase/bbui"
|
||||||
|
|
||||||
|
let selectedType = null
|
||||||
|
|
||||||
|
export let title
|
||||||
|
export let types
|
||||||
|
export let onCancel = () => {}
|
||||||
|
export let onConfirm = () => {}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<ModalContent
|
||||||
|
{title}
|
||||||
|
confirmText="Done"
|
||||||
|
cancelText="Back"
|
||||||
|
onConfirm={() => onConfirm(selectedType)}
|
||||||
|
{onCancel}
|
||||||
|
disabled={!selectedType}
|
||||||
|
size="L"
|
||||||
|
>
|
||||||
|
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||||
|
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||||
|
<Layout noPadding gap="S">
|
||||||
|
{#each types as type}
|
||||||
|
<div
|
||||||
|
class="type"
|
||||||
|
class:selected={selectedType === type.id}
|
||||||
|
on:click={() => (selectedType = type.id)}
|
||||||
|
>
|
||||||
|
<div class="image">
|
||||||
|
<img alt={type.img.alt} src={type.img.src} />
|
||||||
|
</div>
|
||||||
|
<div class="typeContent">
|
||||||
|
<Body noPadding>{type.title}</Body>
|
||||||
|
<Body size="S">{type.description}</Body>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
</Layout>
|
||||||
|
</ModalContent>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.type {
|
||||||
|
cursor: pointer;
|
||||||
|
gap: var(--spacing-s);
|
||||||
|
background: var(--spectrum-alias-background-color-secondary);
|
||||||
|
transition: 0.3s all;
|
||||||
|
border: 1px solid var(--spectrum-global-color-gray-300);
|
||||||
|
border-radius: 4px;
|
||||||
|
display: flex;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.type:hover {
|
||||||
|
border: 1px solid var(--grey-5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.type.selected {
|
||||||
|
border: 1px solid var(--blue);
|
||||||
|
}
|
||||||
|
.type :global(p:nth-child(2)) {
|
||||||
|
color: var(--grey-6);
|
||||||
|
}
|
||||||
|
.typeContent {
|
||||||
|
box-sizing: border-box;
|
||||||
|
padding: var(--spacing-m) var(--spacing-xl);
|
||||||
|
flex-grow: 1;
|
||||||
|
gap: var(--spacing-s);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image {
|
||||||
|
min-width: 133px;
|
||||||
|
height: 73px;
|
||||||
|
background-color: var(--grey-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.image img {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,35 @@
|
||||||
|
import formView from "./images/formView.svg"
|
||||||
|
import formUpdate from "./images/formUpdate.svg"
|
||||||
|
import formCreate from "./images/formCreate.svg"
|
||||||
|
|
||||||
|
const tableTypes = [
|
||||||
|
{
|
||||||
|
id: "create",
|
||||||
|
img: {
|
||||||
|
alt: "A form containing new data",
|
||||||
|
src: formCreate,
|
||||||
|
},
|
||||||
|
title: "Create a new row",
|
||||||
|
description: "For capturing and storing new data from your users",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "update",
|
||||||
|
img: {
|
||||||
|
alt: "A form containing edited data",
|
||||||
|
src: formUpdate,
|
||||||
|
},
|
||||||
|
title: "Update an existing row",
|
||||||
|
description: "For viewing and updating existing data",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "view",
|
||||||
|
img: {
|
||||||
|
alt: "A form containing read-only data",
|
||||||
|
src: formView,
|
||||||
|
},
|
||||||
|
title: "View an existing row",
|
||||||
|
description: "For a read only view of your data",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
export default tableTypes
|
Before Width: | Height: | Size: 32 KiB |
|
@ -0,0 +1,15 @@
|
||||||
|
<svg width="118" height="65" viewBox="0 0 118 65" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<rect width="118" height="65" fill="#34BB84"/>
|
||||||
|
<rect width="118" height="65" fill="#34BB84"/>
|
||||||
|
<mask id="path-3-inside-1_51_2" fill="white">
|
||||||
|
<path d="M22 12H94V65H22V12Z"/>
|
||||||
|
</mask>
|
||||||
|
<path d="M22 12H94V65H22V12Z" fill="url(#paint0_linear_51_2)"/>
|
||||||
|
<path d="M22 12V11H21V12H22ZM94 12H95V11H94V12ZM22 13H94V11H22V13ZM93 12V65H95V12H93ZM23 65V12H21V65H23Z" fill="white" fill-opacity="0.2" mask="url(#path-3-inside-1_51_2)"/>
|
||||||
|
<defs>
|
||||||
|
<linearGradient id="paint0_linear_51_2" x1="22" y1="12" x2="92.4561" y2="66.9785" gradientUnits="userSpaceOnUse">
|
||||||
|
<stop stop-color="white" stop-opacity="0.8"/>
|
||||||
|
<stop offset="1" stop-color="white" stop-opacity="0"/>
|
||||||
|
</linearGradient>
|
||||||
|
</defs>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 771 B |
Before Width: | Height: | Size: 22 KiB |
|
@ -0,0 +1,20 @@
|
||||||
|
<svg width="118" height="65" viewBox="0 0 118 65" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<rect width="118" height="65" fill="#FD4F01"/>
|
||||||
|
<rect width="118" height="65" fill="#FD4F01"/>
|
||||||
|
<mask id="path-3-inside-1_49_949" fill="white">
|
||||||
|
<path d="M22 12H94V65H22V12Z"/>
|
||||||
|
</mask>
|
||||||
|
<path d="M22 12H94V65H22V12Z" fill="url(#paint0_linear_49_949)"/>
|
||||||
|
<path d="M22 12V11H21V12H22ZM94 12H95V11H94V12ZM22 13H94V11H22V13ZM93 12V65H95V12H93ZM23 65V12H21V65H23Z" fill="white" fill-opacity="0.2" mask="url(#path-3-inside-1_49_949)"/>
|
||||||
|
<rect x="46" y="29" width="15" height="4" fill="white" fill-opacity="0.97"/>
|
||||||
|
<rect x="46" y="45" width="15" height="4" fill="white" fill-opacity="0.97"/>
|
||||||
|
<rect x="46" y="35" width="25" height="6" fill="white" fill-opacity="0.97"/>
|
||||||
|
<rect x="46" y="51" width="25" height="6" fill="white" fill-opacity="0.97"/>
|
||||||
|
<path d="M46 21H71V25H46V21Z" fill="white" fill-opacity="0.97"/>
|
||||||
|
<defs>
|
||||||
|
<linearGradient id="paint0_linear_49_949" x1="22" y1="12" x2="92.4561" y2="66.9785" gradientUnits="userSpaceOnUse">
|
||||||
|
<stop stop-color="white" stop-opacity="0.6"/>
|
||||||
|
<stop offset="1" stop-color="white" stop-opacity="0"/>
|
||||||
|
</linearGradient>
|
||||||
|
</defs>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 1.1 KiB |
|
@ -0,0 +1,20 @@
|
||||||
|
<svg width="118" height="65" viewBox="0 0 118 65" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<rect width="118" height="65" fill="#FD4F01"/>
|
||||||
|
<rect width="118" height="65" fill="#FD4F01"/>
|
||||||
|
<mask id="path-3-inside-1_49_940" fill="white">
|
||||||
|
<path d="M22 12H94V65H22V12Z"/>
|
||||||
|
</mask>
|
||||||
|
<path d="M22 12H94V65H22V12Z" fill="url(#paint0_linear_49_940)"/>
|
||||||
|
<path d="M22 12V11H21V12H22ZM94 12H95V11H94V12ZM22 13H94V11H22V13ZM93 12V65H95V12H93ZM23 65V12H21V65H23Z" fill="white" fill-opacity="0.2" mask="url(#path-3-inside-1_49_940)"/>
|
||||||
|
<rect x="46" y="29" width="15" height="4" fill="white" fill-opacity="0.97"/>
|
||||||
|
<rect x="46" y="45" width="15" height="4" fill="white" fill-opacity="0.5"/>
|
||||||
|
<rect x="46" y="35" width="25" height="6" fill="white" fill-opacity="0.97"/>
|
||||||
|
<path d="M46 21H71V25H46V21Z" fill="white" fill-opacity="0.5"/>
|
||||||
|
<rect x="46" y="51" width="25" height="6" fill="white" fill-opacity="0.5"/>
|
||||||
|
<defs>
|
||||||
|
<linearGradient id="paint0_linear_49_940" x1="22" y1="12" x2="92.4561" y2="66.9785" gradientUnits="userSpaceOnUse">
|
||||||
|
<stop stop-color="white" stop-opacity="0.6"/>
|
||||||
|
<stop offset="1" stop-color="white" stop-opacity="0"/>
|
||||||
|
</linearGradient>
|
||||||
|
</defs>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 1.1 KiB |
|
@ -0,0 +1,20 @@
|
||||||
|
<svg width="118" height="65" viewBox="0 0 118 65" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<rect width="118" height="65" fill="#FD4F01"/>
|
||||||
|
<rect width="118" height="65" fill="#FD4F01"/>
|
||||||
|
<mask id="path-3-inside-1_49_931" fill="white">
|
||||||
|
<path d="M22 12H94V65H22V12Z"/>
|
||||||
|
</mask>
|
||||||
|
<path d="M22 12H94V65H22V12Z" fill="url(#paint0_linear_49_931)"/>
|
||||||
|
<path d="M22 12V11H21V12H22ZM94 12H95V11H94V12ZM22 13H94V11H22V13ZM93 12V65H95V12H93ZM23 65V12H21V65H23Z" fill="white" fill-opacity="0.2" mask="url(#path-3-inside-1_49_931)"/>
|
||||||
|
<rect x="46" y="29" width="15" height="4" fill="white" fill-opacity="0.5"/>
|
||||||
|
<rect x="46" y="45" width="15" height="4" fill="white" fill-opacity="0.5"/>
|
||||||
|
<rect x="46" y="35" width="25" height="6" fill="white" fill-opacity="0.5"/>
|
||||||
|
<path d="M46 21H71V25H46V21Z" fill="white" fill-opacity="0.5"/>
|
||||||
|
<rect x="46" y="51" width="25" height="6" fill="white" fill-opacity="0.5"/>
|
||||||
|
<defs>
|
||||||
|
<linearGradient id="paint0_linear_49_931" x1="22" y1="12" x2="92.4561" y2="66.9785" gradientUnits="userSpaceOnUse">
|
||||||
|
<stop stop-color="white" stop-opacity="0.6"/>
|
||||||
|
<stop offset="1" stop-color="white" stop-opacity="0"/>
|
||||||
|
</linearGradient>
|
||||||
|
</defs>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 1.1 KiB |
Before Width: | Height: | Size: 24 KiB |
Before Width: | Height: | Size: 26 KiB |
Before Width: | Height: | Size: 29 KiB |
Before Width: | Height: | Size: 22 KiB |
|
@ -0,0 +1,44 @@
|
||||||
|
<svg width="118" height="65" viewBox="0 0 118 65" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<rect width="118" height="65" fill="#765FFE"/>
|
||||||
|
<mask id="path-2-inside-1_4_100" fill="white">
|
||||||
|
<path d="M22 12H94V65H22V12Z"/>
|
||||||
|
</mask>
|
||||||
|
<path d="M22 12H94V65H22V12Z" fill="url(#paint0_linear_4_100)"/>
|
||||||
|
<path d="M22 12V11H21V12H22ZM94 12H95V11H94V12ZM22 13H94V11H22V13ZM93 12V65H95V12H93ZM23 65V12H21V65H23Z" fill="white" fill-opacity="0.2" mask="url(#path-2-inside-1_4_100)"/>
|
||||||
|
<path d="M35.6901 23.0003H44.8169V28.0003H35.6901V23.0003Z" fill="white" fill-opacity="0.9"/>
|
||||||
|
<path d="M35.6901 28.5004H44.8169V33.5004H35.6901V28.5004Z" fill="white" fill-opacity="0.5"/>
|
||||||
|
<path d="M35.6901 34H44.8169V39H35.6901V34Z" fill="white" fill-opacity="0.5"/>
|
||||||
|
<path d="M35.6901 39.5002H44.8169V44.5002H35.6901V39.5002Z" fill="white" fill-opacity="0.5"/>
|
||||||
|
<path d="M35.6901 45.0003H44.8169V50.0003H35.6901V45.0003Z" fill="white" fill-opacity="0.5"/>
|
||||||
|
<path d="M35.6901 50.5004H44.8169V55.5004H35.6901V50.5004Z" fill="white" fill-opacity="0.5"/>
|
||||||
|
<path d="M45.3234 23.0003H54.4502V28.0003H45.3234V23.0003Z" fill="white" fill-opacity="0.9"/>
|
||||||
|
<path d="M45.3234 28.5004H54.4502V33.5004H45.3234V28.5004Z" fill="white" fill-opacity="0.5"/>
|
||||||
|
<path d="M45.3234 34H54.4502V39H45.3234V34Z" fill="white" fill-opacity="0.5"/>
|
||||||
|
<path d="M45.3234 39.5002H54.4502V44.5002H45.3234V39.5002Z" fill="white" fill-opacity="0.5"/>
|
||||||
|
<path d="M45.3234 45.0003H54.4502V50.0003H45.3234V45.0003Z" fill="white" fill-opacity="0.5"/>
|
||||||
|
<path d="M45.3234 50.5004H54.4502V55.5004H45.3234V50.5004Z" fill="white" fill-opacity="0.5"/>
|
||||||
|
<path d="M54.9576 23.0003H64.0844V28.0003H54.9576V23.0003Z" fill="white" fill-opacity="0.9"/>
|
||||||
|
<path d="M54.9576 28.5004H64.0844V33.5004H54.9576V28.5004Z" fill="white" fill-opacity="0.5"/>
|
||||||
|
<path d="M54.9576 34H64.0844V39H54.9576V34Z" fill="white" fill-opacity="0.5"/>
|
||||||
|
<path d="M54.9576 39.5002H64.0844V44.5002H54.9576V39.5002Z" fill="white" fill-opacity="0.5"/>
|
||||||
|
<path d="M54.9576 45.0003H64.0844V50.0003H54.9576V45.0003Z" fill="white" fill-opacity="0.5"/>
|
||||||
|
<path d="M54.9576 50.5004H64.0844V55.5004H54.9576V50.5004Z" fill="white" fill-opacity="0.5"/>
|
||||||
|
<path d="M64.5915 23.0003H73.7183V28.0003H64.5915V23.0003Z" fill="white" fill-opacity="0.9"/>
|
||||||
|
<path d="M64.5915 28.5004H73.7183V33.5004H64.5915V28.5004Z" fill="white" fill-opacity="0.5"/>
|
||||||
|
<path d="M64.5915 34H73.7183V39H64.5915V34Z" fill="white" fill-opacity="0.5"/>
|
||||||
|
<path d="M64.5915 39.5002H73.7183V44.5002H64.5915V39.5002Z" fill="white" fill-opacity="0.5"/>
|
||||||
|
<path d="M64.5915 45.0003H73.7183V50.0003H64.5915V45.0003Z" fill="white" fill-opacity="0.5"/>
|
||||||
|
<path d="M64.5915 50.5004H73.7183V55.5004H64.5915V50.5004Z" fill="white" fill-opacity="0.5"/>
|
||||||
|
<path d="M74.2253 23.0003H83.3521V28.0003H74.2253V23.0003Z" fill="white" fill-opacity="0.9"/>
|
||||||
|
<path d="M74.2253 28.5004H83.3521V33.5004H74.2253V28.5004Z" fill="white" fill-opacity="0.5"/>
|
||||||
|
<path d="M74.2253 34H83.3521V39H74.2253V34Z" fill="white" fill-opacity="0.5"/>
|
||||||
|
<path d="M74.2253 39.5002H83.3521V44.5002H74.2253V39.5002Z" fill="white" fill-opacity="0.5"/>
|
||||||
|
<path d="M74.2253 45.0003H83.3521V50.0003H74.2253V45.0003Z" fill="white" fill-opacity="0.5"/>
|
||||||
|
<path d="M74.2253 50.5004H83.3521V55.5004H74.2253V50.5004Z" fill="white" fill-opacity="0.5"/>
|
||||||
|
<defs>
|
||||||
|
<linearGradient id="paint0_linear_4_100" x1="22" y1="12" x2="92.4561" y2="66.9785" gradientUnits="userSpaceOnUse">
|
||||||
|
<stop stop-color="white" stop-opacity="0.6"/>
|
||||||
|
<stop offset="1" stop-color="white" stop-opacity="0"/>
|
||||||
|
</linearGradient>
|
||||||
|
</defs>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 3.4 KiB |
|
@ -0,0 +1,28 @@
|
||||||
|
<svg width="118" height="65" viewBox="0 0 118 65" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<rect width="118" height="65" fill="#765FFE"/>
|
||||||
|
<mask id="path-2-inside-1_4_138" fill="white">
|
||||||
|
<path d="M22 12H94V65H22V12Z"/>
|
||||||
|
</mask>
|
||||||
|
<path d="M22 12H94V65H22V12Z" fill="url(#paint0_linear_4_138)"/>
|
||||||
|
<path d="M22 12V11H21V12H22ZM94 12H95V11H94V12ZM22 13H94V11H22V13ZM93 12V65H95V12H93ZM23 65V12H21V65H23Z" fill="white" fill-opacity="0.2" mask="url(#path-2-inside-1_4_138)"/>
|
||||||
|
<g filter="url(#filter0_d_4_138)">
|
||||||
|
<rect x="42" y="17" width="33" height="44" fill="white" fill-opacity="0.4" shape-rendering="crispEdges"/>
|
||||||
|
<rect x="42.5" y="17.5" width="32" height="43" stroke="white" stroke-opacity="0.3" shape-rendering="crispEdges"/>
|
||||||
|
</g>
|
||||||
|
<defs>
|
||||||
|
<filter id="filter0_d_4_138" x="39" y="15" width="39" height="50" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||||
|
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||||
|
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||||
|
<feOffset dy="1"/>
|
||||||
|
<feGaussianBlur stdDeviation="1.5"/>
|
||||||
|
<feComposite in2="hardAlpha" operator="out"/>
|
||||||
|
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.2 0"/>
|
||||||
|
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_4_138"/>
|
||||||
|
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_4_138" result="shape"/>
|
||||||
|
</filter>
|
||||||
|
<linearGradient id="paint0_linear_4_138" x1="22" y1="12" x2="92.4561" y2="66.9785" gradientUnits="userSpaceOnUse">
|
||||||
|
<stop stop-color="white" stop-opacity="0.6"/>
|
||||||
|
<stop offset="1" stop-color="white" stop-opacity="0"/>
|
||||||
|
</linearGradient>
|
||||||
|
</defs>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 1.6 KiB |
|
@ -0,0 +1,20 @@
|
||||||
|
<svg width="118" height="65" viewBox="0 0 118 65" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<rect width="118" height="65" fill="#765FFE"/>
|
||||||
|
<rect width="118" height="65" fill="#765FFE"/>
|
||||||
|
<mask id="path-3-inside-1_56_20" fill="white">
|
||||||
|
<path d="M22 12H94V65H22V12Z"/>
|
||||||
|
</mask>
|
||||||
|
<path d="M22 12H94V65H22V12Z" fill="url(#paint0_linear_56_20)"/>
|
||||||
|
<path d="M22 12V11H21V12H22ZM94 12H95V11H94V12ZM22 13H94V11H22V13ZM93 12V65H95V12H93ZM23 65V12H21V65H23Z" fill="white" fill-opacity="0.2" mask="url(#path-3-inside-1_56_20)"/>
|
||||||
|
<rect x="46" y="29" width="15" height="4" fill="white" fill-opacity="0.97"/>
|
||||||
|
<rect x="46" y="45" width="15" height="4" fill="white" fill-opacity="0.5"/>
|
||||||
|
<rect x="46" y="35" width="25" height="6" fill="white" fill-opacity="0.97"/>
|
||||||
|
<path d="M46 21H71V25H46V21Z" fill="white" fill-opacity="0.5"/>
|
||||||
|
<rect x="46" y="51" width="25" height="6" fill="white" fill-opacity="0.5"/>
|
||||||
|
<defs>
|
||||||
|
<linearGradient id="paint0_linear_56_20" x1="22" y1="12" x2="92.4561" y2="66.9785" gradientUnits="userSpaceOnUse">
|
||||||
|
<stop stop-color="white" stop-opacity="0.6"/>
|
||||||
|
<stop offset="1" stop-color="white" stop-opacity="0"/>
|
||||||
|
</linearGradient>
|
||||||
|
</defs>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 1.1 KiB |
|
@ -0,0 +1,28 @@
|
||||||
|
<svg width="118" height="65" viewBox="0 0 118 65" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<rect width="118" height="65" fill="#765FFE"/>
|
||||||
|
<mask id="path-2-inside-1_4_103" fill="white">
|
||||||
|
<path d="M22 12H94V65H22V12Z"/>
|
||||||
|
</mask>
|
||||||
|
<path d="M22 12H94V65H22V12Z" fill="url(#paint0_linear_4_103)"/>
|
||||||
|
<path d="M22 12V11H21V12H22ZM94 12H95V11H94V12ZM22 13H94V11H22V13ZM93 12V65H95V12H93ZM23 65V12H21V65H23Z" fill="white" fill-opacity="0.2" mask="url(#path-2-inside-1_4_103)"/>
|
||||||
|
<g filter="url(#filter0_d_4_103)">
|
||||||
|
<rect x="70" y="17" width="20" height="44" fill="white" fill-opacity="0.4" shape-rendering="crispEdges"/>
|
||||||
|
<rect x="70.5" y="17.5" width="19" height="43" stroke="white" stroke-opacity="0.3" shape-rendering="crispEdges"/>
|
||||||
|
</g>
|
||||||
|
<defs>
|
||||||
|
<filter id="filter0_d_4_103" x="67" y="15" width="26" height="50" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||||
|
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||||
|
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||||
|
<feOffset dy="1"/>
|
||||||
|
<feGaussianBlur stdDeviation="1.5"/>
|
||||||
|
<feComposite in2="hardAlpha" operator="out"/>
|
||||||
|
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.25 0"/>
|
||||||
|
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_4_103"/>
|
||||||
|
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_4_103" result="shape"/>
|
||||||
|
</filter>
|
||||||
|
<linearGradient id="paint0_linear_4_103" x1="22" y1="12" x2="92.4561" y2="66.9785" gradientUnits="userSpaceOnUse">
|
||||||
|
<stop stop-color="white" stop-opacity="0.6"/>
|
||||||
|
<stop offset="1" stop-color="white" stop-opacity="0"/>
|
||||||
|
</linearGradient>
|
||||||
|
</defs>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 1.6 KiB |
|
@ -1,10 +1,9 @@
|
||||||
<script>
|
<script>
|
||||||
import { Body } from "@budibase/bbui"
|
import { Body } from "@budibase/bbui"
|
||||||
import CreationPage from "components/common/CreationPage.svelte"
|
import CreationPage from "components/common/CreationPage.svelte"
|
||||||
import blankImage from "./images/blank.png"
|
import blank from "./images/blank.svg"
|
||||||
import tableInline from "./images/tableInline.png"
|
import table from "./images/tableInline.svg"
|
||||||
import tableDetails from "./images/tableDetails.png"
|
import form from "./images/formUpdate.svg"
|
||||||
import formImage from "./images/form.png"
|
|
||||||
import CreateScreenModal from "./CreateScreenModal.svelte"
|
import CreateScreenModal from "./CreateScreenModal.svelte"
|
||||||
import { screenStore } from "stores/builder"
|
import { screenStore } from "stores/builder"
|
||||||
|
|
||||||
|
@ -30,37 +29,27 @@
|
||||||
<div class="cards">
|
<div class="cards">
|
||||||
<div class="card" on:click={() => createScreenModal.show("blank")}>
|
<div class="card" on:click={() => createScreenModal.show("blank")}>
|
||||||
<div class="image">
|
<div class="image">
|
||||||
<img alt="" src={blankImage} />
|
<img alt="A blank screen" src={blank} />
|
||||||
</div>
|
</div>
|
||||||
<div class="text">
|
<div class="text">
|
||||||
<Body size="S">Blank screen</Body>
|
<Body size="S">Blank</Body>
|
||||||
<Body size="XS">Add an empty blank screen</Body>
|
<Body size="XS">Add an empty blank screen</Body>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="card" on:click={() => createScreenModal.show("grid")}>
|
<div class="card" on:click={() => createScreenModal.show("table")}>
|
||||||
<div class="image">
|
<div class="image">
|
||||||
<img alt="" src={tableInline} />
|
<img alt="A table of data" src={table} />
|
||||||
</div>
|
</div>
|
||||||
<div class="text">
|
<div class="text">
|
||||||
<Body size="S">Table with inline editing</Body>
|
<Body size="S">Table</Body>
|
||||||
<Body size="XS">View, edit and delete rows inline</Body>
|
<Body size="XS">List rows in a table</Body>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="card" on:click={() => createScreenModal.show("gridDetails")}>
|
|
||||||
<div class="image">
|
|
||||||
<img alt="" src={tableDetails} />
|
|
||||||
</div>
|
|
||||||
<div class="text">
|
|
||||||
<Body size="S">Table with details panel</Body>
|
|
||||||
<Body size="XS">Manage your row details in a side panel</Body>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="card" on:click={() => createScreenModal.show("form")}>
|
<div class="card" on:click={() => createScreenModal.show("form")}>
|
||||||
<div class="image">
|
<div class="image">
|
||||||
<img alt="" src={formImage} />
|
<img alt="A form containing data" src={form} />
|
||||||
</div>
|
</div>
|
||||||
<div class="text">
|
<div class="text">
|
||||||
<Body size="S">Form</Body>
|
<Body size="S">Form</Body>
|
||||||
|
@ -114,8 +103,9 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.card .image {
|
.card .image {
|
||||||
min-height: 130px;
|
|
||||||
min-width: 235px;
|
min-width: 235px;
|
||||||
|
height: 127px;
|
||||||
|
background-color: var(--grey-2);
|
||||||
}
|
}
|
||||||
|
|
||||||
.text {
|
.text {
|
||||||
|
|
|
@ -0,0 +1,45 @@
|
||||||
|
import tableInline from "./images/tableInline.svg"
|
||||||
|
import tableSidePanel from "./images/tableSidePanel.svg"
|
||||||
|
import tableModal from "./images/tableModal.svg"
|
||||||
|
import tableNewScreen from "./images/tableNewScreen.svg"
|
||||||
|
|
||||||
|
const tableTypes = [
|
||||||
|
{
|
||||||
|
id: "inline",
|
||||||
|
img: {
|
||||||
|
alt: "A table of data",
|
||||||
|
src: tableInline,
|
||||||
|
},
|
||||||
|
title: "Inline",
|
||||||
|
description: "Manage data directly on your table",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "sidePanel",
|
||||||
|
img: {
|
||||||
|
alt: "A side panel",
|
||||||
|
src: tableSidePanel,
|
||||||
|
},
|
||||||
|
title: "Side panel",
|
||||||
|
description: "Open row details in a side panel",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "modal",
|
||||||
|
img: {
|
||||||
|
alt: "A modal",
|
||||||
|
src: tableModal,
|
||||||
|
},
|
||||||
|
title: "Modal",
|
||||||
|
description: "Open row details in a modal",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "newScreen",
|
||||||
|
img: {
|
||||||
|
alt: "A new screen",
|
||||||
|
src: tableNewScreen,
|
||||||
|
},
|
||||||
|
title: "New screen",
|
||||||
|
description: "View row details on a separate screen",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
export default tableTypes
|
|
@ -2,11 +2,10 @@
|
||||||
import { Button } from "@budibase/bbui"
|
import { Button } from "@budibase/bbui"
|
||||||
import { goto } from "@roxi/routify"
|
import { goto } from "@roxi/routify"
|
||||||
import { auth, admin, licensing } from "stores/portal"
|
import { auth, admin, licensing } from "stores/portal"
|
||||||
import { isEnabled, TENANT_FEATURE_FLAGS } from "helpers/featureFlags"
|
|
||||||
import { sdk } from "@budibase/shared-core"
|
import { sdk } from "@budibase/shared-core"
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if isEnabled(TENANT_FEATURE_FLAGS.LICENSING) && !$licensing.isEnterprisePlan && !$licensing.isEnterpriseTrial}
|
{#if !$licensing.isEnterprisePlan && !$licensing.isEnterpriseTrial}
|
||||||
{#if $admin.cloud && $auth?.user?.accountPortalAccess}
|
{#if $admin.cloud && $auth?.user?.accountPortalAccess}
|
||||||
<Button
|
<Button
|
||||||
cta
|
cta
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { v4 } from "uuid"
|
import { v4 } from "uuid"
|
||||||
import { Component } from "templates/Component"
|
import { Component } from "templates/Component"
|
||||||
import { Screen } from "templates/Screen"
|
import { Screen } from "templates/screenTemplating/Screen"
|
||||||
import { get } from "svelte/store"
|
import { get } from "svelte/store"
|
||||||
import {
|
import {
|
||||||
BUDIBASE_INTERNAL_DB_ID,
|
BUDIBASE_INTERNAL_DB_ID,
|
||||||
|
|
|
@ -3,7 +3,6 @@ import { API } from "api"
|
||||||
import { auth, admin } from "stores/portal"
|
import { auth, admin } from "stores/portal"
|
||||||
import { Constants } from "@budibase/frontend-core"
|
import { Constants } from "@budibase/frontend-core"
|
||||||
import { StripeStatus } from "components/portal/licensing/constants"
|
import { StripeStatus } from "components/portal/licensing/constants"
|
||||||
import { TENANT_FEATURE_FLAGS, isEnabled } from "helpers/featureFlags"
|
|
||||||
import { PlanModel } from "@budibase/types"
|
import { PlanModel } from "@budibase/types"
|
||||||
|
|
||||||
const UNLIMITED = -1
|
const UNLIMITED = -1
|
||||||
|
@ -183,93 +182,91 @@ export const createLicensingStore = () => {
|
||||||
return usersLimitExceeded(userCount, get(store).userLimit)
|
return usersLimitExceeded(userCount, get(store).userLimit)
|
||||||
},
|
},
|
||||||
setUsageMetrics: async () => {
|
setUsageMetrics: async () => {
|
||||||
if (isEnabled(TENANT_FEATURE_FLAGS.LICENSING)) {
|
const usage = get(store).quotaUsage
|
||||||
const usage = get(store).quotaUsage
|
const license = get(auth).user.license
|
||||||
const license = get(auth).user.license
|
const now = new Date()
|
||||||
const now = new Date()
|
|
||||||
|
|
||||||
const getMetrics = (keys, license, quota) => {
|
const getMetrics = (keys, license, quota) => {
|
||||||
if (!license || !quota || !keys) {
|
if (!license || !quota || !keys) {
|
||||||
return {}
|
return {}
|
||||||
}
|
|
||||||
return keys.reduce((acc, key) => {
|
|
||||||
const quotaLimit = license[key].value
|
|
||||||
const quotaUsed = (quota[key] / quotaLimit) * 100
|
|
||||||
acc[key] = quotaLimit > -1 ? Math.floor(quotaUsed) : -1
|
|
||||||
return acc
|
|
||||||
}, {})
|
|
||||||
}
|
}
|
||||||
const monthlyMetrics = getMetrics(
|
return keys.reduce((acc, key) => {
|
||||||
["dayPasses", "queries", "automations"],
|
const quotaLimit = license[key].value
|
||||||
license.quotas.usage.monthly,
|
const quotaUsed = (quota[key] / quotaLimit) * 100
|
||||||
usage.monthly.current
|
acc[key] = quotaLimit > -1 ? Math.floor(quotaUsed) : -1
|
||||||
)
|
return acc
|
||||||
const staticMetrics = getMetrics(
|
}, {})
|
||||||
["apps", "rows"],
|
|
||||||
license.quotas.usage.static,
|
|
||||||
usage.usageQuota
|
|
||||||
)
|
|
||||||
|
|
||||||
const getDaysBetween = (dateStart, dateEnd) => {
|
|
||||||
return dateEnd > dateStart
|
|
||||||
? Math.round(
|
|
||||||
(dateEnd.getTime() - dateStart.getTime()) / oneDayInMilliseconds
|
|
||||||
)
|
|
||||||
: 0
|
|
||||||
}
|
|
||||||
|
|
||||||
const quotaResetDate = new Date(usage.quotaReset)
|
|
||||||
const quotaResetDaysRemaining = getDaysBetween(now, quotaResetDate)
|
|
||||||
|
|
||||||
const accountDowngraded =
|
|
||||||
license?.billing?.subscription?.downgradeAt &&
|
|
||||||
license?.billing?.subscription?.downgradeAt <= now.getTime() &&
|
|
||||||
license?.billing?.subscription?.status === StripeStatus.PAST_DUE &&
|
|
||||||
license?.plan.type === Constants.PlanType.FREE
|
|
||||||
|
|
||||||
const pastDueAtMilliseconds = license?.billing?.subscription?.pastDueAt
|
|
||||||
const downgradeAtMilliseconds =
|
|
||||||
license?.billing?.subscription?.downgradeAt
|
|
||||||
let pastDueDaysRemaining
|
|
||||||
let pastDueEndDate
|
|
||||||
|
|
||||||
if (pastDueAtMilliseconds && downgradeAtMilliseconds) {
|
|
||||||
pastDueEndDate = new Date(downgradeAtMilliseconds)
|
|
||||||
pastDueDaysRemaining = getDaysBetween(
|
|
||||||
new Date(pastDueAtMilliseconds),
|
|
||||||
pastDueEndDate
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const userQuota = license.quotas.usage.static.users
|
|
||||||
const userLimit = userQuota?.value
|
|
||||||
const userCount = usage.usageQuota.users
|
|
||||||
const userLimitReached = usersLimitReached(userCount, userLimit)
|
|
||||||
const userLimitExceeded = usersLimitExceeded(userCount, userLimit)
|
|
||||||
const isCloudAccount = await isCloud()
|
|
||||||
const errUserLimit =
|
|
||||||
isCloudAccount &&
|
|
||||||
license.plan.model === PlanModel.PER_USER &&
|
|
||||||
userLimitExceeded
|
|
||||||
|
|
||||||
store.update(state => {
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
usageMetrics: { ...monthlyMetrics, ...staticMetrics },
|
|
||||||
quotaResetDaysRemaining,
|
|
||||||
quotaResetDate,
|
|
||||||
accountDowngraded,
|
|
||||||
accountPastDue: pastDueAtMilliseconds != null,
|
|
||||||
pastDueEndDate,
|
|
||||||
pastDueDaysRemaining,
|
|
||||||
// user limits
|
|
||||||
userCount,
|
|
||||||
userLimit,
|
|
||||||
userLimitReached,
|
|
||||||
errUserLimit,
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
const monthlyMetrics = getMetrics(
|
||||||
|
["dayPasses", "queries", "automations"],
|
||||||
|
license.quotas.usage.monthly,
|
||||||
|
usage.monthly.current
|
||||||
|
)
|
||||||
|
const staticMetrics = getMetrics(
|
||||||
|
["apps", "rows"],
|
||||||
|
license.quotas.usage.static,
|
||||||
|
usage.usageQuota
|
||||||
|
)
|
||||||
|
|
||||||
|
const getDaysBetween = (dateStart, dateEnd) => {
|
||||||
|
return dateEnd > dateStart
|
||||||
|
? Math.round(
|
||||||
|
(dateEnd.getTime() - dateStart.getTime()) / oneDayInMilliseconds
|
||||||
|
)
|
||||||
|
: 0
|
||||||
|
}
|
||||||
|
|
||||||
|
const quotaResetDate = new Date(usage.quotaReset)
|
||||||
|
const quotaResetDaysRemaining = getDaysBetween(now, quotaResetDate)
|
||||||
|
|
||||||
|
const accountDowngraded =
|
||||||
|
license?.billing?.subscription?.downgradeAt &&
|
||||||
|
license?.billing?.subscription?.downgradeAt <= now.getTime() &&
|
||||||
|
license?.billing?.subscription?.status === StripeStatus.PAST_DUE &&
|
||||||
|
license?.plan.type === Constants.PlanType.FREE
|
||||||
|
|
||||||
|
const pastDueAtMilliseconds = license?.billing?.subscription?.pastDueAt
|
||||||
|
const downgradeAtMilliseconds =
|
||||||
|
license?.billing?.subscription?.downgradeAt
|
||||||
|
let pastDueDaysRemaining
|
||||||
|
let pastDueEndDate
|
||||||
|
|
||||||
|
if (pastDueAtMilliseconds && downgradeAtMilliseconds) {
|
||||||
|
pastDueEndDate = new Date(downgradeAtMilliseconds)
|
||||||
|
pastDueDaysRemaining = getDaysBetween(
|
||||||
|
new Date(pastDueAtMilliseconds),
|
||||||
|
pastDueEndDate
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const userQuota = license.quotas.usage.static.users
|
||||||
|
const userLimit = userQuota?.value
|
||||||
|
const userCount = usage.usageQuota.users
|
||||||
|
const userLimitReached = usersLimitReached(userCount, userLimit)
|
||||||
|
const userLimitExceeded = usersLimitExceeded(userCount, userLimit)
|
||||||
|
const isCloudAccount = await isCloud()
|
||||||
|
const errUserLimit =
|
||||||
|
isCloudAccount &&
|
||||||
|
license.plan.model === PlanModel.PER_USER &&
|
||||||
|
userLimitExceeded
|
||||||
|
|
||||||
|
store.update(state => {
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
usageMetrics: { ...monthlyMetrics, ...staticMetrics },
|
||||||
|
quotaResetDaysRemaining,
|
||||||
|
quotaResetDate,
|
||||||
|
accountDowngraded,
|
||||||
|
accountPastDue: pastDueAtMilliseconds != null,
|
||||||
|
pastDueEndDate,
|
||||||
|
pastDueDaysRemaining,
|
||||||
|
// user limits
|
||||||
|
userCount,
|
||||||
|
userLimit,
|
||||||
|
userLimitReached,
|
||||||
|
errUserLimit,
|
||||||
|
}
|
||||||
|
})
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
import { derived } from "svelte/store"
|
import { derived } from "svelte/store"
|
||||||
import { isEnabled, TENANT_FEATURE_FLAGS } from "helpers/featureFlags"
|
|
||||||
import { admin } from "./admin"
|
import { admin } from "./admin"
|
||||||
import { auth } from "./auth"
|
import { auth } from "./auth"
|
||||||
import { sdk } from "@budibase/shared-core"
|
import { sdk } from "@budibase/shared-core"
|
||||||
|
@ -15,12 +14,10 @@ export const menu = derived([admin, auth], ([$admin, $auth]) => {
|
||||||
href: "/builder/portal/users/users",
|
href: "/builder/portal/users/users",
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
if (isEnabled(TENANT_FEATURE_FLAGS.USER_GROUPS)) {
|
userSubPages.push({
|
||||||
userSubPages.push({
|
title: "Groups",
|
||||||
title: "Groups",
|
href: "/builder/portal/users/groups",
|
||||||
href: "/builder/portal/users/groups",
|
})
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Pages that all devs and admins can access
|
// Pages that all devs and admins can access
|
||||||
let menu = [
|
let menu = [
|
||||||
|
@ -83,50 +80,48 @@ export const menu = derived([admin, auth], ([$admin, $auth]) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add account page
|
// Add account page
|
||||||
if (isEnabled(TENANT_FEATURE_FLAGS.LICENSING)) {
|
let accountSubPages = [
|
||||||
let accountSubPages = [
|
{
|
||||||
{
|
title: "Usage",
|
||||||
title: "Usage",
|
href: "/builder/portal/account/usage",
|
||||||
href: "/builder/portal/account/usage",
|
},
|
||||||
},
|
]
|
||||||
]
|
if (isAdmin) {
|
||||||
if (isAdmin) {
|
accountSubPages.push({
|
||||||
accountSubPages.push({
|
title: "Audit Logs",
|
||||||
title: "Audit Logs",
|
href: "/builder/portal/account/auditLogs",
|
||||||
href: "/builder/portal/account/auditLogs",
|
})
|
||||||
})
|
|
||||||
|
|
||||||
if (!cloud) {
|
if (!cloud) {
|
||||||
accountSubPages.push({
|
|
||||||
title: "System Logs",
|
|
||||||
href: "/builder/portal/account/systemLogs",
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (cloud && user?.accountPortalAccess) {
|
|
||||||
accountSubPages.push({
|
accountSubPages.push({
|
||||||
title: "Upgrade",
|
title: "System Logs",
|
||||||
href: $admin?.accountPortalUrl + "/portal/upgrade",
|
href: "/builder/portal/account/systemLogs",
|
||||||
})
|
|
||||||
} else if (!cloud && isAdmin) {
|
|
||||||
accountSubPages.push({
|
|
||||||
title: "Upgrade",
|
|
||||||
href: "/builder/portal/account/upgrade",
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
// add license check here
|
}
|
||||||
if (user?.accountPortalAccess && user.account.stripeCustomerId) {
|
if (cloud && user?.accountPortalAccess) {
|
||||||
accountSubPages.push({
|
accountSubPages.push({
|
||||||
title: "Billing",
|
title: "Upgrade",
|
||||||
href: $admin?.accountPortalUrl + "/portal/billing",
|
href: $admin?.accountPortalUrl + "/portal/upgrade",
|
||||||
})
|
})
|
||||||
}
|
} else if (!cloud && isAdmin) {
|
||||||
menu.push({
|
accountSubPages.push({
|
||||||
title: "Account",
|
title: "Upgrade",
|
||||||
href: "/builder/portal/account",
|
href: "/builder/portal/account/upgrade",
|
||||||
subPages: accountSubPages,
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
// add license check here
|
||||||
|
if (user?.accountPortalAccess && user.account.stripeCustomerId) {
|
||||||
|
accountSubPages.push({
|
||||||
|
title: "Billing",
|
||||||
|
href: $admin?.accountPortalUrl + "/portal/billing",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
menu.push({
|
||||||
|
title: "Account",
|
||||||
|
href: "/builder/portal/account",
|
||||||
|
subPages: accountSubPages,
|
||||||
|
})
|
||||||
|
|
||||||
return menu
|
return menu
|
||||||
})
|
})
|
||||||
|
|
|
@ -2,11 +2,11 @@ import { Helpers } from "@budibase/bbui"
|
||||||
import { BaseStructure } from "./BaseStructure"
|
import { BaseStructure } from "./BaseStructure"
|
||||||
|
|
||||||
export class Component extends BaseStructure {
|
export class Component extends BaseStructure {
|
||||||
constructor(name) {
|
constructor(name, _id = Helpers.uuid()) {
|
||||||
super(false)
|
super(false)
|
||||||
this._children = []
|
this._children = []
|
||||||
this._json = {
|
this._json = {
|
||||||
_id: Helpers.uuid(),
|
_id,
|
||||||
_component: name,
|
_component: name,
|
||||||
_styles: {
|
_styles: {
|
||||||
normal: {},
|
normal: {},
|
||||||
|
@ -50,4 +50,8 @@ export class Component extends BaseStructure {
|
||||||
this._json.text = text
|
this._json.text = text
|
||||||
return this
|
return this
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getId() {
|
||||||
|
return this._json._id
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,11 +0,0 @@
|
||||||
import { Screen } from "./Screen"
|
|
||||||
|
|
||||||
const blankScreen = route => {
|
|
||||||
return new Screen()
|
|
||||||
.instanceName("New Screen")
|
|
||||||
.customProps({ layout: "grid" })
|
|
||||||
.route(route)
|
|
||||||
.json()
|
|
||||||
}
|
|
||||||
|
|
||||||
export default blankScreen
|
|
|
@ -1,49 +0,0 @@
|
||||||
import { Screen } from "./Screen"
|
|
||||||
import { Component } from "./Component"
|
|
||||||
import sanitizeUrl from "helpers/sanitizeUrl"
|
|
||||||
|
|
||||||
export const FORM_TEMPLATE = "FORM_TEMPLATE"
|
|
||||||
export const formUrl = (tableOrView, actionType) => {
|
|
||||||
if (actionType === "Create") {
|
|
||||||
return sanitizeUrl(`/${tableOrView.name}/new`)
|
|
||||||
} else if (actionType === "Update") {
|
|
||||||
return sanitizeUrl(`/${tableOrView.name}/edit/:id`)
|
|
||||||
} else if (actionType === "View") {
|
|
||||||
return sanitizeUrl(`/${tableOrView.name}/view/:id`)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const getRole = (permissions, actionType) => {
|
|
||||||
if (actionType === "View") {
|
|
||||||
return permissions.read
|
|
||||||
}
|
|
||||||
|
|
||||||
return permissions.write
|
|
||||||
}
|
|
||||||
|
|
||||||
const generateMultistepFormBlock = (tableOrView, actionType) => {
|
|
||||||
const multistepFormBlock = new Component(
|
|
||||||
"@budibase/standard-components/multistepformblock"
|
|
||||||
)
|
|
||||||
multistepFormBlock
|
|
||||||
.customProps({
|
|
||||||
actionType,
|
|
||||||
dataSource: tableOrView.clientData,
|
|
||||||
steps: [{}],
|
|
||||||
rowId: actionType === "new" ? undefined : `{{ url.id }}`,
|
|
||||||
})
|
|
||||||
.instanceName(`${tableOrView.name} - Multistep Form block`)
|
|
||||||
return multistepFormBlock
|
|
||||||
}
|
|
||||||
|
|
||||||
const createScreen = (tableOrView, actionType, permissions) => {
|
|
||||||
return new Screen()
|
|
||||||
.route(formUrl(tableOrView, actionType))
|
|
||||||
.instanceName(`${tableOrView.name} - Form`)
|
|
||||||
.role(getRole(permissions, actionType))
|
|
||||||
.autoTableId(tableOrView.id)
|
|
||||||
.addChild(generateMultistepFormBlock(tableOrView, actionType))
|
|
||||||
.json()
|
|
||||||
}
|
|
||||||
|
|
||||||
export default createScreen
|
|
|
@ -1,30 +0,0 @@
|
||||||
import sanitizeUrl from "helpers/sanitizeUrl"
|
|
||||||
import { Screen } from "./Screen"
|
|
||||||
import { Component } from "./Component"
|
|
||||||
|
|
||||||
const gridUrl = tableOrView => sanitizeUrl(`/${tableOrView.name}`)
|
|
||||||
|
|
||||||
const createScreen = (tableOrView, permissions) => {
|
|
||||||
const heading = new Component("@budibase/standard-components/heading")
|
|
||||||
.instanceName("Table heading")
|
|
||||||
.customProps({
|
|
||||||
text: tableOrView.name,
|
|
||||||
})
|
|
||||||
|
|
||||||
const gridBlock = new Component("@budibase/standard-components/gridblock")
|
|
||||||
.instanceName(`${tableOrView.name} - Table`)
|
|
||||||
.customProps({
|
|
||||||
table: tableOrView.clientData,
|
|
||||||
})
|
|
||||||
|
|
||||||
return new Screen()
|
|
||||||
.route(gridUrl(tableOrView))
|
|
||||||
.instanceName(`${tableOrView.name} - List`)
|
|
||||||
.role(permissions.write)
|
|
||||||
.autoTableId(tableOrView.id)
|
|
||||||
.addChild(heading)
|
|
||||||
.addChild(gridBlock)
|
|
||||||
.json()
|
|
||||||
}
|
|
||||||
|
|
||||||
export default createScreen
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { BaseStructure } from "./BaseStructure"
|
import { BaseStructure } from "../BaseStructure"
|
||||||
import { Helpers } from "@budibase/bbui"
|
import { Helpers } from "@budibase/bbui"
|
||||||
|
|
||||||
export class Screen extends BaseStructure {
|
export class Screen extends BaseStructure {
|
|
@ -0,0 +1,25 @@
|
||||||
|
import { Screen } from "./Screen"
|
||||||
|
import { capitalise } from "helpers"
|
||||||
|
import getValidRoute from "./getValidRoute"
|
||||||
|
import { Roles } from "constants/backend"
|
||||||
|
|
||||||
|
const blank = ({ route, screens }) => {
|
||||||
|
const validRoute = getValidRoute(screens, route, Roles.BASIC)
|
||||||
|
|
||||||
|
const template = new Screen()
|
||||||
|
.instanceName("Blank screen")
|
||||||
|
.customProps({ layout: "grid" })
|
||||||
|
.role(Roles.BASIC)
|
||||||
|
.route(validRoute)
|
||||||
|
.json()
|
||||||
|
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
data: template,
|
||||||
|
navigationLinkLabel:
|
||||||
|
validRoute === "/" ? null : capitalise(validRoute.split("/")[1]),
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
export default blank
|
|
@ -0,0 +1,67 @@
|
||||||
|
import { Screen } from "./Screen"
|
||||||
|
import { Component } from "../Component"
|
||||||
|
import getValidRoute from "./getValidRoute"
|
||||||
|
|
||||||
|
export const getTypeSpecificRoute = (tableOrView, type) => {
|
||||||
|
if (type === "create") {
|
||||||
|
return `/${tableOrView.name}/new`
|
||||||
|
} else if (type === "update") {
|
||||||
|
return `/${tableOrView.name}/edit/:id`
|
||||||
|
} else if (type === "view") {
|
||||||
|
return `/${tableOrView.name}/view/:id`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const getRole = (permissions, type) => {
|
||||||
|
if (type === "view") {
|
||||||
|
return permissions.read
|
||||||
|
}
|
||||||
|
|
||||||
|
return permissions.write
|
||||||
|
}
|
||||||
|
|
||||||
|
const getActionType = type => {
|
||||||
|
if (type === "create") {
|
||||||
|
return "Create"
|
||||||
|
}
|
||||||
|
if (type === "update") {
|
||||||
|
return "Update"
|
||||||
|
}
|
||||||
|
if (type === "view") {
|
||||||
|
return "View"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const form = ({ tableOrView, type, permissions, screens }) => {
|
||||||
|
const typeSpecificRoute = getTypeSpecificRoute(tableOrView, type)
|
||||||
|
const role = getRole(permissions, type)
|
||||||
|
|
||||||
|
const multistepFormBlock = new Component(
|
||||||
|
"@budibase/standard-components/multistepformblock"
|
||||||
|
)
|
||||||
|
.customProps({
|
||||||
|
actionType: getActionType(type),
|
||||||
|
dataSource: tableOrView.tableSelectFormat,
|
||||||
|
steps: [{}],
|
||||||
|
rowId: type === "new" ? undefined : `{{ url.id }}`,
|
||||||
|
})
|
||||||
|
.instanceName(`${tableOrView.name} - Multistep Form block`)
|
||||||
|
|
||||||
|
const template = new Screen()
|
||||||
|
.route(getValidRoute(screens, typeSpecificRoute, role))
|
||||||
|
.instanceName(`${tableOrView.name} - Form`)
|
||||||
|
.role(role)
|
||||||
|
.autoTableId(tableOrView.id)
|
||||||
|
.addChild(multistepFormBlock)
|
||||||
|
.json()
|
||||||
|
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
data: template,
|
||||||
|
navigationLinkLabel:
|
||||||
|
type === "create" ? `Create ${tableOrView.name}` : null,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
export default form
|
|
@ -0,0 +1,35 @@
|
||||||
|
import sanitizeUrl from "helpers/sanitizeUrl"
|
||||||
|
|
||||||
|
const arbitraryMax = 10000
|
||||||
|
|
||||||
|
const isScreenUrlValid = (screens, url, role) => {
|
||||||
|
return !screens.some(
|
||||||
|
screen => screen.routing?.route === url && screen.routing?.roleId === role
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const getValidScreenUrl = (screens, url, role) => {
|
||||||
|
const [firstPathSegment = "", ...restPathSegments] = url
|
||||||
|
.split("/")
|
||||||
|
.filter(segment => segment !== "")
|
||||||
|
|
||||||
|
const restOfPath =
|
||||||
|
restPathSegments.length > 0 ? `/${restPathSegments.join("/")}` : ""
|
||||||
|
|
||||||
|
const naiveUrl = sanitizeUrl(`/${firstPathSegment}${restOfPath}`)
|
||||||
|
if (isScreenUrlValid(screens, naiveUrl, role)) {
|
||||||
|
return naiveUrl
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let suffix = 2; suffix < arbitraryMax; suffix++) {
|
||||||
|
const suffixedUrl = sanitizeUrl(
|
||||||
|
`/${firstPathSegment}-${suffix}${restOfPath}`
|
||||||
|
)
|
||||||
|
|
||||||
|
if (isScreenUrlValid(screens, suffixedUrl, role)) {
|
||||||
|
return suffixedUrl
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default getValidScreenUrl
|
|
@ -0,0 +1,3 @@
|
||||||
|
export { default as blank } from "./blank"
|
||||||
|
export { default as form } from "./form"
|
||||||
|
export { default as table } from "./table"
|
|
@ -0,0 +1,25 @@
|
||||||
|
import inline from "./inline"
|
||||||
|
import modal from "./modal"
|
||||||
|
import sidePanel from "./sidePanel"
|
||||||
|
import newScreen from "./newScreen"
|
||||||
|
|
||||||
|
const createScreen = ({ tableOrView, type, permissions, screens }) => {
|
||||||
|
if (type === "inline") {
|
||||||
|
return inline({ tableOrView, permissions, screens })
|
||||||
|
}
|
||||||
|
if (type === "modal") {
|
||||||
|
return modal({ tableOrView, permissions, screens })
|
||||||
|
}
|
||||||
|
|
||||||
|
if (type === "sidePanel") {
|
||||||
|
return sidePanel({ tableOrView, permissions, screens })
|
||||||
|
}
|
||||||
|
|
||||||
|
if (type === "newScreen") {
|
||||||
|
return newScreen({ tableOrView, permissions, screens })
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error(`Unrecognized table type ${type}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default createScreen
|
|
@ -0,0 +1,36 @@
|
||||||
|
import { Screen } from "../Screen"
|
||||||
|
import { Component } from "../../Component"
|
||||||
|
import { capitalise } from "helpers"
|
||||||
|
import getValidRoute from "../getValidRoute"
|
||||||
|
|
||||||
|
const inline = ({ tableOrView, permissions, screens }) => {
|
||||||
|
const heading = new Component("@budibase/standard-components/heading")
|
||||||
|
.instanceName("Table heading")
|
||||||
|
.customProps({
|
||||||
|
text: tableOrView.name,
|
||||||
|
})
|
||||||
|
|
||||||
|
const tableBlock = new Component("@budibase/standard-components/gridblock")
|
||||||
|
.instanceName(`${tableOrView.name} - Table`)
|
||||||
|
.customProps({
|
||||||
|
table: tableOrView.datasourceSelectFormat,
|
||||||
|
})
|
||||||
|
|
||||||
|
const screenTemplate = new Screen()
|
||||||
|
.route(getValidRoute(screens, tableOrView.name, permissions.write))
|
||||||
|
.instanceName(`${tableOrView.name} - List`)
|
||||||
|
.role(permissions.write)
|
||||||
|
.autoTableId(tableOrView.id)
|
||||||
|
.addChild(heading)
|
||||||
|
.addChild(tableBlock)
|
||||||
|
.json()
|
||||||
|
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
data: screenTemplate,
|
||||||
|
navigationLinkLabel: capitalise(tableOrView.name),
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
export default inline
|
|
@ -0,0 +1,157 @@
|
||||||
|
import { Screen } from "../Screen"
|
||||||
|
import { Component } from "../../Component"
|
||||||
|
import { generate } from "shortid"
|
||||||
|
import { makePropSafe as safe } from "@budibase/string-templates"
|
||||||
|
import { Utils } from "@budibase/frontend-core"
|
||||||
|
import { capitalise } from "helpers"
|
||||||
|
import getValidRoute from "../getValidRoute"
|
||||||
|
|
||||||
|
const modal = ({ tableOrView, permissions, screens }) => {
|
||||||
|
/*
|
||||||
|
Create Row
|
||||||
|
*/
|
||||||
|
const createRowModal = new Component("@budibase/standard-components/modal")
|
||||||
|
.instanceName("New row modal")
|
||||||
|
.customProps({
|
||||||
|
size: "large",
|
||||||
|
})
|
||||||
|
|
||||||
|
const buttonGroup = new Component("@budibase/standard-components/buttongroup")
|
||||||
|
const createButton = new Component("@budibase/standard-components/button")
|
||||||
|
|
||||||
|
createButton.customProps({
|
||||||
|
onClick: [
|
||||||
|
{
|
||||||
|
id: 0,
|
||||||
|
"##eventHandlerType": "Open Modal",
|
||||||
|
parameters: {
|
||||||
|
id: createRowModal._json._id,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
text: "Create row",
|
||||||
|
type: "cta",
|
||||||
|
})
|
||||||
|
|
||||||
|
buttonGroup.instanceName(`${tableOrView.name} - Create`).customProps({
|
||||||
|
hAlign: "right",
|
||||||
|
buttons: [createButton.json()],
|
||||||
|
})
|
||||||
|
|
||||||
|
const tableHeader = new Component("@budibase/standard-components/container")
|
||||||
|
.instanceName("Heading container")
|
||||||
|
.customProps({
|
||||||
|
direction: "row",
|
||||||
|
hAlign: "stretch",
|
||||||
|
})
|
||||||
|
|
||||||
|
const heading = new Component("@budibase/standard-components/heading")
|
||||||
|
.instanceName("Table heading")
|
||||||
|
.customProps({
|
||||||
|
text: tableOrView.name,
|
||||||
|
})
|
||||||
|
|
||||||
|
tableHeader.addChild(heading)
|
||||||
|
tableHeader.addChild(buttonGroup)
|
||||||
|
|
||||||
|
const createFormBlock = new Component(
|
||||||
|
"@budibase/standard-components/formblock"
|
||||||
|
)
|
||||||
|
createFormBlock.instanceName("Create row form block").customProps({
|
||||||
|
dataSource: tableOrView.tableSelectFormat,
|
||||||
|
labelPosition: "left",
|
||||||
|
buttonPosition: "top",
|
||||||
|
actionType: "Create",
|
||||||
|
title: "Create row",
|
||||||
|
buttons: Utils.buildFormBlockButtonConfig({
|
||||||
|
_id: createFormBlock._json._id,
|
||||||
|
showDeleteButton: false,
|
||||||
|
showSaveButton: true,
|
||||||
|
saveButtonLabel: "Save",
|
||||||
|
actionType: "Create",
|
||||||
|
dataSource: tableOrView.tableSelectFormat,
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
|
||||||
|
createRowModal.addChild(createFormBlock)
|
||||||
|
|
||||||
|
/*
|
||||||
|
Edit Row
|
||||||
|
*/
|
||||||
|
const stateKey = `ID_${generate()}`
|
||||||
|
const detailsModal = new Component("@budibase/standard-components/modal")
|
||||||
|
.instanceName("Edit row modal")
|
||||||
|
.customProps({
|
||||||
|
size: "large",
|
||||||
|
})
|
||||||
|
|
||||||
|
const editFormBlock = new Component("@budibase/standard-components/formblock")
|
||||||
|
editFormBlock.instanceName("Edit row form block").customProps({
|
||||||
|
dataSource: tableOrView.tableSelectFormat,
|
||||||
|
labelPosition: "left",
|
||||||
|
buttonPosition: "top",
|
||||||
|
actionType: "Update",
|
||||||
|
title: "Edit",
|
||||||
|
rowId: `{{ ${safe("state")}.${safe(stateKey)} }}`,
|
||||||
|
buttons: Utils.buildFormBlockButtonConfig({
|
||||||
|
_id: editFormBlock._json._id,
|
||||||
|
showDeleteButton: true,
|
||||||
|
showSaveButton: true,
|
||||||
|
saveButtonLabel: "Save",
|
||||||
|
deleteButtonLabel: "Delete",
|
||||||
|
actionType: "Update",
|
||||||
|
dataSource: tableOrView.tableSelectFormat,
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
|
||||||
|
detailsModal.addChild(editFormBlock)
|
||||||
|
|
||||||
|
const tableBlock = new Component("@budibase/standard-components/gridblock")
|
||||||
|
tableBlock
|
||||||
|
.customProps({
|
||||||
|
table: tableOrView.datasourceSelectFormat,
|
||||||
|
allowAddRows: false,
|
||||||
|
allowEditRows: false,
|
||||||
|
allowDeleteRows: false,
|
||||||
|
onRowClick: [
|
||||||
|
{
|
||||||
|
id: 0,
|
||||||
|
"##eventHandlerType": "Update State",
|
||||||
|
parameters: {
|
||||||
|
key: stateKey,
|
||||||
|
type: "set",
|
||||||
|
persist: false,
|
||||||
|
value: `{{ ${safe("eventContext")}.${safe("row")}._id }}`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
"##eventHandlerType": "Open Modal",
|
||||||
|
parameters: {
|
||||||
|
id: detailsModal._json._id,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
.instanceName(`${tableOrView.name} - Table`)
|
||||||
|
|
||||||
|
const template = new Screen()
|
||||||
|
.route(getValidRoute(screens, tableOrView.name, permissions.write))
|
||||||
|
.instanceName(`${tableOrView.name} - List and details`)
|
||||||
|
.role(permissions.write)
|
||||||
|
.autoTableId(tableOrView.id)
|
||||||
|
.addChild(tableHeader)
|
||||||
|
.addChild(tableBlock)
|
||||||
|
.addChild(createRowModal)
|
||||||
|
.addChild(detailsModal)
|
||||||
|
.json()
|
||||||
|
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
data: template,
|
||||||
|
navigationLinkLabel: capitalise(tableOrView.name),
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
export default modal
|
|
@ -0,0 +1,322 @@
|
||||||
|
import { Screen } from "../Screen"
|
||||||
|
import { Component } from "../../Component"
|
||||||
|
import { capitalise } from "helpers"
|
||||||
|
import { makePropSafe as safe } from "@budibase/string-templates"
|
||||||
|
import getValidRoute from "../getValidRoute"
|
||||||
|
import { Helpers } from "@budibase/bbui"
|
||||||
|
|
||||||
|
const getTableScreenTemplate = ({
|
||||||
|
route,
|
||||||
|
updateScreenRoute,
|
||||||
|
createScreenRoute,
|
||||||
|
tableOrView,
|
||||||
|
permissions,
|
||||||
|
}) => {
|
||||||
|
const newButton = new Component("@budibase/standard-components/button")
|
||||||
|
.instanceName("New button")
|
||||||
|
.customProps({
|
||||||
|
text: "Create row",
|
||||||
|
onClick: [
|
||||||
|
{
|
||||||
|
"##eventHandlerType": "Navigate To",
|
||||||
|
parameters: {
|
||||||
|
type: "url",
|
||||||
|
url: createScreenRoute,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
|
||||||
|
const heading = new Component("@budibase/standard-components/heading")
|
||||||
|
.instanceName("Table heading")
|
||||||
|
.customProps({
|
||||||
|
text: tableOrView.name,
|
||||||
|
})
|
||||||
|
|
||||||
|
const tableHeader = new Component("@budibase/standard-components/container")
|
||||||
|
.instanceName("Heading container")
|
||||||
|
.customProps({
|
||||||
|
direction: "row",
|
||||||
|
hAlign: "stretch",
|
||||||
|
})
|
||||||
|
.addChild(heading)
|
||||||
|
.addChild(newButton)
|
||||||
|
|
||||||
|
const updateScreenRouteSegments = updateScreenRoute.split(":id")
|
||||||
|
if (updateScreenRouteSegments.length !== 2) {
|
||||||
|
throw new Error("Provided edit screen route is invalid")
|
||||||
|
}
|
||||||
|
|
||||||
|
const tableBlock = new Component("@budibase/standard-components/gridblock")
|
||||||
|
.instanceName(`${tableOrView.name} - Table`)
|
||||||
|
.customProps({
|
||||||
|
table: tableOrView.datasourceSelectFormat,
|
||||||
|
allowAddRows: false,
|
||||||
|
allowEditRows: false,
|
||||||
|
allowDeleteRows: false,
|
||||||
|
onRowClick: [
|
||||||
|
{
|
||||||
|
id: 0,
|
||||||
|
"##eventHandlerType": "Navigate To",
|
||||||
|
parameters: {
|
||||||
|
type: "url",
|
||||||
|
url: `${updateScreenRouteSegments[0]}{{ ${safe(
|
||||||
|
"eventContext"
|
||||||
|
)}.${safe("row")}._id }}${updateScreenRouteSegments[1]}`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
|
||||||
|
const template = new Screen()
|
||||||
|
.route(route)
|
||||||
|
.instanceName(`${tableOrView.name} - List`)
|
||||||
|
.role(permissions.write)
|
||||||
|
.autoTableId(tableOrView.id)
|
||||||
|
.addChild(tableHeader)
|
||||||
|
.addChild(tableBlock)
|
||||||
|
.json()
|
||||||
|
|
||||||
|
return {
|
||||||
|
data: template,
|
||||||
|
navigationLinkLabel: capitalise(tableOrView.name),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const getUpdateScreenTemplate = ({
|
||||||
|
route,
|
||||||
|
tableScreenRoute,
|
||||||
|
tableOrView,
|
||||||
|
permissions,
|
||||||
|
}) => {
|
||||||
|
const formBlockId = Helpers.uuid()
|
||||||
|
const formId = `${formBlockId}-form`
|
||||||
|
const repeaterId = `${formBlockId}-repeater`
|
||||||
|
|
||||||
|
const backButton = new Component("@budibase/standard-components/button")
|
||||||
|
.instanceName("Back button")
|
||||||
|
.customProps({
|
||||||
|
type: "primary",
|
||||||
|
icon: "ri-arrow-go-back-fill",
|
||||||
|
text: "Back",
|
||||||
|
onClick: [
|
||||||
|
{
|
||||||
|
"##eventHandlerType": "Navigate To",
|
||||||
|
parameters: {
|
||||||
|
type: "url",
|
||||||
|
url: tableScreenRoute,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
|
||||||
|
const deleteButton = new Component("@budibase/standard-components/button")
|
||||||
|
.instanceName("Delete button")
|
||||||
|
.customProps({
|
||||||
|
type: "secondary",
|
||||||
|
text: "Delete",
|
||||||
|
onClick: [
|
||||||
|
{
|
||||||
|
"##eventHandlerType": "Delete Row",
|
||||||
|
parameters: {
|
||||||
|
confirm: true,
|
||||||
|
tableId: tableOrView.id,
|
||||||
|
rowId: `{{ ${safe(repeaterId)}.${safe("_id")} }}`,
|
||||||
|
revId: `{{ ${safe(repeaterId)}.${safe("_rev")} }}`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"##eventHandlerType": "Navigate To",
|
||||||
|
parameters: {
|
||||||
|
type: "url",
|
||||||
|
url: tableScreenRoute,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
|
||||||
|
const saveButton = new Component("@budibase/standard-components/button")
|
||||||
|
.instanceName("Save button")
|
||||||
|
.customProps({
|
||||||
|
type: "cta",
|
||||||
|
text: "Save",
|
||||||
|
onClick: [
|
||||||
|
{
|
||||||
|
"##eventHandlerType": "Validate Form",
|
||||||
|
parameters: {
|
||||||
|
componentId: formId,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"##eventHandlerType": "Save Row",
|
||||||
|
parameters: {
|
||||||
|
providerId: formId,
|
||||||
|
tableId: tableOrView.id,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"##eventHandlerType": "Navigate To",
|
||||||
|
parameters: {
|
||||||
|
type: "url",
|
||||||
|
url: tableScreenRoute,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
|
||||||
|
const updateFormBlock = new Component(
|
||||||
|
"@budibase/standard-components/formblock",
|
||||||
|
formBlockId
|
||||||
|
)
|
||||||
|
.instanceName("Update row form block")
|
||||||
|
.customProps({
|
||||||
|
dataSource: tableOrView.tableSelectFormat,
|
||||||
|
labelPosition: "left",
|
||||||
|
buttonPosition: "top",
|
||||||
|
actionType: "Update",
|
||||||
|
title: `Update ${tableOrView.name} row`,
|
||||||
|
buttons: [backButton.json(), saveButton.json(), deleteButton.json()],
|
||||||
|
})
|
||||||
|
|
||||||
|
const template = new Screen()
|
||||||
|
.route(route)
|
||||||
|
.instanceName(`Update row`)
|
||||||
|
.role(permissions.write)
|
||||||
|
.autoTableId(tableOrView.id)
|
||||||
|
.addChild(updateFormBlock)
|
||||||
|
.json()
|
||||||
|
|
||||||
|
return {
|
||||||
|
data: template,
|
||||||
|
navigationLinkLabel: null,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const getCreateScreenTemplate = ({
|
||||||
|
route,
|
||||||
|
tableScreenRoute,
|
||||||
|
tableOrView,
|
||||||
|
permissions,
|
||||||
|
}) => {
|
||||||
|
const formBlockId = Helpers.uuid()
|
||||||
|
const formId = `${formBlockId}-form`
|
||||||
|
|
||||||
|
const backButton = new Component("@budibase/standard-components/button")
|
||||||
|
.instanceName("Back button")
|
||||||
|
.customProps({
|
||||||
|
type: "primary",
|
||||||
|
icon: "ri-arrow-go-back-fill",
|
||||||
|
text: "Back",
|
||||||
|
onClick: [
|
||||||
|
{
|
||||||
|
"##eventHandlerType": "Navigate To",
|
||||||
|
parameters: {
|
||||||
|
type: "url",
|
||||||
|
url: tableScreenRoute,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
|
||||||
|
const saveButton = new Component("@budibase/standard-components/button")
|
||||||
|
.instanceName("Save button")
|
||||||
|
.customProps({
|
||||||
|
type: "cta",
|
||||||
|
text: "Save",
|
||||||
|
onClick: [
|
||||||
|
{
|
||||||
|
"##eventHandlerType": "Validate Form",
|
||||||
|
parameters: {
|
||||||
|
componentId: formId,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"##eventHandlerType": "Save Row",
|
||||||
|
parameters: {
|
||||||
|
providerId: formId,
|
||||||
|
tableId: tableOrView.id,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"##eventHandlerType": "Navigate To",
|
||||||
|
parameters: {
|
||||||
|
type: "url",
|
||||||
|
url: tableScreenRoute,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
|
||||||
|
const createFormBlock = new Component(
|
||||||
|
"@budibase/standard-components/formblock",
|
||||||
|
formBlockId
|
||||||
|
)
|
||||||
|
.instanceName("Create row form block")
|
||||||
|
.customProps({
|
||||||
|
dataSource: tableOrView.tableSelectFormat,
|
||||||
|
labelPosition: "left",
|
||||||
|
buttonPosition: "top",
|
||||||
|
actionType: "Create",
|
||||||
|
title: `Create ${tableOrView.name} row`,
|
||||||
|
buttons: [backButton.json(), saveButton.json()],
|
||||||
|
})
|
||||||
|
|
||||||
|
const template = new Screen()
|
||||||
|
.route(route)
|
||||||
|
.instanceName("Create row")
|
||||||
|
.role(permissions.write)
|
||||||
|
.autoTableId(tableOrView.id)
|
||||||
|
.addChild(createFormBlock)
|
||||||
|
.json()
|
||||||
|
|
||||||
|
return {
|
||||||
|
data: template,
|
||||||
|
navigationLinkLabel: null,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const newScreen = ({ tableOrView, permissions, screens }) => {
|
||||||
|
const tableScreenRoute = getValidRoute(
|
||||||
|
screens,
|
||||||
|
tableOrView.name,
|
||||||
|
permissions.write
|
||||||
|
)
|
||||||
|
|
||||||
|
const updateScreenRoute = getValidRoute(
|
||||||
|
screens,
|
||||||
|
`/${tableOrView.name}/edit/:id`,
|
||||||
|
permissions.write
|
||||||
|
)
|
||||||
|
|
||||||
|
const createScreenRoute = getValidRoute(
|
||||||
|
screens,
|
||||||
|
`/${tableOrView.name}/new`,
|
||||||
|
permissions.write
|
||||||
|
)
|
||||||
|
|
||||||
|
const tableScreenTemplate = getTableScreenTemplate({
|
||||||
|
route: tableScreenRoute,
|
||||||
|
updateScreenRoute,
|
||||||
|
createScreenRoute,
|
||||||
|
permissions,
|
||||||
|
tableOrView,
|
||||||
|
})
|
||||||
|
|
||||||
|
const updateScreenTemplate = getUpdateScreenTemplate({
|
||||||
|
route: updateScreenRoute,
|
||||||
|
tableScreenRoute,
|
||||||
|
tableOrView,
|
||||||
|
permissions,
|
||||||
|
})
|
||||||
|
|
||||||
|
const createScreenTemplate = getCreateScreenTemplate({
|
||||||
|
route: createScreenRoute,
|
||||||
|
tableScreenRoute,
|
||||||
|
tableOrView,
|
||||||
|
permissions,
|
||||||
|
})
|
||||||
|
|
||||||
|
return [tableScreenTemplate, updateScreenTemplate, createScreenTemplate]
|
||||||
|
}
|
||||||
|
|
||||||
|
export default newScreen
|
|
@ -1,13 +1,12 @@
|
||||||
import sanitizeUrl from "helpers/sanitizeUrl"
|
import { Screen } from "../Screen"
|
||||||
import { Screen } from "./Screen"
|
import { Component } from "../../Component"
|
||||||
import { Component } from "./Component"
|
|
||||||
import { generate } from "shortid"
|
import { generate } from "shortid"
|
||||||
import { makePropSafe as safe } from "@budibase/string-templates"
|
import { makePropSafe as safe } from "@budibase/string-templates"
|
||||||
import { Utils } from "@budibase/frontend-core"
|
import { Utils } from "@budibase/frontend-core"
|
||||||
|
import { capitalise } from "helpers"
|
||||||
|
import getValidRoute from "../getValidRoute"
|
||||||
|
|
||||||
const gridDetailsUrl = tableOrView => sanitizeUrl(`/${tableOrView.name}`)
|
const sidePanel = ({ tableOrView, permissions, screens }) => {
|
||||||
|
|
||||||
const createScreen = (tableOrView, permissions) => {
|
|
||||||
/*
|
/*
|
||||||
Create Row
|
Create Row
|
||||||
*/
|
*/
|
||||||
|
@ -37,7 +36,7 @@ const createScreen = (tableOrView, permissions) => {
|
||||||
buttons: [createButton.json()],
|
buttons: [createButton.json()],
|
||||||
})
|
})
|
||||||
|
|
||||||
const gridHeader = new Component("@budibase/standard-components/container")
|
const tableHeader = new Component("@budibase/standard-components/container")
|
||||||
.instanceName("Heading container")
|
.instanceName("Heading container")
|
||||||
.customProps({
|
.customProps({
|
||||||
layout: "flex",
|
layout: "flex",
|
||||||
|
@ -51,14 +50,14 @@ const createScreen = (tableOrView, permissions) => {
|
||||||
text: tableOrView.name,
|
text: tableOrView.name,
|
||||||
})
|
})
|
||||||
|
|
||||||
gridHeader.addChild(heading)
|
tableHeader.addChild(heading)
|
||||||
gridHeader.addChild(buttonGroup)
|
tableHeader.addChild(buttonGroup)
|
||||||
|
|
||||||
const createFormBlock = new Component(
|
const createFormBlock = new Component(
|
||||||
"@budibase/standard-components/formblock"
|
"@budibase/standard-components/formblock"
|
||||||
)
|
)
|
||||||
createFormBlock.instanceName("Create row form block").customProps({
|
createFormBlock.instanceName("Create row form block").customProps({
|
||||||
dataSource: tableOrView.clientData,
|
dataSource: tableOrView.tableSelectFormat,
|
||||||
labelPosition: "left",
|
labelPosition: "left",
|
||||||
buttonPosition: "top",
|
buttonPosition: "top",
|
||||||
actionType: "Create",
|
actionType: "Create",
|
||||||
|
@ -69,7 +68,7 @@ const createScreen = (tableOrView, permissions) => {
|
||||||
showSaveButton: true,
|
showSaveButton: true,
|
||||||
saveButtonLabel: "Save",
|
saveButtonLabel: "Save",
|
||||||
actionType: "Create",
|
actionType: "Create",
|
||||||
dataSource: tableOrView.clientData,
|
dataSource: tableOrView.tableSelectFormat,
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -85,7 +84,7 @@ const createScreen = (tableOrView, permissions) => {
|
||||||
|
|
||||||
const editFormBlock = new Component("@budibase/standard-components/formblock")
|
const editFormBlock = new Component("@budibase/standard-components/formblock")
|
||||||
editFormBlock.instanceName("Edit row form block").customProps({
|
editFormBlock.instanceName("Edit row form block").customProps({
|
||||||
dataSource: tableOrView.clientData,
|
dataSource: tableOrView.tableSelectFormat,
|
||||||
labelPosition: "left",
|
labelPosition: "left",
|
||||||
buttonPosition: "top",
|
buttonPosition: "top",
|
||||||
actionType: "Update",
|
actionType: "Update",
|
||||||
|
@ -98,16 +97,16 @@ const createScreen = (tableOrView, permissions) => {
|
||||||
saveButtonLabel: "Save",
|
saveButtonLabel: "Save",
|
||||||
deleteButtonLabel: "Delete",
|
deleteButtonLabel: "Delete",
|
||||||
actionType: "Update",
|
actionType: "Update",
|
||||||
dataSource: tableOrView.clientData,
|
dataSource: tableOrView.tableSelectFormat,
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
|
|
||||||
detailsSidePanel.addChild(editFormBlock)
|
detailsSidePanel.addChild(editFormBlock)
|
||||||
|
|
||||||
const gridBlock = new Component("@budibase/standard-components/gridblock")
|
const tableBlock = new Component("@budibase/standard-components/gridblock")
|
||||||
gridBlock
|
tableBlock
|
||||||
.customProps({
|
.customProps({
|
||||||
table: tableOrView.clientData,
|
table: tableOrView.datasourceSelectFormat,
|
||||||
allowAddRows: false,
|
allowAddRows: false,
|
||||||
allowEditRows: false,
|
allowEditRows: false,
|
||||||
allowDeleteRows: false,
|
allowDeleteRows: false,
|
||||||
|
@ -133,16 +132,23 @@ const createScreen = (tableOrView, permissions) => {
|
||||||
})
|
})
|
||||||
.instanceName(`${tableOrView.name} - Table`)
|
.instanceName(`${tableOrView.name} - Table`)
|
||||||
|
|
||||||
return new Screen()
|
const template = new Screen()
|
||||||
.route(gridDetailsUrl(tableOrView))
|
.route(getValidRoute(screens, tableOrView.name, permissions.write))
|
||||||
.instanceName(`${tableOrView.name} - List and details`)
|
.instanceName(`${tableOrView.name} - List and details`)
|
||||||
.role(permissions.write)
|
.role(permissions.write)
|
||||||
.autoTableId(tableOrView.resourceId)
|
.autoTableId(tableOrView.id)
|
||||||
.addChild(gridHeader)
|
.addChild(tableHeader)
|
||||||
.addChild(gridBlock)
|
.addChild(tableBlock)
|
||||||
.addChild(createRowSidePanel)
|
.addChild(createRowSidePanel)
|
||||||
.addChild(detailsSidePanel)
|
.addChild(detailsSidePanel)
|
||||||
.json()
|
.json()
|
||||||
|
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
data: template,
|
||||||
|
navigationLinkLabel: capitalise(tableOrView.name),
|
||||||
|
},
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
export default createScreen
|
export default sidePanel
|
|
@ -1,95 +1,17 @@
|
||||||
<script>
|
<script>
|
||||||
import { getContext } from "svelte"
|
import { getContext } from "svelte"
|
||||||
import { ActionButton, Popover, Icon, notifications } from "@budibase/bbui"
|
import { ActionButton, Popover } from "@budibase/bbui"
|
||||||
import { getColumnIcon } from "../lib/utils"
|
import ColumnsSettingContent from "./ColumnsSettingContent.svelte"
|
||||||
import ToggleActionButtonGroup from "./ToggleActionButtonGroup.svelte"
|
|
||||||
import { helpers } from "@budibase/shared-core"
|
|
||||||
|
|
||||||
export let allowViewReadonlyColumns = false
|
export let allowViewReadonlyColumns = false
|
||||||
|
|
||||||
const { columns, datasource, dispatch } = getContext("grid")
|
const { columns } = getContext("grid")
|
||||||
|
|
||||||
let open = false
|
let open = false
|
||||||
let anchor
|
let anchor
|
||||||
|
|
||||||
$: restrictedColumns = $columns.filter(col => !col.visible || col.readonly)
|
$: anyRestricted = $columns.filter(col => !col.visible || col.readonly).length
|
||||||
$: anyRestricted = restrictedColumns.length
|
|
||||||
$: text = anyRestricted ? `Columns (${anyRestricted} restricted)` : "Columns"
|
$: text = anyRestricted ? `Columns (${anyRestricted} restricted)` : "Columns"
|
||||||
|
|
||||||
const toggleColumn = async (column, permission) => {
|
|
||||||
const visible = permission !== PERMISSION_OPTIONS.HIDDEN
|
|
||||||
const readonly = permission === PERMISSION_OPTIONS.READONLY
|
|
||||||
|
|
||||||
await datasource.actions.addSchemaMutation(column.name, {
|
|
||||||
visible,
|
|
||||||
readonly,
|
|
||||||
})
|
|
||||||
try {
|
|
||||||
await datasource.actions.saveSchemaMutations()
|
|
||||||
} catch (e) {
|
|
||||||
notifications.error(e.message)
|
|
||||||
} finally {
|
|
||||||
await datasource.actions.resetSchemaMutations()
|
|
||||||
await datasource.actions.refreshDefinition()
|
|
||||||
}
|
|
||||||
dispatch(visible ? "show-column" : "hide-column")
|
|
||||||
}
|
|
||||||
|
|
||||||
const PERMISSION_OPTIONS = {
|
|
||||||
WRITABLE: "writable",
|
|
||||||
READONLY: "readonly",
|
|
||||||
HIDDEN: "hidden",
|
|
||||||
}
|
|
||||||
|
|
||||||
$: displayColumns = $columns.map(c => {
|
|
||||||
const isRequired = helpers.schema.isRequired(c.schema.constraints)
|
|
||||||
const requiredTooltip = isRequired && "Required columns must be writable"
|
|
||||||
const editEnabled =
|
|
||||||
!isRequired ||
|
|
||||||
columnToPermissionOptions(c) !== PERMISSION_OPTIONS.WRITABLE
|
|
||||||
const options = [
|
|
||||||
{
|
|
||||||
icon: "Edit",
|
|
||||||
value: PERMISSION_OPTIONS.WRITABLE,
|
|
||||||
tooltip: (!editEnabled && requiredTooltip) || "Writable",
|
|
||||||
disabled: !editEnabled,
|
|
||||||
},
|
|
||||||
]
|
|
||||||
if ($datasource.type === "viewV2") {
|
|
||||||
options.push({
|
|
||||||
icon: "Visibility",
|
|
||||||
value: PERMISSION_OPTIONS.READONLY,
|
|
||||||
tooltip: allowViewReadonlyColumns
|
|
||||||
? requiredTooltip || "Read only"
|
|
||||||
: "Read only (premium feature)",
|
|
||||||
disabled: !allowViewReadonlyColumns || isRequired,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
options.push({
|
|
||||||
icon: "VisibilityOff",
|
|
||||||
value: PERMISSION_OPTIONS.HIDDEN,
|
|
||||||
disabled: c.primaryDisplay || isRequired,
|
|
||||||
tooltip:
|
|
||||||
(c.primaryDisplay && "Display column cannot be hidden") ||
|
|
||||||
requiredTooltip ||
|
|
||||||
"Hidden",
|
|
||||||
})
|
|
||||||
|
|
||||||
return { ...c, options }
|
|
||||||
})
|
|
||||||
|
|
||||||
function columnToPermissionOptions(column) {
|
|
||||||
if (!column.schema.visible) {
|
|
||||||
return PERMISSION_OPTIONS.HIDDEN
|
|
||||||
}
|
|
||||||
|
|
||||||
if (column.schema.readonly) {
|
|
||||||
return PERMISSION_OPTIONS.READONLY
|
|
||||||
}
|
|
||||||
|
|
||||||
return PERMISSION_OPTIONS.WRITABLE
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div bind:this={anchor}>
|
<div bind:this={anchor}>
|
||||||
|
@ -106,51 +28,5 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Popover bind:open {anchor} align="left">
|
<Popover bind:open {anchor} align="left">
|
||||||
<div class="content">
|
<ColumnsSettingContent columns={$columns} {allowViewReadonlyColumns} />
|
||||||
<div class="columns">
|
|
||||||
{#each displayColumns as column}
|
|
||||||
<div class="column">
|
|
||||||
<Icon size="S" name={getColumnIcon(column)} />
|
|
||||||
<div class="column-label" title={column.label}>
|
|
||||||
{column.label}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<ToggleActionButtonGroup
|
|
||||||
on:click={e => toggleColumn(column, e.detail)}
|
|
||||||
value={columnToPermissionOptions(column)}
|
|
||||||
options={column.options}
|
|
||||||
/>
|
|
||||||
{/each}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Popover>
|
</Popover>
|
||||||
|
|
||||||
<style>
|
|
||||||
.content {
|
|
||||||
padding: 12px 12px;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 12px;
|
|
||||||
}
|
|
||||||
.columns {
|
|
||||||
display: grid;
|
|
||||||
align-items: center;
|
|
||||||
grid-template-columns: 1fr auto;
|
|
||||||
grid-row-gap: 8px;
|
|
||||||
grid-column-gap: 24px;
|
|
||||||
}
|
|
||||||
.columns :global(.spectrum-Switch) {
|
|
||||||
margin-right: 0;
|
|
||||||
}
|
|
||||||
.column {
|
|
||||||
display: flex;
|
|
||||||
gap: 8px;
|
|
||||||
}
|
|
||||||
.column-label {
|
|
||||||
min-width: 80px;
|
|
||||||
max-width: 200px;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
white-space: nowrap;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
|
@ -0,0 +1,134 @@
|
||||||
|
<script>
|
||||||
|
import { getContext } from "svelte"
|
||||||
|
import { Icon, notifications } from "@budibase/bbui"
|
||||||
|
import { getColumnIcon } from "../lib/utils"
|
||||||
|
import ToggleActionButtonGroup from "./ToggleActionButtonGroup.svelte"
|
||||||
|
import { helpers } from "@budibase/shared-core"
|
||||||
|
|
||||||
|
export let allowViewReadonlyColumns = false
|
||||||
|
|
||||||
|
const { columns, datasource, dispatch } = getContext("grid")
|
||||||
|
|
||||||
|
const toggleColumn = async (column, permission) => {
|
||||||
|
const visible = permission !== PERMISSION_OPTIONS.HIDDEN
|
||||||
|
const readonly = permission === PERMISSION_OPTIONS.READONLY
|
||||||
|
|
||||||
|
await datasource.actions.addSchemaMutation(column.name, {
|
||||||
|
visible,
|
||||||
|
readonly,
|
||||||
|
})
|
||||||
|
try {
|
||||||
|
await datasource.actions.saveSchemaMutations()
|
||||||
|
} catch (e) {
|
||||||
|
notifications.error(e.message)
|
||||||
|
} finally {
|
||||||
|
await datasource.actions.resetSchemaMutations()
|
||||||
|
await datasource.actions.refreshDefinition()
|
||||||
|
}
|
||||||
|
dispatch(visible ? "show-column" : "hide-column")
|
||||||
|
}
|
||||||
|
|
||||||
|
const PERMISSION_OPTIONS = {
|
||||||
|
WRITABLE: "writable",
|
||||||
|
READONLY: "readonly",
|
||||||
|
HIDDEN: "hidden",
|
||||||
|
}
|
||||||
|
|
||||||
|
$: displayColumns = $columns.map(c => {
|
||||||
|
const isRequired = helpers.schema.isRequired(c.schema.constraints)
|
||||||
|
const requiredTooltip = isRequired && "Required columns must be writable"
|
||||||
|
const editEnabled =
|
||||||
|
!isRequired ||
|
||||||
|
columnToPermissionOptions(c) !== PERMISSION_OPTIONS.WRITABLE
|
||||||
|
const options = [
|
||||||
|
{
|
||||||
|
icon: "Edit",
|
||||||
|
value: PERMISSION_OPTIONS.WRITABLE,
|
||||||
|
tooltip: (!editEnabled && requiredTooltip) || "Writable",
|
||||||
|
disabled: !editEnabled,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
if ($datasource.type === "viewV2") {
|
||||||
|
options.push({
|
||||||
|
icon: "Visibility",
|
||||||
|
value: PERMISSION_OPTIONS.READONLY,
|
||||||
|
tooltip: allowViewReadonlyColumns
|
||||||
|
? requiredTooltip || "Read only"
|
||||||
|
: "Read only (premium feature)",
|
||||||
|
disabled: !allowViewReadonlyColumns || isRequired,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
options.push({
|
||||||
|
icon: "VisibilityOff",
|
||||||
|
value: PERMISSION_OPTIONS.HIDDEN,
|
||||||
|
disabled: c.primaryDisplay || isRequired,
|
||||||
|
tooltip:
|
||||||
|
(c.primaryDisplay && "Display column cannot be hidden") ||
|
||||||
|
requiredTooltip ||
|
||||||
|
"Hidden",
|
||||||
|
})
|
||||||
|
|
||||||
|
return { ...c, options }
|
||||||
|
})
|
||||||
|
|
||||||
|
function columnToPermissionOptions(column) {
|
||||||
|
if (column.schema.visible === false) {
|
||||||
|
return PERMISSION_OPTIONS.HIDDEN
|
||||||
|
}
|
||||||
|
|
||||||
|
if (column.schema.readonly) {
|
||||||
|
return PERMISSION_OPTIONS.READONLY
|
||||||
|
}
|
||||||
|
|
||||||
|
return PERMISSION_OPTIONS.WRITABLE
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="content">
|
||||||
|
<div class="columns">
|
||||||
|
{#each displayColumns as column}
|
||||||
|
<div class="column">
|
||||||
|
<Icon size="S" name={getColumnIcon(column)} />
|
||||||
|
<div class="column-label" title={column.label}>
|
||||||
|
{column.label}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<ToggleActionButtonGroup
|
||||||
|
on:click={e => toggleColumn(column, e.detail)}
|
||||||
|
value={columnToPermissionOptions(column)}
|
||||||
|
options={column.options}
|
||||||
|
/>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.content {
|
||||||
|
padding: 12px 12px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
.columns {
|
||||||
|
display: grid;
|
||||||
|
align-items: center;
|
||||||
|
grid-template-columns: 1fr auto;
|
||||||
|
grid-row-gap: 8px;
|
||||||
|
grid-column-gap: 24px;
|
||||||
|
}
|
||||||
|
.columns :global(.spectrum-Switch) {
|
||||||
|
margin-right: 0;
|
||||||
|
}
|
||||||
|
.column {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
.column-label {
|
||||||
|
min-width: 80px;
|
||||||
|
max-width: 200px;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -155,6 +155,7 @@ export const buildFormBlockButtonConfig = props => {
|
||||||
providerId: formId,
|
providerId: formId,
|
||||||
tableId: resourceId,
|
tableId: resourceId,
|
||||||
notificationOverride,
|
notificationOverride,
|
||||||
|
confirm: null,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
|
@ -12,7 +12,6 @@ ENV COUCH_DB_URL=https://couchdb.budi.live:5984
|
||||||
ENV BUDIBASE_ENVIRONMENT=PRODUCTION
|
ENV BUDIBASE_ENVIRONMENT=PRODUCTION
|
||||||
ENV SERVICE=app-service
|
ENV SERVICE=app-service
|
||||||
ENV POSTHOG_TOKEN=phc_bIjZL7oh2GEUd2vqvTBH8WvrX0fWTFQMs6H5KQxiUxU
|
ENV POSTHOG_TOKEN=phc_bIjZL7oh2GEUd2vqvTBH8WvrX0fWTFQMs6H5KQxiUxU
|
||||||
ENV TENANT_FEATURE_FLAGS=*:LICENSING,*:USER_GROUPS,*:ONBOARDING_TOUR
|
|
||||||
ENV ACCOUNT_PORTAL_URL=https://account.budibase.app
|
ENV ACCOUNT_PORTAL_URL=https://account.budibase.app
|
||||||
ENV TOP_LEVEL_PATH=/
|
ENV TOP_LEVEL_PATH=/
|
||||||
|
|
||||||
|
|
|
@ -43,7 +43,6 @@ async function init() {
|
||||||
BB_ADMIN_USER_EMAIL: "",
|
BB_ADMIN_USER_EMAIL: "",
|
||||||
BB_ADMIN_USER_PASSWORD: "",
|
BB_ADMIN_USER_PASSWORD: "",
|
||||||
PLUGINS_DIR: "",
|
PLUGINS_DIR: "",
|
||||||
TENANT_FEATURE_FLAGS: "*:LICENSING,*:USER_GROUPS,*:ONBOARDING_TOUR",
|
|
||||||
HTTP_MIGRATIONS: "0",
|
HTTP_MIGRATIONS: "0",
|
||||||
HTTP_LOGGING: "0",
|
HTTP_LOGGING: "0",
|
||||||
VERSION: "0.0.0+local",
|
VERSION: "0.0.0+local",
|
||||||
|
|
|
@ -308,16 +308,21 @@ export async function downloadAttachment(ctx: UserCtx) {
|
||||||
if (attachments.length === 1) {
|
if (attachments.length === 1) {
|
||||||
const attachment = attachments[0]
|
const attachment = attachments[0]
|
||||||
ctx.attachment(attachment.name)
|
ctx.attachment(attachment.name)
|
||||||
ctx.body = await objectStore.getReadStream(
|
if (attachment.key) {
|
||||||
objectStore.ObjectStoreBuckets.APPS,
|
ctx.body = await objectStore.getReadStream(
|
||||||
attachment.key
|
objectStore.ObjectStoreBuckets.APPS,
|
||||||
)
|
attachment.key
|
||||||
|
)
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
const passThrough = new stream.PassThrough()
|
const passThrough = new stream.PassThrough()
|
||||||
const archive = archiver.create("zip")
|
const archive = archiver.create("zip")
|
||||||
archive.pipe(passThrough)
|
archive.pipe(passThrough)
|
||||||
|
|
||||||
for (const attachment of attachments) {
|
for (const attachment of attachments) {
|
||||||
|
if (!attachment.key) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
const attachmentStream = await objectStore.getReadStream(
|
const attachmentStream = await objectStore.getReadStream(
|
||||||
objectStore.ObjectStoreBuckets.APPS,
|
objectStore.ObjectStoreBuckets.APPS,
|
||||||
attachment.key
|
attachment.key
|
||||||
|
|
|
@ -151,7 +151,10 @@ export function buildExternalRelationships(
|
||||||
return relationships
|
return relationships
|
||||||
}
|
}
|
||||||
|
|
||||||
export function buildInternalRelationships(table: Table): RelationshipsJson[] {
|
export function buildInternalRelationships(
|
||||||
|
table: Table,
|
||||||
|
allTables: Table[]
|
||||||
|
): RelationshipsJson[] {
|
||||||
const relationships: RelationshipsJson[] = []
|
const relationships: RelationshipsJson[] = []
|
||||||
const links = Object.values(table.schema).filter(
|
const links = Object.values(table.schema).filter(
|
||||||
column => column.type === FieldType.LINK
|
column => column.type === FieldType.LINK
|
||||||
|
@ -164,6 +167,10 @@ export function buildInternalRelationships(table: Table): RelationshipsJson[] {
|
||||||
const linkTableId = link.tableId!
|
const linkTableId = link.tableId!
|
||||||
const junctionTableId = generateJunctionTableID(tableId, linkTableId)
|
const junctionTableId = generateJunctionTableID(tableId, linkTableId)
|
||||||
const isFirstTable = tableId > linkTableId
|
const isFirstTable = tableId > linkTableId
|
||||||
|
// skip relationships with missing table definitions
|
||||||
|
if (!allTables.find(table => table._id === linkTableId)) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
relationships.push({
|
relationships.push({
|
||||||
through: junctionTableId,
|
through: junctionTableId,
|
||||||
column: link.name,
|
column: link.name,
|
||||||
|
@ -192,10 +199,10 @@ export function buildSqlFieldList(
|
||||||
function extractRealFields(table: Table, existing: string[] = []) {
|
function extractRealFields(table: Table, existing: string[] = []) {
|
||||||
return Object.entries(table.schema)
|
return Object.entries(table.schema)
|
||||||
.filter(
|
.filter(
|
||||||
column =>
|
([columnName, column]) =>
|
||||||
column[1].type !== FieldType.LINK &&
|
column.type !== FieldType.LINK &&
|
||||||
column[1].type !== FieldType.FORMULA &&
|
column.type !== FieldType.FORMULA &&
|
||||||
!existing.find((field: string) => field === column[0])
|
!existing.find((field: string) => field === columnName)
|
||||||
)
|
)
|
||||||
.map(column => `${table.name}.${column[0]}`)
|
.map(column => `${table.name}.${column[0]}`)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1664,7 +1664,7 @@ describe.each([
|
||||||
isInternal &&
|
isInternal &&
|
||||||
describe("attachments and signatures", () => {
|
describe("attachments and signatures", () => {
|
||||||
const coreAttachmentEnrichment = async (
|
const coreAttachmentEnrichment = async (
|
||||||
schema: any,
|
schema: TableSchema,
|
||||||
field: string,
|
field: string,
|
||||||
attachmentCfg: string | string[]
|
attachmentCfg: string | string[]
|
||||||
) => {
|
) => {
|
||||||
|
@ -1691,7 +1691,7 @@ describe.each([
|
||||||
|
|
||||||
await withEnv({ SELF_HOSTED: "true" }, async () => {
|
await withEnv({ SELF_HOSTED: "true" }, async () => {
|
||||||
return context.doInAppContext(config.getAppId(), async () => {
|
return context.doInAppContext(config.getAppId(), async () => {
|
||||||
const enriched: Row[] = await outputProcessing(table, [row])
|
const enriched: Row[] = await outputProcessing(testTable, [row])
|
||||||
const [targetRow] = enriched
|
const [targetRow] = enriched
|
||||||
const attachmentEntries = Array.isArray(targetRow[field])
|
const attachmentEntries = Array.isArray(targetRow[field])
|
||||||
? targetRow[field]
|
? targetRow[field]
|
||||||
|
|
|
@ -30,6 +30,7 @@ import {
|
||||||
withEnv as withCoreEnv,
|
withEnv as withCoreEnv,
|
||||||
setEnv as setCoreEnv,
|
setEnv as setCoreEnv,
|
||||||
} from "@budibase/backend-core"
|
} from "@budibase/backend-core"
|
||||||
|
import sdk from "../../../sdk"
|
||||||
|
|
||||||
describe.each([
|
describe.each([
|
||||||
["lucene", undefined],
|
["lucene", undefined],
|
||||||
|
@ -120,6 +121,7 @@ describe.each([
|
||||||
})
|
})
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
|
jest.clearAllMocks()
|
||||||
mocks.licenses.useCloudFree()
|
mocks.licenses.useCloudFree()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -1602,6 +1604,28 @@ describe.each([
|
||||||
})
|
})
|
||||||
expect(response.rows).toHaveLength(0)
|
expect(response.rows).toHaveLength(0)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it("queries the row api passing the view fields only", async () => {
|
||||||
|
const searchSpy = jest.spyOn(sdk.rows, "search")
|
||||||
|
|
||||||
|
const view = await config.api.viewV2.create({
|
||||||
|
tableId: table._id!,
|
||||||
|
name: generator.guid(),
|
||||||
|
schema: {
|
||||||
|
id: { visible: true },
|
||||||
|
one: { visible: false },
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
await config.api.viewV2.search(view.id, { query: {} })
|
||||||
|
expect(searchSpy).toHaveBeenCalledTimes(1)
|
||||||
|
|
||||||
|
expect(searchSpy).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
fields: ["id"],
|
||||||
|
})
|
||||||
|
)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe("permissions", () => {
|
describe("permissions", () => {
|
||||||
|
|
|
@ -0,0 +1,51 @@
|
||||||
|
import {
|
||||||
|
AutomationActionStepId,
|
||||||
|
AutomationCustomIOType,
|
||||||
|
AutomationIOType,
|
||||||
|
AutomationStepDefinition,
|
||||||
|
AutomationStepType,
|
||||||
|
} from "@budibase/types"
|
||||||
|
|
||||||
|
export const definition: AutomationStepDefinition = {
|
||||||
|
name: "Branch",
|
||||||
|
icon: "Branch3",
|
||||||
|
tagline: "Branch from this step",
|
||||||
|
description: "Branching",
|
||||||
|
stepId: AutomationActionStepId.BRANCH,
|
||||||
|
internal: true,
|
||||||
|
features: {},
|
||||||
|
inputs: {},
|
||||||
|
schema: {
|
||||||
|
inputs: {
|
||||||
|
properties: {
|
||||||
|
branches: {
|
||||||
|
properties: {
|
||||||
|
name: {
|
||||||
|
type: AutomationIOType.STRING,
|
||||||
|
},
|
||||||
|
condition: {
|
||||||
|
customType: AutomationCustomIOType.FILTERS,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
children: {
|
||||||
|
type: AutomationIOType.ARRAY,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
required: ["conditions"],
|
||||||
|
},
|
||||||
|
outputs: {
|
||||||
|
properties: {
|
||||||
|
branchName: {
|
||||||
|
type: AutomationIOType.STRING,
|
||||||
|
},
|
||||||
|
result: {
|
||||||
|
type: AutomationIOType.BOOLEAN,
|
||||||
|
description: "Whether the condition was met",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
required: ["output"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
type: AutomationStepType.LOGIC,
|
||||||
|
}
|
|
@ -7,7 +7,7 @@ import {
|
||||||
ServerLogStepOutputs,
|
ServerLogStepOutputs,
|
||||||
FieldType,
|
FieldType,
|
||||||
} from "@budibase/types"
|
} from "@budibase/types"
|
||||||
import { createAutomationBuilder } from "../utilities/AutomationBuilder"
|
import { createAutomationBuilder } from "../utilities/AutomationTestBuilder"
|
||||||
import { DatabaseName } from "../../../integrations/tests/utils"
|
import { DatabaseName } from "../../../integrations/tests/utils"
|
||||||
|
|
||||||
describe("Automation Scenarios", () => {
|
describe("Automation Scenarios", () => {
|
||||||
|
@ -23,6 +23,43 @@ describe("Automation Scenarios", () => {
|
||||||
|
|
||||||
afterAll(setup.afterAll)
|
afterAll(setup.afterAll)
|
||||||
|
|
||||||
|
// eslint-disable-next-line jest/no-commented-out-tests
|
||||||
|
// describe("Branching automations", () => {
|
||||||
|
// eslint-disable-next-line jest/no-commented-out-tests
|
||||||
|
// it("should run an automation with a trigger, loop, and create row step", async () => {
|
||||||
|
// const builder = createAutomationBuilder({
|
||||||
|
// name: "Test Trigger with Loop and Create Row",
|
||||||
|
// })
|
||||||
|
|
||||||
|
// builder
|
||||||
|
// .serverLog({ text: "Starting automation" })
|
||||||
|
// .branch({
|
||||||
|
// topLevelBranch1: {
|
||||||
|
// steps: stepBuilder =>
|
||||||
|
// stepBuilder.serverLog({ text: "Branch 1" }).branch({
|
||||||
|
// branch1: {
|
||||||
|
// steps: stepBuilder =>
|
||||||
|
// stepBuilder.serverLog({ text: "Branch 1.1" }),
|
||||||
|
// condition: { notEmpty: { column: 10 } },
|
||||||
|
// },
|
||||||
|
// branch2: {
|
||||||
|
// steps: stepBuilder =>
|
||||||
|
// stepBuilder.serverLog({ text: "Branch 1.2" }),
|
||||||
|
// condition: { fuzzy: { column: "sadsd" } },
|
||||||
|
// },
|
||||||
|
// }),
|
||||||
|
// condition: { equal: { column: 10 } },
|
||||||
|
// },
|
||||||
|
// topLevelBranch2: {
|
||||||
|
// steps: stepBuilder => stepBuilder.serverLog({ text: "Branch 2" }),
|
||||||
|
// condition: { equal: { column: 20 } },
|
||||||
|
// },
|
||||||
|
// })
|
||||||
|
// .run()
|
||||||
|
// })
|
||||||
|
|
||||||
|
// })
|
||||||
|
|
||||||
describe("Loop automations", () => {
|
describe("Loop automations", () => {
|
||||||
it("should run an automation with a trigger, loop, and create row step", async () => {
|
it("should run an automation with a trigger, loop, and create row step", async () => {
|
||||||
const builder = createAutomationBuilder({
|
const builder = createAutomationBuilder({
|
||||||
|
|
|
@ -30,10 +30,13 @@ import {
|
||||||
AutomationStepInputs,
|
AutomationStepInputs,
|
||||||
AutomationTriggerInputs,
|
AutomationTriggerInputs,
|
||||||
ServerLogStepInputs,
|
ServerLogStepInputs,
|
||||||
|
BranchStepInputs,
|
||||||
|
SearchFilters,
|
||||||
|
Branch,
|
||||||
} from "@budibase/types"
|
} from "@budibase/types"
|
||||||
import {} from "../../steps/loop"
|
|
||||||
import TestConfiguration from "../../../tests/utilities/TestConfiguration"
|
import TestConfiguration from "../../../tests/utilities/TestConfiguration"
|
||||||
import * as setup from "../utilities"
|
import * as setup from "../utilities"
|
||||||
|
import { definition } from "../../../automations/steps/branch"
|
||||||
|
|
||||||
type TriggerOutputs =
|
type TriggerOutputs =
|
||||||
| RowCreatedTriggerOutputs
|
| RowCreatedTriggerOutputs
|
||||||
|
@ -43,69 +46,56 @@ type TriggerOutputs =
|
||||||
| CronTriggerOutputs
|
| CronTriggerOutputs
|
||||||
| undefined
|
| undefined
|
||||||
|
|
||||||
class AutomationBuilder {
|
type StepBuilderFunction = (stepBuilder: StepBuilder) => void
|
||||||
private automationConfig: Automation = {
|
|
||||||
name: "",
|
|
||||||
definition: {
|
|
||||||
steps: [],
|
|
||||||
trigger: {} as AutomationTrigger,
|
|
||||||
},
|
|
||||||
type: "automation",
|
|
||||||
appId: setup.getConfig().getAppId(),
|
|
||||||
}
|
|
||||||
private config: TestConfiguration = setup.getConfig()
|
|
||||||
private triggerOutputs: TriggerOutputs
|
|
||||||
private triggerSet: boolean = false
|
|
||||||
|
|
||||||
constructor(options: { name?: string } = {}) {
|
type BranchConfig = {
|
||||||
this.automationConfig.name = options.name || `Test Automation ${uuidv4()}`
|
[key: string]: {
|
||||||
|
steps: StepBuilderFunction
|
||||||
|
condition: SearchFilters
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class BaseStepBuilder {
|
||||||
|
protected steps: AutomationStep[] = []
|
||||||
|
|
||||||
|
protected step<TStep extends AutomationActionStepId>(
|
||||||
|
stepId: TStep,
|
||||||
|
stepSchema: Omit<AutomationStep, "id" | "stepId" | "inputs">,
|
||||||
|
inputs: AutomationStepInputs<TStep>
|
||||||
|
): this {
|
||||||
|
this.steps.push({
|
||||||
|
...stepSchema,
|
||||||
|
inputs: inputs as any,
|
||||||
|
id: uuidv4(),
|
||||||
|
stepId,
|
||||||
|
})
|
||||||
|
return this
|
||||||
}
|
}
|
||||||
|
|
||||||
// TRIGGERS
|
protected addBranchStep(branchConfig: BranchConfig): void {
|
||||||
rowSaved(inputs: RowCreatedTriggerInputs, outputs: RowCreatedTriggerOutputs) {
|
const branchStepInputs: BranchStepInputs = {
|
||||||
this.triggerOutputs = outputs
|
branches: [] as Branch[],
|
||||||
return this.trigger(
|
children: {},
|
||||||
TRIGGER_DEFINITIONS.ROW_SAVED,
|
}
|
||||||
AutomationTriggerStepId.ROW_SAVED,
|
|
||||||
inputs,
|
|
||||||
outputs
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
rowUpdated(
|
Object.entries(branchConfig).forEach(([key, branch]) => {
|
||||||
inputs: RowUpdatedTriggerInputs,
|
const stepBuilder = new StepBuilder()
|
||||||
outputs: RowUpdatedTriggerOutputs
|
branch.steps(stepBuilder)
|
||||||
) {
|
|
||||||
this.triggerOutputs = outputs
|
|
||||||
return this.trigger(
|
|
||||||
TRIGGER_DEFINITIONS.ROW_UPDATED,
|
|
||||||
AutomationTriggerStepId.ROW_UPDATED,
|
|
||||||
inputs,
|
|
||||||
outputs
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
rowDeleted(
|
branchStepInputs.branches.push({
|
||||||
inputs: RowDeletedTriggerInputs,
|
name: key,
|
||||||
outputs: RowDeletedTriggerOutputs
|
condition: branch.condition,
|
||||||
) {
|
})
|
||||||
this.triggerOutputs = outputs
|
branchStepInputs.children![key] = stepBuilder.build()
|
||||||
return this.trigger(
|
})
|
||||||
TRIGGER_DEFINITIONS.ROW_DELETED,
|
|
||||||
AutomationTriggerStepId.ROW_DELETED,
|
|
||||||
inputs,
|
|
||||||
outputs
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
appAction(outputs: AppActionTriggerOutputs, inputs?: AppActionTriggerInputs) {
|
const branchStep: AutomationStep = {
|
||||||
this.triggerOutputs = outputs
|
...definition,
|
||||||
return this.trigger(
|
id: uuidv4(),
|
||||||
TRIGGER_DEFINITIONS.APP,
|
stepId: AutomationActionStepId.BRANCH,
|
||||||
AutomationTriggerStepId.APP,
|
inputs: branchStepInputs,
|
||||||
inputs,
|
}
|
||||||
outputs
|
this.steps.push(branchStep)
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// STEPS
|
// STEPS
|
||||||
|
@ -171,6 +161,84 @@ class AutomationBuilder {
|
||||||
input
|
input
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
class StepBuilder extends BaseStepBuilder {
|
||||||
|
build(): AutomationStep[] {
|
||||||
|
return this.steps
|
||||||
|
}
|
||||||
|
|
||||||
|
branch(branchConfig: BranchConfig): this {
|
||||||
|
this.addBranchStep(branchConfig)
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class AutomationBuilder extends BaseStepBuilder {
|
||||||
|
private automationConfig: Automation
|
||||||
|
private config: TestConfiguration
|
||||||
|
private triggerOutputs: any
|
||||||
|
private triggerSet: boolean = false
|
||||||
|
|
||||||
|
constructor(options: { name?: string } = {}) {
|
||||||
|
super()
|
||||||
|
this.automationConfig = {
|
||||||
|
name: options.name || `Test Automation ${uuidv4()}`,
|
||||||
|
definition: {
|
||||||
|
steps: [],
|
||||||
|
trigger: {} as AutomationTrigger,
|
||||||
|
},
|
||||||
|
type: "automation",
|
||||||
|
appId: setup.getConfig().getAppId(),
|
||||||
|
}
|
||||||
|
this.config = setup.getConfig()
|
||||||
|
}
|
||||||
|
|
||||||
|
// TRIGGERS
|
||||||
|
rowSaved(inputs: RowCreatedTriggerInputs, outputs: RowCreatedTriggerOutputs) {
|
||||||
|
this.triggerOutputs = outputs
|
||||||
|
return this.trigger(
|
||||||
|
TRIGGER_DEFINITIONS.ROW_SAVED,
|
||||||
|
AutomationTriggerStepId.ROW_SAVED,
|
||||||
|
inputs,
|
||||||
|
outputs
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
rowUpdated(
|
||||||
|
inputs: RowUpdatedTriggerInputs,
|
||||||
|
outputs: RowUpdatedTriggerOutputs
|
||||||
|
) {
|
||||||
|
this.triggerOutputs = outputs
|
||||||
|
return this.trigger(
|
||||||
|
TRIGGER_DEFINITIONS.ROW_UPDATED,
|
||||||
|
AutomationTriggerStepId.ROW_UPDATED,
|
||||||
|
inputs,
|
||||||
|
outputs
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
rowDeleted(
|
||||||
|
inputs: RowDeletedTriggerInputs,
|
||||||
|
outputs: RowDeletedTriggerOutputs
|
||||||
|
) {
|
||||||
|
this.triggerOutputs = outputs
|
||||||
|
return this.trigger(
|
||||||
|
TRIGGER_DEFINITIONS.ROW_DELETED,
|
||||||
|
AutomationTriggerStepId.ROW_DELETED,
|
||||||
|
inputs,
|
||||||
|
outputs
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
appAction(outputs: AppActionTriggerOutputs, inputs?: AppActionTriggerInputs) {
|
||||||
|
this.triggerOutputs = outputs
|
||||||
|
return this.trigger(
|
||||||
|
TRIGGER_DEFINITIONS.APP,
|
||||||
|
AutomationTriggerStepId.APP,
|
||||||
|
inputs,
|
||||||
|
outputs
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
private trigger<TStep extends AutomationTriggerStepId>(
|
private trigger<TStep extends AutomationTriggerStepId>(
|
||||||
triggerSchema: AutomationTriggerDefinition,
|
triggerSchema: AutomationTriggerDefinition,
|
||||||
|
@ -181,7 +249,6 @@ class AutomationBuilder {
|
||||||
if (this.triggerSet) {
|
if (this.triggerSet) {
|
||||||
throw new Error("Only one trigger can be set for an automation.")
|
throw new Error("Only one trigger can be set for an automation.")
|
||||||
}
|
}
|
||||||
|
|
||||||
this.automationConfig.definition.trigger = {
|
this.automationConfig.definition.trigger = {
|
||||||
...triggerSchema,
|
...triggerSchema,
|
||||||
stepId,
|
stepId,
|
||||||
|
@ -194,21 +261,20 @@ class AutomationBuilder {
|
||||||
return this
|
return this
|
||||||
}
|
}
|
||||||
|
|
||||||
private step<TStep extends AutomationActionStepId>(
|
branch(branchConfig: BranchConfig): {
|
||||||
stepId: TStep,
|
run: () => Promise<AutomationResults>
|
||||||
stepSchema: Omit<AutomationStep, "id" | "stepId" | "inputs">,
|
} {
|
||||||
inputs: AutomationStepInputs<TStep>
|
this.addBranchStep(branchConfig)
|
||||||
): this {
|
return {
|
||||||
this.automationConfig.definition.steps.push({
|
run: () => this.run(),
|
||||||
...stepSchema,
|
}
|
||||||
inputs: inputs as any,
|
|
||||||
id: uuidv4(),
|
|
||||||
stepId,
|
|
||||||
})
|
|
||||||
return this
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async run() {
|
async run() {
|
||||||
|
if (!Object.keys(this.automationConfig.definition.trigger).length) {
|
||||||
|
throw new Error("Please add a trigger to this automation test")
|
||||||
|
}
|
||||||
|
this.automationConfig.definition.steps = this.steps
|
||||||
const automation = await this.config.createAutomation(this.automationConfig)
|
const automation = await this.config.createAutomation(this.automationConfig)
|
||||||
const results = await testAutomation(
|
const results = await testAutomation(
|
||||||
this.config,
|
this.config,
|
||||||
|
@ -218,7 +284,9 @@ class AutomationBuilder {
|
||||||
return this.processResults(results)
|
return this.processResults(results)
|
||||||
}
|
}
|
||||||
|
|
||||||
private processResults(results: { body: AutomationResults }) {
|
private processResults(results: {
|
||||||
|
body: AutomationResults
|
||||||
|
}): AutomationResults {
|
||||||
results.body.steps.shift()
|
results.body.steps.shift()
|
||||||
return {
|
return {
|
||||||
trigger: results.body.trigger,
|
trigger: results.body.trigger,
|
|
@ -34,7 +34,7 @@ type TemplateType = {
|
||||||
|
|
||||||
function rewriteAttachmentUrl(appId: string, attachment: RowAttachment) {
|
function rewriteAttachmentUrl(appId: string, attachment: RowAttachment) {
|
||||||
// URL looks like: /prod-budi-app-assets/appId/attachments/file.csv
|
// URL looks like: /prod-budi-app-assets/appId/attachments/file.csv
|
||||||
const urlParts = attachment.key.split("/")
|
const urlParts = attachment.key?.split("/") || []
|
||||||
// remove the app ID
|
// remove the app ID
|
||||||
urlParts.shift()
|
urlParts.shift()
|
||||||
// add new app ID
|
// add new app ID
|
||||||
|
|
|
@ -0,0 +1,109 @@
|
||||||
|
import { db } from "@budibase/backend-core"
|
||||||
|
import {
|
||||||
|
FieldType,
|
||||||
|
isLogicalSearchOperator,
|
||||||
|
SearchFilters,
|
||||||
|
Table,
|
||||||
|
} from "@budibase/types"
|
||||||
|
import { cloneDeep } from "lodash/fp"
|
||||||
|
import sdk from "../../../sdk"
|
||||||
|
|
||||||
|
export const removeInvalidFilters = (
|
||||||
|
filters: SearchFilters,
|
||||||
|
validFields: string[]
|
||||||
|
) => {
|
||||||
|
const result = cloneDeep(filters)
|
||||||
|
|
||||||
|
validFields = validFields.map(f => f.toLowerCase())
|
||||||
|
for (const filterKey of Object.keys(result) as (keyof SearchFilters)[]) {
|
||||||
|
if (typeof result[filterKey] !== "object") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if (isLogicalSearchOperator(filterKey)) {
|
||||||
|
const resultingConditions: SearchFilters[] = []
|
||||||
|
for (const condition of result[filterKey].conditions) {
|
||||||
|
const resultingCondition = removeInvalidFilters(condition, validFields)
|
||||||
|
if (Object.keys(resultingCondition).length) {
|
||||||
|
resultingConditions.push(resultingCondition)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (resultingConditions.length) {
|
||||||
|
result[filterKey].conditions = resultingConditions
|
||||||
|
} else {
|
||||||
|
delete result[filterKey]
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
const filter = result[filterKey]
|
||||||
|
for (const columnKey of Object.keys(filter)) {
|
||||||
|
const possibleKeys = [columnKey, db.removeKeyNumbering(columnKey)].map(
|
||||||
|
c => c.toLowerCase()
|
||||||
|
)
|
||||||
|
if (!validFields.some(f => possibleKeys.includes(f.toLowerCase()))) {
|
||||||
|
delete filter[columnKey]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!Object.keys(filter).length) {
|
||||||
|
delete result[filterKey]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getQueryableFields = async (
|
||||||
|
fields: string[],
|
||||||
|
table: Table
|
||||||
|
): Promise<string[]> => {
|
||||||
|
const extractTableFields = async (
|
||||||
|
table: Table,
|
||||||
|
allowedFields: string[],
|
||||||
|
fromTables: string[]
|
||||||
|
): Promise<string[]> => {
|
||||||
|
const result = []
|
||||||
|
for (const field of Object.keys(table.schema).filter(
|
||||||
|
f => allowedFields.includes(f) && table.schema[f].visible !== false
|
||||||
|
)) {
|
||||||
|
const subSchema = table.schema[field]
|
||||||
|
if (subSchema.type === FieldType.LINK) {
|
||||||
|
if (fromTables.includes(subSchema.tableId)) {
|
||||||
|
// avoid circular loops
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const relatedTable = await sdk.tables.getTable(subSchema.tableId)
|
||||||
|
const relatedFields = await extractTableFields(
|
||||||
|
relatedTable,
|
||||||
|
Object.keys(relatedTable.schema),
|
||||||
|
[...fromTables, subSchema.tableId]
|
||||||
|
)
|
||||||
|
|
||||||
|
result.push(
|
||||||
|
...relatedFields.flatMap(f => [
|
||||||
|
`${subSchema.name}.${f}`,
|
||||||
|
// should be able to filter by relationship using table name
|
||||||
|
`${relatedTable.name}.${f}`,
|
||||||
|
])
|
||||||
|
)
|
||||||
|
} catch (err: any) {
|
||||||
|
// if related table is removed, ignore
|
||||||
|
if (err.status !== 404) {
|
||||||
|
throw err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
result.push(field)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = [
|
||||||
|
"_id", // Querying by _id is always allowed, even if it's never part of the schema
|
||||||
|
]
|
||||||
|
|
||||||
|
result.push(...(await extractTableFields(table, fields, [table._id!])))
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
|
@ -14,6 +14,7 @@ import sdk from "../../index"
|
||||||
import { searchInputMapping } from "./search/utils"
|
import { searchInputMapping } from "./search/utils"
|
||||||
import { db as dbCore } from "@budibase/backend-core"
|
import { db as dbCore } from "@budibase/backend-core"
|
||||||
import tracer from "dd-trace"
|
import tracer from "dd-trace"
|
||||||
|
import { getQueryableFields, removeInvalidFilters } from "./queryUtils"
|
||||||
|
|
||||||
export { isValidFilter } from "../../../integrations/utils"
|
export { isValidFilter } from "../../../integrations/utils"
|
||||||
|
|
||||||
|
@ -73,6 +74,18 @@ export async function search(
|
||||||
const table = await sdk.tables.getTable(options.tableId)
|
const table = await sdk.tables.getTable(options.tableId)
|
||||||
options = searchInputMapping(table, options)
|
options = searchInputMapping(table, options)
|
||||||
|
|
||||||
|
if (options.query) {
|
||||||
|
const tableFields = Object.keys(table.schema).filter(
|
||||||
|
f => table.schema[f].visible !== false
|
||||||
|
)
|
||||||
|
|
||||||
|
const queriableFields = await getQueryableFields(
|
||||||
|
options.fields?.filter(f => tableFields.includes(f)) ?? tableFields,
|
||||||
|
table
|
||||||
|
)
|
||||||
|
options.query = removeInvalidFilters(options.query, queriableFields)
|
||||||
|
}
|
||||||
|
|
||||||
let result: SearchResponse<Row>
|
let result: SearchResponse<Row>
|
||||||
if (isExternalTable) {
|
if (isExternalTable) {
|
||||||
span?.addTags({ searchType: "external" })
|
span?.addTags({ searchType: "external" })
|
||||||
|
|
|
@ -297,7 +297,7 @@ export async function search(
|
||||||
throw new Error("Unable to find table")
|
throw new Error("Unable to find table")
|
||||||
}
|
}
|
||||||
|
|
||||||
const relationships = buildInternalRelationships(table)
|
const relationships = buildInternalRelationships(table, allTables)
|
||||||
|
|
||||||
const searchFilters: SearchFilters = {
|
const searchFilters: SearchFilters = {
|
||||||
...cleanupFilters(query, table, allTables),
|
...cleanupFilters(query, table, allTables),
|
||||||
|
|
|
@ -1,4 +1,11 @@
|
||||||
import { Datasource, FieldType, Row, Table } from "@budibase/types"
|
import {
|
||||||
|
AutoColumnFieldMetadata,
|
||||||
|
AutoFieldSubType,
|
||||||
|
Datasource,
|
||||||
|
FieldType,
|
||||||
|
NumberFieldMetadata,
|
||||||
|
Table,
|
||||||
|
} from "@budibase/types"
|
||||||
|
|
||||||
import TestConfiguration from "../../../../../tests/utilities/TestConfiguration"
|
import TestConfiguration from "../../../../../tests/utilities/TestConfiguration"
|
||||||
import { search } from "../../../../../sdk/app/rows/search"
|
import { search } from "../../../../../sdk/app/rows/search"
|
||||||
|
@ -32,7 +39,6 @@ describe.each([
|
||||||
let envCleanup: (() => void) | undefined
|
let envCleanup: (() => void) | undefined
|
||||||
let datasource: Datasource | undefined
|
let datasource: Datasource | undefined
|
||||||
let table: Table
|
let table: Table
|
||||||
let rows: Row[]
|
|
||||||
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
await withCoreEnv({ SQS_SEARCH_ENABLE: isSqs ? "true" : "false" }, () =>
|
await withCoreEnv({ SQS_SEARCH_ENABLE: isSqs ? "true" : "false" }, () =>
|
||||||
|
@ -51,16 +57,28 @@ describe.each([
|
||||||
datasource: await dsProvider,
|
datasource: await dsProvider,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
const idFieldSchema: NumberFieldMetadata | AutoColumnFieldMetadata =
|
||||||
|
isInternal
|
||||||
|
? {
|
||||||
|
name: "id",
|
||||||
|
type: FieldType.AUTO,
|
||||||
|
subtype: AutoFieldSubType.AUTO_ID,
|
||||||
|
autocolumn: true,
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
name: "id",
|
||||||
|
type: FieldType.NUMBER,
|
||||||
|
autocolumn: true,
|
||||||
|
}
|
||||||
|
|
||||||
table = await config.api.table.save(
|
table = await config.api.table.save(
|
||||||
tableForDatasource(datasource, {
|
tableForDatasource(datasource, {
|
||||||
primary: ["id"],
|
primary: ["id"],
|
||||||
schema: {
|
schema: {
|
||||||
id: {
|
id: idFieldSchema,
|
||||||
name: "id",
|
|
||||||
type: FieldType.NUMBER,
|
|
||||||
autocolumn: true,
|
|
||||||
},
|
|
||||||
name: {
|
name: {
|
||||||
name: "name",
|
name: "name",
|
||||||
type: FieldType.STRING,
|
type: FieldType.STRING,
|
||||||
|
@ -81,16 +99,13 @@ describe.each([
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
|
||||||
rows = []
|
|
||||||
for (let i = 0; i < 10; i++) {
|
for (let i = 0; i < 10; i++) {
|
||||||
rows.push(
|
await config.api.row.save(table._id!, {
|
||||||
await config.api.row.save(table._id!, {
|
name: generator.first(),
|
||||||
name: generator.first(),
|
surname: generator.last(),
|
||||||
surname: generator.last(),
|
age: generator.age(),
|
||||||
age: generator.age(),
|
address: generator.address(),
|
||||||
address: generator.address(),
|
})
|
||||||
})
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -138,4 +153,100 @@ describe.each([
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it("does not allow accessing hidden fields", async () => {
|
||||||
|
await config.doInContext(config.appId, async () => {
|
||||||
|
await config.api.table.save({
|
||||||
|
...table,
|
||||||
|
schema: {
|
||||||
|
...table.schema,
|
||||||
|
name: {
|
||||||
|
...table.schema.name,
|
||||||
|
visible: true,
|
||||||
|
},
|
||||||
|
age: {
|
||||||
|
...table.schema.age,
|
||||||
|
visible: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
const result = await search({
|
||||||
|
tableId: table._id!,
|
||||||
|
query: {},
|
||||||
|
})
|
||||||
|
expect(result.rows).toHaveLength(10)
|
||||||
|
for (const row of result.rows) {
|
||||||
|
const keys = Object.keys(row)
|
||||||
|
expect(keys).toContain("name")
|
||||||
|
expect(keys).toContain("surname")
|
||||||
|
expect(keys).toContain("address")
|
||||||
|
expect(keys).not.toContain("age")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it("does not allow accessing hidden fields even if requested", async () => {
|
||||||
|
await config.doInContext(config.appId, async () => {
|
||||||
|
await config.api.table.save({
|
||||||
|
...table,
|
||||||
|
schema: {
|
||||||
|
...table.schema,
|
||||||
|
name: {
|
||||||
|
...table.schema.name,
|
||||||
|
visible: true,
|
||||||
|
},
|
||||||
|
age: {
|
||||||
|
...table.schema.age,
|
||||||
|
visible: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
const result = await search({
|
||||||
|
tableId: table._id!,
|
||||||
|
query: {},
|
||||||
|
fields: ["name", "age"],
|
||||||
|
})
|
||||||
|
expect(result.rows).toHaveLength(10)
|
||||||
|
for (const row of result.rows) {
|
||||||
|
const keys = Object.keys(row)
|
||||||
|
expect(keys).toContain("name")
|
||||||
|
expect(keys).not.toContain("age")
|
||||||
|
expect(keys).not.toContain("surname")
|
||||||
|
expect(keys).not.toContain("address")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
!isLucene &&
|
||||||
|
it.each([
|
||||||
|
[["id", "name", "age"], 3],
|
||||||
|
[["name", "age"], 10],
|
||||||
|
])(
|
||||||
|
"cannot query by non search fields (fields: %s)",
|
||||||
|
async (queryFields, expectedRows) => {
|
||||||
|
await config.doInContext(config.appId, async () => {
|
||||||
|
const { rows } = await search({
|
||||||
|
tableId: table._id!,
|
||||||
|
query: {
|
||||||
|
$or: {
|
||||||
|
conditions: [
|
||||||
|
{
|
||||||
|
$and: {
|
||||||
|
conditions: [
|
||||||
|
{ range: { id: { low: 2, high: 4 } } },
|
||||||
|
{ range: { id: { low: 3, high: 5 } } },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{ equal: { id: 7 } },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
fields: queryFields,
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(rows).toHaveLength(expectedRows)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
)
|
||||||
})
|
})
|
||||||
|
|
|
@ -0,0 +1,563 @@
|
||||||
|
import {
|
||||||
|
FieldType,
|
||||||
|
RelationshipType,
|
||||||
|
SearchFilters,
|
||||||
|
Table,
|
||||||
|
} from "@budibase/types"
|
||||||
|
import { getQueryableFields, removeInvalidFilters } from "../queryUtils"
|
||||||
|
import { structures } from "../../../../api/routes/tests/utilities"
|
||||||
|
import TestConfiguration from "../../../../tests/utilities/TestConfiguration"
|
||||||
|
|
||||||
|
describe("query utils", () => {
|
||||||
|
describe("removeInvalidFilters", () => {
|
||||||
|
const fullFilters: SearchFilters = {
|
||||||
|
equal: { one: "foo" },
|
||||||
|
$or: {
|
||||||
|
conditions: [
|
||||||
|
{
|
||||||
|
equal: { one: "foo2", two: "bar" },
|
||||||
|
notEmpty: { one: null },
|
||||||
|
$and: {
|
||||||
|
conditions: [
|
||||||
|
{
|
||||||
|
equal: { three: "baz" },
|
||||||
|
notEmpty: { forth: null },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
$and: {
|
||||||
|
conditions: [{ equal: { one: "foo2" }, notEmpty: { one: null } }],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
it("can filter empty queries", () => {
|
||||||
|
const filters: SearchFilters = {}
|
||||||
|
const result = removeInvalidFilters(filters, [])
|
||||||
|
expect(result).toEqual({})
|
||||||
|
})
|
||||||
|
|
||||||
|
it("does not trim any valid field", () => {
|
||||||
|
const result = removeInvalidFilters(fullFilters, [
|
||||||
|
"one",
|
||||||
|
"two",
|
||||||
|
"three",
|
||||||
|
"forth",
|
||||||
|
])
|
||||||
|
expect(result).toEqual(fullFilters)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("trims invalid field", () => {
|
||||||
|
const result = removeInvalidFilters(fullFilters, [
|
||||||
|
"one",
|
||||||
|
"three",
|
||||||
|
"forth",
|
||||||
|
])
|
||||||
|
expect(result).toEqual({
|
||||||
|
equal: { one: "foo" },
|
||||||
|
$or: {
|
||||||
|
conditions: [
|
||||||
|
{
|
||||||
|
equal: { one: "foo2" },
|
||||||
|
notEmpty: { one: null },
|
||||||
|
$and: {
|
||||||
|
conditions: [
|
||||||
|
{
|
||||||
|
equal: { three: "baz" },
|
||||||
|
notEmpty: { forth: null },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
$and: {
|
||||||
|
conditions: [{ equal: { one: "foo2" }, notEmpty: { one: null } }],
|
||||||
|
},
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it("trims invalid field keeping a valid fields", () => {
|
||||||
|
const result = removeInvalidFilters(fullFilters, ["three", "forth"])
|
||||||
|
const expected: SearchFilters = {
|
||||||
|
$or: {
|
||||||
|
conditions: [
|
||||||
|
{
|
||||||
|
$and: {
|
||||||
|
conditions: [
|
||||||
|
{
|
||||||
|
equal: { three: "baz" },
|
||||||
|
notEmpty: { forth: null },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
expect(result).toEqual(expected)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("keeps filter key numering", () => {
|
||||||
|
const prefixedFilters: SearchFilters = {
|
||||||
|
equal: { "1:one": "foo" },
|
||||||
|
$or: {
|
||||||
|
conditions: [
|
||||||
|
{
|
||||||
|
equal: { "2:one": "foo2", "3:two": "bar" },
|
||||||
|
notEmpty: { "4:one": null },
|
||||||
|
$and: {
|
||||||
|
conditions: [
|
||||||
|
{
|
||||||
|
equal: { "5:three": "baz", two: "bar2" },
|
||||||
|
notEmpty: { forth: null },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
$and: {
|
||||||
|
conditions: [{ equal: { "6:one": "foo2" }, notEmpty: { one: null } }],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = removeInvalidFilters(prefixedFilters, [
|
||||||
|
"one",
|
||||||
|
"three",
|
||||||
|
"forth",
|
||||||
|
])
|
||||||
|
expect(result).toEqual({
|
||||||
|
equal: { "1:one": "foo" },
|
||||||
|
$or: {
|
||||||
|
conditions: [
|
||||||
|
{
|
||||||
|
equal: { "2:one": "foo2" },
|
||||||
|
notEmpty: { "4:one": null },
|
||||||
|
$and: {
|
||||||
|
conditions: [
|
||||||
|
{
|
||||||
|
equal: { "5:three": "baz" },
|
||||||
|
notEmpty: { forth: null },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
$and: {
|
||||||
|
conditions: [{ equal: { "6:one": "foo2" }, notEmpty: { one: null } }],
|
||||||
|
},
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it("handles relationship filters", () => {
|
||||||
|
const prefixedFilters: SearchFilters = {
|
||||||
|
$or: {
|
||||||
|
conditions: [
|
||||||
|
{ equal: { "1:other.one": "foo" } },
|
||||||
|
{
|
||||||
|
equal: {
|
||||||
|
"2:other.one": "foo2",
|
||||||
|
"3:other.two": "bar",
|
||||||
|
"4:other.three": "baz",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{ equal: { "another.three": "baz2" } },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = removeInvalidFilters(prefixedFilters, [
|
||||||
|
"other.one",
|
||||||
|
"other.two",
|
||||||
|
"another.three",
|
||||||
|
])
|
||||||
|
expect(result).toEqual({
|
||||||
|
$or: {
|
||||||
|
conditions: [
|
||||||
|
{ equal: { "1:other.one": "foo" } },
|
||||||
|
{ equal: { "2:other.one": "foo2", "3:other.two": "bar" } },
|
||||||
|
{ equal: { "another.three": "baz2" } },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("getQueryableFields", () => {
|
||||||
|
const config = new TestConfiguration()
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
await config.init()
|
||||||
|
})
|
||||||
|
|
||||||
|
it("returns table schema fields and _id", async () => {
|
||||||
|
const table: Table = await config.api.table.save({
|
||||||
|
...structures.basicTable(),
|
||||||
|
schema: {
|
||||||
|
name: { name: "name", type: FieldType.STRING },
|
||||||
|
age: { name: "age", type: FieldType.NUMBER },
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const result = await getQueryableFields(Object.keys(table.schema), table)
|
||||||
|
expect(result).toEqual(["_id", "name", "age"])
|
||||||
|
})
|
||||||
|
|
||||||
|
it("excludes hidden fields", async () => {
|
||||||
|
const table: Table = await config.api.table.save({
|
||||||
|
...structures.basicTable(),
|
||||||
|
schema: {
|
||||||
|
name: { name: "name", type: FieldType.STRING },
|
||||||
|
age: { name: "age", type: FieldType.NUMBER, visible: false },
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const result = await getQueryableFields(Object.keys(table.schema), table)
|
||||||
|
expect(result).toEqual(["_id", "name"])
|
||||||
|
})
|
||||||
|
|
||||||
|
it("includes relationship fields", async () => {
|
||||||
|
const aux: Table = await config.api.table.save({
|
||||||
|
...structures.basicTable(),
|
||||||
|
name: "auxTable",
|
||||||
|
schema: {
|
||||||
|
title: { name: "title", type: FieldType.STRING },
|
||||||
|
name: { name: "name", type: FieldType.STRING },
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const table: Table = await config.api.table.save({
|
||||||
|
...structures.basicTable(),
|
||||||
|
schema: {
|
||||||
|
name: { name: "name", type: FieldType.STRING },
|
||||||
|
aux: {
|
||||||
|
name: "aux",
|
||||||
|
type: FieldType.LINK,
|
||||||
|
tableId: aux._id!,
|
||||||
|
relationshipType: RelationshipType.ONE_TO_MANY,
|
||||||
|
fieldName: "table",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const result = await config.doInContext(config.appId, () => {
|
||||||
|
return getQueryableFields(Object.keys(table.schema), table)
|
||||||
|
})
|
||||||
|
expect(result).toEqual([
|
||||||
|
"_id",
|
||||||
|
"name",
|
||||||
|
"aux.title",
|
||||||
|
"auxTable.title",
|
||||||
|
"aux.name",
|
||||||
|
"auxTable.name",
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
|
it("excludes hidden relationship fields", async () => {
|
||||||
|
const aux: Table = await config.api.table.save({
|
||||||
|
...structures.basicTable(),
|
||||||
|
name: "auxTable",
|
||||||
|
schema: {
|
||||||
|
title: { name: "title", type: FieldType.STRING, visible: false },
|
||||||
|
name: { name: "name", type: FieldType.STRING, visible: true },
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const table: Table = await config.api.table.save({
|
||||||
|
...structures.basicTable(),
|
||||||
|
schema: {
|
||||||
|
name: { name: "name", type: FieldType.STRING },
|
||||||
|
aux: {
|
||||||
|
name: "aux",
|
||||||
|
type: FieldType.LINK,
|
||||||
|
tableId: aux._id!,
|
||||||
|
relationshipType: RelationshipType.ONE_TO_MANY,
|
||||||
|
fieldName: "table",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const result = await config.doInContext(config.appId, () => {
|
||||||
|
return getQueryableFields(Object.keys(table.schema), table)
|
||||||
|
})
|
||||||
|
expect(result).toEqual(["_id", "name", "aux.name", "auxTable.name"])
|
||||||
|
})
|
||||||
|
|
||||||
|
it("excludes all relationship fields if hidden", async () => {
|
||||||
|
const aux: Table = await config.api.table.save({
|
||||||
|
...structures.basicTable(),
|
||||||
|
name: "auxTable",
|
||||||
|
schema: {
|
||||||
|
title: { name: "title", type: FieldType.STRING, visible: false },
|
||||||
|
name: { name: "name", type: FieldType.STRING, visible: true },
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const table: Table = await config.api.table.save({
|
||||||
|
...structures.basicTable(),
|
||||||
|
schema: {
|
||||||
|
name: { name: "name", type: FieldType.STRING },
|
||||||
|
aux: {
|
||||||
|
name: "aux",
|
||||||
|
type: FieldType.LINK,
|
||||||
|
tableId: aux._id!,
|
||||||
|
relationshipType: RelationshipType.ONE_TO_MANY,
|
||||||
|
fieldName: "table",
|
||||||
|
visible: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const result = await config.doInContext(config.appId, () => {
|
||||||
|
return getQueryableFields(Object.keys(table.schema), table)
|
||||||
|
})
|
||||||
|
expect(result).toEqual(["_id", "name"])
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("nested relationship", () => {
|
||||||
|
describe("one-to-many", () => {
|
||||||
|
let table: Table, aux1: Table, aux2: Table
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
const { _id: aux1Id } = await config.api.table.save({
|
||||||
|
...structures.basicTable(),
|
||||||
|
name: "aux1Table",
|
||||||
|
schema: {
|
||||||
|
name: { name: "name", type: FieldType.STRING },
|
||||||
|
},
|
||||||
|
})
|
||||||
|
const { _id: aux2Id } = await config.api.table.save({
|
||||||
|
...structures.basicTable(),
|
||||||
|
name: "aux2Table",
|
||||||
|
schema: {
|
||||||
|
title: { name: "title", type: FieldType.STRING },
|
||||||
|
aux1_1: {
|
||||||
|
name: "aux1_1",
|
||||||
|
type: FieldType.LINK,
|
||||||
|
tableId: aux1Id!,
|
||||||
|
relationshipType: RelationshipType.ONE_TO_MANY,
|
||||||
|
fieldName: "aux2_1",
|
||||||
|
},
|
||||||
|
aux1_2: {
|
||||||
|
name: "aux1_2",
|
||||||
|
type: FieldType.LINK,
|
||||||
|
tableId: aux1Id!,
|
||||||
|
relationshipType: RelationshipType.ONE_TO_MANY,
|
||||||
|
fieldName: "aux2_2",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const { _id: tableId } = await config.api.table.save({
|
||||||
|
...structures.basicTable(),
|
||||||
|
schema: {
|
||||||
|
name: { name: "name", type: FieldType.STRING },
|
||||||
|
aux1: {
|
||||||
|
name: "aux1",
|
||||||
|
type: FieldType.LINK,
|
||||||
|
tableId: aux1Id!,
|
||||||
|
relationshipType: RelationshipType.ONE_TO_MANY,
|
||||||
|
fieldName: "table",
|
||||||
|
},
|
||||||
|
aux2: {
|
||||||
|
name: "aux2",
|
||||||
|
type: FieldType.LINK,
|
||||||
|
tableId: aux2Id!,
|
||||||
|
relationshipType: RelationshipType.ONE_TO_MANY,
|
||||||
|
fieldName: "table",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
// We need to refech them to get the updated foreign keys
|
||||||
|
aux1 = await config.api.table.get(aux1Id!)
|
||||||
|
aux2 = await config.api.table.get(aux2Id!)
|
||||||
|
table = await config.api.table.get(tableId!)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("includes nested relationship fields from main table", async () => {
|
||||||
|
const result = await config.doInContext(config.appId, () => {
|
||||||
|
return getQueryableFields(Object.keys(table.schema), table)
|
||||||
|
})
|
||||||
|
expect(result).toEqual([
|
||||||
|
"_id",
|
||||||
|
"name",
|
||||||
|
// deep 1 aux1 primitive props
|
||||||
|
"aux1.name",
|
||||||
|
"aux1Table.name",
|
||||||
|
|
||||||
|
// deep 2 aux1 primitive props
|
||||||
|
"aux1.aux2_1.title",
|
||||||
|
"aux1Table.aux2_1.title",
|
||||||
|
"aux1.aux2Table.title",
|
||||||
|
"aux1Table.aux2Table.title",
|
||||||
|
|
||||||
|
// deep 2 aux2 primitive props
|
||||||
|
"aux1.aux2_2.title",
|
||||||
|
"aux1Table.aux2_2.title",
|
||||||
|
"aux1.aux2Table.title",
|
||||||
|
"aux1Table.aux2Table.title",
|
||||||
|
|
||||||
|
// deep 1 aux2 primitive props
|
||||||
|
"aux2.title",
|
||||||
|
"aux2Table.title",
|
||||||
|
|
||||||
|
// deep 2 aux2 primitive props
|
||||||
|
"aux2.aux1_1.name",
|
||||||
|
"aux2Table.aux1_1.name",
|
||||||
|
"aux2.aux1Table.name",
|
||||||
|
"aux2Table.aux1Table.name",
|
||||||
|
"aux2.aux1_2.name",
|
||||||
|
"aux2Table.aux1_2.name",
|
||||||
|
"aux2.aux1Table.name",
|
||||||
|
"aux2Table.aux1Table.name",
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
|
it("includes nested relationship fields from aux 1 table", async () => {
|
||||||
|
const result = await config.doInContext(config.appId, () => {
|
||||||
|
return getQueryableFields(Object.keys(aux1.schema), aux1)
|
||||||
|
})
|
||||||
|
expect(result).toEqual([
|
||||||
|
"_id",
|
||||||
|
"name",
|
||||||
|
|
||||||
|
// deep 1 aux2_1 primitive props
|
||||||
|
"aux2_1.title",
|
||||||
|
"aux2Table.title",
|
||||||
|
|
||||||
|
// deep 2 aux2_1 primitive props
|
||||||
|
"aux2_1.table.name",
|
||||||
|
"aux2Table.table.name",
|
||||||
|
"aux2_1.TestTable.name",
|
||||||
|
"aux2Table.TestTable.name",
|
||||||
|
|
||||||
|
// deep 1 aux2_2 primitive props
|
||||||
|
"aux2_2.title",
|
||||||
|
"aux2Table.title",
|
||||||
|
|
||||||
|
// deep 2 aux2_2 primitive props
|
||||||
|
"aux2_2.table.name",
|
||||||
|
"aux2Table.table.name",
|
||||||
|
"aux2_2.TestTable.name",
|
||||||
|
"aux2Table.TestTable.name",
|
||||||
|
|
||||||
|
// deep 1 table primitive props
|
||||||
|
"table.name",
|
||||||
|
"TestTable.name",
|
||||||
|
|
||||||
|
// deep 2 table primitive props
|
||||||
|
"table.aux2.title",
|
||||||
|
"TestTable.aux2.title",
|
||||||
|
"table.aux2Table.title",
|
||||||
|
"TestTable.aux2Table.title",
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
|
it("includes nested relationship fields from aux 2 table", async () => {
|
||||||
|
const result = await config.doInContext(config.appId, () => {
|
||||||
|
return getQueryableFields(Object.keys(aux2.schema), aux2)
|
||||||
|
})
|
||||||
|
expect(result).toEqual([
|
||||||
|
"_id",
|
||||||
|
"title",
|
||||||
|
|
||||||
|
// deep 1 aux1_1 primitive props
|
||||||
|
"aux1_1.name",
|
||||||
|
"aux1Table.name",
|
||||||
|
|
||||||
|
// deep 2 aux1_1 primitive props
|
||||||
|
"aux1_1.table.name",
|
||||||
|
"aux1Table.table.name",
|
||||||
|
"aux1_1.TestTable.name",
|
||||||
|
"aux1Table.TestTable.name",
|
||||||
|
|
||||||
|
// deep 1 aux1_2 primitive props
|
||||||
|
"aux1_2.name",
|
||||||
|
"aux1Table.name",
|
||||||
|
|
||||||
|
// deep 2 aux1_2 primitive props
|
||||||
|
"aux1_2.table.name",
|
||||||
|
"aux1Table.table.name",
|
||||||
|
"aux1_2.TestTable.name",
|
||||||
|
"aux1Table.TestTable.name",
|
||||||
|
|
||||||
|
// deep 1 table primitive props
|
||||||
|
"table.name",
|
||||||
|
"TestTable.name",
|
||||||
|
|
||||||
|
// deep 2 table primitive props
|
||||||
|
"table.aux1.name",
|
||||||
|
"TestTable.aux1.name",
|
||||||
|
"table.aux1Table.name",
|
||||||
|
"TestTable.aux1Table.name",
|
||||||
|
])
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("many-to-many", () => {
|
||||||
|
let table: Table, aux: Table
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
const { _id: auxId } = await config.api.table.save({
|
||||||
|
...structures.basicTable(),
|
||||||
|
name: "auxTable",
|
||||||
|
schema: {
|
||||||
|
title: { name: "title", type: FieldType.STRING },
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const { _id: tableId } = await config.api.table.save({
|
||||||
|
...structures.basicTable(),
|
||||||
|
schema: {
|
||||||
|
name: { name: "name", type: FieldType.STRING },
|
||||||
|
aux: {
|
||||||
|
name: "aux",
|
||||||
|
type: FieldType.LINK,
|
||||||
|
tableId: auxId!,
|
||||||
|
relationshipType: RelationshipType.MANY_TO_MANY,
|
||||||
|
fieldName: "table",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
// We need to refech them to get the updated foreign keys
|
||||||
|
aux = await config.api.table.get(auxId!)
|
||||||
|
table = await config.api.table.get(tableId!)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("includes nested relationship fields from main table", async () => {
|
||||||
|
const result = await config.doInContext(config.appId, () => {
|
||||||
|
return getQueryableFields(Object.keys(table.schema), table)
|
||||||
|
})
|
||||||
|
expect(result).toEqual([
|
||||||
|
"_id",
|
||||||
|
"name",
|
||||||
|
|
||||||
|
// deep 1 aux primitive props
|
||||||
|
"aux.title",
|
||||||
|
"auxTable.title",
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
|
it("includes nested relationship fields from aux table", async () => {
|
||||||
|
const result = await config.doInContext(config.appId, () => {
|
||||||
|
return getQueryableFields(Object.keys(aux.schema), aux)
|
||||||
|
})
|
||||||
|
expect(result).toEqual([
|
||||||
|
"_id",
|
||||||
|
"title",
|
||||||
|
|
||||||
|
// deep 1 dependency primitive props
|
||||||
|
"table.name",
|
||||||
|
"TestTable.name",
|
||||||
|
])
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
|
@ -44,8 +44,8 @@ export class AttachmentCleanup {
|
||||||
if (type === FieldType.ATTACHMENTS && Array.isArray(rowData)) {
|
if (type === FieldType.ATTACHMENTS && Array.isArray(rowData)) {
|
||||||
return rowData
|
return rowData
|
||||||
.filter(attachment => attachment.key)
|
.filter(attachment => attachment.key)
|
||||||
.map(attachment => attachment.key)
|
.map(attachment => attachment.key!)
|
||||||
} else if ("key" in rowData) {
|
} else if ("key" in rowData && rowData.key) {
|
||||||
return [rowData.key]
|
return [rowData.key]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -26,8 +26,13 @@ import {
|
||||||
processOutputBBReferences,
|
processOutputBBReferences,
|
||||||
} from "./bbReferenceProcessor"
|
} from "./bbReferenceProcessor"
|
||||||
import { isExternalTableID } from "../../integrations/utils"
|
import { isExternalTableID } from "../../integrations/utils"
|
||||||
import { helpers } from "@budibase/shared-core"
|
import {
|
||||||
|
helpers,
|
||||||
|
PROTECTED_EXTERNAL_COLUMNS,
|
||||||
|
PROTECTED_INTERNAL_COLUMNS,
|
||||||
|
} from "@budibase/shared-core"
|
||||||
import { processString } from "@budibase/string-templates"
|
import { processString } from "@budibase/string-templates"
|
||||||
|
import { isUserMetadataTable } from "../../api/controllers/row/utils"
|
||||||
|
|
||||||
export * from "./utils"
|
export * from "./utils"
|
||||||
export * from "./attachments"
|
export * from "./attachments"
|
||||||
|
@ -53,9 +58,9 @@ export async function processAutoColumn(
|
||||||
row: Row,
|
row: Row,
|
||||||
opts?: AutoColumnProcessingOpts
|
opts?: AutoColumnProcessingOpts
|
||||||
) {
|
) {
|
||||||
let noUser = !userId
|
const noUser = !userId
|
||||||
let isUserTable = table._id === InternalTables.USER_METADATA
|
const isUserTable = table._id === InternalTables.USER_METADATA
|
||||||
let now = new Date().toISOString()
|
const now = new Date().toISOString()
|
||||||
// if a row doesn't have a revision then it doesn't exist yet
|
// if a row doesn't have a revision then it doesn't exist yet
|
||||||
const creating = !row._rev
|
const creating = !row._rev
|
||||||
// check its not user table, or whether any of the processing options have been disabled
|
// check its not user table, or whether any of the processing options have been disabled
|
||||||
|
@ -111,7 +116,7 @@ async function processDefaultValues(table: Table, row: Row) {
|
||||||
ctx.user = user
|
ctx.user = user
|
||||||
}
|
}
|
||||||
|
|
||||||
for (let [key, schema] of Object.entries(table.schema)) {
|
for (const [key, schema] of Object.entries(table.schema)) {
|
||||||
if ("default" in schema && schema.default != null && row[key] == null) {
|
if ("default" in schema && schema.default != null && row[key] == null) {
|
||||||
const processed = await processString(schema.default, ctx)
|
const processed = await processString(schema.default, ctx)
|
||||||
|
|
||||||
|
@ -165,10 +170,10 @@ export async function inputProcessing(
|
||||||
row: Row,
|
row: Row,
|
||||||
opts?: AutoColumnProcessingOpts
|
opts?: AutoColumnProcessingOpts
|
||||||
) {
|
) {
|
||||||
let clonedRow = cloneDeep(row)
|
const clonedRow = cloneDeep(row)
|
||||||
|
|
||||||
const dontCleanseKeys = ["type", "_id", "_rev", "tableId"]
|
const dontCleanseKeys = ["type", "_id", "_rev", "tableId"]
|
||||||
for (let [key, value] of Object.entries(clonedRow)) {
|
for (const [key, value] of Object.entries(clonedRow)) {
|
||||||
const field = table.schema[key]
|
const field = table.schema[key]
|
||||||
// cleanse fields that aren't in the schema
|
// cleanse fields that aren't in the schema
|
||||||
if (!field) {
|
if (!field) {
|
||||||
|
@ -268,13 +273,13 @@ export async function outputProcessing<T extends Row[] | Row>(
|
||||||
}
|
}
|
||||||
|
|
||||||
// process complex types: attachments, bb references...
|
// process complex types: attachments, bb references...
|
||||||
for (let [property, column] of Object.entries(table.schema)) {
|
for (const [property, column] of Object.entries(table.schema)) {
|
||||||
if (
|
if (
|
||||||
column.type === FieldType.ATTACHMENTS ||
|
column.type === FieldType.ATTACHMENTS ||
|
||||||
column.type === FieldType.ATTACHMENT_SINGLE ||
|
column.type === FieldType.ATTACHMENT_SINGLE ||
|
||||||
column.type === FieldType.SIGNATURE_SINGLE
|
column.type === FieldType.SIGNATURE_SINGLE
|
||||||
) {
|
) {
|
||||||
for (let row of enriched) {
|
for (const row of enriched) {
|
||||||
if (row[property] == null) {
|
if (row[property] == null) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
@ -299,7 +304,7 @@ export async function outputProcessing<T extends Row[] | Row>(
|
||||||
!opts.skipBBReferences &&
|
!opts.skipBBReferences &&
|
||||||
column.type == FieldType.BB_REFERENCE
|
column.type == FieldType.BB_REFERENCE
|
||||||
) {
|
) {
|
||||||
for (let row of enriched) {
|
for (const row of enriched) {
|
||||||
row[property] = await processOutputBBReferences(
|
row[property] = await processOutputBBReferences(
|
||||||
row[property],
|
row[property],
|
||||||
column.subtype
|
column.subtype
|
||||||
|
@ -309,14 +314,14 @@ export async function outputProcessing<T extends Row[] | Row>(
|
||||||
!opts.skipBBReferences &&
|
!opts.skipBBReferences &&
|
||||||
column.type == FieldType.BB_REFERENCE_SINGLE
|
column.type == FieldType.BB_REFERENCE_SINGLE
|
||||||
) {
|
) {
|
||||||
for (let row of enriched) {
|
for (const row of enriched) {
|
||||||
row[property] = await processOutputBBReference(
|
row[property] = await processOutputBBReference(
|
||||||
row[property],
|
row[property],
|
||||||
column.subtype
|
column.subtype
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
} else if (column.type === FieldType.DATETIME && column.timeOnly) {
|
} else if (column.type === FieldType.DATETIME && column.timeOnly) {
|
||||||
for (let row of enriched) {
|
for (const row of enriched) {
|
||||||
if (row[property] instanceof Date) {
|
if (row[property] instanceof Date) {
|
||||||
const hours = row[property].getUTCHours().toString().padStart(2, "0")
|
const hours = row[property].getUTCHours().toString().padStart(2, "0")
|
||||||
const minutes = row[property]
|
const minutes = row[property]
|
||||||
|
@ -343,14 +348,36 @@ export async function outputProcessing<T extends Row[] | Row>(
|
||||||
)) as Row[]
|
)) as Row[]
|
||||||
}
|
}
|
||||||
// remove null properties to match internal API
|
// remove null properties to match internal API
|
||||||
if (isExternalTableID(table._id!)) {
|
const isExternal = isExternalTableID(table._id!)
|
||||||
for (let row of enriched) {
|
if (isExternal) {
|
||||||
for (let key of Object.keys(row)) {
|
for (const row of enriched) {
|
||||||
|
for (const key of Object.keys(row)) {
|
||||||
if (row[key] === null) {
|
if (row[key] === null) {
|
||||||
delete row[key]
|
delete row[key]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!isUserMetadataTable(table._id!)) {
|
||||||
|
const protectedColumns = isExternal
|
||||||
|
? PROTECTED_EXTERNAL_COLUMNS
|
||||||
|
: PROTECTED_INTERNAL_COLUMNS
|
||||||
|
|
||||||
|
const tableFields = Object.keys(table.schema).filter(
|
||||||
|
f => table.schema[f].visible !== false
|
||||||
|
)
|
||||||
|
const fields = [...tableFields, ...protectedColumns].map(f =>
|
||||||
|
f.toLowerCase()
|
||||||
|
)
|
||||||
|
for (const row of enriched) {
|
||||||
|
for (const key of Object.keys(row)) {
|
||||||
|
if (!fields.includes(key.toLowerCase())) {
|
||||||
|
delete row[key]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return (wasArray ? enriched : enriched[0]) as T
|
return (wasArray ? enriched : enriched[0]) as T
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,17 +1,18 @@
|
||||||
import { Context, createContext, runInNewContext } from "vm"
|
import { Context, createContext, runInNewContext } from "vm"
|
||||||
import { create, TemplateDelegate } from "handlebars"
|
import { create, TemplateDelegate } from "handlebars"
|
||||||
import { registerAll, registerMinimum } from "./helpers/index"
|
import { registerAll, registerMinimum } from "./helpers/index"
|
||||||
import { preprocess, postprocess } from "./processors"
|
import { postprocess, preprocess } from "./processors"
|
||||||
import {
|
import {
|
||||||
atob,
|
atob,
|
||||||
btoa,
|
btoa,
|
||||||
isBackendService,
|
|
||||||
FIND_HBS_REGEX,
|
|
||||||
FIND_ANY_HBS_REGEX,
|
FIND_ANY_HBS_REGEX,
|
||||||
|
FIND_HBS_REGEX,
|
||||||
findDoubleHbsInstances,
|
findDoubleHbsInstances,
|
||||||
|
isBackendService,
|
||||||
|
prefixStrings,
|
||||||
} from "./utilities"
|
} from "./utilities"
|
||||||
import { convertHBSBlock } from "./conversion"
|
import { convertHBSBlock } from "./conversion"
|
||||||
import { setJSRunner, removeJSRunner } from "./helpers/javascript"
|
import { removeJSRunner, setJSRunner } from "./helpers/javascript"
|
||||||
|
|
||||||
import manifest from "./manifest.json"
|
import manifest from "./manifest.json"
|
||||||
import { ProcessOptions } from "./types"
|
import { ProcessOptions } from "./types"
|
||||||
|
@ -23,6 +24,7 @@ export { iifeWrapper } from "./iife"
|
||||||
|
|
||||||
const hbsInstance = create()
|
const hbsInstance = create()
|
||||||
registerAll(hbsInstance)
|
registerAll(hbsInstance)
|
||||||
|
const helperNames = Object.keys(hbsInstance.helpers)
|
||||||
const hbsInstanceNoHelpers = create()
|
const hbsInstanceNoHelpers = create()
|
||||||
registerMinimum(hbsInstanceNoHelpers)
|
registerMinimum(hbsInstanceNoHelpers)
|
||||||
const defaultOpts: ProcessOptions = {
|
const defaultOpts: ProcessOptions = {
|
||||||
|
@ -45,12 +47,25 @@ function testObject(object: any) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function findOverlappingHelpers(context?: object) {
|
||||||
|
if (!context) {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
const contextKeys = Object.keys(context)
|
||||||
|
return contextKeys.filter(key => helperNames.includes(key))
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a HBS template function for a given string, and optionally caches it.
|
* Creates a HBS template function for a given string, and optionally caches it.
|
||||||
*/
|
*/
|
||||||
const templateCache: Record<string, TemplateDelegate<any>> = {}
|
const templateCache: Record<string, TemplateDelegate<any>> = {}
|
||||||
function createTemplate(string: string, opts?: ProcessOptions) {
|
function createTemplate(
|
||||||
|
string: string,
|
||||||
|
opts?: ProcessOptions,
|
||||||
|
context?: object
|
||||||
|
) {
|
||||||
opts = { ...defaultOpts, ...opts }
|
opts = { ...defaultOpts, ...opts }
|
||||||
|
const helpersEnabled = !opts?.noHelpers
|
||||||
|
|
||||||
// Finalising adds a helper, can't do this with no helpers
|
// Finalising adds a helper, can't do this with no helpers
|
||||||
const key = `${string}-${JSON.stringify(opts)}`
|
const key = `${string}-${JSON.stringify(opts)}`
|
||||||
|
@ -60,7 +75,25 @@ function createTemplate(string: string, opts?: ProcessOptions) {
|
||||||
return templateCache[key]
|
return templateCache[key]
|
||||||
}
|
}
|
||||||
|
|
||||||
string = preprocess(string, opts)
|
const overlappingHelpers = helpersEnabled
|
||||||
|
? findOverlappingHelpers(context)
|
||||||
|
: []
|
||||||
|
|
||||||
|
string = preprocess(string, {
|
||||||
|
...opts,
|
||||||
|
disabledHelpers: overlappingHelpers,
|
||||||
|
})
|
||||||
|
|
||||||
|
if (context && helpersEnabled) {
|
||||||
|
if (overlappingHelpers.length > 0) {
|
||||||
|
for (const block of findHBSBlocks(string)) {
|
||||||
|
string = string.replace(
|
||||||
|
block,
|
||||||
|
prefixStrings(block, overlappingHelpers, "./")
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Optionally disable built in HBS escaping
|
// Optionally disable built in HBS escaping
|
||||||
if (opts.noEscaping) {
|
if (opts.noEscaping) {
|
||||||
|
@ -70,6 +103,7 @@ function createTemplate(string: string, opts?: ProcessOptions) {
|
||||||
// This does not throw an error when template can't be fulfilled,
|
// This does not throw an error when template can't be fulfilled,
|
||||||
// have to try correct beforehand
|
// have to try correct beforehand
|
||||||
const instance = opts.noHelpers ? hbsInstanceNoHelpers : hbsInstance
|
const instance = opts.noHelpers ? hbsInstanceNoHelpers : hbsInstance
|
||||||
|
|
||||||
const template = instance.compile(string, {
|
const template = instance.compile(string, {
|
||||||
strict: false,
|
strict: false,
|
||||||
})
|
})
|
||||||
|
@ -171,7 +205,8 @@ export function processStringSync(
|
||||||
throw "Cannot process non-string types."
|
throw "Cannot process non-string types."
|
||||||
}
|
}
|
||||||
function process(stringPart: string) {
|
function process(stringPart: string) {
|
||||||
const template = createTemplate(stringPart, opts)
|
// context is needed to check for overlap between helpers and context
|
||||||
|
const template = createTemplate(stringPart, opts, context)
|
||||||
const now = Math.floor(Date.now() / 1000) * 1000
|
const now = Math.floor(Date.now() / 1000) * 1000
|
||||||
const processedString = template({
|
const processedString = template({
|
||||||
now: new Date(now).toISOString(),
|
now: new Date(now).toISOString(),
|
||||||
|
|
|
@ -29,9 +29,9 @@ export function preprocess(string: string, opts: ProcessOptions) {
|
||||||
processor => processor.name !== preprocessor.PreprocessorNames.FINALISE
|
processor => processor.name !== preprocessor.PreprocessorNames.FINALISE
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
return process(string, processors, opts)
|
return process(string, processors, opts)
|
||||||
}
|
}
|
||||||
export function postprocess(string: string) {
|
export function postprocess(string: string) {
|
||||||
let processors = postprocessor.processors
|
return process(string, postprocessor.processors)
|
||||||
return process(string, processors)
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,19 +1,21 @@
|
||||||
import { LITERAL_MARKER } from "../helpers/constants"
|
import { LITERAL_MARKER } from "../helpers/constants"
|
||||||
|
|
||||||
export const PostProcessorNames = {
|
export enum PostProcessorNames {
|
||||||
CONVERT_LITERALS: "convert-literals",
|
CONVERT_LITERALS = "convert-literals",
|
||||||
}
|
}
|
||||||
|
|
||||||
class Postprocessor {
|
type PostprocessorFn = (statement: string) => string
|
||||||
name: string
|
|
||||||
private fn: any
|
|
||||||
|
|
||||||
constructor(name: string, fn: any) {
|
class Postprocessor {
|
||||||
|
name: PostProcessorNames
|
||||||
|
private readonly fn: PostprocessorFn
|
||||||
|
|
||||||
|
constructor(name: PostProcessorNames, fn: PostprocessorFn) {
|
||||||
this.name = name
|
this.name = name
|
||||||
this.fn = fn
|
this.fn = fn
|
||||||
}
|
}
|
||||||
|
|
||||||
process(statement: any) {
|
process(statement: string) {
|
||||||
return this.fn(statement)
|
return this.fn(statement)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,25 +1,28 @@
|
||||||
import { HelperNames } from "../helpers"
|
import { HelperNames } from "../helpers"
|
||||||
import { swapStrings, isAlphaNumeric } from "../utilities"
|
import { swapStrings, isAlphaNumeric } from "../utilities"
|
||||||
|
import { ProcessOptions } from "../types"
|
||||||
|
|
||||||
const FUNCTION_CASES = ["#", "else", "/"]
|
const FUNCTION_CASES = ["#", "else", "/"]
|
||||||
|
|
||||||
export const PreprocessorNames = {
|
export enum PreprocessorNames {
|
||||||
SWAP_TO_DOT: "swap-to-dot-notation",
|
SWAP_TO_DOT = "swap-to-dot-notation",
|
||||||
FIX_FUNCTIONS: "fix-functions",
|
FIX_FUNCTIONS = "fix-functions",
|
||||||
FINALISE: "finalise",
|
FINALISE = "finalise",
|
||||||
NORMALIZE_SPACES: "normalize-spaces",
|
NORMALIZE_SPACES = "normalize-spaces",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type PreprocessorFn = (statement: string, opts?: ProcessOptions) => string
|
||||||
|
|
||||||
class Preprocessor {
|
class Preprocessor {
|
||||||
name: string
|
name: string
|
||||||
private fn: any
|
private readonly fn: PreprocessorFn
|
||||||
|
|
||||||
constructor(name: string, fn: any) {
|
constructor(name: PreprocessorNames, fn: PreprocessorFn) {
|
||||||
this.name = name
|
this.name = name
|
||||||
this.fn = fn
|
this.fn = fn
|
||||||
}
|
}
|
||||||
|
|
||||||
process(fullString: string, statement: string, opts: Object) {
|
process(fullString: string, statement: string, opts: ProcessOptions) {
|
||||||
const output = this.fn(statement, opts)
|
const output = this.fn(statement, opts)
|
||||||
const idx = fullString.indexOf(statement)
|
const idx = fullString.indexOf(statement)
|
||||||
return swapStrings(fullString, idx, statement.length, output)
|
return swapStrings(fullString, idx, statement.length, output)
|
||||||
|
@ -56,8 +59,9 @@ export const processors = [
|
||||||
}),
|
}),
|
||||||
new Preprocessor(
|
new Preprocessor(
|
||||||
PreprocessorNames.FINALISE,
|
PreprocessorNames.FINALISE,
|
||||||
(statement: string, opts: { noHelpers: any }) => {
|
(statement: string, opts?: ProcessOptions) => {
|
||||||
const noHelpers = opts && opts.noHelpers
|
const noHelpers = opts?.noHelpers
|
||||||
|
const helpersEnabled = !noHelpers
|
||||||
let insideStatement = statement.slice(2, statement.length - 2)
|
let insideStatement = statement.slice(2, statement.length - 2)
|
||||||
if (insideStatement.charAt(0) === " ") {
|
if (insideStatement.charAt(0) === " ") {
|
||||||
insideStatement = insideStatement.slice(1)
|
insideStatement = insideStatement.slice(1)
|
||||||
|
@ -74,7 +78,8 @@ export const processors = [
|
||||||
}
|
}
|
||||||
const testHelper = possibleHelper.trim().toLowerCase()
|
const testHelper = possibleHelper.trim().toLowerCase()
|
||||||
if (
|
if (
|
||||||
!noHelpers &&
|
helpersEnabled &&
|
||||||
|
!opts?.disabledHelpers?.includes(testHelper) &&
|
||||||
HelperNames().some(option => testHelper === option.toLowerCase())
|
HelperNames().some(option => testHelper === option.toLowerCase())
|
||||||
) {
|
) {
|
||||||
insideStatement = `(${insideStatement})`
|
insideStatement = `(${insideStatement})`
|
||||||
|
|
|
@ -5,4 +5,5 @@ export interface ProcessOptions {
|
||||||
noFinalise?: boolean
|
noFinalise?: boolean
|
||||||
escapeNewlines?: boolean
|
escapeNewlines?: boolean
|
||||||
onlyFound?: boolean
|
onlyFound?: boolean
|
||||||
|
disabledHelpers?: string[]
|
||||||
}
|
}
|
||||||
|
|
|
@ -66,3 +66,16 @@ export const btoa = (plainText: string) => {
|
||||||
export const atob = (base64: string) => {
|
export const atob = (base64: string) => {
|
||||||
return Buffer.from(base64, "base64").toString("utf-8")
|
return Buffer.from(base64, "base64").toString("utf-8")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const prefixStrings = (
|
||||||
|
baseString: string,
|
||||||
|
strings: string[],
|
||||||
|
prefix: string
|
||||||
|
) => {
|
||||||
|
// Escape any special characters in the strings to avoid regex errors
|
||||||
|
const escapedStrings = strings.map(str =>
|
||||||
|
str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")
|
||||||
|
)
|
||||||
|
const regexPattern = new RegExp(`\\b(${escapedStrings.join("|")})\\b`, "g")
|
||||||
|
return baseString.replace(regexPattern, `${prefix}$1`)
|
||||||
|
}
|
||||||
|
|
|
@ -483,3 +483,37 @@ describe("uuid", () => {
|
||||||
expect(output).toMatch(UUID_REGEX)
|
expect(output).toMatch(UUID_REGEX)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe("helper overlap", () => {
|
||||||
|
it("should use context over helpers (regex test helper)", async () => {
|
||||||
|
const output = await processString("{{ test }}", { test: "a" })
|
||||||
|
expect(output).toEqual("a")
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should use helper if no sum in context, return the context value otherwise", async () => {
|
||||||
|
const hbs = "{{ sum 1 2 }}"
|
||||||
|
const output = await processString(hbs, {})
|
||||||
|
expect(output).toEqual("3")
|
||||||
|
const secondaryOutput = await processString(hbs, { sum: "a" })
|
||||||
|
expect(secondaryOutput).toEqual("a")
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should handle multiple cases", async () => {
|
||||||
|
const output = await processString("{{ literal (split test sum) }}", {
|
||||||
|
test: "a-b",
|
||||||
|
sum: "-",
|
||||||
|
})
|
||||||
|
expect(output).toEqual(["a", "b"])
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should work as expected when no helpers are set", async () => {
|
||||||
|
const output = await processString(
|
||||||
|
"{{ sum }}",
|
||||||
|
{
|
||||||
|
sum: "a",
|
||||||
|
},
|
||||||
|
{ noHelpers: true }
|
||||||
|
)
|
||||||
|
expect(output).toEqual("a")
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
|
@ -111,10 +111,15 @@ export type LoopStepOutputs = {
|
||||||
}
|
}
|
||||||
|
|
||||||
export type BranchStepInputs = {
|
export type BranchStepInputs = {
|
||||||
conditions: SearchFilters
|
branches: Branch[]
|
||||||
children?: Record<string, AutomationStep[]>
|
children?: Record<string, AutomationStep[]>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type Branch = {
|
||||||
|
name: string
|
||||||
|
condition: SearchFilters
|
||||||
|
}
|
||||||
|
|
||||||
export type MakeIntegrationInputs = {
|
export type MakeIntegrationInputs = {
|
||||||
url: string
|
url: string
|
||||||
body: any
|
body: any
|
||||||
|
|
|
@ -131,7 +131,7 @@ export interface RowAttachment {
|
||||||
size: number
|
size: number
|
||||||
name: string
|
name: string
|
||||||
extension: string
|
extension: string
|
||||||
key: string
|
key?: string
|
||||||
// Populated on read
|
// Populated on read
|
||||||
url?: string
|
url?: string
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
export enum FeatureFlag {
|
export enum FeatureFlag {
|
||||||
LICENSING = "LICENSING",
|
|
||||||
PER_CREATOR_PER_USER_PRICE = "PER_CREATOR_PER_USER_PRICE",
|
PER_CREATOR_PER_USER_PRICE = "PER_CREATOR_PER_USER_PRICE",
|
||||||
PER_CREATOR_PER_USER_PRICE_ALERT = "PER_CREATOR_PER_USER_PRICE_ALERT",
|
PER_CREATOR_PER_USER_PRICE_ALERT = "PER_CREATOR_PER_USER_PRICE_ALERT",
|
||||||
}
|
}
|
||||||
|
|
|
@ -44,7 +44,6 @@ ENV NODE_OPTIONS="--no-node-snapshot"
|
||||||
ENV CLUSTER_MODE=${CLUSTER_MODE}
|
ENV CLUSTER_MODE=${CLUSTER_MODE}
|
||||||
ENV SERVICE=worker-service
|
ENV SERVICE=worker-service
|
||||||
ENV POSTHOG_TOKEN=phc_bIjZL7oh2GEUd2vqvTBH8WvrX0fWTFQMs6H5KQxiUxU
|
ENV POSTHOG_TOKEN=phc_bIjZL7oh2GEUd2vqvTBH8WvrX0fWTFQMs6H5KQxiUxU
|
||||||
ENV TENANT_FEATURE_FLAGS=*:LICENSING,*:USER_GROUPS,*:ONBOARDING_TOUR
|
|
||||||
ENV ACCOUNT_PORTAL_URL=https://account.budibase.app
|
ENV ACCOUNT_PORTAL_URL=https://account.budibase.app
|
||||||
|
|
||||||
ARG BUDIBASE_VERSION
|
ARG BUDIBASE_VERSION
|
||||||
|
|
|
@ -26,7 +26,6 @@ async function init() {
|
||||||
APPS_URL: "http://localhost:4001",
|
APPS_URL: "http://localhost:4001",
|
||||||
SERVICE: "worker-service",
|
SERVICE: "worker-service",
|
||||||
DEPLOYMENT_ENVIRONMENT: "development",
|
DEPLOYMENT_ENVIRONMENT: "development",
|
||||||
TENANT_FEATURE_FLAGS: "*:LICENSING,*:USER_GROUPS,*:ONBOARDING_TOUR",
|
|
||||||
ENABLE_EMAIL_TEST_MODE: "1",
|
ENABLE_EMAIL_TEST_MODE: "1",
|
||||||
HTTP_LOGGING: "0",
|
HTTP_LOGGING: "0",
|
||||||
VERSION: "0.0.0+local",
|
VERSION: "0.0.0+local",
|
||||||
|
|