Merge v3-ui.

This commit is contained in:
Sam Rose 2024-10-23 13:55:57 +01:00
commit da2b2e5c85
No known key found for this signature in database
177 changed files with 5009 additions and 2011 deletions

View File

@ -28,6 +28,7 @@ export enum Config {
OIDC = "oidc", OIDC = "oidc",
OIDC_LOGOS = "logos_oidc", OIDC_LOGOS = "logos_oidc",
SCIM = "scim", SCIM = "scim",
AI = "AI",
} }
export const MIN_VALID_DATE = new Date(-2147483647000) export const MIN_VALID_DATE = new Date(-2147483647000)

View File

@ -1351,7 +1351,8 @@ class InternalBuilder {
schema.constraints?.presence === true || schema.constraints?.presence === true ||
schema.type === FieldType.FORMULA || schema.type === FieldType.FORMULA ||
schema.type === FieldType.AUTO || schema.type === FieldType.AUTO ||
schema.type === FieldType.LINK schema.type === FieldType.LINK ||
schema.type === FieldType.AI
) { ) {
continue continue
} }

View File

@ -102,6 +102,14 @@ export const useAppBuilders = () => {
return useFeature(Feature.APP_BUILDERS) return useFeature(Feature.APP_BUILDERS)
} }
export const useBudibaseAI = () => {
return useFeature(Feature.BUDIBASE_AI)
}
export const useAICustomConfigs = () => {
return useFeature(Feature.AI_CUSTOM_CONFIGS)
}
// QUOTAS // QUOTAS
export const setAutomationLogsQuota = (value: number) => { export const setAutomationLogsQuota = (value: number) => {

View File

@ -1,15 +1,11 @@
<script> <script>
import "@spectrum-css/actionbutton/dist/index-vars.css" import "@spectrum-css/actionbutton/dist/index-vars.css"
import { createEventDispatcher } from "svelte"
import Tooltip from "../Tooltip/Tooltip.svelte" import Tooltip from "../Tooltip/Tooltip.svelte"
import { fade } from "svelte/transition" import { fade } from "svelte/transition"
import { hexToRGBA } from "../helpers"
const dispatch = createEventDispatcher()
export let quiet = false export let quiet = false
export let emphasized = false
export let selected = false export let selected = false
export let longPressable = false
export let disabled = false export let disabled = false
export let icon = "" export let icon = ""
export let size = "M" export let size = "M"
@ -17,60 +13,39 @@
export let fullWidth = false export let fullWidth = false
export let noPadding = false export let noPadding = false
export let tooltip = "" export let tooltip = ""
export let accentColor = null
let showTooltip = false let showTooltip = false
function longPress(element) { $: accentStyle = getAccentStyle(accentColor)
if (!longPressable) return
let timer
const listener = () => { const getAccentStyle = color => {
timer = setTimeout(() => { if (!color) {
dispatch("longpress") return ""
}, 700)
}
element.addEventListener("pointerdown", listener)
return {
destroy() {
clearTimeout(timer)
element.removeEventListener("pointerdown", longPress)
},
} }
let style = ""
style += `--accent-bg-color:${hexToRGBA(color, 0.15)};`
style += `--accent-border-color:${hexToRGBA(color, 0.35)};`
return style
} }
</script> </script>
<!-- svelte-ignore a11y-no-static-element-interactions --> <button
<span class="spectrum-ActionButton spectrum-ActionButton--size{size}"
class="btn-wrap"
on:mouseover={() => (showTooltip = true)}
on:mouseleave={() => (showTooltip = false)}
on:focus={() => (showTooltip = true)}
>
<button
use:longPress
class:spectrum-ActionButton--quiet={quiet} class:spectrum-ActionButton--quiet={quiet}
class:spectrum-ActionButton--emphasized={emphasized}
class:is-selected={selected} class:is-selected={selected}
class:noPadding class:noPadding
class:fullWidth class:fullWidth
class="spectrum-ActionButton spectrum-ActionButton--size{size}"
class:active class:active
class:disabled class:disabled
{disabled} class:accent={accentColor != null}
on:longPress
on:click|preventDefault on:click|preventDefault
> on:mouseover={() => (showTooltip = true)}
{#if longPressable} on:mouseleave={() => (showTooltip = false)}
<svg on:focus={() => (showTooltip = true)}
class="spectrum-Icon spectrum-UIIcon-CornerTriangle100 spectrum-ActionButton-hold" {disabled}
focusable="false" style={accentStyle}
aria-hidden="true" >
>
<use xlink:href="#spectrum-css-icon-CornerTriangle100" />
</svg>
{/if}
{#if icon} {#if icon}
<svg <svg
class="spectrum-Icon spectrum-Icon--sizeS" class="spectrum-Icon spectrum-Icon--sizeS"
@ -89,10 +64,13 @@
<Tooltip textWrapping direction="bottom" text={tooltip} /> <Tooltip textWrapping direction="bottom" text={tooltip} />
</div> </div>
{/if} {/if}
</button> </button>
</span>
<style> <style>
button {
transition: filter 130ms ease-out, background 130ms ease-out,
border 130ms ease-out, color 130ms ease-out;
}
.fullWidth { .fullWidth {
width: 100%; width: 100%;
} }
@ -104,9 +82,7 @@
margin-left: 0; margin-left: 0;
transition: color ease-out 130ms; transition: color ease-out 130ms;
} }
.is-selected:not(.spectrum-ActionButton--emphasized):not( .is-selected:not(.spectrum-ActionButton--quiet) {
.spectrum-ActionButton--quiet
) {
background: var(--spectrum-global-color-gray-300); background: var(--spectrum-global-color-gray-300);
border-color: var(--spectrum-global-color-gray-500); border-color: var(--spectrum-global-color-gray-500);
} }
@ -115,12 +91,13 @@
} }
.spectrum-ActionButton--quiet.is-selected { .spectrum-ActionButton--quiet.is-selected {
color: var(--spectrum-global-color-gray-900); color: var(--spectrum-global-color-gray-900);
background: var(--spectrum-global-color-gray-300);
} }
.noPadding { .noPadding {
padding: 0; padding: 0;
min-width: 0; min-width: 0;
} }
.is-selected:not(.emphasized) .spectrum-Icon { .is-selected .spectrum-Icon {
color: var(--spectrum-global-color-gray-900); color: var(--spectrum-global-color-gray-900);
} }
.is-selected.disabled .spectrum-Icon { .is-selected.disabled .spectrum-Icon {
@ -137,4 +114,12 @@
text-align: center; text-align: center;
z-index: 1; z-index: 1;
} }
.accent.is-selected,
.accent:active {
border: 1px solid var(--accent-border-color);
background: var(--accent-bg-color);
}
.accent:hover {
filter: brightness(1.2);
}
</style> </style>

View File

@ -1,14 +1,20 @@
<script> <script>
import { setContext } from "svelte" import { setContext, getContext } from "svelte"
import Popover from "../Popover/Popover.svelte" import Popover from "../Popover/Popover.svelte"
import Menu from "../Menu/Menu.svelte" import Menu from "../Menu/Menu.svelte"
export let disabled = false export let disabled = false
export let align = "left" export let align = "left"
export let portalTarget export let portalTarget
export let openOnHover = false
export let animate
export let offset
const actionMenuContext = getContext("actionMenu")
let anchor let anchor
let dropdown let dropdown
let timeout
// This is needed because display: contents is considered "invisible". // This is needed because display: contents is considered "invisible".
// It should only ever be an action button, so should be fine. // It should only ever be an action button, so should be fine.
@ -16,11 +22,19 @@
anchor = node.firstChild anchor = node.firstChild
} }
export const show = () => {
cancelHide()
dropdown.show()
}
export const hide = () => { export const hide = () => {
dropdown.hide() dropdown.hide()
} }
export const show = () => {
dropdown.show() // Hides this menu and all parent menus
const hideAll = () => {
hide()
actionMenuContext?.hide()
} }
const openMenu = event => { const openMenu = event => {
@ -30,12 +44,25 @@
} }
} }
setContext("actionMenu", { show, hide }) const queueHide = () => {
timeout = setTimeout(hide, 10)
}
const cancelHide = () => {
clearTimeout(timeout)
}
setContext("actionMenu", { show, hide, hideAll })
</script> </script>
<!-- svelte-ignore a11y-no-static-element-interactions --> <!-- svelte-ignore a11y-no-static-element-interactions -->
<!-- svelte-ignore a11y-click-events-have-key-events --> <!-- svelte-ignore a11y-click-events-have-key-events -->
<div use:getAnchor on:click={openMenu}> <div
use:getAnchor
on:click={openOnHover ? null : openMenu}
on:mouseenter={openOnHover ? show : null}
on:mouseleave={openOnHover ? queueHide : null}
>
<slot name="control" /> <slot name="control" />
</div> </div>
<Popover <Popover
@ -43,9 +70,13 @@
{anchor} {anchor}
{align} {align}
{portalTarget} {portalTarget}
{animate}
{offset}
resizable={false} resizable={false}
on:open on:open
on:close on:close
on:mouseenter={openOnHover ? cancelHide : null}
on:mouseleave={openOnHover ? queueHide : null}
> >
<Menu> <Menu>
<slot /> <slot />

View File

@ -151,9 +151,9 @@ export default function positionDropdown(element, opts) {
// Determine X strategy // Determine X strategy
if (align === "right") { if (align === "right") {
applyXStrategy(Strategies.EndToEnd) applyXStrategy(Strategies.EndToEnd)
} else if (align === "right-outside") { } else if (align === "right-outside" || align === "right-context-menu") {
applyXStrategy(Strategies.StartToEnd) applyXStrategy(Strategies.StartToEnd)
} else if (align === "left-outside") { } else if (align === "left-outside" || align === "left-context-menu") {
applyXStrategy(Strategies.EndToStart) applyXStrategy(Strategies.EndToStart)
} else if (align === "center") { } else if (align === "center") {
applyXStrategy(Strategies.MidPoint) applyXStrategy(Strategies.MidPoint)
@ -164,6 +164,12 @@ export default function positionDropdown(element, opts) {
// Determine Y strategy // Determine Y strategy
if (align === "right-outside" || align === "left-outside") { if (align === "right-outside" || align === "left-outside") {
applyYStrategy(Strategies.MidPoint) applyYStrategy(Strategies.MidPoint)
} else if (
align === "right-context-menu" ||
align === "left-context-menu"
) {
applyYStrategy(Strategies.StartToStart)
styles.top -= 5 // Manual adjustment for action menu padding
} else { } else {
applyYStrategy(Strategies.StartToEnd) applyYStrategy(Strategies.StartToEnd)
} }
@ -240,7 +246,7 @@ export default function positionDropdown(element, opts) {
} }
// Apply initial styles which don't need to change // Apply initial styles which don't need to change
element.style.position = "absolute" element.style.position = "fixed"
element.style.zIndex = "9999" element.style.zIndex = "9999"
// Set up a scroll listener // Set up a scroll listener

View File

@ -17,6 +17,8 @@
export let tooltip = undefined export let tooltip = undefined
export let newStyles = true export let newStyles = true
export let id export let id
export let ref
export let reverse = false
const dispatch = createEventDispatcher() const dispatch = createEventDispatcher()
</script> </script>
@ -25,6 +27,7 @@
<button <button
{id} {id}
{type} {type}
bind:this={ref}
class:spectrum-Button--cta={cta} class:spectrum-Button--cta={cta}
class:spectrum-Button--primary={primary} class:spectrum-Button--primary={primary}
class:spectrum-Button--secondary={secondary} class:spectrum-Button--secondary={secondary}
@ -41,6 +44,9 @@
} }
}} }}
> >
{#if $$slots && reverse}
<span class="spectrum-Button-label"><slot /></span>
{/if}
{#if icon} {#if icon}
<svg <svg
class="spectrum-Icon spectrum-Icon--size{size.toUpperCase()}" class="spectrum-Icon spectrum-Icon--size{size.toUpperCase()}"
@ -51,7 +57,7 @@
<use xlink:href="#spectrum-icon-18-{icon}" /> <use xlink:href="#spectrum-icon-18-{icon}" />
</svg> </svg>
{/if} {/if}
{#if $$slots} {#if $$slots && !reverse}
<span class="spectrum-Button-label"><slot /></span> <span class="spectrum-Button-label"><slot /></span>
{/if} {/if}
</button> </button>
@ -91,4 +97,11 @@
.spectrum-Button--secondary.new-styles.is-disabled { .spectrum-Button--secondary.new-styles.is-disabled {
color: var(--spectrum-global-color-gray-500); color: var(--spectrum-global-color-gray-500);
} }
.spectrum-Button .spectrum-Button-label + .spectrum-Icon {
margin-left: var(--spectrum-button-primary-icon-gap);
margin-right: calc(
-1 * (var(--spectrum-button-primary-textonly-padding-left-adjusted) -
var(--spectrum-button-primary-padding-left-adjusted))
);
}
</style> </style>

View File

@ -0,0 +1,57 @@
<script>
import Button from "../Button/Button.svelte"
import Popover from "../Popover/Popover.svelte"
import Menu from "../Menu/Menu.svelte"
import MenuItem from "../Menu/Item.svelte"
export let buttons
export let text = "Action"
export let size = "M"
export let align = "left"
export let offset
export let animate
export let quiet = false
let anchor
let popover
const handleClick = async button => {
popover.hide()
await button.onClick?.()
}
</script>
<Button
bind:ref={anchor}
{size}
icon="ChevronDown"
{quiet}
primary={quiet}
cta={!quiet}
newStyles={!quiet}
on:click={() => popover?.show()}
on:click
reverse
>
{text || "Action"}
</Button>
<Popover
bind:this={popover}
{align}
{anchor}
{offset}
{animate}
resizable={false}
on:close
on:open
on:mouseenter
on:mouseleave
>
<Menu>
{#each buttons as button}
<MenuItem on:click={() => handleClick(button)} disabled={button.disabled}>
{button.text || "Button"}
</MenuItem>
{/each}
</Menu>
</Popover>

View File

@ -19,6 +19,7 @@
{disabled} {disabled}
on:change={onChange} on:change={onChange}
on:click on:click
on:click|stopPropagation
{id} {id}
type="checkbox" type="checkbox"
class="spectrum-Switch-input" class="spectrum-Switch-input"

View File

@ -1,6 +1,6 @@
<script> <script>
import "@spectrum-css/textfield/dist/index-vars.css" import "@spectrum-css/textfield/dist/index-vars.css"
import { createEventDispatcher, onMount } from "svelte" import { createEventDispatcher, onMount, tick } from "svelte"
export let value = null export let value = null
export let placeholder = null export let placeholder = null
@ -68,10 +68,13 @@
return type === "number" ? "decimal" : "text" return type === "number" ? "decimal" : "text"
} }
onMount(() => { onMount(async () => {
if (disabled) return if (disabled) return
focus = autofocus focus = autofocus
if (focus) field.focus() if (focus) {
await tick()
field.focus()
}
}) })
</script> </script>

View File

@ -60,10 +60,11 @@
.newStyles { .newStyles {
color: var(--spectrum-global-color-gray-700); color: var(--spectrum-global-color-gray-700);
} }
svg {
transition: color var(--spectrum-global-animation-duration-100, 130ms);
}
svg.hoverable { svg.hoverable {
pointer-events: all; pointer-events: all;
transition: color var(--spectrum-global-animation-duration-100, 130ms);
} }
svg.hoverable:hover { svg.hoverable:hover {
color: var(--hover-color) !important; color: var(--hover-color) !important;

View File

@ -1,55 +1,57 @@
<script> <script>
import Body from "../Typography/Body.svelte" import Icon from "../Icon/Icon.svelte"
import IconAvatar from "../Icon/IconAvatar.svelte"
import Label from "../Label/Label.svelte"
import Avatar from "../Avatar/Avatar.svelte"
export let icon = null export let icon = null
export let iconBackground = null
export let iconColor = null export let iconColor = null
export let avatar = false
export let title = null export let title = null
export let subtitle = null export let subtitle = null
export let url = null
export let hoverable = false export let hoverable = false
export let showArrow = false
$: initials = avatar ? title?.[0] : null
</script> </script>
<!-- svelte-ignore a11y-no-static-element-interactions --> <a
<!-- svelte-ignore a11y-click-events-have-key-events --> href={url}
<div class="list-item" class:hoverable on:click> class="list-item"
class:hoverable={hoverable || url != null}
on:click
>
<div class="left"> <div class="left">
{#if icon} {#if icon}
<IconAvatar {icon} color={iconColor} background={iconBackground} /> <Icon name={icon} color={iconColor} />
{/if}
{#if avatar}
<Avatar {initials} />
{/if} {/if}
<div class="list-item__text">
{#if title} {#if title}
<Body>{title}</Body> <div class="list-item__title">
{title}
</div>
{/if} {/if}
{#if subtitle} {#if subtitle}
<Label>{subtitle}</Label> <div class="list-item__subtitle">
{subtitle}
</div>
{/if} {/if}
</div> </div>
{#if $$slots.default} </div>
<div class="right"> <div class="right">
<slot /> <slot name="right" />
</div> {#if showArrow}
<Icon name="ChevronRight" />
{/if} {/if}
</div> </div>
</a>
<style> <style>
.list-item { .list-item {
padding: 0 16px; padding: var(--spacing-m);
height: 56px; background: var(--spectrum-global-color-gray-75);
background: var(--spectrum-global-color-gray-50);
display: flex; display: flex;
flex-direction: row; flex-direction: row;
justify-content: space-between; justify-content: space-between;
border: 1px solid var(--spectrum-global-color-gray-300); border: 1px solid var(--spectrum-global-color-gray-300);
transition: background 130ms ease-out; transition: background 130ms ease-out;
gap: var(--spacing-m); gap: var(--spacing-m);
color: var(--spectrum-global-color-gray-800);
} }
.list-item:not(:first-child) { .list-item:not(:first-child) {
border-top: none; border-top: none;
@ -64,14 +66,15 @@
} }
.hoverable:hover { .hoverable:hover {
cursor: pointer; cursor: pointer;
background: var(--spectrum-global-color-gray-75); background: var(--spectrum-global-color-gray-200);
} }
.left, .left,
.right { .right {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
align-items: center; align-items: center;
gap: var(--spacing-xl); gap: var(--spacing-s);
} }
.left { .left {
width: 0; width: 0;
@ -79,17 +82,20 @@
} }
.right { .right {
flex: 0 0 auto; flex: 0 0 auto;
color: var(--spectrum-global-color-gray-600);
} }
.list-item :global(.spectrum-Icon),
.list-item :global(.spectrum-Avatar) { .list-item__text {
flex: 0 0 auto; flex: 1 1 auto;
width: 0;
} }
.list-item :global(.spectrum-Body) { .list-item__title,
color: var(--spectrum-global-color-gray-900); .list-item__subtitle {
}
.list-item :global(.spectrum-Body) {
white-space: nowrap; white-space: nowrap;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
} }
.list-item__subtitle {
color: var(--spectrum-global-color-gray-600);
}
</style> </style>

View File

@ -27,7 +27,7 @@
const onClick = () => { const onClick = () => {
if (actionMenu && !noClose) { if (actionMenu && !noClose) {
actionMenu.hide() actionMenu.hideAll()
} }
dispatch("click") dispatch("click")
} }
@ -35,7 +35,7 @@
<!-- svelte-ignore a11y-click-events-have-key-events --> <!-- svelte-ignore a11y-click-events-have-key-events -->
<li <li
on:click|preventDefault={disabled ? null : onClick} on:click={disabled ? null : onClick}
class="spectrum-Menu-item" class="spectrum-Menu-item"
class:is-disabled={disabled} class:is-disabled={disabled}
role="menuitem" role="menuitem"
@ -47,8 +47,9 @@
</div> </div>
{/if} {/if}
<span class="spectrum-Menu-itemLabel"><slot /></span> <span class="spectrum-Menu-itemLabel"><slot /></span>
{#if keys?.length} {#if keys?.length || $$slots.right}
<div class="keys"> <div class="keys">
<slot name="right" />
{#each keys as key} {#each keys as key}
<div class="key"> <div class="key">
{#if key.startsWith("!")} {#if key.startsWith("!")}

View File

@ -30,7 +30,9 @@
export let custom = false export let custom = false
const { hide, cancel } = getContext(Context.Modal) const { hide, cancel } = getContext(Context.Modal)
let loading = false let loading = false
$: confirmDisabled = disabled || loading $: confirmDisabled = disabled || loading
async function secondary(e) { async function secondary(e) {
@ -90,7 +92,7 @@
<!-- TODO: Remove content-grid class once Layout components are in bbui --> <!-- TODO: Remove content-grid class once Layout components are in bbui -->
<section class="spectrum-Dialog-content content-grid"> <section class="spectrum-Dialog-content content-grid">
<slot /> <slot {loading} />
</section> </section>
{#if showCancelButton || showConfirmButton || $$slots.footer} {#if showCancelButton || showConfirmButton || $$slots.footer}
<div <div

View File

@ -27,11 +27,7 @@
<div class="spectrum-Toast-body" class:actionBody={!!action}> <div class="spectrum-Toast-body" class:actionBody={!!action}>
<div class="wrap spectrum-Toast-content">{message || ""}</div> <div class="wrap spectrum-Toast-content">{message || ""}</div>
{#if action} {#if action}
<ActionButton <ActionButton quiet on:click={() => action(() => dispatch("dismiss"))}>
quiet
emphasized
on:click={() => action(() => dispatch("dismiss"))}
>
<div style="color: white; font-weight: 600;">{actionMessage}</div> <div style="color: white; font-weight: 600;">{actionMessage}</div>
</ActionButton> </ActionButton>
{/if} {/if}

View File

@ -1,7 +1,7 @@
<script> <script>
import "@spectrum-css/popover/dist/index-vars.css" import "@spectrum-css/popover/dist/index-vars.css"
import Portal from "svelte-portal" import Portal from "svelte-portal"
import { createEventDispatcher, getContext } from "svelte" import { createEventDispatcher, getContext, onDestroy } from "svelte"
import positionDropdown from "../Actions/position_dropdown" import positionDropdown from "../Actions/position_dropdown"
import clickOutside from "../Actions/click_outside" import clickOutside from "../Actions/click_outside"
import { fly } from "svelte/transition" import { fly } from "svelte/transition"
@ -28,7 +28,24 @@
export let resizable = true export let resizable = true
export let wrap = false export let wrap = false
const animationDuration = 260
let timeout
let blockPointerEvents = false
$: target = portalTarget || getContext(Context.PopoverRoot) || ".spectrum" $: target = portalTarget || getContext(Context.PopoverRoot) || ".spectrum"
$: {
// Disable pointer events for the initial part of the animation, because we
// fly from top to bottom and initially can be positioned under the cursor,
// causing a flashing hover state in the content
if (open && animate) {
blockPointerEvents = true
clearTimeout(timeout)
timeout = setTimeout(() => {
blockPointerEvents = false
}, animationDuration / 2)
}
}
export const show = () => { export const show = () => {
dispatch("open") dispatch("open")
@ -77,6 +94,10 @@
hide() hide()
} }
} }
onDestroy(() => {
clearTimeout(timeout)
})
</script> </script>
{#if open} {#if open}
@ -104,9 +125,13 @@
class="spectrum-Popover is-open" class="spectrum-Popover is-open"
class:customZindex class:customZindex
class:hidden={!showPopover} class:hidden={!showPopover}
class:blockPointerEvents
role="presentation" role="presentation"
style="height: {customHeight}; --customZindex: {customZindex};" style="height: {customHeight}; --customZindex: {customZindex};"
transition:fly|local={{ y: -20, duration: animate ? 260 : 0 }} transition:fly|local={{
y: -20,
duration: animate ? animationDuration : 0,
}}
on:mouseenter on:mouseenter
on:mouseleave on:mouseleave
> >
@ -121,6 +146,12 @@
border-color: var(--spectrum-global-color-gray-300); border-color: var(--spectrum-global-color-gray-300);
overflow: auto; overflow: auto;
transition: opacity 260ms ease-out; transition: opacity 260ms ease-out;
filter: none;
-webkit-filter: none;
box-shadow: 0 1px 4px var(--drop-shadow);
}
.blockPointerEvents {
pointer-events: none;
} }
.hidden { .hidden {
opacity: 0; opacity: 0;

View File

@ -228,3 +228,13 @@ export const getDateDisplayValue = (
return value.format(`${localeDateFormat} HH:mm`) return value.format(`${localeDateFormat} HH:mm`)
} }
} }
export const hexToRGBA = (color, opacity) => {
if (color.includes("#")) {
color = color.replace("#", "")
}
const r = parseInt(color.substring(0, 2), 16)
const g = parseInt(color.substring(2, 4), 16)
const b = parseInt(color.substring(4, 6), 16)
return `rgba(${r}, ${g}, ${b}, ${opacity})`
}

View File

@ -39,6 +39,7 @@ export { default as ActionGroup } from "./ActionGroup/ActionGroup.svelte"
export { default as ActionMenu } from "./ActionMenu/ActionMenu.svelte" export { default as ActionMenu } from "./ActionMenu/ActionMenu.svelte"
export { default as Button } from "./Button/Button.svelte" export { default as Button } from "./Button/Button.svelte"
export { default as ButtonGroup } from "./ButtonGroup/ButtonGroup.svelte" export { default as ButtonGroup } from "./ButtonGroup/ButtonGroup.svelte"
export { default as CollapsedButtonGroup } from "./ButtonGroup/CollapsedButtonGroup.svelte"
export { default as ClearButton } from "./ClearButton/ClearButton.svelte" export { default as ClearButton } from "./ClearButton/ClearButton.svelte"
export { default as Icon } from "./Icon/Icon.svelte" export { default as Icon } from "./Icon/Icon.svelte"
export { default as IconAvatar } from "./Icon/IconAvatar.svelte" export { default as IconAvatar } from "./Icon/IconAvatar.svelte"

View File

@ -12,13 +12,17 @@
import { Icon, notifications, Modal, Toggle } from "@budibase/bbui" import { Icon, notifications, Modal, Toggle } from "@budibase/bbui"
import { ActionStepID } from "constants/backend/automations" import { ActionStepID } from "constants/backend/automations"
import UndoRedoControl from "components/common/UndoRedoControl.svelte" import UndoRedoControl from "components/common/UndoRedoControl.svelte"
import { sdk } from "@budibase/shared-core"
export let automation export let automation
let testDataModal let testDataModal
let confirmDeleteDialog let confirmDeleteDialog
let scrolling = false let scrolling = false
$: blocks = getBlocks(automation).filter(x => x.stepId !== ActionStepID.LOOP) $: blocks = getBlocks(automation).filter(x => x.stepId !== ActionStepID.LOOP)
$: isRowAction = sdk.automations.isRowAction(automation)
const getBlocks = automation => { const getBlocks = automation => {
let blocks = [] let blocks = []
if (automation.definition.trigger) { if (automation.definition.trigger) {
@ -74,6 +78,7 @@
Test details Test details
</div> </div>
</div> </div>
{#if !isRowAction}
<div class="setting-spacing"> <div class="setting-spacing">
<Toggle <Toggle
text={automation.disabled ? "Paused" : "Activated"} text={automation.disabled ? "Paused" : "Activated"}
@ -85,6 +90,7 @@
value={!automation.disabled} value={!automation.disabled}
/> />
</div> </div>
{/if}
</div> </div>
</div> </div>
<div class="canvas" on:scroll={handleScroll}> <div class="canvas" on:scroll={handleScroll}>

View File

@ -190,7 +190,7 @@
{#if isTrigger && triggerInfo} {#if isTrigger && triggerInfo}
<InlineAlert <InlineAlert
header={triggerInfo.type} header={triggerInfo.type}
message={`This trigger is tied to the row action ${triggerInfo.rowAction.name} on your ${triggerInfo.table.name} table`} message={`This trigger is tied to the "${triggerInfo.rowAction.name}" row action in your ${triggerInfo.table.name} table`}
/> />
{/if} {/if}
{#if lastStep} {#if lastStep}

View File

@ -9,6 +9,7 @@
import { sdk } from "@budibase/shared-core" import { sdk } from "@budibase/shared-core"
import ConfirmDialog from "components/common/ConfirmDialog.svelte" import ConfirmDialog from "components/common/ConfirmDialog.svelte"
import UpdateAutomationModal from "components/automation/AutomationPanel/UpdateAutomationModal.svelte" import UpdateAutomationModal from "components/automation/AutomationPanel/UpdateAutomationModal.svelte"
import UpdateRowActionModal from "components/automation/AutomationPanel/UpdateRowActionModal.svelte"
import NavItem from "components/common/NavItem.svelte" import NavItem from "components/common/NavItem.svelte"
export let automation export let automation
@ -16,12 +17,16 @@
let confirmDeleteDialog let confirmDeleteDialog
let updateAutomationDialog let updateAutomationDialog
let updateRowActionDialog
$: isRowAction = sdk.automations.isRowAction(automation)
async function deleteAutomation() { async function deleteAutomation() {
try { try {
await automationStore.actions.delete(automation) await automationStore.actions.delete(automation)
notifications.success("Automation deleted successfully") notifications.success("Automation deleted successfully")
} catch (error) { } catch (error) {
console.error(error)
notifications.error("Error deleting automation") notifications.error("Error deleting automation")
} }
} }
@ -36,19 +41,29 @@
} }
const getContextMenuItems = () => { const getContextMenuItems = () => {
const isRowAction = sdk.automations.isRowAction(automation) const pause = {
const result = [] icon: automation.disabled ? "CheckmarkCircle" : "Cancel",
if (!isRowAction) { name: automation.disabled ? "Activate" : "Pause",
result.push( keyBind: null,
...[ visible: true,
{ disabled: !automation.definition.trigger,
callback: () => {
automationStore.actions.toggleDisabled(
automation._id,
automation.disabled
)
},
}
const del = {
icon: "Delete", icon: "Delete",
name: "Delete", name: "Delete",
keyBind: null, keyBind: null,
visible: true, visible: true,
disabled: false, disabled: false,
callback: confirmDeleteDialog.show, callback: confirmDeleteDialog.show,
}, }
if (!isRowAction) {
return [
{ {
icon: "Edit", icon: "Edit",
name: "Edit", name: "Edit",
@ -67,24 +82,21 @@
automation.definition.trigger?.name === "Webhook", automation.definition.trigger?.name === "Webhook",
callback: duplicateAutomation, callback: duplicateAutomation,
}, },
pause,
del,
] ]
) } else {
} return [
{
result.push({ icon: "Edit",
icon: automation.disabled ? "CheckmarkCircle" : "Cancel", name: "Edit",
name: automation.disabled ? "Activate" : "Pause",
keyBind: null, keyBind: null,
visible: true, visible: true,
disabled: !automation.definition.trigger, callback: updateRowActionDialog.show,
callback: () => {
automationStore.actions.toggleDisabled(
automation._id,
automation.disabled
)
}, },
}) del,
return result ]
}
} }
const openContextMenu = e => { const openContextMenu = e => {
@ -99,7 +111,9 @@
<NavItem <NavItem
on:contextmenu={openContextMenu} on:contextmenu={openContextMenu}
{icon} {icon}
iconColor={"var(--spectrum-global-color-gray-900)"} iconColor={automation.disabled
? "var(--spectrum-global-color-gray-600)"
: "var(--spectrum-global-color-gray-900)"}
text={automation.displayName} text={automation.displayName}
selected={automation._id === $selectedAutomation?._id} selected={automation._id === $selectedAutomation?._id}
hovering={automation._id === $contextMenuStore.id} hovering={automation._id === $contextMenuStore.id}
@ -107,9 +121,7 @@
selectedBy={$userSelectedResourceMap[automation._id]} selectedBy={$userSelectedResourceMap[automation._id]}
disabled={automation.disabled} disabled={automation.disabled}
> >
<div class="icon">
<Icon on:click={openContextMenu} size="S" hoverable name="MoreSmallList" /> <Icon on:click={openContextMenu} size="S" hoverable name="MoreSmallList" />
</div>
</NavItem> </NavItem>
<ConfirmDialog <ConfirmDialog
@ -122,13 +134,9 @@
<i>{automation.name}?</i> <i>{automation.name}?</i>
This action cannot be undone. This action cannot be undone.
</ConfirmDialog> </ConfirmDialog>
<UpdateAutomationModal {automation} bind:this={updateAutomationDialog} />
<style> {#if isRowAction}
div.icon { <UpdateRowActionModal {automation} bind:this={updateRowActionDialog} />
display: flex; {:else}
flex-direction: row; <UpdateAutomationModal {automation} bind:this={updateAutomationDialog} />
justify-content: flex-end; {/if}
align-items: center;
}
</style>

View File

@ -3,13 +3,21 @@
import { Modal, notifications, Layout } from "@budibase/bbui" import { Modal, notifications, Layout } from "@budibase/bbui"
import NavHeader from "components/common/NavHeader.svelte" import NavHeader from "components/common/NavHeader.svelte"
import { onMount } from "svelte" import { onMount } from "svelte"
import { automationStore } from "stores/builder" import { automationStore, tables } from "stores/builder"
import AutomationNavItem from "./AutomationNavItem.svelte" import AutomationNavItem from "./AutomationNavItem.svelte"
import { TriggerStepID } from "constants/backend/automations"
export let modal export let modal
export let webhookModal export let webhookModal
let searchString let searchString
const dsTriggers = [
TriggerStepID.ROW_SAVED,
TriggerStepID.ROW_UPDATED,
TriggerStepID.ROW_DELETED,
TriggerStepID.ROW_ACTION,
]
$: filteredAutomations = $automationStore.automations $: filteredAutomations = $automationStore.automations
.filter(automation => { .filter(automation => {
return ( return (
@ -29,19 +37,47 @@
return lowerA > lowerB ? 1 : -1 return lowerA > lowerB ? 1 : -1
}) })
$: groupedAutomations = filteredAutomations.reduce((acc, auto) => { $: groupedAutomations = groupAutomations(filteredAutomations)
const catName = auto.definition?.trigger?.event || "No Trigger"
acc[catName] ??= {
icon: auto.definition?.trigger?.icon || "AlertCircle",
name: (auto.definition?.trigger?.name || "No Trigger").toUpperCase(),
entries: [],
}
acc[catName].entries.push(auto)
return acc
}, {})
$: showNoResults = searchString && !filteredAutomations.length $: showNoResults = searchString && !filteredAutomations.length
const groupAutomations = automations => {
let groups = {}
for (let auto of automations) {
let category = null
let dataTrigger = false
// Group by datasource if possible
if (dsTriggers.includes(auto.definition?.trigger?.stepId)) {
if (auto.definition.trigger.inputs?.tableId) {
const tableId = auto.definition.trigger.inputs?.tableId
category = $tables.list.find(x => x._id === tableId)?.name
}
}
// Otherwise group by trigger
if (!category) {
category = auto.definition?.trigger?.name || "No Trigger"
} else {
dataTrigger = true
}
groups[category] ??= {
icon: auto.definition?.trigger?.icon || "AlertCircle",
name: category.toUpperCase(),
entries: [],
dataTrigger,
}
groups[category].entries.push(auto)
}
return Object.values(groups).sort((a, b) => {
if (a.dataTrigger === b.dataTrigger) {
return a.name < b.name ? -1 : 1
}
return a.dataTrigger ? -1 : 1
})
}
onMount(async () => { onMount(async () => {
try { try {
await automationStore.actions.fetch() await automationStore.actions.fetch()
@ -88,16 +124,22 @@
<style> <style>
.nav-group { .nav-group {
padding-top: var(--spacing-l); padding-top: 24px;
}
.nav-group:first-child {
padding-top: var(--spacing-s);
} }
.nav-group-header { .nav-group-header {
color: var(--spectrum-global-color-gray-600); color: var(--spectrum-global-color-gray-600);
padding: 0px calc(var(--spacing-l) + 4px); padding: 0px calc(var(--spacing-l) + 4px);
padding-bottom: var(--spacing-l); padding-bottom: var(--spacing-m);
white-space: nowrap; white-space: nowrap;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
font-size: 12px;
font-weight: 600;
} }
.side-bar { .side-bar {
flex: 0 0 260px; flex: 0 0 260px;
display: flex; display: flex;

View File

@ -0,0 +1,83 @@
<script>
import { rowActions } from "stores/builder"
import {
notifications,
Icon,
Input,
ModalContent,
Modal,
} from "@budibase/bbui"
export let automation
export let onCancel = undefined
let name
let error = ""
let modal
export const show = () => {
name = automation?.displayName
modal.show()
}
export const hide = () => {
modal.hide()
}
async function saveAutomation() {
try {
await rowActions.rename(
automation.definition.trigger.inputs.tableId,
automation.definition.trigger.inputs.rowActionId,
name
)
notifications.success(`Row action updated successfully`)
hide()
} catch (error) {
notifications.error("Error saving row action")
}
}
function checkValid(evt) {
name = evt.target.value
if (!name) {
error = "Name is required"
return
}
error = ""
}
</script>
<Modal bind:this={modal} on:hide={onCancel}>
<ModalContent
title="Edit Row Action"
confirmText="Save"
size="L"
onConfirm={saveAutomation}
disabled={error}
>
<Input bind:value={name} label="Name" on:input={checkValid} {error} />
<a
slot="footer"
target="_blank"
href="https://docs.budibase.com/docs/automation-steps"
>
<Icon name="InfoOutline" />
<span>Learn about automations</span>
</a>
</ModalContent>
</Modal>
<style>
a {
color: var(--ink);
font-size: 14px;
vertical-align: middle;
display: flex;
align-items: center;
text-decoration: none;
}
a span {
text-decoration: underline;
margin-left: var(--spectrum-alias-item-padding-s);
}
</style>

View File

@ -62,6 +62,7 @@
} from "@budibase/types" } from "@budibase/types"
import { FIELDS } from "constants/backend" import { FIELDS } from "constants/backend"
import PropField from "./PropField.svelte" import PropField from "./PropField.svelte"
import { utils } from "@budibase/shared-core"
export let block export let block
export let testData export let testData
@ -96,8 +97,14 @@
$: memoEnvVariables.set($environment.variables) $: memoEnvVariables.set($environment.variables)
$: memoBlock.set(block) $: memoBlock.set(block)
$: filters = lookForFilters(schemaProperties) || [] $: filters = lookForFilters(schemaProperties)
$: tempFilters = filters $: filterCount =
filters?.groups?.reduce((acc, group) => {
acc = acc += group?.filters?.length || 0
return acc
}, 0) || 0
$: tempFilters = cloneDeep(filters)
$: stepId = $memoBlock.stepId $: stepId = $memoBlock.stepId
$: automationBindings = getAvailableBindings( $: automationBindings = getAvailableBindings(
@ -791,14 +798,13 @@
break break
} }
} }
return filters || [] return utils.processSearchFilters(filters)
} }
function saveFilters(key) { function saveFilters(key) {
const filters = QueryUtils.buildQuery(tempFilters) const query = QueryUtils.buildQuery(tempFilters)
onChange({ onChange({
[key]: filters, [key]: query,
[`${key}-def`]: tempFilters, // need to store the builder definition in the automation [`${key}-def`]: tempFilters, // need to store the builder definition in the automation
}) })
@ -1027,18 +1033,24 @@
</div> </div>
</div> </div>
{:else if value.customType === AutomationCustomIOType.FILTERS || value.customType === AutomationCustomIOType.TRIGGER_FILTER} {:else if value.customType === AutomationCustomIOType.FILTERS || value.customType === AutomationCustomIOType.TRIGGER_FILTER}
<ActionButton fullWidth on:click={drawer.show} <ActionButton fullWidth on:click={drawer.show}>
>{filters.length > 0 {filterCount > 0 ? "Update Filter" : "No Filter set"}
? "Update Filter" </ActionButton>
: "No Filter set"}</ActionButton <Drawer
bind:this={drawer}
title="Filtering"
forceModal
on:drawerShow={() => {
tempFilters = filters
}}
> >
<Drawer bind:this={drawer} title="Filtering">
<Button cta slot="buttons" on:click={() => saveFilters(key)}> <Button cta slot="buttons" on:click={() => saveFilters(key)}>
Save Save
</Button> </Button>
<DrawerContent slot="body"> <DrawerContent slot="body">
<FilterBuilder <FilterBuilder
{filters} filters={tempFilters}
{bindings} {bindings}
{schemaFields} {schemaFields}
datasource={{ type: "table", tableId }} datasource={{ type: "table", tableId }}

View File

@ -233,6 +233,14 @@
) )
dispatch("change", result) dispatch("change", result)
} }
/**
* Converts arrays into strings. The CodeEditor expects a string or encoded JS
* @param{object} fieldValue
*/
const drawerValue = fieldValue => {
return Array.isArray(fieldValue) ? fieldValue.join(",") : fieldValue
}
</script> </script>
{#each schemaFields || [] as [field, schema]} {#each schemaFields || [] as [field, schema]}
@ -257,7 +265,7 @@
panel={AutomationBindingPanel} panel={AutomationBindingPanel}
type={schema.type} type={schema.type}
{schema} {schema}
value={editableRow[field]} value={drawerValue(editableRow[field])}
on:change={e => on:change={e =>
onChange({ onChange({
row: { row: {

View File

@ -1,44 +0,0 @@
<script>
import { API } from "api"
import Table from "./Table.svelte"
import { tables } from "stores/builder"
import { notifications } from "@budibase/bbui"
export let tableId
export let rowId
export let fieldName
let row
let title
$: data = row?.[fieldName] ?? []
$: linkedTableId = data?.length ? data[0].tableId : null
$: linkedTable = $tables.list.find(table => table._id === linkedTableId)
$: schema = linkedTable?.schema
$: table = $tables.list.find(table => table._id === tableId)
$: fetchData(tableId, rowId)
$: {
let rowLabel = row?.[table?.primaryDisplay]
if (rowLabel) {
title = `${rowLabel} - ${fieldName}`
} else {
title = fieldName
}
}
async function fetchData(tableId, rowId) {
try {
row = await API.fetchRelationshipData({
tableId,
rowId,
})
} catch (error) {
row = null
notifications.error("Error fetching relationship data")
}
}
</script>
{#if row && row._id === rowId}
<Table {title} {schema} {data} />
{/if}

View File

@ -1,120 +0,0 @@
<script>
import { datasources, tables, integrations, appStore } from "stores/builder"
import { themeStore, admin } from "stores/portal"
import EditRolesButton from "./buttons/EditRolesButton.svelte"
import { TableNames } from "constants"
import { Grid } from "@budibase/frontend-core"
import { API } from "api"
import GridCreateAutomationButton from "./buttons/grid/GridCreateAutomationButton.svelte"
import GridAddColumnModal from "components/backend/DataTable/modals/grid/GridCreateColumnModal.svelte"
import GridCreateEditRowModal from "components/backend/DataTable/modals/grid/GridCreateEditRowModal.svelte"
import GridEditUserModal from "components/backend/DataTable/modals/grid/GridEditUserModal.svelte"
import GridCreateViewButton from "components/backend/DataTable/buttons/grid/GridCreateViewButton.svelte"
import GridImportButton from "components/backend/DataTable/buttons/grid/GridImportButton.svelte"
import GridExportButton from "components/backend/DataTable/buttons/grid/GridExportButton.svelte"
import GridFilterButton from "components/backend/DataTable/buttons/grid/GridFilterButton.svelte"
import GridManageAccessButton from "components/backend/DataTable/buttons/grid/GridManageAccessButton.svelte"
import GridRelationshipButton from "components/backend/DataTable/buttons/grid/GridRelationshipButton.svelte"
import GridEditColumnModal from "components/backend/DataTable/modals/grid/GridEditColumnModal.svelte"
import GridUsersTableButton from "components/backend/DataTable/modals/grid/GridUsersTableButton.svelte"
import { DB_TYPE_EXTERNAL } from "constants/backend"
const userSchemaOverrides = {
firstName: { displayName: "First name", disabled: true },
lastName: { displayName: "Last name", disabled: true },
email: { displayName: "Email", disabled: true },
roleId: { displayName: "Role", disabled: true },
status: { displayName: "Status", disabled: true },
}
$: id = $tables.selected?._id
$: isUsersTable = id === TableNames.USERS
$: isInternal = $tables.selected?.sourceType !== DB_TYPE_EXTERNAL
$: gridDatasource = {
type: "table",
tableId: id,
}
$: tableDatasource = $datasources.list.find(datasource => {
return datasource._id === $tables.selected?.sourceId
})
$: relationshipsEnabled = relationshipSupport(tableDatasource)
$: currentTheme = $themeStore?.theme
$: darkMode = !currentTheme.includes("light")
const relationshipSupport = datasource => {
const integration = $integrations[datasource?.source]
return !isInternal && integration?.relationships !== false
}
const handleGridTableUpdate = async e => {
tables.replaceTable(id, e.detail)
// We need to refresh datasources when an external table changes.
if (e.detail?.sourceType === DB_TYPE_EXTERNAL) {
await datasources.fetch()
}
}
</script>
<div class="wrapper">
<Grid
{API}
{darkMode}
datasource={gridDatasource}
canAddRows={!isUsersTable}
canDeleteRows={!isUsersTable}
canEditRows={!isUsersTable || !$appStore.features.disableUserMetadata}
canEditColumns={!isUsersTable || !$appStore.features.disableUserMetadata}
schemaOverrides={isUsersTable ? userSchemaOverrides : null}
showAvatars={false}
on:updatedatasource={handleGridTableUpdate}
isCloud={$admin.cloud}
>
<svelte:fragment slot="filter">
{#if isUsersTable && $appStore.features.disableUserMetadata}
<GridUsersTableButton />
{/if}
<GridFilterButton />
</svelte:fragment>
<svelte:fragment slot="controls">
{#if !isUsersTable}
<GridCreateViewButton />
{/if}
<GridManageAccessButton />
{#if !isUsersTable}
<GridCreateAutomationButton />
{/if}
{#if relationshipsEnabled}
<GridRelationshipButton />
{/if}
{#if isUsersTable}
<EditRolesButton />
{:else}
<GridImportButton />
{/if}
<GridExportButton />
{#if isUsersTable}
<GridEditUserModal />
{:else}
<GridCreateEditRowModal />
{/if}
</svelte:fragment>
<svelte:fragment slot="edit-column">
<GridEditColumnModal />
</svelte:fragment>
<svelte:fragment slot="add-column">
<GridAddColumnModal />
</svelte:fragment>
</Grid>
</div>
<style>
.wrapper {
flex: 1 1 auto;
margin: -28px -40px -40px -40px;
display: flex;
flex-direction: column;
background: var(--background);
}
</style>

View File

@ -1,80 +0,0 @@
<script>
import { API } from "api"
import { tables } from "stores/builder"
import Table from "./Table.svelte"
import CalculateButton from "./buttons/CalculateButton.svelte"
import GroupByButton from "./buttons/GroupByButton.svelte"
import ViewFilterButton from "./buttons/ViewFilterButton.svelte"
import ExportButton from "./buttons/ExportButton.svelte"
import ManageAccessButton from "./buttons/ManageAccessButton.svelte"
import HideAutocolumnButton from "./buttons/HideAutocolumnButton.svelte"
import { notifications } from "@budibase/bbui"
import { ROW_EXPORT_FORMATS } from "constants/backend"
export let view = {}
let hideAutocolumns = true
let data = []
let loading = false
$: name = view.name
$: schema = view.schema
$: calculation = view.calculation
$: supportedFormats = Object.values(ROW_EXPORT_FORMATS).filter(key => {
if (calculation && key === ROW_EXPORT_FORMATS.JSON_WITH_SCHEMA) {
return false
}
return true
})
// Fetch rows for specified view
$: fetchViewData(name, view.field, view.groupBy, view.calculation)
async function fetchViewData(name, field, groupBy, calculation) {
loading = true
const _tables = $tables.list
const allTableViews = _tables.map(table => table.views)
const thisView = allTableViews.filter(
views => views != null && views[name] != null
)[0]
// Don't fetch view data if the view no longer exists
if (!thisView) {
loading = false
return
}
try {
data = await API.fetchViewData({
name,
calculation,
field,
groupBy,
})
} catch (error) {
notifications.error("Error fetching view data")
}
loading = false
}
</script>
<Table
title={decodeURI(name)}
{schema}
tableId={view.tableId}
{data}
{loading}
rowCount={10}
allowEditing={false}
bind:hideAutocolumns
>
<ViewFilterButton {view} />
<CalculateButton {view} />
{#if view.calculation}
<GroupByButton {view} />
{/if}
<ManageAccessButton resourceId={decodeURI(name)} />
<HideAutocolumnButton bind:hideAutocolumns />
<ExportButton view={view.name} formats={supportedFormats} />
</Table>

View File

@ -1,58 +0,0 @@
<script>
import { viewsV2 } from "stores/builder"
import { admin, themeStore } from "stores/portal"
import { Grid } from "@budibase/frontend-core"
import { API } from "api"
import GridCreateEditRowModal from "components/backend/DataTable/modals/grid/GridCreateEditRowModal.svelte"
import GridFilterButton from "components/backend/DataTable/buttons/grid/GridFilterButton.svelte"
import GridManageAccessButton from "components/backend/DataTable/buttons/grid/GridManageAccessButton.svelte"
import { isEnabled } from "helpers/featureFlags"
import { FeatureFlag } from "@budibase/types"
$: id = $viewsV2.selected?.id
$: datasource = {
type: "viewV2",
id,
tableId: $viewsV2.selected?.tableId,
}
$: currentTheme = $themeStore?.theme
$: darkMode = !currentTheme.includes("light")
const handleGridViewUpdate = async e => {
viewsV2.replaceView(id, e.detail)
}
</script>
<div class="wrapper">
<Grid
{API}
{datasource}
{darkMode}
allowAddRows
allowDeleteRows
showAvatars={false}
on:updatedatasource={handleGridViewUpdate}
isCloud={$admin.cloud}
canSetRelationshipSchemas={isEnabled(FeatureFlag.ENRICHED_RELATIONSHIPS)}
>
<svelte:fragment slot="filter">
<GridFilterButton />
</svelte:fragment>
<svelte:fragment slot="controls">
<GridCreateEditRowModal />
<GridManageAccessButton />
</svelte:fragment>
</Grid>
</div>
<style>
.wrapper {
flex: 1 1 auto;
margin: -28px -40px -40px -40px;
display: flex;
flex-direction: column;
background: var(--background);
overflow: hidden;
}
</style>

View File

@ -1,13 +1,15 @@
<script> <script>
import { ActionButton, Modal } from "@budibase/bbui" import { Button, Modal } from "@budibase/bbui"
import EditRolesModal from "../modals/EditRoles.svelte" import EditRolesModal from "../modals/EditRoles.svelte"
let modal let modal
</script> </script>
<ActionButton icon="UsersLock" quiet on:click={modal.show}> <div>
<Button secondary icon="UsersLock" on:click on:click={modal.show}>
Edit roles Edit roles
</ActionButton> </Button>
<Modal bind:this={modal}> </div>
<Modal bind:this={modal} on:show on:hide>
<EditRolesModal /> <EditRolesModal />
</Modal> </Modal>

View File

@ -1,23 +1,31 @@
<script> <script>
import { ActionButton, Modal } from "@budibase/bbui" import { ActionButton } from "@budibase/bbui"
import { permissions } from "stores/builder" import { permissions } from "stores/builder"
import ManageAccessModal from "../modals/ManageAccessModal.svelte" import ManageAccessModal from "../modals/ManageAccessModal.svelte"
import DetailPopover from "components/common/DetailPopover.svelte"
import EditRolesButton from "./EditRolesButton.svelte"
export let resourceId export let resourceId
export let disabled = false
let modal
let resourcePermissions let resourcePermissions
let showPopover = true
async function openModal() { $: fetchPermissions(resourceId)
resourcePermissions = await permissions.forResourceDetailed(resourceId)
modal.show() const fetchPermissions = async id => {
resourcePermissions = await permissions.forResourceDetailed(id)
} }
</script> </script>
<ActionButton icon="LockClosed" quiet on:click={openModal} {disabled}> <DetailPopover title="Manage access" {showPopover}>
Access <svelte:fragment slot="anchor" let:open>
</ActionButton> <ActionButton icon="LockClosed" selected={open} quiet>Access</ActionButton>
<Modal bind:this={modal}> </svelte:fragment>
{#if resourcePermissions}
<ManageAccessModal {resourceId} permissions={resourcePermissions} /> <ManageAccessModal {resourceId} permissions={resourcePermissions} />
</Modal> {/if}
<EditRolesButton
on:show={() => (showPopover = false)}
on:hide={() => (showPopover = true)}
/>
</DetailPopover>

View File

@ -5,6 +5,7 @@
import { getUserBindings } from "dataBinding" import { getUserBindings } from "dataBinding"
import { makePropSafe } from "@budibase/string-templates" import { makePropSafe } from "@budibase/string-templates"
import { search } from "@budibase/frontend-core" import { search } from "@budibase/frontend-core"
import { utils } from "@budibase/shared-core"
import { tables } from "stores/builder" import { tables } from "stores/builder"
export let schema export let schema
@ -16,15 +17,19 @@
let drawer let drawer
$: tempValue = filters || [] $: localFilters = utils.processSearchFilters(filters)
$: schemaFields = search.getFields( $: schemaFields = search.getFields(
$tables.list, $tables.list,
Object.values(schema || {}), Object.values(schema || {}),
{ allowLinks: true } { allowLinks: true }
) )
$: text = getText(filters) $: filterCount =
$: selected = tempValue.filter(x => !x.onEmptyFilter)?.length > 0 localFilters?.groups?.reduce((acc, group) => {
return (acc += group.filters.filter(filter => filter.field).length)
}, 0) || 0
$: bindings = [ $: bindings = [
{ {
type: "context", type: "context",
@ -38,28 +43,33 @@
}, },
...getUserBindings(), ...getUserBindings(),
] ]
const getText = filters => {
const count = filters?.filter(filter => filter.field)?.length
return count ? `Filter (${count})` : "Filter"
}
</script> </script>
<ActionButton icon="Filter" quiet {disabled} on:click={drawer.show} {selected}> <ActionButton
{text} icon="Filter"
quiet
{disabled}
on:click={drawer.show}
selected={filterCount > 0}
accentColor="#004EA6"
>
{filterCount ? `Filter: ${filterCount}` : "Filter"}
</ActionButton> </ActionButton>
<Drawer <Drawer
bind:this={drawer} bind:this={drawer}
title="Filtering" title="Filtering"
on:drawerHide on:drawerHide
on:drawerShow on:drawerShow={() => {
localFilters = utils.processSearchFilters(filters)
}}
forceModal forceModal
> >
<Button <Button
cta cta
slot="buttons" slot="buttons"
on:click={() => { on:click={() => {
dispatch("change", tempValue) dispatch("change", localFilters)
drawer.hide() drawer.hide()
}} }}
> >
@ -67,10 +77,10 @@
</Button> </Button>
<DrawerContent slot="body"> <DrawerContent slot="body">
<FilterBuilder <FilterBuilder
{filters} filters={localFilters}
{schemaFields} {schemaFields}
datasource={{ type: "table", tableId }} datasource={{ type: "table", tableId }}
on:change={e => (tempValue = e.detail)} on:change={e => (localFilters = e.detail)}
{bindings} {bindings}
/> />
</DrawerContent> </DrawerContent>

View File

@ -1,20 +1,19 @@
<script> <script>
import { getContext } from "svelte" import { getContext } from "svelte"
import { Icon, notifications, ActionButton, Popover } from "@budibase/bbui" import ToggleActionButtonGroup from "components/common/ToggleActionButtonGroup.svelte"
import { getColumnIcon } from "../lib/utils"
import ToggleActionButtonGroup from "./ToggleActionButtonGroup.svelte"
import { helpers } from "@budibase/shared-core" import { helpers } from "@budibase/shared-core"
import { SchemaUtils } from "@budibase/frontend-core"
import { Icon, notifications, ActionButton, Popover } from "@budibase/bbui"
import { FieldType } from "@budibase/types" import { FieldType } from "@budibase/types"
import { FieldPermissions } from "../../../constants" import { FieldPermissions } from "./GridColumnsSettingButton.svelte"
export let permissions = [FieldPermissions.WRITABLE, FieldPermissions.HIDDEN] export let permissions = [FieldPermissions.WRITABLE, FieldPermissions.HIDDEN]
export let disabledPermissions = [] export let disabledPermissions = []
export let columns export let columns
export let fromRelationshipField export let fromRelationshipField
export let canSetRelationshipSchemas
const { datasource, dispatch, config } = getContext("grid") const { datasource, dispatch } = getContext("grid")
$: canSetRelationshipSchemas = $config.canSetRelationshipSchemas
let relationshipPanelAnchor let relationshipPanelAnchor
let relationshipFieldName let relationshipFieldName
@ -153,9 +152,6 @@
await datasource.actions.saveSchemaMutations() await datasource.actions.saveSchemaMutations()
} catch (e) { } catch (e) {
notifications.error(e.message) notifications.error(e.message)
} finally {
await datasource.actions.resetSchemaMutations()
await datasource.actions.refreshDefinition()
} }
dispatch(visible ? "show-column" : "hide-column") dispatch(visible ? "show-column" : "hide-column")
} }
@ -177,7 +173,7 @@
<div class="columns"> <div class="columns">
{#each displayColumns as column} {#each displayColumns as column}
<div class="column"> <div class="column">
<Icon size="S" name={getColumnIcon(column)} /> <Icon size="S" name={SchemaUtils.getColumnIcon(column)} />
<div class="column-label" title={column.label}> <div class="column-label" title={column.label}>
{column.label} {column.label}
</div> </div>
@ -198,6 +194,7 @@
size="S" size="S"
icon="ChevronRight" icon="ChevronRight"
quiet quiet
selected={relationshipFieldName === column.name}
/> />
</div> </div>
{/if} {/if}

View File

@ -0,0 +1,75 @@
<script>
import { ActionButton, List, ListItem, Button } from "@budibase/bbui"
import DetailPopover from "components/common/DetailPopover.svelte"
import { TriggerStepID } from "constants/backend/automations"
import { automationStore, appStore } from "stores/builder"
import { createEventDispatcher, getContext } from "svelte"
const dispatch = createEventDispatcher()
const { datasource } = getContext("grid")
const triggerTypes = [
TriggerStepID.ROW_SAVED,
TriggerStepID.ROW_UPDATED,
TriggerStepID.ROW_DELETED,
]
let popover
$: ds = $datasource
$: resourceId = ds?.type === "table" ? ds.tableId : ds?.id
$: connectedAutomations = findConnectedAutomations(
$automationStore.automations,
resourceId
)
$: automationCount = connectedAutomations.length
const findConnectedAutomations = (automations, resourceId) => {
return automations.filter(automation => {
if (!triggerTypes.includes(automation.definition?.trigger?.stepId)) {
return false
}
return automation.definition?.trigger?.inputs?.tableId === resourceId
})
}
const generateAutomation = () => {
popover?.hide()
dispatch("request-generate")
}
</script>
<DetailPopover title="Automations" bind:this={popover}>
<svelte:fragment slot="anchor" let:open>
<ActionButton
icon="JourneyVoyager"
selected={open || automationCount}
quiet
accentColor="#5610AD"
>
Automations{automationCount ? `: ${automationCount}` : ""}
</ActionButton>
</svelte:fragment>
{#if !connectedAutomations.length}
There aren't any automations connected to this data.
{:else}
The following automations are connected to this data.
<List>
{#each connectedAutomations as automation}
<ListItem
icon={automation.disabled ? "PauseCircle" : "PlayCircle"}
iconColor={automation.disabled
? "var(--spectrum-global-color-gray-600)"
: "var(--spectrum-global-color-green-600)"}
title={automation.name}
url={`/builder/app/${$appStore.appId}/automation/${automation._id}`}
showArrow
/>
{/each}
</List>
{/if}
<div>
<Button secondary icon="JourneyVoyager" on:click={generateAutomation}>
Generate automation
</Button>
</div>
</DetailPopover>

View File

@ -1,8 +1,17 @@
<script context="module">
export const FieldPermissions = {
WRITABLE: "writable",
READONLY: "readonly",
HIDDEN: "hidden",
}
</script>
<script> <script>
import { getContext } from "svelte" import { getContext } from "svelte"
import { ActionButton, Popover } from "@budibase/bbui" import { ActionButton, Popover } from "@budibase/bbui"
import ColumnsSettingContent from "./ColumnsSettingContent.svelte" import ColumnsSettingContent from "./ColumnsSettingContent.svelte"
import { FieldPermissions } from "../../../constants" import { isEnabled } from "helpers/featureFlags"
import { FeatureFlag } from "@budibase/types"
const { tableColumns, datasource } = getContext("grid") const { tableColumns, datasource } = getContext("grid")
@ -12,7 +21,7 @@
$: anyRestricted = $tableColumns.filter( $: anyRestricted = $tableColumns.filter(
col => !col.visible || col.readonly col => !col.visible || col.readonly
).length ).length
$: text = anyRestricted ? `Columns (${anyRestricted} restricted)` : "Columns" $: text = anyRestricted ? `Columns: ${anyRestricted} restricted` : "Columns"
$: permissions = $: permissions =
$datasource.type === "viewV2" $datasource.type === "viewV2"
? [ ? [
@ -31,11 +40,16 @@
on:click={() => (open = !open)} on:click={() => (open = !open)}
selected={open || anyRestricted} selected={open || anyRestricted}
disabled={!$tableColumns.length} disabled={!$tableColumns.length}
accentColor="#674D00"
> >
{text} {text}
</ActionButton> </ActionButton>
</div> </div>
<Popover bind:open {anchor} align="left"> <Popover bind:open {anchor} align="left">
<ColumnsSettingContent columns={$tableColumns} {permissions} /> <ColumnsSettingContent
columns={$tableColumns}
canSetRelationshipSchemas={isEnabled(FeatureFlag.ENRICHED_RELATIONSHIPS)}
{permissions}
/>
</Popover> </Popover>

View File

@ -1,101 +0,0 @@
<script>
import {
ActionButton,
Popover,
Menu,
MenuItem,
notifications,
} from "@budibase/bbui"
import { getContext } from "svelte"
import { automationStore, tables, builderStore } from "stores/builder"
import { TriggerStepID } from "constants/backend/automations"
import { goto } from "@roxi/routify"
const { datasource } = getContext("grid")
$: triggers = $automationStore.blockDefinitions.CREATABLE_TRIGGER
$: table = $tables.list.find(table => table._id === $datasource.tableId)
async function createAutomation(type) {
const triggerType = triggers[type]
if (!triggerType) {
console.error("Invalid trigger type", type)
notifications.error("Invalid automation trigger type")
return
}
if (!table) {
notifications.error("Invalid table, cannot create automation")
return
}
const automationName = `${table.name} : Row ${
type === TriggerStepID.ROW_SAVED ? "created" : "updated"
}`
const triggerBlock = automationStore.actions.constructBlock(
"TRIGGER",
triggerType.stepId,
triggerType
)
triggerBlock.inputs = { tableId: $datasource.tableId }
try {
const response = await automationStore.actions.create(
automationName,
triggerBlock
)
builderStore.setPreviousTopNavPath(
"/builder/app/:application/data",
window.location.pathname
)
$goto(`/builder/app/${response.appId}/automation/${response.id}`)
notifications.success(`Automation created`)
} catch (e) {
console.error("Error creating automation", e)
notifications.error("Error creating automation")
}
}
let anchor
let open
</script>
<div bind:this={anchor}>
<ActionButton
icon="MagicWand"
quiet
size="M"
on:click={() => (open = !open)}
selected={open}
>
Generate
</ActionButton>
</div>
<Popover bind:open {anchor} align="left">
<Menu>
<MenuItem
icon="ShareAndroid"
on:click={() => {
open = false
createAutomation(TriggerStepID.ROW_SAVED)
}}
>
Automation: when row is created
</MenuItem>
<MenuItem
icon="ShareAndroid"
on:click={() => {
open = false
createAutomation(TriggerStepID.ROW_UPDATED)
}}
>
Automation: when row is updated
</MenuItem>
</Menu>
</Popover>
<style>
</style>

View File

@ -1,29 +0,0 @@
<script>
import { getContext } from "svelte"
import { Modal, ActionButton, TooltipType, TempTooltip } from "@budibase/bbui"
import GridCreateViewModal from "../../modals/grid/GridCreateViewModal.svelte"
const { filter } = getContext("grid")
let modal
let firstFilterUsage = false
$: {
if ($filter?.length && !firstFilterUsage) {
firstFilterUsage = true
}
}
</script>
<TempTooltip
text="Create a view to save your filters"
type={TooltipType.Info}
condition={firstFilterUsage}
>
<ActionButton icon="CollectionAdd" quiet on:click={modal.show}>
Create view
</ActionButton>
</TempTooltip>
<Modal bind:this={modal}>
<GridCreateViewModal />
</Modal>

View File

@ -0,0 +1,191 @@
<script>
import { ActionButton, ListItem, notifications } from "@budibase/bbui"
import { getContext } from "svelte"
import {
automationStore,
tables,
builderStore,
viewsV2,
} from "stores/builder"
import { TriggerStepID } from "constants/backend/automations"
import { goto } from "@roxi/routify"
import DetailPopover from "components/common/DetailPopover.svelte"
import MagicWand from "./magic-wand.svg"
import { AutoScreenTypes } from "constants"
import CreateScreenModal from "pages/builder/app/[application]/design/_components/NewScreen/CreateScreenModal.svelte"
import { getSequentialName } from "helpers/duplicate"
const { datasource } = getContext("grid")
let popover
let createScreenModal
$: triggers = $automationStore.blockDefinitions.CREATABLE_TRIGGER
$: table = $tables.list.find(table => table._id === $datasource.tableId)
export const show = () => popover?.show()
export const hide = () => popover?.hide()
async function createAutomation(type) {
const triggerType = triggers[type]
if (!triggerType) {
console.error("Invalid trigger type", type)
notifications.error("Invalid automation trigger type")
return
}
if (!table) {
notifications.error("Invalid table, cannot create automation")
return
}
const suffixMap = {
[TriggerStepID.ROW_SAVED]: "created",
[TriggerStepID.ROW_UPDATED]: "updated",
[TriggerStepID.ROW_DELETED]: "deleted",
}
const namePrefix = `Row ${suffixMap[type]} `
const automationName = getSequentialName(
$automationStore.automations,
namePrefix,
{
getName: x => x.name,
}
)
const triggerBlock = automationStore.actions.constructBlock(
"TRIGGER",
triggerType.stepId,
triggerType
)
triggerBlock.inputs = { tableId: $datasource.tableId }
try {
const response = await automationStore.actions.create(
automationName,
triggerBlock
)
builderStore.setPreviousTopNavPath(
"/builder/app/:application/data",
window.location.pathname
)
$goto(`/builder/app/${response.appId}/automation/${response._id}`)
notifications.success(`Automation created successfully`)
} catch (e) {
console.error(e)
notifications.error("Error creating automation")
}
}
const startScreenWizard = autoScreenType => {
popover.hide()
let preSelected
if ($datasource.type === "table") {
preSelected = $tables.list.find(x => x._id === $datasource.tableId)
} else {
preSelected = $viewsV2.list.find(x => x.id === $datasource.id)
}
createScreenModal.show(autoScreenType, preSelected)
}
</script>
<DetailPopover title="Generate" bind:this={popover}>
<svelte:fragment slot="anchor" let:open>
<ActionButton selected={open}>
<div class="center">
<img height={16} alt="magic wand" src={MagicWand} />
Generate
</div>
</ActionButton>
</svelte:fragment>
{#if $datasource.type === "table"}
Generate a new app screen or automation from this data.
{:else}
Generate a new app screen from this data.
{/if}
<div class="generate-section">
<div class="generate-section__title">App screens</div>
<div class="generate-section__options">
<div>
<ListItem
title="Table"
icon="TableEdit"
hoverable
on:click={() => startScreenWizard(AutoScreenTypes.TABLE)}
iconColor="var(--spectrum-global-color-gray-600)"
/>
</div>
<div>
<ListItem
title="Form"
icon="Form"
hoverable
on:click={() => startScreenWizard(AutoScreenTypes.FORM)}
iconColor="var(--spectrum-global-color-gray-600)"
/>
</div>
</div>
</div>
{#if $datasource.type === "table"}
<div class="generate-section">
<div class="generate-section__title">Automation triggers (When a...)</div>
<div class="generate-section__options">
<div>
<ListItem
title="Row is created"
icon="TableRowAddBottom"
hoverable
on:click={() => createAutomation(TriggerStepID.ROW_SAVED)}
iconColor="var(--spectrum-global-color-gray-600)"
/>
</div>
<div>
<ListItem
title="Row is updated"
icon="Refresh"
hoverable
on:click={() => createAutomation(TriggerStepID.ROW_UPDATED)}
iconColor="var(--spectrum-global-color-gray-600)"
/>
</div>
<div>
<ListItem
title="Row is deleted"
icon="TableRowRemoveCenter"
hoverable
on:click={() => createAutomation(TriggerStepID.ROW_DELETED)}
iconColor="var(--spectrum-global-color-gray-600)"
/>
</div>
</div>
</div>
{/if}
</DetailPopover>
<CreateScreenModal bind:this={createScreenModal} />
<style>
.center {
display: flex;
align-items: center;
gap: 8px;
}
.generate-section {
display: flex;
flex-direction: column;
gap: 8px;
}
.generate-section__title {
color: var(--spectrum-global-color-gray-600);
}
.generate-section__options {
display: grid;
grid-template-columns: 1fr 1fr;
grid-column-gap: 16px;
grid-row-gap: 8px;
}
</style>

View File

@ -4,14 +4,8 @@
const { datasource } = getContext("grid") const { datasource } = getContext("grid")
$: resourceId = getResourceID($datasource) $: ds = $datasource
$: resourceId = ds?.type === "table" ? ds.tableId : ds?.id
const getResourceID = datasource => {
if (!datasource) {
return null
}
return datasource.type === "table" ? datasource.tableId : datasource.id
}
</script> </script>
<ManageAccessButton {resourceId} /> <ManageAccessButton {resourceId} />

View File

@ -0,0 +1,146 @@
<script>
import {
ActionButton,
List,
ListItem,
Button,
Toggle,
notifications,
Modal,
ModalContent,
Input,
} from "@budibase/bbui"
import DetailPopover from "components/common/DetailPopover.svelte"
import { getContext } from "svelte"
import { appStore, rowActions } from "stores/builder"
import { goto, url } from "@roxi/routify"
import { derived } from "svelte/store"
const { datasource } = getContext("grid")
let popover
let createModal
let newName
$: ds = $datasource
$: tableId = ds?.tableId
$: viewId = ds?.id
$: isView = ds?.type === "viewV2"
$: tableRowActions = $rowActions[tableId] || []
$: viewRowActions = $rowActions[viewId] || []
$: actionCount = isView ? viewRowActions.length : tableRowActions.length
$: newNameInvalid = newName && tableRowActions.some(x => x.name === newName)
const rowActionUrl = derived([url, appStore], ([$url, $appStore]) => {
return ({ automationId }) => {
return $url(`/builder/app/${$appStore.appId}/automation/${automationId}`)
}
})
const toggleAction = async (action, enabled) => {
if (enabled) {
await rowActions.enableView(tableId, viewId, action.id)
} else {
await rowActions.disableView(tableId, viewId, action.id)
}
}
const showCreateModal = () => {
newName = null
popover.hide()
createModal.show()
}
const createRowAction = async () => {
try {
const newRowAction = await rowActions.createRowAction(
tableId,
viewId,
newName
)
notifications.success("Row action created successfully")
$goto($rowActionUrl(newRowAction))
} catch (error) {
console.error(error)
notifications.error("Error creating row action")
}
}
</script>
<DetailPopover title="Row actions" bind:this={popover}>
<svelte:fragment slot="anchor" let:open>
<ActionButton
icon="Engagement"
selected={open || actionCount}
quiet
accentColor="#A24400"
>
Row actions{actionCount ? `: ${actionCount}` : ""}
</ActionButton>
</svelte:fragment>
A row action is a user-triggered automation for a chosen row.
{#if isView && rowActions.length}
<br />
Use the toggle to enable/disable row actions for this view.
<br />
{/if}
{#if !tableRowActions.length}
<br />
You haven't created any row actions.
{:else}
<List>
{#each tableRowActions as action}
<ListItem title={action.name} url={$rowActionUrl(action)} showArrow>
<svelte:fragment slot="right">
{#if isView}
<span>
<Toggle
value={action.allowedSources?.includes(viewId)}
on:change={e => toggleAction(action, e.detail)}
/>
</span>
{/if}
</svelte:fragment>
</ListItem>
{/each}
</List>
{/if}
<div>
<Button secondary icon="Engagement" on:click={showCreateModal}>
Create row action
</Button>
</div>
</DetailPopover>
<Modal bind:this={createModal}>
<ModalContent
size="S"
title="Create row action"
confirmText="Create"
showCancelButton={false}
showDivider={false}
showCloseIcon={false}
disabled={!newName || newNameInvalid}
onConfirm={createRowAction}
let:loading
>
<Input
label="Name"
bind:value={newName}
error={newNameInvalid && !loading
? "A row action with this name already exists"
: null}
/>
</ModalContent>
</Modal>
<style>
span :global(.spectrum-Switch) {
min-height: 0;
margin-right: 0;
}
span :global(.spectrum-Switch-switch) {
margin-bottom: 0;
margin-top: 2px;
}
</style>

View File

@ -0,0 +1,59 @@
<script>
import { ActionButton, List, ListItem, Button } from "@budibase/bbui"
import DetailPopover from "components/common/DetailPopover.svelte"
import { screenStore, appStore } from "stores/builder"
import { getContext, createEventDispatcher } from "svelte"
const { datasource } = getContext("grid")
const dispatch = createEventDispatcher()
let popover
$: ds = $datasource
$: resourceId = ds?.type === "table" ? ds.tableId : ds?.id
$: connectedScreens = findConnectedScreens($screenStore.screens, resourceId)
$: screenCount = connectedScreens.length
const findConnectedScreens = (screens, resourceId) => {
return screens.filter(screen => {
return JSON.stringify(screen).includes(`"${resourceId}"`)
})
}
const generateScreen = () => {
popover?.hide()
dispatch("request-generate")
}
</script>
<DetailPopover title="Screens" bind:this={popover}>
<svelte:fragment slot="anchor" let:open>
<ActionButton
icon="WebPage"
selected={open || screenCount}
quiet
accentColor="#364800"
>
Screens{screenCount ? `: ${screenCount}` : ""}
</ActionButton>
</svelte:fragment>
{#if !connectedScreens.length}
There aren't any screens connected to this data.
{:else}
The following screens are connected to this data.
<List>
{#each connectedScreens as screen}
<ListItem
title={screen.routing.route}
url={`/builder/app/${$appStore.appId}/design/${screen._id}`}
showArrow
/>
{/each}
</List>
{/if}
<div>
<Button secondary icon="WebPage" on:click={generateScreen}>
Generate app screen
</Button>
</div>
</DetailPopover>

View File

@ -1,34 +1,34 @@
<script> <script>
import { getContext } from "svelte" import { getContext } from "svelte"
import { ActionButton, Popover, Label } from "@budibase/bbui" import { ActionButton, Popover, Label } from "@budibase/bbui"
import {
DefaultColumnWidth,
LargeRowHeight,
MediumRowHeight,
SmallRowHeight,
} from "../lib/constants"
const { columns, rowHeight, definition, fixedRowHeight, datasource } = const {
getContext("grid") Constants,
columns,
rowHeight,
definition,
fixedRowHeight,
datasource,
} = getContext("grid")
// Some constants for column width options // Some constants for column width options
const smallColSize = 120 const smallColSize = 120
const mediumColSize = DefaultColumnWidth const mediumColSize = Constants.DefaultColumnWidth
const largeColSize = DefaultColumnWidth * 1.5 const largeColSize = Constants.DefaultColumnWidth * 1.5
// Row height sizes // Row height sizes
const rowSizeOptions = [ const rowSizeOptions = [
{ {
label: "Small", label: "Small",
size: SmallRowHeight, size: Constants.SmallRowHeight,
}, },
{ {
label: "Medium", label: "Medium",
size: MediumRowHeight, size: Constants.MediumRowHeight,
}, },
{ {
label: "Large", label: "Large",
size: LargeRowHeight, size: Constants.LargeRowHeight,
}, },
] ]

View File

@ -0,0 +1,6 @@
<svg width="13" height="12" viewBox="0 0 13 12" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M9.4179 4.13222C9.4179 3.73121 9.26166 3.35428 8.97913 3.07175C8.41342 2.50538 7.4239 2.50408 6.85753 3.07175L5.64342 4.28586C5.6291 4.30018 5.61543 4.3158 5.60305 4.33143C5.58678 4.3438 5.5718 4.35747 5.55683 4.37244L0.491426 9.43785C0.208245 9.72103 0.052002 10.098 0.052002 10.4983C0.052002 10.8987 0.208245 11.2756 0.491426 11.5588C0.774607 11.842 1.15153 11.9982 1.5519 11.9982C1.95227 11.9982 2.32919 11.842 2.61238 11.5588L8.97848 5.1927C9.26166 4.90952 9.4179 4.53259 9.4179 4.13222ZM1.90539 10.8518C1.7166 11.0406 1.3872 11.0406 1.1984 10.8518C1.10401 10.7574 1.05193 10.6318 1.05193 10.4983C1.05193 10.3649 1.104 10.2392 1.1984 10.1448L5.99821 5.34503L6.70845 6.04875L1.90539 10.8518ZM8.2715 4.48571L7.41544 5.34178L6.7052 4.63805L7.56452 3.77873C7.7533 3.58995 8.08271 3.58929 8.2715 3.77939C8.36589 3.87313 8.41798 3.99877 8.41798 4.13223C8.41798 4.26569 8.3659 4.39132 8.2715 4.48571Z" fill="#C8C8C8"/>
<path d="M11.8552 6.55146L11.0144 6.21913L10.879 5.32449C10.8356 5.03919 10.3737 4.98776 10.2686 5.255L9.93606 6.09642L9.04143 6.23085C8.89951 6.25216 8.78884 6.36658 8.77257 6.50947C8.75629 6.65253 8.83783 6.78826 8.97193 6.84148L9.81335 7.17464L9.94794 8.06862C9.9691 8.21053 10.0835 8.32121 10.2266 8.33748C10.3695 8.35375 10.5052 8.27221 10.5586 8.13811L10.8914 7.29751L11.7855 7.1621C11.9283 7.1403 12.0381 7.02637 12.0544 6.88348C12.0707 6.74058 11.9887 6.60403 11.8552 6.55146Z" fill="#F9634C"/>
<path d="M8.94215 1.76145L9.78356 2.0946L9.91815 2.9885C9.93931 3.13049 10.0539 3.24117 10.1968 3.25744C10.3398 3.27371 10.4756 3.19218 10.5288 3.05807L10.8618 2.21739L11.7559 2.08207C11.8985 2.06034 12.0085 1.94633 12.0248 1.80344C12.0411 1.66054 11.959 1.524 11.8254 1.47143L10.9847 1.13909L10.8494 0.244456C10.806 -0.0409246 10.3439 -0.0922745 10.2388 0.174881L9.90643 1.0163L9.0118 1.15089C8.86972 1.17213 8.75905 1.28654 8.74278 1.42952C8.72651 1.57249 8.80804 1.70823 8.94215 1.76145Z" fill="#8488FD"/>
<path d="M3.2379 2.46066L3.92063 2.73091L4.02984 3.45637C4.04709 3.57151 4.14002 3.66135 4.25606 3.67453C4.37194 3.6878 4.48212 3.62163 4.52541 3.51276L4.79557 2.83059L5.52094 2.72074C5.63682 2.70316 5.72601 2.61072 5.73936 2.49468C5.75254 2.37864 5.68597 2.26797 5.57758 2.22533L4.89533 1.95565L4.78548 1.22963C4.75016 0.998038 4.37535 0.956375 4.29007 1.17315L4.0204 1.85597L3.29437 1.96517C3.17915 1.98235 3.08931 2.07527 3.07613 2.19131C3.06294 2.30727 3.12902 2.41737 3.2379 2.46066Z" fill="#F7D804"/>
</svg>

After

Width:  |  Height:  |  Size: 2.5 KiB

View File

@ -4,6 +4,7 @@
Button, Button,
Label, Label,
Select, Select,
Multiselect,
Toggle, Toggle,
Icon, Icon,
DatePicker, DatePicker,
@ -25,6 +26,7 @@
import { createEventDispatcher, getContext, onMount } from "svelte" import { createEventDispatcher, getContext, onMount } from "svelte"
import { cloneDeep } from "lodash/fp" import { cloneDeep } from "lodash/fp"
import { tables, datasources } from "stores/builder" import { tables, datasources } from "stores/builder"
import { licensing } from "stores/portal"
import { TableNames, UNEDITABLE_USER_FIELDS } from "constants" import { TableNames, UNEDITABLE_USER_FIELDS } from "constants"
import { import {
FIELDS, FIELDS,
@ -34,6 +36,7 @@
} from "constants/backend" } from "constants/backend"
import { getAutoColumnInformation, buildAutoColumn } from "helpers/utils" import { getAutoColumnInformation, buildAutoColumn } from "helpers/utils"
import ConfirmDialog from "components/common/ConfirmDialog.svelte" import ConfirmDialog from "components/common/ConfirmDialog.svelte"
import AIFieldConfiguration from "components/common/AIFieldConfiguration.svelte"
import ModalBindableInput from "components/common/bindings/ModalBindableInput.svelte" import ModalBindableInput from "components/common/bindings/ModalBindableInput.svelte"
import { getBindings } from "components/backend/DataTable/formula" import { getBindings } from "components/backend/DataTable/formula"
import JSONSchemaModal from "./JSONSchemaModal.svelte" import JSONSchemaModal from "./JSONSchemaModal.svelte"
@ -49,18 +52,13 @@
import { isEnabled } from "helpers/featureFlags" import { isEnabled } from "helpers/featureFlags"
import { getUserBindings } from "dataBinding" import { getUserBindings } from "dataBinding"
const AUTO_TYPE = FieldType.AUTO export let field
const FORMULA_TYPE = FieldType.FORMULA
const LINK_TYPE = FieldType.LINK
const STRING_TYPE = FieldType.STRING
const NUMBER_TYPE = FieldType.NUMBER
const JSON_TYPE = FieldType.JSON
const DATE_TYPE = FieldType.DATETIME
const dispatch = createEventDispatcher() const dispatch = createEventDispatcher()
const { dispatch: gridDispatch, rows } = getContext("grid") const { dispatch: gridDispatch, rows } = getContext("grid")
const SafeID = `${makePropSafe("user")}.${makePropSafe("_id")}`
export let field const SingleUserDefault = `{{ ${SafeID} }}`
const MultiUserDefault = `{{ js "${btoa(`return [$("${SafeID}")]`)}" }}`
let mounted = false let mounted = false
let originalName let originalName
@ -103,13 +101,15 @@
let optionsValid = true let optionsValid = true
$: rowGoldenSample = RowUtils.generateGoldenSample($rows) $: rowGoldenSample = RowUtils.generateGoldenSample($rows)
$: aiEnabled =
$licensing.customAIConfigsEnabled || $licensing.budibaseAIEnabled
$: if (primaryDisplay) { $: if (primaryDisplay) {
editableColumn.constraints.presence = { allowEmpty: false } editableColumn.constraints.presence = { allowEmpty: false }
} }
$: { $: {
// this parses any changes the user has made when creating a new internal relationship // this parses any changes the user has made when creating a new internal relationship
// into what we expect the schema to look like // into what we expect the schema to look like
if (editableColumn.type === LINK_TYPE) { if (editableColumn.type === FieldType.LINK) {
relationshipTableIdPrimary = table._id relationshipTableIdPrimary = table._id
if (relationshipPart1 === PrettyRelationshipDefinitions.ONE) { if (relationshipPart1 === PrettyRelationshipDefinitions.ONE) {
relationshipOpts2 = relationshipOpts2.filter( relationshipOpts2 = relationshipOpts2.filter(
@ -137,15 +137,16 @@
} }
$: initialiseField(field, savingColumn) $: initialiseField(field, savingColumn)
$: checkConstraints(editableColumn) $: checkConstraints(editableColumn)
$: required = hasDefault $: required =
? false primaryDisplay ||
: !!editableColumn?.constraints?.presence || primaryDisplay editableColumn?.constraints?.presence === true ||
editableColumn?.constraints?.presence?.allowEmpty === false
$: uneditable = $: uneditable =
$tables.selected?._id === TableNames.USERS && $tables.selected?._id === TableNames.USERS &&
UNEDITABLE_USER_FIELDS.includes(editableColumn.name) UNEDITABLE_USER_FIELDS.includes(editableColumn.name)
$: invalid = $: invalid =
!editableColumn?.name || !editableColumn?.name ||
(editableColumn?.type === LINK_TYPE && !editableColumn?.tableId) || (editableColumn?.type === FieldType.LINK && !editableColumn?.tableId) ||
Object.keys(errors).length !== 0 || Object.keys(errors).length !== 0 ||
!optionsValid !optionsValid
$: errors = checkErrors(editableColumn) $: errors = checkErrors(editableColumn)
@ -168,12 +169,12 @@
// used to select what different options can be displayed for column type // used to select what different options can be displayed for column type
$: canBeDisplay = $: canBeDisplay =
canBeDisplayColumn(editableColumn) && !editableColumn.autocolumn canBeDisplayColumn(editableColumn) && !editableColumn.autocolumn
$: canHaveDefault = $: defaultValuesEnabled = isEnabled("DEFAULT_VALUES")
isEnabled("DEFAULT_VALUES") && canHaveDefaultColumn(editableColumn.type) $: canHaveDefault = !required && canHaveDefaultColumn(editableColumn.type)
$: canBeRequired = $: canBeRequired =
editableColumn?.type !== LINK_TYPE && editableColumn?.type !== FieldType.LINK &&
!uneditable && !uneditable &&
editableColumn?.type !== AUTO_TYPE && editableColumn?.type !== FieldType.AUTO &&
!editableColumn.autocolumn !editableColumn.autocolumn
$: hasDefault = $: hasDefault =
editableColumn?.default != null && editableColumn?.default !== "" editableColumn?.default != null && editableColumn?.default !== ""
@ -188,7 +189,6 @@
(originalName && (originalName &&
SWITCHABLE_TYPES[field.type] && SWITCHABLE_TYPES[field.type] &&
!editableColumn?.autocolumn) !editableColumn?.autocolumn)
$: allowedTypes = getAllowedTypes(datasource).map(t => ({ $: allowedTypes = getAllowedTypes(datasource).map(t => ({
fieldId: makeFieldId(t.type, t.subtype), fieldId: makeFieldId(t.type, t.subtype),
...t, ...t,
@ -206,6 +206,11 @@
}, },
...getUserBindings(), ...getUserBindings(),
] ]
$: sanitiseDefaultValue(
editableColumn.type,
editableColumn.constraints?.inclusion || [],
editableColumn.default
)
const fieldDefinitions = Object.values(FIELDS).reduce( const fieldDefinitions = Object.values(FIELDS).reduce(
// Storing the fields by complex field id // Storing the fields by complex field id
@ -218,7 +223,7 @@
function makeFieldId(type, subtype, autocolumn) { function makeFieldId(type, subtype, autocolumn) {
// don't make field IDs for auto types // don't make field IDs for auto types
if (type === AUTO_TYPE || autocolumn) { if (type === FieldType.AUTO || autocolumn) {
return type.toUpperCase() return type.toUpperCase()
} else if ( } else if (
type === FieldType.BB_REFERENCE || type === FieldType.BB_REFERENCE ||
@ -243,7 +248,7 @@
// Here we are setting the relationship values based on the editableColumn // Here we are setting the relationship values based on the editableColumn
// This part of the code is used when viewing an existing field hence the check // This part of the code is used when viewing an existing field hence the check
// for the tableId // for the tableId
if (editableColumn.type === LINK_TYPE && editableColumn.tableId) { if (editableColumn.type === FieldType.LINK && editableColumn.tableId) {
relationshipTableIdPrimary = table._id relationshipTableIdPrimary = table._id
relationshipTableIdSecondary = editableColumn.tableId relationshipTableIdSecondary = editableColumn.tableId
if (editableColumn.relationshipType in relationshipMap) { if (editableColumn.relationshipType in relationshipMap) {
@ -284,17 +289,33 @@
delete saveColumn.fieldId delete saveColumn.fieldId
if (saveColumn.type === AUTO_TYPE) { if (saveColumn.type === FieldType.AUTO) {
saveColumn = buildAutoColumn( saveColumn = buildAutoColumn(
$tables.selected.name, $tables.selected.name,
saveColumn.name, saveColumn.name,
saveColumn.subtype saveColumn.subtype
) )
} }
if (saveColumn.type !== LINK_TYPE) { if (saveColumn.type !== FieldType.LINK) {
delete saveColumn.fieldName delete saveColumn.fieldName
} }
// Ensure we don't have a default value if we can't have one
if (!canHaveDefault || !defaultValuesEnabled) {
delete saveColumn.default
}
// Ensure primary display columns are always required and don't have default values
if (primaryDisplay) {
saveColumn.constraints.presence = { allowEmpty: false }
delete saveColumn.default
}
// Ensure the field is not required if we have a default value
if (saveColumn.default) {
saveColumn.constraints.presence = false
}
try { try {
await tables.saveField({ await tables.saveField({
originalName, originalName,
@ -362,9 +383,9 @@
editableColumn.subtype = definition.subtype editableColumn.subtype = definition.subtype
// Default relationships many to many // Default relationships many to many
if (editableColumn.type === LINK_TYPE) { if (editableColumn.type === FieldType.LINK) {
editableColumn.relationshipType = RelationshipType.MANY_TO_MANY editableColumn.relationshipType = RelationshipType.MANY_TO_MANY
} else if (editableColumn.type === FORMULA_TYPE) { } else if (editableColumn.type === FieldType.FORMULA) {
editableColumn.formulaType = "dynamic" editableColumn.formulaType = "dynamic"
} }
} }
@ -430,6 +451,7 @@
FIELDS.BOOLEAN, FIELDS.BOOLEAN,
FIELDS.DATETIME, FIELDS.DATETIME,
FIELDS.LINK, FIELDS.LINK,
...(aiEnabled ? [FIELDS.AI] : []),
FIELDS.LONGFORM, FIELDS.LONGFORM,
FIELDS.USER, FIELDS.USER,
FIELDS.USERS, FIELDS.USERS,
@ -483,17 +505,23 @@
fieldToCheck.constraints = {} fieldToCheck.constraints = {}
} }
// some string types may have been built by server, may not always have constraints // some string types may have been built by server, may not always have constraints
if (fieldToCheck.type === STRING_TYPE && !fieldToCheck.constraints.length) { if (
fieldToCheck.type === FieldType.STRING &&
!fieldToCheck.constraints.length
) {
fieldToCheck.constraints.length = {} fieldToCheck.constraints.length = {}
} }
// some number types made server-side will be missing constraints // some number types made server-side will be missing constraints
if ( if (
fieldToCheck.type === NUMBER_TYPE && fieldToCheck.type === FieldType.NUMBER &&
!fieldToCheck.constraints.numericality !fieldToCheck.constraints.numericality
) { ) {
fieldToCheck.constraints.numericality = {} fieldToCheck.constraints.numericality = {}
} }
if (fieldToCheck.type === DATE_TYPE && !fieldToCheck.constraints.datetime) { if (
fieldToCheck.type === FieldType.DATETIME &&
!fieldToCheck.constraints.datetime
) {
fieldToCheck.constraints.datetime = {} fieldToCheck.constraints.datetime = {}
} }
} }
@ -541,6 +569,20 @@
return newError return newError
} }
const sanitiseDefaultValue = (type, options, defaultValue) => {
if (!defaultValue?.length) {
return
}
// Delete default value for options fields if the option is no longer available
if (type === FieldType.OPTIONS && !options.includes(defaultValue)) {
delete editableColumn.default
}
// Filter array default values to only valid options
if (type === FieldType.ARRAY) {
editableColumn.default = defaultValue.filter(x => options.includes(x))
}
}
onMount(() => { onMount(() => {
mounted = true mounted = true
}) })
@ -554,13 +596,13 @@
on:input={e => { on:input={e => {
if ( if (
!uneditable && !uneditable &&
!(linkEditDisabled && editableColumn.type === LINK_TYPE) !(linkEditDisabled && editableColumn.type === FieldType.LINK)
) { ) {
editableColumn.name = e.target.value editableColumn.name = e.target.value
} }
}} }}
disabled={uneditable || disabled={uneditable ||
(linkEditDisabled && editableColumn.type === LINK_TYPE)} (linkEditDisabled && editableColumn.type === FieldType.LINK)}
error={errors?.name} error={errors?.name}
/> />
{/if} {/if}
@ -574,7 +616,7 @@
getOptionValue={field => field.fieldId} getOptionValue={field => field.fieldId}
getOptionIcon={field => field.icon} getOptionIcon={field => field.icon}
isOptionEnabled={option => { isOptionEnabled={option => {
if (option.type === AUTO_TYPE) { if (option.type === FieldType.AUTO) {
return availableAutoColumnKeys?.length > 0 return availableAutoColumnKeys?.length > 0
} }
return true return true
@ -617,7 +659,7 @@
bind:optionColors={editableColumn.optionColors} bind:optionColors={editableColumn.optionColors}
bind:valid={optionsValid} bind:valid={optionsValid}
/> />
{:else if editableColumn.type === DATE_TYPE && !editableColumn.autocolumn} {:else if editableColumn.type === FieldType.DATETIME && !editableColumn.autocolumn}
<div class="split-label"> <div class="split-label">
<div class="label-length"> <div class="label-length">
<Label size="M">Earliest</Label> <Label size="M">Earliest</Label>
@ -704,7 +746,7 @@
{tableOptions} {tableOptions}
{errors} {errors}
/> />
{:else if editableColumn.type === FORMULA_TYPE} {:else if editableColumn.type === FieldType.FORMULA}
{#if !externalTable} {#if !externalTable}
<div class="split-label"> <div class="split-label">
<div class="label-length"> <div class="label-length">
@ -747,12 +789,19 @@
/> />
</div> </div>
</div> </div>
{:else if editableColumn.type === JSON_TYPE} {:else if editableColumn.type === FieldType.AI}
<Button primary text on:click={openJsonSchemaEditor} <AIFieldConfiguration
>Open schema editor</Button aiField={editableColumn}
> context={rowGoldenSample}
bindings={getBindings({ table })}
schema={table.schema}
/>
{:else if editableColumn.type === FieldType.JSON}
<Button primary text on:click={openJsonSchemaEditor}>
Open schema editor
</Button>
{/if} {/if}
{#if editableColumn.type === AUTO_TYPE || editableColumn.autocolumn} {#if editableColumn.type === FieldType.AUTO || editableColumn.autocolumn}
<Select <Select
label="Auto column type" label="Auto column type"
value={editableColumn.subtype} value={editableColumn.subtype}
@ -779,27 +828,51 @@
</div> </div>
{/if} {/if}
{#if canHaveDefault} {#if defaultValuesEnabled}
<div> {#if editableColumn.type === FieldType.OPTIONS}
<ModalBindableInput <Select
panel={ServerBindingPanel} disabled={!canHaveDefault}
title="Default" options={editableColumn.constraints?.inclusion || []}
label="Default" label="Default value"
value={editableColumn.default} value={editableColumn.default}
on:change={e => { on:change={e => (editableColumn.default = e.detail)}
editableColumn = { placeholder="None"
...editableColumn, />
default: e.detail, {:else if editableColumn.type === FieldType.ARRAY}
} <Multiselect
disabled={!canHaveDefault}
if (e.detail) { options={editableColumn.constraints?.inclusion || []}
setRequired(false) label="Default value"
} value={editableColumn.default}
}} on:change={e =>
(editableColumn.default = e.detail?.length ? e.detail : undefined)}
placeholder="None"
/>
{:else if editableColumn.subtype === BBReferenceFieldSubType.USER}
{@const defaultValue =
editableColumn.type === FieldType.BB_REFERENCE_SINGLE
? SingleUserDefault
: MultiUserDefault}
<Toggle
disabled={!canHaveDefault}
text="Default to current user"
value={editableColumn.default === defaultValue}
on:change={e =>
(editableColumn.default = e.detail ? defaultValue : undefined)}
/>
{:else}
<ModalBindableInput
disabled={!canHaveDefault}
panel={ServerBindingPanel}
title="Default value"
label="Default value"
placeholder="None"
value={editableColumn.default}
on:change={e => (editableColumn.default = e.detail)}
bindings={defaultValueBindings} bindings={defaultValueBindings}
allowJS allowJS
/> />
</div> {/if}
{/if} {/if}
</Layout> </Layout>

View File

@ -7,6 +7,7 @@
import { FIELDS } from "constants/backend" import { FIELDS } from "constants/backend"
const FORMULA_TYPE = FIELDS.FORMULA.type const FORMULA_TYPE = FIELDS.FORMULA.type
const AI_TYPE = FIELDS.AI.type
export let row = {} export let row = {}
@ -60,7 +61,7 @@
}} }}
> >
{#each tableSchema as [key, meta]} {#each tableSchema as [key, meta]}
{#if !meta.autocolumn && meta.type !== FORMULA_TYPE} {#if !meta.autocolumn && meta.type !== FORMULA_TYPE && meta.type !== AI_TYPE}
<div> <div>
<RowFieldControl error={errors[key]} {meta} bind:value={row[key]} /> <RowFieldControl error={errors[key]} {meta} bind:value={row[key]} />
</div> </div>

View File

@ -7,13 +7,9 @@
Select, Select,
notifications, notifications,
Body, Body,
ModalContent,
Tags,
Tag,
Icon, Icon,
} from "@budibase/bbui" } from "@budibase/bbui"
import { capitalise } from "helpers" import { capitalise } from "helpers"
import { getFormattedPlanName } from "helpers/planTitle"
import { get } from "svelte/store" import { get } from "svelte/store"
export let resourceId export let resourceId
@ -21,6 +17,32 @@
const inheritedRoleId = "inherited" const inheritedRoleId = "inherited"
let dependantsInfoMessage
$: loadDependantInfo(resourceId)
$: computedPermissions = Object.entries(permissions.permissions).reduce(
(p, [level, roleInfo]) => {
p[level] = {
selectedValue:
roleInfo.permissionType === PermissionSource.INHERITED
? inheritedRoleId
: roleInfo.role,
options: [...$roles],
}
if (roleInfo.inheritablePermission) {
p[level].inheritOption = roleInfo.inheritablePermission
p[level].options.unshift({
_id: inheritedRoleId,
name: `Inherit (${
get(roles).find(x => x._id === roleInfo.inheritablePermission).name
})`,
})
}
return p
},
{}
)
async function changePermission(level, role) { async function changePermission(level, role) {
try { try {
if (role === inheritedRoleId) { if (role === inheritedRoleId) {
@ -45,38 +67,9 @@
} }
} }
$: computedPermissions = Object.entries(permissions.permissions).reduce( async function loadDependantInfo(resourceId) {
(p, [level, roleInfo]) => {
p[level] = {
selectedValue:
roleInfo.permissionType === PermissionSource.INHERITED
? inheritedRoleId
: roleInfo.role,
options: [...get(roles)],
}
if (roleInfo.inheritablePermission) {
p[level].inheritOption = roleInfo.inheritablePermission
p[level].options.unshift({
_id: inheritedRoleId,
name: `Inherit (${
get(roles).find(x => x._id === roleInfo.inheritablePermission).name
})`,
})
}
return p
},
{}
)
$: requiresPlanToModify = permissions.requiresPlanToModify
let dependantsInfoMessage
async function loadDependantInfo() {
const dependantsInfo = await permissionsStore.getDependantsInfo(resourceId) const dependantsInfo = await permissionsStore.getDependantsInfo(resourceId)
const resourceByType = dependantsInfo?.resourceByType const resourceByType = dependantsInfo?.resourceByType
if (resourceByType) { if (resourceByType) {
const total = Object.values(resourceByType).reduce((p, c) => p + c, 0) const total = Object.values(resourceByType).reduce((p, c) => p + c, 0)
let resourceDisplay = let resourceDisplay =
@ -91,30 +84,15 @@
} }
} }
} }
loadDependantInfo()
</script> </script>
<ModalContent showCancelButton={false} showConfirmButton={false}> <Body size="S">Specify the minimum access level role for this data.</Body>
<span slot="header"> <div class="row">
Manage Access
{#if requiresPlanToModify}
<span class="lock-tag">
<Tags>
<Tag icon="LockClosed"
>{getFormattedPlanName(requiresPlanToModify)}</Tag
>
</Tags>
</span>
{/if}
</span>
<Body size="S">Specify the minimum access level role for this data.</Body>
<div class="row">
<Label extraSmall grey>Level</Label> <Label extraSmall grey>Level</Label>
<Label extraSmall grey>Role</Label> <Label extraSmall grey>Role</Label>
{#each Object.keys(computedPermissions) as level} {#each Object.keys(computedPermissions) as level}
<Input value={capitalise(level)} disabled /> <Input value={capitalise(level)} disabled />
<Select <Select
disabled={requiresPlanToModify}
placeholder={false} placeholder={false}
value={computedPermissions[level].selectedValue} value={computedPermissions[level].selectedValue}
on:change={e => changePermission(level, e.detail)} on:change={e => changePermission(level, e.detail)}
@ -123,9 +101,9 @@
getOptionValue={x => x._id} getOptionValue={x => x._id}
/> />
{/each} {/each}
</div> </div>
{#if dependantsInfoMessage} {#if dependantsInfoMessage}
<div class="inheriting-resources"> <div class="inheriting-resources">
<Icon name="Alert" /> <Icon name="Alert" />
<Body size="S"> <Body size="S">
@ -134,8 +112,7 @@
</i> </i>
</Body> </Body>
</div> </div>
{/if} {/if}
</ModalContent>
<style> <style>
.row { .row {
@ -143,11 +120,6 @@
grid-template-columns: 1fr 1fr; grid-template-columns: 1fr 1fr;
grid-gap: var(--spacing-s); grid-gap: var(--spacing-s);
} }
.lock-tag {
padding-left: var(--spacing-s);
}
.inheriting-resources { .inheriting-resources {
display: flex; display: flex;
gap: var(--spacing-s); gap: var(--spacing-s);

View File

@ -1,60 +0,0 @@
<script>
import { getContext } from "svelte"
import { Input, notifications, ModalContent } from "@budibase/bbui"
import { goto } from "@roxi/routify"
import { viewsV2 } from "stores/builder"
const { filter, sort, definition } = getContext("grid")
let name
$: views = Object.keys($definition?.views || {}).map(x => x.toLowerCase())
$: nameExists = views.includes(name?.trim().toLowerCase())
const enrichSchema = schema => {
// We need to sure that "visible" is set to true for any fields which have
// not yet been saved with grid metadata attached
const cloned = { ...schema }
Object.entries(cloned).forEach(([field, fieldSchema]) => {
if (fieldSchema.visible == null) {
cloned[field] = { ...cloned[field], visible: true }
}
})
return cloned
}
const saveView = async () => {
name = name?.trim()
try {
const newView = await viewsV2.create({
name,
tableId: $definition._id,
query: $filter,
sort: {
field: $sort.column,
order: $sort.order,
},
schema: enrichSchema($definition.schema),
primaryDisplay: $definition.primaryDisplay,
})
notifications.success(`View ${name} created`)
$goto(`../../view/v2/${newView.id}`)
} catch (error) {
notifications.error("Error creating view")
}
}
</script>
<ModalContent
title="Create view"
confirmText="Create view"
onConfirm={saveView}
disabled={nameExists}
>
<Input
label="View name"
thin
bind:value={name}
error={nameExists ? "A view already exists with that name" : null}
/>
</ModalContent>

View File

@ -39,10 +39,8 @@
const selectTable = tableId => { const selectTable = tableId => {
tables.select(tableId) tables.select(tableId)
if (!$isActive("./table/:tableId")) {
$goto(`./table/${tableId}`) $goto(`./table/${tableId}`)
} }
}
function openNode(datasource) { function openNode(datasource) {
toggledDatasources[datasource._id] = true toggledDatasources[datasource._id] = true

View File

@ -104,7 +104,7 @@
</InlineAlert> </InlineAlert>
</div> </div>
{/if} {/if}
<p class="fourthWarning">Please enter the app name below to confirm.</p> <p class="fourthWarning">Please enter the table name below to confirm.</p>
<Input bind:value={deleteTableName} placeholder={table.name} /> <Input bind:value={deleteTableName} placeholder={table.name} />
</div> </div>
</ConfirmDialog> </ConfirmDialog>

View File

@ -20,14 +20,6 @@
const getContextMenuItems = () => { const getContextMenuItems = () => {
return [ return [
{
icon: "Delete",
name: "Delete",
keyBind: null,
visible: true,
disabled: false,
callback: deleteConfirmationModal.show,
},
{ {
icon: "Edit", icon: "Edit",
name: "Edit", name: "Edit",
@ -36,6 +28,14 @@
disabled: false, disabled: false,
callback: editModal.show, callback: editModal.show,
}, },
{
icon: "Delete",
name: "Delete",
keyBind: null,
visible: true,
disabled: false,
callback: deleteConfirmationModal.show,
},
] ]
} }

View File

@ -1,33 +1,15 @@
<script> <script>
import { goto } from "@roxi/routify"
import TableNavItem from "./TableNavItem/TableNavItem.svelte" import TableNavItem from "./TableNavItem/TableNavItem.svelte"
import ViewNavItem from "./ViewNavItem/ViewNavItem.svelte" import { alphabetical } from "./utils"
export let tables export let tables
export let selectTable export let selectTable
$: sortedTables = tables.sort(alphabetical) $: sortedTables = tables.sort(alphabetical)
const alphabetical = (a, b) => {
return a.name?.toLowerCase() > b.name?.toLowerCase() ? 1 : -1
}
</script> </script>
<div class="hierarchy-items-container"> <div class="hierarchy-items-container">
{#each sortedTables as table, idx} {#each sortedTables as table, idx}
<TableNavItem {table} {idx} on:click={() => selectTable(table._id)} /> <TableNavItem {table} {idx} on:click={() => selectTable(table._id)} />
{#each [...Object.entries(table.views || {})].sort() as [name, view], idx (idx)}
<ViewNavItem
{view}
{name}
on:click={() => {
if (view.version === 2) {
$goto(`./view/v2/${encodeURIComponent(view.id)}`)
} else {
$goto(`./view/v1/${encodeURIComponent(name)}`)
}
}}
/>
{/each}
{/each} {/each}
</div> </div>

View File

@ -1,71 +0,0 @@
<script>
import {
contextMenuStore,
views,
viewsV2,
userSelectedResourceMap,
} from "stores/builder"
import NavItem from "components/common/NavItem.svelte"
import { isActive } from "@roxi/routify"
import { Icon } from "@budibase/bbui"
import EditViewModal from "./EditViewModal.svelte"
import DeleteConfirmationModal from "./DeleteConfirmationModal.svelte"
export let view
export let name
let editModal
let deleteConfirmationModal
const getContextMenuItems = () => {
return [
{
icon: "Delete",
name: "Delete",
keyBind: null,
visible: true,
disabled: false,
callback: deleteConfirmationModal.show,
},
{
icon: "Edit",
name: "Edit",
keyBind: null,
visible: true,
disabled: false,
callback: editModal.show,
},
]
}
const openContextMenu = e => {
e.preventDefault()
e.stopPropagation()
const items = getContextMenuItems()
contextMenuStore.open(view.id, items, { x: e.clientX, y: e.clientY })
}
const isViewActive = (view, isActive, views, viewsV2) => {
return (
(isActive("./view/v1") && views.selected?.name === view.name) ||
(isActive("./view/v2") && viewsV2.selected?.id === view.id)
)
}
</script>
<NavItem
on:contextmenu={openContextMenu}
indentLevel={2}
icon="Remove"
text={name}
selected={isViewActive(view, $isActive, $views, $viewsV2)}
hovering={view.id === $contextMenuStore.id}
on:click
selectedBy={$userSelectedResourceMap[name] ||
$userSelectedResourceMap[view.id]}
>
<Icon on:click={openContextMenu} s hoverable name="MoreSmallList" />
</NavItem>
<EditViewModal {view} bind:this={editModal} />
<DeleteConfirmationModal {view} bind:this={deleteConfirmationModal} />

View File

@ -66,3 +66,7 @@ export const parseFile = e => {
reader.readAsText(file) reader.readAsText(file)
}) })
} }
export const alphabetical = (a, b) => {
return a.name?.toLowerCase() > b.name?.toLowerCase() ? 1 : -1
}

View File

@ -0,0 +1,59 @@
<script>
import { Helpers, Multiselect, Select } from "@budibase/bbui"
import ServerBindingPanel from "components/common/bindings/ServerBindingPanel.svelte"
import ModalBindableInput from "components/common/bindings/ModalBindableInput.svelte"
import {
AIOperations,
OperationFields,
OperationFieldTypes,
} from "@budibase/shared-core"
const AIFieldConfigOptions = Object.keys(AIOperations).map(key => ({
label: AIOperations[key].label,
value: AIOperations[key].value,
}))
export let bindings
export let context
export let schema
export let aiField = {}
$: OperationField = OperationFields[aiField.operation]
$: schemaWithoutRelations = Object.keys(schema).filter(
key => schema[key].type !== "link"
)
</script>
<Select
label={"Operation"}
options={AIFieldConfigOptions}
bind:value={aiField.operation}
/>
{#if aiField.operation}
{#each Object.keys(OperationField) as key}
{#if OperationField[key] === OperationFieldTypes.BINDABLE_TEXT}
<ModalBindableInput
label={Helpers.capitalise(key)}
panel={ServerBindingPanel}
title="Prompt"
on:change={e => (aiField[key] = e.detail)}
value={aiField[key]}
{bindings}
allowJS
{context}
/>
{:else if OperationField[key] === OperationFieldTypes.MULTI_COLUMN}
<Multiselect
bind:value={aiField[key]}
label={Helpers.capitalise(key)}
options={schemaWithoutRelations}
/>
{:else if OperationField[key] === OperationFieldTypes.COLUMN}
<Select
bind:value={aiField[key]}
label={Helpers.capitalise(key)}
options={schemaWithoutRelations}
/>
{/if}
{/each}
{/if}

View File

@ -0,0 +1,75 @@
<script>
import { Popover, Icon } from "@budibase/bbui"
export let title
export let align = "left"
export let showPopover
let popover
let anchor
let open
export const show = () => popover?.show()
export const hide = () => popover?.hide()
</script>
<!-- svelte-ignore a11y-click-events-have-key-events -->
<!-- svelte-ignore a11y-no-static-element-interactions -->
<div class="anchor" bind:this={anchor} on:click={show}>
<slot name="anchor" {open} />
</div>
<Popover
bind:this={popover}
bind:open
minWidth={400}
maxWidth={400}
{anchor}
{align}
{showPopover}
on:open
on:close
>
<div class="detail-popover">
<div class="detail-popover__header">
<div class="detail-popover__title">
{title}
</div>
<Icon
name="Close"
hoverable
color="var(--spectrum-global-color-gray-600)"
hoverColor="var(--spectum-global-color-gray-900)"
on:click={hide}
/>
</div>
<div class="detail-popover__body">
<slot />
</div>
</div>
</Popover>
<style>
.detail-popover {
background-color: var(--spectrum-alias-background-color-primary);
}
.detail-popover__header {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
border-bottom: 1px solid var(--spectrum-global-color-gray-300);
padding: var(--spacing-l) var(--spacing-xl);
}
.detail-popover__title {
font-size: 16px;
font-weight: 600;
}
.detail-popover__body {
padding: var(--spacing-xl) var(--spacing-xl);
display: flex;
flex-direction: column;
align-items: stretch;
gap: var(--spacing-xl);
}
</style>

View File

@ -9,7 +9,7 @@
export let options export let options
</script> </script>
<div class="permissionPicker"> <div>
{#each options as option} {#each options as option}
<AbsTooltip text={option.tooltip} type={TooltipType.Info}> <AbsTooltip text={option.tooltip} type={TooltipType.Info}>
<ActionButton <ActionButton
@ -26,15 +26,14 @@
</div> </div>
<style> <style>
.permissionPicker { div {
display: flex; display: flex;
gap: var(--spacing-xs); gap: var(--spacing-xs);
} }
div :global(.spectrum-Icon) {
.permissionPicker :global(.spectrum-Icon) {
width: 14px; width: 14px;
} }
.permissionPicker :global(.spectrum-ActionButton) { div :global(.spectrum-ActionButton) {
width: 28px; width: 28px;
height: 28px; height: 28px;
} }

View File

@ -0,0 +1,104 @@
<script>
import { Select, Label, Checkbox } from "@budibase/bbui"
import { tables, viewsV2, rowActions } from "stores/builder"
import DrawerBindableInput from "components/common/bindings/DrawerBindableInput.svelte"
export let parameters
export let bindings = []
$: tableOptions = $tables.list.map(table => ({
label: table.name,
resourceId: table._id,
}))
$: viewOptions = $viewsV2.list.map(view => ({
label: view.name,
tableId: view.tableId,
resourceId: view.id,
}))
$: datasourceOptions = [...(tableOptions || []), ...(viewOptions || [])]
$: resourceId = parameters.resourceId
$: rowActions.refreshRowActions(resourceId)
$: enabledRowActions = $rowActions[resourceId] || []
$: rowActionOptions = enabledRowActions.map(action => ({
label: action.name,
value: action.id,
}))
</script>
<div class="root">
<div class="params">
<Label>Table or view</Label>
<Select
bind:value={parameters.resourceId}
options={datasourceOptions}
getOptionLabel={x => x.label}
getOptionValue={x => x.resourceId}
/>
<Label small>Row ID</Label>
<DrawerBindableInput
{bindings}
title="Row ID"
value={parameters.rowId}
on:change={value => (parameters.rowId = value.detail)}
/>
<Label small>Row action</Label>
<Select bind:value={parameters.rowActionId} options={rowActionOptions} />
<br />
<Checkbox text="Require confirmation" bind:value={parameters.confirm} />
{#if parameters.confirm}
<Label small>Title</Label>
<DrawerBindableInput
placeholder="Prompt User"
value={parameters.customTitleText}
on:change={e => (parameters.customTitleText = e.detail)}
{bindings}
/>
<Label small>Text</Label>
<DrawerBindableInput
placeholder="Are you sure you want to continue?"
value={parameters.confirmText}
on:change={e => (parameters.confirmText = e.detail)}
{bindings}
/>
<Label small>Confirm Text</Label>
<DrawerBindableInput
placeholder="Confirm"
value={parameters.confirmButtonText}
on:change={e => (parameters.confirmButtonText = e.detail)}
{bindings}
/>
<Label small>Cancel Text</Label>
<DrawerBindableInput
placeholder="Cancel"
value={parameters.cancelButtonText}
on:change={e => (parameters.cancelButtonText = e.detail)}
{bindings}
/>
{/if}
</div>
</div>
<style>
.root {
width: 100%;
max-width: 800px;
margin: 0 auto;
display: flex;
flex-direction: column;
justify-content: flex-start;
align-items: stretch;
gap: var(--spacing-xl);
}
.params {
display: grid;
column-gap: var(--spacing-l);
row-gap: var(--spacing-s);
grid-template-columns: 60px 1fr;
align-items: center;
}
</style>

View File

@ -33,7 +33,7 @@
const getSchemaFields = resourceId => { const getSchemaFields = resourceId => {
const { schema } = getSchemaForDatasourcePlus(resourceId) const { schema } = getSchemaForDatasourcePlus(resourceId)
return Object.values(schema || {}) return Object.values(schema || {}).filter(field => !field.readonly)
} }
const onFieldsChanged = e => { const onFieldsChanged = e => {

View File

@ -25,3 +25,4 @@ export { default as OpenModal } from "./OpenModal.svelte"
export { default as CloseModal } from "./CloseModal.svelte" export { default as CloseModal } from "./CloseModal.svelte"
export { default as ClearRowSelection } from "./ClearRowSelection.svelte" export { default as ClearRowSelection } from "./ClearRowSelection.svelte"
export { default as DownloadFile } from "./DownloadFile.svelte" export { default as DownloadFile } from "./DownloadFile.svelte"
export { default as RowAction } from "./RowAction.svelte"

View File

@ -178,6 +178,11 @@
"name": "Download File", "name": "Download File",
"type": "data", "type": "data",
"component": "DownloadFile" "component": "DownloadFile"
},
{
"name": "Row Action",
"type": "data",
"component": "RowAction"
} }
] ]
} }

View File

@ -2,10 +2,11 @@
import DraggableList from "../DraggableList/DraggableList.svelte" import DraggableList from "../DraggableList/DraggableList.svelte"
import ButtonSetting from "./ButtonSetting.svelte" import ButtonSetting from "./ButtonSetting.svelte"
import { createEventDispatcher } from "svelte" import { createEventDispatcher } from "svelte"
import { Helpers } from "@budibase/bbui" import { Helpers, Menu, MenuItem, Popover } from "@budibase/bbui"
import { componentStore } from "stores/builder" import { componentStore } from "stores/builder"
import { getEventContextBindings } from "dataBinding" import { getEventContextBindings } from "dataBinding"
import { cloneDeep, isEqual } from "lodash/fp" import { cloneDeep, isEqual } from "lodash/fp"
import { getRowActionButtonTemplates } from "templates/rowActions"
export let componentInstance export let componentInstance
export let componentBindings export let componentBindings
@ -17,13 +18,14 @@
const dispatch = createEventDispatcher() const dispatch = createEventDispatcher()
let focusItem
let cachedValue let cachedValue
let rowActionTemplates = []
let anchor
let popover
$: if (!isEqual(value, cachedValue)) { $: if (!isEqual(value, cachedValue)) {
cachedValue = cloneDeep(value) cachedValue = cloneDeep(value)
} }
$: buttonList = sanitizeValue(cachedValue) || [] $: buttonList = sanitizeValue(cachedValue) || []
$: buttonCount = buttonList.length $: buttonCount = buttonList.length
$: eventContextBindings = getEventContextBindings({ $: eventContextBindings = getEventContextBindings({
@ -73,17 +75,32 @@
_instanceName: Helpers.uuid(), _instanceName: Helpers.uuid(),
text: cfg.text, text: cfg.text,
type: cfg.type || "primary", type: cfg.type || "primary",
}, }
{}
) )
} }
const addButton = () => { const addCustomButton = () => {
const newButton = buildPseudoInstance({ const newButton = buildPseudoInstance({
text: `Button ${buttonCount + 1}`, text: `Button ${buttonCount + 1}`,
}) })
dispatch("change", [...buttonList, newButton]) dispatch("change", [...buttonList, newButton])
focusItem = newButton._id popover.hide()
}
const addRowActionTemplate = template => {
dispatch("change", [...buttonList, template])
popover.hide()
}
const addButton = async () => {
rowActionTemplates = await getRowActionButtonTemplates({
component: componentInstance,
})
if (rowActionTemplates.length) {
popover.show()
} else {
addCustomButton()
}
} }
const removeButton = id => { const removeButton = id => {
@ -105,12 +122,11 @@
listItemKey={"_id"} listItemKey={"_id"}
listType={ButtonSetting} listType={ButtonSetting}
listTypeProps={itemProps} listTypeProps={itemProps}
focus={focusItem}
draggable={buttonCount > 1} draggable={buttonCount > 1}
/> />
{/if} {/if}
<div <div
bind:this={anchor}
class="list-footer" class="list-footer"
class:disabled={!canAddButtons} class:disabled={!canAddButtons}
on:click={addButton} on:click={addButton}
@ -120,6 +136,17 @@
</div> </div>
</div> </div>
<Popover bind:this={popover} {anchor} useAnchorWidth resizable={false}>
<Menu>
<MenuItem on:click={addCustomButton}>Custom button</MenuItem>
{#each rowActionTemplates as template}
<MenuItem on:click={() => addRowActionTemplate(template)}>
{template.text}
</MenuItem>
{/each}
</Menu>
</Popover>
<style> <style>
.button-configuration :global(.spectrum-ActionButton) { .button-configuration :global(.spectrum-ActionButton) {
width: 100%; width: 100%;

View File

@ -1,87 +1,32 @@
<script> <script>
import DrawerBindableInput from "components/common/bindings/DrawerBindableInput.svelte"
import ClientBindingPanel from "components/common/bindings/ClientBindingPanel.svelte" import ClientBindingPanel from "components/common/bindings/ClientBindingPanel.svelte"
import { CoreFilterBuilder } from "@budibase/frontend-core"
import { dataFilters } from "@budibase/shared-core"
import { FilterBuilder } from "@budibase/frontend-core"
import { tables } from "stores/builder" import { tables } from "stores/builder"
import {
import { createEventDispatcher, onMount } from "svelte" runtimeToReadableBinding,
readableToRuntimeBinding,
} from "dataBinding"
export let schemaFields export let schemaFields
export let filters = [] export let filters
export let bindings = [] export let bindings = []
export let panel = ClientBindingPanel export let panel = ClientBindingPanel
export let allowBindings = true export let allowBindings = true
export let datasource export let datasource
export let showFilterEmptyDropdown export let showFilterEmptyDropdown
const dispatch = createEventDispatcher()
let rawFilters
$: parseFilters(rawFilters)
$: dispatch("change", enrichFilters(rawFilters))
// Remove field key prefixes and determine which behaviours to use
const parseFilters = filters => {
rawFilters = (filters || []).map(filter => {
const { field } = filter
let newFilter = { ...filter }
delete newFilter.allOr
newFilter.field = dataFilters.removeKeyNumbering(field)
return newFilter
})
}
onMount(() => {
parseFilters(filters)
rawFilters.forEach(filter => {
filter.type =
schemaFields.find(field => field.name === filter.field)?.type ||
filter.type
})
})
// Add field key prefixes and a special metadata filter object to indicate
// how to handle filter behaviour
const enrichFilters = rawFilters => {
let count = 1
return rawFilters
.filter(filter => filter.field)
.map(filter => ({
...filter,
field: `${count++}:${filter.field}`,
}))
.concat(...rawFilters.filter(filter => !filter.field))
}
</script> </script>
<FilterBuilder <CoreFilterBuilder
bind:filters={rawFilters} toReadable={runtimeToReadableBinding}
toRuntime={readableToRuntimeBinding}
behaviourFilters={true} behaviourFilters={true}
tables={$tables.list} tables={$tables.list}
{filters}
{panel}
{schemaFields} {schemaFields}
{datasource} {datasource}
{allowBindings} {allowBindings}
{showFilterEmptyDropdown} {showFilterEmptyDropdown}
>
<div slot="filtering-hero-content" />
<DrawerBindableInput
let:filter
slot="binding"
disabled={filter.noValue}
title={filter.field}
value={filter.value}
placeholder="Value"
{panel}
{bindings} {bindings}
on:change={event => { on:change
const indexToUpdate = rawFilters.findIndex(f => f.id === filter.id) />
rawFilters[indexToUpdate] = {
...rawFilters[indexToUpdate],
value: event.detail,
}
}}
/>
</FilterBuilder>

View File

@ -5,6 +5,7 @@
Button, Button,
Drawer, Drawer,
DrawerContent, DrawerContent,
Helpers,
} from "@budibase/bbui" } from "@budibase/bbui"
import { createEventDispatcher } from "svelte" import { createEventDispatcher } from "svelte"
import { getDatasourceForProvider, getSchemaForDatasource } from "dataBinding" import { getDatasourceForProvider, getSchemaForDatasource } from "dataBinding"
@ -21,7 +22,7 @@
let drawer let drawer
$: tempValue = value $: localFilters = Helpers.cloneDeep(value)
$: datasource = getDatasourceForProvider($selectedScreen, componentInstance) $: datasource = getDatasourceForProvider($selectedScreen, componentInstance)
$: dsSchema = getSchemaForDatasource($selectedScreen, datasource)?.schema $: dsSchema = getSchemaForDatasource($selectedScreen, datasource)?.schema
$: schemaFields = search.getFields( $: schemaFields = search.getFields(
@ -29,19 +30,24 @@
Object.values(schema || dsSchema || {}), Object.values(schema || dsSchema || {}),
{ allowLinks: true } { allowLinks: true }
) )
$: text = getText(value?.filter(filter => filter.field))
$: text = getText(value?.groups)
async function saveFilter() { async function saveFilter() {
dispatch("change", tempValue) dispatch("change", localFilters)
notifications.success("Filters saved") notifications.success("Filters saved")
drawer.hide() drawer.hide()
} }
const getText = filters => { const getText = (filterGroups = []) => {
if (!filters?.length) { const allFilters = filterGroups.reduce((acc, group) => {
return (acc += group.filters.filter(filter => filter.field).length)
}, 0)
if (allFilters === 0) {
return "No filters set" return "No filters set"
} else { } else {
return `${filters.length} filter${filters.length === 1 ? "" : "s"} set` return `${allFilters} filter${allFilters === 1 ? "" : "s"} set`
} }
} }
</script> </script>
@ -49,15 +55,25 @@
<div class="filter-editor"> <div class="filter-editor">
<ActionButton on:click={drawer.show}>{text}</ActionButton> <ActionButton on:click={drawer.show}>{text}</ActionButton>
</div> </div>
<Drawer bind:this={drawer} title="Filtering" on:drawerHide on:drawerShow> <Drawer
bind:this={drawer}
title="Filtering"
on:drawerHide
on:drawerShow={() => {
// Reset to the currently available value.
localFilters = Helpers.cloneDeep(value)
}}
>
<Button cta slot="buttons" on:click={saveFilter}>Save</Button> <Button cta slot="buttons" on:click={saveFilter}>Save</Button>
<DrawerContent slot="body"> <DrawerContent slot="body">
<FilterBuilder <FilterBuilder
filters={value} filters={localFilters}
{bindings} {bindings}
{schemaFields} {schemaFields}
{datasource} {datasource}
on:change={e => (tempValue = e.detail)} on:change={e => {
localFilters = e.detail
}}
/> />
</DrawerContent> </DrawerContent>
</Drawer> </Drawer>

View File

@ -54,6 +54,8 @@
_instanceName: `Step ${currentStep + 1}`, _instanceName: `Step ${currentStep + 1}`,
title: savedInstance.title ?? defaults?.title, title: savedInstance.title ?? defaults?.title,
buttons: savedInstance.buttons || defaults?.buttons, buttons: savedInstance.buttons || defaults?.buttons,
buttonsCollapsed: savedInstance.buttonsCollapsed,
buttonsCollapsedText: savedInstance.buttonsCollapsedText,
fields: savedInstance.fields, fields: savedInstance.fields,
desc: savedInstance.desc, desc: savedInstance.desc,

View File

@ -7,6 +7,7 @@
import FieldSetting from "./FieldSetting.svelte" import FieldSetting from "./FieldSetting.svelte"
import PrimaryColumnFieldSetting from "./PrimaryColumnFieldSetting.svelte" import PrimaryColumnFieldSetting from "./PrimaryColumnFieldSetting.svelte"
import getColumns from "./getColumns.js" import getColumns from "./getColumns.js"
import InfoDisplay from "pages/builder/app/[application]/design/[screenId]/[componentId]/_components/Component/InfoDisplay.svelte"
export let value export let value
export let componentInstance export let componentInstance
@ -60,7 +61,9 @@
</div> </div>
</div> </div>
{/if} {/if}
<DraggableList
{#if columns?.sortable?.length}
<DraggableList
on:change={e => columns.updateSortable(e.detail)} on:change={e => columns.updateSortable(e.detail)}
on:itemChange={e => columns.update(e.detail)} on:itemChange={e => columns.update(e.detail)}
items={columns.sortable} items={columns.sortable}
@ -69,7 +72,14 @@
listTypeProps={{ listTypeProps={{
bindings, bindings,
}} }}
/> />
{:else}
<InfoDisplay
body={datasource?.type !== "custom"
? "No available columns"
: "No available columns for JSON/CSV data sources"}
/>
{/if}
<style> <style>
.right-content { .right-content {

View File

@ -243,11 +243,14 @@
{/if} {/if}
{:else if (type === FieldType.BB_REFERENCE || type === FieldType.BB_REFERENCE_SINGLE) && condition.valueType === type} {:else if (type === FieldType.BB_REFERENCE || type === FieldType.BB_REFERENCE_SINGLE) && condition.valueType === type}
<FilterUsers <FilterUsers
bind:value={condition.referenceValue} value={condition.referenceValue}
multiselect={[ multiselect={[
Constants.OperatorOptions.In.value, Constants.OperatorOptions.In.value,
Constants.OperatorOptions.ContainsAny.value, Constants.OperatorOptions.ContainsAny.value,
].includes(condition.operator)} ].includes(condition.operator)}
on:change={e => {
condition.referenceValue = e.detail
}}
disabled={condition.noValue} disabled={condition.noValue}
type={condition.valueType} type={condition.valueType}
/> />

View File

@ -6,6 +6,7 @@ export const TriggerStepID = {
WEBHOOK: "WEBHOOK", WEBHOOK: "WEBHOOK",
APP: "APP", APP: "APP",
CRON: "CRON", CRON: "CRON",
ROW_ACTION: "ROW_ACTION",
} }
export const ActionStepID = { export const ActionStepID = {

View File

@ -159,6 +159,12 @@ export const FIELDS = {
icon: TypeIconMap[FieldType.FORMULA], icon: TypeIconMap[FieldType.FORMULA],
constraints: {}, constraints: {},
}, },
AI: {
name: "AI",
type: FieldType.AI,
icon: TypeIconMap[FieldType.AI],
constraints: {},
},
JSON: { JSON: {
name: "JSON", name: "JSON",
type: FieldType.JSON, type: FieldType.JSON,

View File

@ -72,3 +72,9 @@ export const PlanModel = {
} }
export const ChangelogURL = "https://docs.budibase.com/changelog" export const ChangelogURL = "https://docs.budibase.com/changelog"
export const AutoScreenTypes = {
BLANK: "blank",
TABLE: "table",
FORM: "form",
}

View File

@ -213,7 +213,7 @@ export const getComponentText = component => {
return component._instanceName return component._instanceName
} }
const type = const type =
component._component.replace("@budibase/standard-components/", "") || component._component?.replace("@budibase/standard-components/", "") ||
"component" "component"
return capitalise(type) return capitalise(type)
} }

View File

@ -12,7 +12,7 @@
stateKey: "selectedViewId", stateKey: "selectedViewId",
validate: id => $viewsV2.list?.some(view => view.id === id), validate: id => $viewsV2.list?.some(view => view.id === id),
update: viewsV2.select, update: viewsV2.select,
fallbackUrl: "../../", fallbackUrl: "../",
store: viewsV2, store: viewsV2,
routify, routify,
decode: decodeURIComponent, decode: decodeURIComponent,

View File

@ -0,0 +1,71 @@
<script>
import { viewsV2, rowActions } from "stores/builder"
import { admin, themeStore, licensing } from "stores/portal"
import { Grid } from "@budibase/frontend-core"
import { API } from "api"
import { notifications } from "@budibase/bbui"
import GridCreateEditRowModal from "components/backend/DataTable/modals/grid/GridCreateEditRowModal.svelte"
import GridFilterButton from "components/backend/DataTable/buttons/grid/GridFilterButton.svelte"
import GridManageAccessButton from "components/backend/DataTable/buttons/grid/GridManageAccessButton.svelte"
import GridSortButton from "components/backend/DataTable/buttons/grid/GridSortButton.svelte"
import GridColumnsSettingButton from "components/backend/DataTable/buttons/grid/GridColumnsSettingButton.svelte"
import GridSizeButton from "components/backend/DataTable/buttons/grid/GridSizeButton.svelte"
import GridGenerateButton from "components/backend/DataTable/buttons/grid/GridGenerateButton.svelte"
import GridScreensButton from "components/backend/DataTable/buttons/grid/GridScreensButton.svelte"
import GridRowActionsButton from "components/backend/DataTable/buttons/grid/GridRowActionsButton.svelte"
let generateButton
$: id = $viewsV2.selected?.id
$: datasource = {
type: "viewV2",
id,
tableId: $viewsV2.selected?.tableId,
}
$: buttons = makeRowActionButtons($rowActions[id])
$: rowActions.refreshRowActions(id)
$: currentTheme = $themeStore?.theme
$: darkMode = !currentTheme.includes("light")
const makeRowActionButtons = actions => {
return (actions || []).map(action => ({
text: action.name,
onClick: async row => {
await rowActions.trigger(id, action.id, row._id)
notifications.success("Row action triggered successfully")
},
}))
}
const handleGridViewUpdate = async e => {
viewsV2.replaceView(id, e.detail)
}
</script>
<Grid
{API}
{darkMode}
{datasource}
{buttons}
allowAddRows
allowDeleteRows
aiEnabled={$licensing.budibaseAIEnabled || $licensing.customAIConfigsEnabled}
showAvatars={false}
on:updatedatasource={handleGridViewUpdate}
isCloud={$admin.cloud}
buttonsCollapsed
>
<svelte:fragment slot="controls">
<GridFilterButton />
<GridSortButton />
<GridSizeButton />
<GridColumnsSettingButton />
<GridManageAccessButton />
<GridRowActionsButton />
<GridScreensButton on:request-generate={() => generateButton?.show()} />
</svelte:fragment>
<svelte:fragment slot="controls-right">
<GridGenerateButton bind:this={generateButton} />
</svelte:fragment>
<GridCreateEditRowModal />
</Grid>

View File

@ -0,0 +1,102 @@
<script>
import DetailPopover from "components/common/DetailPopover.svelte"
import { Input, notifications, Button, Icon } from "@budibase/bbui"
import { goto } from "@roxi/routify"
import { viewsV2 } from "stores/builder"
export let table
export let firstView = false
let name
let popover
$: views = Object.keys(table?.views || {}).map(x => x.toLowerCase())
$: trimmedName = name?.trim()
$: nameExists = views.includes(trimmedName?.toLowerCase())
$: nameValid = trimmedName?.length && !nameExists
export const show = () => {
popover?.show()
}
const enrichSchema = schema => {
// We need to sure that "visible" is set to true for any fields which have
// not yet been saved with grid metadata attached
const cloned = { ...schema }
Object.entries(cloned).forEach(([field, fieldSchema]) => {
if (fieldSchema.visible == null) {
cloned[field] = { ...cloned[field], visible: true }
}
})
return cloned
}
const saveView = async () => {
popover.hide()
try {
const newView = await viewsV2.create({
name: trimmedName,
tableId: table._id,
schema: enrichSchema(table.schema),
primaryDisplay: table.primaryDisplay,
})
notifications.success(`View ${name} created`)
$goto(`./${newView.id}`)
} catch (error) {
notifications.error("Error creating view")
}
}
</script>
<DetailPopover
title="Create view"
bind:this={popover}
on:open={() => (name = null)}
>
<svelte:fragment slot="anchor" let:open>
{#if firstView}
<Button cta>Create a view</Button>
{:else}
<div class="icon" class:open>
<Icon
name="Add"
size="L"
color="var(--spectrum-global-color-gray-600)"
/>
</div>
{/if}
</svelte:fragment>
<Input
label="Name"
thin
bind:value={name}
error={nameExists ? "A view already exists with that name" : null}
autofocus
/>
<div>
<Button cta on:click={saveView} disabled={nameExists || !nameValid}>
Create view
</Button>
</div>
</DetailPopover>
<style>
.icon {
height: 32px;
padding: 0 8px;
border-radius: 4px;
display: grid;
place-items: center;
transition: background 130ms ease-out;
cursor: pointer;
}
.icon:active,
.icon.open {
background: var(--spectrum-global-color-gray-300);
}
.icon:hover :global(svg),
.icon:active :global(svg),
.icon.open :global(svg) {
color: var(--spectrum-global-color-gray-900) !important;
}
</style>

View File

@ -20,6 +20,7 @@
} }
notifications.success("View deleted") notifications.success("View deleted")
} catch (error) { } catch (error) {
console.error(error)
notifications.error("Error deleting view") notifications.error("Error deleting view")
} }
} }

View File

@ -39,7 +39,7 @@
</script> </script>
<Modal bind:this={editorModal} on:show={initForm}> <Modal bind:this={editorModal} on:show={initForm}>
<ModalContent title="Edit View" onConfirm={save} confirmText="Save"> <ModalContent title="Edit view" onConfirm={save} confirmText="Save">
<Input label="View Name" thin bind:value={updatedName} /> <Input label="Name" thin bind:value={updatedName} />
</ModalContent> </ModalContent>
</Modal> </Modal>

View File

@ -0,0 +1,384 @@
<script>
import {
tables,
datasources,
userSelectedResourceMap,
contextMenuStore,
appStore,
} from "stores/builder"
import IntegrationIcon from "components/backend/DatasourceNavigator/IntegrationIcon.svelte"
import { Icon, ActionButton, ActionMenu, MenuItem } from "@budibase/bbui"
import { params, url } from "@roxi/routify"
import EditViewModal from "./EditViewModal.svelte"
import DeleteViewModal from "./DeleteViewModal.svelte"
import EditTableModal from "components/backend/TableNavigator/TableNavItem/EditModal.svelte"
import DeleteTableModal from "components/backend/TableNavigator/TableNavItem/DeleteConfirmationModal.svelte"
import { UserAvatars } from "@budibase/frontend-core"
import { DB_TYPE_EXTERNAL } from "constants/backend"
import { TableNames } from "constants"
import { alphabetical } from "components/backend/TableNavigator/utils"
import { tick, onDestroy } from "svelte"
import { derived } from "svelte/store"
import CreateViewButton from "./CreateViewButton.svelte"
// View overflow
let observer
let viewContainer
let viewVisibiltyMap = {}
let overflowMenu
// Editing table
let editTableModal
let deleteTableModal
// Editing views
let editableView
let editViewModal
let deleteViewModal
$: tableId = $params.tableId
$: table = $tables.list.find(table => table._id === tableId)
$: datasource = $datasources.list.find(ds => ds._id === table?.sourceId)
$: tableSelectedBy = $userSelectedResourceMap[table?._id]
$: tableEditable = table?._id !== TableNames.USERS
$: activeId = decodeURIComponent(
$params.viewName ?? $params.viewId ?? $params.tableId
)
$: views = Object.values(table?.views || {})
.filter(x => x.version === 2)
.slice()
.sort(alphabetical)
$: v1Views = Object.values(table?.views || {})
.filter(x => x.version !== 2)
.slice()
.sort(alphabetical)
$: setUpObserver(views)
$: hasViews = v1Views.length || views.length
$: overflowedViews = views.filter(view => !viewVisibiltyMap[view.id])
$: viewHidden = viewVisibiltyMap[activeId] === false
const viewUrl = derived([url, params], ([$url, $params]) => viewId => {
return $url(`../${$params.tableId}/${encodeURIComponent(viewId)}`)
})
const viewV1Url = derived([url, params], ([$url, $params]) => viewName => {
return $url(`../${$params.tableId}/v1/${encodeURIComponent(viewName)}`)
})
const tableUrl = derived(url, $url => tableId => $url(`../${tableId}`))
const openTableContextMenu = e => {
if (!tableEditable) {
return
}
e.preventDefault()
e.stopPropagation()
contextMenuStore.open(
table._id,
[
{
icon: "Edit",
name: "Edit",
keyBind: null,
visible: table?.sourceType !== DB_TYPE_EXTERNAL,
disabled: false,
callback: editTableModal?.show,
},
{
icon: "Delete",
name: "Delete",
keyBind: null,
visible: true,
disabled: false,
callback: deleteTableModal?.show,
},
],
{
x: e.clientX,
y: e.clientY,
}
)
}
const openViewContextMenu = async (e, view) => {
e.preventDefault()
e.stopPropagation()
editableView = view
await tick()
contextMenuStore.open(
view.id,
[
{
icon: "Edit",
name: "Edit",
keyBind: null,
visible: true,
disabled: false,
callback: editViewModal?.show,
},
{
icon: "Delete",
name: "Delete",
keyBind: null,
visible: true,
disabled: false,
callback: deleteViewModal?.show,
},
],
{
x: e.clientX,
y: e.clientY,
}
)
}
const editOverflowView = async view => {
editableView = view
await tick()
editViewModal?.show()
}
const deleteOverflowView = async view => {
editableView = view
await tick()
deleteViewModal?.show()
}
const setUpObserver = async views => {
observer?.disconnect()
if (!views.length) {
return
}
await tick()
observer = new IntersectionObserver(
entries => {
let updates = {}
for (let entry of entries) {
updates[entry.target.dataset.id] = entry.isIntersecting
}
viewVisibiltyMap = {
...viewVisibiltyMap,
...updates,
}
},
{
threshold: 1,
root: viewContainer,
}
)
for (let child of viewContainer.children) {
if (child.dataset.id) {
observer.observe(child)
}
}
}
onDestroy(() => {
observer?.disconnect()
})
</script>
<div class="nav">
<a
href={`/builder/app/${$appStore.appId}/data/datasource/${datasource?._id}`}
>
<IntegrationIcon
integrationType={datasource?.source}
schema={datasource?.schema}
size="24"
/>
</a>
<a
href={$tableUrl(tableId)}
class="nav-item"
class:active={tableId === activeId}
on:contextmenu={openTableContextMenu}
>
<div class="nav-item__title">
{table?._id === TableNames.USERS ? "App users" : table?.name || ""}
</div>
{#if tableSelectedBy}
<UserAvatars size="XS" users={tableSelectedBy} />
{/if}
{#if tableEditable}
<Icon
on:click={openTableContextMenu}
hoverable
name="MoreSmallList"
color="var(--spectrum-global-color-gray-600)"
hoverColor="var(--spectrum-global-color-gray-900)"
/>
{/if}
</a>
{#if hasViews}
<div class="nav__views" bind:this={viewContainer}>
{#each v1Views as view (view.name)}
{@const selectedBy = $userSelectedResourceMap[view.name]}
<a
href={$viewV1Url(view.name)}
class="nav-item"
class:active={view.name === activeId}
on:contextmenu={e => openViewContextMenu(e, view)}
data-id={view.name}
>
<div class="nav-item__title">
{view.name}
</div>
{#if selectedBy}
<UserAvatars size="XS" users={selectedBy} />
{/if}
<Icon
on:click={e => openViewContextMenu(e, view)}
hoverable
name="MoreSmallList"
color="var(--spectrum-global-color-gray-600)"
hoverColor="var(--spectrum-global-color-gray-900)"
/>
</a>
{/each}
{#each views as view (view.id)}
{@const selectedBy = $userSelectedResourceMap[view.id]}
<a
href={$viewUrl(view.id)}
class="nav-item"
class:active={view.id === activeId}
class:hidden={!viewVisibiltyMap[view.id]}
on:contextmenu={e => openViewContextMenu(e, view)}
data-id={view.id}
>
<div class="nav-item__title">
{view.name}
</div>
{#if selectedBy}
<UserAvatars size="XS" users={selectedBy} />
{/if}
<Icon
on:click={e => openViewContextMenu(e, view)}
hoverable
name="MoreSmallList"
color="var(--spectrum-global-color-gray-600)"
hoverColor="var(--spectrum-global-color-gray-900)"
/>
</a>
{/each}
</div>
{/if}
{#if !hasViews && tableEditable}
<CreateViewButton firstView {table} />
<span>
To create subsets of data, control access and more, create a view.
</span>
{/if}
{#if overflowedViews.length}
<ActionMenu align="right" bind:this={overflowMenu}>
<div slot="control" let:open>
<ActionButton icon="ChevronDown" quiet selected={open || viewHidden}>
{overflowedViews.length} more
</ActionButton>
</div>
{#each overflowedViews as view}
<ActionMenu
align="left-context-menu"
openOnHover
animate={false}
offset={-4}
>
<div slot="control">
<a
href={$viewUrl(view.id)}
class="nav-overflow-item"
class:active={view.id === activeId}
on:click={overflowMenu?.hide}
>
<MenuItem icon={viewHidden ? "Checkmark" : null}>
{view.name}
<Icon slot="right" name="ChevronRight" />
</MenuItem>
</a>
</div>
<MenuItem icon="Edit" on:click={() => editOverflowView(view)}>
Edit
</MenuItem>
<MenuItem icon="Delete" on:click={() => deleteOverflowView(view)}>
Delete
</MenuItem>
</ActionMenu>
{/each}
</ActionMenu>
{/if}
{#if hasViews}
<CreateViewButton firstView={false} {table} />
{/if}
</div>
{#if table && tableEditable}
<EditTableModal {table} bind:this={editTableModal} />
<DeleteTableModal {table} bind:this={deleteTableModal} />
{/if}
{#if editableView}
<EditViewModal view={editableView} bind:this={editViewModal} />
<DeleteViewModal view={editableView} bind:this={deleteViewModal} />
{/if}
<style>
/* Main containers */
.nav {
height: 50px;
border-bottom: var(--border-light);
display: flex;
flex-direction: row;
justify-content: flex-start;
align-items: center;
padding: 0 var(--spacing-xl);
gap: 8px;
background: var(--background);
}
.nav__views {
flex: 0 1 auto;
display: flex;
flex-direction: row;
justify-content: flex-start;
align-items: center;
overflow: hidden;
gap: 8px;
}
/* Table and view items */
.nav-item {
padding: 0 8px;
height: 32px;
border-radius: 4px;
display: flex;
flex-direction: row;
justify-content: flex-start;
align-items: center;
gap: var(--spacing-m);
transition: background 130ms ease-out, color 130ms ease-out;
color: var(--spectrum-global-color-gray-600);
}
.nav-item.hidden {
visibility: hidden;
}
.nav-item.active,
.nav-item:hover {
background: var(--spectrum-global-color-gray-300);
cursor: pointer;
color: var(--spectrum-global-color-gray-900);
}
.nav-item:not(.active) :global(.icon) {
display: none;
}
.nav-item__title {
max-width: 150px;
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
}
/* OVerflow items */
.nav-overflow-item:not(.active) :global(> .spectrum-Menu-item > .icon) {
visibility: hidden;
}
</style>

View File

@ -3,6 +3,7 @@
import { tables, builderStore } from "stores/builder" import { tables, builderStore } from "stores/builder"
import * as routify from "@roxi/routify" import * as routify from "@roxi/routify"
import { onDestroy } from "svelte" import { onDestroy } from "svelte"
import ViewNavBar from "./_components/ViewNavBar.svelte"
$: tableId = $tables.selectedTableId $: tableId = $tables.selectedTableId
$: builderStore.selectResource(tableId) $: builderStore.selectResource(tableId)
@ -20,4 +21,17 @@
onDestroy(stopSyncing) onDestroy(stopSyncing)
</script> </script>
<slot /> <div class="wrapper">
<ViewNavBar />
<slot />
</div>
<style>
.wrapper {
flex: 1 1 auto;
margin: -28px -40px -40px -40px;
display: flex;
flex-direction: column;
background: var(--spectrum-global-color-gray-50);
}
</style>

View File

@ -1,7 +1,92 @@
<script> <script>
import TableDataTable from "components/backend/DataTable/TableDataTable.svelte" import { Banner, notifications } from "@budibase/bbui"
import { tables } from "stores/builder" import {
import { Banner } from "@budibase/bbui" datasources,
tables,
integrations,
appStore,
rowActions,
} from "stores/builder"
import { themeStore, admin, licensing } from "stores/portal"
import { TableNames } from "constants"
import { Grid } from "@budibase/frontend-core"
import { API } from "api"
import GridAddColumnModal from "components/backend/DataTable/modals/grid/GridCreateColumnModal.svelte"
import GridCreateEditRowModal from "components/backend/DataTable/modals/grid/GridCreateEditRowModal.svelte"
import GridEditUserModal from "components/backend/DataTable/modals/grid/GridEditUserModal.svelte"
import GridImportButton from "components/backend/DataTable/buttons/grid/GridImportButton.svelte"
import GridExportButton from "components/backend/DataTable/buttons/grid/GridExportButton.svelte"
import GridManageAccessButton from "components/backend/DataTable/buttons/grid/GridManageAccessButton.svelte"
import GridRelationshipButton from "components/backend/DataTable/buttons/grid/GridRelationshipButton.svelte"
import GridEditColumnModal from "components/backend/DataTable/modals/grid/GridEditColumnModal.svelte"
import GridUsersTableButton from "components/backend/DataTable/buttons/grid/GridUsersTableButton.svelte"
import GridGenerateButton from "components/backend/DataTable/buttons/grid/GridGenerateButton.svelte"
import GridScreensButton from "components/backend/DataTable/buttons/grid/GridScreensButton.svelte"
import GridAutomationsButton from "components/backend/DataTable/buttons/grid/GridAutomationsButton.svelte"
import GridRowActionsButton from "components/backend/DataTable/buttons/grid/GridRowActionsButton.svelte"
import { DB_TYPE_EXTERNAL } from "constants/backend"
const userSchemaOverrides = {
firstName: { displayName: "First name", disabled: true },
lastName: { displayName: "Last name", disabled: true },
email: { displayName: "Email", disabled: true },
roleId: { displayName: "Role", disabled: true },
status: { displayName: "Status", disabled: true },
}
let generateButton
$: autoColumnStatus = verifyAutocolumns($tables?.selected)
$: duplicates = Object.values(autoColumnStatus).reduce((acc, status) => {
if (status.length > 1) {
acc = [...acc, ...status]
}
return acc
}, [])
$: invalidColumnText = duplicates.map(entry => {
return `${entry.name} (${entry.subtype})`
})
$: id = $tables.selected?._id
$: isUsersTable = id === TableNames.USERS
$: isInternal = $tables.selected?.sourceType !== DB_TYPE_EXTERNAL
$: gridDatasource = {
type: "table",
tableId: id,
}
$: tableDatasource = $datasources.list.find(datasource => {
return datasource._id === $tables.selected?.sourceId
})
$: relationshipsEnabled = relationshipSupport(tableDatasource)
$: currentTheme = $themeStore?.theme
$: darkMode = !currentTheme.includes("light")
$: buttons = makeRowActionButtons($rowActions[id])
$: rowActions.refreshRowActions(id)
const makeRowActionButtons = actions => {
return (actions || [])
.filter(action => action.allowedSources?.includes(id))
.map(action => ({
text: action.name,
onClick: async row => {
await rowActions.trigger(id, action.id, row._id)
notifications.success("Row action triggered successfully")
},
}))
}
const relationshipSupport = datasource => {
const integration = $integrations[datasource?.source]
return !isInternal && integration?.relationships !== false
}
const handleGridTableUpdate = async e => {
tables.replaceTable(id, e.detail)
// We need to refresh datasources when an external table changes.
if (e.detail?.sourceType === DB_TYPE_EXTERNAL) {
await datasources.fetch()
}
}
const verifyAutocolumns = table => { const verifyAutocolumns = table => {
// Check for duplicates // Check for duplicates
@ -17,17 +102,6 @@
return acc return acc
}, {}) }, {})
} }
$: autoColumnStatus = verifyAutocolumns($tables?.selected)
$: duplicates = Object.values(autoColumnStatus).reduce((acc, status) => {
if (status.length > 1) {
acc = [...acc, ...status]
}
return acc
}, [])
$: invalidColumnText = duplicates.map(entry => {
return `${entry.name} (${entry.subtype})`
})
</script> </script>
{#if $tables?.selected?.name} {#if $tables?.selected?.name}
@ -40,7 +114,63 @@
</Banner> </Banner>
</div> </div>
{/if} {/if}
<TableDataTable /> <Grid
{API}
{darkMode}
datasource={gridDatasource}
canAddRows={!isUsersTable}
canDeleteRows={!isUsersTable}
canEditRows={!isUsersTable || !$appStore.features.disableUserMetadata}
canEditColumns={!isUsersTable || !$appStore.features.disableUserMetadata}
schemaOverrides={isUsersTable ? userSchemaOverrides : null}
showAvatars={false}
isCloud={$admin.cloud}
aiEnabled={$licensing.budibaseAIEnabled ||
$licensing.customAIConfigsEnabled}
{buttons}
buttonsCollapsed
on:updatedatasource={handleGridTableUpdate}
>
<!-- Controls -->
<svelte:fragment slot="controls">
{#if isUsersTable && $appStore.features.disableUserMetadata}
<GridUsersTableButton />
{/if}
<GridManageAccessButton />
{#if relationshipsEnabled}
<GridRelationshipButton />
{/if}
{#if !isUsersTable}
<GridRowActionsButton />
<GridScreensButton on:request-generate={() => generateButton?.show()} />
<GridAutomationsButton
on:request-generate={() => generateButton?.show()}
/>
<GridImportButton />
{/if}
<GridExportButton />
</svelte:fragment>
<svelte:fragment slot="controls-right">
{#if !isUsersTable}
<GridGenerateButton bind:this={generateButton} />
{/if}
</svelte:fragment>
<!-- Content for editing columns -->
<svelte:fragment slot="edit-column">
<GridEditColumnModal />
</svelte:fragment>
<svelte:fragment slot="add-column">
<GridAddColumnModal />
</svelte:fragment>
<!-- Listening to events for editing rows in modals -->
{#if isUsersTable}
<GridEditUserModal />
{:else}
<GridCreateEditRowModal />
{/if}
</Grid>
{:else} {:else}
<i>Create your first table to start building</i> <i>Create your first table to start building</i>
{/if} {/if}

View File

@ -1,10 +0,0 @@
<script>
import { params } from "@roxi/routify"
import RelationshipDataTable from "components/backend/DataTable/RelationshipDataTable.svelte"
</script>
<RelationshipDataTable
tableId={$params.tableId}
rowId={$params.rowId}
fieldName={decodeURI($params.field)}
/>

View File

@ -1,7 +0,0 @@
<script>
import { redirect } from "@roxi/routify"
$redirect("../../")
</script>
<!-- routify:options index=false -->

View File

@ -1,7 +0,0 @@
<script>
import { redirect } from "@roxi/routify"
$redirect("../")
</script>
<!-- routify:options index=false -->

View File

@ -0,0 +1,91 @@
<script>
import { views, tables } from "stores/builder"
import { API } from "api"
import Table from "components/backend/DataTable/Table.svelte"
import CalculateButton from "components/backend/DataTable/buttons/CalculateButton.svelte"
import GroupByButton from "components/backend/DataTable/buttons/GroupByButton.svelte"
import ViewFilterButton from "components/backend/DataTable/buttons/ViewFilterButton.svelte"
import ExportButton from "components/backend/DataTable/buttons/ExportButton.svelte"
import ManageAccessButton from "components/backend/DataTable/buttons/ManageAccessButton.svelte"
import HideAutocolumnButton from "components/backend/DataTable/buttons/HideAutocolumnButton.svelte"
import { notifications } from "@budibase/bbui"
import { ROW_EXPORT_FORMATS } from "constants/backend"
let hideAutocolumns = true
let data = []
let loading = false
$: view = $views.selected
$: name = view?.name
$: schema = view?.schema
$: calculation = view?.calculation
$: supportedFormats = Object.values(ROW_EXPORT_FORMATS).filter(key => {
if (calculation && key === ROW_EXPORT_FORMATS.JSON_WITH_SCHEMA) {
return false
}
return true
})
// Fetch rows for specified view
$: fetchViewData(name, view?.field, view?.groupBy, view?.calculation)
async function fetchViewData(name, field, groupBy, calculation) {
loading = true
const _tables = $tables.list
const allTableViews = _tables.map(table => table.views)
const thisView = allTableViews.filter(
views => views != null && views[name] != null
)[0]
// Don't fetch view data if the view no longer exists
if (!thisView) {
loading = false
return
}
try {
data = await API.fetchViewData({
name,
calculation,
field,
groupBy,
})
} catch (error) {
notifications.error("Error fetching view data")
}
loading = false
}
</script>
<div class="view-v1">
{#if view}
<Table
{schema}
tableId={view.tableId}
{data}
{loading}
rowCount={10}
allowEditing={false}
bind:hideAutocolumns
>
<ViewFilterButton {view} />
<CalculateButton {view} />
{#if view.calculation}
<GroupByButton {view} />
{/if}
<ManageAccessButton resourceId={decodeURI(name)} />
<HideAutocolumnButton bind:hideAutocolumns />
<ExportButton view={view.name} formats={supportedFormats} />
</Table>
{:else}<i>Create your first table to start building</i>{/if}
</div>
<style>
.view-v1 {
padding: 0 var(--spacing-xl);
}
i {
font-size: var(--font-size-m);
color: var(--grey-4);
margin-top: 2px;
}
</style>

View File

@ -1,19 +0,0 @@
<script>
import { onMount } from "svelte"
import { views, viewsV2 } from "stores/builder"
import { redirect } from "@roxi/routify"
onMount(async () => {
if ($viewsV2.selected) {
$redirect(`./v2/${$viewsV2.selected.id}`)
} else if ($viewsV2.list?.length) {
$redirect(`./v2/${$viewsV2.list[0].id}`)
} else if ($views.selected) {
$redirect(`./${encodeURIComponent($views.selected?.name)}`)
} else if ($views.list?.length) {
$redirect(`./${encodeURIComponent($views.list[0].name)}`)
} else {
$redirect("../")
}
})
</script>

View File

@ -1,18 +0,0 @@
<script>
import ViewDataTable from "components/backend/DataTable/ViewDataTable.svelte"
import { views } from "stores/builder"
$: selectedView = $views.selected
</script>
{#if selectedView}
<ViewDataTable view={selectedView} />
{:else}<i>Create your first table to start building</i>{/if}
<style>
i {
font-size: var(--font-size-m);
color: var(--grey-4);
margin-top: 2px;
}
</style>

View File

@ -1,5 +0,0 @@
<script>
import ViewV2DataTable from "components/backend/DataTable/ViewV2DataTable.svelte"
</script>
<ViewV2DataTable />

View File

@ -1,5 +0,0 @@
<script>
import { redirect } from "@roxi/routify"
$redirect("../")
</script>

View File

@ -8,6 +8,7 @@
import InfoDisplay from "./InfoDisplay.svelte" import InfoDisplay from "./InfoDisplay.svelte"
import analytics, { Events } from "analytics" import analytics, { Events } from "analytics"
import { shouldDisplaySetting } from "@budibase/frontend-core" import { shouldDisplaySetting } from "@budibase/frontend-core"
import { getContext, setContext } from "svelte"
export let componentDefinition export let componentDefinition
export let componentInstance export let componentInstance
@ -19,6 +20,16 @@
export let includeHidden = false export let includeHidden = false
export let tag export let tag
// Sometimes we render component settings using a complicated nested
// component instance technique. This results in instances with IDs that
// don't exist anywhere in the tree. Therefore we need to keep track of
// what the real component tree ID is so we can always find it.
const rootId = getContext("rootId")
if (!rootId) {
setContext("rootId", componentInstance._id)
}
$: componentInstance._rootId = rootId || componentInstance._id
$: sections = getSections( $: sections = getSections(
componentInstance, componentInstance,
componentDefinition, componentDefinition,

View File

@ -10,12 +10,16 @@
navigationStore, navigationStore,
permissions as permissionsStore, permissions as permissionsStore,
builderStore, builderStore,
datasources,
appStore,
} from "stores/builder" } from "stores/builder"
import { auth } from "stores/portal" import { auth } from "stores/portal"
import { goto } from "@roxi/routify" import { goto } from "@roxi/routify"
import { TOUR_KEYS } from "components/portal/onboarding/tours.js" import { TOUR_KEYS } from "components/portal/onboarding/tours.js"
import * as screenTemplating from "templates/screenTemplating" import * as screenTemplating from "templates/screenTemplating"
import { Roles } from "constants/backend" import { Roles } from "constants/backend"
import { AutoScreenTypes } from "constants"
import { makeTableOption, makeViewOption } from "./utils"
let mode let mode
@ -23,20 +27,33 @@
let datasourceModal let datasourceModal
let formTypeModal let formTypeModal
let tableTypeModal let tableTypeModal
let selectedTablesAndViews = [] let selectedTablesAndViews = []
let permissions = {} let permissions = {}
let hasPreselectedDatasource = false
$: screens = $screenStore.screens $: screens = $screenStore.screens
export const show = newMode => { export const show = (newMode, preselectedDatasource) => {
mode = newMode mode = newMode
selectedTablesAndViews = [] selectedTablesAndViews = []
permissions = {} permissions = {}
hasPreselectedDatasource = preselectedDatasource != null
if (mode === "table" || mode === "form") { if (mode === AutoScreenTypes.TABLE || mode === AutoScreenTypes.FORM) {
if (preselectedDatasource) {
// If preselecting a datasource, skip a step
const isTable = preselectedDatasource.type === "table"
const tableOrView = isTable
? makeTableOption(preselectedDatasource, $datasources.list)
: makeViewOption(preselectedDatasource)
fetchPermission(tableOrView.id)
selectedTablesAndViews.push(tableOrView)
onSelectDatasources()
} else {
// Otherwise choose a datasource
datasourceModal.show() datasourceModal.show()
} else if (mode === "blank") { }
} else if (mode === AutoScreenTypes.BLANK) {
screenDetailsModal.show() screenDetailsModal.show()
} else { } else {
throw new Error("Invalid mode provided") throw new Error("Invalid mode provided")
@ -77,22 +94,23 @@
} }
const onSelectDatasources = async () => { const onSelectDatasources = async () => {
if (mode === "form") { if (mode === AutoScreenTypes.FORM) {
formTypeModal.show() formTypeModal.show()
} else if (mode === "table") { } else if (mode === AutoScreenTypes.TABLE) {
tableTypeModal.show() tableTypeModal.show()
} }
} }
const createBlankScreen = async ({ route }) => { const createBlankScreen = async ({ route }) => {
const screenTemplates = screenTemplating.blank({ route, screens }) const screenTemplates = screenTemplating.blank({ route, screens })
const newScreens = await createScreens(screenTemplates) const newScreens = await createScreens(screenTemplates)
loadNewScreen(newScreens[0]) loadNewScreen(newScreens[0])
} }
const createTableScreen = async type => { const createTableScreen = async type => {
const screenTemplates = selectedTablesAndViews.flatMap(tableOrView => const screenTemplates = (
await Promise.all(
selectedTablesAndViews.map(tableOrView =>
screenTemplating.table({ screenTemplating.table({
screens, screens,
tableOrView, tableOrView,
@ -100,13 +118,16 @@
permissions: permissions[tableOrView.id], permissions: permissions[tableOrView.id],
}) })
) )
)
).flat()
const newScreens = await createScreens(screenTemplates) const newScreens = await createScreens(screenTemplates)
loadNewScreen(newScreens[0]) loadNewScreen(newScreens[0])
} }
const createFormScreen = async type => { const createFormScreen = async type => {
const screenTemplates = selectedTablesAndViews.flatMap(tableOrView => const screenTemplates = (
await Promise.all(
selectedTablesAndViews.map(tableOrView =>
screenTemplating.form({ screenTemplating.form({
screens, screens,
tableOrView, tableOrView,
@ -114,7 +135,8 @@
permissions: permissions[tableOrView.id], permissions: permissions[tableOrView.id],
}) })
) )
)
).flat()
const newScreens = await createScreens(screenTemplates) const newScreens = await createScreens(screenTemplates)
if (type === "update" || type === "create") { if (type === "update" || type === "create") {
@ -136,9 +158,11 @@
if (screen?.props?._children.length) { if (screen?.props?._children.length) {
// Focus on the main component for the screen type // Focus on the main component for the screen type
const mainComponent = screen?.props?._children?.[0]._id const mainComponent = screen?.props?._children?.[0]._id
$goto(`./${screen._id}/${mainComponent}`) $goto(
`/builder/app/${$appStore.appId}/design/${screen._id}/${mainComponent}`
)
} else { } else {
$goto(`./${screen._id}`) $goto(`/builder/app/${$appStore.appId}/design/${screen._id}`)
} }
screenStore.select(screen._id) screenStore.select(screen._id)
@ -214,6 +238,7 @@
tableTypeModal.hide() tableTypeModal.hide()
datasourceModal.show() datasourceModal.show()
}} }}
showCancelButton={!hasPreselectedDatasource}
/> />
</Modal> </Modal>
@ -230,5 +255,6 @@
formTypeModal.hide() formTypeModal.hide()
datasourceModal.show() datasourceModal.show()
}} }}
showCancelButton={!hasPreselectedDatasource}
/> />
</Modal> </Modal>

View File

@ -1,11 +1,11 @@
<script> <script>
import { Body, ModalContent, Layout, notifications } from "@budibase/bbui" import { Body, ModalContent, Layout } from "@budibase/bbui"
import { datasources as datasourcesStore } from "stores/builder" import { datasources as datasourcesStore } from "stores/builder"
import ICONS from "components/backend/DatasourceNavigator/icons" import ICONS from "components/backend/DatasourceNavigator/icons"
import { IntegrationNames } from "constants" import { IntegrationNames } from "constants"
import { createEventDispatcher, onMount } from "svelte" import { createEventDispatcher } from "svelte"
import TableOrViewOption from "./TableOrViewOption.svelte" import TableOrViewOption from "./TableOrViewOption.svelte"
import * as format from "helpers/data/format" import { makeTableOption, makeViewOption } from "./utils"
export let onConfirm export let onConfirm
export let selectedTablesAndViews export let selectedTablesAndViews
@ -16,17 +16,10 @@
const views = Object.values(table.views || {}).filter( const views = Object.values(table.views || {}).filter(
view => view.version === 2 view => view.version === 2
) )
return views.map(makeViewOption)
return views.map(view => ({
icon: "Remove",
name: view.name,
id: view.id,
tableSelectFormat: format.tableSelect.viewV2(view),
datasourceSelectFormat: format.datasourceSelect.viewV2(view),
}))
} }
const getTablesAndViews = datasource => { const getTablesAndViews = (datasource, datasources) => {
let tablesAndViews = [] let tablesAndViews = []
const tables = Array.isArray(datasource.entities) const tables = Array.isArray(datasource.entities)
? datasource.entities ? datasource.entities
@ -37,16 +30,7 @@
continue continue
} }
const formattedTable = { const formattedTable = makeTableOption(table, datasources)
icon: "Table",
name: table.name,
id: table._id,
tableSelectFormat: format.tableSelect.table(table),
datasourceSelectFormat: format.datasourceSelect.table(
table,
$datasourcesStore.list
),
}
tablesAndViews = tablesAndViews.concat([ tablesAndViews = tablesAndViews.concat([
formattedTable, formattedTable,
@ -71,7 +55,7 @@
const datasource = { const datasource = {
name: rawDatasource.name, name: rawDatasource.name,
iconComponent: ICONS[rawDatasource.source], iconComponent: ICONS[rawDatasource.source],
tablesAndViews: getTablesAndViews(rawDatasource), tablesAndViews: getTablesAndViews(rawDatasource, rawDatasources),
} }
datasources.push(datasource) datasources.push(datasource)
@ -85,14 +69,6 @@
const toggleSelection = tableOrView => { const toggleSelection = tableOrView => {
dispatch("toggle", tableOrView) dispatch("toggle", tableOrView)
} }
onMount(async () => {
try {
await datasourcesStore.fetch()
} catch (error) {
notifications.error("Error fetching datasources")
}
})
</script> </script>
<ModalContent <ModalContent

View File

@ -7,6 +7,7 @@
export let types export let types
export let onCancel = () => {} export let onCancel = () => {}
export let onConfirm = () => {} export let onConfirm = () => {}
export let showCancelButton = true
</script> </script>
<ModalContent <ModalContent
@ -17,6 +18,7 @@
{onCancel} {onCancel}
disabled={!selectedType} disabled={!selectedType}
size="L" size="L"
{showCancelButton}
> >
<!-- svelte-ignore a11y-click-events-have-key-events --> <!-- svelte-ignore a11y-click-events-have-key-events -->
<!-- svelte-ignore a11y-no-static-element-interactions --> <!-- svelte-ignore a11y-no-static-element-interactions -->
@ -65,9 +67,9 @@
box-sizing: border-box; box-sizing: border-box;
padding: var(--spacing-m) var(--spacing-xl); padding: var(--spacing-m) var(--spacing-xl);
flex-grow: 1; flex-grow: 1;
gap: var(--spacing-s);
display: flex; display: flex;
flex-direction: column; flex-direction: column;
justify-content: center;
} }
.image { .image {

View File

@ -0,0 +1,17 @@
import * as format from "helpers/data/format"
export const makeViewOption = view => ({
icon: "Remove",
name: view.name,
id: view.id,
tableSelectFormat: format.tableSelect.viewV2(view),
datasourceSelectFormat: format.datasourceSelect.viewV2(view),
})
export const makeTableOption = (table, datasources) => ({
icon: "Table",
name: table.name,
id: table._id,
tableSelectFormat: format.tableSelect.table(table),
datasourceSelectFormat: format.datasourceSelect.table(table, datasources),
})

View File

@ -2,9 +2,7 @@ import { writable } from "svelte/store"
export default class BudiStore { export default class BudiStore {
constructor(init, opts) { constructor(init, opts) {
const store = writable({ const store = writable({ ...init })
...init,
})
/** /**
* Internal Svelte store * Internal Svelte store
@ -23,6 +21,7 @@ export default class BudiStore {
* *Store modification should be kept to a minimum * *Store modification should be kept to a minimum
*/ */
this.update = this.store.update this.update = this.store.update
this.set = this.store.set
/** /**
* Optional debug mode to output the store updates to console * Optional debug mode to output the store updates to console

View File

@ -6,6 +6,8 @@ import { createHistoryStore } from "stores/builder/history"
import { notifications } from "@budibase/bbui" import { notifications } from "@budibase/bbui"
import { updateReferencesInObject } from "dataBinding" import { updateReferencesInObject } from "dataBinding"
import { AutomationTriggerStepId } from "@budibase/types" import { AutomationTriggerStepId } from "@budibase/types"
import { sdk } from "@budibase/shared-core"
import { rowActions } from "./rowActions"
import { import {
updateBindingsInSteps, updateBindingsInSteps,
getNewStepName, getNewStepName,
@ -127,10 +129,18 @@ const automationActions = store => ({
return response.automation return response.automation
}, },
delete: async automation => { delete: async automation => {
const isRowAction = sdk.automations.isRowAction(automation)
if (isRowAction) {
await rowActions.delete(
automation.definition.trigger.inputs.tableId,
automation.definition.trigger.inputs.rowActionId
)
} else {
await API.deleteAutomation({ await API.deleteAutomation({
automationId: automation?._id, automationId: automation?._id,
automationRev: automation?._rev, automationRev: automation?._rev,
}) })
}
store.update(state => { store.update(state => {
// Remove the automation // Remove the automation

View File

@ -31,6 +31,7 @@ import {
import BudiStore from "../BudiStore" import BudiStore from "../BudiStore"
import { Utils } from "@budibase/frontend-core" import { Utils } from "@budibase/frontend-core"
import { FieldType } from "@budibase/types" import { FieldType } from "@budibase/types"
import { utils } from "@budibase/shared-core"
export const INITIAL_COMPONENTS_STATE = { export const INITIAL_COMPONENTS_STATE = {
components: {}, components: {},
@ -196,6 +197,25 @@ export class ComponentStore extends BudiStore {
} }
} }
if (!enrichedComponent?._component) {
return migrated
}
const def = this.getDefinition(enrichedComponent?._component)
const filterableTypes = def?.settings?.filter(setting =>
setting?.type?.startsWith("filter")
)
for (let setting of filterableTypes || []) {
const isLegacy = Array.isArray(enrichedComponent[setting.key])
if (isLegacy) {
const processedSetting = utils.processSearchFilters(
enrichedComponent[setting.key]
)
enrichedComponent[setting.key] = processedSetting
migrated = true
}
}
return migrated return migrated
} }
@ -405,7 +425,13 @@ export class ComponentStore extends BudiStore {
screen: get(selectedScreen), screen: get(selectedScreen),
useDefaultValues: true, useDefaultValues: true,
}) })
try {
this.migrateSettings(instance) this.migrateSettings(instance)
} catch (e) {
console.error(e)
throw e
}
// Custom post processing for creation only // Custom post processing for creation only
let extras = {} let extras = {}

View File

@ -29,6 +29,7 @@ import { integrations } from "./integrations"
import { sortedIntegrations } from "./sortedIntegrations" import { sortedIntegrations } from "./sortedIntegrations"
import { queries } from "./queries" import { queries } from "./queries"
import { flags } from "./flags" import { flags } from "./flags"
import { rowActions } from "./rowActions"
import componentTreeNodesStore from "./componentTreeNodes" import componentTreeNodesStore from "./componentTreeNodes"
export { export {
@ -65,6 +66,7 @@ export {
flags, flags,
hoverStore, hoverStore,
snippets, snippets,
rowActions,
} }
export const reset = () => { export const reset = () => {
@ -74,6 +76,7 @@ export const reset = () => {
componentStore.reset() componentStore.reset()
layoutStore.reset() layoutStore.reset()
navigationStore.reset() navigationStore.reset()
rowActions.reset()
} }
const refreshBuilderData = async () => { const refreshBuilderData = async () => {

View File

@ -0,0 +1,151 @@
import { get, derived } from "svelte/store"
import BudiStore from "stores/BudiStore"
import { tables } from "./tables"
import { viewsV2 } from "./viewsV2"
import { automationStore } from "./automations"
import { API } from "api"
import { getSequentialName } from "helpers/duplicate"
const initialState = {}
export class RowActionStore extends BudiStore {
constructor() {
super(initialState)
}
reset = () => {
this.store.set(initialState)
}
refreshRowActions = async sourceId => {
if (!sourceId) {
return
}
// Get the underlying table ID for this source ID
let tableId = get(tables).list.find(table => table._id === sourceId)?._id
if (!tableId) {
const view = get(viewsV2).list.find(view => view.id === sourceId)
tableId = view?.tableId
}
if (!tableId) {
return
}
// Fetch row actions for this table
const res = await API.rowActions.fetch(tableId)
const actions = Object.values(res || {})
this.update(state => ({
...state,
[tableId]: actions,
}))
}
createRowAction = async (tableId, viewId, name) => {
if (!tableId) {
return
}
// Get a unique name for this action
if (!name) {
const existingRowActions = get(this.store)[tableId] || []
name = getSequentialName(existingRowActions, "New row action ", {
getName: x => x.name,
})
}
// Create the action
const res = await API.rowActions.create({
name,
tableId,
})
// Enable action on this view if adding via a view
if (viewId) {
await Promise.all([
this.enableView(tableId, viewId, res.id),
automationStore.actions.fetch(),
])
} else {
await Promise.all([
this.refreshRowActions(tableId),
automationStore.actions.fetch(),
])
}
return res
}
enableView = async (tableId, viewId, rowActionId) => {
await API.rowActions.enableView({
tableId,
viewId,
rowActionId,
})
await this.refreshRowActions(tableId)
}
disableView = async (tableId, viewId, rowActionId) => {
await API.rowActions.disableView({
tableId,
viewId,
rowActionId,
})
await this.refreshRowActions(tableId)
}
rename = async (tableId, rowActionId, name) => {
await API.rowActions.update({
tableId,
rowActionId,
name,
})
await this.refreshRowActions(tableId)
automationStore.actions.fetch()
}
delete = async (tableId, rowActionId) => {
await API.rowActions.delete({
tableId,
rowActionId,
})
await this.refreshRowActions(tableId)
// We don't need to refresh automations as we can only delete row actions
// from the automations store, so we already handle the state update there
}
trigger = async (sourceId, rowActionId, rowId) => {
await API.rowActions.trigger({
sourceId,
rowActionId,
rowId,
})
}
}
const store = new RowActionStore()
const derivedStore = derived(store, $store => {
let map = {}
// Generate an entry for every view as well
Object.keys($store || {}).forEach(tableId => {
// We need to have all the actions for the table in order to be displayed in the crud section
map[tableId] = $store[tableId]
for (let action of $store[tableId]) {
const otherSources = (action.allowedSources || []).filter(
sourceId => sourceId !== tableId
)
for (let source of otherSources) {
map[source] ??= []
map[source].push(action)
}
}
})
return map
})
export const rowActions = {
...store,
subscribe: derivedStore.subscribe,
}

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