Merge branch 'develop' into account-api-tests

This commit is contained in:
Mitch-Budibase 2023-07-19 15:21:50 +01:00
commit 7eb65ed347
181 changed files with 3283 additions and 2043 deletions

19
.github/stale.yml vendored
View File

@ -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

View File

@ -12,9 +12,6 @@ on:
- master - master
- develop - develop
pull_request: pull_request:
branches:
- master
- develop
workflow_dispatch: workflow_dispatch:
env: env:

29
.github/workflows/stale_bot.yml vendored Normal file
View File

@ -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

View File

@ -1,2 +1,2 @@
nodejs 14.20.1 nodejs 14.21.3
python 3.10.0 python 3.10.0

View File

@ -28,3 +28,4 @@ BB_ADMIN_USER_PASSWORD=
# A path that is watched for plugin bundles. Any bundles found are imported automatically/ # A path that is watched for plugin bundles. Any bundles found are imported automatically/
PLUGINS_DIR= PLUGINS_DIR=
ROLLING_LOG_MAX_SIZE=

View File

@ -22,6 +22,16 @@ server {
proxy_pass http://127.0.0.1:4001; 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 = / { location = / {
proxy_pass http://127.0.0.1:4001; proxy_pass http://127.0.0.1:4001;
} }

View File

@ -1,5 +1,5 @@
{ {
"version": "2.8.2-alpha.5", "version": "2.8.12-alpha.5",
"npmClient": "yarn", "npmClient": "yarn",
"packages": [ "packages": [
"packages/*" "packages/*"

12
nx.json
View File

@ -1,9 +1,13 @@
{ {
"tasksRunnerOptions": { "tasksRunnerOptions": {
"default": { "default": {
"runner": "nx/tasks-runners/default", "runner": "nx-cloud",
"options": { "options": {
"cacheableOperations": ["build", "test"] "cacheableOperations": [
"build",
"test"
],
"accessToken": "YWNiYzc5NTEtMzMzZC00NDhjLTgyNjktZTllMjI1MzM4OGQxfHJlYWQtd3JpdGU="
} }
} }
}, },
@ -11,7 +15,9 @@
"dev:builder": { "dev:builder": {
"dependsOn": [ "dependsOn": [
{ {
"projects": ["@budibase/string-templates"], "projects": [
"@budibase/string-templates"
],
"target": "build" "target": "build"
} }
] ]

View File

@ -3,7 +3,7 @@
"private": true, "private": true,
"devDependencies": { "devDependencies": {
"@esbuild-plugins/tsconfig-paths": "^0.1.2", "@esbuild-plugins/tsconfig-paths": "^0.1.2",
"@nx/js": "16.2.1", "@nx/js": "16.4.3",
"@rollup/plugin-json": "^4.0.2", "@rollup/plugin-json": "^4.0.2",
"@typescript-eslint/parser": "5.45.0", "@typescript-eslint/parser": "5.45.0",
"esbuild": "^0.17.18", "esbuild": "^0.17.18",
@ -13,9 +13,11 @@
"husky": "^8.0.3", "husky": "^8.0.3",
"js-yaml": "^4.1.0", "js-yaml": "^4.1.0",
"kill-port": "^1.6.1", "kill-port": "^1.6.1",
"lerna": "7.0.2", "lerna": "7.1.1",
"madge": "^6.0.0", "madge": "^6.0.0",
"minimist": "^1.2.8", "minimist": "^1.2.8",
"nx": "16.4.3",
"nx-cloud": "16.0.5",
"prettier": "2.8.8", "prettier": "2.8.8",
"prettier-plugin-svelte": "^2.3.0", "prettier-plugin-svelte": "^2.3.0",
"rimraf": "^3.0.2", "rimraf": "^3.0.2",
@ -44,15 +46,15 @@
"restore": "yarn run clean && yarn run bootstrap && yarn run build", "restore": "yarn run clean && yarn run bootstrap && yarn run build",
"nuke": "yarn run nuke:packages && yarn run nuke:docker", "nuke": "yarn run nuke:packages && yarn run nuke:docker",
"nuke:packages": "yarn run restore", "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", "clean": "lerna clean",
"kill-builder": "kill-port 3000", "kill-builder": "kill-port 3000",
"kill-server": "kill-port 4001 4002", "kill-server": "kill-port 4001 4002",
"kill-all": "yarn run kill-builder && yarn run kill-server", "kill-all": "yarn run kill-builder && yarn run kill-server",
"dev": "yarn run kill-all && lerna run --stream --parallel dev:builder --stream", "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 --parallel dev:builder --ignore @budibase/backend-core --ignore @budibase/server --ignore @budibase/worker", "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 && yarn build --projects=@budibase/client && lerna run --stream --parallel dev:builder --scope @budibase/worker --scope @budibase/server", "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 --parallel dev:built", "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", "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", "test": "lerna run --stream test --stream",
"lint:eslint": "eslint packages qa-core --max-warnings=0", "lint:eslint": "eslint packages qa-core --max-warnings=0",
@ -106,6 +108,5 @@
"@budibase/string-templates": "0.0.0", "@budibase/string-templates": "0.0.0",
"@budibase/types": "0.0.0" "@budibase/types": "0.0.0"
}, },
"dependencies": { "dependencies": {}
}
} }

View File

@ -51,6 +51,7 @@
"pouchdb": "7.3.0", "pouchdb": "7.3.0",
"pouchdb-find": "7.2.2", "pouchdb-find": "7.2.2",
"redlock": "4.2.0", "redlock": "4.2.0",
"rotating-file-stream": "3.1.0",
"sanitize-s3-objectkey": "0.0.1", "sanitize-s3-objectkey": "0.0.1",
"semver": "7.3.7", "semver": "7.3.7",
"tar-fs": "2.1.1", "tar-fs": "2.1.1",

View File

@ -159,7 +159,7 @@ export async function updateUserOAuth(userId: string, oAuthConfig: any) {
try { try {
const db = getGlobalDB() 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. //Do not overwrite the refresh token if a valid one is not provided.
if (typeof details.refreshToken !== "string") { if (typeof details.refreshToken !== "string") {

View File

@ -12,7 +12,7 @@ const EXPIRY_SECONDS = 3600
*/ */
async function populateFromDB(userId: string, tenantId: string) { async function populateFromDB(userId: string, tenantId: string) {
const db = tenancy.getTenantDB(tenantId) const db = tenancy.getTenantDB(tenantId)
const user = await db.get(userId) const user = await db.get<any>(userId)
user.budibaseAccess = true user.budibaseAccess = true
if (!env.SELF_HOSTED && !env.DISABLE_ACCOUNT_PORTAL) { if (!env.SELF_HOSTED && !env.DISABLE_ACCOUNT_PORTAL) {
const account = await accounts.getAccount(user.email) const account = await accounts.getAccount(user.email)

View File

@ -433,6 +433,9 @@ export class QueryBuilder<T> {
if (!value) { if (!value) {
return null return null
} }
if (typeof value === "boolean") {
return `(*:* AND !${key}:${value})`
}
return `!${key}:${builder.preprocess(value, allPreProcessingOpts)}` return `!${key}:${builder.preprocess(value, allPreProcessingOpts)}`
}) })
} }

View File

@ -5,7 +5,7 @@ export async function createUserIndex() {
const db = getGlobalDB() const db = getGlobalDB()
let designDoc let designDoc
try { try {
designDoc = await db.get("_design/database") designDoc = await db.get<any>("_design/database")
} catch (err: any) { } catch (err: any) {
if (err.status === 404) { if (err.status === 404) {
designDoc = { _id: "_design/database" } designDoc = { _id: "_design/database" }

View File

@ -47,7 +47,10 @@ function httpLogging() {
return process.env.HTTP_LOGGING return process.env.HTTP_LOGGING
} }
function findVersion() { function getPackageJsonFields(): {
VERSION: string
SERVICE_NAME: string
} {
function findFileInAncestors( function findFileInAncestors(
fileName: string, fileName: string,
currentDir: string currentDir: string
@ -69,10 +72,14 @@ function findVersion() {
try { try {
const packageJsonFile = findFileInAncestors("package.json", process.cwd()) const packageJsonFile = findFileInAncestors("package.json", process.cwd())
const content = readFileSync(packageJsonFile!, "utf-8") 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 { } catch {
// throwing an error here is confusing/causes backend-core to be hard to import // throwing an error here is confusing/causes backend-core to be hard to import
return undefined return { VERSION: "", SERVICE_NAME: "" }
} }
} }
@ -154,13 +161,14 @@ const environment = {
ENABLE_SSO_MAINTENANCE_MODE: selfHosted ENABLE_SSO_MAINTENANCE_MODE: selfHosted
? process.env.ENABLE_SSO_MAINTENANCE_MODE ? process.env.ENABLE_SSO_MAINTENANCE_MODE
: false, : false,
VERSION: findVersion(), ...getPackageJsonFields(),
DISABLE_PINO_LOGGER: process.env.DISABLE_PINO_LOGGER, DISABLE_PINO_LOGGER: process.env.DISABLE_PINO_LOGGER,
_set(key: any, value: any) { _set(key: any, value: any) {
process.env[key] = value process.env[key] = value
// @ts-ignore // @ts-ignore
environment[key] = value environment[key] = value
}, },
ROLLING_LOG_MAX_SIZE: process.env.ROLLING_LOG_MAX_SIZE || "10M",
} }
// clean up any environment variable edge cases // clean up any environment variable edge cases

View File

@ -1,3 +1,7 @@
export * as correlation from "./correlation/correlation" export * as correlation from "./correlation/correlation"
export { logger } from "./pino/logger" export { logger } from "./pino/logger"
export * from "./alerts" export * from "./alerts"
export * as system from "./system"
// turn off or on context logging i.e. tenantId, appId etc
export let LOG_CONTEXT = true

View File

@ -1,8 +1,14 @@
import env from "../../environment"
import pino, { LoggerOptions } from "pino" 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 context from "../../context"
import * as correlation from "../correlation" import * as correlation from "../correlation"
import { IdentityType } from "@budibase/types" import { LOG_CONTEXT } from "../index"
import { localFileDestination } from "../system"
// LOGGER // LOGGER
@ -15,22 +21,27 @@ if (!env.DISABLE_PINO_LOGGER) {
return { level: label.toUpperCase() } return { level: label.toUpperCase() }
}, },
bindings: () => { bindings: () => {
return {} return {
service: env.SERVICE_NAME,
}
}, },
}, },
timestamp: () => `,"timestamp":"${new Date(Date.now()).toISOString()}"`, timestamp: () => `,"timestamp":"${new Date(Date.now()).toISOString()}"`,
} }
const destinations: pino.DestinationStream[] = []
if (env.isDev()) { if (env.isDev()) {
pinoOptions.transport = { destinations.push(pinoPretty({ singleLine: true }))
target: "pino-pretty",
options: {
singleLine: true,
},
}
} }
pinoInstance = pino(pinoOptions) if (env.SELF_HOSTED) {
destinations.push(localFileDestination())
}
pinoInstance = destinations.length
? pino(pinoOptions, pino.multistream(destinations))
: pino(pinoOptions)
// CONSOLE OVERRIDES // CONSOLE OVERRIDES

View File

@ -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
}

View File

@ -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()
}
)
})
})

View File

@ -67,9 +67,9 @@ export const bulkUpdateGlobalUsers = async (users: User[]) => {
export async function getById(id: string, opts?: GetOpts): Promise<User> { export async function getById(id: string, opts?: GetOpts): Promise<User> {
const db = context.getGlobalDB() const db = context.getGlobalDB()
let user = await db.get(id) let user = await db.get<User>(id)
if (opts?.cleanup) { if (opts?.cleanup) {
user = removeUserPassword(user) user = removeUserPassword(user) as User
} }
return user return user
} }

View File

@ -1,6 +1,7 @@
<script> <script>
import "@spectrum-css/button/dist/index-vars.css" 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 type
export let disabled = false export let disabled = false
@ -16,48 +17,53 @@
export let tooltip = undefined export let tooltip = undefined
export let newStyles = true export let newStyles = true
export let id export let id
const dispatch = createEventDispatcher()
</script> </script>
<button <AbsTooltip text={tooltip}>
{id} <button
{type} {id}
class:spectrum-Button--cta={cta} {type}
class:spectrum-Button--primary={primary} class:spectrum-Button--cta={cta}
class:spectrum-Button--secondary={secondary} class:spectrum-Button--primary={primary}
class:spectrum-Button--warning={warning} class:spectrum-Button--secondary={secondary}
class:spectrum-Button--overBackground={overBackground} class:spectrum-Button--warning={warning}
class:spectrum-Button--quiet={quiet} class:spectrum-Button--overBackground={overBackground}
class:new-styles={newStyles} class:spectrum-Button--quiet={quiet}
class:active class:new-styles={newStyles}
class:disabled class:active
class="spectrum-Button spectrum-Button--size{size.toUpperCase()}" class:is-disabled={disabled}
{disabled} class="spectrum-Button spectrum-Button--size{size.toUpperCase()}"
on:click|preventDefault on:click|preventDefault={() => {
> if (!disabled) {
{#if icon} dispatch("click")
<svg }
class="spectrum-Icon spectrum-Icon--size{size.toUpperCase()}" }}
focusable="false" >
aria-hidden="true" {#if icon}
aria-label={icon} <svg
> class="spectrum-Icon spectrum-Icon--size{size.toUpperCase()}"
<use xlink:href="#spectrum-icon-18-{icon}" /> focusable="false"
</svg> aria-hidden="true"
{/if} aria-label={icon}
{#if $$slots} >
<span class="spectrum-Button-label"><slot /></span> <use xlink:href="#spectrum-icon-18-{icon}" />
{/if} </svg>
{#if tooltip} {/if}
<div class="tooltip"> {#if $$slots}
<Tooltip textWrapping={true} direction={"bottom"} text={tooltip} /> <span class="spectrum-Button-label"><slot /></span>
</div> {/if}
{/if} </button>
</button> </AbsTooltip>
<style> <style>
button { button {
position: relative; position: relative;
} }
button.is-disabled {
cursor: default;
}
.spectrum-Button-label { .spectrum-Button-label {
white-space: nowrap; white-space: nowrap;
overflow: hidden; overflow: hidden;
@ -66,23 +72,6 @@
.active { .active {
color: var(--spectrum-global-color-blue-600) !important; 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 { .spectrum-Button--primary.new-styles {
background: var(--spectrum-global-color-gray-800); background: var(--spectrum-global-color-gray-800);
border-color: transparent; border-color: transparent;
@ -96,10 +85,10 @@
border-color: transparent; border-color: transparent;
color: var(--spectrum-global-color-gray-900); 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); 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); color: var(--spectrum-global-color-gray-500);
} }
</style> </style>

View File

@ -82,7 +82,7 @@
{#if open} {#if open}
<div class="overlay" on:mousedown|self={() => (open = false)} /> <div class="overlay" on:mousedown|self={() => (open = false)} />
<div <div
transition:fly={{ y: -20, duration: 200 }} transition:fly|local={{ y: -20, duration: 200 }}
class="spectrum-Popover spectrum-Popover--bottom is-open" class="spectrum-Popover spectrum-Popover--bottom is-open"
> >
<ul class="spectrum-Menu" role="listbox"> <ul class="spectrum-Menu" role="listbox">

View File

@ -90,6 +90,6 @@
.spectrum-Popover { .spectrum-Popover {
min-width: var(--spectrum-global-dimension-size-2000); min-width: var(--spectrum-global-dimension-size-2000);
border-color: var(--spectrum-global-color-gray-300); border-color: var(--spectrum-global-color-gray-300);
overflow: visible; overflow: auto;
} }
</style> </style>

View File

@ -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>

View File

@ -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>

View File

@ -36,6 +36,12 @@ export { default as Layout } from "./Layout/Layout.svelte"
export { default as Page } from "./Layout/Page.svelte" export { default as Page } from "./Layout/Page.svelte"
export { default as Link } from "./Link/Link.svelte" export { default as Link } from "./Link/Link.svelte"
export { default as Tooltip } from "./Tooltip/Tooltip.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 TooltipWrapper } from "./Tooltip/TooltipWrapper.svelte"
export { default as Menu } from "./Menu/Menu.svelte" export { default as Menu } from "./Menu/Menu.svelte"
export { default as MenuSection } from "./Menu/Section.svelte" export { default as MenuSection } from "./Menu/Section.svelte"

View File

@ -127,8 +127,12 @@ export const selectedAutomation = derived(automationStore, $automationStore => {
export const userSelectedResourceMap = derived(userStore, $userStore => { export const userSelectedResourceMap = derived(userStore, $userStore => {
let map = {} let map = {}
$userStore.forEach(user => { $userStore.forEach(user => {
if (user.builderMetadata?.selectedResourceId) { const resource = user.builderMetadata?.selectedResourceId
map[user.builderMetadata?.selectedResourceId] = user if (resource) {
if (!map[resource]) {
map[resource] = []
}
map[resource].push(user)
} }
}) })
return map return map

View File

@ -248,4 +248,36 @@ const automationActions = store => ({
} }
await store.actions.save(newAutomation) 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
})
}
}
},
}) })

View File

@ -1,5 +1,10 @@
import { createWebsocket } from "@budibase/frontend-core" 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 { datasources, tables } from "stores/backend"
import { get } from "svelte/store" import { get } from "svelte/store"
import { auth } from "stores/portal" 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 return socket
} }

View File

@ -1,6 +1,10 @@
<script> <script>
import { onMount } from "svelte" import { onMount } from "svelte"
import { automationStore, selectedAutomation } from "builderStore" import {
automationStore,
selectedAutomation,
userSelectedResourceMap,
} from "builderStore"
import NavItem from "components/common/NavItem.svelte" import NavItem from "components/common/NavItem.svelte"
import EditAutomationPopover from "./EditAutomationPopover.svelte" import EditAutomationPopover from "./EditAutomationPopover.svelte"
import { notifications } from "@budibase/bbui" import { notifications } from "@budibase/bbui"
@ -21,13 +25,13 @@
</script> </script>
<div class="automations-list"> <div class="automations-list">
{#each $automationStore.automations.sort(aut => aut.name) as automation, idx} {#each $automationStore.automations.sort(aut => aut.name) as automation}
<NavItem <NavItem
border={idx > 0}
icon="ShareAndroid" icon="ShareAndroid"
text={automation.name} text={automation.name}
selected={automation._id === selectedAutomationId} selected={automation._id === selectedAutomationId}
on:click={() => selectAutomation(automation._id)} on:click={() => selectAutomation(automation._id)}
selectedBy={$userSelectedResourceMap[automation._id]}
> >
<EditAutomationPopover {automation} /> <EditAutomationPopover {automation} />
</NavItem> </NavItem>
@ -40,6 +44,5 @@
flex-direction: column; flex-direction: column;
justify-content: flex-start; justify-content: flex-start;
align-items: stretch; align-items: stretch;
margin: 0 calc(-1 * var(--spacing-xl));
} }
</style> </style>

View File

@ -11,8 +11,8 @@
<Panel title="Automations" borderRight> <Panel title="Automations" borderRight>
<Layout paddingX="L" paddingY="XL" gap="S"> <Layout paddingX="L" paddingY="XL" gap="S">
<Button cta on:click={modal.show}>Add automation</Button> <Button cta on:click={modal.show}>Add automation</Button>
<AutomationList />
</Layout> </Layout>
<AutomationList />
</Panel> </Panel>
<Modal bind:this={modal}> <Modal bind:this={modal}>

View File

@ -17,7 +17,7 @@
$: text = getText(filters) $: text = getText(filters)
const getText = filters => { const getText = filters => {
const count = filters?.length const count = filters?.filter(filter => filter.field)?.length
return count ? `Filter (${count})` : "Filter" return count ? `Filter (${count})` : "Filter"
} }
</script> </script>

View File

@ -10,9 +10,8 @@ export const createTableSelectionStore = (integration, datasource) => {
datasources.getTableNames(datasource).then(tableNames => { datasources.getTableNames(datasource).then(tableNames => {
tableNamesStore.set(tableNames) tableNamesStore.set(tableNames)
selectedTableNamesStore.set( selectedTableNamesStore.set(
tableNames.filter(tableName => datasource.entities[tableName]) tableNames.filter(tableName => datasource.entities?.[tableName])
) )
loadingStore.set(false) loadingStore.set(false)

View File

@ -2,21 +2,13 @@
import { goto, url } from "@roxi/routify" import { goto, url } from "@roxi/routify"
import { tables } from "stores/backend" import { tables } from "stores/backend"
import { notifications } from "@budibase/bbui" import { notifications } from "@budibase/bbui"
import { import { Input, Label, ModalContent, Layout } from "@budibase/bbui"
Input,
Label,
ModalContent,
Toggle,
Divider,
Layout,
} from "@budibase/bbui"
import { datasources } from "stores/backend" import { datasources } from "stores/backend"
import TableDataImport from "../TableDataImport.svelte" import TableDataImport from "../TableDataImport.svelte"
import { import {
BUDIBASE_INTERNAL_DB_ID, BUDIBASE_INTERNAL_DB_ID,
BUDIBASE_DATASOURCE_TYPE, BUDIBASE_DATASOURCE_TYPE,
} from "constants/backend" } from "constants/backend"
import { buildAutoColumn, getAutoColumnInformation } from "builderStore/utils"
$: tableNames = $tables.list.map(table => table.name) $: tableNames = $tables.list.map(table => table.name)
$: selectedSource = $datasources.list.find( $: selectedSource = $datasources.list.find(
@ -43,28 +35,12 @@
} }
let error = "" let error = ""
let autoColumns = getAutoColumnInformation()
let schema = {} let schema = {}
let rows = [] let rows = []
let allValid = true let allValid = true
let displayColumn = null 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) { function checkValid(evt) {
const tableName = evt.target.value const tableName = evt.target.value
if (tableNames.includes(tableName)) { if (tableNames.includes(tableName)) {
@ -77,7 +53,7 @@
async function saveTable() { async function saveTable() {
let newTable = { let newTable = {
name, name,
schema: { ...schema, ...getAutoColumns() }, schema: { ...schema },
rows, rows,
type: "internal", type: "internal",
sourceId: targetDatasourceId, sourceId: targetDatasourceId,
@ -118,21 +94,6 @@
bind:value={name} bind:value={name}
{error} {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> <div>
<Layout gap="XS" noPadding> <Layout gap="XS" noPadding>
<Label grey extraSmall <Label grey extraSmall
@ -148,24 +109,3 @@
</Layout> </Layout>
</div> </div>
</ModalContent> </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>

View File

@ -35,9 +35,8 @@
try { try {
const isSelected = const isSelected =
decodeURIComponent($params.viewName) === $views.selectedViewName decodeURIComponent($params.viewName) === $views.selectedViewName
const name = view.name
const id = view.tableId const id = view.tableId
await views.delete(name) await views.delete(view)
notifications.success("View deleted") notifications.success("View deleted")
if (isSelected) { if (isSelected) {
$goto(`./table/${id}`) $goto(`./table/${id}`)

View File

@ -7,6 +7,8 @@
closeBrackets, closeBrackets,
completionKeymap, completionKeymap,
closeBracketsKeymap, closeBracketsKeymap,
acceptCompletion,
completionStatus,
} from "@codemirror/autocomplete" } from "@codemirror/autocomplete"
import { import {
EditorView, EditorView,
@ -34,7 +36,8 @@
defaultKeymap, defaultKeymap,
historyKeymap, historyKeymap,
history, history,
indentWithTab, indentMore,
indentLess,
} from "@codemirror/commands" } from "@codemirror/commands"
import { Compartment } from "@codemirror/state" import { Compartment } from "@codemirror/state"
import { javascript } from "@codemirror/lang-javascript" import { javascript } from "@codemirror/lang-javascript"
@ -107,6 +110,22 @@
let isDark = !currentTheme.includes("light") let isDark = !currentTheme.includes("light")
let themeConfig = new Compartment() 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 buildKeymap = () => {
const baseMap = [ const baseMap = [
...closeBracketsKeymap, ...closeBracketsKeymap,
@ -114,7 +133,7 @@
...historyKeymap, ...historyKeymap,
...foldKeymap, ...foldKeymap,
...completionKeymap, ...completionKeymap,
indentWithTab, indentWithTabCustom,
] ]
return baseMap return baseMap
} }

View File

@ -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>

View File

@ -2,6 +2,7 @@
import { Icon } from "@budibase/bbui" import { Icon } from "@budibase/bbui"
import { createEventDispatcher, getContext } from "svelte" import { createEventDispatcher, getContext } from "svelte"
import { helpers } from "@budibase/shared-core" import { helpers } from "@budibase/shared-core"
import { UserAvatars } from "@budibase/frontend-core"
export let icon export let icon
export let withArrow = false export let withArrow = false
@ -98,21 +99,25 @@
<Icon color={iconColor} size="S" name={icon} /> <Icon color={iconColor} size="S" name={icon} />
</div> </div>
{/if} {/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} {#if withActions}
<div class="actions"> <div class="actions">
<slot /> <slot />
</div> </div>
{/if} {/if}
{#if $$slots.right} {#if $$slots.right}
<div class="right"> <div class="right">
<slot name="right" /> <slot name="right" />
</div> </div>
{/if} {/if}
</div> </div>
{#if selectedBy}
<div class="selected-by-label">{helpers.getUserLabel(selectedBy)}</div>
{/if}
</div> </div>
<style> <style>
@ -136,13 +141,16 @@
} }
.nav-item.highlighted { .nav-item.highlighted {
background-color: var(--spectrum-global-color-gray-200); background-color: var(--spectrum-global-color-gray-200);
--avatars-background: var(--spectrum-global-color-gray-200);
} }
.nav-item.selected { .nav-item.selected {
background-color: var(--spectrum-global-color-gray-300); background-color: var(--spectrum-global-color-gray-300);
--avatars-background: var(--spectrum-global-color-gray-300);
color: var(--ink); color: var(--ink);
} }
.nav-item:hover { .nav-item:hover {
background-color: var(--spectrum-global-color-gray-300); background-color: var(--spectrum-global-color-gray-300);
--avatars-background: var(--spectrum-global-color-gray-300);
} }
.nav-item:hover .actions { .nav-item:hover .actions {
visibility: visible; visibility: visible;
@ -159,37 +167,6 @@
padding-left: var(--spacing-l); 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 */ /* Needed to fully display the actions icon */
.nav-item.scrollable .nav-item-content { .nav-item.scrollable .nav-item-content {
padding-right: 1px; padding-right: 1px;
@ -245,6 +222,9 @@
color: var(--spectrum-global-color-gray-900); color: var(--spectrum-global-color-gray-900);
order: 2; order: 2;
width: 0; width: 0;
display: flex;
align-items: center;
gap: 8px;
} }
.scrollable .text { .scrollable .text {
flex: 0 0 auto; flex: 0 0 auto;

View File

@ -208,7 +208,9 @@
<div class="syntax-error"> <div class="syntax-error">
Current Handlebars syntax is invalid, please check the Current Handlebars syntax is invalid, please check the
guide guide
<a href="https://handlebarsjs.com/guide/">here</a> <a href="https://handlebarsjs.com/guide/" target="_blank"
>here</a
>
for more details. for more details.
</div> </div>
{:else} {:else}

View File

@ -10,6 +10,7 @@
Link, Link,
Modal, Modal,
StatusLight, StatusLight,
AbsTooltip,
} from "@budibase/bbui" } from "@budibase/bbui"
import RevertModal from "components/deploy/RevertModal.svelte" import RevertModal from "components/deploy/RevertModal.svelte"
import VersionModal from "components/deploy/VersionModal.svelte" import VersionModal from "components/deploy/VersionModal.svelte"
@ -250,15 +251,20 @@
<Link quiet on:click={unpublishApp}>Unpublish</Link> <Link quiet on:click={unpublishApp}>Unpublish</Link>
</span> </span>
<span class="revert-link"> <span class="revert-link">
<Link <AbsTooltip
disabled={!$isOnlyUser} text={$isOnlyUser
quiet ? null
secondary : "Unavailable - another user is editing this app"}
on:click={revertApp}
tooltip="Unavailable - another user is editing this app"
> >
Revert <Link
</Link> disabled={!$isOnlyUser}
quiet
secondary
on:click={revertApp}
>
Revert
</Link>
</AbsTooltip>
</span> </span>
{:else} {:else}
<span class="status-text unpublished">Not published</span> <span class="status-text unpublished">Not published</span>

View File

@ -16,6 +16,7 @@
makeStateBinding, makeStateBinding,
} from "builderStore/dataBinding" } from "builderStore/dataBinding"
import { currentAsset, store } from "builderStore" import { currentAsset, store } from "builderStore"
import { cloneDeep } from "lodash/fp"
const flipDurationMs = 150 const flipDurationMs = 150
const EVENT_TYPE_KEY = "##eventHandlerType" const EVENT_TYPE_KEY = "##eventHandlerType"
@ -29,6 +30,26 @@
let actionQuery let actionQuery
let selectedAction = actions?.length ? actions[0] : null 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 // Ensure parameters object is never null
if (selectedAction && !selectedAction.parameters) { if (selectedAction && !selectedAction.parameters) {
@ -125,8 +146,9 @@
actions = e.detail.items actions = e.detail.items
} }
const getAllBindings = (bindings, eventContextBindings, actions) => { const getAllBindings = (actionBindings, eventContextBindings, actions) => {
let allBindings = [] let allBindings = []
let cloneActionBindings = cloneDeep(actionBindings)
if (!actions) { if (!actions) {
return [] return []
} }
@ -144,11 +166,19 @@
.forEach(action => { .forEach(action => {
// Check we have a binding for this action, and generate one if not // Check we have a binding for this action, and generate one if not
const stateBinding = makeStateBinding(action.parameters.key) const stateBinding = makeStateBinding(action.parameters.key)
const hasKey = bindings.some(binding => { const hasKey = actionBindings.some(binding => {
return binding.runtimeBinding === stateBinding.runtimeBinding return binding.runtimeBinding === stateBinding.runtimeBinding
}) })
if (!hasKey) { 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 // Get which indexes are asynchronous automations as we want to filter them out from the bindings
@ -164,15 +194,16 @@
.filter(index => index !== undefined) .filter(index => index !== undefined)
// Based on the above, filter out the asynchronous automations from the bindings // Based on the above, filter out the asynchronous automations from the bindings
if (asynchronousAutomationIndexes) { let contextBindings = asynchronousAutomationIndexes
allBindings = eventContextBindings ? eventContextBindings.filter((binding, index) => {
.filter((binding, index) => {
return !asynchronousAutomationIndexes.includes(index) return !asynchronousAutomationIndexes.includes(index)
}) })
.concat(bindings) : eventContextBindings
} else {
allBindings = eventContextBindings.concat(bindings) allBindings = contextBindings
} .concat(cloneActionBindings)
.concat(allBindings)
return allBindings return allBindings
} }
</script> </script>

View File

@ -70,8 +70,9 @@
} set` } set`
</script> </script>
<div class="action-count">{actionText}</div> <div class="action-editor">
<ActionButton on:click={openDrawer}>Define actions</ActionButton> <ActionButton on:click={openDrawer}>{actionText}</ActionButton>
</div>
<Drawer bind:this={drawer} title={"Actions"}> <Drawer bind:this={drawer} title={"Actions"}>
<svelte:fragment slot="description"> <svelte:fragment slot="description">
@ -89,9 +90,7 @@
</Drawer> </Drawer>
<style> <style>
.action-count { .action-editor :global(.spectrum-ActionButton) {
padding-top: 6px; width: 100%;
padding-bottom: var(--spacing-s);
font-weight: 600;
} }
</style> </style>

View File

@ -20,6 +20,7 @@
let drawer let drawer
let boundValue let boundValue
$: text = getText(value)
$: datasource = getDatasourceForProvider($currentAsset, componentInstance) $: datasource = getDatasourceForProvider($currentAsset, componentInstance)
$: schema = getSchema($currentAsset, datasource) $: schema = getSchema($currentAsset, datasource)
$: options = allowCellEditing $: options = allowCellEditing
@ -31,6 +32,17 @@
allowLinks: true, 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 getSchema = (asset, datasource) => {
const schema = getSchemaForDatasource(asset, datasource).schema const schema = getSchemaForDatasource(asset, datasource).schema
@ -76,7 +88,7 @@
</script> </script>
<div class="column-editor"> <div class="column-editor">
<ActionButton on:click={open}>Configure columns</ActionButton> <ActionButton on:click={open}>{text}</ActionButton>
</div> </div>
<Drawer bind:this={drawer} title="Columns"> <Drawer bind:this={drawer} title="Columns">
<Button cta slot="buttons" on:click={save}>Save</Button> <Button cta slot="buttons" on:click={save}>Save</Button>

View File

@ -12,25 +12,36 @@
export let componentInstance export let componentInstance
export let value = [] export let value = []
const convertOldColumnFormat = oldColumns => {
if (typeof oldColumns?.[0] === "string") {
value = oldColumns.map(field => ({ name: field, displayName: field }))
}
}
$: convertOldColumnFormat(value)
const dispatch = createEventDispatcher() const dispatch = createEventDispatcher()
let drawer let drawer
let boundValue let boundValue
$: text = getText(value)
$: convertOldColumnFormat(value)
$: datasource = getDatasourceForProvider($currentAsset, componentInstance) $: datasource = getDatasourceForProvider($currentAsset, componentInstance)
$: schema = getSchema($currentAsset, datasource) $: schema = getSchema($currentAsset, datasource)
$: options = Object.keys(schema || {}) $: options = Object.keys(schema || {})
$: sanitisedValue = getValidColumns(value, options) $: sanitisedValue = getValidColumns(value, options)
$: updateBoundValue(sanitisedValue) $: 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 getSchema = (asset, datasource) => {
const schema = getSchemaForDatasource(asset, datasource).schema const schema = getSchemaForDatasource(asset, datasource).schema
@ -75,7 +86,10 @@
} }
</script> </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"> <Drawer bind:this={drawer} title="Form Fields">
<svelte:fragment slot="description"> <svelte:fragment slot="description">
Configure the fields in your form. Configure the fields in your form.
@ -83,3 +97,9 @@
<Button cta slot="buttons" on:click={save}>Save</Button> <Button cta slot="buttons" on:click={save}>Save</Button>
<ColumnDrawer slot="body" bind:columns={boundValue} {options} {schema} /> <ColumnDrawer slot="body" bind:columns={boundValue} {options} {schema} />
</Drawer> </Drawer>
<style>
.field-configuration :global(.spectrum-ActionButton) {
width: 100%;
}
</style>

View File

@ -20,7 +20,7 @@
$: datasource = getDatasourceForProvider($currentAsset, componentInstance) $: datasource = getDatasourceForProvider($currentAsset, componentInstance)
$: schema = getSchemaForDatasource($currentAsset, datasource)?.schema $: schema = getSchemaForDatasource($currentAsset, datasource)?.schema
$: schemaFields = Object.values(schema || {}) $: schemaFields = Object.values(schema || {})
$: text = getText(value) $: text = getText(value?.filter(filter => filter.field))
async function saveFilter() { async function saveFilter() {
dispatch("change", tempValue) dispatch("change", tempValue)

View File

@ -1,11 +1,8 @@
<script> <script>
import { Tooltip } from "@budibase/bbui"
export let text export let text
export let url export let url
export let active = false export let active = false
export let disabled = false export let disabled = false
export let tooltip = null
</script> </script>
<div class="side-nav-item"> <div class="side-nav-item">
@ -18,11 +15,6 @@
{text || ""} {text || ""}
</div> </div>
{/if} {/if}
{#if tooltip}
<div class="tooltip">
<Tooltip textWrapping direction="right" text={tooltip} />
</div>
{/if}
</div> </div>
<style> <style>
@ -45,17 +37,4 @@
pointer-events: none; pointer-events: none;
color: var(--spectrum-global-color-gray-500) !important; 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> </style>

View File

@ -2,12 +2,12 @@
import { Heading, Body, Button, Icon } from "@budibase/bbui" import { Heading, Body, Button, Icon } from "@budibase/bbui"
import { processStringSync } from "@budibase/string-templates" import { processStringSync } from "@budibase/string-templates"
import { goto } from "@roxi/routify" import { goto } from "@roxi/routify"
import { UserAvatar } from "@budibase/frontend-core" import { UserAvatars } from "@budibase/frontend-core"
export let app export let app
export let lockedAction export let lockedAction
$: editing = app?.lockedBy != null $: editing = app.sessions?.length
const handleDefaultClick = () => { const handleDefaultClick = () => {
if (window.innerWidth < 640) { if (window.innerWidth < 640) {
@ -41,7 +41,7 @@
<div class="updated"> <div class="updated">
{#if editing} {#if editing}
Currently editing Currently editing
<UserAvatar user={app.lockedBy} /> <UserAvatars users={app.sessions} />
{:else if app.updatedAt} {:else if app.updatedAt}
{processStringSync("Updated {{ duration time 'millisecond' }} ago", { {processStringSync("Updated {{ duration time 'millisecond' }} ago", {
time: new Date().getTime() - new Date(app.updatedAt).getTime(), time: new Date().getTime() - new Date(app.updatedAt).getTime(),

View File

@ -15,8 +15,6 @@
import { createValidationStore } from "helpers/validation/yup" import { createValidationStore } from "helpers/validation/yup"
import * as appValidation from "helpers/validation/yup/app" import * as appValidation from "helpers/validation/yup/app"
import TemplateCard from "components/common/TemplateCard.svelte" import TemplateCard from "components/common/TemplateCard.svelte"
import createFromScratchScreen from "builderStore/store/screenTemplates/createFromScratchScreen"
import { Roles } from "constants/backend"
import { lowercase } from "helpers" import { lowercase } from "helpers"
export let template export let template
@ -142,21 +140,6 @@
// Create user // Create user
await auth.setInitInfo({}) 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}`) $goto(`/builder/app/${createdApp.instance._id}`)
} catch (error) { } catch (error) {
creating = false creating = false

View File

@ -15,6 +15,7 @@
} }
onMount(() => { onMount(() => {
window.isBuilder = true
window.closePreview = () => { window.closePreview = () => {
store.update(state => ({ store.update(state => ({
...state, ...state,

View File

@ -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>

View File

@ -15,6 +15,7 @@
Heading, Heading,
Modal, Modal,
notifications, notifications,
TooltipPosition,
} from "@budibase/bbui" } from "@budibase/bbui"
import AppActions from "components/deploy/AppActions.svelte" import AppActions from "components/deploy/AppActions.svelte"
import { API } from "api" import { API } from "api"
@ -25,7 +26,7 @@
import TourWrap from "components/portal/onboarding/TourWrap.svelte" import TourWrap from "components/portal/onboarding/TourWrap.svelte"
import TourPopover from "components/portal/onboarding/TourPopover.svelte" import TourPopover from "components/portal/onboarding/TourPopover.svelte"
import BuilderSidePanel from "./_components/BuilderSidePanel.svelte" import BuilderSidePanel from "./_components/BuilderSidePanel.svelte"
import UserAvatars from "./_components/UserAvatars.svelte" import { UserAvatars } from "@budibase/frontend-core"
import { TOUR_KEYS, TOURS } from "components/portal/onboarding/tours.js" import { TOUR_KEYS, TOURS } from "components/portal/onboarding/tours.js"
import PreviewOverlay from "./_components/PreviewOverlay.svelte" import PreviewOverlay from "./_components/PreviewOverlay.svelte"
@ -172,7 +173,11 @@
</div> </div>
<div class="toprightnav"> <div class="toprightnav">
<span> <span>
<UserAvatars users={$userStore} /> <UserAvatars
users={$userStore}
order="rtl"
tooltipPosition={TooltipPosition.Bottom}
/>
</span> </span>
<AppActions {application} {loaded} /> <AppActions {application} {loaded} />
</div> </div>
@ -228,7 +233,7 @@
.top-nav { .top-nav {
flex: 0 0 60px; flex: 0 0 60px;
background: var(--background); background: var(--background);
padding: 0 var(--spacing-xl); padding-left: var(--spacing-xl);
display: grid; display: grid;
grid-template-columns: 1fr auto 1fr; grid-template-columns: 1fr auto 1fr;
flex-direction: row; flex-direction: row;

View File

@ -8,6 +8,10 @@
import { onDestroy, onMount } from "svelte" import { onDestroy, onMount } from "svelte"
import { syncURLToState } from "helpers/urlStateSync" import { syncURLToState } from "helpers/urlStateSync"
import * as routify from "@roxi/routify" 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 // Keep URL and state in sync for selected screen ID
const stopSyncing = syncURLToState({ const stopSyncing = syncURLToState({

View File

@ -2,11 +2,12 @@
import { Button, Modal } from "@budibase/bbui" import { Button, Modal } from "@budibase/bbui"
import ImportQueriesModal from "./RestImportQueriesModal.svelte" import ImportQueriesModal from "./RestImportQueriesModal.svelte"
export let datasourceId
let importQueriesModal let importQueriesModal
</script> </script>
<Modal bind:this={importQueriesModal}> <Modal bind:this={importQueriesModal}>
<ImportQueriesModal createDatasource={false} datasourceId={"todo"} /> <ImportQueriesModal createDatasource={false} {datasourceId} />
</Modal> </Modal>
<div class="button"> <div class="button">

View File

@ -7,14 +7,14 @@
} from "stores/backend" } from "stores/backend"
import { hasData } from "stores/selectors" 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 { params, goto } from "@roxi/routify"
import CreateExternalDatasourceModal from "./_components/CreateExternalDatasourceModal/index.svelte" import CreateExternalDatasourceModal from "./_components/CreateExternalDatasourceModal/index.svelte"
import CreateInternalTableModal from "./_components/CreateInternalTableModal.svelte" import CreateInternalTableModal from "./_components/CreateInternalTableModal.svelte"
import DatasourceOption from "./_components/DatasourceOption.svelte" import DatasourceOption from "./_components/DatasourceOption.svelte"
import IntegrationIcon from "components/backend/DatasourceNavigator/IntegrationIcon.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 ICONS from "components/backend/DatasourceNavigator/icons/index.js"
import FontAwesomeIcon from "components/common/FontAwesomeIcon.svelte"
let internalTableModal let internalTableModal
let externalDatasourceModal let externalDatasourceModal
@ -46,25 +46,16 @@
bind:this={externalDatasourceModal} bind:this={externalDatasourceModal}
/> />
<div class="page"> <CreationPage
<div class="closeButton"> showClose={hasData($datasources, $tables)}
{#if hasData($datasources, $tables)} onClose={() => $goto("./table")}
<Icon hoverable name="Close" on:click={$goto("./table")} /> heading="Add new data source"
{/if} >
</div>
<div class="heading">
<Heading weight="light">Add new data source</Heading>
</div>
<div class="subHeading"> <div class="subHeading">
<Body>Get started with our Budibase DB</Body> <Body>Get started with our Budibase DB</Body>
<div <AbsTooltip text="Budibase DB is built with CouchDB">
role="tooltip" <Icon name="Info" size="S" />
title="Budibase DB is built with CouchDB" </AbsTooltip>
class="tooltip"
>
<FontAwesomeIcon name="fa-solid fa-circle-info" />
</div>
</div> </div>
<div class="options"> <div class="options">
@ -113,37 +104,19 @@
</DatasourceOption> </DatasourceOption>
{/each} {/each}
</div> </div>
</div> </CreationPage>
<style> <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 { .subHeading {
display: flex; display: flex;
align-items: center; align-items: center;
margin-bottom: 24px; margin-top: 12px;
margin-bottom: 36px;
gap: 8px;
} }
.subHeading :global(p) {
.tooltip { color: var(--spectrum-global-color-gray-600) !important;
margin-left: 6px;
} }
.options { .options {
width: 100%; width: 100%;
display: grid; display: grid;

View File

@ -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>

View File

@ -9,7 +9,7 @@
Helpers, Helpers,
notifications, notifications,
} from "@budibase/bbui" } from "@budibase/bbui"
import ScreenDetailsModal from "./ScreenDetailsModal.svelte" import ScreenDetailsModal from "components/design/ScreenDetailsModal.svelte"
import sanitizeUrl from "builderStore/store/screenTemplates/utils/sanitizeUrl" import sanitizeUrl from "builderStore/store/screenTemplates/utils/sanitizeUrl"
import { makeComponentUnique } from "builderStore/componentUtils" import { makeComponentUnique } from "builderStore/componentUtils"

View File

@ -1,17 +1,16 @@
<script> <script>
import { Search, Layout, Select, Body, Button } from "@budibase/bbui" import { Search, Layout, Select, Body, Button } from "@budibase/bbui"
import Panel from "components/design/Panel.svelte" import Panel from "components/design/Panel.svelte"
import { goto } from "@roxi/routify"
import { roles } from "stores/backend" import { roles } from "stores/backend"
import { store, sortedScreens, userSelectedResourceMap } from "builderStore" import { store, sortedScreens, userSelectedResourceMap } from "builderStore"
import NavItem from "components/common/NavItem.svelte" import NavItem from "components/common/NavItem.svelte"
import ScreenDropdownMenu from "./ScreenDropdownMenu.svelte" import ScreenDropdownMenu from "./ScreenDropdownMenu.svelte"
import ScreenWizard from "./ScreenWizard.svelte"
import RoleIndicator from "./RoleIndicator.svelte" import RoleIndicator from "./RoleIndicator.svelte"
import { RoleUtils } from "@budibase/frontend-core" import { RoleUtils } from "@budibase/frontend-core"
let searchString let searchString
let accessRole = "all" let accessRole = "all"
let showNewScreenModal
$: filteredScreens = getFilteredScreens( $: filteredScreens = getFilteredScreens(
$sortedScreens, $sortedScreens,
@ -31,7 +30,7 @@
<Panel title="Screens" borderRight> <Panel title="Screens" borderRight>
<Layout paddingX="L" paddingY="XL" gap="S"> <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 <Search
placeholder="Search" placeholder="Search"
value={searchString} value={searchString}
@ -74,5 +73,3 @@
</Layout> </Layout>
{/if} {/if}
</Panel> </Panel>
<ScreenWizard bind:showModal={showNewScreenModal} />

View File

@ -1,6 +1,5 @@
<script> <script>
import ScreenDetailsModal from "./ScreenDetailsModal.svelte" import ScreenDetailsModal from "components/design/ScreenDetailsModal.svelte"
import NewScreenModal from "./NewScreenModal.svelte"
import DatasourceModal from "./DatasourceModal.svelte" import DatasourceModal from "./DatasourceModal.svelte"
import ScreenRoleModal from "./ScreenRoleModal.svelte" import ScreenRoleModal from "./ScreenRoleModal.svelte"
import sanitizeUrl from "builderStore/store/screenTemplates/utils/sanitizeUrl" import sanitizeUrl from "builderStore/store/screenTemplates/utils/sanitizeUrl"
@ -11,11 +10,11 @@
import { tables } from "stores/backend" import { tables } from "stores/backend"
import { Roles } from "constants/backend" import { Roles } from "constants/backend"
import { capitalise } from "helpers" import { capitalise } from "helpers"
import { goto } from "@roxi/routify"
let pendingScreen let pendingScreen
// Modal refs // Modal refs
let newScreenModal
let screenDetailsModal let screenDetailsModal
let datasourceModal let datasourceModal
let screenAccessRoleModal let screenAccessRoleModal
@ -26,16 +25,6 @@
let blankScreenUrl = null let blankScreenUrl = null
let screenMode = 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 // Creates an array of screens, checking and sanitising their URLs
const createScreens = async ({ screens, screenAccessRole }) => { const createScreens = async ({ screens, screenAccessRole }) => {
if (!screens?.length) { if (!screens?.length) {
@ -43,6 +32,8 @@
} }
try { try {
let screenId
for (let screen of screens) { for (let screen of screens) {
// Check we aren't clashing with an existing URL // Check we aren't clashing with an existing URL
if (hasExistingUrl(screen.routing.route)) { if (hasExistingUrl(screen.routing.route)) {
@ -64,7 +55,8 @@
screen.routing.roleId = screenAccessRole screen.routing.roleId = screenAccessRole
// Create the screen // 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 // Add link in layout for list screens
if (screen.props._instanceName.endsWith("List")) { if (screen.props._instanceName.endsWith("List")) {
@ -74,7 +66,10 @@
) )
} }
} }
$goto(`./${screenId}`)
} catch (error) { } catch (error) {
console.log(error)
notifications.error("Error creating screens") notifications.error("Error creating screens")
} }
} }
@ -104,18 +99,24 @@
} }
// Handler for NewScreenModal // Handler for NewScreenModal
const confirmScreenSelection = async mode => { export const show = mode => {
selectedTemplates = null
blankScreenUrl = null
screenMode = mode screenMode = mode
pendingScreen = null
screenAccessRole = Roles.BASIC
if (mode === "autoCreate") { if (mode === "table") {
datasourceModal.show() datasourceModal.show()
} else { } else if (mode === "blank") {
let templates = getTemplates($store, $tables.list) let templates = getTemplates($store, $tables.list)
const blankScreenTemplate = templates.find( const blankScreenTemplate = templates.find(
t => t.id === "createFromScratch" t => t.id === "createFromScratch"
) )
pendingScreen = blankScreenTemplate.create() pendingScreen = blankScreenTemplate.create()
screenDetailsModal.show() screenDetailsModal.show()
} else {
throw new Error("Invalid mode provided")
} }
} }
@ -155,7 +156,7 @@
// Submit screen config for creation. // Submit screen config for creation.
const confirmScreenCreation = async () => { const confirmScreenCreation = async () => {
if (screenMode === "blankScreen") { if (screenMode === "blank") {
confirmBlankScreenCreation({ confirmBlankScreenCreation({
screenUrl: blankScreenUrl, screenUrl: blankScreenUrl,
screenAccessRole, screenAccessRole,
@ -166,7 +167,7 @@
} }
const roleSelectBack = () => { const roleSelectBack = () => {
if (screenMode === "blankScreen") { if (screenMode === "blank") {
screenDetailsModal.show() screenDetailsModal.show()
} else { } else {
datasourceModal.show() datasourceModal.show()
@ -174,14 +175,9 @@
} }
</script> </script>
<Modal bind:this={newScreenModal}>
<NewScreenModal onConfirm={confirmScreenSelection} />
</Modal>
<Modal bind:this={datasourceModal}> <Modal bind:this={datasourceModal}>
<DatasourceModal <DatasourceModal
onConfirm={confirmScreenDatasources} onConfirm={confirmScreenDatasources}
onCancel={() => newScreenModal.show()}
initalScreens={!selectedTemplates ? [] : [...selectedTemplates]} initalScreens={!selectedTemplates ? [] : [...selectedTemplates]}
/> />
</Modal> </Modal>
@ -198,7 +194,6 @@
<Modal bind:this={screenDetailsModal}> <Modal bind:this={screenDetailsModal}>
<ScreenDetailsModal <ScreenDetailsModal
onConfirm={confirmScreenBlank} onConfirm={confirmScreenBlank}
onCancel={() => newScreenModal.show()}
initialUrl={blankScreenUrl} initialUrl={blankScreenUrl}
/> />
</Modal> </Modal>

View File

@ -40,14 +40,14 @@
</script> </script>
<ModalContent <ModalContent
title="Autogenerated screens" title="Access"
confirmText="Done" confirmText="Done"
cancelText="Back" cancelText="Back"
{onConfirm} {onConfirm}
{onCancel} {onCancel}
disabled={!!error} disabled={!!error}
> >
Select which level of access you want your screens to have Select the level of access required to see these screens
<Select <Select
bind:value={screenAccessRole} bind:value={screenAccessRole}
on:change={onChangeRole} on:change={onChangeRole}

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

View File

@ -1,67 +1,12 @@
<script> <script>
import { store, selectedScreen } from "builderStore"
import { onMount } from "svelte"
import { redirect } from "@roxi/routify" import { redirect } from "@roxi/routify"
import { Layout, Button, Detail, notifications } from "@budibase/bbui" import { store as frontendStore } from "builderStore"
import Logo from "assets/bb-space-man.svg"
import createFromScratchScreen from "builderStore/store/screenTemplates/createFromScratchScreen"
import { Roles } from "constants/backend"
let loaded = false $: {
if ($frontendStore.screens.length > 0) {
const createFirstScreen = async () => { $redirect(`./${$frontendStore.screens[0]._id}`)
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}`)
} else { } else {
loaded = true $redirect("./new")
} }
}) }
</script> </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">LETS 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>

View File

@ -0,0 +1,104 @@
<script>
import { Body } from "@budibase/bbui"
import CreationPage from "components/common/CreationPage.svelte"
import blankImage from "./blank.png"
import tableImage from "./table.png"
import CreateScreenModal from "./_components/CreateScreenModal.svelte"
import { store } from "builderStore"
import { goto } from "@roxi/routify"
let createScreenModal
$: hasScreens = $store.screens?.length
</script>
<div class="page">
<CreationPage
showClose={$store.screens.length > 0}
onClose={() => $goto(`./${$store.screens[0]._id}`)}
heading={hasScreens ? "Create new screen" : "Create your first screen"}
>
<div class="subHeading">
<Body>Start from scratch or create screens from your data</Body>
</div>
<div class="cards">
<div class="card" on:click={() => createScreenModal.show("blank")}>
<div class="image">
<img alt="" src={blankImage} />
</div>
<div class="text">
<Body size="S">Blank screen</Body>
<Body size="XS">Add an empty blank screen</Body>
</div>
</div>
<div class="card" on:click={() => createScreenModal.show("table")}>
<div class="image">
<img alt="" src={tableImage} />
</div>
<div class="text">
<Body size="S">Table</Body>
<Body size="XS">View, edit and delete rows on a table</Body>
</div>
</div>
</div>
</CreationPage>
</div>
<CreateScreenModal bind:this={createScreenModal} />
<style>
.page {
padding: 28px 40px 40px 40px;
}
.subHeading :global(p) {
text-align: center;
margin-top: 12px;
margin-bottom: 36px;
color: var(--spectrum-global-color-gray-600);
}
.cards {
display: flex;
flex-wrap: wrap;
justify-content: center;
gap: 24px;
}
.card {
max-width: 235px;
transition: filter 150ms;
}
.card:hover {
filter: brightness(1.1);
cursor: pointer;
}
.image {
border-radius: 4px 4px 0 0;
width: 100%;
max-height: 127px;
overflow: hidden;
}
.image img {
width: 100%;
}
.text {
border: 1px solid var(--grey-4);
border-radius: 0 0 4px 4px;
padding: 8px 16px 13px 16px;
}
.text :global(p:nth-child(1)) {
margin-bottom: 6px;
}
.text :global(p:nth-child(2)) {
color: var(--grey-6);
}
</style>

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

View File

@ -1,6 +1,6 @@
<script> <script>
import { Content, SideNav, SideNavItem } from "components/portal/page" import { Content, SideNav, SideNavItem } from "components/portal/page"
import { Page, Layout } from "@budibase/bbui" import { Page, Layout, AbsTooltip, TooltipPosition } from "@budibase/bbui"
import { url, isActive } from "@roxi/routify" import { url, isActive } from "@roxi/routify"
import DeleteModal from "components/deploy/DeleteModal.svelte" import DeleteModal from "components/deploy/DeleteModal.svelte"
import { isOnlyUser } from "builderStore" import { isOnlyUser } from "builderStore"
@ -45,16 +45,20 @@
active={$isActive("./version")} active={$isActive("./version")}
/> />
<div class="delete-action"> <div class="delete-action">
<SideNavItem <AbsTooltip
text="Delete app" position={TooltipPosition.Bottom}
on:click={() => { text={$isOnlyUser
deleteModal.show()
}}
disabled={!$isOnlyUser}
tooltip={$isOnlyUser
? null ? null
: "Unavailable - another user is editing this app"} : "Unavailable - another user is editing this app"}
/> >
<SideNavItem
text="Delete app"
disabled={!$isOnlyUser}
on:click={() => {
deleteModal.show()
}}
/>
</AbsTooltip>
</div> </div>
</SideNav> </SideNav>
<slot /> <slot />

View File

@ -6,6 +6,8 @@
Heading, Heading,
Body, Body,
Modal, Modal,
AbsTooltip,
TooltipPosition,
} from "@budibase/bbui" } from "@budibase/bbui"
import ConfirmDialog from "components/common/ConfirmDialog.svelte" import ConfirmDialog from "components/common/ConfirmDialog.svelte"
import CreateRestoreModal from "./CreateRestoreModal.svelte" import CreateRestoreModal from "./CreateRestoreModal.svelte"
@ -46,16 +48,18 @@
</div> </div>
{#if row.type !== "restore"} {#if row.type !== "restore"}
<MenuItem <AbsTooltip
on:click={restoreDialog.show} position={TooltipPosition.Left}
icon="Revert" text="Unavailable - another user is editing this app"
disabled={!$isOnlyUser}
tooltip={$isOnlyUser
? null
: "Unavailable - another user is editing this app"}
> >
Restore <MenuItem
</MenuItem> on:click={restoreDialog.show}
icon="Revert"
disabled={!$isOnlyUser}
>
Restore
</MenuItem>
</AbsTooltip>
<MenuItem on:click={deleteDialog.show} icon="Delete">Delete</MenuItem> <MenuItem on:click={deleteDialog.show} icon="Delete">Delete</MenuItem>
<MenuItem on:click={downloadExport} icon="Download">Download</MenuItem> <MenuItem on:click={downloadExport} icon="Download">Download</MenuItem>
{/if} {/if}

View File

@ -0,0 +1,40 @@
<script>
import { Layout, Body, Button } from "@budibase/bbui"
import { downloadStream } from "@budibase/frontend-core"
import Spinner from "components/common/Spinner.svelte"
import { API } from "api"
let loading = false
async function download() {
loading = true
try {
await downloadStream(await API.getSystemLogs())
} finally {
loading = false
}
}
</script>
<Layout noPadding>
<Body>Download your latest logs to share with the Budibase team</Body>
<div class="download-button">
<Button cta on:click={download} disabled={loading}>
<div class="button-content">
{#if loading}
<Spinner size="10" />
{/if}
Download system logs
</div>
</Button>
</div>
</Layout>
<style>
.button-content {
display: flex;
align-items: center;
gap: var(--spacing-m);
}
</style>

View File

@ -7,8 +7,6 @@
import { API } from "api" import { API } from "api"
import { store, automationStore } from "builderStore" import { store, automationStore } from "builderStore"
import { auth, admin } from "stores/portal" import { auth, admin } from "stores/portal"
import createFromScratchScreen from "builderStore/store/screenTemplates/createFromScratchScreen"
import { Roles } from "constants/backend"
let name = "My first app" let name = "My first app"
let url = "my-first-app" let url = "my-first-app"
@ -38,11 +36,6 @@
// Create user // Create user
await auth.setInitInfo({}) await auth.setInitInfo({})
let defaultScreenTemplate = createFromScratchScreen.create()
defaultScreenTemplate.routing.route = "/home"
defaultScreenTemplate.routing.roldId = Roles.BASIC
await store.actions.screens.save(defaultScreenTemplate)
appId = createdApp.instance._id appId = createdApp.instance._id
return createdApp return createdApp
} }

View File

@ -0,0 +1,84 @@
<script>
import {
Layout,
Heading,
Body,
Helpers,
Divider,
notifications,
Icon,
TextArea,
} from "@budibase/bbui"
import { auth, admin } from "stores/portal"
import { redirect } from "@roxi/routify"
import { API } from "api"
import { onMount } from "svelte"
let diagnosticInfo = ""
// Make sure page can't be visited directly in cloud
$: {
if ($admin.cloud) {
$redirect("../../portal")
}
}
async function fetchSystemDebugInfo() {
const diagnostics = await API.fetchSystemDebugInfo()
diagnosticInfo = {
browser: {
language: navigator.language || navigator.userLanguage,
userAgent: navigator.userAgent,
platform: navigator.platform,
vendor: navigator.vendor,
},
server: diagnostics,
}
}
const copyToClipboard = async () => {
await Helpers.copyToClipboard(JSON.stringify(diagnosticInfo, undefined, 2))
notifications.success("Copied")
}
onMount(async () => {
await fetchSystemDebugInfo()
})
</script>
{#if $auth.isAdmin && diagnosticInfo}
<Layout noPadding>
<Layout gap="XS">
<Heading size="M">Diagnostics</Heading>
Please include this diagnostic information in support requests and github issues
by clicking the button on the top right to copy to clipboard.
<Divider />
<Body size="M">
<section>
<div on:click={copyToClipboard} class="copy-icon">
<Icon name="Copy" size="M" />
</div>
<TextArea
height="45vh"
disabled
value={JSON.stringify(diagnosticInfo, undefined, 2)}
/>
</section>
</Body>
</Layout>
</Layout>
{/if}
<style>
section {
position: relative;
}
.copy-icon {
z-index: 1;
position: absolute;
top: 10px;
right: 10px;
cursor: pointer;
}
</style>

View File

@ -26,14 +26,12 @@ export function createViewsStore() {
} }
const deleteView = async view => { const deleteView = async view => {
await API.deleteView(view) await API.deleteView(view.name)
// Update tables // Update tables
tables.update(state => { tables.update(state => {
const table = state.list.find(table => table._id === view.tableId) const table = state.list.find(table => table._id === view.tableId)
if (table) { delete table.views[view.name]
delete table.views[view.name]
}
return { ...state } return { ...state }
}) })
} }

View File

@ -64,6 +64,10 @@ export const menu = derived([admin, auth], ([$admin, $auth]) => {
title: "Version", title: "Version",
href: "/builder/portal/settings/version", href: "/builder/portal/settings/version",
}) })
settingsSubPages.push({
title: "Diagnostics",
href: "/builder/portal/settings/diagnostics",
})
} }
menu.push({ menu.push({
title: "Settings", title: "Settings",
@ -85,6 +89,13 @@ export const menu = derived([admin, auth], ([$admin, $auth]) => {
title: "Audit Logs", title: "Audit Logs",
href: "/builder/portal/account/auditLogs", href: "/builder/portal/account/auditLogs",
}) })
if (!$admin.cloud) {
accountSubPages.push({
title: "System Logs",
href: "/builder/portal/account/systemLogs",
})
}
} }
if ($admin.cloud && $auth?.user?.accountPortalAccess) { if ($admin.cloud && $auth?.user?.accountPortalAccess) {
accountSubPages.push({ accountSubPages.push({

View File

@ -3223,6 +3223,46 @@
"key": "allowManualEntry", "key": "allowManualEntry",
"defaultValue": false "defaultValue": false
}, },
{
"type": "boolean",
"label": "Play sound on scan",
"key": "beepOnScan",
"defaultValue": false
},
{
"type": "select",
"label": "Sound pitch",
"key": "beepFrequency",
"dependsOn": "beepOnScan",
"defaultValue": 2637,
"options": [
{
"label": "Low",
"value": 2096
},
{
"label": "Regular",
"value": 2637
},
{
"label": "High",
"value": 3136
},
{ "label": "Custom", "value": "custom" }
]
},
{
"type": "number",
"label": "Sound frequency (Hz)",
"key": "customFrequency",
"defaultValue": 1046,
"min": 20,
"max": 8000,
"dependsOn": {
"setting": "beepFrequency",
"value": "custom"
}
},
{ {
"type": "validation/string", "type": "validation/string",
"label": "Validation", "label": "Validation",
@ -4541,6 +4581,16 @@
"setting": "clickBehaviour", "setting": "clickBehaviour",
"value": "details" "value": "details"
} }
},
{
"type": "boolean",
"label": "Hide notifications",
"key": "notificationOverride",
"defaultValue": false,
"dependsOn": {
"setting": "clickBehaviour",
"value": "details"
}
} }
] ]
}, },
@ -5178,6 +5228,16 @@
"value": "View", "value": "View",
"invert": true "invert": true
} }
},
{
"type": "boolean",
"label": "Hide notifications",
"key": "notificationOverride",
"defaultValue": false,
"dependsOn": {
"setting": "showSaveButton",
"value": true
}
} }
] ]
} }

View File

@ -30,6 +30,7 @@
export let sidePanelShowDelete export let sidePanelShowDelete
export let sidePanelSaveLabel export let sidePanelSaveLabel
export let sidePanelDeleteLabel export let sidePanelDeleteLabel
export let notificationOverride
const { fetchDatasourceSchema, API } = getContext("sdk") const { fetchDatasourceSchema, API } = getContext("sdk")
const stateKey = `ID_${generate()}` const stateKey = `ID_${generate()}`
@ -253,6 +254,7 @@
fields: sidePanelFields || normalFields, fields: sidePanelFields || normalFields,
title: editTitle, title: editTitle,
labelPosition: "left", labelPosition: "left",
notificationOverride,
}} }}
/> />
</BlockComponent> </BlockComponent>
@ -277,6 +279,7 @@
fields: sidePanelFields || normalFields, fields: sidePanelFields || normalFields,
title: "Create Row", title: "Create Row",
labelPosition: "left", labelPosition: "left",
notificationOverride,
}} }}
/> />
</BlockComponent> </BlockComponent>

View File

@ -19,6 +19,7 @@
export let rowId export let rowId
export let actionUrl export let actionUrl
export let noRowsMessage export let noRowsMessage
export let notificationOverride
const { fetchDatasourceSchema } = getContext("sdk") const { fetchDatasourceSchema } = getContext("sdk")
@ -87,6 +88,7 @@
showDeleteButton, showDeleteButton,
schema, schema,
repeaterId, repeaterId,
notificationOverride,
} }
const fetchSchema = async () => { const fetchSchema = async () => {

View File

@ -17,6 +17,7 @@
export let showDeleteButton export let showDeleteButton
export let schema export let schema
export let repeaterId export let repeaterId
export let notificationOverride
const FieldTypeToComponentMap = { const FieldTypeToComponentMap = {
string: "stringfield", string: "stringfield",
@ -47,6 +48,7 @@
parameters: { parameters: {
providerId: formId, providerId: formId,
tableId: dataSource?.tableId, tableId: dataSource?.tableId,
notificationOverride,
}, },
}, },
{ {

View File

@ -8,6 +8,10 @@
export let disabled = false export let disabled = false
export let allowManualEntry = false export let allowManualEntry = false
export let scanButtonText = "Scan code" export let scanButtonText = "Scan code"
export let beepOnScan = false
export let beepFrequency = 2637
export let customFrequency = 1046
const dispatch = createEventDispatcher() const dispatch = createEventDispatcher()
let videoEle let videoEle
@ -21,8 +25,13 @@
fps: 25, fps: 25,
qrbox: { width: 250, height: 250 }, qrbox: { width: 250, height: 250 },
} }
const audioCtx = new (window.AudioContext || window.webkitAudioContext)()
const onScanSuccess = decodedText => { const onScanSuccess = decodedText => {
if (value != decodedText) { if (value != decodedText) {
if (beepOnScan) {
beep()
}
dispatch("change", decodedText) dispatch("change", decodedText)
} }
} }
@ -84,6 +93,27 @@
} }
camModal.hide() camModal.hide()
} }
const beep = () => {
const oscillator = audioCtx.createOscillator()
const gainNode = audioCtx.createGain()
oscillator.connect(gainNode)
gainNode.connect(audioCtx.destination)
const frequency =
beepFrequency === "custom" ? customFrequency : beepFrequency
oscillator.frequency.value = frequency
oscillator.type = "square"
const duration = 420
const endTime = audioCtx.currentTime + duration / 1000
gainNode.gain.setValueAtTime(1, audioCtx.currentTime)
gainNode.gain.exponentialRampToValueAtTime(0.001, endTime)
oscillator.start()
oscillator.stop(endTime)
}
</script> </script>
<div class="scanner-video-wrapper"> <div class="scanner-video-wrapper">

View File

@ -11,6 +11,9 @@
export let onChange export let onChange
export let allowManualEntry export let allowManualEntry
export let scanButtonText export let scanButtonText
export let beepOnScan
export let beepFrequency
export let customFrequency
let fieldState let fieldState
let fieldApi let fieldApi
@ -42,6 +45,9 @@
disabled={fieldState.disabled} disabled={fieldState.disabled}
{allowManualEntry} {allowManualEntry}
scanButtonText={scanText} scanButtonText={scanText}
{beepOnScan}
{beepFrequency}
{customFrequency}
/> />
{/if} {/if}
</Field> </Field>

View File

@ -1,6 +1,6 @@
<script> <script>
import { Heading, Select, ActionButton } from "@budibase/bbui" import { Heading, Select, ActionButton } from "@budibase/bbui"
import { devToolsStore } from "../../stores" import { devToolsStore, appStore } from "../../stores"
import { getContext } from "svelte" import { getContext } from "svelte"
const context = getContext("context") const context = getContext("context")
@ -45,27 +45,41 @@
icon="Code" icon="Code"
on:click={() => devToolsStore.actions.setVisible(!$devToolsStore.visible)} on:click={() => devToolsStore.actions.setVisible(!$devToolsStore.visible)}
> >
{$devToolsStore.visible ? "Close" : "Open"} DevTools DevTools
</ActionButton>
{/if}
{#if window.parent.isBuilder}
<ActionButton
quiet
icon="LinkOut"
on:click={() => {
window.parent.closePreview?.()
window.open(`/${$appStore.appId}`, "_blank")
}}
>
Fullscreen
</ActionButton>
<ActionButton
quiet
icon="Close"
on:click={() => window.parent.closePreview?.()}
>
Close
</ActionButton> </ActionButton>
{/if} {/if}
<ActionButton
quiet
icon="Close"
on:click={() => window.parent.closePreview?.()}
>
Close preview
</ActionButton>
</div> </div>
<style> <style>
.dev-preview-header { .dev-preview-header {
flex: 0 0 60px; flex: 0 0 60px;
display: grid;
align-items: center;
background-color: black; background-color: black;
padding: 0 var(--spacing-xl); padding: 0 var(--spacing-xl);
grid-template-columns: 1fr auto auto auto; display: flex;
grid-gap: var(--spacing-xl); align-items: center;
gap: var(--spacing-xl);
}
.dev-preview-header :global(.spectrum-Heading) {
flex: 1 1 auto;
} }
.dev-preview-header.mobile { .dev-preview-header.mobile {
grid-template-columns: 1fr auto auto; grid-template-columns: 1fr auto auto;

View File

@ -123,6 +123,15 @@ export const buildAppEndpoints = API => ({
}) })
}, },
/**
* Gets budibase platform debug information.
*/
fetchSystemDebugInfo: async () => {
return await API.get({
url: `/api/debug/diagnostics`,
})
},
/** /**
* Syncs an app with the production database. * Syncs an app with the production database.
* @param appId the ID of the app to sync * @param appId the ID of the app to sync

View File

@ -30,6 +30,7 @@ import { buildBackupsEndpoints } from "./backups"
import { buildEnvironmentVariableEndpoints } from "./environmentVariables" import { buildEnvironmentVariableEndpoints } from "./environmentVariables"
import { buildEventEndpoints } from "./events" import { buildEventEndpoints } from "./events"
import { buildAuditLogsEndpoints } from "./auditLogs" import { buildAuditLogsEndpoints } from "./auditLogs"
import { buildLogsEndpoints } from "./logs"
/** /**
* Random identifier to uniquely identify a session in a tab. This is * Random identifier to uniquely identify a session in a tab. This is
@ -277,5 +278,6 @@ export const createAPIClient = config => {
...buildEnvironmentVariableEndpoints(API), ...buildEnvironmentVariableEndpoints(API),
...buildEventEndpoints(API), ...buildEventEndpoints(API),
...buildAuditLogsEndpoints(API), ...buildAuditLogsEndpoints(API),
...buildLogsEndpoints(API),
} }
} }

View File

@ -0,0 +1,14 @@
export const buildLogsEndpoints = API => ({
/**
* Gets a stream for the system logs.
*/
getSystemLogs: async () => {
return await API.get({
url: "/api/system/logs",
json: false,
parseResponse: async response => {
return response
},
})
},
})

View File

@ -1,60 +1,23 @@
<script> <script>
import { Avatar, Tooltip } from "@budibase/bbui" import { Avatar, AbsTooltip, TooltipPosition } from "@budibase/bbui"
import { helpers } from "@budibase/shared-core" import { helpers } from "@budibase/shared-core"
export let user export let user
export let size export let size = "S"
export let tooltipDirection = "top" export let tooltipPosition = TooltipPosition.Top
export let showTooltip = true export let showTooltip = true
$: tooltipStyle = getTooltipStyle(tooltipDirection)
const getTooltipStyle = direction => {
if (!direction) {
return ""
}
if (direction === "top") {
return "transform: translateX(-50%) translateY(-100%);"
} else if (direction === "bottom") {
return "transform: translateX(-50%) translateY(100%);"
}
}
</script> </script>
{#if user} {#if user}
<div class="user-avatar"> <AbsTooltip
text={showTooltip ? helpers.getUserLabel(user) : null}
position={tooltipPosition}
color={helpers.getUserColor(user)}
>
<Avatar <Avatar
{size} {size}
initials={helpers.getUserInitials(user)} initials={helpers.getUserInitials(user)}
color={helpers.getUserColor(user)} color={helpers.getUserColor(user)}
/> />
{#if showTooltip} </AbsTooltip>
<div class="tooltip" style={tooltipStyle}>
<Tooltip
direction={tooltipDirection}
textWrapping
text={helpers.getUserLabel(user)}
size="S"
/>
</div>
{/if}
</div>
{/if} {/if}
<style>
.user-avatar {
position: relative;
}
.tooltip {
position: absolute;
top: 0;
left: 50%;
white-space: nowrap;
pointer-events: none;
opacity: 0;
transition: opacity 130ms ease-out;
}
.user-avatar:hover .tooltip {
opacity: 1;
}
</style>

View File

@ -0,0 +1,67 @@
<script>
import { UserAvatar } from "@budibase/frontend-core"
import { TooltipPosition, Avatar } from "@budibase/bbui"
export let users = []
export let order = "ltr"
export let size = "S"
export let tooltipPosition = TooltipPosition.Top
$: uniqueUsers = unique(users, order)
$: avatars = getAvatars(uniqueUsers, order)
const unique = users => {
let uniqueUsers = {}
users?.forEach(user => {
uniqueUsers[user.email] = user
})
return Object.values(uniqueUsers)
}
const getAvatars = (users, order) => {
const avatars = users.slice(0, 3)
if (users.length > 3) {
const overflow = {
_id: "overflow",
label: `+${users.length - 3}`,
}
if (order === "ltr") {
avatars.push(overflow)
} else {
avatars.unshift(overflow)
}
}
return avatars.map((user, idx) => ({
...user,
zIndex: order === "ltr" ? idx : uniqueUsers.length - idx,
}))
}
</script>
<div class="avatars">
{#each avatars as user}
<span style="z-index:{user.zIndex};">
{#if user._id === "overflow"}
<Avatar
{size}
initials={user.label}
color="var(--spectrum-global-color-gray-500)"
/>
{:else}
<UserAvatar {size} {user} {tooltipPosition} />
{/if}
</span>
{/each}
</div>
<style>
.avatars {
display: flex;
}
span:not(:first-of-type) {
margin-left: -6px;
}
.avatars :global(.spectrum-Avatar) {
border: 2px solid var(--avatars-background, var(--background));
}
</style>

View File

@ -1,16 +0,0 @@
<script>
import { ActionButton } from "@budibase/bbui"
import { getContext } from "svelte"
const { config, dispatch } = getContext("grid")
</script>
<ActionButton
icon="TableColumnAddRight"
quiet
size="M"
on:click={() => dispatch("add-column")}
disabled={!$config.allowSchemaChanges}
>
Add column
</ActionButton>

View File

@ -1,18 +0,0 @@
<script>
import { ActionButton } from "@budibase/bbui"
import { getContext } from "svelte"
const { dispatch, columns, stickyColumn, config, loaded } = getContext("grid")
</script>
<ActionButton
icon="TableRowAddBottom"
quiet
size="M"
on:click={() => dispatch("add-row-inline")}
disabled={!loaded ||
!$config.allowAddRows ||
(!$columns.length && !$stickyColumn)}
>
Add row
</ActionButton>

View File

@ -71,6 +71,7 @@
contentLines, contentLines,
gridFocused, gridFocused,
error, error,
canAddRows,
} = context } = context
// Keep config store up to date with props // Keep config store up to date with props
@ -143,7 +144,7 @@
<HeaderRow /> <HeaderRow />
<GridBody /> <GridBody />
</div> </div>
{#if allowAddRows} {#if $canAddRows}
<NewRow /> <NewRow />
{/if} {/if}
<div class="overlays"> <div class="overlays">

View File

@ -9,7 +9,7 @@
renderedRows, renderedRows,
renderedColumns, renderedColumns,
rowVerticalInversionIndex, rowVerticalInversionIndex,
config, canAddRows,
hoveredRowId, hoveredRowId,
dispatch, dispatch,
isDragging, isDragging,
@ -43,7 +43,7 @@
invertY={idx >= $rowVerticalInversionIndex} invertY={idx >= $rowVerticalInversionIndex}
/> />
{/each} {/each}
{#if $config.allowAddRows && $renderedColumns.length} {#if $canAddRows}
<div <div
class="blank" class="blank"
class:highlighted={$hoveredRowId === BlankRowID} class:highlighted={$hoveredRowId === BlankRowID}

View File

@ -2,7 +2,7 @@
import { getContext } from "svelte" import { getContext } from "svelte"
import GridScrollWrapper from "./GridScrollWrapper.svelte" import GridScrollWrapper from "./GridScrollWrapper.svelte"
import HeaderCell from "../cells/HeaderCell.svelte" import HeaderCell from "../cells/HeaderCell.svelte"
import { Icon } from "@budibase/bbui" import { Icon, TempTooltip, TooltipType } from "@budibase/bbui"
const { const {
renderedColumns, renderedColumns,
@ -11,10 +11,13 @@
hiddenColumnsWidth, hiddenColumnsWidth,
width, width,
config, config,
hasNonAutoColumn,
tableId,
loading,
} = getContext("grid") } = getContext("grid")
$: columnsWidth = $renderedColumns.reduce( $: columnsWidth = $renderedColumns.reduce(
(total, col) => (total += col.width), (total, col) => total + col.width,
0 0
) )
$: end = $hiddenColumnsWidth + columnsWidth - 1 - $scroll.left $: end = $hiddenColumnsWidth + columnsWidth - 1 - $scroll.left
@ -30,13 +33,21 @@
</div> </div>
</GridScrollWrapper> </GridScrollWrapper>
{#if $config.allowSchemaChanges} {#if $config.allowSchemaChanges}
<div {#key $tableId}
class="add" <TempTooltip
style="left:{left}px" text="Click here to create your first column"
on:click={() => dispatch("add-column")} type={TooltipType.Info}
> condition={!$hasNonAutoColumn && !$loading}
<Icon name="Add" /> >
</div> <div
class="add"
style="left:{left}px;"
on:click={() => dispatch("add-column")}
>
<Icon name="Add" />
</div>
</TempTooltip>
{/key}
{/if} {/if}
</div> </div>

View File

@ -1,6 +1,6 @@
<script> <script>
import { getContext, onDestroy, onMount, tick } from "svelte" import { getContext, onDestroy, onMount, tick } from "svelte"
import { Icon, Button } from "@budibase/bbui" import { Icon, Button, TempTooltip, TooltipType } from "@budibase/bbui"
import GridScrollWrapper from "./GridScrollWrapper.svelte" import GridScrollWrapper from "./GridScrollWrapper.svelte"
import DataCell from "../cells/DataCell.svelte" import DataCell from "../cells/DataCell.svelte"
import { fade } from "svelte/transition" import { fade } from "svelte/transition"
@ -27,7 +27,8 @@
rowVerticalInversionIndex, rowVerticalInversionIndex,
columnHorizontalInversionIndex, columnHorizontalInversionIndex,
selectedRows, selectedRows,
config, loading,
canAddRows,
} = getContext("grid") } = getContext("grid")
let visible = false let visible = false
@ -40,6 +41,7 @@
$: $tableId, (visible = false) $: $tableId, (visible = false)
$: invertY = shouldInvertY(offset, $rowVerticalInversionIndex, $renderedRows) $: invertY = shouldInvertY(offset, $rowVerticalInversionIndex, $renderedRows)
$: selectedRowCount = Object.values($selectedRows).length $: selectedRowCount = Object.values($selectedRows).length
$: hasNoRows = !$rows.length
const shouldInvertY = (offset, inversionIndex, rows) => { const shouldInvertY = (offset, inversionIndex, rows) => {
if (offset === 0) { if (offset === 0) {
@ -147,16 +149,22 @@
</script> </script>
<!-- New row FAB --> <!-- New row FAB -->
{#if !visible && !selectedRowCount && $config.allowAddRows && firstColumn} <TempTooltip
<div text="Click here to create your first row"
class="new-row-fab" condition={hasNoRows && !$loading}
on:click={() => dispatch("add-row-inline")} type={TooltipType.Info}
transition:fade|local={{ duration: 130 }} >
class:offset={!$stickyColumn} {#if !visible && !selectedRowCount && $canAddRows}
> <div
<Icon name="Add" size="S" /> class="new-row-fab"
</div> on:click={() => dispatch("add-row-inline")}
{/if} transition:fade|local={{ duration: 130 }}
class:offset={!$stickyColumn}
>
<Icon name="Add" size="S" />
</div>
{/if}
</TempTooltip>
<!-- Only show new row functionality if we have any columns --> <!-- Only show new row functionality if we have any columns -->
{#if visible} {#if visible}

View File

@ -13,11 +13,10 @@
rows, rows,
selectedRows, selectedRows,
stickyColumn, stickyColumn,
renderedColumns,
renderedRows, renderedRows,
focusedCellId, focusedCellId,
hoveredRowId, hoveredRowId,
config, canAddRows,
selectedCellMap, selectedCellMap,
focusedRow, focusedRow,
scrollLeft, scrollLeft,
@ -93,7 +92,7 @@
{/if} {/if}
</div> </div>
{/each} {/each}
{#if $config.allowAddRows && ($renderedColumns.length || $stickyColumn)} {#if $canAddRows}
<div <div
class="row new" class="row new"
on:mouseenter={$isDragging on:mouseenter={$isDragging

View File

@ -16,6 +16,7 @@
config, config,
menu, menu,
gridFocused, gridFocused,
canAddRows,
} = getContext("grid") } = getContext("grid")
const ignoredOriginSelectors = [ const ignoredOriginSelectors = [
@ -45,7 +46,7 @@
e.preventDefault() e.preventDefault()
focusFirstCell() focusFirstCell()
} else if (e.key === "Enter" && (e.ctrlKey || e.metaKey)) { } else if (e.key === "Enter" && (e.ctrlKey || e.metaKey)) {
if ($config.allowAddRows) { if ($canAddRows) {
e.preventDefault() e.preventDefault()
dispatch("add-row-inline") dispatch("add-row-inline")
} }
@ -99,7 +100,7 @@
} }
break break
case "Enter": case "Enter":
if ($config.allowAddRows) { if ($canAddRows) {
dispatch("add-row-inline") dispatch("add-row-inline")
} }
} }

View File

@ -17,6 +17,7 @@
focusedCellAPI, focusedCellAPI,
focusedRowId, focusedRowId,
notifications, notifications,
canAddRows,
} = getContext("grid") } = getContext("grid")
$: style = makeStyle($menu) $: style = makeStyle($menu)
@ -93,7 +94,7 @@
</MenuItem> </MenuItem>
<MenuItem <MenuItem
icon="Duplicate" icon="Duplicate"
disabled={isNewRow || !$config.allowAddRows} disabled={isNewRow || !$canAddRows}
on:click={duplicate} on:click={duplicate}
> >
Duplicate row Duplicate row

View File

@ -83,6 +83,21 @@ export const deriveStores = context => {
await saveChanges() await saveChanges()
} }
// Derive if we have any normal columns
const hasNonAutoColumn = derived(
[columns, stickyColumn],
([$columns, $stickyColumn]) => {
let allCols = $columns || []
if ($stickyColumn) {
allCols = [...allCols, $stickyColumn]
}
const normalCols = allCols.filter(column => {
return !column.schema?.autocolumn
})
return normalCols.length > 0
}
)
// Persists column changes by saving metadata against table schema // Persists column changes by saving metadata against table schema
const saveChanges = async () => { const saveChanges = async () => {
const $columns = get(columns) const $columns = get(columns)
@ -128,6 +143,7 @@ export const deriveStores = context => {
} }
return { return {
hasNonAutoColumn,
columns: { columns: {
...columns, ...columns,
actions: { actions: {

View File

@ -70,6 +70,8 @@ export const deriveStores = context => {
rowHeight, rowHeight,
stickyColumn, stickyColumn,
width, width,
hasNonAutoColumn,
config,
} = context } = context
// Derive the row that contains the selected cell // Derive the row that contains the selected cell
@ -112,7 +114,16 @@ export const deriveStores = context => {
return ($stickyColumn?.width || 0) + $width + GutterWidth < 1100 return ($stickyColumn?.width || 0) + $width + GutterWidth < 1100
}) })
// Derive if we're able to add rows
const canAddRows = derived(
[config, hasNonAutoColumn],
([$config, $hasNonAutoColumn]) => {
return $config.allowAddRows && $hasNonAutoColumn
}
)
return { return {
canAddRows,
focusedRow, focusedRow,
contentLines, contentLines,
compact, compact,

View File

@ -2,4 +2,5 @@ export { default as SplitPage } from "./SplitPage.svelte"
export { default as TestimonialPage } from "./TestimonialPage.svelte" export { default as TestimonialPage } from "./TestimonialPage.svelte"
export { default as Testimonial } from "./Testimonial.svelte" export { default as Testimonial } from "./Testimonial.svelte"
export { default as UserAvatar } from "./UserAvatar.svelte" export { default as UserAvatar } from "./UserAvatar.svelte"
export { default as UserAvatars } from "./UserAvatars.svelte"
export { Grid } from "./grid" export { Grid } from "./grid"

View File

@ -11,3 +11,26 @@ export function downloadText(filename, text) {
URL.revokeObjectURL(url) URL.revokeObjectURL(url)
} }
export async function downloadStream(streamResponse) {
const blob = await streamResponse.blob()
const contentDisposition = streamResponse.headers.get("Content-Disposition")
const matches = /filename[^;=\n]*=((['"]).*?\2|[^;\n]*)/.exec(
contentDisposition
)
const filename = matches[1].replace(/['"]/g, "")
const resBlob = new Blob([blob])
const blobUrl = URL.createObjectURL(resBlob)
const link = document.createElement("a")
link.href = blobUrl
link.download = filename
link.click()
URL.revokeObjectURL(blobUrl)
}

View File

@ -5,4 +5,4 @@ export * as RoleUtils from "./roles"
export * as Utils from "./utils" export * as Utils from "./utils"
export { memo, derivedMemo } from "./memo" export { memo, derivedMemo } from "./memo"
export { createWebsocket } from "./websocket" export { createWebsocket } from "./websocket"
export { downloadText } from "./download" export * from "./download"

Some files were not shown because too many files have changed in this diff Show More