Merge branch 'master' into Fix-user-access-roles-from-displaying-business
|
@ -43,6 +43,7 @@
|
|||
},
|
||||
"rules": {
|
||||
"no-unused-vars": "off",
|
||||
"local-rules/no-barrel-imports": "error",
|
||||
"local-rules/no-budibase-imports": "error",
|
||||
"local-rules/no-console-error": "error",
|
||||
"@typescript-eslint/no-unused-vars": [
|
||||
|
|
|
@ -240,7 +240,7 @@ jobs:
|
|||
|
||||
any_commit=$(git log --no-merges $base_commit_excluding_merges...$pro_commit)
|
||||
|
||||
if [ -n "$any_commit" ]; then
|
||||
if [ -n "$any_commit" ] && [ "$base_commit" != "$pro_commit" ]; then
|
||||
echo $any_commit
|
||||
|
||||
echo "An error occurred: <error_message>"
|
||||
|
|
|
@ -22,6 +22,7 @@ jobs:
|
|||
env:
|
||||
PAYLOAD_BRANCH: ${{ github.head_ref }}
|
||||
PAYLOAD_PR_NUMBER: ${{ github.event.pull_request.number }}
|
||||
PAYLOAD_LICENSE_TYPE: "free"
|
||||
with:
|
||||
repository: budibase/budibase-deploys
|
||||
event: featurebranch-qa-deploy
|
||||
|
|
|
@ -21,6 +21,7 @@ jobs:
|
|||
l_max_size: "1000"
|
||||
fail_if_xl: "false"
|
||||
files_to_ignore: "yarn.lock"
|
||||
message_if_xl: ""
|
||||
|
||||
team-labeler:
|
||||
runs-on: ubuntu-latest
|
||||
|
|
|
@ -65,7 +65,7 @@ Budibase is open-source - licensed as GPL v3. This should fill you with confiden
|
|||
<br /><br />
|
||||
|
||||
### Load data or start from scratch
|
||||
Budibase pulls data from multiple sources, including MongoDB, CouchDB, PostgreSQL, MySQL, Airtable, S3, DynamoDB, or a REST API. And unlike other platforms, with Budibase you can start from scratch and create business apps with no data sources. [Request new datasources](https://github.com/Budibase/budibase/discussions?discussions_q=category%3AIdeas).
|
||||
Budibase pulls data from multiple sources, including MongoDB, CouchDB, PostgreSQL, MariaDB, MySQL, Airtable, S3, DynamoDB, or a REST API. And unlike other platforms, with Budibase you can start from scratch and create business apps with no data sources. [Request new datasources](https://github.com/Budibase/budibase/discussions?discussions_q=category%3AIdeas).
|
||||
|
||||
<p align="center">
|
||||
<img alt="Budibase data" src="https://res.cloudinary.com/daog6scxm/image/upload/v1680281798/ui/data_klbuna.png">
|
||||
|
|
|
@ -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.sqs.enabled | bool | `false` | Whether to use the CouchDB "structured query service" or not. This is disabled by default for now, but will become the default in a future release. |
|
||||
| globals.tempBucketName | string | `""` | |
|
||||
| globals.tenantFeatureFlags | string | `"*:LICENSING,*:USER_GROUPS,*:ONBOARDING_TOUR"` | Sets what feature flags are enabled and for which tenants. Should not ordinarily need to be changed. |
|
||||
| globals.tenantFeatureFlags | string | `` | 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. |
|
||||
| ingress.className | string | `""` | What ingress class to use. |
|
||||
| ingress.enabled | bool | `true` | Whether to create an Ingress resource pointing to the Budibase proxy. |
|
||||
|
|
|
@ -65,7 +65,10 @@ spec:
|
|||
- name: ENABLE_ANALYTICS
|
||||
value: {{ .Values.globals.enableAnalytics | quote }}
|
||||
- name: API_ENCRYPTION_KEY
|
||||
value: {{ .Values.globals.apiEncryptionKey | quote }}
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: {{ template "budibase.fullname" . }}
|
||||
key: apiEncryptionKey
|
||||
- name: HTTP_LOGGING
|
||||
value: {{ .Values.services.apps.httpLogging | quote }}
|
||||
- name: INTERNAL_API_KEY
|
||||
|
@ -161,7 +164,10 @@ spec:
|
|||
- name: TENANT_FEATURE_FLAGS
|
||||
value: {{ .Values.globals.tenantFeatureFlags | quote }}
|
||||
- name: ENCRYPTION_KEY
|
||||
value: {{ .Values.globals.bbEncryptionKey | quote }}
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: {{ template "budibase.fullname" . }}
|
||||
key: bbEncryptionKey
|
||||
{{ if .Values.globals.bbAdminUserEmail }}
|
||||
- name: BB_ADMIN_USER_EMAIL
|
||||
value: {{ .Values.globals.bbAdminUserEmail | quote }}
|
||||
|
@ -206,10 +212,6 @@ spec:
|
|||
- name: APP_FEATURES
|
||||
value: "api"
|
||||
{{- end }}
|
||||
{{- if .Values.globals.sqs.enabled }}
|
||||
- name: SQS_SEARCH_ENABLE
|
||||
value: "true"
|
||||
{{- end }}
|
||||
{{- range .Values.services.apps.extraEnv }}
|
||||
- name: {{ .name }}
|
||||
value: {{ .value | quote }}
|
||||
|
@ -221,7 +223,7 @@ spec:
|
|||
name: {{ .secretName }}
|
||||
key: {{ .secretKey | quote }}
|
||||
{{- end}}
|
||||
image: budibase/apps:{{ .Values.globals.appVersion | default .Chart.AppVersion }}
|
||||
image: {{ .Values.globals.dockerRegistry }}budibase/apps:{{ .Values.globals.appVersion | default .Chart.AppVersion }}
|
||||
imagePullPolicy: Always
|
||||
{{- if .Values.services.apps.startupProbe }}
|
||||
{{- with .Values.services.apps.startupProbe }}
|
||||
|
|
|
@ -58,7 +58,10 @@ spec:
|
|||
- name: ENABLE_ANALYTICS
|
||||
value: {{ .Values.globals.enableAnalytics | quote }}
|
||||
- name: API_ENCRYPTION_KEY
|
||||
value: {{ .Values.globals.apiEncryptionKey | quote }}
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: {{ template "budibase.fullname" . }}
|
||||
key: apiEncryptionKey
|
||||
- name: HTTP_LOGGING
|
||||
value: {{ .Values.services.automationWorkers.httpLogging | quote }}
|
||||
- name: INTERNAL_API_KEY
|
||||
|
@ -154,7 +157,10 @@ spec:
|
|||
- name: TENANT_FEATURE_FLAGS
|
||||
value: {{ .Values.globals.tenantFeatureFlags | quote }}
|
||||
- name: ENCRYPTION_KEY
|
||||
value: {{ .Values.globals.bbEncryptionKey | quote }}
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: {{ template "budibase.fullname" . }}
|
||||
key: bbEncryptionKey
|
||||
{{ if .Values.globals.bbAdminUserEmail }}
|
||||
- name: BB_ADMIN_USER_EMAIL
|
||||
value: {{ .Values.globals.bbAdminUserEmail | quote }}
|
||||
|
@ -209,7 +215,7 @@ spec:
|
|||
key: {{ .secretKey | quote }}
|
||||
{{- end}}
|
||||
|
||||
image: budibase/apps:{{ .Values.globals.appVersion | default .Chart.AppVersion }}
|
||||
image: {{ .Values.globals.dockerRegistry }}budibase/apps:{{ .Values.globals.appVersion | default .Chart.AppVersion }}
|
||||
imagePullPolicy: Always
|
||||
{{- if .Values.services.automationWorkers.startupProbe }}
|
||||
{{- with .Values.services.automationWorkers.startupProbe }}
|
||||
|
|
|
@ -35,7 +35,7 @@ spec:
|
|||
name: {{ template "budibase.fullname" . }}
|
||||
key: objectStoreSecret
|
||||
|
||||
image: minio/minio
|
||||
image: {{ .Values.globals.dockerRegistry }}minio/minio
|
||||
imagePullPolicy: ""
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
|
|
|
@ -32,7 +32,7 @@ spec:
|
|||
{{ end }}
|
||||
spec:
|
||||
containers:
|
||||
- image: budibase/proxy:{{ .Values.globals.appVersion | default .Chart.AppVersion }}
|
||||
- image: {{ .Values.globals.dockerRegistry }}budibase/proxy:{{ .Values.globals.appVersion | default .Chart.AppVersion }}
|
||||
imagePullPolicy: Always
|
||||
name: proxy-service
|
||||
{{- if .Values.services.proxy.startupProbe }}
|
||||
|
|
|
@ -22,7 +22,7 @@ spec:
|
|||
- redis-server
|
||||
- --requirepass
|
||||
- {{ .Values.services.redis.password }}
|
||||
image: {{ .Values.services.redis.image }}
|
||||
image: {{ .Values.globals.dockerRegistry }}{{ .Values.services.redis.image }}
|
||||
imagePullPolicy: ""
|
||||
name: redis-service
|
||||
ports:
|
||||
|
|
|
@ -16,10 +16,14 @@ data:
|
|||
jwtSecret: {{ index $existingSecret.data "jwtSecret" }}
|
||||
objectStoreAccess: {{ index $existingSecret.data "objectStoreAccess" }}
|
||||
objectStoreSecret: {{ index $existingSecret.data "objectStoreSecret" }}
|
||||
bbEncryptionKey: {{ index $existingSecret.data "bbEncryptionKey" }}
|
||||
apiEncryptionKey: {{ index $existingSecret.data "apiEncryptionKey" }}
|
||||
{{- else }}
|
||||
internalApiKey: {{ template "budibase.defaultsecret" .Values.globals.internalApiKey }}
|
||||
jwtSecret: {{ template "budibase.defaultsecret" .Values.globals.jwtSecret }}
|
||||
objectStoreAccess: {{ template "budibase.defaultsecret" .Values.services.objectStore.accessKey }}
|
||||
objectStoreSecret: {{ template "budibase.defaultsecret" .Values.services.objectStore.secretKey }}
|
||||
bbEncryptionKey: {{ template "budibase.defaultsecret" .Values.globals.bbEncryptionKey }}
|
||||
apiEncryptionKey: {{ template "budibase.defaultsecret" .Values.globals.apiEncryptionKey }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
|
|
|
@ -65,7 +65,10 @@ spec:
|
|||
{{ end }}
|
||||
{{ end }}
|
||||
- name: API_ENCRYPTION_KEY
|
||||
value: {{ .Values.globals.apiEncryptionKey | quote }}
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: {{ template "budibase.fullname" . }}
|
||||
key: apiEncryptionKey
|
||||
- name: HTTP_LOGGING
|
||||
value: {{ .Values.services.worker.httpLogging | quote }}
|
||||
- name: INTERNAL_API_KEY
|
||||
|
@ -167,7 +170,10 @@ spec:
|
|||
- name: TENANT_FEATURE_FLAGS
|
||||
value: {{ .Values.globals.tenantFeatureFlags | quote }}
|
||||
- name: ENCRYPTION_KEY
|
||||
value: {{ .Values.globals.bbEncryptionKey | quote }}
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: {{ template "budibase.fullname" . }}
|
||||
key: bbEncryptionKey
|
||||
{{ if .Values.globals.datadogApmEnabled }}
|
||||
- name: DD_LOGS_INJECTION
|
||||
value: {{ .Values.globals.datadogApmEnabled | quote }}
|
||||
|
@ -192,10 +198,6 @@ spec:
|
|||
- name: NODE_TLS_REJECT_UNAUTHORIZED
|
||||
value: {{ .Values.services.tlsRejectUnauthorized }}
|
||||
{{ end }}
|
||||
{{- if .Values.globals.sqs.enabled }}
|
||||
- name: SQS_SEARCH_ENABLE
|
||||
value: "true"
|
||||
{{- end }}
|
||||
{{- range .Values.services.worker.extraEnv }}
|
||||
- name: {{ .name }}
|
||||
value: {{ .value | quote }}
|
||||
|
@ -207,7 +209,7 @@ spec:
|
|||
name: {{ .secretName }}
|
||||
key: {{ .secretKey | quote }}
|
||||
{{- end}}
|
||||
image: budibase/worker:{{ .Values.globals.appVersion | default .Chart.AppVersion }}
|
||||
image: {{ .Values.globals.dockerRegistry }}budibase/worker:{{ .Values.globals.appVersion | default .Chart.AppVersion }}
|
||||
imagePullPolicy: Always
|
||||
{{- if .Values.services.worker.startupProbe }}
|
||||
{{- with .Values.services.worker.startupProbe }}
|
||||
|
|
|
@ -62,7 +62,7 @@ globals:
|
|||
budibaseEnv: PRODUCTION
|
||||
# -- Sets what feature flags are enabled and for which tenants. Should not ordinarily need to be
|
||||
# changed.
|
||||
tenantFeatureFlags: "*:LICENSING,*:USER_GROUPS,*:ONBOARDING_TOUR"
|
||||
tenantFeatureFlags: ""
|
||||
# -- Whether to enable analytics or not. You can read more about our analytics here:
|
||||
# <https://docs.budibase.com/docs/analytics>.
|
||||
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 = {
|
||||
"no-console-error": {
|
||||
create: function(context) {
|
||||
create: function (context) {
|
||||
return {
|
||||
CallExpression(node) {
|
||||
if (
|
||||
|
@ -13,11 +24,12 @@ module.exports = {
|
|||
) {
|
||||
context.report({
|
||||
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": {
|
||||
|
@ -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,
|
||||
},
|
||||
})
|
||||
}
|
||||
},
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
|
|
|
@ -329,7 +329,7 @@ brace-expansion@^1.1.7:
|
|||
balanced-match "^1.0.0"
|
||||
concat-map "0.0.1"
|
||||
|
||||
braces@^3.0.1, braces@~3.0.2:
|
||||
braces@^3.0.3, braces@~3.0.2:
|
||||
version "3.0.3"
|
||||
resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.3.tgz#490332f40919452272d55a8480adc0c441358789"
|
||||
integrity sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==
|
||||
|
@ -1201,12 +1201,12 @@ merge2@^1.3.0, merge2@^1.4.1:
|
|||
integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==
|
||||
|
||||
micromatch@^4.0.4:
|
||||
version "4.0.4"
|
||||
resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.4.tgz#896d519dfe9db25fce94ceb7a500919bf881ebf9"
|
||||
integrity sha512-pRmzw/XUcwXGpD9aI9q/0XOwLNygjETJ8y0ao0wdqprrzDa4YnxLcz7fQRZr8voh8V10kGhABbNcHVk5wHgWwg==
|
||||
version "4.0.8"
|
||||
resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.8.tgz#d66fa18f3a47076789320b9b1af32bd86d9fa202"
|
||||
integrity sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==
|
||||
dependencies:
|
||||
braces "^3.0.1"
|
||||
picomatch "^2.2.3"
|
||||
braces "^3.0.3"
|
||||
picomatch "^2.3.1"
|
||||
|
||||
minimatch@^3.0.4, minimatch@^3.1.2:
|
||||
version "3.1.2"
|
||||
|
@ -1422,7 +1422,7 @@ picocolors@^1.0.0:
|
|||
resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.0.tgz#cb5bdc74ff3f51892236eaf79d68bc44564ab81c"
|
||||
integrity sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==
|
||||
|
||||
picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.2.3:
|
||||
picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.3.1:
|
||||
version "2.3.1"
|
||||
resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42"
|
||||
integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==
|
||||
|
|
|
@ -73,7 +73,7 @@ sed -i "s#COUCHDB_ERLANG_COOKIE#${COUCHDB_ERLANG_COOKIE}#g" /opt/clouseau/clouse
|
|||
/docker-entrypoint.sh /opt/couchdb/bin/couchdb > /dev/stdout 2>&1 &
|
||||
|
||||
# Start SQS. Use 127.0.0.1 instead of localhost to avoid IPv6 issues.
|
||||
/opt/sqs/sqs --server "http://127.0.0.1:5984" --data-dir ${DATA_DIR}/sqs --bind-address=0.0.0.0 > /dev/stdout 2>&1 &
|
||||
/opt/sqs/sqs --server "http://127.0.0.1:5984" --data-dir ${DATA_DIR}/sqs --bind-address=0.0.0.0 --max-threads=20 > /dev/stdout 2>&1 &
|
||||
|
||||
# Wait for CouchDB to start up.
|
||||
while [[ $(curl -s -w "%{http_code}\n" http://localhost:5984/_up -o /dev/null) -ne 200 ]]; do
|
||||
|
|
|
@ -29,7 +29,7 @@ services:
|
|||
BB_ADMIN_USER_EMAIL: ${BB_ADMIN_USER_EMAIL}
|
||||
BB_ADMIN_USER_PASSWORD: ${BB_ADMIN_USER_PASSWORD}
|
||||
PLUGINS_DIR: ${PLUGINS_DIR}
|
||||
SQS_SEARCH_ENABLE: 1
|
||||
TENANT_FEATURE_FLAGS: "*:SQS"
|
||||
depends_on:
|
||||
- worker-service
|
||||
- redis-service
|
||||
|
@ -57,7 +57,7 @@ services:
|
|||
INTERNAL_API_KEY: ${INTERNAL_API_KEY}
|
||||
REDIS_URL: redis-service:6379
|
||||
REDIS_PASSWORD: ${REDIS_PASSWORD}
|
||||
SQS_SEARCH_ENABLE: 1
|
||||
TENANT_FEATURE_FLAGS: "*:SQS"
|
||||
depends_on:
|
||||
- redis-service
|
||||
- minio-service
|
||||
|
|
|
@ -42,7 +42,7 @@ services:
|
|||
couchdb-service:
|
||||
container_name: budi-couchdb3-dev
|
||||
restart: on-failure
|
||||
image: budibase/couchdb:v3.2.1-sqs
|
||||
image: budibase/couchdb:v3.3.3
|
||||
environment:
|
||||
- COUCHDB_PASSWORD=${COUCH_DB_PASSWORD}
|
||||
- COUCHDB_USER=${COUCH_DB_USER}
|
||||
|
|
|
@ -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 "${NODE_ENV}" ]] && export NODE_ENV=production
|
||||
[[ -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 "${REDIS_URL}" ]] && export REDIS_URL=127.0.0.1:6379
|
||||
[[ -z "${SELF_HOSTED}" ]] && export SELF_HOSTED=1
|
||||
|
@ -45,8 +44,7 @@ fi
|
|||
# randomise any unset environment variables
|
||||
for ENV_VAR in "${ENV_VARS[@]}"
|
||||
do
|
||||
temp=$(eval "echo \$$ENV_VAR")
|
||||
if [[ -z "${temp}" ]]; then
|
||||
if [[ -z "${!ENV_VAR}" ]]; then
|
||||
eval "export $ENV_VAR=$(uuidgen | sed -e 's/-//g')"
|
||||
fi
|
||||
done
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"$schema": "node_modules/lerna/schemas/lerna-schema.json",
|
||||
"version": "2.30.3",
|
||||
"version": "2.31.8",
|
||||
"npmClient": "yarn",
|
||||
"packages": [
|
||||
"packages/*",
|
||||
|
|
|
@ -1 +1 @@
|
|||
Subproject commit 32b8fa4643b4f0f74ee89760deffe431ab347ad9
|
||||
Subproject commit c24374879d2b61516fabc24d7404e7da235be05e
|
|
@ -375,3 +375,22 @@ export function getCurrentContext(): ContextMap | undefined {
|
|||
return undefined
|
||||
}
|
||||
}
|
||||
|
||||
export function getFeatureFlags<T extends Record<string, any>>(
|
||||
key: string
|
||||
): T | undefined {
|
||||
const context = getCurrentContext()
|
||||
if (!context) {
|
||||
return undefined
|
||||
}
|
||||
return context.featureFlagCache?.[key] as T
|
||||
}
|
||||
|
||||
export function setFeatureFlags(key: string, value: Record<string, any>) {
|
||||
const context = getCurrentContext()
|
||||
if (!context) {
|
||||
return
|
||||
}
|
||||
context.featureFlagCache ??= {}
|
||||
context.featureFlagCache[key] = value
|
||||
}
|
||||
|
|
|
@ -2,7 +2,7 @@ import { testEnv } from "../../../tests/extra"
|
|||
import * as context from "../"
|
||||
import { DEFAULT_TENANT_ID } from "../../constants"
|
||||
import { structures } from "../../../tests"
|
||||
import { db } from "../.."
|
||||
import * as db from "../../db"
|
||||
import Context from "../Context"
|
||||
import { ContextMap } from "../types"
|
||||
import { IdentityType } from "@budibase/types"
|
||||
|
|
|
@ -18,4 +18,7 @@ export type ContextMap = {
|
|||
oauthClient: OAuth2Client
|
||||
clients: Record<string, GoogleSpreadsheet>
|
||||
}
|
||||
featureFlagCache?: {
|
||||
[key: string]: Record<string, any>
|
||||
}
|
||||
}
|
||||
|
|
|
@ -25,8 +25,8 @@ import { newid } from "../../docIds/newid"
|
|||
import { SQLITE_DESIGN_DOC_ID } from "../../constants"
|
||||
import { DDInstrumentedDatabase } from "../instrumentation"
|
||||
import { checkSlashesInUrl } from "../../helpers"
|
||||
import env from "../../environment"
|
||||
import { sqlLog } from "../../sql/utils"
|
||||
import { flags } from "../../features"
|
||||
|
||||
const DATABASE_NOT_FOUND = "Database does not exist."
|
||||
|
||||
|
@ -401,7 +401,10 @@ export class DatabaseImpl implements Database {
|
|||
}
|
||||
|
||||
async destroy() {
|
||||
if (env.SQS_SEARCH_ENABLE && (await this.exists(SQLITE_DESIGN_DOC_ID))) {
|
||||
if (
|
||||
(await flags.isEnabled("SQS")) &&
|
||||
(await this.exists(SQLITE_DESIGN_DOC_ID))
|
||||
) {
|
||||
// delete the design document, then run the cleanup operation
|
||||
const definition = await this.get<SQLiteDefinition>(SQLITE_DESIGN_DOC_ID)
|
||||
// remove all tables - save the definition then trigger a cleanup
|
||||
|
|
|
@ -1,24 +0,0 @@
|
|||
require("../../../tests")
|
||||
const { structures } = require("../../../tests")
|
||||
const { getDB } = require("../db")
|
||||
|
||||
describe("db", () => {
|
||||
describe("getDB", () => {
|
||||
it("returns a db", async () => {
|
||||
const dbName = structures.db.id()
|
||||
const db = getDB(dbName)
|
||||
expect(db).toBeDefined()
|
||||
expect(db.name).toBe(dbName)
|
||||
})
|
||||
|
||||
it("uses the custom put function", async () => {
|
||||
const db = getDB(structures.db.id())
|
||||
let doc = { _id: "test" }
|
||||
await db.put(doc)
|
||||
doc = await db.get(doc._id)
|
||||
expect(doc.createdAt).toBe(new Date().toISOString())
|
||||
expect(doc.updatedAt).toBe(new Date().toISOString())
|
||||
await db.destroy()
|
||||
})
|
||||
})
|
||||
})
|
|
@ -0,0 +1,32 @@
|
|||
import { doInTenant } from "../../context"
|
||||
import { structures } from "../../../tests"
|
||||
import { getDB } from "../db"
|
||||
|
||||
interface Doc {
|
||||
_id: string
|
||||
createdAt?: string
|
||||
updatedAt?: string
|
||||
}
|
||||
|
||||
describe("db", () => {
|
||||
describe("getDB", () => {
|
||||
it("returns a db", async () => {
|
||||
const dbName = structures.db.id()
|
||||
const db = getDB(dbName)
|
||||
expect(db).toBeDefined()
|
||||
expect(db.name).toBe(dbName)
|
||||
})
|
||||
|
||||
it("uses the custom put function", async () => {
|
||||
await doInTenant("foo", async () => {
|
||||
const db = getDB(structures.db.id())
|
||||
let doc: Doc = { _id: "test" }
|
||||
await db.put(doc)
|
||||
doc = await db.get(doc._id)
|
||||
expect(doc.createdAt).toBe(new Date().toISOString())
|
||||
expect(doc.updatedAt).toBe(new Date().toISOString())
|
||||
await db.destroy()
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
|
@ -1,6 +1,6 @@
|
|||
import env from "../environment"
|
||||
import { DEFAULT_TENANT_ID, SEPARATOR, DocumentType } from "../constants"
|
||||
import { getTenantId, getGlobalDBName, isMultiTenant } from "../context"
|
||||
import { getTenantId, getGlobalDBName } from "../context"
|
||||
import { doWithDB, directCouchAllDbs } from "./db"
|
||||
import { AppState, DeletedApp, getAppMetadata } from "../cache/appMetadata"
|
||||
import { isDevApp, isDevAppID, getProdAppID } from "../docIds/conversions"
|
||||
|
@ -206,34 +206,3 @@ export function pagination<T>(
|
|||
nextPage,
|
||||
}
|
||||
}
|
||||
|
||||
export function isSqsEnabledForTenant(): boolean {
|
||||
const tenantId = getTenantId()
|
||||
if (!env.SQS_SEARCH_ENABLE) {
|
||||
return false
|
||||
}
|
||||
|
||||
// single tenant (self host and dev) always enabled if flag set
|
||||
if (!isMultiTenant()) {
|
||||
return true
|
||||
}
|
||||
|
||||
// This is to guard against the situation in tests where tests pass because
|
||||
// we're not actually using SQS, we're using Lucene and the tests pass due to
|
||||
// parity.
|
||||
if (env.isTest() && env.SQS_SEARCH_ENABLE_TENANTS.length === 0) {
|
||||
throw new Error(
|
||||
"to enable SQS you must specify a list of tenants in the SQS_SEARCH_ENABLE_TENANTS env var"
|
||||
)
|
||||
}
|
||||
|
||||
// Special case to enable all tenants, for testing in QA.
|
||||
if (
|
||||
env.SQS_SEARCH_ENABLE_TENANTS.length === 1 &&
|
||||
env.SQS_SEARCH_ENABLE_TENANTS[0] === "*"
|
||||
) {
|
||||
return true
|
||||
}
|
||||
|
||||
return env.SQS_SEARCH_ENABLE_TENANTS.includes(tenantId)
|
||||
}
|
||||
|
|
|
@ -71,7 +71,7 @@ export function getQueryIndex(viewName: ViewName) {
|
|||
export const isTableId = (id: string) => {
|
||||
// this includes datasource plus tables
|
||||
return (
|
||||
id &&
|
||||
!!id &&
|
||||
(id.startsWith(`${DocumentType.TABLE}${SEPARATOR}`) ||
|
||||
id.startsWith(`${DocumentType.DATASOURCE_PLUS}${SEPARATOR}`))
|
||||
)
|
||||
|
|
|
@ -116,10 +116,6 @@ const environment = {
|
|||
API_ENCRYPTION_KEY: getAPIEncryptionKey(),
|
||||
COUCH_DB_URL: process.env.COUCH_DB_URL || "http://localhost:4005",
|
||||
COUCH_DB_SQL_URL: process.env.COUCH_DB_SQL_URL,
|
||||
SQS_SEARCH_ENABLE: process.env.SQS_SEARCH_ENABLE,
|
||||
SQS_SEARCH_ENABLE_TENANTS:
|
||||
process.env.SQS_SEARCH_ENABLE_TENANTS?.split(",") || [],
|
||||
SQS_MIGRATION_ENABLE: process.env.SQS_MIGRATION_ENABLE,
|
||||
COUCH_DB_USERNAME: process.env.COUCH_DB_USER,
|
||||
COUCH_DB_PASSWORD: process.env.COUCH_DB_PASSWORD,
|
||||
GOOGLE_CLIENT_ID: process.env.GOOGLE_CLIENT_ID,
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
import env from "../environment"
|
||||
import * as context from "../context"
|
||||
import { PostHog, PostHogOptions } from "posthog-node"
|
||||
import { IdentityType, UserCtx } from "@budibase/types"
|
||||
import { FeatureFlag, IdentityType, UserCtx } from "@budibase/types"
|
||||
import tracer from "dd-trace"
|
||||
|
||||
let posthog: PostHog | undefined
|
||||
export function init(opts?: PostHogOptions) {
|
||||
if (env.POSTHOG_TOKEN && env.POSTHOG_API_HOST) {
|
||||
if (env.POSTHOG_TOKEN && env.POSTHOG_API_HOST && !env.SELF_HOSTED) {
|
||||
console.log("initializing posthog client...")
|
||||
posthog = new PostHog(env.POSTHOG_TOKEN, {
|
||||
host: env.POSTHOG_API_HOST,
|
||||
|
@ -18,6 +18,10 @@ export function init(opts?: PostHogOptions) {
|
|||
}
|
||||
}
|
||||
|
||||
export function shutdown() {
|
||||
posthog?.shutdown()
|
||||
}
|
||||
|
||||
export abstract class Flag<T> {
|
||||
static boolean(defaultValue: boolean): Flag<boolean> {
|
||||
return new BooleanFlag(defaultValue)
|
||||
|
@ -87,7 +91,14 @@ class NumberFlag extends Flag<number> {
|
|||
}
|
||||
|
||||
export class FlagSet<V extends Flag<any>, T extends { [key: string]: V }> {
|
||||
constructor(private readonly flagSchema: T) {}
|
||||
// This is used to safely cache flags sets in the current request context.
|
||||
// Because multiple sets could theoretically exist, we don't want the cache of
|
||||
// one to leak into another.
|
||||
private readonly setId: string
|
||||
|
||||
constructor(private readonly flagSchema: T) {
|
||||
this.setId = crypto.randomUUID()
|
||||
}
|
||||
|
||||
defaults(): FlagValues<T> {
|
||||
return Object.keys(this.flagSchema).reduce((acc, key) => {
|
||||
|
@ -119,6 +130,12 @@ export class FlagSet<V extends Flag<any>, T extends { [key: string]: V }> {
|
|||
|
||||
async fetch(ctx?: UserCtx): Promise<FlagValues<T>> {
|
||||
return await tracer.trace("features.fetch", async span => {
|
||||
const cachedFlags = context.getFeatureFlags<FlagValues<T>>(this.setId)
|
||||
if (cachedFlags) {
|
||||
span?.addTags({ fromCache: true })
|
||||
return cachedFlags
|
||||
}
|
||||
|
||||
const tags: Record<string, any> = {}
|
||||
const flagValues = this.defaults()
|
||||
const currentTenantId = context.getTenantId()
|
||||
|
@ -142,8 +159,9 @@ export class FlagSet<V extends Flag<any>, T extends { [key: string]: V }> {
|
|||
specificallySetFalse.add(feature)
|
||||
}
|
||||
|
||||
// ignore unknown flags
|
||||
if (!this.isFlagName(feature)) {
|
||||
throw new Error(`Feature: ${feature} is not an allowed option`)
|
||||
continue
|
||||
}
|
||||
|
||||
if (typeof flagValues[feature] !== "boolean") {
|
||||
|
@ -152,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,
|
||||
// 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"
|
||||
}
|
||||
}
|
||||
|
@ -187,10 +205,7 @@ export class FlagSet<V extends Flag<any>, T extends { [key: string]: V }> {
|
|||
tags[`identity.tenantId`] = identity?.tenantId
|
||||
tags[`identity._id`] = identity?._id
|
||||
|
||||
// Until we're confident this performs well, we're only enabling it in QA
|
||||
// and test environments.
|
||||
const usePosthog = env.isTest() || env.isQA()
|
||||
if (usePosthog && posthog && identity?.type === IdentityType.USER) {
|
||||
if (posthog && identity?.type === IdentityType.USER) {
|
||||
tags[`readFromPostHog`] = true
|
||||
|
||||
const personProperties: Record<string, string> = {}
|
||||
|
@ -204,7 +219,6 @@ export class FlagSet<V extends Flag<any>, T extends { [key: string]: V }> {
|
|||
personProperties,
|
||||
}
|
||||
)
|
||||
console.log("posthog flags", JSON.stringify(posthogFlags))
|
||||
|
||||
for (const [name, value] of Object.entries(posthogFlags.featureFlags)) {
|
||||
if (!this.isFlagName(name)) {
|
||||
|
@ -236,6 +250,7 @@ export class FlagSet<V extends Flag<any>, T extends { [key: string]: V }> {
|
|||
}
|
||||
}
|
||||
|
||||
context.setFeatureFlags(this.setId, flagValues)
|
||||
for (const [key, value] of Object.entries(flagValues)) {
|
||||
tags[`flags.${key}.value`] = value
|
||||
}
|
||||
|
@ -251,8 +266,8 @@ 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
|
||||
// default values set correctly and their types flow through the system.
|
||||
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(env.isDev()),
|
||||
AUTOMATION_BRANCHING: Flag.boolean(env.isDev()),
|
||||
SQS: Flag.boolean(env.isDev()),
|
||||
[FeatureFlag.ENRICHED_RELATIONSHIPS]: Flag.boolean(false),
|
||||
})
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { IdentityContext, IdentityType, UserCtx } from "@budibase/types"
|
||||
import { Flag, FlagSet, FlagValues, init } from "../"
|
||||
import { context } from "../.."
|
||||
import { Flag, FlagSet, FlagValues, init, shutdown } from "../"
|
||||
import * as context from "../../context"
|
||||
import environment, { withEnv } from "../../environment"
|
||||
import nodeFetch from "node-fetch"
|
||||
import nock from "nock"
|
||||
|
@ -67,9 +67,9 @@ describe("feature flags", () => {
|
|||
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",
|
||||
errorMessage: "Feature: FOO is not an allowed option",
|
||||
expected: { TEST_BOOLEAN: true },
|
||||
},
|
||||
{
|
||||
it: "should be able to read boolean flags from PostHog",
|
||||
|
@ -147,13 +147,13 @@ describe("feature flags", () => {
|
|||
}) => {
|
||||
const env: Partial<typeof environment> = {
|
||||
TENANT_FEATURE_FLAGS: environmentFlags,
|
||||
SELF_HOSTED: false,
|
||||
}
|
||||
|
||||
if (posthogFlags) {
|
||||
mockPosthogFlags(posthogFlags)
|
||||
env.POSTHOG_TOKEN = "test"
|
||||
env.POSTHOG_API_HOST = "https://us.i.posthog.com"
|
||||
env.POSTHOG_PERSONAL_TOKEN = "test"
|
||||
}
|
||||
|
||||
const ctx = { user: { license: { features: licenseFlags || [] } } }
|
||||
|
@ -197,6 +197,8 @@ describe("feature flags", () => {
|
|||
throw new Error("No expected value")
|
||||
}
|
||||
})
|
||||
|
||||
shutdown()
|
||||
})
|
||||
}
|
||||
)
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { GenericContainer, StartedTestContainer } from "testcontainers"
|
||||
import { generator, structures } from "../../../tests"
|
||||
import RedisWrapper, { closeAll } from "../redis"
|
||||
import { env } from "../.."
|
||||
import env from "../../environment"
|
||||
import { randomUUID } from "crypto"
|
||||
|
||||
jest.setTimeout(30000)
|
||||
|
|
|
@ -36,6 +36,7 @@ import {
|
|||
} from "@budibase/types"
|
||||
import environment from "../environment"
|
||||
import { dataFilters, helpers } from "@budibase/shared-core"
|
||||
import { cloneDeep } from "lodash"
|
||||
|
||||
type QueryFunction = (query: SqlQuery | SqlQuery[], operation: Operation) => any
|
||||
|
||||
|
@ -268,6 +269,7 @@ class InternalBuilder {
|
|||
}
|
||||
|
||||
private parseFilters(filters: SearchFilters): SearchFilters {
|
||||
filters = cloneDeep(filters)
|
||||
for (const op of Object.values(BasicOperator)) {
|
||||
const filter = filters[op]
|
||||
if (!filter) {
|
||||
|
@ -337,7 +339,7 @@ class InternalBuilder {
|
|||
if (!filters) {
|
||||
return query
|
||||
}
|
||||
filters = this.parseFilters(filters)
|
||||
filters = this.parseFilters({ ...filters })
|
||||
const aliases = this.query.tableAliases
|
||||
// if all or specified in filters, then everything is an or
|
||||
const allOr = filters.allOr
|
||||
|
@ -371,10 +373,11 @@ class InternalBuilder {
|
|||
),
|
||||
castedTypeValue.values
|
||||
)
|
||||
} else if (!opts?.relationship && !isRelationshipField) {
|
||||
} else if (!isRelationshipField) {
|
||||
const alias = getTableAlias(tableName)
|
||||
fn(alias ? `${alias}.${updatedKey}` : updatedKey, value)
|
||||
} else if (opts?.relationship && isRelationshipField) {
|
||||
}
|
||||
if (opts?.relationship && isRelationshipField) {
|
||||
const [filterTableName, property] = updatedKey.split(".")
|
||||
const alias = getTableAlias(filterTableName)
|
||||
fn(alias ? `${alias}.${property}` : property, value)
|
||||
|
@ -465,18 +468,20 @@ class InternalBuilder {
|
|||
|
||||
if (filters.$and) {
|
||||
const { $and } = filters
|
||||
query = query.where(x => {
|
||||
for (const condition of $and.conditions) {
|
||||
x = this.addFilters(x, condition, opts)
|
||||
}
|
||||
})
|
||||
for (const condition of $and.conditions) {
|
||||
query = query.where(b => {
|
||||
this.addFilters(b, condition, opts)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if (filters.$or) {
|
||||
const { $or } = filters
|
||||
query = query.where(x => {
|
||||
query = query.where(b => {
|
||||
for (const condition of $or.conditions) {
|
||||
x = this.addFilters(x, { ...condition, allOr: true }, opts)
|
||||
b.orWhere(c =>
|
||||
this.addFilters(c, { ...condition, allOr: true }, opts)
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
|
@ -15,7 +15,15 @@ export async function saveTenantInfo(tenantInfo: TenantInfo) {
|
|||
})
|
||||
}
|
||||
|
||||
export async function getTenantInfo(tenantId: string): Promise<TenantInfo> {
|
||||
const db = getTenantDB(tenantId)
|
||||
return db.get("tenant_info")
|
||||
export async function getTenantInfo(
|
||||
tenantId: string
|
||||
): Promise<TenantInfo | undefined> {
|
||||
try {
|
||||
const db = getTenantDB(tenantId)
|
||||
const tenantInfo = (await db.get("tenant_info")) as TenantInfo
|
||||
delete tenantInfo.owner.password
|
||||
return tenantInfo
|
||||
} catch {
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
|
|
|
@ -72,6 +72,7 @@
|
|||
"@spectrum-css/switch": "1.0.2",
|
||||
"@spectrum-css/table": "3.0.1",
|
||||
"@spectrum-css/tabs": "3.2.12",
|
||||
"@spectrum-css/tag": "3.0.0",
|
||||
"@spectrum-css/tags": "3.0.2",
|
||||
"@spectrum-css/textfield": "3.0.1",
|
||||
"@spectrum-css/toast": "3.0.1",
|
||||
|
|
|
@ -10,7 +10,7 @@
|
|||
export let inline = false
|
||||
export let disableCancel = false
|
||||
export let autoFocus = true
|
||||
export let zIndex = 999
|
||||
export let zIndex = 1001
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
let visible = fixed || inline
|
||||
|
|
|
@ -6,10 +6,11 @@
|
|||
export let onEdit
|
||||
export let allowSelectRows = false
|
||||
export let allowEditRows = false
|
||||
export let data
|
||||
</script>
|
||||
|
||||
<div>
|
||||
{#if allowSelectRows}
|
||||
{#if allowSelectRows && data.__selectable !== false}
|
||||
<Checkbox value={selected} />
|
||||
{/if}
|
||||
{#if allowEditRows}
|
||||
|
|
|
@ -43,6 +43,8 @@
|
|||
export let showHeaderBorder = true
|
||||
export let placeholderText = "No rows found"
|
||||
export let snippets = []
|
||||
export let defaultSortColumn
|
||||
export let defaultSortOrder = "Ascending"
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
|
@ -162,6 +164,8 @@
|
|||
}
|
||||
|
||||
const sortRows = (rows, sortColumn, sortOrder) => {
|
||||
sortColumn = sortColumn ?? defaultSortColumn
|
||||
sortOrder = sortOrder ?? defaultSortOrder
|
||||
if (!sortColumn || !sortOrder || disableSorting) {
|
||||
return rows
|
||||
}
|
||||
|
@ -259,7 +263,10 @@
|
|||
if (select) {
|
||||
// Add any rows which are not already in selected rows
|
||||
rows.forEach(row => {
|
||||
if (selectedRows.findIndex(x => x._id === row._id) === -1) {
|
||||
if (
|
||||
row.__selectable !== false &&
|
||||
selectedRows.findIndex(x => x._id === row._id) === -1
|
||||
) {
|
||||
selectedRows.push(row)
|
||||
}
|
||||
})
|
||||
|
@ -396,6 +403,9 @@
|
|||
class:noBorderCheckbox={!showHeaderBorder}
|
||||
class="spectrum-Table-cell spectrum-Table-cell--divider spectrum-Table-cell--edit"
|
||||
on:click={e => {
|
||||
if (row.__selectable === false) {
|
||||
return
|
||||
}
|
||||
toggleSelectRow(row)
|
||||
e.stopPropagation()
|
||||
}}
|
||||
|
|
|
@ -70,6 +70,7 @@
|
|||
|
||||
// Stop unnecessary rendering
|
||||
const memoBlock = memo(block)
|
||||
const memoEnvVariables = memo($environment.variables)
|
||||
|
||||
const rowTriggers = [
|
||||
TriggerStepID.ROW_UPDATED,
|
||||
|
@ -91,11 +92,20 @@
|
|||
let insertAtPos, getCaretPosition
|
||||
let stepLayouts = {}
|
||||
|
||||
$: memoEnvVariables.set($environment.variables)
|
||||
$: memoBlock.set(block)
|
||||
|
||||
$: filters = lookForFilters(schemaProperties) || []
|
||||
$: tempFilters = filters
|
||||
$: stepId = block.stepId
|
||||
$: bindings = getAvailableBindings(block, $selectedAutomation?.definition)
|
||||
$: stepId = $memoBlock.stepId
|
||||
|
||||
$: automationBindings = getAvailableBindings(
|
||||
$memoBlock,
|
||||
$selectedAutomation?.definition
|
||||
)
|
||||
$: environmentBindings = buildEnvironmentBindings($memoEnvVariables)
|
||||
$: bindings = [...automationBindings, ...environmentBindings]
|
||||
|
||||
$: getInputData(testData, $memoBlock.inputs)
|
||||
$: tableId = inputData ? inputData.tableId : null
|
||||
$: table = tableId
|
||||
|
@ -110,7 +120,7 @@
|
|||
{ allowLinks: true }
|
||||
)
|
||||
$: queryLimit = tableId?.includes("datasource") ? "∞" : "1000"
|
||||
$: isTrigger = block?.type === AutomationStepType.TRIGGER
|
||||
$: isTrigger = $memoBlock?.type === AutomationStepType.TRIGGER
|
||||
$: codeMode =
|
||||
stepId === AutomationActionStepId.EXECUTE_BASH
|
||||
? EditorModes.Handlebars
|
||||
|
@ -119,13 +129,30 @@
|
|||
disableWrapping: true,
|
||||
})
|
||||
$: editingJs = codeMode === EditorModes.JS
|
||||
$: requiredProperties = isTestModal ? [] : block.schema["inputs"].required
|
||||
$: requiredProperties = isTestModal
|
||||
? []
|
||||
: $memoBlock.schema["inputs"].required
|
||||
|
||||
$: stepCompletions =
|
||||
codeMode === EditorModes.Handlebars
|
||||
? [hbAutocomplete([...bindingsToCompletions(bindings, codeMode)])]
|
||||
: []
|
||||
|
||||
const buildEnvironmentBindings = () => {
|
||||
if ($licensing.environmentVariablesEnabled) {
|
||||
return getEnvironmentBindings().map(binding => {
|
||||
return {
|
||||
...binding,
|
||||
display: {
|
||||
...binding.display,
|
||||
rank: 98,
|
||||
},
|
||||
}
|
||||
})
|
||||
}
|
||||
return []
|
||||
}
|
||||
|
||||
const getInputData = (testData, blockInputs) => {
|
||||
// Test data is not cloned for reactivity
|
||||
let newInputData = testData || cloneDeep(blockInputs)
|
||||
|
@ -151,9 +178,9 @@
|
|||
|
||||
// Store for any UX related data
|
||||
const stepStore = writable({})
|
||||
$: currentStep = $stepStore?.[block.id]
|
||||
$: stepState = $stepStore?.[block.id]
|
||||
|
||||
$: customStepLayouts($memoBlock, schemaProperties, currentStep)
|
||||
$: customStepLayouts($memoBlock, schemaProperties, stepState)
|
||||
|
||||
const customStepLayouts = block => {
|
||||
if (
|
||||
|
@ -185,7 +212,6 @@
|
|||
onChange: e => {
|
||||
onChange({ ["revision"]: e.detail })
|
||||
},
|
||||
bindings,
|
||||
updateOnChange: false,
|
||||
forceModal: true,
|
||||
},
|
||||
|
@ -214,7 +240,6 @@
|
|||
onChange: e => {
|
||||
onChange({ [rowIdentifier]: e.detail })
|
||||
},
|
||||
bindings,
|
||||
updateOnChange: false,
|
||||
forceModal: true,
|
||||
},
|
||||
|
@ -275,7 +300,7 @@
|
|||
isUpdateRow: block.stepId === ActionStepID.UPDATE_ROW,
|
||||
}
|
||||
|
||||
if (isTestModal && currentStep?.rowType === "oldRow") {
|
||||
if (isTestModal && stepState?.rowType === "oldRow") {
|
||||
return [
|
||||
{
|
||||
type: RowSelector,
|
||||
|
@ -722,22 +747,9 @@
|
|||
)
|
||||
}
|
||||
|
||||
// Environment bindings
|
||||
if ($licensing.environmentVariablesEnabled) {
|
||||
bindings = bindings.concat(
|
||||
getEnvironmentBindings().map(binding => {
|
||||
return {
|
||||
...binding,
|
||||
display: {
|
||||
...binding.display,
|
||||
rank: 98,
|
||||
},
|
||||
}
|
||||
})
|
||||
)
|
||||
}
|
||||
return bindings
|
||||
}
|
||||
|
||||
function lookForFilters(properties) {
|
||||
if (!properties) {
|
||||
return []
|
||||
|
@ -770,7 +782,7 @@
|
|||
drawer.hide()
|
||||
}
|
||||
|
||||
function canShowField(key, value) {
|
||||
function canShowField(value) {
|
||||
const dependsOn = value?.dependsOn
|
||||
return !dependsOn || !!inputData[dependsOn]
|
||||
}
|
||||
|
@ -829,6 +841,7 @@
|
|||
<svelte:component
|
||||
this={config.type}
|
||||
{...config.props}
|
||||
{bindings}
|
||||
on:change={config.props.onChange}
|
||||
/>
|
||||
</PropField>
|
||||
|
@ -836,6 +849,7 @@
|
|||
<svelte:component
|
||||
this={config.type}
|
||||
{...config.props}
|
||||
{bindings}
|
||||
on:change={config.props.onChange}
|
||||
/>
|
||||
{/if}
|
||||
|
@ -1051,7 +1065,12 @@
|
|||
value={inputData[key]}
|
||||
/>
|
||||
{:else if value.customType === "code"}
|
||||
<CodeEditorModal>
|
||||
<CodeEditorModal
|
||||
on:hide={() => {
|
||||
// Push any pending changes when the window closes
|
||||
onChange({ [key]: inputData[key] })
|
||||
}}
|
||||
>
|
||||
<div class:js-editor={editingJs}>
|
||||
<div
|
||||
class:js-code={editingJs}
|
||||
|
@ -1061,7 +1080,6 @@
|
|||
value={inputData[key]}
|
||||
on:change={e => {
|
||||
// need to pass without the value inside
|
||||
onChange({ [key]: e.detail })
|
||||
inputData[key] = e.detail
|
||||
}}
|
||||
completions={stepCompletions}
|
||||
|
|
|
@ -11,7 +11,7 @@
|
|||
}
|
||||
</script>
|
||||
|
||||
<Modal bind:this={modal}>
|
||||
<Modal bind:this={modal} on:hide>
|
||||
<ModalContent
|
||||
size="XL"
|
||||
title="Edit Code"
|
||||
|
|
|
@ -13,6 +13,10 @@
|
|||
import AutomationBindingPanel from "../../common/bindings/ServerBindingPanel.svelte"
|
||||
import CodeEditor from "components/common/CodeEditor/CodeEditor.svelte"
|
||||
import KeyValueBuilder from "components/integration/KeyValueBuilder.svelte"
|
||||
import {
|
||||
readableToRuntimeBinding,
|
||||
runtimeToReadableBinding,
|
||||
} from "dataBinding"
|
||||
|
||||
export let onChange
|
||||
export let field
|
||||
|
@ -30,6 +34,8 @@
|
|||
return clone
|
||||
})
|
||||
|
||||
$: readableValue = runtimeToReadableBinding(parsedBindings, fieldData)
|
||||
|
||||
let attachmentTypes = [
|
||||
FieldType.ATTACHMENTS,
|
||||
FieldType.ATTACHMENT_SINGLE,
|
||||
|
@ -132,11 +138,11 @@
|
|||
/>
|
||||
{:else if schema.type === "longform"}
|
||||
<TextArea
|
||||
value={fieldData}
|
||||
value={readableValue}
|
||||
on:change={e =>
|
||||
onChange({
|
||||
row: {
|
||||
[field]: e.detail,
|
||||
[field]: readableToRuntimeBinding(parsedBindings, e.detail),
|
||||
},
|
||||
})}
|
||||
/>
|
||||
|
@ -144,11 +150,11 @@
|
|||
<span>
|
||||
<div class="field-wrap json-field">
|
||||
<CodeEditor
|
||||
value={fieldData}
|
||||
on:change={e => {
|
||||
value={readableValue}
|
||||
on:blur={e => {
|
||||
onChange({
|
||||
row: {
|
||||
[field]: e.detail,
|
||||
[field]: readableToRuntimeBinding(parsedBindings, e.detail),
|
||||
},
|
||||
})
|
||||
}}
|
||||
|
|
|
@ -6,6 +6,8 @@
|
|||
import GridCreateEditRowModal from "components/backend/DataTable/modals/grid/GridCreateEditRowModal.svelte"
|
||||
import GridFilterButton from "components/backend/DataTable/buttons/grid/GridFilterButton.svelte"
|
||||
import GridManageAccessButton from "components/backend/DataTable/buttons/grid/GridManageAccessButton.svelte"
|
||||
import { isEnabled } from "helpers/featureFlags"
|
||||
import { FeatureFlag } from "@budibase/types"
|
||||
|
||||
$: id = $viewsV2.selected?.id
|
||||
$: datasource = {
|
||||
|
@ -29,6 +31,7 @@
|
|||
on:updatedatasource={handleGridViewUpdate}
|
||||
isCloud={$admin.cloud}
|
||||
allowViewReadonlyColumns={$licensing.isViewReadonlyColumnsEnabled}
|
||||
canSetRelationshipSchemas={isEnabled(FeatureFlag.ENRICHED_RELATIONSHIPS)}
|
||||
>
|
||||
<svelte:fragment slot="filter">
|
||||
<GridFilterButton />
|
||||
|
|
|
@ -46,7 +46,7 @@
|
|||
import { RowUtils } from "@budibase/frontend-core"
|
||||
import ServerBindingPanel from "components/common/bindings/ServerBindingPanel.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 FORMULA_TYPE = FieldType.FORMULA
|
||||
|
@ -84,7 +84,7 @@
|
|||
}
|
||||
let relationshipOpts1 = Object.values(PrettyRelationshipDefinitions)
|
||||
let relationshipOpts2 = Object.values(PrettyRelationshipDefinitions)
|
||||
let relationshipMap = {
|
||||
const relationshipMap = {
|
||||
[RelationshipType.ONE_TO_MANY]: {
|
||||
part1: PrettyRelationshipDefinitions.MANY,
|
||||
part2: PrettyRelationshipDefinitions.ONE,
|
||||
|
@ -98,7 +98,7 @@
|
|||
part2: PrettyRelationshipDefinitions.MANY,
|
||||
},
|
||||
}
|
||||
let autoColumnInfo = getAutoColumnInformation()
|
||||
const autoColumnInfo = getAutoColumnInformation()
|
||||
let optionsValid = true
|
||||
|
||||
$: rowGoldenSample = RowUtils.generateGoldenSample($rows)
|
||||
|
@ -168,8 +168,7 @@
|
|||
$: canBeDisplay =
|
||||
canBeDisplayColumn(editableColumn.type) && !editableColumn.autocolumn
|
||||
$: canHaveDefault =
|
||||
isEnabled(TENANT_FEATURE_FLAGS.DEFAULT_VALUES) &&
|
||||
canHaveDefaultColumn(editableColumn.type)
|
||||
isEnabled("DEFAULT_VALUES") && canHaveDefaultColumn(editableColumn.type)
|
||||
$: canBeRequired =
|
||||
editableColumn?.type !== LINK_TYPE &&
|
||||
!uneditable &&
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<script>
|
||||
import { Label } from "@budibase/bbui"
|
||||
import { onMount, createEventDispatcher } from "svelte"
|
||||
import { onMount, createEventDispatcher, onDestroy } from "svelte"
|
||||
import { FIND_ANY_HBS_REGEX } from "@budibase/string-templates"
|
||||
|
||||
import {
|
||||
|
@ -58,6 +58,64 @@
|
|||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
let textarea
|
||||
let editor
|
||||
let mounted = false
|
||||
let isEditorInitialised = false
|
||||
let queuedRefresh = false
|
||||
|
||||
// Theming!
|
||||
let currentTheme = $themeStore?.theme
|
||||
let isDark = !currentTheme.includes("light")
|
||||
let themeConfig = new Compartment()
|
||||
|
||||
$: {
|
||||
if (autofocus && isEditorInitialised) {
|
||||
editor.focus()
|
||||
}
|
||||
}
|
||||
|
||||
// Init when all elements are ready
|
||||
$: if (mounted && !isEditorInitialised) {
|
||||
isEditorInitialised = true
|
||||
initEditor()
|
||||
}
|
||||
|
||||
// Theme change
|
||||
$: if (mounted && isEditorInitialised && $themeStore?.theme) {
|
||||
if (currentTheme != $themeStore?.theme) {
|
||||
currentTheme = $themeStore?.theme
|
||||
isDark = !currentTheme.includes("light")
|
||||
|
||||
// Issue theme compartment update
|
||||
editor.dispatch({
|
||||
effects: themeConfig.reconfigure([...(isDark ? [oneDark] : [])]),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Wait to try and gracefully replace
|
||||
$: refresh(value, isEditorInitialised, mounted)
|
||||
|
||||
/**
|
||||
* Will refresh the editor contents only after
|
||||
* it has been fully initialised
|
||||
* @param value {string} the editor value
|
||||
*/
|
||||
const refresh = (value, initialised, mounted) => {
|
||||
if (!initialised || !mounted) {
|
||||
queuedRefresh = true
|
||||
return
|
||||
}
|
||||
|
||||
if (editor.state.doc.toString() !== value || queuedRefresh) {
|
||||
editor.dispatch({
|
||||
changes: { from: 0, to: editor.state.doc.length, insert: value },
|
||||
})
|
||||
queuedRefresh = false
|
||||
}
|
||||
}
|
||||
|
||||
// Export a function to expose caret position
|
||||
export const getCaretPosition = () => {
|
||||
const selection_range = editor.state.selection.ranges[0]
|
||||
|
@ -132,11 +190,6 @@
|
|||
}
|
||||
)
|
||||
|
||||
// Theming!
|
||||
let currentTheme = $themeStore?.theme
|
||||
let isDark = !currentTheme.includes("light")
|
||||
let themeConfig = new Compartment()
|
||||
|
||||
const indentWithTabCustom = {
|
||||
key: "Tab",
|
||||
run: view => {
|
||||
|
@ -253,6 +306,11 @@
|
|||
lineNumbers(),
|
||||
foldGutter(),
|
||||
keymap.of(buildKeymap()),
|
||||
EditorView.domEventHandlers({
|
||||
blur: () => {
|
||||
dispatch("blur", editor.state.doc.toString())
|
||||
},
|
||||
}),
|
||||
EditorView.updateListener.of(v => {
|
||||
const docStr = v.state.doc?.toString()
|
||||
if (docStr === value) {
|
||||
|
@ -266,11 +324,6 @@
|
|||
return complete
|
||||
}
|
||||
|
||||
let textarea
|
||||
let editor
|
||||
let mounted = false
|
||||
let isEditorInitialised = false
|
||||
|
||||
const initEditor = () => {
|
||||
const baseExtensions = buildBaseExtensions()
|
||||
|
||||
|
@ -281,37 +334,13 @@
|
|||
})
|
||||
}
|
||||
|
||||
$: {
|
||||
if (autofocus && isEditorInitialised) {
|
||||
editor.focus()
|
||||
}
|
||||
}
|
||||
|
||||
// Init when all elements are ready
|
||||
$: if (mounted && !isEditorInitialised) {
|
||||
isEditorInitialised = true
|
||||
initEditor()
|
||||
}
|
||||
|
||||
// Theme change
|
||||
$: if (mounted && isEditorInitialised && $themeStore?.theme) {
|
||||
if (currentTheme != $themeStore?.theme) {
|
||||
currentTheme = $themeStore?.theme
|
||||
isDark = !currentTheme.includes("light")
|
||||
|
||||
// Issue theme compartment update
|
||||
editor.dispatch({
|
||||
effects: themeConfig.reconfigure([...(isDark ? [oneDark] : [])]),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
onMount(async () => {
|
||||
mounted = true
|
||||
return () => {
|
||||
if (editor) {
|
||||
editor.destroy()
|
||||
}
|
||||
})
|
||||
|
||||
onDestroy(() => {
|
||||
if (editor) {
|
||||
editor.destroy()
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
<script>
|
||||
import FontAwesomeIcon from "./FontAwesomeIcon.svelte"
|
||||
import { Popover, Heading, Body } from "@budibase/bbui"
|
||||
import { isEnabled, TENANT_FEATURE_FLAGS } from "helpers/featureFlags"
|
||||
import { licensing } from "stores/portal"
|
||||
import { isPremiumOrAbove } from "helpers/planTitle"
|
||||
import { ChangelogURL } from "constants"
|
||||
|
@ -62,31 +61,26 @@
|
|||
<Body size="S">Budibase University</Body>
|
||||
</a>
|
||||
<div class="divider" />
|
||||
{#if isEnabled(TENANT_FEATURE_FLAGS.LICENSING)}
|
||||
<a
|
||||
href={premiumOrAboveLicense
|
||||
? "mailto:support@budibase.com"
|
||||
: "/builder/portal/account/usage"}
|
||||
>
|
||||
<div
|
||||
class="premiumLinkContent"
|
||||
class:disabled={!premiumOrAboveLicense}
|
||||
>
|
||||
<div class="icon">
|
||||
<FontAwesomeIcon name="fa-solid fa-envelope" />
|
||||
</div>
|
||||
<Body size="S">Email support</Body>
|
||||
<a
|
||||
href={premiumOrAboveLicense
|
||||
? "mailto:support@budibase.com"
|
||||
: "/builder/portal/account/usage"}
|
||||
>
|
||||
<div class="premiumLinkContent" class:disabled={!premiumOrAboveLicense}>
|
||||
<div class="icon">
|
||||
<FontAwesomeIcon name="fa-solid fa-envelope" />
|
||||
</div>
|
||||
{#if !premiumOrAboveLicense}
|
||||
<div class="premiumBadge">
|
||||
<div class="icon">
|
||||
<FontAwesomeIcon name="fa-solid fa-lock" />
|
||||
</div>
|
||||
<Body size="XS">Premium</Body>
|
||||
<Body size="S">Email support</Body>
|
||||
</div>
|
||||
{#if !premiumOrAboveLicense}
|
||||
<div class="premiumBadge">
|
||||
<div class="icon">
|
||||
<FontAwesomeIcon name="fa-solid fa-lock" />
|
||||
</div>
|
||||
{/if}
|
||||
</a>
|
||||
{/if}
|
||||
<Body size="XS">Premium</Body>
|
||||
</div>
|
||||
{/if}
|
||||
</a>
|
||||
</nav>
|
||||
</Popover>
|
||||
</div>
|
||||
|
|
|
@ -20,7 +20,6 @@
|
|||
export let allowJS = true
|
||||
export let allowHelpers = true
|
||||
export let updateOnChange = true
|
||||
export let drawerLeft
|
||||
export let type
|
||||
export let schema
|
||||
|
||||
|
@ -170,14 +169,7 @@
|
|||
<Icon disabled={isJS} size="S" name="Close" />
|
||||
</div>
|
||||
{:else}
|
||||
<slot
|
||||
{label}
|
||||
{disabled}
|
||||
readonly={isJS}
|
||||
value={isJS ? "(JavaScript function)" : readableValue}
|
||||
{placeholder}
|
||||
{updateOnChange}
|
||||
/>
|
||||
<slot />
|
||||
{/if}
|
||||
{#if !disabled && type !== "formula" && !disabled && !attachmentTypes.includes(type)}
|
||||
<div
|
||||
|
@ -195,7 +187,7 @@
|
|||
on:drawerShow
|
||||
bind:this={bindingDrawer}
|
||||
title={title ?? placeholder ?? "Bindings"}
|
||||
left={drawerLeft}
|
||||
forceModal={true}
|
||||
>
|
||||
<Button cta slot="buttons" on:click={saveBinding}>Save</Button>
|
||||
<svelte:component
|
||||
|
|
|
@ -6,8 +6,8 @@
|
|||
|
||||
export let onConfirm
|
||||
export let onCancel
|
||||
export let screenUrl
|
||||
export let screenRole
|
||||
export let route
|
||||
export let role
|
||||
export let confirmText = "Continue"
|
||||
|
||||
const appPrefix = "/app"
|
||||
|
@ -15,17 +15,17 @@
|
|||
let error
|
||||
let modal
|
||||
|
||||
$: appUrl = screenUrl
|
||||
? `${window.location.origin}${appPrefix}${screenUrl}`
|
||||
$: appUrl = route
|
||||
? `${window.location.origin}${appPrefix}${route}`
|
||||
: `${window.location.origin}${appPrefix}`
|
||||
|
||||
const routeChanged = event => {
|
||||
if (!event.detail.startsWith("/")) {
|
||||
screenUrl = "/" + event.detail
|
||||
route = "/" + event.detail
|
||||
}
|
||||
touched = true
|
||||
screenUrl = sanitizeUrl(screenUrl)
|
||||
if (routeExists(screenUrl)) {
|
||||
route = sanitizeUrl(route)
|
||||
if (routeExists(route)) {
|
||||
error = "This URL is already taken for this access role"
|
||||
} else {
|
||||
error = null
|
||||
|
@ -33,19 +33,19 @@
|
|||
}
|
||||
|
||||
const routeExists = url => {
|
||||
if (!screenRole) {
|
||||
if (!role) {
|
||||
return false
|
||||
}
|
||||
return get(screenStore).screens.some(
|
||||
screen =>
|
||||
screen.routing.route.toLowerCase() === url.toLowerCase() &&
|
||||
screen.routing.roleId === screenRole
|
||||
screen.routing.roleId === role
|
||||
)
|
||||
}
|
||||
|
||||
const confirmScreenDetails = async () => {
|
||||
await onConfirm({
|
||||
screenUrl,
|
||||
route,
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
@ -58,13 +58,13 @@
|
|||
onConfirm={confirmScreenDetails}
|
||||
{onCancel}
|
||||
cancelText={"Back"}
|
||||
disabled={!screenUrl || error || !touched}
|
||||
disabled={!route || error || !touched}
|
||||
>
|
||||
<form on:submit|preventDefault={() => modal.confirm()}>
|
||||
<Input
|
||||
label="Enter a URL for the new screen"
|
||||
{error}
|
||||
bind:value={screenUrl}
|
||||
bind:value={route}
|
||||
on:change={routeChanged}
|
||||
/>
|
||||
<div class="app-server" title={appUrl}>
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { getComponentContexts } from "dataBinding"
|
||||
import { getAllComponentContexts } from "dataBinding"
|
||||
import { capitalise } from "helpers"
|
||||
|
||||
// Generates bindings for all components that provider "datasource like"
|
||||
|
@ -7,7 +7,7 @@ import { capitalise } from "helpers"
|
|||
// Some examples are saving rows or duplicating rows.
|
||||
export const getDatasourceLikeProviders = ({ asset, componentId, nested }) => {
|
||||
// Get all form context providers
|
||||
const formComponentContexts = getComponentContexts(
|
||||
const formComponentContexts = getAllComponentContexts(
|
||||
asset,
|
||||
componentId,
|
||||
"form",
|
||||
|
@ -16,7 +16,7 @@ export const getDatasourceLikeProviders = ({ asset, componentId, nested }) => {
|
|||
}
|
||||
)
|
||||
// Get all schema context providers
|
||||
const schemaComponentContexts = getComponentContexts(
|
||||
const schemaComponentContexts = getAllComponentContexts(
|
||||
asset,
|
||||
componentId,
|
||||
"schema",
|
||||
|
|
|
@ -29,7 +29,7 @@
|
|||
on:click={() => onSelect(data)}
|
||||
>
|
||||
<span class="spectrum-Menu-itemLabel">
|
||||
{data.datasource?.name ? `${data.datasource.name} - ` : ""}{data.label}
|
||||
{data.datasourceName ? `${data.datasourceName} - ` : ""}{data.label}
|
||||
</span>
|
||||
<svg
|
||||
class="spectrum-Icon spectrum-UIIcon-Checkmark100 spectrum-Menu-checkmark spectrum-Menu-itemIcon"
|
||||
|
|
|
@ -34,6 +34,7 @@
|
|||
import ClientBindingPanel from "components/common/bindings/ClientBindingPanel.svelte"
|
||||
import DataSourceCategory from "components/design/settings/controls/DataSourceSelect/DataSourceCategory.svelte"
|
||||
import { API } from "api"
|
||||
import { datasourceSelect as format } from "helpers/data/format"
|
||||
|
||||
export let value = {}
|
||||
export let otherSources
|
||||
|
@ -51,24 +52,15 @@
|
|||
let modal
|
||||
|
||||
$: text = value?.label ?? "Choose an option"
|
||||
$: tables = $tablesStore.list.map(m => ({
|
||||
label: m.name,
|
||||
tableId: m._id,
|
||||
type: "table",
|
||||
datasource: $datasources.list.find(
|
||||
ds => ds._id === m.sourceId || m.datasourceId
|
||||
),
|
||||
}))
|
||||
$: tables = $tablesStore.list.map(table =>
|
||||
format.table(table, $datasources.list)
|
||||
)
|
||||
$: viewsV1 = $viewsStore.list.map(view => ({
|
||||
...view,
|
||||
label: view.name,
|
||||
type: "view",
|
||||
}))
|
||||
$: viewsV2 = $viewsV2Store.list.map(view => ({
|
||||
...view,
|
||||
label: view.name,
|
||||
type: "viewV2",
|
||||
}))
|
||||
$: viewsV2 = $viewsV2Store.list.map(format.viewV2)
|
||||
$: views = [...(viewsV1 || []), ...(viewsV2 || [])]
|
||||
$: queries = $queriesStore.list
|
||||
.filter(q => showAllQueries || q.queryVerb === "read" || q.readable)
|
||||
|
|
|
@ -2,24 +2,14 @@
|
|||
import { Select } from "@budibase/bbui"
|
||||
import { createEventDispatcher, onMount } from "svelte"
|
||||
import { tables as tablesStore, viewsV2 } from "stores/builder"
|
||||
import { tableSelect as format } from "helpers/data/format"
|
||||
|
||||
export let value
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
$: tables = $tablesStore.list.map(table => ({
|
||||
type: "table",
|
||||
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,
|
||||
}))
|
||||
$: tables = $tablesStore.list.map(format.table)
|
||||
$: views = $viewsV2.list.map(format.viewV2)
|
||||
$: options = [...(tables || []), ...(views || [])]
|
||||
|
||||
const onChange = e => {
|
||||
|
|
|
@ -7,7 +7,6 @@
|
|||
import { ExpiringKeys } from "./constants"
|
||||
import { getBanners } from "./licensingBanners"
|
||||
import { banner } from "@budibase/bbui"
|
||||
import { TENANT_FEATURE_FLAGS, isEnabled } from "helpers/featureFlags"
|
||||
|
||||
const oneDayInSeconds = 86400
|
||||
|
||||
|
@ -89,8 +88,7 @@
|
|||
userLoaded &&
|
||||
$licensing.usageMetrics &&
|
||||
domLoaded &&
|
||||
!licensingLoaded &&
|
||||
isEnabled(TENANT_FEATURE_FLAGS.LICENSING)
|
||||
!licensingLoaded
|
||||
) {
|
||||
licensingLoaded = true
|
||||
queuedModals = processModals()
|
||||
|
|
|
@ -6,6 +6,7 @@ import {
|
|||
findAllMatchingComponents,
|
||||
findComponent,
|
||||
findComponentPath,
|
||||
getComponentContexts,
|
||||
} from "helpers/components"
|
||||
import {
|
||||
componentStore,
|
||||
|
@ -56,6 +57,7 @@ export const getBindableProperties = (asset, componentId) => {
|
|||
const stateBindings = getStateBindings()
|
||||
const selectedRowsBindings = getSelectedRowsBindings(asset)
|
||||
const roleBindings = getRoleBindings()
|
||||
const embedBindings = getEmbedBindings()
|
||||
return [
|
||||
...contextBindings,
|
||||
...urlBindings,
|
||||
|
@ -64,6 +66,7 @@ export const getBindableProperties = (asset, componentId) => {
|
|||
...deviceBindings,
|
||||
...selectedRowsBindings,
|
||||
...roleBindings,
|
||||
...embedBindings,
|
||||
]
|
||||
}
|
||||
|
||||
|
@ -213,7 +216,7 @@ export const getComponentBindableProperties = (asset, componentId) => {
|
|||
* both global and local bindings, taking into account a component's position
|
||||
* in the component tree.
|
||||
*/
|
||||
export const getComponentContexts = (
|
||||
export const getAllComponentContexts = (
|
||||
asset,
|
||||
componentId,
|
||||
type,
|
||||
|
@ -229,11 +232,6 @@ export const getComponentContexts = (
|
|||
|
||||
// Processes all contexts exposed by a component
|
||||
const processContexts = scope => component => {
|
||||
const def = componentStore.getDefinition(component._component)
|
||||
if (!def?.context) {
|
||||
return
|
||||
}
|
||||
|
||||
// Filter out global contexts not in the same branch.
|
||||
// Global contexts are only valid if their branch root is an ancestor of
|
||||
// this component.
|
||||
|
@ -242,8 +240,8 @@ export const getComponentContexts = (
|
|||
return
|
||||
}
|
||||
|
||||
// Process all contexts provided by this component
|
||||
const contexts = Array.isArray(def.context) ? def.context : [def.context]
|
||||
const componentType = component._component
|
||||
const contexts = getComponentContexts(componentType)
|
||||
contexts.forEach(context => {
|
||||
// Ensure type matches
|
||||
if (type && context.type !== type) {
|
||||
|
@ -261,7 +259,7 @@ export const getComponentContexts = (
|
|||
if (!map[component._id]) {
|
||||
map[component._id] = {
|
||||
component,
|
||||
definition: def,
|
||||
definition: componentStore.getDefinition(componentType),
|
||||
contexts: [],
|
||||
}
|
||||
}
|
||||
|
@ -286,7 +284,7 @@ export const getComponentContexts = (
|
|||
}
|
||||
|
||||
/**
|
||||
* Gets all data provider components above a component.
|
||||
* Gets all components available to this component that expose a certain action
|
||||
*/
|
||||
export const getActionProviders = (
|
||||
asset,
|
||||
|
@ -294,36 +292,30 @@ export const getActionProviders = (
|
|||
actionType,
|
||||
options = { includeSelf: false }
|
||||
) => {
|
||||
if (!asset) {
|
||||
return []
|
||||
}
|
||||
|
||||
// Get all components
|
||||
const components = findAllComponents(asset.props)
|
||||
|
||||
// Find matching contexts and generate bindings
|
||||
let providers = []
|
||||
components.forEach(component => {
|
||||
if (!options?.includeSelf && component._id === componentId) {
|
||||
return
|
||||
}
|
||||
const def = componentStore.getDefinition(component._component)
|
||||
const actions = (def?.actions || []).map(action => {
|
||||
return typeof action === "string" ? { type: action } : action
|
||||
})
|
||||
const action = actions.find(x => x.type === actionType)
|
||||
if (action) {
|
||||
let runtimeBinding = component._id
|
||||
if (action.suffix) {
|
||||
runtimeBinding += `-${action.suffix}`
|
||||
}
|
||||
providers.push({
|
||||
readableBinding: component._instanceName,
|
||||
runtimeBinding,
|
||||
})
|
||||
}
|
||||
const contexts = getAllComponentContexts(asset, componentId, "action", {
|
||||
includeSelf: options?.includeSelf,
|
||||
})
|
||||
return providers
|
||||
return (
|
||||
contexts
|
||||
// Find the definition of the action in question, if one is provided
|
||||
.map(context => ({
|
||||
...context,
|
||||
action: context.contexts[0]?.actions?.find(x => x.type === actionType),
|
||||
}))
|
||||
// Filter out contexts which don't have this action
|
||||
.filter(({ action }) => action != null)
|
||||
// Generate bindings for this component and action
|
||||
.map(({ component, action }) => {
|
||||
let runtimeBinding = component._id
|
||||
if (action.suffix) {
|
||||
runtimeBinding += `-${action.suffix}`
|
||||
}
|
||||
return {
|
||||
readableBinding: component._instanceName,
|
||||
runtimeBinding,
|
||||
}
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -371,7 +363,7 @@ export const getDatasourceForProvider = (asset, component) => {
|
|||
*/
|
||||
const getContextBindings = (asset, componentId) => {
|
||||
// Get all available contexts for this component
|
||||
const componentContexts = getComponentContexts(asset, componentId)
|
||||
const componentContexts = getAllComponentContexts(asset, componentId)
|
||||
|
||||
// Generate bindings for each context
|
||||
return componentContexts
|
||||
|
@ -823,6 +815,25 @@ export const getActionBindings = (actions, actionId) => {
|
|||
return bindings
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets all device bindings for embeds.
|
||||
*/
|
||||
const getEmbedBindings = () => {
|
||||
let bindings = []
|
||||
const safeEmbed = makePropSafe("embed")
|
||||
|
||||
bindings = [
|
||||
{
|
||||
type: "context",
|
||||
runtimeBinding: `${safeEmbed}`,
|
||||
readableBinding: `ParentWindow`,
|
||||
category: "Embed",
|
||||
icon: "DistributeVertically",
|
||||
},
|
||||
]
|
||||
return bindings
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the schema for a certain datasource plus.
|
||||
* The options which can be passed in are:
|
||||
|
|
|
@ -228,6 +228,25 @@ export const getComponentName = component => {
|
|||
return componentDefinition.friendlyName || componentDefinition.name || ""
|
||||
}
|
||||
|
||||
// Gets all contexts exposed by a certain component type, including actions
|
||||
export const getComponentContexts = component => {
|
||||
const def = componentStore.getDefinition(component)
|
||||
let contexts = []
|
||||
if (def?.context) {
|
||||
contexts = Array.isArray(def.context) ? [...def.context] : [def.context]
|
||||
}
|
||||
if (def?.actions) {
|
||||
contexts.push({
|
||||
type: "action",
|
||||
scope: ContextScopes.Global,
|
||||
|
||||
// Ensure all actions are their verbose object versions
|
||||
actions: def.actions.map(x => (typeof x === "string" ? { type: x } : x)),
|
||||
})
|
||||
}
|
||||
return contexts
|
||||
}
|
||||
|
||||
/**
|
||||
* Recurses through the component tree and builds a tree of contexts provided
|
||||
* by components.
|
||||
|
@ -243,10 +262,9 @@ export const buildContextTree = (
|
|||
}
|
||||
|
||||
// Process this component's contexts
|
||||
const def = componentStore.getDefinition(rootComponent._component)
|
||||
if (def?.context) {
|
||||
const contexts = getComponentContexts(rootComponent._component)
|
||||
if (contexts.length) {
|
||||
tree[currentBranch].push(rootComponent._id)
|
||||
const contexts = Array.isArray(def.context) ? def.context : [def.context]
|
||||
|
||||
// If we provide local context, start a new branch for our children
|
||||
if (contexts.some(context => context.scope === ContextScopes.Local)) {
|
||||
|
|
|
@ -0,0 +1,33 @@
|
|||
export const datasourceSelect = {
|
||||
table: (table, datasources) => {
|
||||
const sourceId = table.sourceId || table.datasourceId
|
||||
const datasource = datasources.find(ds => ds._id === sourceId)
|
||||
return {
|
||||
label: table.name,
|
||||
tableId: table._id,
|
||||
type: "table",
|
||||
datasourceName: datasource?.name,
|
||||
}
|
||||
},
|
||||
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 { 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 => {
|
||||
const user = get(auth).user
|
||||
return !!user?.flags?.[featureFlag]
|
||||
|
|
|
@ -7,10 +7,22 @@ import {
|
|||
FIELDS,
|
||||
isAutoColumnUserRelationship,
|
||||
} from "constants/backend"
|
||||
import { isEnabled } from "helpers/featureFlags"
|
||||
|
||||
export function getAutoColumnInformation(enabled = true) {
|
||||
let info = {}
|
||||
for (let [key, subtype] of Object.entries(AUTO_COLUMN_SUB_TYPES)) {
|
||||
for (const [key, subtype] of Object.entries(AUTO_COLUMN_SUB_TYPES)) {
|
||||
// Because it's possible to replicate the functionality of CREATED_AT and
|
||||
// CREATED_BY columns, we disable their creation when the DEFAULT_VALUES
|
||||
// feature flag is enabled.
|
||||
if (isEnabled("DEFAULT_VALUES")) {
|
||||
if (
|
||||
subtype === AUTO_COLUMN_SUB_TYPES.CREATED_AT ||
|
||||
subtype === AUTO_COLUMN_SUB_TYPES.CREATED_BY
|
||||
) {
|
||||
continue
|
||||
}
|
||||
}
|
||||
info[subtype] = { enabled, name: AUTO_COLUMN_DISPLAY_NAMES[key] }
|
||||
}
|
||||
return info
|
||||
|
|
|
@ -49,7 +49,7 @@
|
|||
right: 0;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
z-index: 999;
|
||||
z-index: 9000;
|
||||
position: absolute;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
display: flex;
|
||||
|
|
|
@ -9,7 +9,6 @@
|
|||
deploymentStore,
|
||||
} from "stores/builder"
|
||||
import { auth, appsStore } from "stores/portal"
|
||||
import { TENANT_FEATURE_FLAGS, isEnabled } from "helpers/featureFlags"
|
||||
import {
|
||||
Icon,
|
||||
Tabs,
|
||||
|
@ -90,16 +89,14 @@
|
|||
|
||||
const initTour = async () => {
|
||||
// Check if onboarding is enabled.
|
||||
if (isEnabled(TENANT_FEATURE_FLAGS.ONBOARDING_TOUR)) {
|
||||
if (!$auth.user?.onboardedAt) {
|
||||
builderStore.startBuilderOnboarding()
|
||||
} else {
|
||||
// Feature tour date
|
||||
const release_date = new Date("2023-03-01T00:00:00.000Z")
|
||||
const onboarded = new Date($auth.user?.onboardedAt)
|
||||
if (onboarded < release_date) {
|
||||
builderStore.setTour(TOUR_KEYS.FEATURE_ONBOARDING)
|
||||
}
|
||||
if (!$auth.user?.onboardedAt) {
|
||||
builderStore.startBuilderOnboarding()
|
||||
} else {
|
||||
// Feature tour date
|
||||
const release_date = new Date("2023-03-01T00:00:00.000Z")
|
||||
const onboarded = new Date($auth.user?.onboardedAt)
|
||||
if (onboarded < release_date) {
|
||||
builderStore.setTour(TOUR_KEYS.FEATURE_ONBOARDING)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
<script>
|
||||
import { helpers } from "@budibase/shared-core"
|
||||
import { DetailSummary, notifications } from "@budibase/bbui"
|
||||
import { componentStore, builderStore } from "stores/builder"
|
||||
import PropertyControl from "components/design/settings/controls/PropertyControl.svelte"
|
||||
|
@ -8,6 +7,7 @@
|
|||
import { getComponentForSetting } from "components/design/settings/componentSettings"
|
||||
import InfoDisplay from "./InfoDisplay.svelte"
|
||||
import analytics, { Events } from "analytics"
|
||||
import { shouldDisplaySetting } from "@budibase/frontend-core"
|
||||
|
||||
export let componentDefinition
|
||||
export let componentInstance
|
||||
|
@ -48,7 +48,7 @@
|
|||
|
||||
// Filter out settings which shouldn't be rendered
|
||||
sections.forEach(section => {
|
||||
section.visible = shouldDisplay(instance, section)
|
||||
section.visible = shouldDisplaySetting(instance, section)
|
||||
if (!section.visible) {
|
||||
return
|
||||
}
|
||||
|
@ -88,46 +88,6 @@
|
|||
}
|
||||
}
|
||||
|
||||
const shouldDisplay = (instance, setting) => {
|
||||
let dependsOn = setting.dependsOn
|
||||
if (dependsOn && !Array.isArray(dependsOn)) {
|
||||
dependsOn = [dependsOn]
|
||||
}
|
||||
if (!dependsOn?.length) {
|
||||
return true
|
||||
}
|
||||
|
||||
// Ensure all conditions are met
|
||||
return dependsOn.every(condition => {
|
||||
let dependantSetting = condition
|
||||
let dependantValues = null
|
||||
let invert = !!condition.invert
|
||||
if (typeof condition === "object") {
|
||||
dependantSetting = condition.setting
|
||||
dependantValues = condition.value
|
||||
}
|
||||
if (!dependantSetting) {
|
||||
return false
|
||||
}
|
||||
|
||||
// Ensure values is an array
|
||||
if (!Array.isArray(dependantValues)) {
|
||||
dependantValues = [dependantValues]
|
||||
}
|
||||
|
||||
// If inverting, we want to ensure that we don't have any matches.
|
||||
// If not inverting, we want to ensure that we do have any matches.
|
||||
const currentVal = helpers.deepGet(instance, dependantSetting)
|
||||
const anyMatches = dependantValues.some(dependantVal => {
|
||||
if (dependantVal == null) {
|
||||
return currentVal != null && currentVal !== false && currentVal !== ""
|
||||
}
|
||||
return dependantVal === currentVal
|
||||
})
|
||||
return anyMatches !== invert
|
||||
})
|
||||
}
|
||||
|
||||
const canRenderControl = (instance, setting, isScreen, includeHidden) => {
|
||||
// Prevent rendering on click setting for screens
|
||||
if (setting?.type === "event" && isScreen) {
|
||||
|
@ -142,7 +102,7 @@
|
|||
if (setting.hidden && !includeHidden) {
|
||||
return false
|
||||
}
|
||||
return shouldDisplay(instance, setting)
|
||||
return shouldDisplaySetting(instance, setting)
|
||||
}
|
||||
</script>
|
||||
|
||||
|
|
|
@ -147,6 +147,15 @@
|
|||
onOperatorChange(condition, condition.operator)
|
||||
}
|
||||
}
|
||||
|
||||
const onSettingChange = (e, condition) => {
|
||||
const setting = settings.find(x => x.key === e.detail)
|
||||
if (setting?.defaultValue != null) {
|
||||
condition.settingValue = setting.defaultValue
|
||||
} else {
|
||||
delete condition.settingValue
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||
|
@ -189,7 +198,7 @@
|
|||
<Select
|
||||
options={settingOptions}
|
||||
bind:value={condition.setting}
|
||||
on:change={() => delete condition.settingValue}
|
||||
on:change={e => onSettingChange(e, condition)}
|
||||
/>
|
||||
<div>TO</div>
|
||||
{#if definition}
|
||||
|
|
|
@ -14,11 +14,90 @@
|
|||
import sanitizeUrl from "helpers/sanitizeUrl"
|
||||
import ButtonActionEditor from "components/design/settings/controls/ButtonActionEditor/ButtonActionEditor.svelte"
|
||||
import { getBindableProperties } from "dataBinding"
|
||||
import BarButtonList from "components/design/settings/controls/BarButtonList.svelte"
|
||||
|
||||
$: bindings = getBindableProperties($selectedScreen, null)
|
||||
$: screenSettings = getScreenSettings($selectedScreen)
|
||||
|
||||
let errors = {}
|
||||
|
||||
const getScreenSettings = screen => {
|
||||
let settings = [
|
||||
{
|
||||
key: "routing.homeScreen",
|
||||
control: Checkbox,
|
||||
props: {
|
||||
text: "Set as home screen",
|
||||
},
|
||||
},
|
||||
{
|
||||
key: "routing.route",
|
||||
label: "Route",
|
||||
control: Input,
|
||||
parser: val => {
|
||||
if (!val.startsWith("/")) {
|
||||
val = "/" + val
|
||||
}
|
||||
return sanitizeUrl(val)
|
||||
},
|
||||
validate: route => {
|
||||
const existingRoute = screen.routing.route
|
||||
if (route !== existingRoute && routeTaken(route)) {
|
||||
return "That URL is already in use for this role"
|
||||
}
|
||||
return null
|
||||
},
|
||||
},
|
||||
{
|
||||
key: "routing.roleId",
|
||||
label: "Access",
|
||||
control: RoleSelect,
|
||||
validate: role => {
|
||||
const existingRole = screen.routing.roleId
|
||||
if (role !== existingRole && roleTaken(role)) {
|
||||
return "That role is already in use for this URL"
|
||||
}
|
||||
return null
|
||||
},
|
||||
},
|
||||
{
|
||||
key: "onLoad",
|
||||
label: "On screen load",
|
||||
control: ButtonActionEditor,
|
||||
},
|
||||
{
|
||||
key: "width",
|
||||
label: "Width",
|
||||
control: Select,
|
||||
props: {
|
||||
options: ["Extra small", "Small", "Medium", "Large", "Max"],
|
||||
placeholder: "Default",
|
||||
disabled: !!screen.layoutId,
|
||||
},
|
||||
},
|
||||
{
|
||||
key: "props.layout",
|
||||
label: "Layout",
|
||||
defaultValue: "flex",
|
||||
control: BarButtonList,
|
||||
props: {
|
||||
options: [
|
||||
{
|
||||
barIcon: "ModernGridView",
|
||||
value: "flex",
|
||||
},
|
||||
{
|
||||
barIcon: "ViewGrid",
|
||||
value: "grid",
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
return settings
|
||||
}
|
||||
|
||||
const routeTaken = url => {
|
||||
const roleId = get(selectedScreen).routing.roleId || "BASIC"
|
||||
return get(screenStore).screens.some(
|
||||
|
@ -71,61 +150,6 @@
|
|||
}
|
||||
}
|
||||
|
||||
$: screenSettings = [
|
||||
{
|
||||
key: "routing.homeScreen",
|
||||
control: Checkbox,
|
||||
props: {
|
||||
text: "Set as home screen",
|
||||
},
|
||||
},
|
||||
{
|
||||
key: "routing.route",
|
||||
label: "Route",
|
||||
control: Input,
|
||||
parser: val => {
|
||||
if (!val.startsWith("/")) {
|
||||
val = "/" + val
|
||||
}
|
||||
return sanitizeUrl(val)
|
||||
},
|
||||
validate: route => {
|
||||
const existingRoute = get(selectedScreen).routing.route
|
||||
if (route !== existingRoute && routeTaken(route)) {
|
||||
return "That URL is already in use for this role"
|
||||
}
|
||||
return null
|
||||
},
|
||||
},
|
||||
{
|
||||
key: "routing.roleId",
|
||||
label: "Access",
|
||||
control: RoleSelect,
|
||||
validate: role => {
|
||||
const existingRole = get(selectedScreen).routing.roleId
|
||||
if (role !== existingRole && roleTaken(role)) {
|
||||
return "That role is already in use for this URL"
|
||||
}
|
||||
return null
|
||||
},
|
||||
},
|
||||
{
|
||||
key: "onLoad",
|
||||
label: "On screen load",
|
||||
control: ButtonActionEditor,
|
||||
},
|
||||
{
|
||||
key: "width",
|
||||
label: "Width",
|
||||
control: Select,
|
||||
props: {
|
||||
options: ["Extra small", "Small", "Medium", "Large", "Max"],
|
||||
placeholder: "Default",
|
||||
disabled: !!$selectedScreen.layoutId,
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
const removeCustomLayout = async () => {
|
||||
return screenStore.removeCustomLayout(get(selectedScreen))
|
||||
}
|
||||
|
@ -149,6 +173,7 @@
|
|||
value={Helpers.deepGet($selectedScreen, setting.key)}
|
||||
onChange={val => setScreenSetting(setting, val)}
|
||||
props={{ ...setting.props, error: errors[setting.key] }}
|
||||
defaultValue={setting.defaultValue}
|
||||
{bindings}
|
||||
/>
|
||||
{/each}
|
||||
|
|
|
@ -33,7 +33,7 @@
|
|||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
<Layout gap="S" paddingX="L" paddingY="XL">
|
||||
<Layout gap="XS" paddingX="L" paddingY="XL">
|
||||
{#if activeTab === "theme"}
|
||||
<ThemePanel />
|
||||
{:else}
|
||||
|
|
|
@ -144,7 +144,12 @@
|
|||
const rootComponent = get(selectedScreen).props
|
||||
const component = findComponent(rootComponent, data.id)
|
||||
componentStore.copy(component)
|
||||
await componentStore.paste(component)
|
||||
await componentStore.paste(
|
||||
component,
|
||||
data.mode,
|
||||
null,
|
||||
data.selectComponent
|
||||
)
|
||||
} else if (type === "preview-loaded") {
|
||||
// Wait for this event to show the client library if intelligent
|
||||
// loading is supported
|
||||
|
@ -246,13 +251,13 @@
|
|||
|
||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<div class="component-container">
|
||||
<div
|
||||
class="component-container"
|
||||
class:tablet={$previewStore.previewDevice === "tablet"}
|
||||
class:mobile={$previewStore.previewDevice === "mobile"}
|
||||
>
|
||||
{#if loading}
|
||||
<div
|
||||
class={`loading ${$themeStore.baseTheme} ${$themeStore.theme}`}
|
||||
class:tablet={$previewStore.previewDevice === "tablet"}
|
||||
class:mobile={$previewStore.previewDevice === "mobile"}
|
||||
>
|
||||
<div class={`loading ${$themeStore.baseTheme} ${$themeStore.theme}`}>
|
||||
<ClientAppSkeleton
|
||||
sideNav={$navigationStore?.navigation === "Left"}
|
||||
hideFooter
|
||||
|
@ -275,6 +280,7 @@
|
|||
src="/app/preview"
|
||||
class:hidden={loading || error}
|
||||
/>
|
||||
<div class="underlay" />
|
||||
<div
|
||||
class="add-component"
|
||||
class:active={isAddingComponent}
|
||||
|
@ -293,34 +299,13 @@
|
|||
/>
|
||||
|
||||
<style>
|
||||
.loading {
|
||||
position: absolute;
|
||||
container-type: inline-size;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border: 2px solid transparent;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.loading.tablet {
|
||||
width: calc(1024px + 6px);
|
||||
max-height: calc(768px + 6px);
|
||||
}
|
||||
|
||||
.loading.mobile {
|
||||
width: calc(390px + 6px);
|
||||
max-height: calc(844px + 6px);
|
||||
}
|
||||
|
||||
.component-container {
|
||||
grid-row-start: middle;
|
||||
grid-column-start: middle;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
margin: auto;
|
||||
height: 100%;
|
||||
--client-padding: 6px;
|
||||
}
|
||||
.component-container iframe {
|
||||
border: 0;
|
||||
|
@ -329,6 +314,33 @@
|
|||
width: 100%;
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
.loading,
|
||||
.underlay {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
top: 50%;
|
||||
transform: translateX(-50%) translateY(-50%);
|
||||
width: calc(100% - var(--client-padding) * 2);
|
||||
height: calc(100% - var(--client-padding) * 2);
|
||||
}
|
||||
.tablet .loading,
|
||||
.tablet .underlay {
|
||||
max-width: 1024px;
|
||||
max-height: 768px;
|
||||
}
|
||||
.mobile .loading,
|
||||
.mobile .underlay {
|
||||
max-width: 390px;
|
||||
max-height: 844px;
|
||||
}
|
||||
|
||||
.underlay {
|
||||
background: var(--spectrum-global-color-gray-200);
|
||||
z-index: -1;
|
||||
padding: 2px;
|
||||
}
|
||||
|
||||
.center {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
|
|
|
@ -20,7 +20,7 @@
|
|||
let confirmDeleteDialog
|
||||
let screenDetailsModal
|
||||
|
||||
const createDuplicateScreen = async ({ screenName, screenUrl }) => {
|
||||
const createDuplicateScreen = async ({ route }) => {
|
||||
// Create a dupe and ensure it is unique
|
||||
let duplicateScreen = Helpers.cloneDeep(screen)
|
||||
delete duplicateScreen._id
|
||||
|
@ -28,9 +28,8 @@
|
|||
duplicateScreen.props = makeComponentUnique(duplicateScreen.props)
|
||||
|
||||
// Attach the new name and URL
|
||||
duplicateScreen.routing.route = sanitizeUrl(screenUrl)
|
||||
duplicateScreen.routing.route = sanitizeUrl(route)
|
||||
duplicateScreen.routing.homeScreen = false
|
||||
duplicateScreen.props._instanceName = screenName
|
||||
|
||||
try {
|
||||
// Create the screen
|
||||
|
@ -136,8 +135,8 @@
|
|||
<Modal bind:this={screenDetailsModal}>
|
||||
<ScreenDetailsModal
|
||||
onConfirm={createDuplicateScreen}
|
||||
screenUrl={screen?.routing.route}
|
||||
screenRole={screen?.routing.roleId}
|
||||
route={screen?.routing.route}
|
||||
role={screen?.routing.roleId}
|
||||
confirmText="Duplicate"
|
||||
/>
|
||||
</Modal>
|
||||
|
|
|
@ -1,8 +1,9 @@
|
|||
<script>
|
||||
import ScreenDetailsModal from "components/design/ScreenDetailsModal.svelte"
|
||||
import DatasourceModal from "./DatasourceModal.svelte"
|
||||
import sanitizeUrl from "helpers/sanitizeUrl"
|
||||
import FormTypeModal from "./FormTypeModal.svelte"
|
||||
import TypeModal from "./TypeModal.svelte"
|
||||
import tableTypes from "./tableTypes"
|
||||
import formTypes from "./formTypes"
|
||||
import { Modal, notifications } from "@budibase/bbui"
|
||||
import {
|
||||
screenStore,
|
||||
|
@ -11,14 +12,9 @@
|
|||
builderStore,
|
||||
} from "stores/builder"
|
||||
import { auth } from "stores/portal"
|
||||
import { get } from "svelte/store"
|
||||
import { capitalise } from "helpers"
|
||||
import { goto } from "@roxi/routify"
|
||||
import { TOUR_KEYS } from "components/portal/onboarding/tours.js"
|
||||
import blankScreen from "templates/blankScreen"
|
||||
import formScreen from "templates/formScreen"
|
||||
import gridScreen from "templates/gridScreen"
|
||||
import gridDetailsScreen from "templates/gridDetailsScreen"
|
||||
import * as screenTemplating from "templates/screenTemplating"
|
||||
import { Roles } from "constants/backend"
|
||||
|
||||
let mode
|
||||
|
@ -26,16 +22,19 @@
|
|||
let screenDetailsModal
|
||||
let datasourceModal
|
||||
let formTypeModal
|
||||
let tableTypeModal
|
||||
|
||||
let selectedTablesAndViews = []
|
||||
let permissions = {}
|
||||
|
||||
$: screens = $screenStore.screens
|
||||
|
||||
export const show = newMode => {
|
||||
mode = newMode
|
||||
selectedTablesAndViews = []
|
||||
permissions = {}
|
||||
|
||||
if (mode === "grid" || mode === "gridDetails" || mode === "form") {
|
||||
if (mode === "table" || mode === "form") {
|
||||
datasourceModal.show()
|
||||
} else if (mode === "blank") {
|
||||
screenDetailsModal.show()
|
||||
|
@ -44,136 +43,83 @@
|
|||
}
|
||||
}
|
||||
|
||||
const createScreen = async screen => {
|
||||
const createScreen = async screenTemplate => {
|
||||
try {
|
||||
// Check we aren't clashing with an existing URL
|
||||
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)
|
||||
return await screenStore.save(screenTemplate)
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
notifications.error("Error creating screens")
|
||||
}
|
||||
}
|
||||
|
||||
const addNavigationLink = async screen =>
|
||||
await navigationStore.saveLink(
|
||||
screen.routing.route,
|
||||
capitalise(screen.routing.route.split("/")[1]),
|
||||
screen.routing.roleId
|
||||
)
|
||||
const createScreens = async screenTemplates => {
|
||||
const newScreens = []
|
||||
|
||||
// Checks if any screens exist in the store with the given route and
|
||||
// currently selected role
|
||||
const hasExistingUrl = (url, screenAccessRole) => {
|
||||
const screens = get(screenStore).screens.filter(
|
||||
s => s.routing.roleId === screenAccessRole
|
||||
)
|
||||
return !!screens.find(s => s.routing?.route === url)
|
||||
for (let screenTemplate of screenTemplates) {
|
||||
await addNavigationLink(
|
||||
screenTemplate.data,
|
||||
screenTemplate.navigationLinkLabel
|
||||
)
|
||||
newScreens.push(await createScreen(screenTemplate.data))
|
||||
}
|
||||
|
||||
return newScreens
|
||||
}
|
||||
|
||||
// Constructs a candidate URL for a new screen, appending a given suffix to the
|
||||
// screen's URL
|
||||
// e.g. "/sales/:id" => "/sales-1/:id"
|
||||
const makeCandidateUrl = (screen, suffix) => {
|
||||
let url = screen.routing?.route || ""
|
||||
if (url.startsWith("/")) {
|
||||
url = url.slice(1)
|
||||
}
|
||||
if (!url.includes("/")) {
|
||||
return `/${url}-${suffix}`
|
||||
} else {
|
||||
const split = url.split("/")
|
||||
return `/${split[0]}-${suffix}/${split.slice(1).join("/")}`
|
||||
}
|
||||
const addNavigationLink = async (screen, linkLabel) => {
|
||||
if (linkLabel == null) return
|
||||
|
||||
await navigationStore.saveLink(
|
||||
screen.routing.route,
|
||||
linkLabel,
|
||||
screen.routing.roleId
|
||||
)
|
||||
}
|
||||
|
||||
const onSelectDatasources = async () => {
|
||||
if (mode === "form") {
|
||||
formTypeModal.show()
|
||||
} else if (mode === "grid") {
|
||||
await createGridScreen()
|
||||
} else if (mode === "gridDetails") {
|
||||
await createGridDetailsScreen()
|
||||
} else if (mode === "table") {
|
||||
tableTypeModal.show()
|
||||
}
|
||||
}
|
||||
|
||||
const createBlankScreen = async ({ screenUrl }) => {
|
||||
const screenTemplate = blankScreen(screenUrl)
|
||||
const screen = await createScreen(screenTemplate)
|
||||
await addNavigationLink(screenTemplate)
|
||||
const createBlankScreen = async ({ route }) => {
|
||||
const screenTemplates = screenTemplating.blank({ route, screens })
|
||||
|
||||
loadNewScreen(screen)
|
||||
const newScreens = await createScreens(screenTemplates)
|
||||
loadNewScreen(newScreens[0])
|
||||
}
|
||||
|
||||
const createGridScreen = async () => {
|
||||
let firstScreen = null
|
||||
|
||||
for (let tableOrView of selectedTablesAndViews) {
|
||||
const screenTemplate = gridScreen(
|
||||
const createTableScreen = async type => {
|
||||
const screenTemplates = selectedTablesAndViews.flatMap(tableOrView =>
|
||||
screenTemplating.table({
|
||||
screens,
|
||||
tableOrView,
|
||||
permissions[tableOrView.id]
|
||||
)
|
||||
type,
|
||||
permissions: permissions[tableOrView.id],
|
||||
})
|
||||
)
|
||||
|
||||
const screen = await createScreen(screenTemplate)
|
||||
await addNavigationLink(screen)
|
||||
|
||||
firstScreen ??= screen
|
||||
}
|
||||
|
||||
loadNewScreen(firstScreen)
|
||||
const newScreens = await createScreens(screenTemplates)
|
||||
loadNewScreen(newScreens[0])
|
||||
}
|
||||
|
||||
const createGridDetailsScreen = async () => {
|
||||
let firstScreen = null
|
||||
|
||||
for (let tableOrView of selectedTablesAndViews) {
|
||||
const screenTemplate = gridDetailsScreen(
|
||||
const createFormScreen = async type => {
|
||||
const screenTemplates = selectedTablesAndViews.flatMap(tableOrView =>
|
||||
screenTemplating.form({
|
||||
screens,
|
||||
tableOrView,
|
||||
permissions[tableOrView.id]
|
||||
)
|
||||
type,
|
||||
permissions: permissions[tableOrView.id],
|
||||
})
|
||||
)
|
||||
|
||||
const screen = await createScreen(screenTemplate)
|
||||
await addNavigationLink(screen)
|
||||
const newScreens = await createScreens(screenTemplates)
|
||||
|
||||
firstScreen ??= screen
|
||||
}
|
||||
|
||||
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") {
|
||||
if (type === "update" || type === "create") {
|
||||
const associatedTour =
|
||||
formType === "Update"
|
||||
type === "update"
|
||||
? TOUR_KEYS.BUILDER_FORM_VIEW_UPDATE
|
||||
: TOUR_KEYS.BUILDER_FORM_CREATE
|
||||
|
||||
|
@ -183,7 +129,7 @@
|
|||
}
|
||||
}
|
||||
|
||||
loadNewScreen(firstScreen)
|
||||
loadNewScreen(newScreens[0])
|
||||
}
|
||||
|
||||
const loadNewScreen = screen => {
|
||||
|
@ -199,7 +145,11 @@
|
|||
}
|
||||
|
||||
const fetchPermission = resourceId => {
|
||||
permissions[resourceId] = { loading: true, read: null, write: null }
|
||||
permissions[resourceId] = {
|
||||
loading: true,
|
||||
read: Roles.BASIC,
|
||||
write: Roles.BASIC,
|
||||
}
|
||||
|
||||
permissionsStore
|
||||
.forResource(resourceId)
|
||||
|
@ -218,8 +168,8 @@
|
|||
if (permissions[resourceId]?.loading) {
|
||||
permissions[resourceId] = {
|
||||
loading: false,
|
||||
read: Roles.PUBLIC,
|
||||
write: Roles.PUBLIC,
|
||||
read: Roles.BASIC,
|
||||
write: Roles.BASIC,
|
||||
}
|
||||
}
|
||||
})
|
||||
|
@ -250,18 +200,31 @@
|
|||
<Modal bind:this={datasourceModal} autoFocus={false}>
|
||||
<DatasourceModal
|
||||
{selectedTablesAndViews}
|
||||
{permissions}
|
||||
onConfirm={onSelectDatasources}
|
||||
on:toggle={handleTableOrViewToggle}
|
||||
/>
|
||||
</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}>
|
||||
<ScreenDetailsModal onConfirm={createBlankScreen} />
|
||||
</Modal>
|
||||
|
||||
<Modal bind:this={formTypeModal}>
|
||||
<FormTypeModal
|
||||
<TypeModal
|
||||
title="Select form type"
|
||||
types={formTypes}
|
||||
onConfirm={createFormScreen}
|
||||
onCancel={() => {
|
||||
formTypeModal.hide()
|
||||
|
|
|
@ -1,14 +1,14 @@
|
|||
<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 ICONS from "components/backend/DatasourceNavigator/icons"
|
||||
import { IntegrationNames } from "constants"
|
||||
import { createEventDispatcher, onMount } from "svelte"
|
||||
import TableOrViewOption from "./TableOrViewOption.svelte"
|
||||
import * as format from "helpers/data/format"
|
||||
|
||||
export let onConfirm
|
||||
export let selectedTablesAndViews
|
||||
export let permissions
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
|
@ -21,38 +21,37 @@
|
|||
icon: "Remove",
|
||||
name: view.name,
|
||||
id: view.id,
|
||||
clientData: {
|
||||
...view,
|
||||
type: "viewV2",
|
||||
label: view.name,
|
||||
},
|
||||
tableSelectFormat: format.tableSelect.viewV2(view),
|
||||
datasourceSelectFormat: format.datasourceSelect.viewV2(view),
|
||||
}))
|
||||
}
|
||||
|
||||
const getTablesAndViews = datasource => {
|
||||
let tablesAndViews = []
|
||||
const rawTables = Array.isArray(datasource.entities)
|
||||
const tables = Array.isArray(datasource.entities)
|
||||
? datasource.entities
|
||||
: Object.values(datasource.entities ?? {})
|
||||
|
||||
for (const rawTable of rawTables) {
|
||||
if (rawTable._id === "ta_users") {
|
||||
for (const table of tables) {
|
||||
if (table._id === "ta_users") {
|
||||
continue
|
||||
}
|
||||
|
||||
const table = {
|
||||
const formattedTable = {
|
||||
icon: "Table",
|
||||
name: rawTable.name,
|
||||
id: rawTable._id,
|
||||
clientData: {
|
||||
...rawTable,
|
||||
label: rawTable.name,
|
||||
tableId: rawTable._id,
|
||||
type: "table",
|
||||
},
|
||||
name: table.name,
|
||||
id: table._id,
|
||||
tableSelectFormat: format.tableSelect.table(table),
|
||||
datasourceSelectFormat: format.datasourceSelect.table(
|
||||
table,
|
||||
$datasourcesStore.list
|
||||
),
|
||||
}
|
||||
|
||||
tablesAndViews = tablesAndViews.concat([table, ...getViews(rawTable)])
|
||||
tablesAndViews = tablesAndViews.concat([
|
||||
formattedTable,
|
||||
...getViews(table),
|
||||
])
|
||||
}
|
||||
|
||||
return tablesAndViews
|
||||
|
@ -96,60 +95,76 @@
|
|||
})
|
||||
</script>
|
||||
|
||||
<span>
|
||||
<ModalContent
|
||||
title="Autogenerated screens"
|
||||
confirmText="Confirm"
|
||||
cancelText="Back"
|
||||
{onConfirm}
|
||||
disabled={!selectedTablesAndViews.length}
|
||||
size="L"
|
||||
>
|
||||
<Body size="S">
|
||||
Select which datasources you would like to use to create your screens
|
||||
</Body>
|
||||
<Layout noPadding gap="S">
|
||||
{#each datasources as datasource}
|
||||
<div class="data-source-wrap">
|
||||
<div class="data-source-header">
|
||||
<svelte:component
|
||||
this={datasource.iconComponent}
|
||||
height="24"
|
||||
width="24"
|
||||
/>
|
||||
<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}
|
||||
<ModalContent
|
||||
title="Autogenerated screens"
|
||||
confirmText="Next"
|
||||
cancelText="Cancel"
|
||||
{onConfirm}
|
||||
disabled={!selectedTablesAndViews.length}
|
||||
size="L"
|
||||
>
|
||||
<Body size="S">
|
||||
Select which datasources you would like to use to create your screens
|
||||
</Body>
|
||||
<Layout noPadding gap="S">
|
||||
{#each datasources as datasource}
|
||||
<div class="datasource">
|
||||
<div class="header">
|
||||
<svelte:component
|
||||
this={datasource.iconComponent}
|
||||
height="18"
|
||||
width="18"
|
||||
/>
|
||||
<h2>{datasource.name}</h2>
|
||||
</div>
|
||||
{/each}
|
||||
</Layout>
|
||||
</ModalContent>
|
||||
</span>
|
||||
<!-- List all tables -->
|
||||
{#each datasource.tablesAndViews as tableOrView}
|
||||
{@const selected = selectedTablesAndViews.some(
|
||||
selected => selected.id === tableOrView.id
|
||||
)}
|
||||
<TableOrViewOption
|
||||
on:click={() => toggleSelection(tableOrView)}
|
||||
{selected}
|
||||
{tableOrView}
|
||||
/>
|
||||
{/each}
|
||||
</div>
|
||||
{/each}
|
||||
</Layout>
|
||||
</ModalContent>
|
||||
|
||||
<style>
|
||||
.data-source-wrap {
|
||||
padding-bottom: var(--spectrum-alias-item-padding-s);
|
||||
.datasource {
|
||||
padding-bottom: 15px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
grid-gap: var(--spacing-s);
|
||||
max-width: 100%;
|
||||
min-width: 0;
|
||||
}
|
||||
.data-source-header {
|
||||
|
||||
.datasource:last-child {
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-m);
|
||||
padding-bottom: var(--spacing-xs);
|
||||
padding-bottom: var(--spacing-m);
|
||||
}
|
||||
|
||||
.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>
|
||||
|
|
|
@ -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>
|
||||
import { Icon, AbsTooltip } from "@budibase/bbui"
|
||||
import RoleIcon from "components/common/RoleIcon.svelte"
|
||||
import { Icon } from "@budibase/bbui"
|
||||
|
||||
export let tableOrView
|
||||
export let roles
|
||||
export let selected = false
|
||||
|
||||
$: hideRoles = roles == undefined || roles?.loading
|
||||
</script>
|
||||
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<div role="button" tabindex="0" class="datasource" class:selected on:click>
|
||||
<div class="content">
|
||||
<Icon name={tableOrView.icon} />
|
||||
<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>
|
||||
<Icon name={tableOrView.icon} />
|
||||
<span>{tableOrView.name}</span>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
|
@ -52,18 +17,8 @@
|
|||
border: 1px solid var(--spectrum-global-color-gray-300);
|
||||
transition: 160ms all;
|
||||
border-radius: 4px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
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);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
@ -71,7 +26,12 @@
|
|||
min-width: 0;
|
||||
}
|
||||
|
||||
.content span {
|
||||
.datasource :global(svg) {
|
||||
transition: 160ms all;
|
||||
color: var(--spectrum-global-color-gray-600);
|
||||
}
|
||||
|
||||
.datasource span {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
|
@ -84,29 +44,4 @@
|
|||
.selected {
|
||||
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>
|
||||
|
|
|
@ -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>
|
||||
import { Body } from "@budibase/bbui"
|
||||
import CreationPage from "components/common/CreationPage.svelte"
|
||||
import blankImage from "./images/blank.png"
|
||||
import tableInline from "./images/tableInline.png"
|
||||
import tableDetails from "./images/tableDetails.png"
|
||||
import formImage from "./images/form.png"
|
||||
import blank from "./images/blank.svg"
|
||||
import table from "./images/tableInline.svg"
|
||||
import form from "./images/formUpdate.svg"
|
||||
import CreateScreenModal from "./CreateScreenModal.svelte"
|
||||
import { screenStore } from "stores/builder"
|
||||
|
||||
|
@ -30,37 +29,27 @@
|
|||
<div class="cards">
|
||||
<div class="card" on:click={() => createScreenModal.show("blank")}>
|
||||
<div class="image">
|
||||
<img alt="" src={blankImage} />
|
||||
<img alt="A blank screen" src={blank} />
|
||||
</div>
|
||||
<div class="text">
|
||||
<Body size="S">Blank screen</Body>
|
||||
<Body size="S">Blank</Body>
|
||||
<Body size="XS">Add an empty blank screen</Body>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card" on:click={() => createScreenModal.show("grid")}>
|
||||
<div class="card" on:click={() => createScreenModal.show("table")}>
|
||||
<div class="image">
|
||||
<img alt="" src={tableInline} />
|
||||
<img alt="A table of data" src={table} />
|
||||
</div>
|
||||
<div class="text">
|
||||
<Body size="S">Table with inline editing</Body>
|
||||
<Body size="XS">View, edit and delete rows inline</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>
|
||||
<Body size="S">Table</Body>
|
||||
<Body size="XS">List rows in a table</Body>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card" on:click={() => createScreenModal.show("form")}>
|
||||
<div class="image">
|
||||
<img alt="" src={formImage} />
|
||||
<img alt="A form containing data" src={form} />
|
||||
</div>
|
||||
<div class="text">
|
||||
<Body size="S">Form</Body>
|
||||
|
@ -114,8 +103,9 @@
|
|||
}
|
||||
|
||||
.card .image {
|
||||
min-height: 130px;
|
||||
min-width: 235px;
|
||||
height: 127px;
|
||||
background-color: var(--grey-2);
|
||||
}
|
||||
|
||||
.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
|
|
@ -1,10 +1,12 @@
|
|||
<script>
|
||||
import {
|
||||
AbsTooltip,
|
||||
Layout,
|
||||
Heading,
|
||||
Body,
|
||||
Button,
|
||||
Divider,
|
||||
Icon,
|
||||
Tags,
|
||||
Tag,
|
||||
} from "@budibase/bbui"
|
||||
|
@ -15,6 +17,8 @@
|
|||
export let description
|
||||
export let enabled
|
||||
export let upgradeButtonClick
|
||||
|
||||
$: upgradeDisabled = !$auth.accountPortalAccess && $admin.cloud
|
||||
</script>
|
||||
|
||||
<Layout noPadding>
|
||||
|
@ -36,8 +40,9 @@
|
|||
{:else}
|
||||
<div class="buttons">
|
||||
<Button
|
||||
primary
|
||||
disabled={!$auth.accountPortalAccess && $admin.cloud}
|
||||
primary={!upgradeDisabled}
|
||||
secondary={upgradeDisabled}
|
||||
disabled={upgradeDisabled}
|
||||
on:click={async () => upgradeButtonClick()}
|
||||
>
|
||||
Upgrade
|
||||
|
@ -51,6 +56,16 @@
|
|||
>
|
||||
View Plans
|
||||
</Button>
|
||||
{#if upgradeDisabled}
|
||||
<AbsTooltip
|
||||
text={"Please contact the account holder to upgrade"}
|
||||
position={"right"}
|
||||
>
|
||||
<div class="icon" on:focus>
|
||||
<Icon name="InfoOutline" size="L" disabled hoverable />
|
||||
</div>
|
||||
</AbsTooltip>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</Layout>
|
||||
|
@ -67,7 +82,11 @@
|
|||
justify-content: flex-start;
|
||||
gap: var(--spacing-m);
|
||||
}
|
||||
|
||||
.icon {
|
||||
position: relative;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
.buttons {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
|
|
|
@ -2,11 +2,10 @@
|
|||
import { Button } from "@budibase/bbui"
|
||||
import { goto } from "@roxi/routify"
|
||||
import { auth, admin, licensing } from "stores/portal"
|
||||
import { isEnabled, TENANT_FEATURE_FLAGS } from "helpers/featureFlags"
|
||||
import { sdk } from "@budibase/shared-core"
|
||||
</script>
|
||||
|
||||
{#if isEnabled(TENANT_FEATURE_FLAGS.LICENSING) && !$licensing.isEnterprisePlan && !$licensing.isEnterpriseTrial}
|
||||
{#if !$licensing.isEnterprisePlan && !$licensing.isEnterpriseTrial}
|
||||
{#if $admin.cloud && $auth?.user?.accountPortalAccess}
|
||||
<Button
|
||||
cta
|
||||
|
|
|
@ -85,7 +85,7 @@
|
|||
let popoverAnchor
|
||||
let searchTerm = ""
|
||||
let popover
|
||||
let user
|
||||
let user, tenantOwner
|
||||
let loaded = false
|
||||
|
||||
$: internalGroups = $groups?.filter(g => !g?.scimInfo?.isSync)
|
||||
|
@ -104,6 +104,7 @@
|
|||
})
|
||||
})
|
||||
$: globalRole = users.getUserRole(user)
|
||||
$: isTenantOwner = tenantOwner?.email && tenantOwner.email === user?.email
|
||||
|
||||
const getAvailableApps = (appList, privileged, roles) => {
|
||||
let availableApps = appList.slice()
|
||||
|
@ -205,6 +206,7 @@
|
|||
if (!user?._id) {
|
||||
$goto("./")
|
||||
}
|
||||
tenantOwner = await users.tenantOwner($auth.tenantId)
|
||||
}
|
||||
|
||||
async function toggleFlags(detail) {
|
||||
|
@ -268,9 +270,11 @@
|
|||
Force password reset
|
||||
</MenuItem>
|
||||
{/if}
|
||||
<MenuItem on:click={deleteModal.show} icon="Delete">
|
||||
Delete
|
||||
</MenuItem>
|
||||
{#if !isTenantOwner}
|
||||
<MenuItem on:click={deleteModal.show} icon="Delete">
|
||||
Delete
|
||||
</MenuItem>
|
||||
{/if}
|
||||
</ActionMenu>
|
||||
</div>
|
||||
{/if}
|
||||
|
@ -310,9 +314,11 @@
|
|||
<Label size="L">Role</Label>
|
||||
<Select
|
||||
placeholder={null}
|
||||
disabled={!sdk.users.isAdmin($auth.user)}
|
||||
value={globalRole}
|
||||
options={Constants.BudibaseRoleOptions}
|
||||
disabled={!sdk.users.isAdmin($auth.user) || isTenantOwner}
|
||||
value={isTenantOwner ? "owner" : globalRole}
|
||||
options={isTenantOwner
|
||||
? Constants.ExtendedBudibaseRoleOptions
|
||||
: Constants.BudibaseRoleOptions}
|
||||
on:change={updateUserRole}
|
||||
/>
|
||||
</div>
|
||||
|
|
|
@ -7,7 +7,15 @@
|
|||
|
||||
export let user
|
||||
|
||||
const password = Math.random().toString(36).slice(2, 20)
|
||||
const generatePassword = length => {
|
||||
const array = new Uint8Array(length)
|
||||
crypto.getRandomValues(array)
|
||||
return Array.from(array, byte => byte.toString(36).padStart(2, "0"))
|
||||
.join("")
|
||||
.slice(0, length)
|
||||
}
|
||||
|
||||
const password = generatePassword(12)
|
||||
|
||||
async function resetPassword() {
|
||||
try {
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
|
||||
export let row
|
||||
|
||||
$: role = Constants.BudibaseRoleOptions.find(
|
||||
$: role = Constants.ExtendedBudibaseRoleOptions.find(
|
||||
x => x.value === users.getUserRole(row)
|
||||
)
|
||||
$: value = role?.label || "Not available"
|
||||
|
|
|
@ -52,6 +52,7 @@
|
|||
|
||||
let groupsLoaded = !$licensing.groupsEnabled || $groups?.length
|
||||
let enrichedUsers = []
|
||||
let tenantOwner
|
||||
let createUserModal,
|
||||
inviteConfirmationModal,
|
||||
onboardingTypeModal,
|
||||
|
@ -70,6 +71,7 @@
|
|||
]
|
||||
let userData = []
|
||||
let invitesLoaded = false
|
||||
let tenantOwnerLoaded = false
|
||||
let pendingInvites = []
|
||||
let parsedInvites = []
|
||||
|
||||
|
@ -98,8 +100,14 @@
|
|||
$: pendingSchema = getPendingSchema(schema)
|
||||
$: userData = []
|
||||
$: inviteUsersResponse = { successful: [], unsuccessful: [] }
|
||||
$: {
|
||||
enrichedUsers = $fetch.rows?.map(user => {
|
||||
$: setEnrichedUsers($fetch.rows, tenantOwnerLoaded)
|
||||
|
||||
const setEnrichedUsers = async rows => {
|
||||
if (!tenantOwnerLoaded) {
|
||||
enrichedUsers = []
|
||||
return
|
||||
}
|
||||
enrichedUsers = rows?.map(user => {
|
||||
let userGroups = []
|
||||
$groups.forEach(group => {
|
||||
if (group.users) {
|
||||
|
@ -110,15 +118,24 @@
|
|||
})
|
||||
}
|
||||
})
|
||||
user.tenantOwnerEmail = tenantOwner?.email
|
||||
const role = Constants.ExtendedBudibaseRoleOptions.find(
|
||||
x => x.value === users.getUserRole(user)
|
||||
)
|
||||
return {
|
||||
...user,
|
||||
name: user.firstName ? user.firstName + " " + user.lastName : "",
|
||||
userGroups,
|
||||
__selectable:
|
||||
role.value === Constants.BudibaseRoles.Owner ||
|
||||
$auth.user?.email === user.email
|
||||
? false
|
||||
: true,
|
||||
apps: [...new Set(Object.keys(user.roles))],
|
||||
access: role.sortOrder,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const getPendingSchema = tblSchema => {
|
||||
if (!tblSchema) {
|
||||
return {}
|
||||
|
@ -302,6 +319,8 @@
|
|||
groupsLoaded = true
|
||||
pendingInvites = await users.getInvites()
|
||||
invitesLoaded = true
|
||||
tenantOwner = await users.tenantOwner($auth.tenantId)
|
||||
tenantOwnerLoaded = true
|
||||
} catch (error) {
|
||||
notifications.error("Error fetching user group data")
|
||||
}
|
||||
|
@ -376,6 +395,7 @@
|
|||
allowSelectRows={!readonly}
|
||||
{customRenderers}
|
||||
loading={!$fetch.loaded || !groupsLoaded}
|
||||
defaultSortColumn={"__selectable"}
|
||||
/>
|
||||
|
||||
<div class="pagination">
|
||||
|
|
|
@ -574,15 +574,26 @@ export class ComponentStore extends BudiStore {
|
|||
return
|
||||
}
|
||||
|
||||
// Determine the next component to select after deletion
|
||||
// Determine the next component to select, and select it before deletion
|
||||
// to avoid an intermediate state of no component selection
|
||||
const state = get(this.store)
|
||||
let nextSelectedComponentId
|
||||
let nextId
|
||||
if (state.selectedComponentId === component._id) {
|
||||
nextSelectedComponentId = this.getNext()
|
||||
if (!nextSelectedComponentId) {
|
||||
nextSelectedComponentId = this.getPrevious()
|
||||
nextId = this.getNext()
|
||||
if (!nextId) {
|
||||
nextId = this.getPrevious()
|
||||
}
|
||||
}
|
||||
if (nextId) {
|
||||
// If this is the nav, select the screen instead
|
||||
if (nextId.endsWith("-navigation")) {
|
||||
nextId = nextId.replace("-navigation", "-screen")
|
||||
}
|
||||
this.update(state => {
|
||||
state.selectedComponentId = nextId
|
||||
return state
|
||||
})
|
||||
}
|
||||
|
||||
// Patch screen
|
||||
await screenStore.patch(screen => {
|
||||
|
@ -601,14 +612,6 @@ export class ComponentStore extends BudiStore {
|
|||
child => child._id !== component._id
|
||||
)
|
||||
})
|
||||
|
||||
// Update selected component if required
|
||||
if (nextSelectedComponentId) {
|
||||
this.update(state => {
|
||||
state.selectedComponentId = nextSelectedComponentId
|
||||
return state
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
copy(component, cut = false, selectParent = true) {
|
||||
|
@ -616,6 +619,7 @@ export class ComponentStore extends BudiStore {
|
|||
this.update(state => {
|
||||
state.componentToPaste = cloneDeep(component)
|
||||
state.componentToPaste.isCut = cut
|
||||
state.componentToPaste.screenId = get(screenStore).selectedScreenId
|
||||
return state
|
||||
})
|
||||
|
||||
|
@ -650,7 +654,7 @@ export class ComponentStore extends BudiStore {
|
|||
* @param {object} targetScreen
|
||||
* @returns
|
||||
*/
|
||||
async paste(targetComponent, mode, targetScreen) {
|
||||
async paste(targetComponent, mode, targetScreen, selectComponent = true) {
|
||||
const state = get(this.store)
|
||||
if (!state.componentToPaste) {
|
||||
return
|
||||
|
@ -674,8 +678,10 @@ export class ComponentStore extends BudiStore {
|
|||
return false
|
||||
}
|
||||
const cut = componentToPaste.isCut
|
||||
const sourceScreenId = componentToPaste.screenId
|
||||
const originalId = componentToPaste._id
|
||||
delete componentToPaste.isCut
|
||||
delete componentToPaste.screenId
|
||||
|
||||
// Make new component unique if copying
|
||||
if (!cut) {
|
||||
|
@ -683,6 +689,19 @@ export class ComponentStore extends BudiStore {
|
|||
}
|
||||
newComponentId = componentToPaste._id
|
||||
|
||||
// Strip grid position metadata if pasting into a new screen, but keep
|
||||
// alignment metadata
|
||||
if (sourceScreenId && sourceScreenId !== screen._id) {
|
||||
for (let style of Object.keys(componentToPaste._styles?.normal || {})) {
|
||||
if (
|
||||
style.startsWith("--grid") &&
|
||||
(style.endsWith("-start") || style.endsWith("-end"))
|
||||
) {
|
||||
delete componentToPaste._styles.normal[style]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Delete old component if cutting
|
||||
if (cut) {
|
||||
const parent = findComponentParent(screen.props, originalId)
|
||||
|
@ -725,12 +744,13 @@ export class ComponentStore extends BudiStore {
|
|||
await screenStore.patch(patch, targetScreenId)
|
||||
|
||||
// Select the new component
|
||||
this.update(state => {
|
||||
state.selectedScreenId = targetScreenId
|
||||
state.selectedComponentId = newComponentId
|
||||
|
||||
return state
|
||||
})
|
||||
if (selectComponent) {
|
||||
this.update(state => {
|
||||
state.selectedScreenId = targetScreenId
|
||||
state.selectedComponentId = newComponentId
|
||||
return state
|
||||
})
|
||||
}
|
||||
|
||||
componentTreeNodesStore.makeNodeVisible(newComponentId)
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { v4 } from "uuid"
|
||||
import { Component } from "templates/Component"
|
||||
import { Screen } from "templates/Screen"
|
||||
import { Screen } from "templates/screenTemplating/Screen"
|
||||
import { get } from "svelte/store"
|
||||
import {
|
||||
BUDIBASE_INTERNAL_DB_ID,
|
||||
|
|
|
@ -3,7 +3,6 @@ import { API } from "api"
|
|||
import { auth, admin } from "stores/portal"
|
||||
import { Constants } from "@budibase/frontend-core"
|
||||
import { StripeStatus } from "components/portal/licensing/constants"
|
||||
import { TENANT_FEATURE_FLAGS, isEnabled } from "helpers/featureFlags"
|
||||
import { PlanModel } from "@budibase/types"
|
||||
|
||||
const UNLIMITED = -1
|
||||
|
@ -183,93 +182,91 @@ export const createLicensingStore = () => {
|
|||
return usersLimitExceeded(userCount, get(store).userLimit)
|
||||
},
|
||||
setUsageMetrics: async () => {
|
||||
if (isEnabled(TENANT_FEATURE_FLAGS.LICENSING)) {
|
||||
const usage = get(store).quotaUsage
|
||||
const license = get(auth).user.license
|
||||
const now = new Date()
|
||||
const usage = get(store).quotaUsage
|
||||
const license = get(auth).user.license
|
||||
const now = new Date()
|
||||
|
||||
const getMetrics = (keys, license, quota) => {
|
||||
if (!license || !quota || !keys) {
|
||||
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 getMetrics = (keys, license, quota) => {
|
||||
if (!license || !quota || !keys) {
|
||||
return {}
|
||||
}
|
||||
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,
|
||||
}
|
||||
})
|
||||
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(
|
||||
["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 { isEnabled, TENANT_FEATURE_FLAGS } from "helpers/featureFlags"
|
||||
import { admin } from "./admin"
|
||||
import { auth } from "./auth"
|
||||
import { sdk } from "@budibase/shared-core"
|
||||
|
@ -15,12 +14,10 @@ export const menu = derived([admin, auth], ([$admin, $auth]) => {
|
|||
href: "/builder/portal/users/users",
|
||||
},
|
||||
]
|
||||
if (isEnabled(TENANT_FEATURE_FLAGS.USER_GROUPS)) {
|
||||
userSubPages.push({
|
||||
title: "Groups",
|
||||
href: "/builder/portal/users/groups",
|
||||
})
|
||||
}
|
||||
userSubPages.push({
|
||||
title: "Groups",
|
||||
href: "/builder/portal/users/groups",
|
||||
})
|
||||
|
||||
// Pages that all devs and admins can access
|
||||
let menu = [
|
||||
|
@ -83,50 +80,48 @@ export const menu = derived([admin, auth], ([$admin, $auth]) => {
|
|||
}
|
||||
|
||||
// Add account page
|
||||
if (isEnabled(TENANT_FEATURE_FLAGS.LICENSING)) {
|
||||
let accountSubPages = [
|
||||
{
|
||||
title: "Usage",
|
||||
href: "/builder/portal/account/usage",
|
||||
},
|
||||
]
|
||||
if (isAdmin) {
|
||||
accountSubPages.push({
|
||||
title: "Audit Logs",
|
||||
href: "/builder/portal/account/auditLogs",
|
||||
})
|
||||
let accountSubPages = [
|
||||
{
|
||||
title: "Usage",
|
||||
href: "/builder/portal/account/usage",
|
||||
},
|
||||
]
|
||||
if (isAdmin) {
|
||||
accountSubPages.push({
|
||||
title: "Audit Logs",
|
||||
href: "/builder/portal/account/auditLogs",
|
||||
})
|
||||
|
||||
if (!cloud) {
|
||||
accountSubPages.push({
|
||||
title: "System Logs",
|
||||
href: "/builder/portal/account/systemLogs",
|
||||
})
|
||||
}
|
||||
}
|
||||
if (cloud && user?.accountPortalAccess) {
|
||||
if (!cloud) {
|
||||
accountSubPages.push({
|
||||
title: "Upgrade",
|
||||
href: $admin?.accountPortalUrl + "/portal/upgrade",
|
||||
})
|
||||
} else if (!cloud && isAdmin) {
|
||||
accountSubPages.push({
|
||||
title: "Upgrade",
|
||||
href: "/builder/portal/account/upgrade",
|
||||
title: "System Logs",
|
||||
href: "/builder/portal/account/systemLogs",
|
||||
})
|
||||
}
|
||||
// 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,
|
||||
}
|
||||
if (cloud && user?.accountPortalAccess) {
|
||||
accountSubPages.push({
|
||||
title: "Upgrade",
|
||||
href: $admin?.accountPortalUrl + "/portal/upgrade",
|
||||
})
|
||||
} else if (!cloud && isAdmin) {
|
||||
accountSubPages.push({
|
||||
title: "Upgrade",
|
||||
href: "/builder/portal/account/upgrade",
|
||||
})
|
||||
}
|
||||
// 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
|
||||
})
|
||||
|
|
|
@ -128,8 +128,15 @@ export function createUsersStore() {
|
|||
return await API.removeAppBuilder({ userId, appId })
|
||||
}
|
||||
|
||||
async function getTenantOwner(tenantId) {
|
||||
const tenantInfo = await API.getTenantInfo({ tenantId })
|
||||
return tenantInfo?.owner
|
||||
}
|
||||
|
||||
const getUserRole = user => {
|
||||
if (sdk.users.isAdmin(user)) {
|
||||
if (user && user.email === user.tenantOwnerEmail) {
|
||||
return Constants.BudibaseRoles.Owner
|
||||
} else if (sdk.users.isAdmin(user)) {
|
||||
return Constants.BudibaseRoles.Admin
|
||||
} else if (sdk.users.isBuilder(user)) {
|
||||
return Constants.BudibaseRoles.Developer
|
||||
|
@ -169,6 +176,7 @@ export function createUsersStore() {
|
|||
save: refreshUsage(save),
|
||||
bulkDelete: refreshUsage(bulkDelete),
|
||||
delete: refreshUsage(del),
|
||||
tenantOwner: getTenantOwner,
|
||||
}
|
||||
}
|
||||
|
||||
|
|