Merge branch 'develop' of github.com:Budibase/budibase into labday/sqs
This commit is contained in:
commit
c185dd7dd0
|
@ -1,19 +0,0 @@
|
|||
# Configuration for probot-stale - https://github.com/probot/stale
|
||||
# Number of days of inactivity before an Issue or Pull Request becomes stale
|
||||
daysUntilStale: 60
|
||||
# Number of days of inactivity before an Issue or Pull Request with the stale label is closed.
|
||||
# Set to false to disable. If disabled, issues still need to be closed manually, but will remain marked as stale.
|
||||
daysUntilClose: false
|
||||
# Issues with these labels will never be considered stale
|
||||
exemptLabels:
|
||||
- pinned
|
||||
- security
|
||||
- roadmap
|
||||
# Label to use when marking an issue as stale
|
||||
staleLabel: stale
|
||||
# Comment to post when marking an issue as stale. Set to `false` to disable
|
||||
markComment: >
|
||||
This issue has been automatically marked as stale because it has not had
|
||||
recent activity.
|
||||
# Comment to post when closing a stale issue. Set to `false` to disable
|
||||
closeComment: false
|
|
@ -12,9 +12,6 @@ on:
|
|||
- master
|
||||
- develop
|
||||
pull_request:
|
||||
branches:
|
||||
- master
|
||||
- develop
|
||||
workflow_dispatch:
|
||||
|
||||
env:
|
||||
|
@ -162,7 +159,7 @@ jobs:
|
|||
run: |
|
||||
cd qa-core
|
||||
yarn setup
|
||||
yarn test:ci
|
||||
yarn serve:test:self:ci
|
||||
env:
|
||||
BB_ADMIN_USER_EMAIL: admin
|
||||
BB_ADMIN_USER_PASSWORD: admin
|
||||
|
|
|
@ -6,7 +6,7 @@ concurrency:
|
|||
on:
|
||||
push:
|
||||
tags:
|
||||
- v*-alpha.*
|
||||
- "*-alpha.*"
|
||||
workflow_dispatch:
|
||||
|
||||
env:
|
||||
|
|
|
@ -6,9 +6,9 @@ concurrency:
|
|||
on:
|
||||
push:
|
||||
tags:
|
||||
- "v[0-9]+.[0-9]+.[0-9]+"
|
||||
- "[0-9]+.[0-9]+.[0-9]+"
|
||||
# Exclude all pre-releases
|
||||
- "!v*[0-9]+.[0-9]+.[0-9]+-*"
|
||||
- "!*[0-9]+.[0-9]+.[0-9]+-*"
|
||||
|
||||
env:
|
||||
# Posthog token used by ui at build time
|
||||
|
@ -34,7 +34,6 @@ jobs:
|
|||
exit 1
|
||||
fi
|
||||
|
||||
|
||||
- uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: 14.x
|
||||
|
@ -58,9 +57,12 @@ jobs:
|
|||
echo //registry.npmjs.org/:_authToken=${NPM_TOKEN} >> .npmrc
|
||||
yarn release
|
||||
|
||||
- name: "Get Previous tag"
|
||||
id: previoustag
|
||||
uses: "WyriHaximus/github-action-get-previous-tag@v1"
|
||||
- name: "Get Current tag"
|
||||
id: currenttag
|
||||
run: |
|
||||
version=v$(./scripts/getCurrentVersion.sh)
|
||||
echo 'Using tag $version'
|
||||
echo "::set-output name=tag::$resversionult"
|
||||
|
||||
- name: Build/release Docker images
|
||||
run: |
|
||||
|
@ -69,7 +71,7 @@ jobs:
|
|||
env:
|
||||
DOCKER_USER: ${{ secrets.DOCKER_USERNAME }}
|
||||
DOCKER_PASSWORD: ${{ secrets.DOCKER_API_KEY }}
|
||||
BUDIBASE_RELEASE_VERSION: ${{ steps.previoustag.outputs.tag }}
|
||||
BUDIBASE_RELEASE_VERSION: ${{ steps.currenttag.outputs.tag }}
|
||||
|
||||
release-helm-chart:
|
||||
needs: [release-images]
|
||||
|
@ -96,7 +98,7 @@ jobs:
|
|||
git fetch
|
||||
mkdir sync
|
||||
echo "Packaging chart to sync dir"
|
||||
helm package charts/budibase --version 0.0.0-master --app-version v"$RELEASE_VERSION" --destination sync
|
||||
helm package charts/budibase --version 0.0.0-master --app-version "$RELEASE_VERSION" --destination sync
|
||||
echo "Packaging successful"
|
||||
git checkout gh-pages
|
||||
echo "Indexing helm repo"
|
||||
|
|
|
@ -43,7 +43,7 @@ jobs:
|
|||
run: |
|
||||
docker login -u $DOCKER_USER -p $DOCKER_PASSWORD
|
||||
|
||||
release_tag=v${{ env.RELEASE_VERSION }}
|
||||
release_tag=${{ env.RELEASE_VERSION }}
|
||||
|
||||
# Pull apps and worker images
|
||||
docker pull budibase/apps:$release_tag
|
||||
|
@ -108,8 +108,8 @@ jobs:
|
|||
- name: Perform Github Release
|
||||
uses: softprops/action-gh-release@v1
|
||||
with:
|
||||
name: v${{ env.RELEASE_VERSION }}
|
||||
tag_name: v${{ env.RELEASE_VERSION }}
|
||||
name: ${{ env.RELEASE_VERSION }}
|
||||
tag_name: ${{ env.RELEASE_VERSION }}
|
||||
generate_release_notes: true
|
||||
files: |
|
||||
packages/cli/build/cli-win.exe
|
||||
|
|
|
@ -71,7 +71,7 @@ jobs:
|
|||
context: .
|
||||
push: true
|
||||
platforms: linux/amd64,linux/arm64
|
||||
tags: budibase/budibase,budibase/budibase:v${{ env.RELEASE_VERSION }}
|
||||
tags: budibase/budibase,budibase/budibase:${{ env.RELEASE_VERSION }}
|
||||
file: ./hosting/single/Dockerfile
|
||||
- name: Tag and release Budibase Azure App Service docker image
|
||||
uses: docker/build-push-action@v2
|
||||
|
@ -80,5 +80,5 @@ jobs:
|
|||
push: true
|
||||
platforms: linux/amd64
|
||||
build-args: TARGETBUILD=aas
|
||||
tags: budibase/budibase-aas,budibase/budibase-aas:v${{ env.RELEASE_VERSION }}
|
||||
tags: budibase/budibase-aas,budibase/budibase-aas:${{ env.RELEASE_VERSION }}
|
||||
file: ./hosting/single/Dockerfile
|
||||
|
|
|
@ -0,0 +1,29 @@
|
|||
name: Close stale issues and PRs # https://github.com/actions/stale
|
||||
on:
|
||||
workflow_dispatch:
|
||||
schedule:
|
||||
- cron: '30 1 * * *' # 1:30 every morning
|
||||
|
||||
jobs:
|
||||
stale:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/stale@v8
|
||||
with:
|
||||
# stale rules
|
||||
days-before-stale: 60
|
||||
days-before-pr-stale: 7
|
||||
stale-issue-label: stale
|
||||
stale-issue-message: "This issue has been automatically marked as stale because it has not had any activity for 60 days."
|
||||
|
||||
# close rules
|
||||
# days after being marked as stale to close
|
||||
days-before-close: 30
|
||||
close-issue-label: closed-stale
|
||||
close-issue-message: This issue has been automatically closed it has not had any activity in 90 days."
|
||||
days-before-pr-close: 7
|
||||
|
||||
# exemptions
|
||||
exempt-pr-labels: pinned,security,roadmap
|
||||
|
||||
|
|
@ -1,2 +1,2 @@
|
|||
nodejs 14.20.1
|
||||
nodejs 14.21.3
|
||||
python 3.10.0
|
|
@ -40,6 +40,24 @@ spec:
|
|||
- image: budibase/proxy:{{ .Values.globals.appVersion | default .Chart.AppVersion }}
|
||||
imagePullPolicy: Always
|
||||
name: proxy-service
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
path: /health
|
||||
port: {{ .Values.services.proxy.port }}
|
||||
initialDelaySeconds: 0
|
||||
periodSeconds: 5
|
||||
successThreshold: 1
|
||||
failureThreshold: 2
|
||||
timeoutSeconds: 3
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: /health
|
||||
port: {{ .Values.services.proxy.port }}
|
||||
initialDelaySeconds: 0
|
||||
periodSeconds: 5
|
||||
successThreshold: 1
|
||||
failureThreshold: 2
|
||||
timeoutSeconds: 3
|
||||
ports:
|
||||
- containerPort: {{ .Values.services.proxy.port }}
|
||||
env:
|
||||
|
|
|
@ -209,7 +209,7 @@ services:
|
|||
# Override values in couchDB subchart
|
||||
couchdb:
|
||||
## clusterSize is the initial size of the CouchDB cluster.
|
||||
clusterSize: 3
|
||||
clusterSize: 1
|
||||
allowAdminParty: false
|
||||
|
||||
# Secret Management
|
||||
|
|
|
@ -231,18 +231,33 @@ An overview of the CI pipelines can be found [here](../.github/workflows/README.
|
|||
|
||||
### Pro
|
||||
|
||||
@budibase/pro is the closed source package that supports licensed features in budibase. By default the package will be pulled from NPM and will not normally need to be touched in local development. If you require to update code inside the pro package it can be cloned to the same root level as budibase, e.g.
|
||||
@budibase/pro is the closed source package that supports licensed features in budibase. By default the package will be pulled from NPM and will not normally need to be touched in local development. If you need to make an update to pro and have access to the repo, then you can update your submodule within the mono-repo by running `git submodule update --init` - from here you can use normal submodule flow to develop a change within pro.
|
||||
|
||||
Once you have updated to use the pro submodule, it will be linked into all of your local dependencies by NX as with all other monorepo packages. If you have been using the NPM version of `@budibase/pro` then you may need to run a `git reset --hard` to fix all of the pro versions back to `0.0.0` to be monorepo aware.
|
||||
|
||||
From here - to develop a change in pro, you can follow the below flow:
|
||||
|
||||
```
|
||||
.
|
||||
|_ budibase
|
||||
|_ budibase-pro
|
||||
# enter the pro submodule
|
||||
cd packages/pro
|
||||
# get the base branch you are working from (same as monorepo)
|
||||
git fetch
|
||||
git checkout <develop | master>
|
||||
# create a branch, named the same as the branch in your monorepo
|
||||
git checkout -b <some branch>
|
||||
... make changes
|
||||
# commit the changes you've made, with a message for pro
|
||||
git commit <something>
|
||||
# within the monorepo, add the pro reference to your branch, commit it with a message like "Update pro ref"
|
||||
cd ../..
|
||||
git add packages/pro
|
||||
git commit <add the new reference to main repo>
|
||||
```
|
||||
|
||||
From here, you will have created a branch in the pro repository and commited the reference to your branch on the monorepo. When you eventually PR this work back into the mainline branch, you will need to first merge your pro PR to the pro mainline, then go into your PR in the monorepo and update the reference again to the new mainline.
|
||||
|
||||
Note that only budibase maintainers will be able to access the pro repo.
|
||||
|
||||
By default, NX will make sure that dependencies are replaced with local source aware version. This is achieved using the `yarn link` command. To see specifically how dependencies are linked see [scripts/link-dependencies.sh](../scripts/link-dependencies.sh). The same link script is used to link dependencies to account-portal in local dev.
|
||||
|
||||
### Troubleshooting
|
||||
|
||||
Sometimes, things go wrong. This can be due to incompatible updates on the budibase platform. To clear down your development environment and start again follow **Step 6. Cleanup**, then proceed from **Step 3. Install and Build** in the setup guide above to create a fresh Budibase installation.
|
||||
|
|
|
@ -28,3 +28,4 @@ BB_ADMIN_USER_PASSWORD=
|
|||
|
||||
# A path that is watched for plugin bundles. Any bundles found are imported automatically/
|
||||
PLUGINS_DIR=
|
||||
ROLLING_LOG_MAX_SIZE=
|
|
@ -22,6 +22,16 @@ server {
|
|||
proxy_pass http://127.0.0.1:4001;
|
||||
}
|
||||
|
||||
location /embed {
|
||||
rewrite /embed/(.*) /app/$1 break;
|
||||
proxy_pass http://127.0.0.1:4001;
|
||||
proxy_redirect off;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header x-budibase-embed "true";
|
||||
add_header x-budibase-embed "true";
|
||||
add_header Content-Security-Policy "frame-ancestors *";
|
||||
}
|
||||
|
||||
location = / {
|
||||
proxy_pass http://127.0.0.1:4001;
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"version": "2.8.2-alpha.2",
|
||||
"version": "2.8.22-alpha.0",
|
||||
"npmClient": "yarn",
|
||||
"packages": [
|
||||
"packages/*"
|
||||
|
|
12
nx.json
12
nx.json
|
@ -1,9 +1,13 @@
|
|||
{
|
||||
"tasksRunnerOptions": {
|
||||
"default": {
|
||||
"runner": "nx/tasks-runners/default",
|
||||
"runner": "nx-cloud",
|
||||
"options": {
|
||||
"cacheableOperations": ["build", "test"]
|
||||
"cacheableOperations": [
|
||||
"build",
|
||||
"test"
|
||||
],
|
||||
"accessToken": "YWNiYzc5NTEtMzMzZC00NDhjLTgyNjktZTllMjI1MzM4OGQxfHJlYWQtd3JpdGU="
|
||||
}
|
||||
}
|
||||
},
|
||||
|
@ -11,7 +15,9 @@
|
|||
"dev:builder": {
|
||||
"dependsOn": [
|
||||
{
|
||||
"projects": ["@budibase/string-templates"],
|
||||
"projects": [
|
||||
"@budibase/string-templates"
|
||||
],
|
||||
"target": "build"
|
||||
}
|
||||
]
|
||||
|
|
19
package.json
19
package.json
|
@ -3,7 +3,7 @@
|
|||
"private": true,
|
||||
"devDependencies": {
|
||||
"@esbuild-plugins/tsconfig-paths": "^0.1.2",
|
||||
"@nx/js": "16.2.1",
|
||||
"@nx/js": "16.4.3",
|
||||
"@rollup/plugin-json": "^4.0.2",
|
||||
"@typescript-eslint/parser": "5.45.0",
|
||||
"esbuild": "^0.17.18",
|
||||
|
@ -13,9 +13,11 @@
|
|||
"husky": "^8.0.3",
|
||||
"js-yaml": "^4.1.0",
|
||||
"kill-port": "^1.6.1",
|
||||
"lerna": "7.0.2",
|
||||
"lerna": "7.1.1",
|
||||
"madge": "^6.0.0",
|
||||
"minimist": "^1.2.8",
|
||||
"nx": "16.4.3",
|
||||
"nx-cloud": "16.0.5",
|
||||
"prettier": "2.8.8",
|
||||
"prettier-plugin-svelte": "^2.3.0",
|
||||
"rimraf": "^3.0.2",
|
||||
|
@ -44,15 +46,15 @@
|
|||
"restore": "yarn run clean && yarn run bootstrap && yarn run build",
|
||||
"nuke": "yarn run nuke:packages && yarn run nuke:docker",
|
||||
"nuke:packages": "yarn run restore",
|
||||
"nuke:docker": "lerna run --stream --parallel dev:stack:nuke",
|
||||
"nuke:docker": "lerna run --stream dev:stack:nuke",
|
||||
"clean": "lerna clean",
|
||||
"kill-builder": "kill-port 3000",
|
||||
"kill-server": "kill-port 4001 4002",
|
||||
"kill-all": "yarn run kill-builder && yarn run kill-server",
|
||||
"dev": "yarn run kill-all && lerna run --stream --parallel dev:builder --stream",
|
||||
"dev:noserver": "yarn run kill-builder && lerna run --stream dev:stack:up && lerna run --stream --parallel dev:builder --ignore @budibase/backend-core --ignore @budibase/server --ignore @budibase/worker",
|
||||
"dev:server": "yarn run kill-server && yarn build --projects=@budibase/client && lerna run --stream --parallel dev:builder --scope @budibase/worker --scope @budibase/server",
|
||||
"dev:built": "yarn run kill-all && cd packages/server && yarn dev:stack:up && cd ../../ && lerna run --stream --parallel dev:built",
|
||||
"dev": "yarn run kill-all && lerna run --stream dev:builder --stream",
|
||||
"dev:noserver": "yarn run kill-builder && lerna run --stream dev:stack:up && lerna run --stream dev:builder --ignore @budibase/backend-core --ignore @budibase/server --ignore @budibase/worker",
|
||||
"dev:server": "yarn run kill-server && lerna run --stream dev:builder --scope @budibase/worker --scope @budibase/server",
|
||||
"dev:built": "yarn run kill-all && cd packages/server && yarn dev:stack:up && cd ../../ && lerna run --stream dev:built",
|
||||
"dev:docker": "yarn build:docker:pre && docker-compose -f hosting/docker-compose.build.yaml -f hosting/docker-compose.dev.yaml --env-file hosting/.env up --build --scale proxy-service=0",
|
||||
"test": "lerna run --stream test --stream",
|
||||
"lint:eslint": "eslint packages qa-core --max-warnings=0",
|
||||
|
@ -106,6 +108,5 @@
|
|||
"@budibase/string-templates": "0.0.0",
|
||||
"@budibase/types": "0.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
}
|
||||
"dependencies": {}
|
||||
}
|
||||
|
|
|
@ -51,6 +51,7 @@
|
|||
"pouchdb": "7.3.0",
|
||||
"pouchdb-find": "7.2.2",
|
||||
"redlock": "4.2.0",
|
||||
"rotating-file-stream": "3.1.0",
|
||||
"sanitize-s3-objectkey": "0.0.1",
|
||||
"semver": "7.3.7",
|
||||
"tar-fs": "2.1.1",
|
||||
|
|
|
@ -159,7 +159,7 @@ export async function updateUserOAuth(userId: string, oAuthConfig: any) {
|
|||
|
||||
try {
|
||||
const db = getGlobalDB()
|
||||
const dbUser = await db.get(userId)
|
||||
const dbUser = await db.get<any>(userId)
|
||||
|
||||
//Do not overwrite the refresh token if a valid one is not provided.
|
||||
if (typeof details.refreshToken !== "string") {
|
||||
|
|
|
@ -12,7 +12,7 @@ const EXPIRY_SECONDS = 3600
|
|||
*/
|
||||
async function populateFromDB(userId: string, tenantId: string) {
|
||||
const db = tenancy.getTenantDB(tenantId)
|
||||
const user = await db.get(userId)
|
||||
const user = await db.get<any>(userId)
|
||||
user.budibaseAccess = true
|
||||
if (!env.SELF_HOSTED && !env.DISABLE_ACCOUNT_PORTAL) {
|
||||
const account = await accounts.getAccount(user.email)
|
||||
|
|
|
@ -20,6 +20,8 @@ export enum Header {
|
|||
TYPE = "x-budibase-type",
|
||||
PREVIEW_ROLE = "x-budibase-role",
|
||||
TENANT_ID = "x-budibase-tenant-id",
|
||||
VERIFICATION_CODE = "x-budibase-verification-code",
|
||||
RETURN_VERIFICATION_CODE = "x-budibase-return-verification-code",
|
||||
TOKEN = "x-budibase-token",
|
||||
CSRF_TOKEN = "x-csrf-token",
|
||||
CORRELATION_ID = "x-budibase-correlation-id",
|
||||
|
|
|
@ -433,6 +433,9 @@ export class QueryBuilder<T> {
|
|||
if (!value) {
|
||||
return null
|
||||
}
|
||||
if (typeof value === "boolean") {
|
||||
return `(*:* AND !${key}:${value})`
|
||||
}
|
||||
return `!${key}:${builder.preprocess(value, allPreProcessingOpts)}`
|
||||
})
|
||||
}
|
||||
|
|
|
@ -5,7 +5,7 @@ export async function createUserIndex() {
|
|||
const db = getGlobalDB()
|
||||
let designDoc
|
||||
try {
|
||||
designDoc = await db.get("_design/database")
|
||||
designDoc = await db.get<any>("_design/database")
|
||||
} catch (err: any) {
|
||||
if (err.status === 404) {
|
||||
designDoc = { _id: "_design/database" }
|
||||
|
|
|
@ -47,7 +47,10 @@ function httpLogging() {
|
|||
return process.env.HTTP_LOGGING
|
||||
}
|
||||
|
||||
function findVersion() {
|
||||
function getPackageJsonFields(): {
|
||||
VERSION: string
|
||||
SERVICE_NAME: string
|
||||
} {
|
||||
function findFileInAncestors(
|
||||
fileName: string,
|
||||
currentDir: string
|
||||
|
@ -69,10 +72,14 @@ function findVersion() {
|
|||
try {
|
||||
const packageJsonFile = findFileInAncestors("package.json", process.cwd())
|
||||
const content = readFileSync(packageJsonFile!, "utf-8")
|
||||
return JSON.parse(content).version
|
||||
const parsedContent = JSON.parse(content)
|
||||
return {
|
||||
VERSION: parsedContent.version,
|
||||
SERVICE_NAME: parsedContent.name,
|
||||
}
|
||||
} catch {
|
||||
// throwing an error here is confusing/causes backend-core to be hard to import
|
||||
return undefined
|
||||
return { VERSION: "", SERVICE_NAME: "" }
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -155,13 +162,14 @@ const environment = {
|
|||
ENABLE_SSO_MAINTENANCE_MODE: selfHosted
|
||||
? process.env.ENABLE_SSO_MAINTENANCE_MODE
|
||||
: false,
|
||||
VERSION: findVersion(),
|
||||
...getPackageJsonFields(),
|
||||
DISABLE_PINO_LOGGER: process.env.DISABLE_PINO_LOGGER,
|
||||
_set(key: any, value: any) {
|
||||
process.env[key] = value
|
||||
// @ts-ignore
|
||||
environment[key] = value
|
||||
},
|
||||
ROLLING_LOG_MAX_SIZE: process.env.ROLLING_LOG_MAX_SIZE || "10M",
|
||||
}
|
||||
|
||||
// clean up any environment variable edge cases
|
||||
|
|
|
@ -1,6 +1,4 @@
|
|||
export * as correlation from "./correlation/correlation"
|
||||
export { logger } from "./pino/logger"
|
||||
export * from "./alerts"
|
||||
|
||||
// turn off or on context logging i.e. tenantId, appId etc
|
||||
export let LOG_CONTEXT = true
|
||||
export * as system from "./system"
|
||||
|
|
|
@ -1,37 +1,60 @@
|
|||
import env from "../../environment"
|
||||
import pino, { LoggerOptions } from "pino"
|
||||
import pinoPretty from "pino-pretty"
|
||||
|
||||
import { IdentityType } from "@budibase/types"
|
||||
import env from "../../environment"
|
||||
import * as context from "../../context"
|
||||
import * as correlation from "../correlation"
|
||||
import { IdentityType } from "@budibase/types"
|
||||
import { LOG_CONTEXT } from "../index"
|
||||
|
||||
import { localFileDestination } from "../system"
|
||||
|
||||
// LOGGER
|
||||
|
||||
let pinoInstance: pino.Logger | undefined
|
||||
if (!env.DISABLE_PINO_LOGGER) {
|
||||
const level = env.LOG_LEVEL
|
||||
const pinoOptions: LoggerOptions = {
|
||||
level: env.LOG_LEVEL,
|
||||
level,
|
||||
formatters: {
|
||||
level: label => {
|
||||
return { level: label.toUpperCase() }
|
||||
level: level => {
|
||||
return { level: level.toUpperCase() }
|
||||
},
|
||||
bindings: () => {
|
||||
return {}
|
||||
if (env.SELF_HOSTED) {
|
||||
// "service" is being injected in datadog using the pod names,
|
||||
// so we should leave it blank to allow the default behaviour if it's not running self-hosted
|
||||
return {
|
||||
service: env.SERVICE_NAME,
|
||||
}
|
||||
} else {
|
||||
return {}
|
||||
}
|
||||
},
|
||||
},
|
||||
timestamp: () => `,"timestamp":"${new Date(Date.now()).toISOString()}"`,
|
||||
}
|
||||
|
||||
if (env.isDev()) {
|
||||
pinoOptions.transport = {
|
||||
target: "pino-pretty",
|
||||
options: {
|
||||
singleLine: true,
|
||||
},
|
||||
}
|
||||
const destinations: pino.StreamEntry[] = []
|
||||
|
||||
destinations.push(
|
||||
env.isDev()
|
||||
? {
|
||||
stream: pinoPretty({ singleLine: true }),
|
||||
level: level as pino.Level,
|
||||
}
|
||||
: { stream: process.stdout, level: level as pino.Level }
|
||||
)
|
||||
|
||||
if (env.SELF_HOSTED) {
|
||||
destinations.push({
|
||||
stream: localFileDestination(),
|
||||
level: level as pino.Level,
|
||||
})
|
||||
}
|
||||
|
||||
pinoInstance = pino(pinoOptions)
|
||||
pinoInstance = destinations.length
|
||||
? pino(pinoOptions, pino.multistream(destinations))
|
||||
: pino(pinoOptions)
|
||||
|
||||
// CONSOLE OVERRIDES
|
||||
|
||||
|
@ -83,15 +106,13 @@ if (!env.DISABLE_PINO_LOGGER) {
|
|||
|
||||
let contextObject = {}
|
||||
|
||||
if (LOG_CONTEXT) {
|
||||
contextObject = {
|
||||
tenantId: getTenantId(),
|
||||
appId: getAppId(),
|
||||
automationId: getAutomationId(),
|
||||
identityId: identity?._id,
|
||||
identityType: identity?.type,
|
||||
correlationId: correlation.getId(),
|
||||
}
|
||||
contextObject = {
|
||||
tenantId: getTenantId(),
|
||||
appId: getAppId(),
|
||||
automationId: getAutomationId(),
|
||||
identityId: identity?._id,
|
||||
identityType: identity?.type,
|
||||
correlationId: correlation.getId(),
|
||||
}
|
||||
|
||||
const mergingObject: any = {
|
||||
|
|
|
@ -0,0 +1,81 @@
|
|||
import fs from "fs"
|
||||
import path from "path"
|
||||
import * as rfs from "rotating-file-stream"
|
||||
|
||||
import env from "../environment"
|
||||
import { budibaseTempDir } from "../objectStore"
|
||||
|
||||
const logsFileName = `budibase.log`
|
||||
const budibaseLogsHistoryFileName = "budibase-logs-history.txt"
|
||||
|
||||
const logsPath = path.join(budibaseTempDir(), "systemlogs")
|
||||
|
||||
function getFullPath(fileName: string) {
|
||||
return path.join(logsPath, fileName)
|
||||
}
|
||||
|
||||
export function getSingleFileMaxSizeInfo(totalMaxSize: string) {
|
||||
const regex = /(\d+)([A-Za-z])/
|
||||
const match = totalMaxSize?.match(regex)
|
||||
if (!match) {
|
||||
console.warn(`totalMaxSize does not have a valid value`, {
|
||||
totalMaxSize,
|
||||
})
|
||||
return undefined
|
||||
}
|
||||
|
||||
const size = +match[1]
|
||||
const unit = match[2]
|
||||
if (size === 1) {
|
||||
switch (unit) {
|
||||
case "B":
|
||||
return { size: `${size}B`, totalHistoryFiles: 1 }
|
||||
case "K":
|
||||
return { size: `${(size * 1000) / 2}B`, totalHistoryFiles: 1 }
|
||||
case "M":
|
||||
return { size: `${(size * 1000) / 2}K`, totalHistoryFiles: 1 }
|
||||
case "G":
|
||||
return { size: `${(size * 1000) / 2}M`, totalHistoryFiles: 1 }
|
||||
default:
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
|
||||
if (size % 2 === 0) {
|
||||
return { size: `${size / 2}${unit}`, totalHistoryFiles: 1 }
|
||||
}
|
||||
|
||||
return { size: `1${unit}`, totalHistoryFiles: size - 1 }
|
||||
}
|
||||
|
||||
export function localFileDestination() {
|
||||
const fileInfo = getSingleFileMaxSizeInfo(env.ROLLING_LOG_MAX_SIZE)
|
||||
const outFile = rfs.createStream(logsFileName, {
|
||||
// As we have a rolling size, we want to half the max size
|
||||
size: fileInfo?.size,
|
||||
path: logsPath,
|
||||
maxFiles: fileInfo?.totalHistoryFiles || 1,
|
||||
immutable: true,
|
||||
history: budibaseLogsHistoryFileName,
|
||||
initialRotation: false,
|
||||
})
|
||||
|
||||
return outFile
|
||||
}
|
||||
|
||||
export function getLogReadStream() {
|
||||
const streams = []
|
||||
const historyFile = getFullPath(budibaseLogsHistoryFileName)
|
||||
if (fs.existsSync(historyFile)) {
|
||||
const fileContent = fs.readFileSync(historyFile, "utf-8")
|
||||
const historyFiles = fileContent.split("\n")
|
||||
for (const historyFile of historyFiles.filter(x => x)) {
|
||||
streams.push(fs.readFileSync(historyFile))
|
||||
}
|
||||
}
|
||||
|
||||
streams.push(fs.readFileSync(getFullPath(logsFileName)))
|
||||
|
||||
const combinedContent = Buffer.concat(streams)
|
||||
return combinedContent
|
||||
}
|
|
@ -0,0 +1,61 @@
|
|||
import { getSingleFileMaxSizeInfo } from "../system"
|
||||
|
||||
describe("system", () => {
|
||||
describe("getSingleFileMaxSizeInfo", () => {
|
||||
it.each([
|
||||
["100B", "50B"],
|
||||
["200K", "100K"],
|
||||
["20M", "10M"],
|
||||
["4G", "2G"],
|
||||
])(
|
||||
"Halving even number (%s) returns halved size and 1 history file (%s)",
|
||||
(totalValue, expectedMaxSize) => {
|
||||
const result = getSingleFileMaxSizeInfo(totalValue)
|
||||
expect(result).toEqual({
|
||||
size: expectedMaxSize,
|
||||
totalHistoryFiles: 1,
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
it.each([
|
||||
["5B", "1B", 4],
|
||||
["17K", "1K", 16],
|
||||
["21M", "1M", 20],
|
||||
["3G", "1G", 2],
|
||||
])(
|
||||
"Halving an odd number (%s) returns as many files as size (-1) (%s)",
|
||||
(totalValue, expectedMaxSize, totalHistoryFiles) => {
|
||||
const result = getSingleFileMaxSizeInfo(totalValue)
|
||||
expect(result).toEqual({
|
||||
size: expectedMaxSize,
|
||||
totalHistoryFiles,
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
it.each([
|
||||
["1B", "1B"],
|
||||
["1K", "500B"],
|
||||
["1M", "500K"],
|
||||
["1G", "500M"],
|
||||
])(
|
||||
"Halving '%s' returns halved unit (%s)",
|
||||
(totalValue, expectedMaxSize) => {
|
||||
const result = getSingleFileMaxSizeInfo(totalValue)
|
||||
expect(result).toEqual({
|
||||
size: expectedMaxSize,
|
||||
totalHistoryFiles: 1,
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
it.each([[undefined], [""], ["50"], ["wrongvalue"]])(
|
||||
"Halving wrongly formatted value ('%s') returns undefined",
|
||||
totalValue => {
|
||||
const result = getSingleFileMaxSizeInfo(totalValue!)
|
||||
expect(result).toBeUndefined()
|
||||
}
|
||||
)
|
||||
})
|
||||
})
|
|
@ -67,9 +67,9 @@ export const bulkUpdateGlobalUsers = async (users: User[]) => {
|
|||
|
||||
export async function getById(id: string, opts?: GetOpts): Promise<User> {
|
||||
const db = context.getGlobalDB()
|
||||
let user = await db.get(id)
|
||||
let user = await db.get<User>(id)
|
||||
if (opts?.cleanup) {
|
||||
user = removeUserPassword(user)
|
||||
user = removeUserPassword(user) as User
|
||||
}
|
||||
return user
|
||||
}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
<script>
|
||||
import "@spectrum-css/button/dist/index-vars.css"
|
||||
import Tooltip from "../Tooltip/Tooltip.svelte"
|
||||
import AbsTooltip from "../Tooltip/AbsTooltip.svelte"
|
||||
import { createEventDispatcher } from "svelte"
|
||||
|
||||
export let type
|
||||
export let disabled = false
|
||||
|
@ -16,48 +17,53 @@
|
|||
export let tooltip = undefined
|
||||
export let newStyles = true
|
||||
export let id
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
</script>
|
||||
|
||||
<button
|
||||
{id}
|
||||
{type}
|
||||
class:spectrum-Button--cta={cta}
|
||||
class:spectrum-Button--primary={primary}
|
||||
class:spectrum-Button--secondary={secondary}
|
||||
class:spectrum-Button--warning={warning}
|
||||
class:spectrum-Button--overBackground={overBackground}
|
||||
class:spectrum-Button--quiet={quiet}
|
||||
class:new-styles={newStyles}
|
||||
class:active
|
||||
class:disabled
|
||||
class="spectrum-Button spectrum-Button--size{size.toUpperCase()}"
|
||||
{disabled}
|
||||
on:click|preventDefault
|
||||
>
|
||||
{#if icon}
|
||||
<svg
|
||||
class="spectrum-Icon spectrum-Icon--size{size.toUpperCase()}"
|
||||
focusable="false"
|
||||
aria-hidden="true"
|
||||
aria-label={icon}
|
||||
>
|
||||
<use xlink:href="#spectrum-icon-18-{icon}" />
|
||||
</svg>
|
||||
{/if}
|
||||
{#if $$slots}
|
||||
<span class="spectrum-Button-label"><slot /></span>
|
||||
{/if}
|
||||
{#if tooltip}
|
||||
<div class="tooltip">
|
||||
<Tooltip textWrapping={true} direction={"bottom"} text={tooltip} />
|
||||
</div>
|
||||
{/if}
|
||||
</button>
|
||||
<AbsTooltip text={tooltip}>
|
||||
<button
|
||||
{id}
|
||||
{type}
|
||||
class:spectrum-Button--cta={cta}
|
||||
class:spectrum-Button--primary={primary}
|
||||
class:spectrum-Button--secondary={secondary}
|
||||
class:spectrum-Button--warning={warning}
|
||||
class:spectrum-Button--overBackground={overBackground}
|
||||
class:spectrum-Button--quiet={quiet}
|
||||
class:new-styles={newStyles}
|
||||
class:active
|
||||
class:is-disabled={disabled}
|
||||
class="spectrum-Button spectrum-Button--size{size.toUpperCase()}"
|
||||
on:click|preventDefault={() => {
|
||||
if (!disabled) {
|
||||
dispatch("click")
|
||||
}
|
||||
}}
|
||||
>
|
||||
{#if icon}
|
||||
<svg
|
||||
class="spectrum-Icon spectrum-Icon--size{size.toUpperCase()}"
|
||||
focusable="false"
|
||||
aria-hidden="true"
|
||||
aria-label={icon}
|
||||
>
|
||||
<use xlink:href="#spectrum-icon-18-{icon}" />
|
||||
</svg>
|
||||
{/if}
|
||||
{#if $$slots}
|
||||
<span class="spectrum-Button-label"><slot /></span>
|
||||
{/if}
|
||||
</button>
|
||||
</AbsTooltip>
|
||||
|
||||
<style>
|
||||
button {
|
||||
position: relative;
|
||||
}
|
||||
button.is-disabled {
|
||||
cursor: default;
|
||||
}
|
||||
.spectrum-Button-label {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
|
@ -66,23 +72,6 @@
|
|||
.active {
|
||||
color: var(--spectrum-global-color-blue-600) !important;
|
||||
}
|
||||
.tooltip {
|
||||
position: absolute;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
z-index: 100;
|
||||
width: 160px;
|
||||
text-align: center;
|
||||
transform: translateX(-50%);
|
||||
left: 50%;
|
||||
top: 100%;
|
||||
opacity: 0;
|
||||
transition: opacity 130ms ease-out;
|
||||
pointer-events: none;
|
||||
}
|
||||
button:hover .tooltip {
|
||||
opacity: 1;
|
||||
}
|
||||
.spectrum-Button--primary.new-styles {
|
||||
background: var(--spectrum-global-color-gray-800);
|
||||
border-color: transparent;
|
||||
|
@ -96,10 +85,10 @@
|
|||
border-color: transparent;
|
||||
color: var(--spectrum-global-color-gray-900);
|
||||
}
|
||||
.spectrum-Button--secondary.new-styles:not(.disabled):hover {
|
||||
.spectrum-Button--secondary.new-styles:not(.is-disabled):hover {
|
||||
background: var(--spectrum-global-color-gray-300);
|
||||
}
|
||||
.spectrum-Button--secondary.new-styles.disabled {
|
||||
.spectrum-Button--secondary.new-styles.is-disabled {
|
||||
color: var(--spectrum-global-color-gray-500);
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -30,6 +30,7 @@
|
|||
setContext("drawer-actions", {
|
||||
hide,
|
||||
show,
|
||||
headless,
|
||||
})
|
||||
|
||||
const easeInOutQuad = x => {
|
||||
|
|
|
@ -12,23 +12,24 @@
|
|||
export let getOptionValue = option => option
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
const onChange = e => {
|
||||
let tempValue = value
|
||||
let isChecked = e.target.checked
|
||||
if (!tempValue.includes(e.target.value) && isChecked) {
|
||||
tempValue.push(e.target.value)
|
||||
const optionValue = e.target.value
|
||||
if (e.target.checked && !value.includes(optionValue)) {
|
||||
dispatch("change", [...value, optionValue])
|
||||
} else {
|
||||
dispatch(
|
||||
"change",
|
||||
value.filter(x => x !== optionValue)
|
||||
)
|
||||
}
|
||||
value = tempValue
|
||||
dispatch(
|
||||
"change",
|
||||
tempValue.filter(val => val !== e.target.value || isChecked)
|
||||
)
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class={`spectrum-FieldGroup spectrum-FieldGroup--${direction}`}>
|
||||
{#if options && Array.isArray(options)}
|
||||
{#each options as option}
|
||||
{@const optionValue = getOptionValue(option)}
|
||||
<div
|
||||
title={getOptionLabel(option)}
|
||||
class="spectrum-Checkbox spectrum-FieldGroup-item"
|
||||
|
@ -39,11 +40,11 @@
|
|||
>
|
||||
<input
|
||||
on:change={onChange}
|
||||
value={getOptionValue(option)}
|
||||
type="checkbox"
|
||||
class="spectrum-Checkbox-input"
|
||||
value={optionValue}
|
||||
checked={value.includes(optionValue)}
|
||||
{disabled}
|
||||
checked={value.includes(getOptionValue(option))}
|
||||
/>
|
||||
<span class="spectrum-Checkbox-box">
|
||||
<svg
|
||||
|
|
|
@ -82,7 +82,7 @@
|
|||
{#if open}
|
||||
<div class="overlay" on:mousedown|self={() => (open = false)} />
|
||||
<div
|
||||
transition:fly={{ y: -20, duration: 200 }}
|
||||
transition:fly|local={{ y: -20, duration: 200 }}
|
||||
class="spectrum-Popover spectrum-Popover--bottom is-open"
|
||||
>
|
||||
<ul class="spectrum-Menu" role="listbox">
|
||||
|
|
|
@ -62,6 +62,13 @@
|
|||
}
|
||||
}
|
||||
|
||||
const getInputMode = type => {
|
||||
if (type === "bigint") {
|
||||
return "numeric"
|
||||
}
|
||||
return type === "number" ? "decimal" : "text"
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
focus = autofocus
|
||||
if (focus) field.focus()
|
||||
|
@ -103,7 +110,7 @@
|
|||
{type}
|
||||
class="spectrum-Textfield-input"
|
||||
style={align ? `text-align: ${align};` : ""}
|
||||
inputmode={type === "number" ? "decimal" : "text"}
|
||||
inputmode={getInputMode(type)}
|
||||
{autocomplete}
|
||||
/>
|
||||
</div>
|
||||
|
|
|
@ -47,7 +47,7 @@
|
|||
</svg>
|
||||
{#if tooltip && showTooltip}
|
||||
<div class="tooltip" in:fade={{ duration: 130, delay: 250 }}>
|
||||
<Tooltip textWrapping direction="bottom" text={tooltip} />
|
||||
<Tooltip textWrapping direction="top" text={tooltip} />
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
@ -80,15 +80,14 @@
|
|||
position: absolute;
|
||||
pointer-events: none;
|
||||
left: 50%;
|
||||
top: calc(100% + 4px);
|
||||
width: 100vw;
|
||||
max-width: 150px;
|
||||
bottom: calc(100% + 4px);
|
||||
transform: translateX(-50%);
|
||||
text-align: center;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.spectrum-Icon--sizeXS {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
width: var(--spectrum-global-dimension-size-150);
|
||||
height: var(--spectrum-global-dimension-size-150);
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -90,6 +90,6 @@
|
|||
.spectrum-Popover {
|
||||
min-width: var(--spectrum-global-dimension-size-2000);
|
||||
border-color: var(--spectrum-global-color-gray-300);
|
||||
overflow: visible;
|
||||
overflow: auto;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -0,0 +1,157 @@
|
|||
<script context="module">
|
||||
export const TooltipPosition = {
|
||||
Top: "top",
|
||||
Right: "right",
|
||||
Bottom: "bottom",
|
||||
Left: "left",
|
||||
}
|
||||
export const TooltipType = {
|
||||
Default: "default",
|
||||
Info: "info",
|
||||
Positive: "positive",
|
||||
Negative: "negative",
|
||||
}
|
||||
</script>
|
||||
|
||||
<script>
|
||||
import Portal from "svelte-portal"
|
||||
import { fade } from "svelte/transition"
|
||||
import "@spectrum-css/tooltip/dist/index-vars.css"
|
||||
import { onDestroy } from "svelte"
|
||||
|
||||
export let position = TooltipPosition.Top
|
||||
export let type = TooltipType.Default
|
||||
export let text = ""
|
||||
export let fixed = false
|
||||
export let color = null
|
||||
|
||||
let wrapper
|
||||
let hovered = false
|
||||
let left
|
||||
let top
|
||||
let visible = false
|
||||
let timeout
|
||||
let interval
|
||||
|
||||
$: {
|
||||
if (hovered || fixed) {
|
||||
// Debounce showing by 200ms to avoid flashing tooltip
|
||||
timeout = setTimeout(show, 200)
|
||||
} else {
|
||||
hide()
|
||||
}
|
||||
}
|
||||
$: tooltipStyle = color ? `background:${color};` : null
|
||||
$: tipStyle = color ? `border-top-color:${color};` : null
|
||||
|
||||
// Computes the position of the tooltip
|
||||
const updateTooltipPosition = () => {
|
||||
const node = wrapper?.children?.[0]
|
||||
if (!node) {
|
||||
left = null
|
||||
top = null
|
||||
return
|
||||
}
|
||||
const bounds = node.getBoundingClientRect()
|
||||
|
||||
// Determine where to render tooltip based on position prop
|
||||
if (position === TooltipPosition.Top) {
|
||||
left = bounds.left + bounds.width / 2
|
||||
top = bounds.top
|
||||
} else if (position === TooltipPosition.Right) {
|
||||
left = bounds.left + bounds.width
|
||||
top = bounds.top + bounds.height / 2
|
||||
} else if (position === TooltipPosition.Bottom) {
|
||||
left = bounds.left + bounds.width / 2
|
||||
top = bounds.top + bounds.height
|
||||
} else if (position === TooltipPosition.Left) {
|
||||
left = bounds.left
|
||||
top = bounds.top + bounds.height / 2
|
||||
}
|
||||
}
|
||||
|
||||
// Computes the position of the tooltip then shows it.
|
||||
// We set up a poll to frequently update the position of the tooltip in case
|
||||
// the target moves.
|
||||
const show = () => {
|
||||
updateTooltipPosition()
|
||||
interval = setInterval(updateTooltipPosition, 100)
|
||||
visible = true
|
||||
}
|
||||
|
||||
// Hides the tooltip
|
||||
const hide = () => {
|
||||
clearTimeout(timeout)
|
||||
clearInterval(interval)
|
||||
visible = false
|
||||
}
|
||||
|
||||
// Ensure we clean up interval and timeout
|
||||
onDestroy(hide)
|
||||
</script>
|
||||
|
||||
<div
|
||||
bind:this={wrapper}
|
||||
class="abs-tooltip"
|
||||
on:focus={null}
|
||||
on:mouseover={() => (hovered = true)}
|
||||
on:mouseleave={() => (hovered = false)}
|
||||
>
|
||||
<slot />
|
||||
</div>
|
||||
|
||||
{#if visible && text && left != null && top != null}
|
||||
<Portal target=".spectrum">
|
||||
<span
|
||||
class="spectrum-Tooltip spectrum-Tooltip--{type} spectrum-Tooltip--{position} is-open"
|
||||
style={`left:${left}px;top:${top}px;${tooltipStyle}`}
|
||||
transition:fade|local={{ duration: 130 }}
|
||||
>
|
||||
<span class="spectrum-Tooltip-label">{text}</span>
|
||||
<span class="spectrum-Tooltip-tip" style={tipStyle} />
|
||||
</span>
|
||||
</Portal>
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
.abs-tooltip {
|
||||
display: contents;
|
||||
}
|
||||
.spectrum-Tooltip {
|
||||
position: absolute;
|
||||
z-index: 9999;
|
||||
pointer-events: none;
|
||||
margin: 0;
|
||||
max-width: 280px;
|
||||
transition: top 130ms ease-out, left 130ms ease-out;
|
||||
}
|
||||
.spectrum-Tooltip-label {
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* Colour overrides for default type */
|
||||
.spectrum-Tooltip--default {
|
||||
background: var(--spectrum-global-color-gray-500);
|
||||
}
|
||||
.spectrum-Tooltip--default .spectrum-Tooltip-tip {
|
||||
border-top-color: var(--spectrum-global-color-gray-500);
|
||||
}
|
||||
|
||||
/* Position styles */
|
||||
.spectrum-Tooltip--top {
|
||||
transform: translateX(-50%) translateY(calc(-100% - 8px));
|
||||
}
|
||||
.spectrum-Tooltip--right {
|
||||
transform: translateX(8px) translateY(-50%);
|
||||
}
|
||||
.spectrum-Tooltip--bottom {
|
||||
transform: translateX(-50%) translateY(8px);
|
||||
}
|
||||
.spectrum-Tooltip--left {
|
||||
transform: translateX(calc(-100% - 8px)) translateY(-50%);
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,39 @@
|
|||
<script>
|
||||
import AbsTooltip from "./AbsTooltip.svelte"
|
||||
import { onDestroy } from "svelte"
|
||||
|
||||
export let text = null
|
||||
export let condition = true
|
||||
export let duration = 3000
|
||||
export let position
|
||||
export let type
|
||||
|
||||
let visible = false
|
||||
let timeout
|
||||
|
||||
$: {
|
||||
if (condition) {
|
||||
showTooltip()
|
||||
} else {
|
||||
hideTooltip()
|
||||
}
|
||||
}
|
||||
|
||||
const showTooltip = () => {
|
||||
visible = true
|
||||
timeout = setTimeout(() => {
|
||||
visible = false
|
||||
}, duration)
|
||||
}
|
||||
|
||||
const hideTooltip = () => {
|
||||
visible = false
|
||||
clearTimeout(timeout)
|
||||
}
|
||||
|
||||
onDestroy(hideTooltip)
|
||||
</script>
|
||||
|
||||
<AbsTooltip {position} {type} text={visible ? text : null} fixed={visible}>
|
||||
<slot />
|
||||
</AbsTooltip>
|
|
@ -36,6 +36,12 @@ export { default as Layout } from "./Layout/Layout.svelte"
|
|||
export { default as Page } from "./Layout/Page.svelte"
|
||||
export { default as Link } from "./Link/Link.svelte"
|
||||
export { default as Tooltip } from "./Tooltip/Tooltip.svelte"
|
||||
export { default as TempTooltip } from "./Tooltip/TempTooltip.svelte"
|
||||
export {
|
||||
default as AbsTooltip,
|
||||
TooltipPosition,
|
||||
TooltipType,
|
||||
} from "./Tooltip/AbsTooltip.svelte"
|
||||
export { default as TooltipWrapper } from "./Tooltip/TooltipWrapper.svelte"
|
||||
export { default as Menu } from "./Menu/Menu.svelte"
|
||||
export { default as MenuSection } from "./Menu/Section.svelte"
|
||||
|
|
|
@ -127,13 +127,17 @@ export const selectedAutomation = derived(automationStore, $automationStore => {
|
|||
export const userSelectedResourceMap = derived(userStore, $userStore => {
|
||||
let map = {}
|
||||
$userStore.forEach(user => {
|
||||
if (user.builderMetadata?.selectedResourceId) {
|
||||
map[user.builderMetadata?.selectedResourceId] = user
|
||||
const resource = user.builderMetadata?.selectedResourceId
|
||||
if (resource) {
|
||||
if (!map[resource]) {
|
||||
map[resource] = []
|
||||
}
|
||||
map[resource].push(user)
|
||||
}
|
||||
})
|
||||
return map
|
||||
})
|
||||
|
||||
export const isOnlyUser = derived(userStore, $userStore => {
|
||||
return $userStore.length === 1
|
||||
return $userStore.length < 2
|
||||
})
|
||||
|
|
|
@ -248,4 +248,36 @@ const automationActions = store => ({
|
|||
}
|
||||
await store.actions.save(newAutomation)
|
||||
},
|
||||
replace: async (automationId, automation) => {
|
||||
if (!automation) {
|
||||
store.update(state => {
|
||||
// Remove the automation
|
||||
state.automations = state.automations.filter(
|
||||
x => x._id !== automationId
|
||||
)
|
||||
// Select a new automation if required
|
||||
if (automationId === state.selectedAutomationId) {
|
||||
store.actions.select(state.automations[0]?._id)
|
||||
}
|
||||
return state
|
||||
})
|
||||
} else {
|
||||
const index = get(store).automations.findIndex(
|
||||
x => x._id === automation._id
|
||||
)
|
||||
if (index === -1) {
|
||||
// Automation addition
|
||||
store.update(state => ({
|
||||
...state,
|
||||
automations: [...state.automations, automation],
|
||||
}))
|
||||
} else {
|
||||
// Automation update
|
||||
store.update(state => {
|
||||
state.automations[index] = automation
|
||||
return state
|
||||
})
|
||||
}
|
||||
}
|
||||
},
|
||||
})
|
||||
|
|
|
@ -4,6 +4,7 @@ import { getSchemaForDatasource } from "../../../dataBinding"
|
|||
const fieldTypeToComponentMap = {
|
||||
string: "stringfield",
|
||||
number: "numberfield",
|
||||
bigint: "bigintfield",
|
||||
options: "optionsfield",
|
||||
array: "multifieldselect",
|
||||
boolean: "booleanfield",
|
||||
|
|
|
@ -1,5 +1,10 @@
|
|||
import { createWebsocket } from "@budibase/frontend-core"
|
||||
import { userStore, store, deploymentStore } from "builderStore"
|
||||
import {
|
||||
userStore,
|
||||
store,
|
||||
deploymentStore,
|
||||
automationStore,
|
||||
} from "builderStore"
|
||||
import { datasources, tables } from "stores/backend"
|
||||
import { get } from "svelte/store"
|
||||
import { auth } from "stores/portal"
|
||||
|
@ -67,5 +72,10 @@ export const createBuilderWebsocket = appId => {
|
|||
}
|
||||
)
|
||||
|
||||
// Automations
|
||||
socket.onOther(BuilderSocketEvent.AutomationChange, ({ id, automation }) => {
|
||||
automationStore.actions.replace(id, automation)
|
||||
})
|
||||
|
||||
return socket
|
||||
}
|
||||
|
|
|
@ -1,6 +1,10 @@
|
|||
<script>
|
||||
import { onMount } from "svelte"
|
||||
import { automationStore, selectedAutomation } from "builderStore"
|
||||
import {
|
||||
automationStore,
|
||||
selectedAutomation,
|
||||
userSelectedResourceMap,
|
||||
} from "builderStore"
|
||||
import NavItem from "components/common/NavItem.svelte"
|
||||
import EditAutomationPopover from "./EditAutomationPopover.svelte"
|
||||
import { notifications } from "@budibase/bbui"
|
||||
|
@ -21,13 +25,13 @@
|
|||
</script>
|
||||
|
||||
<div class="automations-list">
|
||||
{#each $automationStore.automations.sort(aut => aut.name) as automation, idx}
|
||||
{#each $automationStore.automations.sort(aut => aut.name) as automation}
|
||||
<NavItem
|
||||
border={idx > 0}
|
||||
icon="ShareAndroid"
|
||||
text={automation.name}
|
||||
selected={automation._id === selectedAutomationId}
|
||||
on:click={() => selectAutomation(automation._id)}
|
||||
selectedBy={$userSelectedResourceMap[automation._id]}
|
||||
>
|
||||
<EditAutomationPopover {automation} />
|
||||
</NavItem>
|
||||
|
@ -40,6 +44,5 @@
|
|||
flex-direction: column;
|
||||
justify-content: flex-start;
|
||||
align-items: stretch;
|
||||
margin: 0 calc(-1 * var(--spacing-xl));
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -11,8 +11,8 @@
|
|||
<Panel title="Automations" borderRight>
|
||||
<Layout paddingX="L" paddingY="XL" gap="S">
|
||||
<Button cta on:click={modal.show}>Add automation</Button>
|
||||
<AutomationList />
|
||||
</Layout>
|
||||
<AutomationList />
|
||||
</Panel>
|
||||
|
||||
<Modal bind:this={modal}>
|
||||
|
|
|
@ -15,6 +15,7 @@
|
|||
Icon,
|
||||
Checkbox,
|
||||
DatePicker,
|
||||
Detail,
|
||||
} from "@budibase/bbui"
|
||||
import CreateWebhookModal from "components/automation/Shared/CreateWebhookModal.svelte"
|
||||
import { automationStore, selectedAutomation } from "builderStore"
|
||||
|
@ -32,7 +33,7 @@
|
|||
import CodeEditor from "components/common/CodeEditor/CodeEditor.svelte"
|
||||
import {
|
||||
bindingsToCompletions,
|
||||
jsAutocomplete,
|
||||
hbAutocomplete,
|
||||
EditorModes,
|
||||
} from "components/common/CodeEditor"
|
||||
import FilterDrawer from "components/design/settings/controls/FilterEditor/FilterDrawer.svelte"
|
||||
|
@ -55,6 +56,7 @@
|
|||
let drawer
|
||||
let fillWidth = true
|
||||
let inputData
|
||||
let codeBindingOpen = false
|
||||
|
||||
$: filters = lookForFilters(schemaProperties) || []
|
||||
$: tempFilters = filters
|
||||
|
@ -70,6 +72,13 @@
|
|||
$: queryLimit = tableId?.includes("datasource") ? "∞" : "1000"
|
||||
$: isTrigger = block?.type === "TRIGGER"
|
||||
$: isUpdateRow = stepId === ActionStepID.UPDATE_ROW
|
||||
$: codeMode =
|
||||
stepId === "EXECUTE_BASH" ? EditorModes.Handlebars : EditorModes.JS
|
||||
|
||||
$: stepCompletions =
|
||||
codeMode === EditorModes.Handlebars
|
||||
? [hbAutocomplete([...bindingsToCompletions(bindings, codeMode)])]
|
||||
: []
|
||||
|
||||
/**
|
||||
* TODO - Remove after November 2023
|
||||
|
@ -489,6 +498,18 @@
|
|||
/>
|
||||
{:else if value.customType === "code"}
|
||||
<CodeEditorModal>
|
||||
{#if codeMode == EditorModes.JS}
|
||||
<ActionButton
|
||||
on:click={() => (codeBindingOpen = !codeBindingOpen)}
|
||||
quiet
|
||||
icon={codeBindingOpen ? "ChevronDown" : "ChevronRight"}
|
||||
>
|
||||
<Detail size="S">Bindings</Detail>
|
||||
</ActionButton>
|
||||
{#if codeBindingOpen}
|
||||
<pre>{JSON.stringify(bindings, null, 2)}</pre>
|
||||
{/if}
|
||||
{/if}
|
||||
<CodeEditor
|
||||
value={inputData[key]}
|
||||
on:change={e => {
|
||||
|
@ -496,19 +517,22 @@
|
|||
onChange({ detail: e.detail }, key)
|
||||
inputData[key] = e.detail
|
||||
}}
|
||||
completions={[
|
||||
jsAutocomplete([
|
||||
...bindingsToCompletions(bindings, EditorModes.JS),
|
||||
]),
|
||||
]}
|
||||
mode={EditorModes.JS}
|
||||
completions={stepCompletions}
|
||||
mode={codeMode}
|
||||
autocompleteEnabled={codeMode != EditorModes.JS}
|
||||
height={500}
|
||||
/>
|
||||
<div class="messaging">
|
||||
<Icon name="FlashOn" />
|
||||
<div class="messaging-wrap">
|
||||
<div>Add available bindings by typing <strong>$</strong></div>
|
||||
</div>
|
||||
{#if codeMode == EditorModes.Handlebars}
|
||||
<Icon name="FlashOn" />
|
||||
<div class="messaging-wrap">
|
||||
<div>
|
||||
Add available bindings by typing <strong>
|
||||
}}
|
||||
</strong>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</CodeEditorModal>
|
||||
{:else if value.customType === "loopOption"}
|
||||
|
|
|
@ -109,6 +109,7 @@
|
|||
{disableSorting}
|
||||
{customPlaceholder}
|
||||
allowEditRows={allowEditing}
|
||||
allowEditColumns={allowEditing}
|
||||
showAutoColumns={!hideAutocolumns}
|
||||
{allowClickRows}
|
||||
on:clickrelationship={e => selectRelationship(e.detail)}
|
||||
|
|
|
@ -17,7 +17,7 @@
|
|||
$: text = getText(filters)
|
||||
|
||||
const getText = filters => {
|
||||
const count = filters?.length
|
||||
const count = filters?.filter(filter => filter.field)?.length
|
||||
return count ? `Filter (${count})` : "Filter"
|
||||
}
|
||||
</script>
|
||||
|
|
|
@ -18,7 +18,7 @@
|
|||
import { TableNames, UNEDITABLE_USER_FIELDS } from "constants"
|
||||
import {
|
||||
FIELDS,
|
||||
RelationshipTypes,
|
||||
RelationshipType,
|
||||
ALLOWABLE_STRING_OPTIONS,
|
||||
ALLOWABLE_NUMBER_OPTIONS,
|
||||
ALLOWABLE_STRING_TYPES,
|
||||
|
@ -33,6 +33,7 @@
|
|||
import { getBindings } from "components/backend/DataTable/formula"
|
||||
import { getContext } from "svelte"
|
||||
import JSONSchemaModal from "./JSONSchemaModal.svelte"
|
||||
import { ValidColumnNameRegex } from "@budibase/shared-core"
|
||||
|
||||
const AUTO_TYPE = "auto"
|
||||
const FORMULA_TYPE = FIELDS.FORMULA.type
|
||||
|
@ -183,7 +184,7 @@
|
|||
dispatch("updatecolumns")
|
||||
if (
|
||||
saveColumn.type === LINK_TYPE &&
|
||||
saveColumn.relationshipType === RelationshipTypes.MANY_TO_MANY
|
||||
saveColumn.relationshipType === RelationshipType.MANY_TO_MANY
|
||||
) {
|
||||
// Fetching the new tables
|
||||
tables.fetch()
|
||||
|
@ -237,7 +238,7 @@
|
|||
|
||||
// Default relationships many to many
|
||||
if (editableColumn.type === LINK_TYPE) {
|
||||
editableColumn.relationshipType = RelationshipTypes.MANY_TO_MANY
|
||||
editableColumn.relationshipType = RelationshipType.MANY_TO_MANY
|
||||
}
|
||||
if (editableColumn.type === FORMULA_TYPE) {
|
||||
editableColumn.formulaType = "dynamic"
|
||||
|
@ -285,17 +286,17 @@
|
|||
{
|
||||
name: `Many ${thisName} rows → many ${linkName} rows`,
|
||||
alt: `Many ${table.name} rows → many ${linkTable.name} rows`,
|
||||
value: RelationshipTypes.MANY_TO_MANY,
|
||||
value: RelationshipType.MANY_TO_MANY,
|
||||
},
|
||||
{
|
||||
name: `One ${linkName} row → many ${thisName} rows`,
|
||||
alt: `One ${linkTable.name} rows → many ${table.name} rows`,
|
||||
value: RelationshipTypes.ONE_TO_MANY,
|
||||
value: RelationshipType.ONE_TO_MANY,
|
||||
},
|
||||
{
|
||||
name: `One ${thisName} row → many ${linkName} rows`,
|
||||
alt: `One ${table.name} rows → many ${linkTable.name} rows`,
|
||||
value: RelationshipTypes.MANY_TO_ONE,
|
||||
value: RelationshipType.MANY_TO_ONE,
|
||||
},
|
||||
]
|
||||
}
|
||||
|
@ -326,6 +327,7 @@
|
|||
FIELDS.NUMBER,
|
||||
FIELDS.BOOLEAN,
|
||||
FIELDS.FORMULA,
|
||||
FIELDS.BIGINT,
|
||||
]
|
||||
// no-sql or a spreadsheet
|
||||
if (!external || table.sql) {
|
||||
|
@ -374,7 +376,7 @@
|
|||
const newError = {}
|
||||
if (!external && fieldInfo.name?.startsWith("_")) {
|
||||
newError.name = `Column name cannot start with an underscore.`
|
||||
} else if (fieldInfo.name && !fieldInfo.name.match(/^[_a-zA-Z0-9\s]*$/g)) {
|
||||
} else if (fieldInfo.name && !fieldInfo.name.match(ValidColumnNameRegex)) {
|
||||
newError.name = `Illegal character; must be alpha-numeric.`
|
||||
} else if (PROHIBITED_COLUMN_NAMES.some(name => fieldInfo.name === name)) {
|
||||
newError.name = `${PROHIBITED_COLUMN_NAMES.join(
|
||||
|
|
|
@ -95,9 +95,9 @@
|
|||
{#if !creating}
|
||||
<div>
|
||||
A user's email, role, first and last names cannot be changed from within
|
||||
the app builder. Please go to the <Link
|
||||
on:click={$goto("/builder/portal/manage/users")}>user portal</Link
|
||||
> to do this.
|
||||
the app builder. Please go to the
|
||||
<Link on:click={$goto("/builder/portal/users/users")}>user portal</Link>
|
||||
to do this.
|
||||
</div>
|
||||
{/if}
|
||||
<RowFieldControl
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<script>
|
||||
import { RelationshipTypes } from "constants/backend"
|
||||
import { RelationshipType } from "constants/backend"
|
||||
import {
|
||||
keepOpen,
|
||||
Button,
|
||||
|
@ -25,11 +25,11 @@
|
|||
const relationshipTypes = [
|
||||
{
|
||||
label: "One to Many",
|
||||
value: RelationshipTypes.MANY_TO_ONE,
|
||||
value: RelationshipType.MANY_TO_ONE,
|
||||
},
|
||||
{
|
||||
label: "Many to Many",
|
||||
value: RelationshipTypes.MANY_TO_MANY,
|
||||
value: RelationshipType.MANY_TO_MANY,
|
||||
},
|
||||
]
|
||||
|
||||
|
@ -58,8 +58,8 @@
|
|||
value: table._id,
|
||||
}))
|
||||
$: valid = getErrorCount(errors) === 0 && allRequiredAttributesSet()
|
||||
$: isManyToMany = relationshipType === RelationshipTypes.MANY_TO_MANY
|
||||
$: isManyToOne = relationshipType === RelationshipTypes.MANY_TO_ONE
|
||||
$: isManyToMany = relationshipType === RelationshipType.MANY_TO_MANY
|
||||
$: isManyToOne = relationshipType === RelationshipType.MANY_TO_ONE
|
||||
|
||||
function getTable(id) {
|
||||
return plusTables.find(table => table._id === id)
|
||||
|
@ -116,7 +116,7 @@
|
|||
|
||||
function allRequiredAttributesSet() {
|
||||
const base = getTable(fromId) && getTable(toId) && fromColumn && toColumn
|
||||
if (relationshipType === RelationshipTypes.MANY_TO_ONE) {
|
||||
if (relationshipType === RelationshipType.MANY_TO_ONE) {
|
||||
return base && fromPrimary && fromForeign
|
||||
} else {
|
||||
return base && getTable(throughId) && throughFromKey && throughToKey
|
||||
|
@ -181,12 +181,12 @@
|
|||
}
|
||||
|
||||
function otherRelationshipType(type) {
|
||||
if (type === RelationshipTypes.MANY_TO_ONE) {
|
||||
return RelationshipTypes.ONE_TO_MANY
|
||||
} else if (type === RelationshipTypes.ONE_TO_MANY) {
|
||||
return RelationshipTypes.MANY_TO_ONE
|
||||
} else if (type === RelationshipTypes.MANY_TO_MANY) {
|
||||
return RelationshipTypes.MANY_TO_MANY
|
||||
if (type === RelationshipType.MANY_TO_ONE) {
|
||||
return RelationshipType.ONE_TO_MANY
|
||||
} else if (type === RelationshipType.ONE_TO_MANY) {
|
||||
return RelationshipType.MANY_TO_ONE
|
||||
} else if (type === RelationshipType.MANY_TO_MANY) {
|
||||
return RelationshipType.MANY_TO_MANY
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -218,7 +218,7 @@
|
|||
|
||||
// if any to many only need to check from
|
||||
const manyToMany =
|
||||
relateFrom.relationshipType === RelationshipTypes.MANY_TO_MANY
|
||||
relateFrom.relationshipType === RelationshipType.MANY_TO_MANY
|
||||
|
||||
if (!manyToMany) {
|
||||
delete relateFrom.through
|
||||
|
@ -253,7 +253,7 @@
|
|||
}
|
||||
relateTo = {
|
||||
...relateTo,
|
||||
relationshipType: RelationshipTypes.ONE_TO_MANY,
|
||||
relationshipType: RelationshipType.ONE_TO_MANY,
|
||||
foreignKey: relateFrom.fieldName,
|
||||
fieldName: fromPrimary,
|
||||
}
|
||||
|
@ -321,7 +321,7 @@
|
|||
fromColumn = toRelationship.name
|
||||
}
|
||||
relationshipType =
|
||||
fromRelationship.relationshipType || RelationshipTypes.MANY_TO_ONE
|
||||
fromRelationship.relationshipType || RelationshipType.MANY_TO_ONE
|
||||
if (selectedFromTable) {
|
||||
fromId = selectedFromTable._id
|
||||
fromColumn = selectedFromTable.name
|
||||
|
|
|
@ -10,9 +10,8 @@ export const createTableSelectionStore = (integration, datasource) => {
|
|||
|
||||
datasources.getTableNames(datasource).then(tableNames => {
|
||||
tableNamesStore.set(tableNames)
|
||||
|
||||
selectedTableNamesStore.set(
|
||||
tableNames.filter(tableName => datasource.entities[tableName])
|
||||
tableNames.filter(tableName => datasource.entities?.[tableName])
|
||||
)
|
||||
|
||||
loadingStore.set(false)
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { RelationshipTypes } from "constants/backend"
|
||||
import { RelationshipType } from "constants/backend"
|
||||
|
||||
const typeMismatch = "Column type of the foreign key must match the primary key"
|
||||
const columnBeingUsed = "Column name cannot be an existing column"
|
||||
|
@ -40,7 +40,7 @@ export class RelationshipErrorChecker {
|
|||
}
|
||||
|
||||
isMany() {
|
||||
return this.type === RelationshipTypes.MANY_TO_MANY
|
||||
return this.type === RelationshipType.MANY_TO_MANY
|
||||
}
|
||||
|
||||
relationshipTypeSet(type) {
|
||||
|
|
|
@ -1,17 +1,9 @@
|
|||
<script>
|
||||
import { Select } from "@budibase/bbui"
|
||||
import { Select, Icon } from "@budibase/bbui"
|
||||
import { FIELDS } from "constants/backend"
|
||||
import { API } from "api"
|
||||
import { parseFile } from "./utils"
|
||||
|
||||
let fileInput
|
||||
let error = null
|
||||
let fileName = null
|
||||
|
||||
let loading = false
|
||||
let validation = {}
|
||||
let validateHash = ""
|
||||
|
||||
export let rows = []
|
||||
export let schema = {}
|
||||
export let allValid = true
|
||||
|
@ -49,6 +41,28 @@
|
|||
},
|
||||
]
|
||||
|
||||
let fileInput
|
||||
let error = null
|
||||
let fileName = null
|
||||
let fileType = null
|
||||
let loading = false
|
||||
let validation = {}
|
||||
let validateHash = ""
|
||||
let errors = {}
|
||||
|
||||
$: displayColumnOptions = Object.keys(schema || {}).filter(column => {
|
||||
return validation[column]
|
||||
})
|
||||
$: {
|
||||
// binding in consumer is causing double renders here
|
||||
const newValidateHash = JSON.stringify(rows) + JSON.stringify(schema)
|
||||
if (newValidateHash !== validateHash) {
|
||||
validate(rows, schema)
|
||||
}
|
||||
validateHash = newValidateHash
|
||||
}
|
||||
$: openFileUpload(promptUpload, fileInput)
|
||||
|
||||
async function handleFile(e) {
|
||||
loading = true
|
||||
error = null
|
||||
|
@ -67,34 +81,23 @@
|
|||
|
||||
async function validate(rows, schema) {
|
||||
loading = true
|
||||
error = null
|
||||
validation = {}
|
||||
allValid = false
|
||||
|
||||
try {
|
||||
if (rows.length > 0) {
|
||||
const response = await API.validateNewTableImport({ rows, schema })
|
||||
validation = response.schemaValidation
|
||||
allValid = response.allValid
|
||||
errors = response.errors
|
||||
error = null
|
||||
}
|
||||
} catch (e) {
|
||||
error = e.message
|
||||
validation = {}
|
||||
allValid = false
|
||||
errors = {}
|
||||
}
|
||||
|
||||
loading = false
|
||||
}
|
||||
|
||||
$: {
|
||||
// binding in consumer is causing double renders here
|
||||
const newValidateHash = JSON.stringify(rows) + JSON.stringify(schema)
|
||||
|
||||
if (newValidateHash !== validateHash) {
|
||||
validate(rows, schema)
|
||||
}
|
||||
|
||||
validateHash = newValidateHash
|
||||
}
|
||||
|
||||
const handleChange = (name, e) => {
|
||||
schema[name].type = e.detail
|
||||
schema[name].constraints = FIELDS[e.detail.toUpperCase()].constraints
|
||||
|
@ -106,7 +109,13 @@
|
|||
}
|
||||
}
|
||||
|
||||
$: openFileUpload(promptUpload, fileInput)
|
||||
const deleteColumn = name => {
|
||||
if (loading) {
|
||||
return
|
||||
}
|
||||
delete schema[name]
|
||||
schema = schema
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="dropzone">
|
||||
|
@ -119,10 +128,8 @@
|
|||
on:change={handleFile}
|
||||
/>
|
||||
<label for="file-upload" class:uploaded={rows.length > 0}>
|
||||
{#if loading}
|
||||
loading...
|
||||
{:else if error}
|
||||
error: {error}
|
||||
{#if error}
|
||||
Error: {error}
|
||||
{:else if fileName}
|
||||
{fileName}
|
||||
{:else}
|
||||
|
@ -142,23 +149,26 @@
|
|||
placeholder={null}
|
||||
getOptionLabel={option => option.label}
|
||||
getOptionValue={option => option.value}
|
||||
disabled={loading}
|
||||
/>
|
||||
<span
|
||||
class={loading || validation[column.name]
|
||||
class={validation[column.name]
|
||||
? "fieldStatusSuccess"
|
||||
: "fieldStatusFailure"}
|
||||
>
|
||||
{validation[column.name] ? "Success" : "Failure"}
|
||||
{#if validation[column.name]}
|
||||
Success
|
||||
{:else}
|
||||
Failure
|
||||
{#if errors[column.name]}
|
||||
<Icon name="Help" tooltip={errors[column.name]} />
|
||||
{/if}
|
||||
{/if}
|
||||
</span>
|
||||
<i
|
||||
class={`omit-button ri-close-circle-fill ${
|
||||
loading ? "omit-button-disabled" : ""
|
||||
}`}
|
||||
on:click={() => {
|
||||
delete schema[column.name]
|
||||
schema = schema
|
||||
}}
|
||||
<Icon
|
||||
size="S"
|
||||
name="Close"
|
||||
hoverable
|
||||
on:click={() => deleteColumn(column.name)}
|
||||
/>
|
||||
</div>
|
||||
{/each}
|
||||
|
@ -167,7 +177,7 @@
|
|||
<Select
|
||||
label="Display Column"
|
||||
bind:value={displayColumn}
|
||||
options={Object.keys(schema)}
|
||||
options={displayColumnOptions}
|
||||
sort
|
||||
/>
|
||||
</div>
|
||||
|
@ -235,23 +245,16 @@
|
|||
justify-self: center;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.fieldStatusFailure {
|
||||
color: var(--red);
|
||||
justify-self: center;
|
||||
font-weight: 600;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.omit-button {
|
||||
font-size: 1.2em;
|
||||
color: var(--grey-7);
|
||||
cursor: pointer;
|
||||
justify-self: flex-end;
|
||||
}
|
||||
|
||||
.omit-button-disabled {
|
||||
pointer-events: none;
|
||||
opacity: 70%;
|
||||
.fieldStatusFailure :global(.spectrum-Icon) {
|
||||
width: 12px;
|
||||
}
|
||||
|
||||
.display-column {
|
||||
|
|
|
@ -2,21 +2,13 @@
|
|||
import { goto, url } from "@roxi/routify"
|
||||
import { tables } from "stores/backend"
|
||||
import { notifications } from "@budibase/bbui"
|
||||
import {
|
||||
Input,
|
||||
Label,
|
||||
ModalContent,
|
||||
Toggle,
|
||||
Divider,
|
||||
Layout,
|
||||
} from "@budibase/bbui"
|
||||
import { Input, Label, ModalContent, Layout } from "@budibase/bbui"
|
||||
import { datasources } from "stores/backend"
|
||||
import TableDataImport from "../TableDataImport.svelte"
|
||||
import {
|
||||
BUDIBASE_INTERNAL_DB_ID,
|
||||
BUDIBASE_DATASOURCE_TYPE,
|
||||
} from "constants/backend"
|
||||
import { buildAutoColumn, getAutoColumnInformation } from "builderStore/utils"
|
||||
|
||||
$: tableNames = $tables.list.map(table => table.name)
|
||||
$: selectedSource = $datasources.list.find(
|
||||
|
@ -43,28 +35,12 @@
|
|||
}
|
||||
|
||||
let error = ""
|
||||
let autoColumns = getAutoColumnInformation()
|
||||
|
||||
let schema = {}
|
||||
let rows = []
|
||||
let allValid = true
|
||||
let displayColumn = null
|
||||
|
||||
function getAutoColumns() {
|
||||
const selectedAutoColumns = {}
|
||||
|
||||
Object.entries(autoColumns).forEach(([subtype, column]) => {
|
||||
if (column.enabled) {
|
||||
selectedAutoColumns[column.name] = buildAutoColumn(
|
||||
name,
|
||||
column.name,
|
||||
subtype
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
return selectedAutoColumns
|
||||
}
|
||||
|
||||
function checkValid(evt) {
|
||||
const tableName = evt.target.value
|
||||
if (tableNames.includes(tableName)) {
|
||||
|
@ -77,7 +53,7 @@
|
|||
async function saveTable() {
|
||||
let newTable = {
|
||||
name,
|
||||
schema: { ...schema, ...getAutoColumns() },
|
||||
schema: { ...schema },
|
||||
rows,
|
||||
type: "internal",
|
||||
sourceId: targetDatasourceId,
|
||||
|
@ -118,21 +94,6 @@
|
|||
bind:value={name}
|
||||
{error}
|
||||
/>
|
||||
<div class="autocolumns">
|
||||
<Label extraSmall grey>Auto Columns</Label>
|
||||
<div class="toggles">
|
||||
<div class="toggle-1">
|
||||
<Toggle text="Created by" bind:value={autoColumns.createdBy.enabled} />
|
||||
<Toggle text="Created at" bind:value={autoColumns.createdAt.enabled} />
|
||||
<Toggle text="Auto ID" bind:value={autoColumns.autoID.enabled} />
|
||||
</div>
|
||||
<div class="toggle-2">
|
||||
<Toggle text="Updated by" bind:value={autoColumns.updatedBy.enabled} />
|
||||
<Toggle text="Updated at" bind:value={autoColumns.updatedAt.enabled} />
|
||||
</div>
|
||||
</div>
|
||||
<Divider />
|
||||
</div>
|
||||
<div>
|
||||
<Layout gap="XS" noPadding>
|
||||
<Label grey extraSmall
|
||||
|
@ -148,24 +109,3 @@
|
|||
</Layout>
|
||||
</div>
|
||||
</ModalContent>
|
||||
|
||||
<style>
|
||||
.autocolumns {
|
||||
margin-bottom: -10px;
|
||||
}
|
||||
|
||||
.toggles {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
margin-top: 6px;
|
||||
}
|
||||
|
||||
.toggle-1 :global(> *) {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.toggle-2 :global(> *) {
|
||||
margin-bottom: 10px;
|
||||
margin-left: 20px;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -35,9 +35,8 @@
|
|||
try {
|
||||
const isSelected =
|
||||
decodeURIComponent($params.viewName) === $views.selectedViewName
|
||||
const name = view.name
|
||||
const id = view.tableId
|
||||
await views.delete(name)
|
||||
await views.delete(view)
|
||||
notifications.success("View deleted")
|
||||
if (isSelected) {
|
||||
$goto(`./table/${id}`)
|
||||
|
|
|
@ -1,12 +1,15 @@
|
|||
<script>
|
||||
import { Label } from "@budibase/bbui"
|
||||
import { onMount, createEventDispatcher } from "svelte"
|
||||
import { FIND_ANY_HBS_REGEX } from "@budibase/string-templates"
|
||||
|
||||
import {
|
||||
autocompletion,
|
||||
closeBrackets,
|
||||
completionKeymap,
|
||||
closeBracketsKeymap,
|
||||
acceptCompletion,
|
||||
completionStatus,
|
||||
} from "@codemirror/autocomplete"
|
||||
import {
|
||||
EditorView,
|
||||
|
@ -34,7 +37,8 @@
|
|||
defaultKeymap,
|
||||
historyKeymap,
|
||||
history,
|
||||
indentWithTab,
|
||||
indentMore,
|
||||
indentLess,
|
||||
} from "@codemirror/commands"
|
||||
import { Compartment } from "@codemirror/state"
|
||||
import { javascript } from "@codemirror/lang-javascript"
|
||||
|
@ -48,6 +52,7 @@
|
|||
export let mode = EditorModes.Handlebars
|
||||
export let value = ""
|
||||
export let placeholder = null
|
||||
export let autocompleteEnabled = true
|
||||
|
||||
// Export a function to expose caret position
|
||||
export const getCaretPosition = () => {
|
||||
|
@ -77,7 +82,7 @@
|
|||
|
||||
// For handlebars only.
|
||||
const bindStyle = new MatchDecorator({
|
||||
regexp: /{{[."#\-\w\s\][]*}}/g,
|
||||
regexp: FIND_ANY_HBS_REGEX,
|
||||
decoration: () => {
|
||||
return Decoration.mark({
|
||||
tag: "span",
|
||||
|
@ -107,6 +112,22 @@
|
|||
let isDark = !currentTheme.includes("light")
|
||||
let themeConfig = new Compartment()
|
||||
|
||||
const indentWithTabCustom = {
|
||||
key: "Tab",
|
||||
run: view => {
|
||||
if (completionStatus(view.state) == "active") {
|
||||
acceptCompletion(view)
|
||||
return true
|
||||
}
|
||||
indentMore(view)
|
||||
return true
|
||||
},
|
||||
shift: view => {
|
||||
indentLess(view)
|
||||
return true
|
||||
},
|
||||
}
|
||||
|
||||
const buildKeymap = () => {
|
||||
const baseMap = [
|
||||
...closeBracketsKeymap,
|
||||
|
@ -114,7 +135,7 @@
|
|||
...historyKeymap,
|
||||
...foldKeymap,
|
||||
...completionKeymap,
|
||||
indentWithTab,
|
||||
indentWithTabCustom,
|
||||
]
|
||||
return baseMap
|
||||
}
|
||||
|
@ -131,12 +152,6 @@
|
|||
syntaxHighlighting(oneDarkHighlightStyle, { fallback: true }),
|
||||
highlightActiveLineGutter(),
|
||||
highlightSpecialChars(),
|
||||
autocompletion({
|
||||
override: [...completions],
|
||||
closeOnBlur: true,
|
||||
icons: false,
|
||||
optionClass: () => "autocomplete-option",
|
||||
}),
|
||||
EditorView.lineWrapping,
|
||||
EditorView.updateListener.of(v => {
|
||||
const docStr = v.state.doc?.toString()
|
||||
|
@ -159,11 +174,16 @@
|
|||
|
||||
const buildExtensions = base => {
|
||||
const complete = [...base]
|
||||
if (mode.name == "javascript") {
|
||||
complete.push(javascript())
|
||||
complete.push(highlightWhitespace())
|
||||
complete.push(lineNumbers())
|
||||
complete.push(foldGutter())
|
||||
|
||||
if (autocompleteEnabled) {
|
||||
complete.push(
|
||||
autocompletion({
|
||||
override: [...completions],
|
||||
closeOnBlur: true,
|
||||
icons: false,
|
||||
optionClass: () => "autocomplete-option",
|
||||
})
|
||||
)
|
||||
complete.push(
|
||||
EditorView.inputHandler.of((view, from, to, insert) => {
|
||||
if (insert === "$") {
|
||||
|
@ -193,6 +213,13 @@
|
|||
)
|
||||
}
|
||||
|
||||
if (mode.name == "javascript") {
|
||||
complete.push(javascript())
|
||||
complete.push(highlightWhitespace())
|
||||
complete.push(lineNumbers())
|
||||
complete.push(foldGutter())
|
||||
}
|
||||
|
||||
if (placeholder) {
|
||||
complete.push(placeholderFn(placeholder))
|
||||
}
|
||||
|
|
|
@ -0,0 +1,39 @@
|
|||
<script>
|
||||
import { Icon, Heading } from "@budibase/bbui"
|
||||
|
||||
export let showClose = false
|
||||
export let onClose = () => {}
|
||||
export let heading = ""
|
||||
</script>
|
||||
|
||||
<section class="page">
|
||||
<div class="closeButton">
|
||||
{#if showClose}
|
||||
<Icon hoverable name="Close" on:click={onClose} />
|
||||
{/if}
|
||||
</div>
|
||||
<div class="heading">
|
||||
<Heading weight="light">{heading}</Heading>
|
||||
</div>
|
||||
<slot />
|
||||
</section>
|
||||
|
||||
<style>
|
||||
.page {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.heading {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.closeButton {
|
||||
height: 38px;
|
||||
display: flex;
|
||||
justify-content: right;
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
|
@ -4,7 +4,8 @@
|
|||
import { licensing } from "stores/portal"
|
||||
import { isEnabled, TENANT_FEATURE_FLAGS } from "helpers/featureFlags"
|
||||
|
||||
$: isPremiumUser = $licensing.license && !$licensing.isFreePlan
|
||||
$: isBusinessAndAbove =
|
||||
$licensing.isBusinessPlan || $licensing.isEnterprisePlan
|
||||
|
||||
let show
|
||||
let hide
|
||||
|
@ -55,22 +56,22 @@
|
|||
<div class="divider" />
|
||||
{#if isEnabled(TENANT_FEATURE_FLAGS.LICENSING)}
|
||||
<a
|
||||
href={isPremiumUser
|
||||
href={isBusinessAndAbove
|
||||
? "mailto:support@budibase.com"
|
||||
: "/builder/portal/account/usage"}
|
||||
>
|
||||
<div class="premiumLinkContent" class:disabled={!isPremiumUser}>
|
||||
<div class="premiumLinkContent" class:disabled={!isBusinessAndAbove}>
|
||||
<div class="icon">
|
||||
<FontAwesomeIcon name="fa-solid fa-envelope" />
|
||||
</div>
|
||||
<Body size="S">Email support</Body>
|
||||
</div>
|
||||
{#if !isPremiumUser}
|
||||
{#if !isBusinessAndAbove}
|
||||
<div class="premiumBadge">
|
||||
<div class="icon">
|
||||
<FontAwesomeIcon name="fa-solid fa-lock" />
|
||||
</div>
|
||||
<Body size="XS">Premium</Body>
|
||||
<Body size="XS">Business</Body>
|
||||
</div>
|
||||
{/if}
|
||||
</a>
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
import { Icon } from "@budibase/bbui"
|
||||
import { createEventDispatcher, getContext } from "svelte"
|
||||
import { helpers } from "@budibase/shared-core"
|
||||
import { UserAvatars } from "@budibase/frontend-core"
|
||||
|
||||
export let icon
|
||||
export let withArrow = false
|
||||
|
@ -98,21 +99,25 @@
|
|||
<Icon color={iconColor} size="S" name={icon} />
|
||||
</div>
|
||||
{/if}
|
||||
<div class="text" title={showTooltip ? text : null}>{text}</div>
|
||||
<div class="text" title={showTooltip ? text : null}>
|
||||
{text}
|
||||
{#if selectedBy}
|
||||
<UserAvatars size="XS" users={selectedBy} />
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{#if withActions}
|
||||
<div class="actions">
|
||||
<slot />
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if $$slots.right}
|
||||
<div class="right">
|
||||
<slot name="right" />
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{#if selectedBy}
|
||||
<div class="selected-by-label">{helpers.getUserLabel(selectedBy)}</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
|
@ -127,7 +132,6 @@
|
|||
flex-direction: row;
|
||||
justify-content: flex-start;
|
||||
align-items: stretch;
|
||||
position: relative;
|
||||
}
|
||||
.nav-item.scrollable {
|
||||
flex-direction: column;
|
||||
|
@ -136,13 +140,16 @@
|
|||
}
|
||||
.nav-item.highlighted {
|
||||
background-color: var(--spectrum-global-color-gray-200);
|
||||
--avatars-background: var(--spectrum-global-color-gray-200);
|
||||
}
|
||||
.nav-item.selected {
|
||||
background-color: var(--spectrum-global-color-gray-300);
|
||||
--avatars-background: var(--spectrum-global-color-gray-300);
|
||||
color: var(--ink);
|
||||
}
|
||||
.nav-item:hover {
|
||||
background-color: var(--spectrum-global-color-gray-300);
|
||||
--avatars-background: var(--spectrum-global-color-gray-300);
|
||||
}
|
||||
.nav-item:hover .actions {
|
||||
visibility: visible;
|
||||
|
@ -159,37 +166,6 @@
|
|||
padding-left: var(--spacing-l);
|
||||
}
|
||||
|
||||
/* Selected user styles */
|
||||
.nav-item.selectedBy:after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
width: calc(100% - 4px);
|
||||
height: 28px;
|
||||
border: 2px solid var(--selected-by-color);
|
||||
left: 0;
|
||||
top: 0;
|
||||
border-radius: 2px;
|
||||
pointer-events: none;
|
||||
}
|
||||
.selected-by-label {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
background: var(--selected-by-color);
|
||||
padding: 2px 4px;
|
||||
font-size: 12px;
|
||||
color: white;
|
||||
transform: translateY(calc(1px - 100%));
|
||||
border-top-right-radius: 2px;
|
||||
border-top-left-radius: 2px;
|
||||
pointer-events: none;
|
||||
opacity: 0;
|
||||
transition: opacity 130ms ease-out;
|
||||
}
|
||||
.nav-item.selectedBy:hover .selected-by-label {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
/* Needed to fully display the actions icon */
|
||||
.nav-item.scrollable .nav-item-content {
|
||||
padding-right: 1px;
|
||||
|
@ -245,6 +221,9 @@
|
|||
color: var(--spectrum-global-color-gray-900);
|
||||
order: 2;
|
||||
width: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
.scrollable .text {
|
||||
flex: 0 0 auto;
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
<script>
|
||||
import { Icon } from "@budibase/bbui"
|
||||
import { onMount } from "svelte"
|
||||
import { isBuilderInputFocused } from "helpers"
|
||||
|
||||
export let store
|
||||
|
||||
|
@ -8,9 +9,16 @@
|
|||
if (!(e.ctrlKey || e.metaKey)) {
|
||||
return
|
||||
}
|
||||
if (e.shiftKey && e.key === "Z") {
|
||||
|
||||
let keyLowerCase = e.key.toLowerCase()
|
||||
|
||||
// Ignore events when typing
|
||||
if (isBuilderInputFocused(e)) {
|
||||
return
|
||||
}
|
||||
if (e.shiftKey && keyLowerCase === "z") {
|
||||
store.redo()
|
||||
} else if (e.key === "z") {
|
||||
} else if (keyLowerCase === "z") {
|
||||
store.undo()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -208,7 +208,9 @@
|
|||
<div class="syntax-error">
|
||||
Current Handlebars syntax is invalid, please check the
|
||||
guide
|
||||
<a href="https://handlebarsjs.com/guide/">here</a>
|
||||
<a href="https://handlebarsjs.com/guide/" target="_blank"
|
||||
>here</a
|
||||
>
|
||||
for more details.
|
||||
</div>
|
||||
{:else}
|
||||
|
@ -339,7 +341,7 @@
|
|||
</Tab>
|
||||
{/if}
|
||||
<div class="drawer-actions">
|
||||
{#if drawerActions?.hide}
|
||||
{#if typeof drawerActions.hide === "function" && drawerActions.headless}
|
||||
<Button
|
||||
secondary
|
||||
quiet
|
||||
|
@ -350,7 +352,7 @@
|
|||
Cancel
|
||||
</Button>
|
||||
{/if}
|
||||
{#if bindingDrawerActions?.save}
|
||||
{#if typeof bindingDrawerActions?.save === "function" && drawerActions.headless}
|
||||
<Button
|
||||
cta
|
||||
disabled={!valid}
|
||||
|
|
|
@ -10,6 +10,7 @@
|
|||
Link,
|
||||
Modal,
|
||||
StatusLight,
|
||||
AbsTooltip,
|
||||
} from "@budibase/bbui"
|
||||
import RevertModal from "components/deploy/RevertModal.svelte"
|
||||
import VersionModal from "components/deploy/VersionModal.svelte"
|
||||
|
@ -250,15 +251,20 @@
|
|||
<Link quiet on:click={unpublishApp}>Unpublish</Link>
|
||||
</span>
|
||||
<span class="revert-link">
|
||||
<Link
|
||||
disabled={!$isOnlyUser}
|
||||
quiet
|
||||
secondary
|
||||
on:click={revertApp}
|
||||
tooltip="Unavailable - another user is editing this app"
|
||||
<AbsTooltip
|
||||
text={$isOnlyUser
|
||||
? null
|
||||
: "Unavailable - another user is editing this app"}
|
||||
>
|
||||
Revert
|
||||
</Link>
|
||||
<Link
|
||||
disabled={!$isOnlyUser}
|
||||
quiet
|
||||
secondary
|
||||
on:click={revertApp}
|
||||
>
|
||||
Revert
|
||||
</Link>
|
||||
</AbsTooltip>
|
||||
</span>
|
||||
{:else}
|
||||
<span class="status-text unpublished">Not published</span>
|
||||
|
|
|
@ -52,6 +52,7 @@ const componentMap = {
|
|||
"field/sortable": SortableFieldSelect,
|
||||
"field/string": FormFieldSelect,
|
||||
"field/number": FormFieldSelect,
|
||||
"field/bigint": FormFieldSelect,
|
||||
"field/options": FormFieldSelect,
|
||||
"field/boolean": FormFieldSelect,
|
||||
"field/longform": FormFieldSelect,
|
||||
|
|
|
@ -16,6 +16,7 @@
|
|||
makeStateBinding,
|
||||
} from "builderStore/dataBinding"
|
||||
import { currentAsset, store } from "builderStore"
|
||||
import { cloneDeep } from "lodash/fp"
|
||||
|
||||
const flipDurationMs = 150
|
||||
const EVENT_TYPE_KEY = "##eventHandlerType"
|
||||
|
@ -29,6 +30,26 @@
|
|||
let actionQuery
|
||||
let selectedAction = actions?.length ? actions[0] : null
|
||||
|
||||
const setUpdateActions = actions => {
|
||||
return actions
|
||||
? cloneDeep(actions)
|
||||
.filter(action => {
|
||||
return (
|
||||
action[EVENT_TYPE_KEY] === "Update State" &&
|
||||
action.parameters?.type === "set" &&
|
||||
action.parameters.key
|
||||
)
|
||||
})
|
||||
.reduce((acc, action) => {
|
||||
acc[action.id] = action
|
||||
return acc
|
||||
}, {})
|
||||
: []
|
||||
}
|
||||
|
||||
// Snapshot original action state
|
||||
let updateStateActions = setUpdateActions(actions)
|
||||
|
||||
$: {
|
||||
// Ensure parameters object is never null
|
||||
if (selectedAction && !selectedAction.parameters) {
|
||||
|
@ -125,8 +146,9 @@
|
|||
actions = e.detail.items
|
||||
}
|
||||
|
||||
const getAllBindings = (bindings, eventContextBindings, actions) => {
|
||||
const getAllBindings = (actionBindings, eventContextBindings, actions) => {
|
||||
let allBindings = []
|
||||
let cloneActionBindings = cloneDeep(actionBindings)
|
||||
if (!actions) {
|
||||
return []
|
||||
}
|
||||
|
@ -144,11 +166,19 @@
|
|||
.forEach(action => {
|
||||
// Check we have a binding for this action, and generate one if not
|
||||
const stateBinding = makeStateBinding(action.parameters.key)
|
||||
const hasKey = bindings.some(binding => {
|
||||
const hasKey = actionBindings.some(binding => {
|
||||
return binding.runtimeBinding === stateBinding.runtimeBinding
|
||||
})
|
||||
if (!hasKey) {
|
||||
bindings.push(stateBinding)
|
||||
let existing = updateStateActions[action.id]
|
||||
if (existing) {
|
||||
const existingBinding = makeStateBinding(existing.parameters.key)
|
||||
cloneActionBindings = cloneActionBindings.filter(
|
||||
binding =>
|
||||
binding.runtimeBinding !== existingBinding.runtimeBinding
|
||||
)
|
||||
}
|
||||
allBindings.push(stateBinding)
|
||||
}
|
||||
})
|
||||
// Get which indexes are asynchronous automations as we want to filter them out from the bindings
|
||||
|
@ -164,15 +194,16 @@
|
|||
.filter(index => index !== undefined)
|
||||
|
||||
// Based on the above, filter out the asynchronous automations from the bindings
|
||||
if (asynchronousAutomationIndexes) {
|
||||
allBindings = eventContextBindings
|
||||
.filter((binding, index) => {
|
||||
let contextBindings = asynchronousAutomationIndexes
|
||||
? eventContextBindings.filter((binding, index) => {
|
||||
return !asynchronousAutomationIndexes.includes(index)
|
||||
})
|
||||
.concat(bindings)
|
||||
} else {
|
||||
allBindings = eventContextBindings.concat(bindings)
|
||||
}
|
||||
: eventContextBindings
|
||||
|
||||
allBindings = contextBindings
|
||||
.concat(cloneActionBindings)
|
||||
.concat(allBindings)
|
||||
|
||||
return allBindings
|
||||
}
|
||||
</script>
|
||||
|
|
|
@ -70,8 +70,9 @@
|
|||
} set`
|
||||
</script>
|
||||
|
||||
<div class="action-count">{actionText}</div>
|
||||
<ActionButton on:click={openDrawer}>Define actions</ActionButton>
|
||||
<div class="action-editor">
|
||||
<ActionButton on:click={openDrawer}>{actionText}</ActionButton>
|
||||
</div>
|
||||
|
||||
<Drawer bind:this={drawer} title={"Actions"}>
|
||||
<svelte:fragment slot="description">
|
||||
|
@ -89,9 +90,7 @@
|
|||
</Drawer>
|
||||
|
||||
<style>
|
||||
.action-count {
|
||||
padding-top: 6px;
|
||||
padding-bottom: var(--spacing-s);
|
||||
font-weight: 600;
|
||||
.action-editor :global(.spectrum-ActionButton) {
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -20,6 +20,7 @@
|
|||
let drawer
|
||||
let boundValue
|
||||
|
||||
$: text = getText(value)
|
||||
$: datasource = getDatasourceForProvider($currentAsset, componentInstance)
|
||||
$: schema = getSchema($currentAsset, datasource)
|
||||
$: options = allowCellEditing
|
||||
|
@ -31,6 +32,17 @@
|
|||
allowLinks: true,
|
||||
})
|
||||
|
||||
const getText = value => {
|
||||
if (!value?.length) {
|
||||
return "All columns"
|
||||
}
|
||||
let text = `${value.length} column`
|
||||
if (value.length !== 1) {
|
||||
text += "s"
|
||||
}
|
||||
return text
|
||||
}
|
||||
|
||||
const getSchema = (asset, datasource) => {
|
||||
const schema = getSchemaForDatasource(asset, datasource).schema
|
||||
|
||||
|
@ -76,7 +88,7 @@
|
|||
</script>
|
||||
|
||||
<div class="column-editor">
|
||||
<ActionButton on:click={open}>Configure columns</ActionButton>
|
||||
<ActionButton on:click={open}>{text}</ActionButton>
|
||||
</div>
|
||||
<Drawer bind:this={drawer} title="Columns">
|
||||
<Button cta slot="buttons" on:click={save}>Save</Button>
|
||||
|
|
|
@ -12,25 +12,36 @@
|
|||
export let componentInstance
|
||||
export let value = []
|
||||
|
||||
const convertOldColumnFormat = oldColumns => {
|
||||
if (typeof oldColumns?.[0] === "string") {
|
||||
value = oldColumns.map(field => ({ name: field, displayName: field }))
|
||||
}
|
||||
}
|
||||
|
||||
$: convertOldColumnFormat(value)
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
let drawer
|
||||
let boundValue
|
||||
|
||||
$: text = getText(value)
|
||||
$: convertOldColumnFormat(value)
|
||||
$: datasource = getDatasourceForProvider($currentAsset, componentInstance)
|
||||
$: schema = getSchema($currentAsset, datasource)
|
||||
$: options = Object.keys(schema || {})
|
||||
$: sanitisedValue = getValidColumns(value, options)
|
||||
$: updateBoundValue(sanitisedValue)
|
||||
|
||||
const getText = value => {
|
||||
if (!value?.length) {
|
||||
return "All fields"
|
||||
}
|
||||
let text = `${value.length} field`
|
||||
if (value.length !== 1) {
|
||||
text += "s"
|
||||
}
|
||||
return text
|
||||
}
|
||||
|
||||
const convertOldColumnFormat = oldColumns => {
|
||||
if (typeof oldColumns?.[0] === "string") {
|
||||
value = oldColumns.map(field => ({ name: field, displayName: field }))
|
||||
}
|
||||
}
|
||||
|
||||
const getSchema = (asset, datasource) => {
|
||||
const schema = getSchemaForDatasource(asset, datasource).schema
|
||||
|
||||
|
@ -75,7 +86,10 @@
|
|||
}
|
||||
</script>
|
||||
|
||||
<ActionButton on:click={open}>Configure fields</ActionButton>
|
||||
<div class="field-configuration">
|
||||
<ActionButton on:click={open}>{text}</ActionButton>
|
||||
</div>
|
||||
|
||||
<Drawer bind:this={drawer} title="Form Fields">
|
||||
<svelte:fragment slot="description">
|
||||
Configure the fields in your form.
|
||||
|
@ -83,3 +97,9 @@
|
|||
<Button cta slot="buttons" on:click={save}>Save</Button>
|
||||
<ColumnDrawer slot="body" bind:columns={boundValue} {options} {schema} />
|
||||
</Drawer>
|
||||
|
||||
<style>
|
||||
.field-configuration :global(.spectrum-ActionButton) {
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -228,7 +228,7 @@
|
|||
on:change={event => (filter.value = event.detail)}
|
||||
{fillWidth}
|
||||
/>
|
||||
{:else if ["string", "longform", "number", "formula"].includes(filter.type)}
|
||||
{:else if ["string", "longform", "number", "bigint", "formula"].includes(filter.type)}
|
||||
<Input disabled={filter.noValue} bind:value={filter.value} />
|
||||
{:else if filter.type === "array" || (filter.type === "options" && filter.operator === "oneOf")}
|
||||
<Multiselect
|
||||
|
|
|
@ -20,7 +20,7 @@
|
|||
$: datasource = getDatasourceForProvider($currentAsset, componentInstance)
|
||||
$: schema = getSchemaForDatasource($currentAsset, datasource)?.schema
|
||||
$: schemaFields = Object.values(schema || {})
|
||||
$: text = getText(value)
|
||||
$: text = getText(value?.filter(filter => filter.field))
|
||||
|
||||
async function saveFilter() {
|
||||
dispatch("change", tempValue)
|
||||
|
|
|
@ -419,16 +419,22 @@
|
|||
if (query && !query.fields.pagination) {
|
||||
query.fields.pagination = {}
|
||||
}
|
||||
dynamicVariables = getDynamicVariables(
|
||||
datasource,
|
||||
query._id,
|
||||
(variable, queryId) => variable.queryId === queryId
|
||||
)
|
||||
globalDynamicBindings = getDynamicVariables(
|
||||
datasource,
|
||||
query._id,
|
||||
(variable, queryId) => variable.queryId !== queryId
|
||||
)
|
||||
// if query doesn't have ID then its new - don't try to copy existing dynamic variables
|
||||
if (!queryId) {
|
||||
dynamicVariables = []
|
||||
globalDynamicBindings = getDynamicVariables(datasource)
|
||||
} else {
|
||||
dynamicVariables = getDynamicVariables(
|
||||
datasource,
|
||||
query._id,
|
||||
(variable, queryId) => variable.queryId === queryId
|
||||
)
|
||||
globalDynamicBindings = getDynamicVariables(
|
||||
datasource,
|
||||
query._id,
|
||||
(variable, queryId) => variable.queryId !== queryId
|
||||
)
|
||||
}
|
||||
|
||||
prettifyQueryRequestBody(
|
||||
query,
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<script>
|
||||
import { Popover, Layout, Heading, Body, Button } from "@budibase/bbui"
|
||||
import { Popover, Layout, Heading, Body, Button, Link } from "@budibase/bbui"
|
||||
import { store } from "builderStore"
|
||||
import { TOURS } from "./tours.js"
|
||||
import { goto, layout, isActive } from "@roxi/routify"
|
||||
|
@ -10,17 +10,20 @@
|
|||
let tourStep
|
||||
let tourStepIdx
|
||||
let lastStep
|
||||
let skipping = false
|
||||
|
||||
$: tourNodes = { ...$store.tourNodes }
|
||||
$: tourKey = $store.tourKey
|
||||
$: tourStepKey = $store.tourStepKey
|
||||
$: tour = TOURS[tourKey]
|
||||
$: tourOnSkip = tour?.onSkip
|
||||
|
||||
const updateTourStep = (targetStepKey, tourKey) => {
|
||||
if (!tourKey) {
|
||||
return
|
||||
}
|
||||
if (!tourSteps?.length) {
|
||||
tourSteps = [...TOURS[tourKey]]
|
||||
tourSteps = [...tour.steps]
|
||||
}
|
||||
tourStepIdx = getCurrentStepIdx(tourSteps, targetStepKey)
|
||||
lastStep = tourStepIdx + 1 == tourSteps.length
|
||||
|
@ -71,23 +74,8 @@
|
|||
tourStep.onComplete()
|
||||
}
|
||||
popover.hide()
|
||||
if (tourStep.endRoute) {
|
||||
$goto(tourStep.endRoute)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const previousStep = async () => {
|
||||
if (tourStepIdx > 0) {
|
||||
let target = tourSteps[tourStepIdx - 1]
|
||||
if (target) {
|
||||
store.update(state => ({
|
||||
...state,
|
||||
tourStepKey: target.id,
|
||||
}))
|
||||
navigateStep(target)
|
||||
} else {
|
||||
console.log("Could not retrieve step")
|
||||
if (tour.endRoute) {
|
||||
$goto(tour.endRoute)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -132,16 +120,23 @@
|
|||
</Body>
|
||||
<div class="tour-footer">
|
||||
<div class="tour-navigation">
|
||||
{#if tourStepIdx > 0}
|
||||
<Button
|
||||
{#if typeof tourOnSkip === "function"}
|
||||
<Link
|
||||
secondary
|
||||
on:click={previousStep}
|
||||
disabled={tourStepIdx == 0}
|
||||
quiet
|
||||
on:click={() => {
|
||||
skipping = true
|
||||
tourOnSkip()
|
||||
if (tour.endRoute) {
|
||||
$goto(tour.endRoute)
|
||||
}
|
||||
}}
|
||||
disabled={skipping}
|
||||
>
|
||||
<div>Back</div>
|
||||
</Button>
|
||||
Skip
|
||||
</Link>
|
||||
{/if}
|
||||
<Button cta on:click={nextStep}>
|
||||
<Button cta on:click={nextStep} disabled={skipping}>
|
||||
<div>{lastStep ? "Finish" : "Next"}</div>
|
||||
</Button>
|
||||
</div>
|
||||
|
@ -157,9 +152,10 @@
|
|||
padding: var(--spacing-xl);
|
||||
}
|
||||
.tour-navigation {
|
||||
grid-gap: var(--spectrum-alias-grid-baseline);
|
||||
grid-gap: var(--spacing-xl);
|
||||
display: flex;
|
||||
justify-content: end;
|
||||
align-items: center;
|
||||
}
|
||||
.tour-body :global(.feature-list) {
|
||||
margin-bottom: 0px;
|
||||
|
|
|
@ -13,7 +13,7 @@
|
|||
|
||||
const registerTourNode = (tourKey, stepKey) => {
|
||||
if (ready && !registered && tourKey) {
|
||||
currentTourStep = TOURS[tourKey].find(step => step.id === stepKey)
|
||||
currentTourStep = TOURS[tourKey].steps.find(step => step.id === stepKey)
|
||||
if (!currentTourStep) {
|
||||
return
|
||||
}
|
||||
|
|
|
@ -4,6 +4,7 @@ import { auth } from "stores/portal"
|
|||
import analytics from "analytics"
|
||||
import { OnboardingData, OnboardingDesign, OnboardingPublish } from "./steps"
|
||||
import { API } from "api"
|
||||
|
||||
const ONBOARDING_EVENT_PREFIX = "onboarding"
|
||||
|
||||
export const TOUR_STEP_KEYS = {
|
||||
|
@ -20,6 +21,37 @@ export const TOUR_KEYS = {
|
|||
FEATURE_ONBOARDING: "feature-onboarding",
|
||||
}
|
||||
|
||||
const endUserOnboarding = async ({ skipped = false } = {}) => {
|
||||
// Mark the users onboarding as complete
|
||||
// Clear all tour related state
|
||||
if (get(auth).user) {
|
||||
try {
|
||||
await API.updateSelf({
|
||||
onboardedAt: new Date().toISOString(),
|
||||
})
|
||||
|
||||
if (skipped) {
|
||||
tourEvent("skipped")
|
||||
}
|
||||
|
||||
// Update the cached user
|
||||
await auth.getSelf()
|
||||
|
||||
store.update(state => ({
|
||||
...state,
|
||||
tourNodes: undefined,
|
||||
tourKey: undefined,
|
||||
tourKeyStep: undefined,
|
||||
onboarding: false,
|
||||
}))
|
||||
} catch (e) {
|
||||
console.log("Onboarding failed", e)
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
const tourEvent = eventKey => {
|
||||
analytics.captureEvent(`${ONBOARDING_EVENT_PREFIX}:${eventKey}`, {
|
||||
eventSource: EventSource.PORTAL,
|
||||
|
@ -28,111 +60,81 @@ const tourEvent = eventKey => {
|
|||
|
||||
const getTours = () => {
|
||||
return {
|
||||
[TOUR_KEYS.TOUR_BUILDER_ONBOARDING]: [
|
||||
{
|
||||
id: TOUR_STEP_KEYS.BUILDER_DATA_SECTION,
|
||||
title: "Data",
|
||||
route: "/builder/app/:application/data",
|
||||
layout: OnboardingData,
|
||||
query: ".topleftnav .spectrum-Tabs-item#builder-data-tab",
|
||||
onLoad: async () => {
|
||||
tourEvent(TOUR_STEP_KEYS.BUILDER_DATA_SECTION)
|
||||
[TOUR_KEYS.TOUR_BUILDER_ONBOARDING]: {
|
||||
steps: [
|
||||
{
|
||||
id: TOUR_STEP_KEYS.BUILDER_DATA_SECTION,
|
||||
title: "Data",
|
||||
route: "/builder/app/:application/data",
|
||||
layout: OnboardingData,
|
||||
query: ".topleftnav .spectrum-Tabs-item#builder-data-tab",
|
||||
onLoad: async () => {
|
||||
tourEvent(TOUR_STEP_KEYS.BUILDER_DATA_SECTION)
|
||||
},
|
||||
align: "left",
|
||||
},
|
||||
align: "left",
|
||||
{
|
||||
id: TOUR_STEP_KEYS.BUILDER_DESIGN_SECTION,
|
||||
title: "Design",
|
||||
route: "/builder/app/:application/design",
|
||||
layout: OnboardingDesign,
|
||||
query: ".topleftnav .spectrum-Tabs-item#builder-design-tab",
|
||||
onLoad: () => {
|
||||
tourEvent(TOUR_STEP_KEYS.BUILDER_DESIGN_SECTION)
|
||||
},
|
||||
align: "left",
|
||||
},
|
||||
{
|
||||
id: TOUR_STEP_KEYS.BUILDER_AUTOMATION_SECTION,
|
||||
title: "Automations",
|
||||
route: "/builder/app/:application/automation",
|
||||
query: ".topleftnav .spectrum-Tabs-item#builder-automation-tab",
|
||||
body: "Once you have your app screens made, you can set up automations to fit in with your current workflow",
|
||||
onLoad: () => {
|
||||
tourEvent(TOUR_STEP_KEYS.BUILDER_AUTOMATION_SECTION)
|
||||
},
|
||||
align: "left",
|
||||
},
|
||||
{
|
||||
id: TOUR_STEP_KEYS.BUILDER_USER_MANAGEMENT,
|
||||
title: "Users",
|
||||
query: ".toprightnav #builder-app-users-button",
|
||||
body: "Add users to your app and control what level of access they have.",
|
||||
onLoad: () => {
|
||||
tourEvent(TOUR_STEP_KEYS.BUILDER_USER_MANAGEMENT)
|
||||
},
|
||||
},
|
||||
{
|
||||
id: TOUR_STEP_KEYS.BUILDER_APP_PUBLISH,
|
||||
title: "Publish",
|
||||
layout: OnboardingPublish,
|
||||
route: "/builder/app/:application/design",
|
||||
query: ".toprightnav #builder-app-publish-button",
|
||||
onLoad: () => {
|
||||
tourEvent(TOUR_STEP_KEYS.BUILDER_APP_PUBLISH)
|
||||
},
|
||||
onComplete: endUserOnboarding,
|
||||
},
|
||||
],
|
||||
onSkip: async () => {
|
||||
await endUserOnboarding({ skipped: true })
|
||||
},
|
||||
{
|
||||
id: TOUR_STEP_KEYS.BUILDER_DESIGN_SECTION,
|
||||
title: "Design",
|
||||
route: "/builder/app/:application/design",
|
||||
layout: OnboardingDesign,
|
||||
query: ".topleftnav .spectrum-Tabs-item#builder-design-tab",
|
||||
onLoad: () => {
|
||||
tourEvent(TOUR_STEP_KEYS.BUILDER_DESIGN_SECTION)
|
||||
endRoute: "/builder/app/:application/data",
|
||||
},
|
||||
[TOUR_KEYS.FEATURE_ONBOARDING]: {
|
||||
steps: [
|
||||
{
|
||||
id: TOUR_STEP_KEYS.FEATURE_USER_MANAGEMENT,
|
||||
title: "Users",
|
||||
query: ".toprightnav #builder-app-users-button",
|
||||
body: "Add users to your app and control what level of access they have.",
|
||||
onLoad: () => {
|
||||
tourEvent(TOUR_STEP_KEYS.FEATURE_USER_MANAGEMENT)
|
||||
},
|
||||
onComplete: endUserOnboarding,
|
||||
},
|
||||
align: "left",
|
||||
},
|
||||
{
|
||||
id: TOUR_STEP_KEYS.BUILDER_AUTOMATION_SECTION,
|
||||
title: "Automations",
|
||||
route: "/builder/app/:application/automation",
|
||||
query: ".topleftnav .spectrum-Tabs-item#builder-automation-tab",
|
||||
body: "Once you have your app screens made, you can set up automations to fit in with your current workflow",
|
||||
onLoad: () => {
|
||||
tourEvent(TOUR_STEP_KEYS.BUILDER_AUTOMATION_SECTION)
|
||||
},
|
||||
align: "left",
|
||||
},
|
||||
{
|
||||
id: TOUR_STEP_KEYS.BUILDER_USER_MANAGEMENT,
|
||||
title: "Users",
|
||||
query: ".toprightnav #builder-app-users-button",
|
||||
body: "Add users to your app and control what level of access they have.",
|
||||
onLoad: () => {
|
||||
tourEvent(TOUR_STEP_KEYS.BUILDER_USER_MANAGEMENT)
|
||||
},
|
||||
},
|
||||
{
|
||||
id: TOUR_STEP_KEYS.BUILDER_APP_PUBLISH,
|
||||
title: "Publish",
|
||||
layout: OnboardingPublish,
|
||||
route: "/builder/app/:application/design",
|
||||
endRoute: "/builder/app/:application/data",
|
||||
query: ".toprightnav #builder-app-publish-button",
|
||||
onLoad: () => {
|
||||
tourEvent(TOUR_STEP_KEYS.BUILDER_APP_PUBLISH)
|
||||
},
|
||||
onComplete: async () => {
|
||||
// Mark the users onboarding as complete
|
||||
// Clear all tour related state
|
||||
if (get(auth).user) {
|
||||
await API.updateSelf({
|
||||
onboardedAt: new Date().toISOString(),
|
||||
})
|
||||
|
||||
// Update the cached user
|
||||
await auth.getSelf()
|
||||
|
||||
store.update(state => ({
|
||||
...state,
|
||||
tourNodes: undefined,
|
||||
tourKey: undefined,
|
||||
tourKeyStep: undefined,
|
||||
onboarding: false,
|
||||
}))
|
||||
}
|
||||
},
|
||||
},
|
||||
],
|
||||
[TOUR_KEYS.FEATURE_ONBOARDING]: [
|
||||
{
|
||||
id: TOUR_STEP_KEYS.FEATURE_USER_MANAGEMENT,
|
||||
title: "Users",
|
||||
query: ".toprightnav #builder-app-users-button",
|
||||
body: "Add users to your app and control what level of access they have.",
|
||||
onLoad: () => {
|
||||
tourEvent(TOUR_STEP_KEYS.FEATURE_USER_MANAGEMENT)
|
||||
},
|
||||
onComplete: async () => {
|
||||
// Push the onboarding forward
|
||||
if (get(auth).user) {
|
||||
await API.updateSelf({
|
||||
onboardedAt: new Date().toISOString(),
|
||||
})
|
||||
|
||||
// Update the cached user
|
||||
await auth.getSelf()
|
||||
|
||||
store.update(state => ({
|
||||
...state,
|
||||
tourNodes: undefined,
|
||||
tourKey: undefined,
|
||||
tourKeyStep: undefined,
|
||||
onboarding: false,
|
||||
}))
|
||||
}
|
||||
},
|
||||
},
|
||||
],
|
||||
],
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,11 +1,8 @@
|
|||
<script>
|
||||
import { Tooltip } from "@budibase/bbui"
|
||||
|
||||
export let text
|
||||
export let url
|
||||
export let active = false
|
||||
export let disabled = false
|
||||
export let tooltip = null
|
||||
</script>
|
||||
|
||||
<div class="side-nav-item">
|
||||
|
@ -18,11 +15,6 @@
|
|||
{text || ""}
|
||||
</div>
|
||||
{/if}
|
||||
{#if tooltip}
|
||||
<div class="tooltip">
|
||||
<Tooltip textWrapping direction="right" text={tooltip} />
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
|
@ -45,17 +37,4 @@
|
|||
pointer-events: none;
|
||||
color: var(--spectrum-global-color-gray-500) !important;
|
||||
}
|
||||
.tooltip {
|
||||
position: absolute;
|
||||
transform: translateY(-50%);
|
||||
left: 100%;
|
||||
top: 50%;
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
transition: opacity 130ms ease-out;
|
||||
z-index: 100;
|
||||
}
|
||||
.side-nav-item:hover .tooltip {
|
||||
opacity: 1;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -2,12 +2,12 @@
|
|||
import { Heading, Body, Button, Icon } from "@budibase/bbui"
|
||||
import { processStringSync } from "@budibase/string-templates"
|
||||
import { goto } from "@roxi/routify"
|
||||
import { UserAvatar } from "@budibase/frontend-core"
|
||||
import { UserAvatars } from "@budibase/frontend-core"
|
||||
|
||||
export let app
|
||||
export let lockedAction
|
||||
|
||||
$: editing = app?.lockedBy != null
|
||||
$: editing = app.sessions?.length
|
||||
|
||||
const handleDefaultClick = () => {
|
||||
if (window.innerWidth < 640) {
|
||||
|
@ -41,7 +41,7 @@
|
|||
<div class="updated">
|
||||
{#if editing}
|
||||
Currently editing
|
||||
<UserAvatar user={app.lockedBy} />
|
||||
<UserAvatars users={app.sessions} />
|
||||
{:else if app.updatedAt}
|
||||
{processStringSync("Updated {{ duration time 'millisecond' }} ago", {
|
||||
time: new Date().getTime() - new Date(app.updatedAt).getTime(),
|
||||
|
|
|
@ -15,8 +15,6 @@
|
|||
import { createValidationStore } from "helpers/validation/yup"
|
||||
import * as appValidation from "helpers/validation/yup/app"
|
||||
import TemplateCard from "components/common/TemplateCard.svelte"
|
||||
import createFromScratchScreen from "builderStore/store/screenTemplates/createFromScratchScreen"
|
||||
import { Roles } from "constants/backend"
|
||||
import { lowercase } from "helpers"
|
||||
|
||||
export let template
|
||||
|
@ -142,21 +140,6 @@
|
|||
// Create user
|
||||
await auth.setInitInfo({})
|
||||
|
||||
// Create a default home screen if no template was selected
|
||||
if (template == null) {
|
||||
let defaultScreenTemplate = createFromScratchScreen.create()
|
||||
defaultScreenTemplate.routing.route = "/home"
|
||||
defaultScreenTemplate.routing.roldId = Roles.BASIC
|
||||
try {
|
||||
await store.actions.screens.save(defaultScreenTemplate)
|
||||
} catch (err) {
|
||||
console.error("Could not create a default application screen", err)
|
||||
notifications.warning(
|
||||
"Encountered an issue creating the default screen."
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
$goto(`/builder/app/${createdApp.instance._id}`)
|
||||
} catch (error) {
|
||||
creating = false
|
||||
|
|
|
@ -53,6 +53,10 @@ export const FIELDS = {
|
|||
numericality: { greaterThanOrEqualTo: "", lessThanOrEqualTo: "" },
|
||||
},
|
||||
},
|
||||
BIGINT: {
|
||||
name: "BigInt",
|
||||
type: "bigint",
|
||||
},
|
||||
BOOLEAN: {
|
||||
name: "Boolean",
|
||||
type: "boolean",
|
||||
|
@ -147,7 +151,7 @@ export function isAutoColumnUserRelationship(subtype) {
|
|||
)
|
||||
}
|
||||
|
||||
export const RelationshipTypes = {
|
||||
export const RelationshipType = {
|
||||
MANY_TO_MANY: "many-to-many",
|
||||
ONE_TO_MANY: "one-to-many",
|
||||
MANY_TO_ONE: "many-to-one",
|
||||
|
|
|
@ -29,3 +29,15 @@ export const lowercase = s => s.substring(0, 1).toLowerCase() + s.substring(1)
|
|||
export const get_name = s => (!s ? "" : last(s.split("/")))
|
||||
|
||||
export const get_capitalised_name = name => pipe(name, [get_name, capitalise])
|
||||
|
||||
export const isBuilderInputFocused = e => {
|
||||
const activeTag = document.activeElement?.tagName.toLowerCase()
|
||||
const inCodeEditor = document.activeElement?.classList?.contains("cm-content")
|
||||
if (
|
||||
(inCodeEditor || ["input", "textarea"].indexOf(activeTag) !== -1) &&
|
||||
e.key !== "Escape"
|
||||
) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
|
|
@ -7,4 +7,5 @@ export {
|
|||
get_name,
|
||||
get_capitalised_name,
|
||||
lowercase,
|
||||
isBuilderInputFocused,
|
||||
} from "./helpers"
|
||||
|
|
|
@ -15,6 +15,7 @@
|
|||
}
|
||||
|
||||
onMount(() => {
|
||||
window.isBuilder = true
|
||||
window.closePreview = () => {
|
||||
store.update(state => ({
|
||||
...state,
|
||||
|
|
|
@ -1,28 +0,0 @@
|
|||
<script>
|
||||
import { UserAvatar } from "@budibase/frontend-core"
|
||||
|
||||
export let users = []
|
||||
|
||||
$: uniqueUsers = unique(users)
|
||||
|
||||
const unique = users => {
|
||||
let uniqueUsers = {}
|
||||
users?.forEach(user => {
|
||||
uniqueUsers[user.email] = user
|
||||
})
|
||||
return Object.values(uniqueUsers)
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="avatars">
|
||||
{#each uniqueUsers as user}
|
||||
<UserAvatar {user} tooltipDirection="bottom" />
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.avatars {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
}
|
||||
</style>
|
|
@ -15,6 +15,7 @@
|
|||
Heading,
|
||||
Modal,
|
||||
notifications,
|
||||
TooltipPosition,
|
||||
} from "@budibase/bbui"
|
||||
import AppActions from "components/deploy/AppActions.svelte"
|
||||
import { API } from "api"
|
||||
|
@ -25,8 +26,8 @@
|
|||
import TourWrap from "components/portal/onboarding/TourWrap.svelte"
|
||||
import TourPopover from "components/portal/onboarding/TourPopover.svelte"
|
||||
import BuilderSidePanel from "./_components/BuilderSidePanel.svelte"
|
||||
import UserAvatars from "./_components/UserAvatars.svelte"
|
||||
import { TOUR_KEYS, TOURS } from "components/portal/onboarding/tours.js"
|
||||
import { UserAvatars } from "@budibase/frontend-core"
|
||||
import { TOUR_KEYS } from "components/portal/onboarding/tours.js"
|
||||
import PreviewOverlay from "./_components/PreviewOverlay.svelte"
|
||||
|
||||
export let application
|
||||
|
@ -86,17 +87,10 @@
|
|||
// Check if onboarding is enabled.
|
||||
if (isEnabled(TENANT_FEATURE_FLAGS.ONBOARDING_TOUR)) {
|
||||
if (!$auth.user?.onboardedAt) {
|
||||
// Determine the correct step
|
||||
const activeNav = $layout.children.find(c => $isActive(c.path))
|
||||
const onboardingTour = TOURS[TOUR_KEYS.TOUR_BUILDER_ONBOARDING]
|
||||
const targetStep = activeNav
|
||||
? onboardingTour.find(step => step.route === activeNav?.path)
|
||||
: null
|
||||
await store.update(state => ({
|
||||
...state,
|
||||
onboarding: true,
|
||||
tourKey: TOUR_KEYS.TOUR_BUILDER_ONBOARDING,
|
||||
tourStepKey: targetStep?.id,
|
||||
}))
|
||||
} else {
|
||||
// Feature tour date
|
||||
|
@ -172,7 +166,11 @@
|
|||
</div>
|
||||
<div class="toprightnav">
|
||||
<span>
|
||||
<UserAvatars users={$userStore} />
|
||||
<UserAvatars
|
||||
users={$userStore}
|
||||
order="rtl"
|
||||
tooltipPosition={TooltipPosition.Bottom}
|
||||
/>
|
||||
</span>
|
||||
<AppActions {application} {loaded} />
|
||||
</div>
|
||||
|
@ -228,7 +226,7 @@
|
|||
.top-nav {
|
||||
flex: 0 0 60px;
|
||||
background: var(--background);
|
||||
padding: 0 var(--spacing-xl);
|
||||
padding-left: var(--spacing-xl);
|
||||
display: grid;
|
||||
grid-template-columns: 1fr auto 1fr;
|
||||
flex-direction: row;
|
||||
|
|
|
@ -8,6 +8,10 @@
|
|||
import { onDestroy, onMount } from "svelte"
|
||||
import { syncURLToState } from "helpers/urlStateSync"
|
||||
import * as routify from "@roxi/routify"
|
||||
import { store } from "builderStore"
|
||||
|
||||
$: automationId = $selectedAutomation?._id
|
||||
$: store.actions.websocket.selectResource(automationId)
|
||||
|
||||
// Keep URL and state in sync for selected screen ID
|
||||
const stopSyncing = syncURLToState({
|
||||
|
|
|
@ -74,11 +74,12 @@
|
|||
border: 1px solid var(--spectrum-global-color-gray-300);
|
||||
border-radius: 5px;
|
||||
width: 100%;
|
||||
background-color: #00000047;
|
||||
background: var(--spectrum-global-color-gray-50);
|
||||
color: white;
|
||||
overflow: hidden;
|
||||
padding: 12px 16px;
|
||||
box-sizing: border-box;
|
||||
transition: background 130ms ease-out;
|
||||
}
|
||||
.left {
|
||||
flex: 1;
|
||||
|
@ -94,7 +95,7 @@
|
|||
}
|
||||
.button:hover {
|
||||
cursor: pointer;
|
||||
filter: brightness(1.2);
|
||||
background: var(--spectrum-global-color-gray-100);
|
||||
}
|
||||
.connected {
|
||||
display: flex;
|
||||
|
|
|
@ -2,11 +2,12 @@
|
|||
import { Button, Modal } from "@budibase/bbui"
|
||||
import ImportQueriesModal from "./RestImportQueriesModal.svelte"
|
||||
|
||||
export let datasourceId
|
||||
let importQueriesModal
|
||||
</script>
|
||||
|
||||
<Modal bind:this={importQueriesModal}>
|
||||
<ImportQueriesModal createDatasource={false} datasourceId={"todo"} />
|
||||
<ImportQueriesModal createDatasource={false} {datasourceId} />
|
||||
</Modal>
|
||||
|
||||
<div class="button">
|
||||
|
|
|
@ -18,7 +18,7 @@
|
|||
const onClick = dynamicVariable => {
|
||||
const queryId = dynamicVariable.queryId
|
||||
queries.select({ _id: queryId })
|
||||
$goto(`./${queryId}`)
|
||||
$goto(`../../query/${queryId}`)
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -7,14 +7,14 @@
|
|||
} from "stores/backend"
|
||||
|
||||
import { hasData } from "stores/selectors"
|
||||
import { Icon, notifications, Heading, Body } from "@budibase/bbui"
|
||||
import { notifications, Body, Icon, AbsTooltip } from "@budibase/bbui"
|
||||
import { params, goto } from "@roxi/routify"
|
||||
import CreateExternalDatasourceModal from "./_components/CreateExternalDatasourceModal/index.svelte"
|
||||
import CreateInternalTableModal from "./_components/CreateInternalTableModal.svelte"
|
||||
import DatasourceOption from "./_components/DatasourceOption.svelte"
|
||||
import IntegrationIcon from "components/backend/DatasourceNavigator/IntegrationIcon.svelte"
|
||||
import CreationPage from "components/common/CreationPage.svelte"
|
||||
import ICONS from "components/backend/DatasourceNavigator/icons/index.js"
|
||||
import FontAwesomeIcon from "components/common/FontAwesomeIcon.svelte"
|
||||
|
||||
let internalTableModal
|
||||
let externalDatasourceModal
|
||||
|
@ -46,25 +46,16 @@
|
|||
bind:this={externalDatasourceModal}
|
||||
/>
|
||||
|
||||
<div class="page">
|
||||
<div class="closeButton">
|
||||
{#if hasData($datasources, $tables)}
|
||||
<Icon hoverable name="Close" on:click={$goto("./table")} />
|
||||
{/if}
|
||||
</div>
|
||||
<div class="heading">
|
||||
<Heading weight="light">Add new data source</Heading>
|
||||
</div>
|
||||
|
||||
<CreationPage
|
||||
showClose={hasData($datasources, $tables)}
|
||||
onClose={() => $goto("./table")}
|
||||
heading="Add new data source"
|
||||
>
|
||||
<div class="subHeading">
|
||||
<Body>Get started with our Budibase DB</Body>
|
||||
<div
|
||||
role="tooltip"
|
||||
title="Budibase DB is built with CouchDB"
|
||||
class="tooltip"
|
||||
>
|
||||
<FontAwesomeIcon name="fa-solid fa-circle-info" />
|
||||
</div>
|
||||
<AbsTooltip text="Budibase DB is built with CouchDB">
|
||||
<Icon name="Info" size="S" />
|
||||
</AbsTooltip>
|
||||
</div>
|
||||
|
||||
<div class="options">
|
||||
|
@ -113,37 +104,19 @@
|
|||
</DatasourceOption>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
</CreationPage>
|
||||
|
||||
<style>
|
||||
.page {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.closeButton {
|
||||
height: 38px;
|
||||
display: flex;
|
||||
justify-content: right;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.heading {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.subHeading {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 24px;
|
||||
margin-top: 12px;
|
||||
margin-bottom: 36px;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.tooltip {
|
||||
margin-left: 6px;
|
||||
.subHeading :global(p) {
|
||||
color: var(--spectrum-global-color-gray-600) !important;
|
||||
}
|
||||
|
||||
.options {
|
||||
width: 100%;
|
||||
display: grid;
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
import { goto, isActive } from "@roxi/routify"
|
||||
import { notifications } from "@budibase/bbui"
|
||||
import ConfirmDialog from "components/common/ConfirmDialog.svelte"
|
||||
import { isBuilderInputFocused } from "helpers"
|
||||
|
||||
let confirmDeleteDialog
|
||||
let confirmEjectDialog
|
||||
|
@ -100,13 +101,7 @@
|
|||
return
|
||||
}
|
||||
// Ignore events when typing
|
||||
const activeTag = document.activeElement?.tagName.toLowerCase()
|
||||
const inCodeEditor =
|
||||
document.activeElement?.classList?.contains("cm-content")
|
||||
if (
|
||||
(inCodeEditor || ["input", "textarea"].indexOf(activeTag) !== -1) &&
|
||||
e.key !== "Escape"
|
||||
) {
|
||||
if (isBuilderInputFocused(e)) {
|
||||
return
|
||||
}
|
||||
// Key events are always for the selected component
|
||||
|
|
|
@ -15,12 +15,7 @@
|
|||
{
|
||||
"name": "Layout",
|
||||
"icon": "ClassicGridView",
|
||||
"children": [
|
||||
"container",
|
||||
"section",
|
||||
"grid",
|
||||
"sidepanel"
|
||||
]
|
||||
"children": ["container", "section", "grid", "sidepanel"]
|
||||
},
|
||||
{
|
||||
"name": "Data",
|
||||
|
@ -63,6 +58,7 @@
|
|||
"fieldgroup",
|
||||
"stringfield",
|
||||
"numberfield",
|
||||
"bigintfield",
|
||||
"passwordfield",
|
||||
"optionsfield",
|
||||
"booleanfield",
|
||||
|
@ -79,13 +75,6 @@
|
|||
{
|
||||
"name": "Chart",
|
||||
"icon": "GraphBarVertical",
|
||||
"children": [
|
||||
"bar",
|
||||
"line",
|
||||
"area",
|
||||
"candlestick",
|
||||
"pie",
|
||||
"donut"
|
||||
]
|
||||
"children": ["bar", "line", "area", "candlestick", "pie", "donut"]
|
||||
}
|
||||
]
|
||||
|
|
|
@ -1,165 +0,0 @@
|
|||
<script>
|
||||
import { tables } from "stores/backend"
|
||||
import { ModalContent, Body, Layout, Icon, Heading } from "@budibase/bbui"
|
||||
import blankScreenPreview from "./blankScreenPreview.png"
|
||||
import listScreenPreview from "./listScreenPreview.png"
|
||||
|
||||
export let onConfirm
|
||||
export let onCancel
|
||||
|
||||
let listScreenModeKey = "autoCreate"
|
||||
let blankScreenModeKey = "blankScreen"
|
||||
|
||||
let selectedScreenMode
|
||||
|
||||
const confirmScreenSelection = async () => {
|
||||
await onConfirm(selectedScreenMode)
|
||||
}
|
||||
</script>
|
||||
|
||||
<div>
|
||||
<ModalContent
|
||||
title="Add screens"
|
||||
confirmText="Continue"
|
||||
cancelText="Cancel"
|
||||
onConfirm={confirmScreenSelection}
|
||||
{onCancel}
|
||||
disabled={!selectedScreenMode}
|
||||
size="M"
|
||||
>
|
||||
<Layout noPadding gap="S">
|
||||
<div
|
||||
class="screen-type item blankView"
|
||||
class:selected={selectedScreenMode == blankScreenModeKey}
|
||||
on:click={() => {
|
||||
selectedScreenMode = blankScreenModeKey
|
||||
}}
|
||||
>
|
||||
<div class="content screen-type-wrap">
|
||||
<img
|
||||
alt="blank screen preview"
|
||||
class="preview"
|
||||
src={blankScreenPreview}
|
||||
/>
|
||||
<div class="screen-type-text">
|
||||
<Heading size="XS">Blank screen</Heading>
|
||||
<Body size="S">Add an empty blank screen</Body>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
style="color: var(--spectrum-global-color-green-600); float: right"
|
||||
>
|
||||
<div
|
||||
class={`checkmark-spacing ${
|
||||
selectedScreenMode == blankScreenModeKey ? "visible" : ""
|
||||
}`}
|
||||
>
|
||||
<Icon size="S" name="CheckmarkCircle" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="listViewTitle">
|
||||
<Heading size="XS">Quickly create a screen from your data</Heading>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="screen-type item"
|
||||
class:selected={selectedScreenMode == listScreenModeKey}
|
||||
on:click={() => {
|
||||
selectedScreenMode = listScreenModeKey
|
||||
}}
|
||||
class:disabled={!$tables.list.filter(table => table._id !== "ta_users")
|
||||
.length}
|
||||
>
|
||||
<div class="content screen-type-wrap">
|
||||
<img
|
||||
alt="list screen preview"
|
||||
class="preview"
|
||||
src={listScreenPreview}
|
||||
/>
|
||||
<div class="screen-type-text">
|
||||
<Heading size="XS">List view</Heading>
|
||||
<Body size="S">
|
||||
Create, edit and view your data in a list view screen with side
|
||||
panel
|
||||
</Body>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
style="color: var(--spectrum-global-color-green-600); float: right"
|
||||
>
|
||||
<div
|
||||
class={`checkmark-spacing ${
|
||||
selectedScreenMode == listScreenModeKey ? "visible" : ""
|
||||
}`}
|
||||
>
|
||||
<Icon size="S" name="CheckmarkCircle" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Layout>
|
||||
</ModalContent>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.screen-type-wrap {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
}
|
||||
.disabled {
|
||||
opacity: 0.3;
|
||||
pointer-events: none;
|
||||
}
|
||||
.checkmark-spacing {
|
||||
margin-right: var(--spacing-m);
|
||||
opacity: 0;
|
||||
}
|
||||
.content {
|
||||
letter-spacing: 0px;
|
||||
}
|
||||
.item {
|
||||
cursor: pointer;
|
||||
grid-gap: var(--spectrum-alias-grid-margin-xsmall);
|
||||
background: var(--spectrum-alias-background-color-secondary);
|
||||
transition: 0.3s all;
|
||||
border: 1px solid var(--spectrum-global-color-gray-300);
|
||||
border-radius: 4px;
|
||||
border-width: 1px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
.item:hover,
|
||||
.selected {
|
||||
background: var(--spectrum-alias-background-color-tertiary);
|
||||
}
|
||||
.screen-type-wrap .screen-type-text {
|
||||
padding-left: var(--spectrum-alias-item-padding-xl);
|
||||
}
|
||||
.screen-type-wrap .screen-type-text :global(h1) {
|
||||
padding-bottom: var(--spacing-xs);
|
||||
}
|
||||
.screen-type-wrap :global(.spectrum-Icon) {
|
||||
min-width: var(--spectrum-icon-size-m);
|
||||
}
|
||||
.screen-type-wrap :global(.spectrum-Heading) {
|
||||
padding-bottom: var(--spectrum-alias-item-padding-s);
|
||||
}
|
||||
.preview {
|
||||
width: 140px;
|
||||
}
|
||||
|
||||
.listViewTitle {
|
||||
margin-top: 35px;
|
||||
}
|
||||
|
||||
.blankView {
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.visible {
|
||||
opacity: 1;
|
||||
}
|
||||
</style>
|
|
@ -9,7 +9,7 @@
|
|||
Helpers,
|
||||
notifications,
|
||||
} from "@budibase/bbui"
|
||||
import ScreenDetailsModal from "./ScreenDetailsModal.svelte"
|
||||
import ScreenDetailsModal from "components/design/ScreenDetailsModal.svelte"
|
||||
import sanitizeUrl from "builderStore/store/screenTemplates/utils/sanitizeUrl"
|
||||
import { makeComponentUnique } from "builderStore/componentUtils"
|
||||
|
||||
|
|
|
@ -1,17 +1,16 @@
|
|||
<script>
|
||||
import { Search, Layout, Select, Body, Button } from "@budibase/bbui"
|
||||
import Panel from "components/design/Panel.svelte"
|
||||
import { goto } from "@roxi/routify"
|
||||
import { roles } from "stores/backend"
|
||||
import { store, sortedScreens, userSelectedResourceMap } from "builderStore"
|
||||
import NavItem from "components/common/NavItem.svelte"
|
||||
import ScreenDropdownMenu from "./ScreenDropdownMenu.svelte"
|
||||
import ScreenWizard from "./ScreenWizard.svelte"
|
||||
import RoleIndicator from "./RoleIndicator.svelte"
|
||||
import { RoleUtils } from "@budibase/frontend-core"
|
||||
|
||||
let searchString
|
||||
let accessRole = "all"
|
||||
let showNewScreenModal
|
||||
|
||||
$: filteredScreens = getFilteredScreens(
|
||||
$sortedScreens,
|
||||
|
@ -31,7 +30,7 @@
|
|||
|
||||
<Panel title="Screens" borderRight>
|
||||
<Layout paddingX="L" paddingY="XL" gap="S">
|
||||
<Button on:click={showNewScreenModal} cta>Add screen</Button>
|
||||
<Button on:click={() => $goto("../../new")} cta>Add screen</Button>
|
||||
<Search
|
||||
placeholder="Search"
|
||||
value={searchString}
|
||||
|
@ -74,5 +73,3 @@
|
|||
</Layout>
|
||||
{/if}
|
||||
</Panel>
|
||||
|
||||
<ScreenWizard bind:showModal={showNewScreenModal} />
|
||||
|
|
Binary file not shown.
Before Width: | Height: | Size: 69 KiB |
Binary file not shown.
Before Width: | Height: | Size: 106 KiB |
|
@ -1,6 +1,5 @@
|
|||
<script>
|
||||
import ScreenDetailsModal from "./ScreenDetailsModal.svelte"
|
||||
import NewScreenModal from "./NewScreenModal.svelte"
|
||||
import ScreenDetailsModal from "components/design/ScreenDetailsModal.svelte"
|
||||
import DatasourceModal from "./DatasourceModal.svelte"
|
||||
import ScreenRoleModal from "./ScreenRoleModal.svelte"
|
||||
import sanitizeUrl from "builderStore/store/screenTemplates/utils/sanitizeUrl"
|
||||
|
@ -11,11 +10,11 @@
|
|||
import { tables } from "stores/backend"
|
||||
import { Roles } from "constants/backend"
|
||||
import { capitalise } from "helpers"
|
||||
import { goto } from "@roxi/routify"
|
||||
|
||||
let pendingScreen
|
||||
|
||||
// Modal refs
|
||||
let newScreenModal
|
||||
let screenDetailsModal
|
||||
let datasourceModal
|
||||
let screenAccessRoleModal
|
||||
|
@ -26,16 +25,6 @@
|
|||
let blankScreenUrl = null
|
||||
let screenMode = null
|
||||
|
||||
// External handler to show the screen wizard
|
||||
export const showModal = () => {
|
||||
selectedTemplates = null
|
||||
blankScreenUrl = null
|
||||
screenMode = null
|
||||
pendingScreen = null
|
||||
screenAccessRole = Roles.BASIC
|
||||
newScreenModal.show()
|
||||
}
|
||||
|
||||
// Creates an array of screens, checking and sanitising their URLs
|
||||
const createScreens = async ({ screens, screenAccessRole }) => {
|
||||
if (!screens?.length) {
|
||||
|
@ -43,6 +32,8 @@
|
|||
}
|
||||
|
||||
try {
|
||||
let screenId
|
||||
|
||||
for (let screen of screens) {
|
||||
// Check we aren't clashing with an existing URL
|
||||
if (hasExistingUrl(screen.routing.route)) {
|
||||
|
@ -64,7 +55,8 @@
|
|||
screen.routing.roleId = screenAccessRole
|
||||
|
||||
// Create the screen
|
||||
await store.actions.screens.save(screen)
|
||||
const response = await store.actions.screens.save(screen)
|
||||
screenId = response._id
|
||||
|
||||
// Add link in layout for list screens
|
||||
if (screen.props._instanceName.endsWith("List")) {
|
||||
|
@ -74,7 +66,10 @@
|
|||
)
|
||||
}
|
||||
}
|
||||
|
||||
$goto(`./${screenId}`)
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
notifications.error("Error creating screens")
|
||||
}
|
||||
}
|
||||
|
@ -104,18 +99,24 @@
|
|||
}
|
||||
|
||||
// Handler for NewScreenModal
|
||||
const confirmScreenSelection = async mode => {
|
||||
export const show = mode => {
|
||||
selectedTemplates = null
|
||||
blankScreenUrl = null
|
||||
screenMode = mode
|
||||
pendingScreen = null
|
||||
screenAccessRole = Roles.BASIC
|
||||
|
||||
if (mode === "autoCreate") {
|
||||
if (mode === "table") {
|
||||
datasourceModal.show()
|
||||
} else {
|
||||
} else if (mode === "blank") {
|
||||
let templates = getTemplates($store, $tables.list)
|
||||
const blankScreenTemplate = templates.find(
|
||||
t => t.id === "createFromScratch"
|
||||
)
|
||||
pendingScreen = blankScreenTemplate.create()
|
||||
screenDetailsModal.show()
|
||||
} else {
|
||||
throw new Error("Invalid mode provided")
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -155,7 +156,7 @@
|
|||
|
||||
// Submit screen config for creation.
|
||||
const confirmScreenCreation = async () => {
|
||||
if (screenMode === "blankScreen") {
|
||||
if (screenMode === "blank") {
|
||||
confirmBlankScreenCreation({
|
||||
screenUrl: blankScreenUrl,
|
||||
screenAccessRole,
|
||||
|
@ -166,7 +167,7 @@
|
|||
}
|
||||
|
||||
const roleSelectBack = () => {
|
||||
if (screenMode === "blankScreen") {
|
||||
if (screenMode === "blank") {
|
||||
screenDetailsModal.show()
|
||||
} else {
|
||||
datasourceModal.show()
|
||||
|
@ -174,14 +175,9 @@
|
|||
}
|
||||
</script>
|
||||
|
||||
<Modal bind:this={newScreenModal}>
|
||||
<NewScreenModal onConfirm={confirmScreenSelection} />
|
||||
</Modal>
|
||||
|
||||
<Modal bind:this={datasourceModal}>
|
||||
<DatasourceModal
|
||||
onConfirm={confirmScreenDatasources}
|
||||
onCancel={() => newScreenModal.show()}
|
||||
initalScreens={!selectedTemplates ? [] : [...selectedTemplates]}
|
||||
/>
|
||||
</Modal>
|
||||
|
@ -198,7 +194,6 @@
|
|||
<Modal bind:this={screenDetailsModal}>
|
||||
<ScreenDetailsModal
|
||||
onConfirm={confirmScreenBlank}
|
||||
onCancel={() => newScreenModal.show()}
|
||||
initialUrl={blankScreenUrl}
|
||||
/>
|
||||
</Modal>
|
|
@ -40,14 +40,14 @@
|
|||
</script>
|
||||
|
||||
<ModalContent
|
||||
title="Autogenerated screens"
|
||||
title="Access"
|
||||
confirmText="Done"
|
||||
cancelText="Back"
|
||||
{onConfirm}
|
||||
{onCancel}
|
||||
disabled={!!error}
|
||||
>
|
||||
Select which level of access you want your screens to have
|
||||
Select the level of access required to see these screens
|
||||
<Select
|
||||
bind:value={screenAccessRole}
|
||||
on:change={onChangeRole}
|
Binary file not shown.
After Width: | Height: | Size: 32 KiB |
|
@ -1,67 +1,12 @@
|
|||
<script>
|
||||
import { store, selectedScreen } from "builderStore"
|
||||
import { onMount } from "svelte"
|
||||
import { redirect } from "@roxi/routify"
|
||||
import { Layout, Button, Detail, notifications } from "@budibase/bbui"
|
||||
import Logo from "assets/bb-space-man.svg"
|
||||
import createFromScratchScreen from "builderStore/store/screenTemplates/createFromScratchScreen"
|
||||
import { Roles } from "constants/backend"
|
||||
import { store as frontendStore } from "builderStore"
|
||||
|
||||
let loaded = false
|
||||
|
||||
const createFirstScreen = async () => {
|
||||
let screen = createFromScratchScreen.create()
|
||||
screen.routing.route = "/home"
|
||||
screen.routing.roldId = Roles.BASIC
|
||||
screen.routing.homeScreen = true
|
||||
try {
|
||||
const savedScreen = await store.actions.screens.save(screen)
|
||||
notifications.success("Screen created successfully")
|
||||
$redirect(`./${savedScreen._id}`)
|
||||
} catch (err) {
|
||||
console.error("Could not create screen", err)
|
||||
notifications.error("Error creating screen")
|
||||
}
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
if ($selectedScreen) {
|
||||
$redirect(`./${$selectedScreen._id}`)
|
||||
} else if ($store.screens?.length) {
|
||||
$redirect(`./${$store.screens[0]._id}`)
|
||||
$: {
|
||||
if ($frontendStore.screens.length > 0) {
|
||||
$redirect(`./${$frontendStore.screens[0]._id}`)
|
||||
} else {
|
||||
loaded = true
|
||||
$redirect("./new")
|
||||
}
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if loaded}
|
||||
<div class="centered">
|
||||
<Layout gap="S" justifyItems="center">
|
||||
<img class="img-size" alt="logo" src={Logo} />
|
||||
<div class="new-screen-text">
|
||||
<Detail size="L">LET’S BRING THIS APP TO LIFE</Detail>
|
||||
</div>
|
||||
<Button on:click={createFirstScreen} size="M" cta icon="Add">
|
||||
Create first screen
|
||||
</Button>
|
||||
</Layout>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
.centered {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
}
|
||||
.new-screen-text {
|
||||
width: 150px;
|
||||
text-align: center;
|
||||
font-weight: 600;
|
||||
}
|
||||
.img-size {
|
||||
width: 170px;
|
||||
}
|
||||
</style>
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue