Merge remote-tracking branch 'origin/new-design-nav-component' into screen-theme-rightpanel
This commit is contained in:
commit
794ed5a29a
|
@ -12,9 +12,6 @@ on:
|
||||||
- master
|
- master
|
||||||
- develop
|
- develop
|
||||||
pull_request:
|
pull_request:
|
||||||
branches:
|
|
||||||
- master
|
|
||||||
- develop
|
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
env:
|
env:
|
||||||
|
|
|
@ -98,7 +98,7 @@ jobs:
|
||||||
git fetch
|
git fetch
|
||||||
mkdir sync
|
mkdir sync
|
||||||
echo "Packaging chart to sync dir"
|
echo "Packaging chart to sync dir"
|
||||||
helm package charts/budibase --version 0.0.0-master --app-version v"$RELEASE_VERSION" --destination sync
|
helm package charts/budibase --version 0.0.0-master --app-version "$RELEASE_VERSION" --destination sync
|
||||||
echo "Packaging successful"
|
echo "Packaging successful"
|
||||||
git checkout gh-pages
|
git checkout gh-pages
|
||||||
echo "Indexing helm repo"
|
echo "Indexing helm repo"
|
||||||
|
|
|
@ -43,7 +43,7 @@ jobs:
|
||||||
run: |
|
run: |
|
||||||
docker login -u $DOCKER_USER -p $DOCKER_PASSWORD
|
docker login -u $DOCKER_USER -p $DOCKER_PASSWORD
|
||||||
|
|
||||||
release_tag=v${{ env.RELEASE_VERSION }}
|
release_tag=${{ env.RELEASE_VERSION }}
|
||||||
|
|
||||||
# Pull apps and worker images
|
# Pull apps and worker images
|
||||||
docker pull budibase/apps:$release_tag
|
docker pull budibase/apps:$release_tag
|
||||||
|
@ -108,8 +108,8 @@ jobs:
|
||||||
- name: Perform Github Release
|
- name: Perform Github Release
|
||||||
uses: softprops/action-gh-release@v1
|
uses: softprops/action-gh-release@v1
|
||||||
with:
|
with:
|
||||||
name: v${{ env.RELEASE_VERSION }}
|
name: ${{ env.RELEASE_VERSION }}
|
||||||
tag_name: v${{ env.RELEASE_VERSION }}
|
tag_name: ${{ env.RELEASE_VERSION }}
|
||||||
generate_release_notes: true
|
generate_release_notes: true
|
||||||
files: |
|
files: |
|
||||||
packages/cli/build/cli-win.exe
|
packages/cli/build/cli-win.exe
|
||||||
|
|
|
@ -71,7 +71,7 @@ jobs:
|
||||||
context: .
|
context: .
|
||||||
push: true
|
push: true
|
||||||
platforms: linux/amd64,linux/arm64
|
platforms: linux/amd64,linux/arm64
|
||||||
tags: budibase/budibase,budibase/budibase:v${{ env.RELEASE_VERSION }}
|
tags: budibase/budibase,budibase/budibase:${{ env.RELEASE_VERSION }}
|
||||||
file: ./hosting/single/Dockerfile
|
file: ./hosting/single/Dockerfile
|
||||||
- name: Tag and release Budibase Azure App Service docker image
|
- name: Tag and release Budibase Azure App Service docker image
|
||||||
uses: docker/build-push-action@v2
|
uses: docker/build-push-action@v2
|
||||||
|
@ -80,5 +80,5 @@ jobs:
|
||||||
push: true
|
push: true
|
||||||
platforms: linux/amd64
|
platforms: linux/amd64
|
||||||
build-args: TARGETBUILD=aas
|
build-args: TARGETBUILD=aas
|
||||||
tags: budibase/budibase-aas,budibase/budibase-aas:v${{ env.RELEASE_VERSION }}
|
tags: budibase/budibase-aas,budibase/budibase-aas:${{ env.RELEASE_VERSION }}
|
||||||
file: ./hosting/single/Dockerfile
|
file: ./hosting/single/Dockerfile
|
||||||
|
|
|
@ -1,2 +1,2 @@
|
||||||
nodejs 14.20.1
|
nodejs 14.21.3
|
||||||
python 3.10.0
|
python 3.10.0
|
|
@ -209,7 +209,7 @@ services:
|
||||||
# Override values in couchDB subchart
|
# Override values in couchDB subchart
|
||||||
couchdb:
|
couchdb:
|
||||||
## clusterSize is the initial size of the CouchDB cluster.
|
## clusterSize is the initial size of the CouchDB cluster.
|
||||||
clusterSize: 3
|
clusterSize: 1
|
||||||
allowAdminParty: false
|
allowAdminParty: false
|
||||||
|
|
||||||
# Secret Management
|
# Secret Management
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
{
|
{
|
||||||
"version": "2.8.10-alpha.0",
|
"version": "2.8.16-alpha.0",
|
||||||
"npmClient": "yarn",
|
"npmClient": "yarn",
|
||||||
"packages": [
|
"packages": [
|
||||||
"packages/*"
|
"packages/*"
|
||||||
|
|
|
@ -53,7 +53,7 @@
|
||||||
"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 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 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 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 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",
|
||||||
|
|
|
@ -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") {
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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" }
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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">
|
||||||
|
|
|
@ -98,6 +98,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>
|
||||||
|
|
|
@ -0,0 +1,157 @@
|
||||||
|
<script context="module">
|
||||||
|
export const TooltipPosition = {
|
||||||
|
Top: "top",
|
||||||
|
Right: "right",
|
||||||
|
Bottom: "bottom",
|
||||||
|
Left: "left",
|
||||||
|
}
|
||||||
|
export const TooltipType = {
|
||||||
|
Default: "default",
|
||||||
|
Info: "info",
|
||||||
|
Positive: "positive",
|
||||||
|
Negative: "negative",
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import Portal from "svelte-portal"
|
||||||
|
import { fade } from "svelte/transition"
|
||||||
|
import "@spectrum-css/tooltip/dist/index-vars.css"
|
||||||
|
import { onDestroy } from "svelte"
|
||||||
|
|
||||||
|
export let position = TooltipPosition.Top
|
||||||
|
export let type = TooltipType.Default
|
||||||
|
export let text = ""
|
||||||
|
export let fixed = false
|
||||||
|
export let color = null
|
||||||
|
|
||||||
|
let wrapper
|
||||||
|
let hovered = false
|
||||||
|
let left
|
||||||
|
let top
|
||||||
|
let visible = false
|
||||||
|
let timeout
|
||||||
|
let interval
|
||||||
|
|
||||||
|
$: {
|
||||||
|
if (hovered || fixed) {
|
||||||
|
// Debounce showing by 200ms to avoid flashing tooltip
|
||||||
|
timeout = setTimeout(show, 200)
|
||||||
|
} else {
|
||||||
|
hide()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$: tooltipStyle = color ? `background:${color};` : null
|
||||||
|
$: tipStyle = color ? `border-top-color:${color};` : null
|
||||||
|
|
||||||
|
// Computes the position of the tooltip
|
||||||
|
const updateTooltipPosition = () => {
|
||||||
|
const node = wrapper?.children?.[0]
|
||||||
|
if (!node) {
|
||||||
|
left = null
|
||||||
|
top = null
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const bounds = node.getBoundingClientRect()
|
||||||
|
|
||||||
|
// Determine where to render tooltip based on position prop
|
||||||
|
if (position === TooltipPosition.Top) {
|
||||||
|
left = bounds.left + bounds.width / 2
|
||||||
|
top = bounds.top
|
||||||
|
} else if (position === TooltipPosition.Right) {
|
||||||
|
left = bounds.left + bounds.width
|
||||||
|
top = bounds.top + bounds.height / 2
|
||||||
|
} else if (position === TooltipPosition.Bottom) {
|
||||||
|
left = bounds.left + bounds.width / 2
|
||||||
|
top = bounds.top + bounds.height
|
||||||
|
} else if (position === TooltipPosition.Left) {
|
||||||
|
left = bounds.left
|
||||||
|
top = bounds.top + bounds.height / 2
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Computes the position of the tooltip then shows it.
|
||||||
|
// We set up a poll to frequently update the position of the tooltip in case
|
||||||
|
// the target moves.
|
||||||
|
const show = () => {
|
||||||
|
updateTooltipPosition()
|
||||||
|
interval = setInterval(updateTooltipPosition, 100)
|
||||||
|
visible = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hides the tooltip
|
||||||
|
const hide = () => {
|
||||||
|
clearTimeout(timeout)
|
||||||
|
clearInterval(interval)
|
||||||
|
visible = false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure we clean up interval and timeout
|
||||||
|
onDestroy(hide)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div
|
||||||
|
bind:this={wrapper}
|
||||||
|
class="abs-tooltip"
|
||||||
|
on:focus={null}
|
||||||
|
on:mouseover={() => (hovered = true)}
|
||||||
|
on:mouseleave={() => (hovered = false)}
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{#if visible && text && left != null && top != null}
|
||||||
|
<Portal target=".spectrum">
|
||||||
|
<span
|
||||||
|
class="spectrum-Tooltip spectrum-Tooltip--{type} spectrum-Tooltip--{position} is-open"
|
||||||
|
style={`left:${left}px;top:${top}px;${tooltipStyle}`}
|
||||||
|
transition:fade|local={{ duration: 130 }}
|
||||||
|
>
|
||||||
|
<span class="spectrum-Tooltip-label">{text}</span>
|
||||||
|
<span class="spectrum-Tooltip-tip" style={tipStyle} />
|
||||||
|
</span>
|
||||||
|
</Portal>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.abs-tooltip {
|
||||||
|
display: contents;
|
||||||
|
}
|
||||||
|
.spectrum-Tooltip {
|
||||||
|
position: absolute;
|
||||||
|
z-index: 9999;
|
||||||
|
pointer-events: none;
|
||||||
|
margin: 0;
|
||||||
|
max-width: 280px;
|
||||||
|
transition: top 130ms ease-out, left 130ms ease-out;
|
||||||
|
}
|
||||||
|
.spectrum-Tooltip-label {
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Colour overrides for default type */
|
||||||
|
.spectrum-Tooltip--default {
|
||||||
|
background: var(--spectrum-global-color-gray-500);
|
||||||
|
}
|
||||||
|
.spectrum-Tooltip--default .spectrum-Tooltip-tip {
|
||||||
|
border-top-color: var(--spectrum-global-color-gray-500);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Position styles */
|
||||||
|
.spectrum-Tooltip--top {
|
||||||
|
transform: translateX(-50%) translateY(calc(-100% - 8px));
|
||||||
|
}
|
||||||
|
.spectrum-Tooltip--right {
|
||||||
|
transform: translateX(8px) translateY(-50%);
|
||||||
|
}
|
||||||
|
.spectrum-Tooltip--bottom {
|
||||||
|
transform: translateX(-50%) translateY(8px);
|
||||||
|
}
|
||||||
|
.spectrum-Tooltip--left {
|
||||||
|
transform: translateX(calc(-100% - 8px)) translateY(-50%);
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,39 @@
|
||||||
|
<script>
|
||||||
|
import AbsTooltip from "./AbsTooltip.svelte"
|
||||||
|
import { onDestroy } from "svelte"
|
||||||
|
|
||||||
|
export let text = null
|
||||||
|
export let condition = true
|
||||||
|
export let duration = 3000
|
||||||
|
export let position
|
||||||
|
export let type
|
||||||
|
|
||||||
|
let visible = false
|
||||||
|
let timeout
|
||||||
|
|
||||||
|
$: {
|
||||||
|
if (condition) {
|
||||||
|
showTooltip()
|
||||||
|
} else {
|
||||||
|
hideTooltip()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const showTooltip = () => {
|
||||||
|
visible = true
|
||||||
|
timeout = setTimeout(() => {
|
||||||
|
visible = false
|
||||||
|
}, duration)
|
||||||
|
}
|
||||||
|
|
||||||
|
const hideTooltip = () => {
|
||||||
|
visible = false
|
||||||
|
clearTimeout(timeout)
|
||||||
|
}
|
||||||
|
|
||||||
|
onDestroy(hideTooltip)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<AbsTooltip {position} {type} text={visible ? text : null} fixed={visible}>
|
||||||
|
<slot />
|
||||||
|
</AbsTooltip>
|
|
@ -36,6 +36,12 @@ export { default as Layout } from "./Layout/Layout.svelte"
|
||||||
export { default as Page } from "./Layout/Page.svelte"
|
export { default as 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"
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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}>
|
||||||
|
|
|
@ -15,6 +15,7 @@
|
||||||
Icon,
|
Icon,
|
||||||
Checkbox,
|
Checkbox,
|
||||||
DatePicker,
|
DatePicker,
|
||||||
|
Detail,
|
||||||
} from "@budibase/bbui"
|
} from "@budibase/bbui"
|
||||||
import CreateWebhookModal from "components/automation/Shared/CreateWebhookModal.svelte"
|
import CreateWebhookModal from "components/automation/Shared/CreateWebhookModal.svelte"
|
||||||
import { automationStore, selectedAutomation } from "builderStore"
|
import { automationStore, selectedAutomation } from "builderStore"
|
||||||
|
@ -32,7 +33,7 @@
|
||||||
import CodeEditor from "components/common/CodeEditor/CodeEditor.svelte"
|
import CodeEditor from "components/common/CodeEditor/CodeEditor.svelte"
|
||||||
import {
|
import {
|
||||||
bindingsToCompletions,
|
bindingsToCompletions,
|
||||||
jsAutocomplete,
|
hbAutocomplete,
|
||||||
EditorModes,
|
EditorModes,
|
||||||
} from "components/common/CodeEditor"
|
} from "components/common/CodeEditor"
|
||||||
import FilterDrawer from "components/design/settings/controls/FilterEditor/FilterDrawer.svelte"
|
import FilterDrawer from "components/design/settings/controls/FilterEditor/FilterDrawer.svelte"
|
||||||
|
@ -55,6 +56,7 @@
|
||||||
let drawer
|
let drawer
|
||||||
let fillWidth = true
|
let fillWidth = true
|
||||||
let inputData
|
let inputData
|
||||||
|
let codeBindingOpen = false
|
||||||
|
|
||||||
$: filters = lookForFilters(schemaProperties) || []
|
$: filters = lookForFilters(schemaProperties) || []
|
||||||
$: tempFilters = filters
|
$: tempFilters = filters
|
||||||
|
@ -70,6 +72,13 @@
|
||||||
$: queryLimit = tableId?.includes("datasource") ? "∞" : "1000"
|
$: queryLimit = tableId?.includes("datasource") ? "∞" : "1000"
|
||||||
$: isTrigger = block?.type === "TRIGGER"
|
$: isTrigger = block?.type === "TRIGGER"
|
||||||
$: isUpdateRow = stepId === ActionStepID.UPDATE_ROW
|
$: isUpdateRow = stepId === ActionStepID.UPDATE_ROW
|
||||||
|
$: codeMode =
|
||||||
|
stepId === "EXECUTE_BASH" ? EditorModes.Handlebars : EditorModes.JS
|
||||||
|
|
||||||
|
$: stepCompletions =
|
||||||
|
codeMode === EditorModes.Handlebars
|
||||||
|
? [hbAutocomplete([...bindingsToCompletions(bindings, codeMode)])]
|
||||||
|
: []
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* TODO - Remove after November 2023
|
* TODO - Remove after November 2023
|
||||||
|
@ -489,6 +498,18 @@
|
||||||
/>
|
/>
|
||||||
{:else if value.customType === "code"}
|
{:else if value.customType === "code"}
|
||||||
<CodeEditorModal>
|
<CodeEditorModal>
|
||||||
|
{#if codeMode == EditorModes.JS}
|
||||||
|
<ActionButton
|
||||||
|
on:click={() => (codeBindingOpen = !codeBindingOpen)}
|
||||||
|
quiet
|
||||||
|
icon={codeBindingOpen ? "ChevronDown" : "ChevronRight"}
|
||||||
|
>
|
||||||
|
<Detail size="S">Bindings</Detail>
|
||||||
|
</ActionButton>
|
||||||
|
{#if codeBindingOpen}
|
||||||
|
<pre>{JSON.stringify(bindings, null, 2)}</pre>
|
||||||
|
{/if}
|
||||||
|
{/if}
|
||||||
<CodeEditor
|
<CodeEditor
|
||||||
value={inputData[key]}
|
value={inputData[key]}
|
||||||
on:change={e => {
|
on:change={e => {
|
||||||
|
@ -496,19 +517,22 @@
|
||||||
onChange({ detail: e.detail }, key)
|
onChange({ detail: e.detail }, key)
|
||||||
inputData[key] = e.detail
|
inputData[key] = e.detail
|
||||||
}}
|
}}
|
||||||
completions={[
|
completions={stepCompletions}
|
||||||
jsAutocomplete([
|
mode={codeMode}
|
||||||
...bindingsToCompletions(bindings, EditorModes.JS),
|
autocompleteEnabled={codeMode != EditorModes.JS}
|
||||||
]),
|
|
||||||
]}
|
|
||||||
mode={EditorModes.JS}
|
|
||||||
height={500}
|
height={500}
|
||||||
/>
|
/>
|
||||||
<div class="messaging">
|
<div class="messaging">
|
||||||
<Icon name="FlashOn" />
|
{#if codeMode == EditorModes.Handlebars}
|
||||||
<div class="messaging-wrap">
|
<Icon name="FlashOn" />
|
||||||
<div>Add available bindings by typing <strong>$</strong></div>
|
<div class="messaging-wrap">
|
||||||
</div>
|
<div>
|
||||||
|
Add available bindings by typing <strong>
|
||||||
|
}}
|
||||||
|
</strong>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</CodeEditorModal>
|
</CodeEditorModal>
|
||||||
{:else if value.customType === "loopOption"}
|
{:else if value.customType === "loopOption"}
|
||||||
|
|
|
@ -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}`)
|
||||||
|
|
|
@ -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"
|
||||||
|
@ -48,6 +51,7 @@
|
||||||
export let mode = EditorModes.Handlebars
|
export let mode = EditorModes.Handlebars
|
||||||
export let value = ""
|
export let value = ""
|
||||||
export let placeholder = null
|
export let placeholder = null
|
||||||
|
export let autocompleteEnabled = true
|
||||||
|
|
||||||
// Export a function to expose caret position
|
// Export a function to expose caret position
|
||||||
export const getCaretPosition = () => {
|
export const getCaretPosition = () => {
|
||||||
|
@ -107,6 +111,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 +134,7 @@
|
||||||
...historyKeymap,
|
...historyKeymap,
|
||||||
...foldKeymap,
|
...foldKeymap,
|
||||||
...completionKeymap,
|
...completionKeymap,
|
||||||
indentWithTab,
|
indentWithTabCustom,
|
||||||
]
|
]
|
||||||
return baseMap
|
return baseMap
|
||||||
}
|
}
|
||||||
|
@ -131,12 +151,6 @@
|
||||||
syntaxHighlighting(oneDarkHighlightStyle, { fallback: true }),
|
syntaxHighlighting(oneDarkHighlightStyle, { fallback: true }),
|
||||||
highlightActiveLineGutter(),
|
highlightActiveLineGutter(),
|
||||||
highlightSpecialChars(),
|
highlightSpecialChars(),
|
||||||
autocompletion({
|
|
||||||
override: [...completions],
|
|
||||||
closeOnBlur: true,
|
|
||||||
icons: false,
|
|
||||||
optionClass: () => "autocomplete-option",
|
|
||||||
}),
|
|
||||||
EditorView.lineWrapping,
|
EditorView.lineWrapping,
|
||||||
EditorView.updateListener.of(v => {
|
EditorView.updateListener.of(v => {
|
||||||
const docStr = v.state.doc?.toString()
|
const docStr = v.state.doc?.toString()
|
||||||
|
@ -159,11 +173,16 @@
|
||||||
|
|
||||||
const buildExtensions = base => {
|
const buildExtensions = base => {
|
||||||
const complete = [...base]
|
const complete = [...base]
|
||||||
if (mode.name == "javascript") {
|
|
||||||
complete.push(javascript())
|
if (autocompleteEnabled) {
|
||||||
complete.push(highlightWhitespace())
|
complete.push(
|
||||||
complete.push(lineNumbers())
|
autocompletion({
|
||||||
complete.push(foldGutter())
|
override: [...completions],
|
||||||
|
closeOnBlur: true,
|
||||||
|
icons: false,
|
||||||
|
optionClass: () => "autocomplete-option",
|
||||||
|
})
|
||||||
|
)
|
||||||
complete.push(
|
complete.push(
|
||||||
EditorView.inputHandler.of((view, from, to, insert) => {
|
EditorView.inputHandler.of((view, from, to, insert) => {
|
||||||
if (insert === "$") {
|
if (insert === "$") {
|
||||||
|
@ -193,6 +212,13 @@
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (mode.name == "javascript") {
|
||||||
|
complete.push(javascript())
|
||||||
|
complete.push(highlightWhitespace())
|
||||||
|
complete.push(lineNumbers())
|
||||||
|
complete.push(foldGutter())
|
||||||
|
}
|
||||||
|
|
||||||
if (placeholder) {
|
if (placeholder) {
|
||||||
complete.push(placeholderFn(placeholder))
|
complete.push(placeholderFn(placeholder))
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
<script>
|
<script>
|
||||||
import { Popover, Layout, Heading, Body, Button } from "@budibase/bbui"
|
import { Popover, Layout, Heading, Body, Button, Link } from "@budibase/bbui"
|
||||||
import { store } from "builderStore"
|
import { store } from "builderStore"
|
||||||
import { TOURS } from "./tours.js"
|
import { TOURS } from "./tours.js"
|
||||||
import { goto, layout, isActive } from "@roxi/routify"
|
import { goto, layout, isActive } from "@roxi/routify"
|
||||||
|
@ -10,17 +10,20 @@
|
||||||
let tourStep
|
let tourStep
|
||||||
let tourStepIdx
|
let tourStepIdx
|
||||||
let lastStep
|
let lastStep
|
||||||
|
let skipping = false
|
||||||
|
|
||||||
$: tourNodes = { ...$store.tourNodes }
|
$: tourNodes = { ...$store.tourNodes }
|
||||||
$: tourKey = $store.tourKey
|
$: tourKey = $store.tourKey
|
||||||
$: tourStepKey = $store.tourStepKey
|
$: tourStepKey = $store.tourStepKey
|
||||||
|
$: tour = TOURS[tourKey]
|
||||||
|
$: tourOnSkip = tour?.onSkip
|
||||||
|
|
||||||
const updateTourStep = (targetStepKey, tourKey) => {
|
const updateTourStep = (targetStepKey, tourKey) => {
|
||||||
if (!tourKey) {
|
if (!tourKey) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (!tourSteps?.length) {
|
if (!tourSteps?.length) {
|
||||||
tourSteps = [...TOURS[tourKey]]
|
tourSteps = [...tour.steps]
|
||||||
}
|
}
|
||||||
tourStepIdx = getCurrentStepIdx(tourSteps, targetStepKey)
|
tourStepIdx = getCurrentStepIdx(tourSteps, targetStepKey)
|
||||||
lastStep = tourStepIdx + 1 == tourSteps.length
|
lastStep = tourStepIdx + 1 == tourSteps.length
|
||||||
|
@ -71,23 +74,8 @@
|
||||||
tourStep.onComplete()
|
tourStep.onComplete()
|
||||||
}
|
}
|
||||||
popover.hide()
|
popover.hide()
|
||||||
if (tourStep.endRoute) {
|
if (tour.endRoute) {
|
||||||
$goto(tourStep.endRoute)
|
$goto(tour.endRoute)
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const previousStep = async () => {
|
|
||||||
if (tourStepIdx > 0) {
|
|
||||||
let target = tourSteps[tourStepIdx - 1]
|
|
||||||
if (target) {
|
|
||||||
store.update(state => ({
|
|
||||||
...state,
|
|
||||||
tourStepKey: target.id,
|
|
||||||
}))
|
|
||||||
navigateStep(target)
|
|
||||||
} else {
|
|
||||||
console.log("Could not retrieve step")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -132,16 +120,23 @@
|
||||||
</Body>
|
</Body>
|
||||||
<div class="tour-footer">
|
<div class="tour-footer">
|
||||||
<div class="tour-navigation">
|
<div class="tour-navigation">
|
||||||
{#if tourStepIdx > 0}
|
{#if typeof tourOnSkip === "function"}
|
||||||
<Button
|
<Link
|
||||||
secondary
|
secondary
|
||||||
on:click={previousStep}
|
quiet
|
||||||
disabled={tourStepIdx == 0}
|
on:click={() => {
|
||||||
|
skipping = true
|
||||||
|
tourOnSkip()
|
||||||
|
if (tour.endRoute) {
|
||||||
|
$goto(tour.endRoute)
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
disabled={skipping}
|
||||||
>
|
>
|
||||||
<div>Back</div>
|
Skip
|
||||||
</Button>
|
</Link>
|
||||||
{/if}
|
{/if}
|
||||||
<Button cta on:click={nextStep}>
|
<Button cta on:click={nextStep} disabled={skipping}>
|
||||||
<div>{lastStep ? "Finish" : "Next"}</div>
|
<div>{lastStep ? "Finish" : "Next"}</div>
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
@ -157,9 +152,10 @@
|
||||||
padding: var(--spacing-xl);
|
padding: var(--spacing-xl);
|
||||||
}
|
}
|
||||||
.tour-navigation {
|
.tour-navigation {
|
||||||
grid-gap: var(--spectrum-alias-grid-baseline);
|
grid-gap: var(--spacing-xl);
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: end;
|
justify-content: end;
|
||||||
|
align-items: center;
|
||||||
}
|
}
|
||||||
.tour-body :global(.feature-list) {
|
.tour-body :global(.feature-list) {
|
||||||
margin-bottom: 0px;
|
margin-bottom: 0px;
|
||||||
|
|
|
@ -13,7 +13,7 @@
|
||||||
|
|
||||||
const registerTourNode = (tourKey, stepKey) => {
|
const registerTourNode = (tourKey, stepKey) => {
|
||||||
if (ready && !registered && tourKey) {
|
if (ready && !registered && tourKey) {
|
||||||
currentTourStep = TOURS[tourKey].find(step => step.id === stepKey)
|
currentTourStep = TOURS[tourKey].steps.find(step => step.id === stepKey)
|
||||||
if (!currentTourStep) {
|
if (!currentTourStep) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,6 +4,7 @@ import { auth } from "stores/portal"
|
||||||
import analytics from "analytics"
|
import analytics from "analytics"
|
||||||
import { OnboardingData, OnboardingDesign, OnboardingPublish } from "./steps"
|
import { OnboardingData, OnboardingDesign, OnboardingPublish } from "./steps"
|
||||||
import { API } from "api"
|
import { API } from "api"
|
||||||
|
|
||||||
const ONBOARDING_EVENT_PREFIX = "onboarding"
|
const ONBOARDING_EVENT_PREFIX = "onboarding"
|
||||||
|
|
||||||
export const TOUR_STEP_KEYS = {
|
export const TOUR_STEP_KEYS = {
|
||||||
|
@ -20,6 +21,37 @@ export const TOUR_KEYS = {
|
||||||
FEATURE_ONBOARDING: "feature-onboarding",
|
FEATURE_ONBOARDING: "feature-onboarding",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const endUserOnboarding = async ({ skipped = false } = {}) => {
|
||||||
|
// Mark the users onboarding as complete
|
||||||
|
// Clear all tour related state
|
||||||
|
if (get(auth).user) {
|
||||||
|
try {
|
||||||
|
await API.updateSelf({
|
||||||
|
onboardedAt: new Date().toISOString(),
|
||||||
|
})
|
||||||
|
|
||||||
|
if (skipped) {
|
||||||
|
tourEvent("skipped")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update the cached user
|
||||||
|
await auth.getSelf()
|
||||||
|
|
||||||
|
store.update(state => ({
|
||||||
|
...state,
|
||||||
|
tourNodes: undefined,
|
||||||
|
tourKey: undefined,
|
||||||
|
tourKeyStep: undefined,
|
||||||
|
onboarding: false,
|
||||||
|
}))
|
||||||
|
} catch (e) {
|
||||||
|
console.log("Onboarding failed", e)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const tourEvent = eventKey => {
|
const tourEvent = eventKey => {
|
||||||
analytics.captureEvent(`${ONBOARDING_EVENT_PREFIX}:${eventKey}`, {
|
analytics.captureEvent(`${ONBOARDING_EVENT_PREFIX}:${eventKey}`, {
|
||||||
eventSource: EventSource.PORTAL,
|
eventSource: EventSource.PORTAL,
|
||||||
|
@ -28,111 +60,81 @@ const tourEvent = eventKey => {
|
||||||
|
|
||||||
const getTours = () => {
|
const getTours = () => {
|
||||||
return {
|
return {
|
||||||
[TOUR_KEYS.TOUR_BUILDER_ONBOARDING]: [
|
[TOUR_KEYS.TOUR_BUILDER_ONBOARDING]: {
|
||||||
{
|
steps: [
|
||||||
id: TOUR_STEP_KEYS.BUILDER_DATA_SECTION,
|
{
|
||||||
title: "Data",
|
id: TOUR_STEP_KEYS.BUILDER_DATA_SECTION,
|
||||||
route: "/builder/app/:application/data",
|
title: "Data",
|
||||||
layout: OnboardingData,
|
route: "/builder/app/:application/data",
|
||||||
query: ".topleftnav .spectrum-Tabs-item#builder-data-tab",
|
layout: OnboardingData,
|
||||||
onLoad: async () => {
|
query: ".topleftnav .spectrum-Tabs-item#builder-data-tab",
|
||||||
tourEvent(TOUR_STEP_KEYS.BUILDER_DATA_SECTION)
|
onLoad: async () => {
|
||||||
|
tourEvent(TOUR_STEP_KEYS.BUILDER_DATA_SECTION)
|
||||||
|
},
|
||||||
|
align: "left",
|
||||||
},
|
},
|
||||||
align: "left",
|
{
|
||||||
|
id: TOUR_STEP_KEYS.BUILDER_DESIGN_SECTION,
|
||||||
|
title: "Design",
|
||||||
|
route: "/builder/app/:application/design",
|
||||||
|
layout: OnboardingDesign,
|
||||||
|
query: ".topleftnav .spectrum-Tabs-item#builder-design-tab",
|
||||||
|
onLoad: () => {
|
||||||
|
tourEvent(TOUR_STEP_KEYS.BUILDER_DESIGN_SECTION)
|
||||||
|
},
|
||||||
|
align: "left",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: TOUR_STEP_KEYS.BUILDER_AUTOMATION_SECTION,
|
||||||
|
title: "Automations",
|
||||||
|
route: "/builder/app/:application/automation",
|
||||||
|
query: ".topleftnav .spectrum-Tabs-item#builder-automation-tab",
|
||||||
|
body: "Once you have your app screens made, you can set up automations to fit in with your current workflow",
|
||||||
|
onLoad: () => {
|
||||||
|
tourEvent(TOUR_STEP_KEYS.BUILDER_AUTOMATION_SECTION)
|
||||||
|
},
|
||||||
|
align: "left",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: TOUR_STEP_KEYS.BUILDER_USER_MANAGEMENT,
|
||||||
|
title: "Users",
|
||||||
|
query: ".toprightnav #builder-app-users-button",
|
||||||
|
body: "Add users to your app and control what level of access they have.",
|
||||||
|
onLoad: () => {
|
||||||
|
tourEvent(TOUR_STEP_KEYS.BUILDER_USER_MANAGEMENT)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: TOUR_STEP_KEYS.BUILDER_APP_PUBLISH,
|
||||||
|
title: "Publish",
|
||||||
|
layout: OnboardingPublish,
|
||||||
|
route: "/builder/app/:application/design",
|
||||||
|
query: ".toprightnav #builder-app-publish-button",
|
||||||
|
onLoad: () => {
|
||||||
|
tourEvent(TOUR_STEP_KEYS.BUILDER_APP_PUBLISH)
|
||||||
|
},
|
||||||
|
onComplete: endUserOnboarding,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
onSkip: async () => {
|
||||||
|
await endUserOnboarding({ skipped: true })
|
||||||
},
|
},
|
||||||
{
|
endRoute: "/builder/app/:application/data",
|
||||||
id: TOUR_STEP_KEYS.BUILDER_DESIGN_SECTION,
|
},
|
||||||
title: "Design",
|
[TOUR_KEYS.FEATURE_ONBOARDING]: {
|
||||||
route: "/builder/app/:application/design",
|
steps: [
|
||||||
layout: OnboardingDesign,
|
{
|
||||||
query: ".topleftnav .spectrum-Tabs-item#builder-design-tab",
|
id: TOUR_STEP_KEYS.FEATURE_USER_MANAGEMENT,
|
||||||
onLoad: () => {
|
title: "Users",
|
||||||
tourEvent(TOUR_STEP_KEYS.BUILDER_DESIGN_SECTION)
|
query: ".toprightnav #builder-app-users-button",
|
||||||
|
body: "Add users to your app and control what level of access they have.",
|
||||||
|
onLoad: () => {
|
||||||
|
tourEvent(TOUR_STEP_KEYS.FEATURE_USER_MANAGEMENT)
|
||||||
|
},
|
||||||
|
onComplete: endUserOnboarding,
|
||||||
},
|
},
|
||||||
align: "left",
|
],
|
||||||
},
|
},
|
||||||
{
|
|
||||||
id: TOUR_STEP_KEYS.BUILDER_AUTOMATION_SECTION,
|
|
||||||
title: "Automations",
|
|
||||||
route: "/builder/app/:application/automation",
|
|
||||||
query: ".topleftnav .spectrum-Tabs-item#builder-automation-tab",
|
|
||||||
body: "Once you have your app screens made, you can set up automations to fit in with your current workflow",
|
|
||||||
onLoad: () => {
|
|
||||||
tourEvent(TOUR_STEP_KEYS.BUILDER_AUTOMATION_SECTION)
|
|
||||||
},
|
|
||||||
align: "left",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: TOUR_STEP_KEYS.BUILDER_USER_MANAGEMENT,
|
|
||||||
title: "Users",
|
|
||||||
query: ".toprightnav #builder-app-users-button",
|
|
||||||
body: "Add users to your app and control what level of access they have.",
|
|
||||||
onLoad: () => {
|
|
||||||
tourEvent(TOUR_STEP_KEYS.BUILDER_USER_MANAGEMENT)
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: TOUR_STEP_KEYS.BUILDER_APP_PUBLISH,
|
|
||||||
title: "Publish",
|
|
||||||
layout: OnboardingPublish,
|
|
||||||
route: "/builder/app/:application/design",
|
|
||||||
endRoute: "/builder/app/:application/data",
|
|
||||||
query: ".toprightnav #builder-app-publish-button",
|
|
||||||
onLoad: () => {
|
|
||||||
tourEvent(TOUR_STEP_KEYS.BUILDER_APP_PUBLISH)
|
|
||||||
},
|
|
||||||
onComplete: async () => {
|
|
||||||
// Mark the users onboarding as complete
|
|
||||||
// Clear all tour related state
|
|
||||||
if (get(auth).user) {
|
|
||||||
await API.updateSelf({
|
|
||||||
onboardedAt: new Date().toISOString(),
|
|
||||||
})
|
|
||||||
|
|
||||||
// Update the cached user
|
|
||||||
await auth.getSelf()
|
|
||||||
|
|
||||||
store.update(state => ({
|
|
||||||
...state,
|
|
||||||
tourNodes: undefined,
|
|
||||||
tourKey: undefined,
|
|
||||||
tourKeyStep: undefined,
|
|
||||||
onboarding: false,
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
[TOUR_KEYS.FEATURE_ONBOARDING]: [
|
|
||||||
{
|
|
||||||
id: TOUR_STEP_KEYS.FEATURE_USER_MANAGEMENT,
|
|
||||||
title: "Users",
|
|
||||||
query: ".toprightnav #builder-app-users-button",
|
|
||||||
body: "Add users to your app and control what level of access they have.",
|
|
||||||
onLoad: () => {
|
|
||||||
tourEvent(TOUR_STEP_KEYS.FEATURE_USER_MANAGEMENT)
|
|
||||||
},
|
|
||||||
onComplete: async () => {
|
|
||||||
// Push the onboarding forward
|
|
||||||
if (get(auth).user) {
|
|
||||||
await API.updateSelf({
|
|
||||||
onboardedAt: new Date().toISOString(),
|
|
||||||
})
|
|
||||||
|
|
||||||
// Update the cached user
|
|
||||||
await auth.getSelf()
|
|
||||||
|
|
||||||
store.update(state => ({
|
|
||||||
...state,
|
|
||||||
tourNodes: undefined,
|
|
||||||
tourKey: undefined,
|
|
||||||
tourKeyStep: undefined,
|
|
||||||
onboarding: false,
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,11 +1,8 @@
|
||||||
<script>
|
<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>
|
||||||
|
|
|
@ -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(),
|
||||||
|
|
|
@ -15,6 +15,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
|
window.isBuilder = true
|
||||||
window.closePreview = () => {
|
window.closePreview = () => {
|
||||||
store.update(state => ({
|
store.update(state => ({
|
||||||
...state,
|
...state,
|
||||||
|
|
|
@ -1,28 +0,0 @@
|
||||||
<script>
|
|
||||||
import { UserAvatar } from "@budibase/frontend-core"
|
|
||||||
|
|
||||||
export let users = []
|
|
||||||
|
|
||||||
$: uniqueUsers = unique(users)
|
|
||||||
|
|
||||||
const unique = users => {
|
|
||||||
let uniqueUsers = {}
|
|
||||||
users?.forEach(user => {
|
|
||||||
uniqueUsers[user.email] = user
|
|
||||||
})
|
|
||||||
return Object.values(uniqueUsers)
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<div class="avatars">
|
|
||||||
{#each uniqueUsers as user}
|
|
||||||
<UserAvatar {user} tooltipDirection="bottom" />
|
|
||||||
{/each}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
.avatars {
|
|
||||||
display: flex;
|
|
||||||
gap: 4px;
|
|
||||||
}
|
|
||||||
</style>
|
|
|
@ -15,6 +15,7 @@
|
||||||
Heading,
|
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,8 +26,8 @@
|
||||||
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 } from "components/portal/onboarding/tours.js"
|
||||||
import PreviewOverlay from "./_components/PreviewOverlay.svelte"
|
import PreviewOverlay from "./_components/PreviewOverlay.svelte"
|
||||||
|
|
||||||
export let application
|
export let application
|
||||||
|
@ -86,17 +87,10 @@
|
||||||
// Check if onboarding is enabled.
|
// Check if onboarding is enabled.
|
||||||
if (isEnabled(TENANT_FEATURE_FLAGS.ONBOARDING_TOUR)) {
|
if (isEnabled(TENANT_FEATURE_FLAGS.ONBOARDING_TOUR)) {
|
||||||
if (!$auth.user?.onboardedAt) {
|
if (!$auth.user?.onboardedAt) {
|
||||||
// Determine the correct step
|
|
||||||
const activeNav = $layout.children.find(c => $isActive(c.path))
|
|
||||||
const onboardingTour = TOURS[TOUR_KEYS.TOUR_BUILDER_ONBOARDING]
|
|
||||||
const targetStep = activeNav
|
|
||||||
? onboardingTour.find(step => step.route === activeNav?.path)
|
|
||||||
: null
|
|
||||||
await store.update(state => ({
|
await store.update(state => ({
|
||||||
...state,
|
...state,
|
||||||
onboarding: true,
|
onboarding: true,
|
||||||
tourKey: TOUR_KEYS.TOUR_BUILDER_ONBOARDING,
|
tourKey: TOUR_KEYS.TOUR_BUILDER_ONBOARDING,
|
||||||
tourStepKey: targetStep?.id,
|
|
||||||
}))
|
}))
|
||||||
} else {
|
} else {
|
||||||
// Feature tour date
|
// Feature tour date
|
||||||
|
@ -172,7 +166,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 +226,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;
|
||||||
|
|
|
@ -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({
|
||||||
|
|
|
@ -7,7 +7,7 @@
|
||||||
} from "stores/backend"
|
} from "stores/backend"
|
||||||
|
|
||||||
import { hasData } from "stores/selectors"
|
import { hasData } from "stores/selectors"
|
||||||
import { notifications, 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"
|
||||||
|
@ -15,7 +15,6 @@
|
||||||
import IntegrationIcon from "components/backend/DatasourceNavigator/IntegrationIcon.svelte"
|
import IntegrationIcon from "components/backend/DatasourceNavigator/IntegrationIcon.svelte"
|
||||||
import CreationPage from "components/common/CreationPage.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
|
||||||
|
@ -54,13 +53,9 @@
|
||||||
>
|
>
|
||||||
<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">
|
||||||
|
@ -116,13 +111,12 @@
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
margin-top: 12px;
|
margin-top: 12px;
|
||||||
margin-bottom: 24px;
|
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;
|
||||||
|
|
|
@ -19,7 +19,7 @@
|
||||||
heading={hasScreens ? "Create new screen" : "Create your first screen"}
|
heading={hasScreens ? "Create new screen" : "Create your first screen"}
|
||||||
>
|
>
|
||||||
<div class="subHeading">
|
<div class="subHeading">
|
||||||
<Body size="L">Start from scratch or create screens from your data</Body>
|
<Body>Start from scratch or create screens from your data</Body>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="cards">
|
<div class="cards">
|
||||||
|
@ -56,18 +56,18 @@
|
||||||
.subHeading :global(p) {
|
.subHeading :global(p) {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
margin-top: 12px;
|
margin-top: 12px;
|
||||||
margin-bottom: 24px;
|
margin-bottom: 36px;
|
||||||
color: var(--grey-6);
|
color: var(--spectrum-global-color-gray-600);
|
||||||
}
|
}
|
||||||
|
|
||||||
.cards {
|
.cards {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
gap: 24px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.card {
|
.card {
|
||||||
margin: 12px;
|
|
||||||
max-width: 235px;
|
max-width: 235px;
|
||||||
transition: filter 150ms;
|
transition: filter 150ms;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 />
|
||||||
|
|
|
@ -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}
|
||||||
|
|
|
@ -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>
|
|
@ -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 }
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -8,27 +8,31 @@
|
||||||
|
|
||||||
let structureLookupMap = {}
|
let structureLookupMap = {}
|
||||||
|
|
||||||
const registerBlockComponent = (id, order, parentId, instance) => {
|
const registerBlockComponent = (id, parentId, order, instance) => {
|
||||||
// Ensure child map exists
|
// Ensure child map exists
|
||||||
if (!structureLookupMap[parentId]) {
|
if (!structureLookupMap[parentId]) {
|
||||||
structureLookupMap[parentId] = {}
|
structureLookupMap[parentId] = {}
|
||||||
}
|
}
|
||||||
// Add this instance in this order, overwriting any existing instance in
|
// Add this instance in this order, overwriting any existing instance in
|
||||||
// this order in case of repeaters
|
// this order in case of repeaters
|
||||||
structureLookupMap[parentId][order] = instance
|
structureLookupMap[parentId][id] = { order, instance }
|
||||||
}
|
}
|
||||||
|
|
||||||
const unregisterBlockComponent = (order, parentId) => {
|
const unregisterBlockComponent = (id, parentId) => {
|
||||||
// Ensure child map exists
|
// Ensure child map exists
|
||||||
if (!structureLookupMap[parentId]) {
|
if (!structureLookupMap[parentId]) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
delete structureLookupMap[parentId][order]
|
delete structureLookupMap[parentId][id]
|
||||||
}
|
}
|
||||||
|
|
||||||
const eject = () => {
|
const eject = () => {
|
||||||
// Start the new structure with the root component
|
// Start the new structure with the root component
|
||||||
let definition = structureLookupMap[$component.id][0]
|
const rootMap = structureLookupMap[$component.id] || {}
|
||||||
|
let definition = Object.values(rootMap)[0]?.instance
|
||||||
|
if (!definition) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// Copy styles from block to root component
|
// Copy styles from block to root component
|
||||||
definition._styles = {
|
definition._styles = {
|
||||||
|
@ -49,10 +53,7 @@
|
||||||
const attachChildren = (rootComponent, map) => {
|
const attachChildren = (rootComponent, map) => {
|
||||||
// Transform map into children array
|
// Transform map into children array
|
||||||
let id = rootComponent._id
|
let id = rootComponent._id
|
||||||
const children = Object.entries(map[id] || {}).map(([order, instance]) => ({
|
const children = Object.values(map[id] || {})
|
||||||
order,
|
|
||||||
instance,
|
|
||||||
}))
|
|
||||||
if (!children.length) {
|
if (!children.length) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,6 +23,8 @@
|
||||||
// Create a fake component instance so that we can use the core Component
|
// Create a fake component instance so that we can use the core Component
|
||||||
// to render this part of the block, taking advantage of binding enrichment
|
// to render this part of the block, taking advantage of binding enrichment
|
||||||
$: id = `${block.id}-${context ?? rand}`
|
$: id = `${block.id}-${context ?? rand}`
|
||||||
|
$: parentId = $component?.id
|
||||||
|
$: inBuilder = $builderStore.inBuilder
|
||||||
$: instance = {
|
$: instance = {
|
||||||
_component: `@budibase/standard-components/${type}`,
|
_component: `@budibase/standard-components/${type}`,
|
||||||
_id: id,
|
_id: id,
|
||||||
|
@ -38,14 +40,14 @@
|
||||||
// Register this block component if we're inside the builder so it can be
|
// Register this block component if we're inside the builder so it can be
|
||||||
// ejected later
|
// ejected later
|
||||||
$: {
|
$: {
|
||||||
if ($builderStore.inBuilder) {
|
if (inBuilder) {
|
||||||
block.registerComponent(id, order ?? 0, $component?.id, instance)
|
block.registerComponent(id, parentId, order ?? 0, instance)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onDestroy(() => {
|
onDestroy(() => {
|
||||||
if ($builderStore.inBuilder) {
|
if (inBuilder) {
|
||||||
block.unregisterComponent(order ?? 0, $component?.id)
|
block.unregisterComponent(id, parentId)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -126,7 +126,7 @@
|
||||||
order={1}
|
order={1}
|
||||||
>
|
>
|
||||||
{#if enrichedSearchColumns?.length}
|
{#if enrichedSearchColumns?.length}
|
||||||
{#each enrichedSearchColumns as column, idx}
|
{#each enrichedSearchColumns as column, idx (column.name)}
|
||||||
<BlockComponent
|
<BlockComponent
|
||||||
type={column.componentType}
|
type={column.componentType}
|
||||||
props={{
|
props={{
|
||||||
|
|
|
@ -170,7 +170,7 @@
|
||||||
order={1}
|
order={1}
|
||||||
>
|
>
|
||||||
{#if enrichedSearchColumns?.length}
|
{#if enrichedSearchColumns?.length}
|
||||||
{#each enrichedSearchColumns as column, idx}
|
{#each enrichedSearchColumns as column, idx (column.name)}
|
||||||
<BlockComponent
|
<BlockComponent
|
||||||
type={column.componentType}
|
type={column.componentType}
|
||||||
props={{
|
props={{
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -478,7 +478,7 @@ export const enrichButtonActions = (actions, context) => {
|
||||||
actions.slice(i + 1),
|
actions.slice(i + 1),
|
||||||
newContext
|
newContext
|
||||||
)
|
)
|
||||||
resolve(await next())
|
resolve(typeof next === "function" ? await next() : true)
|
||||||
} else {
|
} else {
|
||||||
resolve(false)
|
resolve(false)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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>
|
|
||||||
|
|
|
@ -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>
|
|
@ -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>
|
|
|
@ -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>
|
|
|
@ -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">
|
||||||
|
|
|
@ -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}
|
||||||
|
|
|
@ -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>
|
||||||
|
|
||||||
|
|
|
@ -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}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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: {
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -1 +1 @@
|
||||||
Subproject commit 544c7e067de69832469cde673e59501480d6d98a
|
Subproject commit 9c564edb37cb619cb5971e10c4317fa6e7c5bb00
|
|
@ -98,7 +98,7 @@
|
||||||
"koa2-ratelimit": "1.1.1",
|
"koa2-ratelimit": "1.1.1",
|
||||||
"lodash": "4.17.21",
|
"lodash": "4.17.21",
|
||||||
"memorystream": "0.3.1",
|
"memorystream": "0.3.1",
|
||||||
"mongodb": "5.6",
|
"mongodb": "5.7",
|
||||||
"mssql": "9.1.1",
|
"mssql": "9.1.1",
|
||||||
"mysql2": "2.3.3",
|
"mysql2": "2.3.3",
|
||||||
"node-fetch": "2.6.7",
|
"node-fetch": "2.6.7",
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { events } from "@budibase/backend-core"
|
import { events } from "@budibase/backend-core"
|
||||||
import { AnalyticsPingRequest, PingSource } from "@budibase/types"
|
import { AnalyticsPingRequest, App, PingSource } from "@budibase/types"
|
||||||
import { DocumentType, isDevAppID } from "../../db/utils"
|
import { DocumentType, isDevAppID } from "../../db/utils"
|
||||||
import { context } from "@budibase/backend-core"
|
import { context } from "@budibase/backend-core"
|
||||||
|
|
||||||
|
@ -16,7 +16,7 @@ export const ping = async (ctx: any) => {
|
||||||
switch (body.source) {
|
switch (body.source) {
|
||||||
case PingSource.APP: {
|
case PingSource.APP: {
|
||||||
const db = context.getAppDB({ skip_setup: true })
|
const db = context.getAppDB({ skip_setup: true })
|
||||||
const appInfo = await db.get(DocumentType.APP_METADATA)
|
const appInfo = await db.get<App>(DocumentType.APP_METADATA)
|
||||||
let appId = context.getAppId()
|
let appId = context.getAppId()
|
||||||
|
|
||||||
if (isDevAppID(appId)) {
|
if (isDevAppID(appId)) {
|
||||||
|
|
|
@ -6,7 +6,7 @@ const KEYS_DOC = dbCore.StaticDatabases.GLOBAL.docs.apiKeys
|
||||||
async function getBuilderMainDoc() {
|
async function getBuilderMainDoc() {
|
||||||
const db = tenancy.getGlobalDB()
|
const db = tenancy.getGlobalDB()
|
||||||
try {
|
try {
|
||||||
return await db.get(KEYS_DOC)
|
return await db.get<any>(KEYS_DOC)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
// doesn't exist yet, nothing to get
|
// doesn't exist yet, nothing to get
|
||||||
return {
|
return {
|
||||||
|
|
|
@ -49,6 +49,7 @@ import {
|
||||||
MigrationType,
|
MigrationType,
|
||||||
PlanType,
|
PlanType,
|
||||||
Screen,
|
Screen,
|
||||||
|
SocketSession,
|
||||||
UserCtx,
|
UserCtx,
|
||||||
} from "@budibase/types"
|
} from "@budibase/types"
|
||||||
import { BASE_LAYOUT_PROP_IDS } from "../../constants/layouts"
|
import { BASE_LAYOUT_PROP_IDS } from "../../constants/layouts"
|
||||||
|
@ -183,6 +184,7 @@ export async function fetch(ctx: UserCtx) {
|
||||||
const appIds = apps
|
const appIds = apps
|
||||||
.filter((app: any) => app.status === "development")
|
.filter((app: any) => app.status === "development")
|
||||||
.map((app: any) => app.appId)
|
.map((app: any) => app.appId)
|
||||||
|
|
||||||
// get the locks for all the dev apps
|
// get the locks for all the dev apps
|
||||||
if (dev || all) {
|
if (dev || all) {
|
||||||
const locks = await getLocksById(appIds)
|
const locks = await getLocksById(appIds)
|
||||||
|
@ -197,7 +199,10 @@ export async function fetch(ctx: UserCtx) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx.body = await checkAppMetadata(apps)
|
// Enrich apps with all builder user sessions
|
||||||
|
const enrichedApps = await sdk.users.sessions.enrichApps(apps)
|
||||||
|
|
||||||
|
ctx.body = await checkAppMetadata(enrichedApps)
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function fetchAppDefinition(ctx: UserCtx) {
|
export async function fetchAppDefinition(ctx: UserCtx) {
|
||||||
|
@ -217,7 +222,7 @@ export async function fetchAppDefinition(ctx: UserCtx) {
|
||||||
|
|
||||||
export async function fetchAppPackage(ctx: UserCtx) {
|
export async function fetchAppPackage(ctx: UserCtx) {
|
||||||
const db = context.getAppDB()
|
const db = context.getAppDB()
|
||||||
let application = await db.get(DocumentType.APP_METADATA)
|
let application = await db.get<any>(DocumentType.APP_METADATA)
|
||||||
const layouts = await getLayouts()
|
const layouts = await getLayouts()
|
||||||
let screens = await getScreens()
|
let screens = await getScreens()
|
||||||
const license = await licensing.cache.getCachedLicense()
|
const license = await licensing.cache.getCachedLicense()
|
||||||
|
@ -448,7 +453,7 @@ export async function update(ctx: UserCtx) {
|
||||||
export async function updateClient(ctx: UserCtx) {
|
export async function updateClient(ctx: UserCtx) {
|
||||||
// Get current app version
|
// Get current app version
|
||||||
const db = context.getAppDB()
|
const db = context.getAppDB()
|
||||||
const application = await db.get(DocumentType.APP_METADATA)
|
const application = await db.get<App>(DocumentType.APP_METADATA)
|
||||||
const currentVersion = application.version
|
const currentVersion = application.version
|
||||||
|
|
||||||
// Update client library and manifest
|
// Update client library and manifest
|
||||||
|
@ -472,7 +477,7 @@ export async function updateClient(ctx: UserCtx) {
|
||||||
export async function revertClient(ctx: UserCtx) {
|
export async function revertClient(ctx: UserCtx) {
|
||||||
// Check app can be reverted
|
// Check app can be reverted
|
||||||
const db = context.getAppDB()
|
const db = context.getAppDB()
|
||||||
const application = await db.get(DocumentType.APP_METADATA)
|
const application = await db.get<App>(DocumentType.APP_METADATA)
|
||||||
if (!application.revertableVersion) {
|
if (!application.revertableVersion) {
|
||||||
ctx.throw(400, "There is no version to revert to")
|
ctx.throw(400, "There is no version to revert to")
|
||||||
}
|
}
|
||||||
|
@ -525,7 +530,7 @@ async function destroyApp(ctx: UserCtx) {
|
||||||
|
|
||||||
const db = dbCore.getDB(devAppId)
|
const db = dbCore.getDB(devAppId)
|
||||||
// standard app deletion flow
|
// standard app deletion flow
|
||||||
const app = await db.get(DocumentType.APP_METADATA)
|
const app = await db.get<App>(DocumentType.APP_METADATA)
|
||||||
const result = await db.destroy()
|
const result = await db.destroy()
|
||||||
await quotas.removeApp()
|
await quotas.removeApp()
|
||||||
await events.app.deleted(app)
|
await events.app.deleted(app)
|
||||||
|
@ -588,7 +593,7 @@ export async function sync(ctx: UserCtx) {
|
||||||
export async function updateAppPackage(appPackage: any, appId: any) {
|
export async function updateAppPackage(appPackage: any, appId: any) {
|
||||||
return context.doInAppContext(appId, async () => {
|
return context.doInAppContext(appId, async () => {
|
||||||
const db = context.getAppDB()
|
const db = context.getAppDB()
|
||||||
const application = await db.get(DocumentType.APP_METADATA)
|
const application = await db.get<App>(DocumentType.APP_METADATA)
|
||||||
|
|
||||||
const newAppPackage = { ...application, ...appPackage }
|
const newAppPackage = { ...application, ...appPackage }
|
||||||
if (appPackage._rev !== application._rev) {
|
if (appPackage._rev !== application._rev) {
|
||||||
|
|
|
@ -4,6 +4,7 @@ import { getFullUser } from "../../utilities/users"
|
||||||
import { roles, context } from "@budibase/backend-core"
|
import { roles, context } from "@budibase/backend-core"
|
||||||
import { groups } from "@budibase/pro"
|
import { groups } from "@budibase/pro"
|
||||||
import { ContextUser, User, Row, UserCtx } from "@budibase/types"
|
import { ContextUser, User, Row, UserCtx } from "@budibase/types"
|
||||||
|
import sdk from "../../sdk"
|
||||||
|
|
||||||
const PUBLIC_ROLE = roles.BUILTIN_ROLE_IDS.PUBLIC
|
const PUBLIC_ROLE = roles.BUILTIN_ROLE_IDS.PUBLIC
|
||||||
|
|
||||||
|
@ -41,7 +42,7 @@ export async function fetchSelf(ctx: UserCtx) {
|
||||||
// remove the full roles structure
|
// remove the full roles structure
|
||||||
delete user.roles
|
delete user.roles
|
||||||
try {
|
try {
|
||||||
const userTable = await db.get(InternalTables.USER_METADATA)
|
const userTable = await sdk.tables.getTable(InternalTables.USER_METADATA)
|
||||||
// specifically needs to make sure is enriched
|
// specifically needs to make sure is enriched
|
||||||
ctx.body = await outputProcessing(userTable, user as Row)
|
ctx.body = await outputProcessing(userTable, user as Row)
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
|
|
|
@ -16,6 +16,7 @@ import { setTestFlag, clearTestFlag } from "../../utilities/redis"
|
||||||
import { context, cache, events } from "@budibase/backend-core"
|
import { context, cache, events } from "@budibase/backend-core"
|
||||||
import { automations, features } from "@budibase/pro"
|
import { automations, features } from "@budibase/pro"
|
||||||
import {
|
import {
|
||||||
|
App,
|
||||||
Automation,
|
Automation,
|
||||||
AutomationActionStepId,
|
AutomationActionStepId,
|
||||||
AutomationResults,
|
AutomationResults,
|
||||||
|
@ -24,6 +25,7 @@ import {
|
||||||
import { getActionDefinitions as actionDefs } from "../../automations/actions"
|
import { getActionDefinitions as actionDefs } from "../../automations/actions"
|
||||||
import sdk from "../../sdk"
|
import sdk from "../../sdk"
|
||||||
import { db as dbCore } from "@budibase/backend-core"
|
import { db as dbCore } from "@budibase/backend-core"
|
||||||
|
import { builderSocket } from "../../websockets"
|
||||||
|
|
||||||
async function getActionDefinitions() {
|
async function getActionDefinitions() {
|
||||||
return removeDeprecated(await actionDefs())
|
return removeDeprecated(await actionDefs())
|
||||||
|
@ -107,6 +109,7 @@ export async function create(ctx: BBContext) {
|
||||||
...response,
|
...response,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
builderSocket?.emitAutomationUpdate(ctx, automation)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getNewSteps(oldAutomation: Automation, automation: Automation) {
|
export function getNewSteps(oldAutomation: Automation, automation: Automation) {
|
||||||
|
@ -150,7 +153,7 @@ export async function update(ctx: BBContext) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const oldAutomation = await db.get(automation._id)
|
const oldAutomation = await db.get<Automation>(automation._id)
|
||||||
automation = cleanAutomationInputs(automation)
|
automation = cleanAutomationInputs(automation)
|
||||||
automation = await checkForWebhooks({
|
automation = await checkForWebhooks({
|
||||||
oldAuto: oldAutomation,
|
oldAuto: oldAutomation,
|
||||||
|
@ -187,6 +190,7 @@ export async function update(ctx: BBContext) {
|
||||||
_id: response.id,
|
_id: response.id,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
builderSocket?.emitAutomationUpdate(ctx, automation)
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function fetch(ctx: BBContext) {
|
export async function fetch(ctx: BBContext) {
|
||||||
|
@ -207,7 +211,7 @@ export async function find(ctx: BBContext) {
|
||||||
export async function destroy(ctx: BBContext) {
|
export async function destroy(ctx: BBContext) {
|
||||||
const db = context.getAppDB()
|
const db = context.getAppDB()
|
||||||
const automationId = ctx.params.id
|
const automationId = ctx.params.id
|
||||||
const oldAutomation = await db.get(automationId)
|
const oldAutomation = await db.get<Automation>(automationId)
|
||||||
await checkForWebhooks({
|
await checkForWebhooks({
|
||||||
oldAuto: oldAutomation,
|
oldAuto: oldAutomation,
|
||||||
})
|
})
|
||||||
|
@ -215,6 +219,7 @@ export async function destroy(ctx: BBContext) {
|
||||||
await cleanupAutomationMetadata(automationId)
|
await cleanupAutomationMetadata(automationId)
|
||||||
ctx.body = await db.remove(automationId, ctx.params.rev)
|
ctx.body = await db.remove(automationId, ctx.params.rev)
|
||||||
await events.automation.deleted(oldAutomation)
|
await events.automation.deleted(oldAutomation)
|
||||||
|
builderSocket?.emitAutomationDeletion(ctx, automationId)
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function logSearch(ctx: BBContext) {
|
export async function logSearch(ctx: BBContext) {
|
||||||
|
@ -225,7 +230,7 @@ export async function clearLogError(ctx: BBContext) {
|
||||||
const { automationId, appId } = ctx.request.body
|
const { automationId, appId } = ctx.request.body
|
||||||
await context.doInAppContext(appId, async () => {
|
await context.doInAppContext(appId, async () => {
|
||||||
const db = context.getProdAppDB()
|
const db = context.getProdAppDB()
|
||||||
const metadata = await db.get(DocumentType.APP_METADATA)
|
const metadata = await db.get<App>(DocumentType.APP_METADATA)
|
||||||
if (!automationId) {
|
if (!automationId) {
|
||||||
delete metadata.automationErrors
|
delete metadata.automationErrors
|
||||||
} else if (
|
} else if (
|
||||||
|
@ -263,7 +268,7 @@ export async function getDefinitionList(ctx: BBContext) {
|
||||||
|
|
||||||
export async function trigger(ctx: BBContext) {
|
export async function trigger(ctx: BBContext) {
|
||||||
const db = context.getAppDB()
|
const db = context.getAppDB()
|
||||||
let automation = await db.get(ctx.params.id)
|
let automation = await db.get<Automation>(ctx.params.id)
|
||||||
|
|
||||||
let hasCollectStep = sdk.automations.utils.checkForCollectStep(automation)
|
let hasCollectStep = sdk.automations.utils.checkForCollectStep(automation)
|
||||||
if (hasCollectStep && (await features.isSyncAutomationsEnabled())) {
|
if (hasCollectStep && (await features.isSyncAutomationsEnabled())) {
|
||||||
|
@ -308,8 +313,8 @@ function prepareTestInput(input: any) {
|
||||||
|
|
||||||
export async function test(ctx: BBContext) {
|
export async function test(ctx: BBContext) {
|
||||||
const db = context.getAppDB()
|
const db = context.getAppDB()
|
||||||
let automation = await db.get(ctx.params.id)
|
let automation = await db.get<Automation>(ctx.params.id)
|
||||||
await setTestFlag(automation._id)
|
await setTestFlag(automation._id!)
|
||||||
const testInput = prepareTestInput(ctx.request.body)
|
const testInput = prepareTestInput(ctx.request.body)
|
||||||
const response = await triggers.externalTrigger(
|
const response = await triggers.externalTrigger(
|
||||||
automation,
|
automation,
|
||||||
|
@ -324,7 +329,7 @@ export async function test(ctx: BBContext) {
|
||||||
...ctx.request.body,
|
...ctx.request.body,
|
||||||
occurredAt: new Date().getTime(),
|
occurredAt: new Date().getTime(),
|
||||||
})
|
})
|
||||||
await clearTestFlag(automation._id)
|
await clearTestFlag(automation._id!)
|
||||||
ctx.body = response
|
ctx.body = response
|
||||||
await events.automation.tested(automation)
|
await events.automation.tested(automation)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import sdk from "../../sdk"
|
import sdk from "../../sdk"
|
||||||
import { events, context, db } from "@budibase/backend-core"
|
import { events, context, db } from "@budibase/backend-core"
|
||||||
import { DocumentType } from "../../db/utils"
|
import { DocumentType } from "../../db/utils"
|
||||||
import { Ctx } from "@budibase/types"
|
import { App, Ctx } from "@budibase/types"
|
||||||
|
|
||||||
interface ExportAppDumpRequest {
|
interface ExportAppDumpRequest {
|
||||||
excludeRows: boolean
|
excludeRows: boolean
|
||||||
|
@ -29,7 +29,7 @@ export async function exportAppDump(ctx: Ctx<ExportAppDumpRequest>) {
|
||||||
|
|
||||||
await context.doInAppContext(appId, async () => {
|
await context.doInAppContext(appId, async () => {
|
||||||
const appDb = context.getAppDB()
|
const appDb = context.getAppDB()
|
||||||
const app = await appDb.get(DocumentType.APP_METADATA)
|
const app = await appDb.get<App>(DocumentType.APP_METADATA)
|
||||||
await events.app.exported(app)
|
await events.app.exported(app)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { DocumentType } from "../../db/utils"
|
import { DocumentType } from "../../db/utils"
|
||||||
import { Plugin } from "@budibase/types"
|
import { App, Plugin } from "@budibase/types"
|
||||||
import { db as dbCore, context, tenancy } from "@budibase/backend-core"
|
import { db as dbCore, context, tenancy } from "@budibase/backend-core"
|
||||||
import { getComponentLibraryManifest } from "../../utilities/fileSystem"
|
import { getComponentLibraryManifest } from "../../utilities/fileSystem"
|
||||||
import { UserCtx } from "@budibase/types"
|
import { UserCtx } from "@budibase/types"
|
||||||
|
@ -7,7 +7,7 @@ import { UserCtx } from "@budibase/types"
|
||||||
export async function fetchAppComponentDefinitions(ctx: UserCtx) {
|
export async function fetchAppComponentDefinitions(ctx: UserCtx) {
|
||||||
try {
|
try {
|
||||||
const db = context.getAppDB()
|
const db = context.getAppDB()
|
||||||
const app = await db.get(DocumentType.APP_METADATA)
|
const app = await db.get<App>(DocumentType.APP_METADATA)
|
||||||
|
|
||||||
let componentManifests = await Promise.all(
|
let componentManifests = await Promise.all(
|
||||||
app.componentLibraries.map(async (library: any) => {
|
app.componentLibraries.map(async (library: any) => {
|
||||||
|
|
|
@ -9,7 +9,6 @@ import {
|
||||||
import { destroy as tableDestroy } from "./table/internal"
|
import { destroy as tableDestroy } from "./table/internal"
|
||||||
import { BuildSchemaErrors, InvalidColumns } from "../../constants"
|
import { BuildSchemaErrors, InvalidColumns } from "../../constants"
|
||||||
import { getIntegration } from "../../integrations"
|
import { getIntegration } from "../../integrations"
|
||||||
import { getDatasourceAndQuery } from "./row/utils"
|
|
||||||
import { invalidateDynamicVariables } from "../../threads/utils"
|
import { invalidateDynamicVariables } from "../../threads/utils"
|
||||||
import { db as dbCore, context, events } from "@budibase/backend-core"
|
import { db as dbCore, context, events } from "@budibase/backend-core"
|
||||||
import {
|
import {
|
||||||
|
@ -433,8 +432,7 @@ export async function destroy(ctx: UserCtx) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function find(ctx: UserCtx) {
|
export async function find(ctx: UserCtx) {
|
||||||
const db = context.getAppDB()
|
const datasource = await sdk.datasources.get(ctx.params.datasourceId)
|
||||||
const datasource = await db.get(ctx.params.datasourceId)
|
|
||||||
ctx.body = await sdk.datasources.removeSecretSingle(datasource)
|
ctx.body = await sdk.datasources.removeSecretSingle(datasource)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -442,15 +440,14 @@ export async function find(ctx: UserCtx) {
|
||||||
export async function query(ctx: UserCtx) {
|
export async function query(ctx: UserCtx) {
|
||||||
const queryJson = ctx.request.body
|
const queryJson = ctx.request.body
|
||||||
try {
|
try {
|
||||||
ctx.body = await getDatasourceAndQuery(queryJson)
|
ctx.body = await sdk.rows.utils.getDatasourceAndQuery(queryJson)
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
ctx.throw(400, err)
|
ctx.throw(400, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getExternalSchema(ctx: UserCtx) {
|
export async function getExternalSchema(ctx: UserCtx) {
|
||||||
const db = context.getAppDB()
|
const datasource = await sdk.datasources.get(ctx.params.datasourceId)
|
||||||
const datasource = await db.get(ctx.params.datasourceId)
|
|
||||||
const enrichedDatasource = await getAndMergeDatasource(datasource)
|
const enrichedDatasource = await getAndMergeDatasource(datasource)
|
||||||
const connector = await getConnector(enrichedDatasource)
|
const connector = await getConnector(enrichedDatasource)
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,48 @@
|
||||||
|
import os from "os"
|
||||||
|
import process from "process"
|
||||||
|
import { env } from "@budibase/backend-core"
|
||||||
|
import { GetDiagnosticsResponse, UserCtx } from "@budibase/types"
|
||||||
|
|
||||||
|
export async function systemDebugInfo(
|
||||||
|
ctx: UserCtx<void, GetDiagnosticsResponse>
|
||||||
|
) {
|
||||||
|
const { days, hours, minutes } = secondsToHMS(os.uptime())
|
||||||
|
const totalMemory = convertBytes(os.totalmem())
|
||||||
|
|
||||||
|
ctx.body = {
|
||||||
|
budibaseVersion: env.VERSION,
|
||||||
|
hosting: env.DEPLOYMENT_ENVIRONMENT,
|
||||||
|
nodeVersion: process.version,
|
||||||
|
platform: process.platform,
|
||||||
|
cpuArch: process.arch,
|
||||||
|
cpuCores: os.cpus().length,
|
||||||
|
cpuInfo: os.cpus()[0].model,
|
||||||
|
totalMemory: `${totalMemory.gb}GB`,
|
||||||
|
uptime: `${days} day(s), ${hours} hour(s), ${minutes} minute(s)`,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function secondsToHMS(seconds: number) {
|
||||||
|
const MINUTE_IN_SECONDS = 60
|
||||||
|
const HOUR_IN_SECONDS = 3600
|
||||||
|
const DAY_IN_SECONDS = HOUR_IN_SECONDS * 24
|
||||||
|
|
||||||
|
const minutes = Math.floor((seconds / MINUTE_IN_SECONDS) % 60)
|
||||||
|
const hours = Math.floor((seconds / HOUR_IN_SECONDS) % 24)
|
||||||
|
const days = Math.floor(seconds / DAY_IN_SECONDS)
|
||||||
|
|
||||||
|
return {
|
||||||
|
days,
|
||||||
|
hours,
|
||||||
|
minutes,
|
||||||
|
seconds,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function convertBytes(bytes: number) {
|
||||||
|
const kb = bytes / 1024
|
||||||
|
const mb = kb / 1024
|
||||||
|
const gb = mb / 1024
|
||||||
|
|
||||||
|
return { gb, mb, kb }
|
||||||
|
}
|
|
@ -7,7 +7,7 @@ import {
|
||||||
enableCronTrigger,
|
enableCronTrigger,
|
||||||
} from "../../../automations/utils"
|
} from "../../../automations/utils"
|
||||||
import { backups } from "@budibase/pro"
|
import { backups } from "@budibase/pro"
|
||||||
import { AppBackupTrigger } from "@budibase/types"
|
import { App, AppBackupTrigger } from "@budibase/types"
|
||||||
import sdk from "../../../sdk"
|
import sdk from "../../../sdk"
|
||||||
import { builderSocket } from "../../../websockets"
|
import { builderSocket } from "../../../websockets"
|
||||||
|
|
||||||
|
@ -44,7 +44,7 @@ async function storeDeploymentHistory(deployment: any) {
|
||||||
let deploymentDoc
|
let deploymentDoc
|
||||||
try {
|
try {
|
||||||
// theres only one deployment doc per app database
|
// theres only one deployment doc per app database
|
||||||
deploymentDoc = await db.get(DocumentType.DEPLOYMENTS)
|
deploymentDoc = await db.get<any>(DocumentType.DEPLOYMENTS)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
deploymentDoc = { _id: DocumentType.DEPLOYMENTS, history: {} }
|
deploymentDoc = { _id: DocumentType.DEPLOYMENTS, history: {} }
|
||||||
}
|
}
|
||||||
|
@ -113,7 +113,7 @@ export async function fetchDeployments(ctx: any) {
|
||||||
export async function deploymentProgress(ctx: any) {
|
export async function deploymentProgress(ctx: any) {
|
||||||
try {
|
try {
|
||||||
const db = context.getAppDB()
|
const db = context.getAppDB()
|
||||||
const deploymentDoc = await db.get(DocumentType.DEPLOYMENTS)
|
const deploymentDoc = await db.get<any>(DocumentType.DEPLOYMENTS)
|
||||||
ctx.body = deploymentDoc[ctx.params.deploymentId]
|
ctx.body = deploymentDoc[ctx.params.deploymentId]
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
ctx.throw(
|
ctx.throw(
|
||||||
|
@ -165,9 +165,9 @@ export const publishApp = async function (ctx: any) {
|
||||||
// app metadata is excluded as it is likely to be in conflict
|
// app metadata is excluded as it is likely to be in conflict
|
||||||
// replicate the app metadata document manually
|
// replicate the app metadata document manually
|
||||||
const db = context.getProdAppDB()
|
const db = context.getProdAppDB()
|
||||||
const appDoc = await devDb.get(DocumentType.APP_METADATA)
|
const appDoc = await devDb.get<App>(DocumentType.APP_METADATA)
|
||||||
try {
|
try {
|
||||||
const prodAppDoc = await db.get(DocumentType.APP_METADATA)
|
const prodAppDoc = await db.get<App>(DocumentType.APP_METADATA)
|
||||||
appDoc._rev = prodAppDoc._rev
|
appDoc._rev = prodAppDoc._rev
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
delete appDoc._rev
|
delete appDoc._rev
|
||||||
|
|
|
@ -6,6 +6,7 @@ import { clearLock as redisClearLock } from "../../utilities/redis"
|
||||||
import { DocumentType } from "../../db/utils"
|
import { DocumentType } from "../../db/utils"
|
||||||
import { context, env as envCore } from "@budibase/backend-core"
|
import { context, env as envCore } from "@budibase/backend-core"
|
||||||
import { events, db as dbCore, cache } from "@budibase/backend-core"
|
import { events, db as dbCore, cache } from "@budibase/backend-core"
|
||||||
|
import { App } from "@budibase/types"
|
||||||
|
|
||||||
async function redirect(ctx: any, method: string, path: string = "global") {
|
async function redirect(ctx: any, method: string, path: string = "global") {
|
||||||
const { devPath } = ctx.params
|
const { devPath } = ctx.params
|
||||||
|
@ -81,7 +82,7 @@ export async function revert(ctx: any) {
|
||||||
if (!exists) {
|
if (!exists) {
|
||||||
throw new Error("App must be deployed to be reverted.")
|
throw new Error("App must be deployed to be reverted.")
|
||||||
}
|
}
|
||||||
const deploymentDoc = await db.get(DocumentType.DEPLOYMENTS)
|
const deploymentDoc = await db.get<any>(DocumentType.DEPLOYMENTS)
|
||||||
if (
|
if (
|
||||||
!deploymentDoc.history ||
|
!deploymentDoc.history ||
|
||||||
Object.keys(deploymentDoc.history).length === 0
|
Object.keys(deploymentDoc.history).length === 0
|
||||||
|
@ -104,7 +105,7 @@ export async function revert(ctx: any) {
|
||||||
|
|
||||||
// update appID in reverted app to be dev version again
|
// update appID in reverted app to be dev version again
|
||||||
const db = context.getAppDB()
|
const db = context.getAppDB()
|
||||||
const appDoc = await db.get(DocumentType.APP_METADATA)
|
const appDoc = await db.get<App>(DocumentType.APP_METADATA)
|
||||||
appDoc.appId = appId
|
appDoc.appId = appId
|
||||||
appDoc.instance._id = appId
|
appDoc.instance._id = appId
|
||||||
await db.put(appDoc)
|
await db.put(appDoc)
|
||||||
|
|
|
@ -14,7 +14,7 @@ export async function addRev(
|
||||||
id = DocumentType.APP_METADATA
|
id = DocumentType.APP_METADATA
|
||||||
}
|
}
|
||||||
const db = context.getAppDB()
|
const db = context.getAppDB()
|
||||||
const dbDoc = await db.get(id)
|
const dbDoc = await db.get<any>(id)
|
||||||
body._rev = dbDoc._rev
|
body._rev = dbDoc._rev
|
||||||
// update ID in case it is an app ID
|
// update ID in case it is an app ID
|
||||||
body._id = id
|
body._id = id
|
||||||
|
|
|
@ -9,6 +9,7 @@ import { quotas } from "@budibase/pro"
|
||||||
import { events, context, utils, constants } from "@budibase/backend-core"
|
import { events, context, utils, constants } from "@budibase/backend-core"
|
||||||
import sdk from "../../../sdk"
|
import sdk from "../../../sdk"
|
||||||
import { QueryEvent } from "../../../threads/definitions"
|
import { QueryEvent } from "../../../threads/definitions"
|
||||||
|
import { Query } from "@budibase/types"
|
||||||
|
|
||||||
const Runner = new Thread(ThreadType.QUERY, {
|
const Runner = new Thread(ThreadType.QUERY, {
|
||||||
timeoutMs: env.QUERY_THREAD_TIMEOUT || 10000,
|
timeoutMs: env.QUERY_THREAD_TIMEOUT || 10000,
|
||||||
|
@ -206,7 +207,7 @@ async function execute(
|
||||||
) {
|
) {
|
||||||
const db = context.getAppDB()
|
const db = context.getAppDB()
|
||||||
|
|
||||||
const query = await db.get(ctx.params.queryId)
|
const query = await db.get<Query>(ctx.params.queryId)
|
||||||
const { datasource, envVars } = await sdk.datasources.getWithEnvVars(
|
const { datasource, envVars } = await sdk.datasources.getWithEnvVars(
|
||||||
query.datasourceId
|
query.datasourceId
|
||||||
)
|
)
|
||||||
|
@ -275,7 +276,7 @@ export async function executeV2(
|
||||||
|
|
||||||
const removeDynamicVariables = async (queryId: any) => {
|
const removeDynamicVariables = async (queryId: any) => {
|
||||||
const db = context.getAppDB()
|
const db = context.getAppDB()
|
||||||
const query = await db.get(queryId)
|
const query = await db.get<Query>(queryId)
|
||||||
const datasource = await sdk.datasources.get(query.datasourceId)
|
const datasource = await sdk.datasources.get(query.datasourceId)
|
||||||
const dynamicVariables = datasource.config?.dynamicVariables as any[]
|
const dynamicVariables = datasource.config?.dynamicVariables as any[]
|
||||||
|
|
||||||
|
@ -298,7 +299,7 @@ export async function destroy(ctx: any) {
|
||||||
const db = context.getAppDB()
|
const db = context.getAppDB()
|
||||||
const queryId = ctx.params.queryId
|
const queryId = ctx.params.queryId
|
||||||
await removeDynamicVariables(queryId)
|
await removeDynamicVariables(queryId)
|
||||||
const query = await db.get(queryId)
|
const query = await db.get<Query>(queryId)
|
||||||
const datasource = await sdk.datasources.get(query.datasourceId)
|
const datasource = await sdk.datasources.get(query.datasourceId)
|
||||||
await db.remove(ctx.params.queryId, ctx.params.revId)
|
await db.remove(ctx.params.queryId, ctx.params.revId)
|
||||||
ctx.message = `Query deleted.`
|
ctx.message = `Query deleted.`
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import { roles, context, events, db as dbCore } from "@budibase/backend-core"
|
import { roles, context, events, db as dbCore } from "@budibase/backend-core"
|
||||||
import { getUserMetadataParams, InternalTables } from "../../db/utils"
|
import { getUserMetadataParams, InternalTables } from "../../db/utils"
|
||||||
import { UserCtx, Database } from "@budibase/types"
|
import { UserCtx, Database, UserRoles, Role } from "@budibase/types"
|
||||||
|
import sdk from "../../sdk"
|
||||||
|
|
||||||
const UpdateRolesOptions = {
|
const UpdateRolesOptions = {
|
||||||
CREATED: "created",
|
CREATED: "created",
|
||||||
|
@ -13,23 +14,23 @@ async function updateRolesOnUserTable(
|
||||||
updateOption: string,
|
updateOption: string,
|
||||||
roleVersion: string | undefined
|
roleVersion: string | undefined
|
||||||
) {
|
) {
|
||||||
const table = await db.get(InternalTables.USER_METADATA)
|
const table = await sdk.tables.getTable(InternalTables.USER_METADATA)
|
||||||
const schema = table.schema
|
const schema = table.schema
|
||||||
const remove = updateOption === UpdateRolesOptions.REMOVED
|
const remove = updateOption === UpdateRolesOptions.REMOVED
|
||||||
let updated = false
|
let updated = false
|
||||||
for (let prop of Object.keys(schema)) {
|
for (let prop of Object.keys(schema)) {
|
||||||
if (prop === "roleId") {
|
if (prop === "roleId") {
|
||||||
updated = true
|
updated = true
|
||||||
const constraints = schema[prop].constraints
|
const constraints = schema[prop].constraints!
|
||||||
const updatedRoleId =
|
const updatedRoleId =
|
||||||
roleVersion === roles.RoleIDVersion.NAME
|
roleVersion === roles.RoleIDVersion.NAME
|
||||||
? roles.getExternalRoleID(roleId, roleVersion)
|
? roles.getExternalRoleID(roleId, roleVersion)
|
||||||
: roleId
|
: roleId
|
||||||
const indexOfRoleId = constraints.inclusion.indexOf(updatedRoleId)
|
const indexOfRoleId = constraints.inclusion!.indexOf(updatedRoleId)
|
||||||
if (remove && indexOfRoleId !== -1) {
|
if (remove && indexOfRoleId !== -1) {
|
||||||
constraints.inclusion.splice(indexOfRoleId, 1)
|
constraints.inclusion!.splice(indexOfRoleId, 1)
|
||||||
} else if (!remove && indexOfRoleId === -1) {
|
} else if (!remove && indexOfRoleId === -1) {
|
||||||
constraints.inclusion.push(updatedRoleId)
|
constraints.inclusion!.push(updatedRoleId)
|
||||||
}
|
}
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
@ -69,7 +70,7 @@ export async function save(ctx: UserCtx) {
|
||||||
|
|
||||||
let dbRole
|
let dbRole
|
||||||
if (!isCreate) {
|
if (!isCreate) {
|
||||||
dbRole = await db.get(_id)
|
dbRole = await db.get<UserRoles>(_id)
|
||||||
}
|
}
|
||||||
if (dbRole && dbRole.name !== name && isNewVersion) {
|
if (dbRole && dbRole.name !== name && isNewVersion) {
|
||||||
ctx.throw(400, "Cannot change custom role name")
|
ctx.throw(400, "Cannot change custom role name")
|
||||||
|
@ -105,7 +106,7 @@ export async function destroy(ctx: UserCtx) {
|
||||||
// make sure has the prefix (if it has it then it won't be added)
|
// make sure has the prefix (if it has it then it won't be added)
|
||||||
roleId = dbCore.generateRoleID(roleId)
|
roleId = dbCore.generateRoleID(roleId)
|
||||||
}
|
}
|
||||||
const role = await db.get(roleId)
|
const role = await db.get<Role>(roleId)
|
||||||
// first check no users actively attached to role
|
// first check no users actively attached to role
|
||||||
const users = (
|
const users = (
|
||||||
await db.allDocs(
|
await db.allDocs(
|
||||||
|
|
|
@ -23,14 +23,13 @@ import {
|
||||||
isRowId,
|
isRowId,
|
||||||
isSQL,
|
isSQL,
|
||||||
} from "../../../integrations/utils"
|
} from "../../../integrations/utils"
|
||||||
import { getDatasourceAndQuery } from "./utils"
|
import { getDatasourceAndQuery } from "../../../sdk/app/rows/utils"
|
||||||
import { FieldTypes } from "../../../constants"
|
import { FieldTypes } from "../../../constants"
|
||||||
import { processObjectSync } from "@budibase/string-templates"
|
import { processObjectSync } from "@budibase/string-templates"
|
||||||
import { cloneDeep } from "lodash/fp"
|
import { cloneDeep } from "lodash/fp"
|
||||||
import { processDates, processFormulas } from "../../../utilities/rowProcessor"
|
import { processDates, processFormulas } from "../../../utilities/rowProcessor"
|
||||||
import { db as dbCore } from "@budibase/backend-core"
|
import { db as dbCore } from "@budibase/backend-core"
|
||||||
import sdk from "../../../sdk"
|
import sdk from "../../../sdk"
|
||||||
import { isEditableColumn } from "../../../sdk/app/tables/validation"
|
|
||||||
|
|
||||||
export interface ManyRelationship {
|
export interface ManyRelationship {
|
||||||
tableId?: string
|
tableId?: string
|
||||||
|
|
|
@ -1,30 +1,20 @@
|
||||||
import {
|
import { FieldTypes, NoEmptyFilterStrings } from "../../../constants"
|
||||||
FieldTypes,
|
|
||||||
NoEmptyFilterStrings,
|
|
||||||
SortDirection,
|
|
||||||
} from "../../../constants"
|
|
||||||
import {
|
import {
|
||||||
breakExternalTableId,
|
breakExternalTableId,
|
||||||
breakRowIdField,
|
breakRowIdField,
|
||||||
} from "../../../integrations/utils"
|
} from "../../../integrations/utils"
|
||||||
import { ExternalRequest, RunConfig } from "./ExternalRequest"
|
import { ExternalRequest, RunConfig } from "./ExternalRequest"
|
||||||
import * as exporters from "../view/exporters"
|
|
||||||
import { apiFileReturn } from "../../../utilities/fileSystem"
|
|
||||||
import {
|
import {
|
||||||
Datasource,
|
Datasource,
|
||||||
IncludeRelationship,
|
IncludeRelationship,
|
||||||
Operation,
|
Operation,
|
||||||
PaginationJson,
|
|
||||||
Row,
|
Row,
|
||||||
SortJson,
|
|
||||||
Table,
|
Table,
|
||||||
UserCtx,
|
UserCtx,
|
||||||
} from "@budibase/types"
|
} from "@budibase/types"
|
||||||
import sdk from "../../../sdk"
|
import sdk from "../../../sdk"
|
||||||
import * as utils from "./utils"
|
import * as utils from "./utils"
|
||||||
|
|
||||||
const { cleanExportRows } = require("./utils")
|
|
||||||
|
|
||||||
async function getRow(
|
async function getRow(
|
||||||
tableId: string,
|
tableId: string,
|
||||||
rowId: string,
|
rowId: string,
|
||||||
|
@ -59,6 +49,7 @@ export async function handleRequest(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return new ExternalRequest(operation, tableId, opts?.datasource).run(
|
return new ExternalRequest(operation, tableId, opts?.datasource).run(
|
||||||
opts || {}
|
opts || {}
|
||||||
)
|
)
|
||||||
|
@ -114,21 +105,6 @@ export async function save(ctx: UserCtx) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function fetchView(ctx: UserCtx) {
|
|
||||||
// there are no views in external datasources, shouldn't ever be called
|
|
||||||
// for now just fetch
|
|
||||||
const split = ctx.params.viewName.split("all_")
|
|
||||||
ctx.params.tableId = split[1] ? split[1] : split[0]
|
|
||||||
return fetch(ctx)
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function fetch(ctx: UserCtx) {
|
|
||||||
const tableId = ctx.params.tableId
|
|
||||||
return handleRequest(Operation.READ, tableId, {
|
|
||||||
includeSqlRelationships: IncludeRelationship.INCLUDE,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function find(ctx: UserCtx) {
|
export async function find(ctx: UserCtx) {
|
||||||
const id = ctx.params.rowId
|
const id = ctx.params.rowId
|
||||||
const tableId = ctx.params.tableId
|
const tableId = ctx.params.tableId
|
||||||
|
@ -161,129 +137,6 @@ export async function bulkDestroy(ctx: UserCtx) {
|
||||||
return { response: { ok: true }, rows: responses.map(resp => resp.row) }
|
return { response: { ok: true }, rows: responses.map(resp => resp.row) }
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function search(ctx: UserCtx) {
|
|
||||||
const tableId = ctx.params.tableId
|
|
||||||
const { paginate, query, ...params } = ctx.request.body
|
|
||||||
let { bookmark, limit } = params
|
|
||||||
if (!bookmark && paginate) {
|
|
||||||
bookmark = 1
|
|
||||||
}
|
|
||||||
let paginateObj = {}
|
|
||||||
|
|
||||||
if (paginate) {
|
|
||||||
paginateObj = {
|
|
||||||
// add one so we can track if there is another page
|
|
||||||
limit: limit,
|
|
||||||
page: bookmark,
|
|
||||||
}
|
|
||||||
} else if (params && limit) {
|
|
||||||
paginateObj = {
|
|
||||||
limit: limit,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
let sort: SortJson | undefined
|
|
||||||
if (params.sort) {
|
|
||||||
const direction =
|
|
||||||
params.sortOrder === "descending"
|
|
||||||
? SortDirection.DESCENDING
|
|
||||||
: SortDirection.ASCENDING
|
|
||||||
sort = {
|
|
||||||
[params.sort]: { direction },
|
|
||||||
}
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
const rows = (await handleRequest(Operation.READ, tableId, {
|
|
||||||
filters: query,
|
|
||||||
sort,
|
|
||||||
paginate: paginateObj as PaginationJson,
|
|
||||||
includeSqlRelationships: IncludeRelationship.INCLUDE,
|
|
||||||
})) as Row[]
|
|
||||||
let hasNextPage = false
|
|
||||||
if (paginate && rows.length === limit) {
|
|
||||||
const nextRows = (await handleRequest(Operation.READ, tableId, {
|
|
||||||
filters: query,
|
|
||||||
sort,
|
|
||||||
paginate: {
|
|
||||||
limit: 1,
|
|
||||||
page: bookmark * limit + 1,
|
|
||||||
},
|
|
||||||
includeSqlRelationships: IncludeRelationship.INCLUDE,
|
|
||||||
})) as Row[]
|
|
||||||
hasNextPage = nextRows.length > 0
|
|
||||||
}
|
|
||||||
// need wrapper object for bookmarks etc when paginating
|
|
||||||
return { rows, hasNextPage, bookmark: bookmark + 1 }
|
|
||||||
} catch (err: any) {
|
|
||||||
if (err.message && err.message.includes("does not exist")) {
|
|
||||||
throw new Error(
|
|
||||||
`Table updated externally, please re-fetch - ${err.message}`
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
throw err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function exportRows(ctx: UserCtx) {
|
|
||||||
const { datasourceId, tableName } = breakExternalTableId(ctx.params.tableId)
|
|
||||||
const format = ctx.query.format
|
|
||||||
const { columns } = ctx.request.body
|
|
||||||
const datasource = await sdk.datasources.get(datasourceId!)
|
|
||||||
if (!datasource || !datasource.entities) {
|
|
||||||
ctx.throw(400, "Datasource has not been configured for plus API.")
|
|
||||||
}
|
|
||||||
|
|
||||||
if (ctx.request.body.rows) {
|
|
||||||
ctx.request.body = {
|
|
||||||
query: {
|
|
||||||
oneOf: {
|
|
||||||
_id: ctx.request.body.rows.map((row: string) => {
|
|
||||||
const ids = JSON.parse(
|
|
||||||
decodeURI(row).replace(/'/g, `"`).replace(/%2C/g, ",")
|
|
||||||
)
|
|
||||||
if (ids.length > 1) {
|
|
||||||
ctx.throw(400, "Export data does not support composite keys.")
|
|
||||||
}
|
|
||||||
return ids[0]
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let result = await search(ctx)
|
|
||||||
let rows: Row[] = []
|
|
||||||
|
|
||||||
// Filter data to only specified columns if required
|
|
||||||
|
|
||||||
if (columns && columns.length) {
|
|
||||||
for (let i = 0; i < result.rows.length; i++) {
|
|
||||||
rows[i] = {}
|
|
||||||
for (let column of columns) {
|
|
||||||
rows[i][column] = result.rows[i][column]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
rows = result.rows
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!tableName) {
|
|
||||||
ctx.throw(400, "Could not find table name.")
|
|
||||||
}
|
|
||||||
let schema = datasource.entities[tableName].schema
|
|
||||||
let exportRows = cleanExportRows(rows, schema, format, columns)
|
|
||||||
|
|
||||||
let headers = Object.keys(schema)
|
|
||||||
|
|
||||||
// @ts-ignore
|
|
||||||
const exporter = exporters[format]
|
|
||||||
const filename = `export.${format}`
|
|
||||||
|
|
||||||
// send down the file
|
|
||||||
ctx.attachment(filename)
|
|
||||||
return apiFileReturn(exporter(headers, exportRows))
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function fetchEnrichedRow(ctx: UserCtx) {
|
export async function fetchEnrichedRow(ctx: UserCtx) {
|
||||||
const id = ctx.params.rowId
|
const id = ctx.params.rowId
|
||||||
const tableId = ctx.params.tableId
|
const tableId = ctx.params.tableId
|
||||||
|
|
|
@ -5,6 +5,9 @@ import { isExternalTable } from "../../../integrations/utils"
|
||||||
import { Ctx } from "@budibase/types"
|
import { Ctx } from "@budibase/types"
|
||||||
import * as utils from "./utils"
|
import * as utils from "./utils"
|
||||||
import { gridSocket } from "../../../websockets"
|
import { gridSocket } from "../../../websockets"
|
||||||
|
import sdk from "../../../sdk"
|
||||||
|
import * as exporters from "../view/exporters"
|
||||||
|
import { apiFileReturn } from "../../../utilities/fileSystem"
|
||||||
|
|
||||||
function pickApi(tableId: any) {
|
function pickApi(tableId: any) {
|
||||||
if (isExternalTable(tableId)) {
|
if (isExternalTable(tableId)) {
|
||||||
|
@ -64,14 +67,26 @@ export const save = async (ctx: any) => {
|
||||||
}
|
}
|
||||||
export async function fetchView(ctx: any) {
|
export async function fetchView(ctx: any) {
|
||||||
const tableId = utils.getTableId(ctx)
|
const tableId = utils.getTableId(ctx)
|
||||||
ctx.body = await quotas.addQuery(() => pickApi(tableId).fetchView(ctx), {
|
const viewName = decodeURIComponent(ctx.params.viewName)
|
||||||
datasourceId: tableId,
|
|
||||||
})
|
const { calculation, group, field } = ctx.query
|
||||||
|
|
||||||
|
ctx.body = await quotas.addQuery(
|
||||||
|
() =>
|
||||||
|
sdk.rows.fetchView(tableId, viewName, {
|
||||||
|
calculation,
|
||||||
|
group,
|
||||||
|
field,
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
datasourceId: tableId,
|
||||||
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function fetch(ctx: any) {
|
export async function fetch(ctx: any) {
|
||||||
const tableId = utils.getTableId(ctx)
|
const tableId = utils.getTableId(ctx)
|
||||||
ctx.body = await quotas.addQuery(() => pickApi(tableId).fetch(ctx), {
|
ctx.body = await quotas.addQuery(() => sdk.rows.fetch(tableId), {
|
||||||
datasourceId: tableId,
|
datasourceId: tableId,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -119,8 +134,14 @@ export async function destroy(ctx: any) {
|
||||||
|
|
||||||
export async function search(ctx: any) {
|
export async function search(ctx: any) {
|
||||||
const tableId = utils.getTableId(ctx)
|
const tableId = utils.getTableId(ctx)
|
||||||
|
|
||||||
|
const searchParams = {
|
||||||
|
...ctx.request.body,
|
||||||
|
tableId,
|
||||||
|
}
|
||||||
|
|
||||||
ctx.status = 200
|
ctx.status = 200
|
||||||
ctx.body = await quotas.addQuery(() => pickApi(tableId).search(ctx), {
|
ctx.body = await quotas.addQuery(() => sdk.rows.search(searchParams), {
|
||||||
datasourceId: tableId,
|
datasourceId: tableId,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -150,7 +171,33 @@ export async function fetchEnrichedRow(ctx: any) {
|
||||||
|
|
||||||
export const exportRows = async (ctx: any) => {
|
export const exportRows = async (ctx: any) => {
|
||||||
const tableId = utils.getTableId(ctx)
|
const tableId = utils.getTableId(ctx)
|
||||||
ctx.body = await quotas.addQuery(() => pickApi(tableId).exportRows(ctx), {
|
|
||||||
datasourceId: tableId,
|
const format = ctx.query.format
|
||||||
})
|
|
||||||
|
const { rows, columns, query } = ctx.request.body
|
||||||
|
if (typeof format !== "string" || !exporters.isFormat(format)) {
|
||||||
|
ctx.throw(
|
||||||
|
400,
|
||||||
|
`Format ${format} not valid. Valid values: ${Object.values(
|
||||||
|
exporters.Format
|
||||||
|
)}`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.body = await quotas.addQuery(
|
||||||
|
async () => {
|
||||||
|
const { fileName, content } = await sdk.rows.exportRows({
|
||||||
|
tableId,
|
||||||
|
format,
|
||||||
|
rowIds: rows,
|
||||||
|
columns,
|
||||||
|
query,
|
||||||
|
})
|
||||||
|
ctx.attachment(fileName)
|
||||||
|
return apiFileReturn(content)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
datasourceId: tableId,
|
||||||
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,9 +1,7 @@
|
||||||
import * as linkRows from "../../../db/linkedRows"
|
import * as linkRows from "../../../db/linkedRows"
|
||||||
import {
|
import {
|
||||||
generateRowID,
|
generateRowID,
|
||||||
getRowParams,
|
|
||||||
getTableIDFromRowID,
|
getTableIDFromRowID,
|
||||||
DocumentType,
|
|
||||||
InternalTables,
|
InternalTables,
|
||||||
} from "../../../db/utils"
|
} from "../../../db/utils"
|
||||||
import * as userController from "../user"
|
import * as userController from "../user"
|
||||||
|
@ -14,78 +12,11 @@ import {
|
||||||
} from "../../../utilities/rowProcessor"
|
} from "../../../utilities/rowProcessor"
|
||||||
import { FieldTypes } from "../../../constants"
|
import { FieldTypes } from "../../../constants"
|
||||||
import * as utils from "./utils"
|
import * as utils from "./utils"
|
||||||
import { fullSearch, paginatedSearch } from "./internalSearch"
|
|
||||||
import { getGlobalUsersFromMetadata } from "../../../utilities/global"
|
|
||||||
import * as inMemoryViews from "../../../db/inMemoryView"
|
|
||||||
import env from "../../../environment"
|
|
||||||
import {
|
|
||||||
migrateToInMemoryView,
|
|
||||||
migrateToDesignView,
|
|
||||||
getFromDesignDoc,
|
|
||||||
getFromMemoryDoc,
|
|
||||||
} from "../view/utils"
|
|
||||||
import { cloneDeep } from "lodash/fp"
|
import { cloneDeep } from "lodash/fp"
|
||||||
import { context, db as dbCore } from "@budibase/backend-core"
|
import { context, db as dbCore } from "@budibase/backend-core"
|
||||||
import { finaliseRow, updateRelatedFormula } from "./staticFormula"
|
import { finaliseRow, updateRelatedFormula } from "./staticFormula"
|
||||||
import { csv, json, jsonWithSchema, Format } from "../view/exporters"
|
import { UserCtx, LinkDocumentValue, Row, Table } from "@budibase/types"
|
||||||
import { apiFileReturn } from "../../../utilities/fileSystem"
|
import sdk from "../../../sdk"
|
||||||
import {
|
|
||||||
UserCtx,
|
|
||||||
Database,
|
|
||||||
LinkDocumentValue,
|
|
||||||
Row,
|
|
||||||
Table,
|
|
||||||
} from "@budibase/types"
|
|
||||||
|
|
||||||
import { cleanExportRows } from "./utils"
|
|
||||||
|
|
||||||
const CALCULATION_TYPES = {
|
|
||||||
SUM: "sum",
|
|
||||||
COUNT: "count",
|
|
||||||
STATS: "stats",
|
|
||||||
}
|
|
||||||
|
|
||||||
async function getView(db: Database, viewName: string) {
|
|
||||||
let mainGetter = env.SELF_HOSTED ? getFromDesignDoc : getFromMemoryDoc
|
|
||||||
let secondaryGetter = env.SELF_HOSTED ? getFromMemoryDoc : getFromDesignDoc
|
|
||||||
let migration = env.SELF_HOSTED ? migrateToDesignView : migrateToInMemoryView
|
|
||||||
let viewInfo,
|
|
||||||
migrate = false
|
|
||||||
try {
|
|
||||||
viewInfo = await mainGetter(db, viewName)
|
|
||||||
} catch (err: any) {
|
|
||||||
// check if it can be retrieved from design doc (needs migrated)
|
|
||||||
if (err.status !== 404) {
|
|
||||||
viewInfo = null
|
|
||||||
} else {
|
|
||||||
viewInfo = await secondaryGetter(db, viewName)
|
|
||||||
migrate = !!viewInfo
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (migrate) {
|
|
||||||
await migration(db, viewName)
|
|
||||||
}
|
|
||||||
if (!viewInfo) {
|
|
||||||
throw "View does not exist."
|
|
||||||
}
|
|
||||||
return viewInfo
|
|
||||||
}
|
|
||||||
|
|
||||||
async function getRawTableData(ctx: UserCtx, db: Database, tableId: string) {
|
|
||||||
let rows
|
|
||||||
if (tableId === InternalTables.USER_METADATA) {
|
|
||||||
await userController.fetchMetadata(ctx)
|
|
||||||
rows = ctx.body
|
|
||||||
} else {
|
|
||||||
const response = await db.allDocs(
|
|
||||||
getRowParams(tableId, null, {
|
|
||||||
include_docs: true,
|
|
||||||
})
|
|
||||||
)
|
|
||||||
rows = response.rows.map(row => row.doc)
|
|
||||||
}
|
|
||||||
return rows as Row[]
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function patch(ctx: UserCtx) {
|
export async function patch(ctx: UserCtx) {
|
||||||
const db = context.getAppDB()
|
const db = context.getAppDB()
|
||||||
|
@ -94,7 +25,7 @@ export async function patch(ctx: UserCtx) {
|
||||||
const isUserTable = tableId === InternalTables.USER_METADATA
|
const isUserTable = tableId === InternalTables.USER_METADATA
|
||||||
let oldRow
|
let oldRow
|
||||||
try {
|
try {
|
||||||
let dbTable = await db.get(tableId)
|
let dbTable = await sdk.tables.getTable(tableId)
|
||||||
oldRow = await outputProcessing(
|
oldRow = await outputProcessing(
|
||||||
dbTable,
|
dbTable,
|
||||||
await utils.findRow(ctx, tableId, inputs._id)
|
await utils.findRow(ctx, tableId, inputs._id)
|
||||||
|
@ -110,7 +41,7 @@ export async function patch(ctx: UserCtx) {
|
||||||
throw "Row does not exist"
|
throw "Row does not exist"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let dbTable = await db.get(tableId)
|
let dbTable = await sdk.tables.getTable(tableId)
|
||||||
// need to build up full patch fields before coerce
|
// need to build up full patch fields before coerce
|
||||||
let combinedRow: any = cloneDeep(oldRow)
|
let combinedRow: any = cloneDeep(oldRow)
|
||||||
for (let key of Object.keys(inputs)) {
|
for (let key of Object.keys(inputs)) {
|
||||||
|
@ -165,7 +96,7 @@ export async function save(ctx: UserCtx) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// this returns the table and row incase they have been updated
|
// this returns the table and row incase they have been updated
|
||||||
const dbTable = await db.get(inputs.tableId)
|
const dbTable = await sdk.tables.getTable(inputs.tableId)
|
||||||
|
|
||||||
// need to copy the table so it can be differenced on way out
|
// need to copy the table so it can be differenced on way out
|
||||||
const tableClone = cloneDeep(dbTable)
|
const tableClone = cloneDeep(dbTable)
|
||||||
|
@ -195,85 +126,9 @@ export async function save(ctx: UserCtx) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function fetchView(ctx: UserCtx) {
|
|
||||||
const viewName = decodeURIComponent(ctx.params.viewName)
|
|
||||||
|
|
||||||
// if this is a table view being looked for just transfer to that
|
|
||||||
if (viewName.startsWith(DocumentType.TABLE)) {
|
|
||||||
ctx.params.tableId = viewName
|
|
||||||
return fetch(ctx)
|
|
||||||
}
|
|
||||||
|
|
||||||
const db = context.getAppDB()
|
|
||||||
const { calculation, group, field } = ctx.query
|
|
||||||
const viewInfo = await getView(db, viewName)
|
|
||||||
let response
|
|
||||||
if (env.SELF_HOSTED) {
|
|
||||||
response = await db.query(`database/${viewName}`, {
|
|
||||||
include_docs: !calculation,
|
|
||||||
group: !!group,
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
const tableId = viewInfo.meta.tableId
|
|
||||||
const data = await getRawTableData(ctx, db, tableId)
|
|
||||||
response = await inMemoryViews.runView(
|
|
||||||
viewInfo,
|
|
||||||
calculation as string,
|
|
||||||
!!group,
|
|
||||||
data
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
let rows
|
|
||||||
if (!calculation) {
|
|
||||||
response.rows = response.rows.map(row => row.doc)
|
|
||||||
let table
|
|
||||||
try {
|
|
||||||
table = await db.get(viewInfo.meta.tableId)
|
|
||||||
} catch (err) {
|
|
||||||
/* istanbul ignore next */
|
|
||||||
table = {
|
|
||||||
schema: {},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
rows = await outputProcessing(table, response.rows)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (calculation === CALCULATION_TYPES.STATS) {
|
|
||||||
response.rows = response.rows.map(row => ({
|
|
||||||
group: row.key,
|
|
||||||
field,
|
|
||||||
...row.value,
|
|
||||||
avg: row.value.sum / row.value.count,
|
|
||||||
}))
|
|
||||||
rows = response.rows
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
calculation === CALCULATION_TYPES.COUNT ||
|
|
||||||
calculation === CALCULATION_TYPES.SUM
|
|
||||||
) {
|
|
||||||
rows = response.rows.map(row => ({
|
|
||||||
group: row.key,
|
|
||||||
field,
|
|
||||||
value: row.value,
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
return rows
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function fetch(ctx: UserCtx) {
|
|
||||||
const db = context.getAppDB()
|
|
||||||
|
|
||||||
const tableId = ctx.params.tableId
|
|
||||||
let table = await db.get(tableId)
|
|
||||||
let rows = await getRawTableData(ctx, db, tableId)
|
|
||||||
return outputProcessing(table, rows)
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function find(ctx: UserCtx) {
|
export async function find(ctx: UserCtx) {
|
||||||
const db = dbCore.getDB(ctx.appId)
|
const db = dbCore.getDB(ctx.appId)
|
||||||
const table = await db.get(ctx.params.tableId)
|
const table = await sdk.tables.getTable(ctx.params.tableId)
|
||||||
let row = await utils.findRow(ctx, ctx.params.tableId, ctx.params.rowId)
|
let row = await utils.findRow(ctx, ctx.params.tableId, ctx.params.rowId)
|
||||||
row = await outputProcessing(table, row)
|
row = await outputProcessing(table, row)
|
||||||
return row
|
return row
|
||||||
|
@ -282,13 +137,13 @@ export async function find(ctx: UserCtx) {
|
||||||
export async function destroy(ctx: UserCtx) {
|
export async function destroy(ctx: UserCtx) {
|
||||||
const db = context.getAppDB()
|
const db = context.getAppDB()
|
||||||
const { _id } = ctx.request.body
|
const { _id } = ctx.request.body
|
||||||
let row = await db.get(_id)
|
let row = await db.get<Row>(_id)
|
||||||
let _rev = ctx.request.body._rev || row._rev
|
let _rev = ctx.request.body._rev || row._rev
|
||||||
|
|
||||||
if (row.tableId !== ctx.params.tableId) {
|
if (row.tableId !== ctx.params.tableId) {
|
||||||
throw "Supplied tableId doesn't match the row's tableId"
|
throw "Supplied tableId doesn't match the row's tableId"
|
||||||
}
|
}
|
||||||
const table = await db.get(row.tableId)
|
const table = await sdk.tables.getTable(row.tableId)
|
||||||
// update the row to include full relationships before deleting them
|
// update the row to include full relationships before deleting them
|
||||||
row = await outputProcessing(table, row, { squash: false })
|
row = await outputProcessing(table, row, { squash: false })
|
||||||
// now remove the relationships
|
// now remove the relationships
|
||||||
|
@ -318,7 +173,7 @@ export async function destroy(ctx: UserCtx) {
|
||||||
export async function bulkDestroy(ctx: UserCtx) {
|
export async function bulkDestroy(ctx: UserCtx) {
|
||||||
const db = context.getAppDB()
|
const db = context.getAppDB()
|
||||||
const tableId = ctx.params.tableId
|
const tableId = ctx.params.tableId
|
||||||
const table = await db.get(tableId)
|
const table = await sdk.tables.getTable(tableId)
|
||||||
let { rows } = ctx.request.body
|
let { rows } = ctx.request.body
|
||||||
|
|
||||||
// before carrying out any updates, make sure the rows are ready to be returned
|
// before carrying out any updates, make sure the rows are ready to be returned
|
||||||
|
@ -354,108 +209,13 @@ export async function bulkDestroy(ctx: UserCtx) {
|
||||||
return { response: { ok: true }, rows: processedRows }
|
return { response: { ok: true }, rows: processedRows }
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function search(ctx: UserCtx) {
|
|
||||||
// Fetch the whole table when running in cypress, as search doesn't work
|
|
||||||
if (!env.COUCH_DB_URL && env.isCypress()) {
|
|
||||||
return { rows: await fetch(ctx) }
|
|
||||||
}
|
|
||||||
|
|
||||||
const { tableId } = ctx.params
|
|
||||||
const db = context.getAppDB()
|
|
||||||
const { paginate, query, ...params } = ctx.request.body
|
|
||||||
params.version = ctx.version
|
|
||||||
params.tableId = tableId
|
|
||||||
|
|
||||||
let table
|
|
||||||
if (params.sort && !params.sortType) {
|
|
||||||
table = await db.get(tableId)
|
|
||||||
const schema = table.schema
|
|
||||||
const sortField = schema[params.sort]
|
|
||||||
params.sortType = sortField.type == "number" ? "number" : "string"
|
|
||||||
}
|
|
||||||
|
|
||||||
let response
|
|
||||||
if (paginate) {
|
|
||||||
response = await paginatedSearch(query, params)
|
|
||||||
} else {
|
|
||||||
response = await fullSearch(query, params)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Enrich search results with relationships
|
|
||||||
if (response.rows && response.rows.length) {
|
|
||||||
// enrich with global users if from users table
|
|
||||||
if (tableId === InternalTables.USER_METADATA) {
|
|
||||||
response.rows = await getGlobalUsersFromMetadata(response.rows)
|
|
||||||
}
|
|
||||||
table = table || (await db.get(tableId))
|
|
||||||
response.rows = await outputProcessing(table, response.rows)
|
|
||||||
}
|
|
||||||
|
|
||||||
return response
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function exportRows(ctx: UserCtx) {
|
|
||||||
const db = context.getAppDB()
|
|
||||||
const table = await db.get(ctx.params.tableId)
|
|
||||||
const rowIds = ctx.request.body.rows
|
|
||||||
let format = ctx.query.format
|
|
||||||
if (typeof format !== "string") {
|
|
||||||
ctx.throw(400, "Format parameter is not valid")
|
|
||||||
}
|
|
||||||
const { columns, query } = ctx.request.body
|
|
||||||
|
|
||||||
let result
|
|
||||||
if (rowIds) {
|
|
||||||
let response = (
|
|
||||||
await db.allDocs({
|
|
||||||
include_docs: true,
|
|
||||||
keys: rowIds,
|
|
||||||
})
|
|
||||||
).rows.map(row => row.doc)
|
|
||||||
|
|
||||||
result = await outputProcessing(table, response)
|
|
||||||
} else if (query) {
|
|
||||||
let searchResponse = await search(ctx)
|
|
||||||
result = searchResponse.rows
|
|
||||||
}
|
|
||||||
|
|
||||||
let rows: Row[] = []
|
|
||||||
let schema = table.schema
|
|
||||||
|
|
||||||
// Filter data to only specified columns if required
|
|
||||||
if (columns && columns.length) {
|
|
||||||
for (let i = 0; i < result.length; i++) {
|
|
||||||
rows[i] = {}
|
|
||||||
for (let column of columns) {
|
|
||||||
rows[i][column] = result[i][column]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
rows = result
|
|
||||||
}
|
|
||||||
|
|
||||||
let exportRows = cleanExportRows(rows, schema, format, columns)
|
|
||||||
if (format === Format.CSV) {
|
|
||||||
ctx.attachment("export.csv")
|
|
||||||
return apiFileReturn(csv(Object.keys(rows[0]), exportRows))
|
|
||||||
} else if (format === Format.JSON) {
|
|
||||||
ctx.attachment("export.json")
|
|
||||||
return apiFileReturn(json(exportRows))
|
|
||||||
} else if (format === Format.JSON_WITH_SCHEMA) {
|
|
||||||
ctx.attachment("export.json")
|
|
||||||
return apiFileReturn(jsonWithSchema(schema, exportRows))
|
|
||||||
} else {
|
|
||||||
throw "Format not recognised"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function fetchEnrichedRow(ctx: UserCtx) {
|
export async function fetchEnrichedRow(ctx: UserCtx) {
|
||||||
const db = context.getAppDB()
|
const db = context.getAppDB()
|
||||||
const tableId = ctx.params.tableId
|
const tableId = ctx.params.tableId
|
||||||
const rowId = ctx.params.rowId
|
const rowId = ctx.params.rowId
|
||||||
// need table to work out where links go in row
|
// need table to work out where links go in row
|
||||||
let [table, row] = await Promise.all([
|
let [table, row] = await Promise.all([
|
||||||
db.get(tableId),
|
sdk.tables.getTable(tableId),
|
||||||
utils.findRow(ctx, tableId, rowId),
|
utils.findRow(ctx, tableId, rowId),
|
||||||
])
|
])
|
||||||
// get the link docs
|
// get the link docs
|
||||||
|
|
|
@ -8,8 +8,9 @@ import { FieldTypes, FormulaTypes } from "../../../constants"
|
||||||
import { context } from "@budibase/backend-core"
|
import { context } from "@budibase/backend-core"
|
||||||
import { Table, Row } from "@budibase/types"
|
import { Table, Row } from "@budibase/types"
|
||||||
import * as linkRows from "../../../db/linkedRows"
|
import * as linkRows from "../../../db/linkedRows"
|
||||||
const { isEqual } = require("lodash")
|
import sdk from "../../../sdk"
|
||||||
const { cloneDeep } = require("lodash/fp")
|
import { isEqual } from "lodash"
|
||||||
|
import { cloneDeep } from "lodash/fp"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This function runs through a list of enriched rows, looks at the rows which
|
* This function runs through a list of enriched rows, looks at the rows which
|
||||||
|
@ -148,7 +149,7 @@ export async function finaliseRow(
|
||||||
await db.put(table)
|
await db.put(table)
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
if (err.status === 409) {
|
if (err.status === 409) {
|
||||||
const updatedTable = await db.get(table._id)
|
const updatedTable = await sdk.tables.getTable(table._id)
|
||||||
let response = processAutoColumn(null, updatedTable, row, {
|
let response = processAutoColumn(null, updatedTable, row, {
|
||||||
reprocessing: true,
|
reprocessing: true,
|
||||||
})
|
})
|
||||||
|
|
|
@ -1,14 +1,19 @@
|
||||||
import { InternalTables } from "../../../db/utils"
|
import { InternalTables } from "../../../db/utils"
|
||||||
import * as userController from "../user"
|
import * as userController from "../user"
|
||||||
import { FieldTypes } from "../../../constants"
|
|
||||||
import { context } from "@budibase/backend-core"
|
import { context } from "@budibase/backend-core"
|
||||||
import { makeExternalQuery } from "../../../integrations/base/query"
|
import { Ctx, FieldType, Row, Table, UserCtx } from "@budibase/types"
|
||||||
import { FieldType, Row, Table, UserCtx } from "@budibase/types"
|
import { FieldTypes } from "../../../constants"
|
||||||
import { Format } from "../view/exporters"
|
|
||||||
import sdk from "../../../sdk"
|
import sdk from "../../../sdk"
|
||||||
|
|
||||||
const validateJs = require("validate.js")
|
import validateJs from "validate.js"
|
||||||
const { cloneDeep } = require("lodash/fp")
|
import { cloneDeep } from "lodash/fp"
|
||||||
|
|
||||||
|
function isForeignKey(key: string, table: Table) {
|
||||||
|
const relationships = Object.values(table.schema).filter(
|
||||||
|
column => column.type === FieldType.LINK
|
||||||
|
)
|
||||||
|
return relationships.some(relationship => relationship.foreignKey === key)
|
||||||
|
}
|
||||||
|
|
||||||
validateJs.extend(validateJs.validators.datetime, {
|
validateJs.extend(validateJs.validators.datetime, {
|
||||||
parse: function (value: string) {
|
parse: function (value: string) {
|
||||||
|
@ -20,19 +25,6 @@ validateJs.extend(validateJs.validators.datetime, {
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
function isForeignKey(key: string, table: Table) {
|
|
||||||
const relationships = Object.values(table.schema).filter(
|
|
||||||
column => column.type === FieldType.LINK
|
|
||||||
)
|
|
||||||
return relationships.some(relationship => relationship.foreignKey === key)
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function getDatasourceAndQuery(json: any) {
|
|
||||||
const datasourceId = json.endpoint.datasourceId
|
|
||||||
const datasource = await sdk.datasources.get(datasourceId)
|
|
||||||
return makeExternalQuery(datasource, json)
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function findRow(ctx: UserCtx, tableId: string, rowId: string) {
|
export async function findRow(ctx: UserCtx, tableId: string, rowId: string) {
|
||||||
const db = context.getAppDB()
|
const db = context.getAppDB()
|
||||||
let row
|
let row
|
||||||
|
@ -52,6 +44,18 @@ export async function findRow(ctx: UserCtx, tableId: string, rowId: string) {
|
||||||
return row
|
return row
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getTableId(ctx: Ctx) {
|
||||||
|
if (ctx.request.body && ctx.request.body.tableId) {
|
||||||
|
return ctx.request.body.tableId
|
||||||
|
}
|
||||||
|
if (ctx.params && ctx.params.tableId) {
|
||||||
|
return ctx.params.tableId
|
||||||
|
}
|
||||||
|
if (ctx.params && ctx.params.viewName) {
|
||||||
|
return ctx.params.viewName
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export async function validate({
|
export async function validate({
|
||||||
tableId,
|
tableId,
|
||||||
row,
|
row,
|
||||||
|
@ -81,8 +85,8 @@ export async function validate({
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
// special case for options, need to always allow unselected (empty)
|
// special case for options, need to always allow unselected (empty)
|
||||||
if (type === FieldTypes.OPTIONS && constraints.inclusion) {
|
if (type === FieldTypes.OPTIONS && constraints?.inclusion) {
|
||||||
constraints.inclusion.push(null, "")
|
constraints.inclusion.push(null as any, "")
|
||||||
}
|
}
|
||||||
let res
|
let res
|
||||||
|
|
||||||
|
@ -94,13 +98,13 @@ export async function validate({
|
||||||
}
|
}
|
||||||
row[fieldName].map((val: any) => {
|
row[fieldName].map((val: any) => {
|
||||||
if (
|
if (
|
||||||
!constraints.inclusion.includes(val) &&
|
!constraints?.inclusion?.includes(val) &&
|
||||||
constraints.inclusion.length !== 0
|
constraints?.inclusion?.length !== 0
|
||||||
) {
|
) {
|
||||||
errors[fieldName] = "Field not in list"
|
errors[fieldName] = "Field not in list"
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
} else if (constraints.presence && row[fieldName].length === 0) {
|
} else if (constraints?.presence && row[fieldName].length === 0) {
|
||||||
// non required MultiSelect creates an empty array, which should not throw errors
|
// non required MultiSelect creates an empty array, which should not throw errors
|
||||||
errors[fieldName] = [`${fieldName} is required`]
|
errors[fieldName] = [`${fieldName} is required`]
|
||||||
}
|
}
|
||||||
|
@ -128,52 +132,3 @@ export async function validate({
|
||||||
}
|
}
|
||||||
return { valid: Object.keys(errors).length === 0, errors }
|
return { valid: Object.keys(errors).length === 0, errors }
|
||||||
}
|
}
|
||||||
|
|
||||||
export function cleanExportRows(
|
|
||||||
rows: any[],
|
|
||||||
schema: any,
|
|
||||||
format: string,
|
|
||||||
columns: string[]
|
|
||||||
) {
|
|
||||||
let cleanRows = [...rows]
|
|
||||||
|
|
||||||
const relationships = Object.entries(schema)
|
|
||||||
.filter((entry: any[]) => entry[1].type === FieldTypes.LINK)
|
|
||||||
.map(entry => entry[0])
|
|
||||||
|
|
||||||
relationships.forEach(column => {
|
|
||||||
cleanRows.forEach(row => {
|
|
||||||
delete row[column]
|
|
||||||
})
|
|
||||||
delete schema[column]
|
|
||||||
})
|
|
||||||
|
|
||||||
if (format === Format.CSV) {
|
|
||||||
// Intended to append empty values in export
|
|
||||||
const schemaKeys = Object.keys(schema)
|
|
||||||
for (let key of schemaKeys) {
|
|
||||||
if (columns?.length && columns.indexOf(key) > 0) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
for (let row of cleanRows) {
|
|
||||||
if (row[key] == null) {
|
|
||||||
row[key] = undefined
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return cleanRows
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getTableId(ctx: any) {
|
|
||||||
if (ctx.request.body && ctx.request.body.tableId) {
|
|
||||||
return ctx.request.body.tableId
|
|
||||||
}
|
|
||||||
if (ctx.params && ctx.params.tableId) {
|
|
||||||
return ctx.params.tableId
|
|
||||||
}
|
|
||||||
if (ctx.params && ctx.params.viewName) {
|
|
||||||
return ctx.params.viewName
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -7,7 +7,7 @@ import {
|
||||||
roles,
|
roles,
|
||||||
} from "@budibase/backend-core"
|
} from "@budibase/backend-core"
|
||||||
import { updateAppPackage } from "./application"
|
import { updateAppPackage } from "./application"
|
||||||
import { Plugin, ScreenProps, BBContext } from "@budibase/types"
|
import { Plugin, ScreenProps, BBContext, Screen } from "@budibase/types"
|
||||||
import { builderSocket } from "../../websockets"
|
import { builderSocket } from "../../websockets"
|
||||||
|
|
||||||
export async function fetch(ctx: BBContext) {
|
export async function fetch(ctx: BBContext) {
|
||||||
|
@ -64,7 +64,7 @@ export async function save(ctx: BBContext) {
|
||||||
})
|
})
|
||||||
|
|
||||||
// Update the app metadata
|
// Update the app metadata
|
||||||
const application = await db.get(DocumentType.APP_METADATA)
|
const application = await db.get<any>(DocumentType.APP_METADATA)
|
||||||
let usedPlugins = application.usedPlugins || []
|
let usedPlugins = application.usedPlugins || []
|
||||||
|
|
||||||
requiredPlugins.forEach((plugin: Plugin) => {
|
requiredPlugins.forEach((plugin: Plugin) => {
|
||||||
|
@ -104,7 +104,7 @@ export async function save(ctx: BBContext) {
|
||||||
export async function destroy(ctx: BBContext) {
|
export async function destroy(ctx: BBContext) {
|
||||||
const db = context.getAppDB()
|
const db = context.getAppDB()
|
||||||
const id = ctx.params.screenId
|
const id = ctx.params.screenId
|
||||||
const screen = await db.get(id)
|
const screen = await db.get<Screen>(id)
|
||||||
|
|
||||||
await db.remove(id, ctx.params.screenRev)
|
await db.remove(id, ctx.params.screenRev)
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
require("svelte/register")
|
require("svelte/register")
|
||||||
|
|
||||||
import { join } from "../../../utilities/centralPath"
|
import { join } from "../../../utilities/centralPath"
|
||||||
const uuid = require("uuid")
|
import uuid from "uuid"
|
||||||
import { ObjectStoreBuckets } from "../../../constants"
|
import { ObjectStoreBuckets } from "../../../constants"
|
||||||
import { processString } from "@budibase/string-templates"
|
import { processString } from "@budibase/string-templates"
|
||||||
import {
|
import {
|
||||||
|
@ -16,6 +16,7 @@ import AWS from "aws-sdk"
|
||||||
import fs from "fs"
|
import fs from "fs"
|
||||||
import sdk from "../../../sdk"
|
import sdk from "../../../sdk"
|
||||||
import * as pro from "@budibase/pro"
|
import * as pro from "@budibase/pro"
|
||||||
|
import { App } from "@budibase/types"
|
||||||
|
|
||||||
const send = require("koa-send")
|
const send = require("koa-send")
|
||||||
|
|
||||||
|
@ -110,7 +111,7 @@ export const serveApp = async function (ctx: any) {
|
||||||
let db
|
let db
|
||||||
try {
|
try {
|
||||||
db = context.getAppDB({ skip_setup: true })
|
db = context.getAppDB({ skip_setup: true })
|
||||||
const appInfo = await db.get(DocumentType.APP_METADATA)
|
const appInfo = await db.get<any>(DocumentType.APP_METADATA)
|
||||||
let appId = context.getAppId()
|
let appId = context.getAppId()
|
||||||
|
|
||||||
if (!env.isJest()) {
|
if (!env.isJest()) {
|
||||||
|
@ -177,7 +178,7 @@ export const serveApp = async function (ctx: any) {
|
||||||
|
|
||||||
export const serveBuilderPreview = async function (ctx: any) {
|
export const serveBuilderPreview = async function (ctx: any) {
|
||||||
const db = context.getAppDB({ skip_setup: true })
|
const db = context.getAppDB({ skip_setup: true })
|
||||||
const appInfo = await db.get(DocumentType.APP_METADATA)
|
const appInfo = await db.get<App>(DocumentType.APP_METADATA)
|
||||||
|
|
||||||
if (!env.isJest()) {
|
if (!env.isJest()) {
|
||||||
let appId = context.getAppId()
|
let appId = context.getAppId()
|
||||||
|
|
|
@ -323,7 +323,7 @@ export async function save(ctx: UserCtx) {
|
||||||
|
|
||||||
// Since tables are stored inside datasources, we need to notify clients
|
// Since tables are stored inside datasources, we need to notify clients
|
||||||
// that the datasource definition changed
|
// that the datasource definition changed
|
||||||
const updatedDatasource = await db.get(datasource._id)
|
const updatedDatasource = await sdk.datasources.get(datasource._id!)
|
||||||
builderSocket?.emitDatasourceUpdate(ctx, updatedDatasource)
|
builderSocket?.emitDatasourceUpdate(ctx, updatedDatasource)
|
||||||
|
|
||||||
return tableToSave
|
return tableToSave
|
||||||
|
@ -354,7 +354,7 @@ export async function destroy(ctx: UserCtx) {
|
||||||
|
|
||||||
// Since tables are stored inside datasources, we need to notify clients
|
// Since tables are stored inside datasources, we need to notify clients
|
||||||
// that the datasource definition changed
|
// that the datasource definition changed
|
||||||
const updatedDatasource = await db.get(datasource._id)
|
const updatedDatasource = await sdk.datasources.get(datasource._id!)
|
||||||
builderSocket?.emitDatasourceUpdate(ctx, updatedDatasource)
|
builderSocket?.emitDatasourceUpdate(ctx, updatedDatasource)
|
||||||
|
|
||||||
return tableToDelete
|
return tableToDelete
|
||||||
|
|
|
@ -15,7 +15,7 @@ import { isEqual } from "lodash"
|
||||||
import { cloneDeep } from "lodash/fp"
|
import { cloneDeep } from "lodash/fp"
|
||||||
import sdk from "../../../sdk"
|
import sdk from "../../../sdk"
|
||||||
|
|
||||||
function checkAutoColumns(table: Table, oldTable: Table) {
|
function checkAutoColumns(table: Table, oldTable?: Table) {
|
||||||
if (!table.schema) {
|
if (!table.schema) {
|
||||||
return table
|
return table
|
||||||
}
|
}
|
||||||
|
@ -46,7 +46,7 @@ export async function save(ctx: any) {
|
||||||
// if the table obj had an _id then it will have been retrieved
|
// if the table obj had an _id then it will have been retrieved
|
||||||
let oldTable
|
let oldTable
|
||||||
if (ctx.request.body && ctx.request.body._id) {
|
if (ctx.request.body && ctx.request.body._id) {
|
||||||
oldTable = await db.get(ctx.request.body._id)
|
oldTable = await sdk.tables.getTable(ctx.request.body._id)
|
||||||
}
|
}
|
||||||
|
|
||||||
// check all types are correct
|
// check all types are correct
|
||||||
|
@ -70,8 +70,8 @@ export async function save(ctx: any) {
|
||||||
if (oldTable && oldTable.schema) {
|
if (oldTable && oldTable.schema) {
|
||||||
for (let propKey of Object.keys(tableToSave.schema)) {
|
for (let propKey of Object.keys(tableToSave.schema)) {
|
||||||
let oldColumn = oldTable.schema[propKey]
|
let oldColumn = oldTable.schema[propKey]
|
||||||
if (oldColumn && oldColumn.type === "internal") {
|
if (oldColumn && oldColumn.type === FieldTypes.INTERNAL) {
|
||||||
oldColumn.type = "auto"
|
oldColumn.type = FieldTypes.AUTO
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -138,7 +138,7 @@ export async function save(ctx: any) {
|
||||||
|
|
||||||
export async function destroy(ctx: any) {
|
export async function destroy(ctx: any) {
|
||||||
const db = context.getAppDB()
|
const db = context.getAppDB()
|
||||||
const tableToDelete = await db.get(ctx.params.tableId)
|
const tableToDelete = await sdk.tables.getTable(ctx.params.tableId)
|
||||||
|
|
||||||
// Delete all rows for that table
|
// Delete all rows for that table
|
||||||
const rowsData = await db.allDocs(
|
const rowsData = await db.allDocs(
|
||||||
|
@ -160,7 +160,7 @@ export async function destroy(ctx: any) {
|
||||||
})
|
})
|
||||||
|
|
||||||
// don't remove the table itself until very end
|
// don't remove the table itself until very end
|
||||||
await db.remove(tableToDelete._id, tableToDelete._rev)
|
await db.remove(tableToDelete._id!, tableToDelete._rev)
|
||||||
|
|
||||||
// remove table search index
|
// remove table search index
|
||||||
if (!env.isTest() || env.COUCH_DB_URL) {
|
if (!env.isTest() || env.COUCH_DB_URL) {
|
||||||
|
@ -184,7 +184,6 @@ export async function destroy(ctx: any) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function bulkImport(ctx: any) {
|
export async function bulkImport(ctx: any) {
|
||||||
const db = context.getAppDB()
|
|
||||||
const table = await sdk.tables.getTable(ctx.params.tableId)
|
const table = await sdk.tables.getTable(ctx.params.tableId)
|
||||||
const { rows, identifierFields } = ctx.request.body
|
const { rows, identifierFields } = ctx.request.body
|
||||||
await handleDataImport(ctx.user, table, rows, identifierFields)
|
await handleDataImport(ctx.user, table, rows, identifierFields)
|
||||||
|
|
|
@ -20,16 +20,10 @@ import viewTemplate from "../view/viewBuilder"
|
||||||
import { cloneDeep } from "lodash/fp"
|
import { cloneDeep } from "lodash/fp"
|
||||||
import { quotas } from "@budibase/pro"
|
import { quotas } from "@budibase/pro"
|
||||||
import { events, context } from "@budibase/backend-core"
|
import { events, context } from "@budibase/backend-core"
|
||||||
import {
|
import { ContextUser, Datasource, SourceName, Table } from "@budibase/types"
|
||||||
ContextUser,
|
|
||||||
Database,
|
|
||||||
Datasource,
|
|
||||||
SourceName,
|
|
||||||
Table,
|
|
||||||
} from "@budibase/types"
|
|
||||||
|
|
||||||
export async function clearColumns(table: any, columnNames: any) {
|
export async function clearColumns(table: any, columnNames: any) {
|
||||||
const db: Database = context.getAppDB()
|
const db = context.getAppDB()
|
||||||
const rows = await db.allDocs(
|
const rows = await db.allDocs(
|
||||||
getRowParams(table._id, null, {
|
getRowParams(table._id, null, {
|
||||||
include_docs: true,
|
include_docs: true,
|
||||||
|
|
|
@ -1,120 +0,0 @@
|
||||||
import { exportRows } from "../row/external"
|
|
||||||
import sdk from "../../../sdk"
|
|
||||||
import { ExternalRequest } from "../row/ExternalRequest"
|
|
||||||
|
|
||||||
// @ts-ignore
|
|
||||||
sdk.datasources = {
|
|
||||||
get: jest.fn(),
|
|
||||||
}
|
|
||||||
|
|
||||||
jest.mock("../row/ExternalRequest")
|
|
||||||
jest.mock("../view/exporters", () => ({
|
|
||||||
csv: jest.fn(),
|
|
||||||
Format: {
|
|
||||||
CSV: "csv",
|
|
||||||
},
|
|
||||||
}))
|
|
||||||
jest.mock("../../../utilities/fileSystem")
|
|
||||||
|
|
||||||
function getUserCtx() {
|
|
||||||
return {
|
|
||||||
params: {
|
|
||||||
tableId: "datasource__tablename",
|
|
||||||
},
|
|
||||||
query: {
|
|
||||||
format: "csv",
|
|
||||||
},
|
|
||||||
request: {
|
|
||||||
body: {},
|
|
||||||
},
|
|
||||||
throw: jest.fn(() => {
|
|
||||||
throw "Err"
|
|
||||||
}),
|
|
||||||
attachment: jest.fn(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
describe("external row controller", () => {
|
|
||||||
describe("exportRows", () => {
|
|
||||||
beforeAll(() => {
|
|
||||||
//@ts-ignore
|
|
||||||
jest.spyOn(ExternalRequest.prototype, "run").mockImplementation(() => [])
|
|
||||||
})
|
|
||||||
|
|
||||||
afterEach(() => {
|
|
||||||
jest.clearAllMocks()
|
|
||||||
})
|
|
||||||
|
|
||||||
it("should throw a 400 if no datasource entities are present", async () => {
|
|
||||||
let userCtx = getUserCtx()
|
|
||||||
try {
|
|
||||||
//@ts-ignore
|
|
||||||
await exportRows(userCtx)
|
|
||||||
} catch (e) {
|
|
||||||
expect(userCtx.throw).toHaveBeenCalledWith(
|
|
||||||
400,
|
|
||||||
"Datasource has not been configured for plus API."
|
|
||||||
)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
it("should handle single quotes from a row ID", async () => {
|
|
||||||
//@ts-ignore
|
|
||||||
sdk.datasources.get.mockImplementation(() => ({
|
|
||||||
entities: {
|
|
||||||
tablename: {
|
|
||||||
schema: {},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}))
|
|
||||||
let userCtx = getUserCtx()
|
|
||||||
userCtx.request.body = {
|
|
||||||
rows: ["['d001']"],
|
|
||||||
}
|
|
||||||
|
|
||||||
//@ts-ignore
|
|
||||||
await exportRows(userCtx)
|
|
||||||
|
|
||||||
expect(userCtx.request.body).toEqual({
|
|
||||||
query: {
|
|
||||||
oneOf: {
|
|
||||||
_id: ["d001"],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
it("should throw a 400 if any composite keys are present", async () => {
|
|
||||||
let userCtx = getUserCtx()
|
|
||||||
userCtx.request.body = {
|
|
||||||
rows: ["[123]", "['d001'%2C'10111']"],
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
//@ts-ignore
|
|
||||||
await exportRows(userCtx)
|
|
||||||
} catch (e) {
|
|
||||||
expect(userCtx.throw).toHaveBeenCalledWith(
|
|
||||||
400,
|
|
||||||
"Export data does not support composite keys."
|
|
||||||
)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
it("should throw a 400 if no table name was found", async () => {
|
|
||||||
let userCtx = getUserCtx()
|
|
||||||
userCtx.params.tableId = "datasource__"
|
|
||||||
userCtx.request.body = {
|
|
||||||
rows: ["[123]"],
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
//@ts-ignore
|
|
||||||
await exportRows(userCtx)
|
|
||||||
} catch (e) {
|
|
||||||
expect(userCtx.throw).toHaveBeenCalledWith(
|
|
||||||
400,
|
|
||||||
"Could not find table name."
|
|
||||||
)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
|
@ -3,25 +3,11 @@ import { InternalTables } from "../../db/utils"
|
||||||
import { getGlobalUsers } from "../../utilities/global"
|
import { getGlobalUsers } from "../../utilities/global"
|
||||||
import { getFullUser } from "../../utilities/users"
|
import { getFullUser } from "../../utilities/users"
|
||||||
import { context } from "@budibase/backend-core"
|
import { context } from "@budibase/backend-core"
|
||||||
import { UserCtx } from "@budibase/types"
|
import { Ctx, UserCtx } from "@budibase/types"
|
||||||
import sdk from "../../sdk"
|
import sdk from "../../sdk"
|
||||||
|
|
||||||
export async function fetchMetadata(ctx: UserCtx) {
|
export async function fetchMetadata(ctx: Ctx) {
|
||||||
const global = await getGlobalUsers()
|
const users = await sdk.users.fetchMetadata()
|
||||||
const metadata = await sdk.users.rawUserMetadata()
|
|
||||||
const users = []
|
|
||||||
for (let user of global) {
|
|
||||||
// find the metadata that matches up to the global ID
|
|
||||||
const info = metadata.find(meta => meta._id.includes(user._id))
|
|
||||||
// remove these props, not for the correct DB
|
|
||||||
users.push({
|
|
||||||
...user,
|
|
||||||
...info,
|
|
||||||
tableId: InternalTables.USER_METADATA,
|
|
||||||
// make sure the ID is always a local ID, not a global one
|
|
||||||
_id: generateUserMetadataID(user._id),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
ctx.body = users
|
ctx.body = users
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -50,8 +36,8 @@ export async function updateMetadata(ctx: UserCtx) {
|
||||||
export async function destroyMetadata(ctx: UserCtx) {
|
export async function destroyMetadata(ctx: UserCtx) {
|
||||||
const db = context.getAppDB()
|
const db = context.getAppDB()
|
||||||
try {
|
try {
|
||||||
const dbUser = await db.get(ctx.params.id)
|
const dbUser = await sdk.users.get(ctx.params.id)
|
||||||
await db.remove(dbUser._id, dbUser._rev)
|
await db.remove(dbUser._id!, dbUser._rev)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
// error just means the global user has no config in this app
|
// error just means the global user has no config in this app
|
||||||
}
|
}
|
||||||
|
@ -74,7 +60,7 @@ export async function setFlag(ctx: UserCtx) {
|
||||||
const db = context.getAppDB()
|
const db = context.getAppDB()
|
||||||
let doc
|
let doc
|
||||||
try {
|
try {
|
||||||
doc = await db.get(flagDocId)
|
doc = await db.get<any>(flagDocId)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
doc = { _id: flagDocId }
|
doc = { _id: flagDocId }
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,7 +15,6 @@ import {
|
||||||
TableSchema,
|
TableSchema,
|
||||||
View,
|
View,
|
||||||
} from "@budibase/types"
|
} from "@budibase/types"
|
||||||
import { cleanExportRows } from "../row/utils"
|
|
||||||
import { builderSocket } from "../../../websockets"
|
import { builderSocket } from "../../../websockets"
|
||||||
|
|
||||||
const { cloneDeep, isEqual } = require("lodash")
|
const { cloneDeep, isEqual } = require("lodash")
|
||||||
|
@ -28,7 +27,8 @@ export async function save(ctx: Ctx) {
|
||||||
const db = context.getAppDB()
|
const db = context.getAppDB()
|
||||||
const { originalName, ...viewToSave } = ctx.request.body
|
const { originalName, ...viewToSave } = ctx.request.body
|
||||||
|
|
||||||
const existingTable = await db.get(ctx.request.body.tableId)
|
const existingTable = await sdk.tables.getTable(ctx.request.body.tableId)
|
||||||
|
existingTable.views ??= {}
|
||||||
const table = cloneDeep(existingTable)
|
const table = cloneDeep(existingTable)
|
||||||
|
|
||||||
const groupByField: any = Object.values(table.schema).find(
|
const groupByField: any = Object.values(table.schema).find(
|
||||||
|
@ -121,8 +121,8 @@ export async function destroy(ctx: Ctx) {
|
||||||
const db = context.getAppDB()
|
const db = context.getAppDB()
|
||||||
const viewName = decodeURIComponent(ctx.params.viewName)
|
const viewName = decodeURIComponent(ctx.params.viewName)
|
||||||
const view = await deleteView(viewName)
|
const view = await deleteView(viewName)
|
||||||
const table = await db.get(view.meta.tableId)
|
const table = await sdk.tables.getTable(view.meta.tableId)
|
||||||
delete table.views[viewName]
|
delete table.views![viewName]
|
||||||
await db.put(table)
|
await db.put(table)
|
||||||
await events.view.deleted(view)
|
await events.view.deleted(view)
|
||||||
|
|
||||||
|
@ -163,13 +163,16 @@ export async function exportView(ctx: Ctx) {
|
||||||
let rows = ctx.body as Row[]
|
let rows = ctx.body as Row[]
|
||||||
|
|
||||||
let schema: TableSchema = view && view.meta && view.meta.schema
|
let schema: TableSchema = view && view.meta && view.meta.schema
|
||||||
const tableId = ctx.params.tableId || view.meta.tableId
|
const tableId =
|
||||||
|
ctx.params.tableId ||
|
||||||
|
view?.meta?.tableId ||
|
||||||
|
(viewName.startsWith(DocumentType.TABLE) && viewName)
|
||||||
const table: Table = await sdk.tables.getTable(tableId)
|
const table: Table = await sdk.tables.getTable(tableId)
|
||||||
if (!schema) {
|
if (!schema) {
|
||||||
schema = table.schema
|
schema = table.schema
|
||||||
}
|
}
|
||||||
|
|
||||||
let exportRows = cleanExportRows(rows, schema, format, [])
|
let exportRows = sdk.rows.utils.cleanExportRows(rows, schema, format, [])
|
||||||
|
|
||||||
if (format === Format.CSV) {
|
if (format === Format.CSV) {
|
||||||
ctx.attachment(`${viewName}.csv`)
|
ctx.attachment(`${viewName}.csv`)
|
||||||
|
|
|
@ -13,7 +13,7 @@ import { Database } from "@budibase/types"
|
||||||
export async function getView(viewName: string) {
|
export async function getView(viewName: string) {
|
||||||
const db = context.getAppDB()
|
const db = context.getAppDB()
|
||||||
if (env.SELF_HOSTED) {
|
if (env.SELF_HOSTED) {
|
||||||
const designDoc = await db.get("_design/database")
|
const designDoc = await db.get<any>("_design/database")
|
||||||
return designDoc.views[viewName]
|
return designDoc.views[viewName]
|
||||||
} else {
|
} else {
|
||||||
// This is a table view, don't read the view from the DB
|
// This is a table view, don't read the view from the DB
|
||||||
|
@ -22,7 +22,7 @@ export async function getView(viewName: string) {
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const viewDoc = await db.get(generateMemoryViewID(viewName))
|
const viewDoc = await db.get<any>(generateMemoryViewID(viewName))
|
||||||
return viewDoc.view
|
return viewDoc.view
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
// Return null when PouchDB doesn't found the view
|
// Return null when PouchDB doesn't found the view
|
||||||
|
@ -39,7 +39,7 @@ export async function getViews() {
|
||||||
const db = context.getAppDB()
|
const db = context.getAppDB()
|
||||||
const response = []
|
const response = []
|
||||||
if (env.SELF_HOSTED) {
|
if (env.SELF_HOSTED) {
|
||||||
const designDoc = await db.get("_design/database")
|
const designDoc = await db.get<any>("_design/database")
|
||||||
for (let name of Object.keys(designDoc.views)) {
|
for (let name of Object.keys(designDoc.views)) {
|
||||||
// Only return custom views, not built ins
|
// Only return custom views, not built ins
|
||||||
const viewNames = Object.values(ViewName) as string[]
|
const viewNames = Object.values(ViewName) as string[]
|
||||||
|
@ -76,7 +76,7 @@ export async function saveView(
|
||||||
) {
|
) {
|
||||||
const db = context.getAppDB()
|
const db = context.getAppDB()
|
||||||
if (env.SELF_HOSTED) {
|
if (env.SELF_HOSTED) {
|
||||||
const designDoc = await db.get("_design/database")
|
const designDoc = await db.get<any>("_design/database")
|
||||||
designDoc.views = {
|
designDoc.views = {
|
||||||
...designDoc.views,
|
...designDoc.views,
|
||||||
[viewName]: viewTemplate,
|
[viewName]: viewTemplate,
|
||||||
|
@ -96,9 +96,9 @@ export async function saveView(
|
||||||
tableId: viewTemplate.meta.tableId,
|
tableId: viewTemplate.meta.tableId,
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
const old = await db.get(id)
|
const old = await db.get<any>(id)
|
||||||
if (originalId) {
|
if (originalId) {
|
||||||
const originalDoc = await db.get(originalId)
|
const originalDoc = await db.get<any>(originalId)
|
||||||
await db.remove(originalDoc._id, originalDoc._rev)
|
await db.remove(originalDoc._id, originalDoc._rev)
|
||||||
}
|
}
|
||||||
if (old && old._rev) {
|
if (old && old._rev) {
|
||||||
|
@ -114,14 +114,14 @@ export async function saveView(
|
||||||
export async function deleteView(viewName: string) {
|
export async function deleteView(viewName: string) {
|
||||||
const db = context.getAppDB()
|
const db = context.getAppDB()
|
||||||
if (env.SELF_HOSTED) {
|
if (env.SELF_HOSTED) {
|
||||||
const designDoc = await db.get("_design/database")
|
const designDoc = await db.get<any>("_design/database")
|
||||||
const view = designDoc.views[viewName]
|
const view = designDoc.views[viewName]
|
||||||
delete designDoc.views[viewName]
|
delete designDoc.views[viewName]
|
||||||
await db.put(designDoc)
|
await db.put(designDoc)
|
||||||
return view
|
return view
|
||||||
} else {
|
} else {
|
||||||
const id = generateMemoryViewID(viewName)
|
const id = generateMemoryViewID(viewName)
|
||||||
const viewDoc = await db.get(id)
|
const viewDoc = await db.get<any>(id)
|
||||||
await db.remove(viewDoc._id, viewDoc._rev)
|
await db.remove(viewDoc._id, viewDoc._rev)
|
||||||
return viewDoc.view
|
return viewDoc.view
|
||||||
}
|
}
|
||||||
|
@ -129,7 +129,7 @@ export async function deleteView(viewName: string) {
|
||||||
|
|
||||||
export async function migrateToInMemoryView(db: Database, viewName: string) {
|
export async function migrateToInMemoryView(db: Database, viewName: string) {
|
||||||
// delete the view initially
|
// delete the view initially
|
||||||
const designDoc = await db.get("_design/database")
|
const designDoc = await db.get<any>("_design/database")
|
||||||
// run the view back through the view builder to update it
|
// run the view back through the view builder to update it
|
||||||
const view = viewBuilder(designDoc.views[viewName].meta)
|
const view = viewBuilder(designDoc.views[viewName].meta)
|
||||||
delete designDoc.views[viewName]
|
delete designDoc.views[viewName]
|
||||||
|
@ -138,15 +138,15 @@ export async function migrateToInMemoryView(db: Database, viewName: string) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function migrateToDesignView(db: Database, viewName: string) {
|
export async function migrateToDesignView(db: Database, viewName: string) {
|
||||||
let view = await db.get(generateMemoryViewID(viewName))
|
let view = await db.get<any>(generateMemoryViewID(viewName))
|
||||||
const designDoc = await db.get("_design/database")
|
const designDoc = await db.get<any>("_design/database")
|
||||||
designDoc.views[viewName] = viewBuilder(view.view.meta)
|
designDoc.views[viewName] = viewBuilder(view.view.meta)
|
||||||
await db.put(designDoc)
|
await db.put(designDoc)
|
||||||
await db.remove(view._id, view._rev)
|
await db.remove(view._id, view._rev)
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getFromDesignDoc(db: Database, viewName: string) {
|
export async function getFromDesignDoc(db: Database, viewName: string) {
|
||||||
const designDoc = await db.get("_design/database")
|
const designDoc = await db.get<any>("_design/database")
|
||||||
let view = designDoc.views[viewName]
|
let view = designDoc.views[viewName]
|
||||||
if (view == null) {
|
if (view == null) {
|
||||||
throw { status: 404, message: "Unable to get view" }
|
throw { status: 404, message: "Unable to get view" }
|
||||||
|
@ -155,7 +155,7 @@ export async function getFromDesignDoc(db: Database, viewName: string) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getFromMemoryDoc(db: Database, viewName: string) {
|
export async function getFromMemoryDoc(db: Database, viewName: string) {
|
||||||
let view = await db.get(generateMemoryViewID(viewName))
|
let view = await db.get<any>(generateMemoryViewID(viewName))
|
||||||
if (view) {
|
if (view) {
|
||||||
view = view.view
|
view = view.view
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -77,7 +77,7 @@ export async function trigger(ctx: BBContext) {
|
||||||
if (webhook.bodySchema) {
|
if (webhook.bodySchema) {
|
||||||
validate(ctx.request.body, webhook.bodySchema)
|
validate(ctx.request.body, webhook.bodySchema)
|
||||||
}
|
}
|
||||||
const target = await db.get(webhook.action.target)
|
const target = await db.get<Automation>(webhook.action.target)
|
||||||
if (webhook.action.type === WebhookActionType.AUTOMATION) {
|
if (webhook.action.type === WebhookActionType.AUTOMATION) {
|
||||||
// trigger with both the pure request and then expand it
|
// trigger with both the pure request and then expand it
|
||||||
// incase the user has produced a schema to bind to
|
// incase the user has produced a schema to bind to
|
||||||
|
|
|
@ -0,0 +1,14 @@
|
||||||
|
import Router from "@koa/router"
|
||||||
|
import * as controller from "../controllers/debug"
|
||||||
|
import authorized from "../../middleware/authorized"
|
||||||
|
import { permissions } from "@budibase/backend-core"
|
||||||
|
|
||||||
|
const router: Router = new Router()
|
||||||
|
|
||||||
|
router.get(
|
||||||
|
"/api/debug/diagnostics",
|
||||||
|
authorized(permissions.BUILDER),
|
||||||
|
controller.systemDebugInfo
|
||||||
|
)
|
||||||
|
|
||||||
|
export default router
|
|
@ -25,6 +25,7 @@ import devRoutes from "./dev"
|
||||||
import migrationRoutes from "./migrations"
|
import migrationRoutes from "./migrations"
|
||||||
import pluginRoutes from "./plugin"
|
import pluginRoutes from "./plugin"
|
||||||
import opsRoutes from "./ops"
|
import opsRoutes from "./ops"
|
||||||
|
import debugRoutes from "./debug"
|
||||||
import Router from "@koa/router"
|
import Router from "@koa/router"
|
||||||
import { api as pro } from "@budibase/pro"
|
import { api as pro } from "@budibase/pro"
|
||||||
|
|
||||||
|
@ -63,6 +64,7 @@ export const mainRoutes: Router[] = [
|
||||||
migrationRoutes,
|
migrationRoutes,
|
||||||
pluginRoutes,
|
pluginRoutes,
|
||||||
opsRoutes,
|
opsRoutes,
|
||||||
|
debugRoutes,
|
||||||
scheduleRoutes,
|
scheduleRoutes,
|
||||||
environmentVariableRoutes,
|
environmentVariableRoutes,
|
||||||
// these need to be handled last as they still use /api/:tableId
|
// these need to be handled last as they still use /api/:tableId
|
||||||
|
|
|
@ -0,0 +1,64 @@
|
||||||
|
const { checkBuilderEndpoint } = require("./utilities/TestFunctions")
|
||||||
|
const setup = require("./utilities")
|
||||||
|
import os from "os"
|
||||||
|
|
||||||
|
jest.mock("process", () => ({
|
||||||
|
arch: "arm64",
|
||||||
|
version: "v14.20.1",
|
||||||
|
platform: "darwin",
|
||||||
|
}))
|
||||||
|
|
||||||
|
describe("/component", () => {
|
||||||
|
let request = setup.getRequest()
|
||||||
|
let config = setup.getConfig()
|
||||||
|
|
||||||
|
afterAll(setup.afterAll)
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
await config.init()
|
||||||
|
os.cpus = () => [
|
||||||
|
{
|
||||||
|
model: "test",
|
||||||
|
speed: 12323,
|
||||||
|
times: {
|
||||||
|
user: 0,
|
||||||
|
nice: 0,
|
||||||
|
sys: 0,
|
||||||
|
idle: 0,
|
||||||
|
irq: 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
os.uptime = () => 123123123123
|
||||||
|
os.totalmem = () => 10000000000
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("/api/debug", () => {
|
||||||
|
it("should return debug information to the frontend", async () => {
|
||||||
|
const res = await request
|
||||||
|
.get(`/api/debug/diagnostics`)
|
||||||
|
.set(config.defaultHeaders())
|
||||||
|
.expect("Content-Type", /json/)
|
||||||
|
.expect(200)
|
||||||
|
expect(res.body).toEqual({
|
||||||
|
budibaseVersion: "0.0.0",
|
||||||
|
cpuArch: "arm64",
|
||||||
|
cpuCores: 1,
|
||||||
|
cpuInfo: "test",
|
||||||
|
hosting: "docker-compose",
|
||||||
|
nodeVersion: "v14.20.1",
|
||||||
|
platform: "darwin",
|
||||||
|
totalMemory: "9.313225746154785GB",
|
||||||
|
uptime: "1425036 day(s), 3 hour(s), 32 minute(s)",
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should apply authorization to endpoint", async () => {
|
||||||
|
await checkBuilderEndpoint({
|
||||||
|
config,
|
||||||
|
method: "GET",
|
||||||
|
url: `/api/debug/diagnostics`,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue