Merge v3-ui.
This commit is contained in:
commit
da2b2e5c85
|
@ -28,6 +28,7 @@ export enum Config {
|
|||
OIDC = "oidc",
|
||||
OIDC_LOGOS = "logos_oidc",
|
||||
SCIM = "scim",
|
||||
AI = "AI",
|
||||
}
|
||||
|
||||
export const MIN_VALID_DATE = new Date(-2147483647000)
|
||||
|
|
|
@ -1351,7 +1351,8 @@ class InternalBuilder {
|
|||
schema.constraints?.presence === true ||
|
||||
schema.type === FieldType.FORMULA ||
|
||||
schema.type === FieldType.AUTO ||
|
||||
schema.type === FieldType.LINK
|
||||
schema.type === FieldType.LINK ||
|
||||
schema.type === FieldType.AI
|
||||
) {
|
||||
continue
|
||||
}
|
||||
|
|
|
@ -102,6 +102,14 @@ export const useAppBuilders = () => {
|
|||
return useFeature(Feature.APP_BUILDERS)
|
||||
}
|
||||
|
||||
export const useBudibaseAI = () => {
|
||||
return useFeature(Feature.BUDIBASE_AI)
|
||||
}
|
||||
|
||||
export const useAICustomConfigs = () => {
|
||||
return useFeature(Feature.AI_CUSTOM_CONFIGS)
|
||||
}
|
||||
|
||||
// QUOTAS
|
||||
|
||||
export const setAutomationLogsQuota = (value: number) => {
|
||||
|
|
|
@ -1,15 +1,11 @@
|
|||
<script>
|
||||
import "@spectrum-css/actionbutton/dist/index-vars.css"
|
||||
import { createEventDispatcher } from "svelte"
|
||||
import Tooltip from "../Tooltip/Tooltip.svelte"
|
||||
import { fade } from "svelte/transition"
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
import { hexToRGBA } from "../helpers"
|
||||
|
||||
export let quiet = false
|
||||
export let emphasized = false
|
||||
export let selected = false
|
||||
export let longPressable = false
|
||||
export let disabled = false
|
||||
export let icon = ""
|
||||
export let size = "M"
|
||||
|
@ -17,82 +13,64 @@
|
|||
export let fullWidth = false
|
||||
export let noPadding = false
|
||||
export let tooltip = ""
|
||||
export let accentColor = null
|
||||
|
||||
let showTooltip = false
|
||||
|
||||
function longPress(element) {
|
||||
if (!longPressable) return
|
||||
let timer
|
||||
$: accentStyle = getAccentStyle(accentColor)
|
||||
|
||||
const listener = () => {
|
||||
timer = setTimeout(() => {
|
||||
dispatch("longpress")
|
||||
}, 700)
|
||||
}
|
||||
|
||||
element.addEventListener("pointerdown", listener)
|
||||
|
||||
return {
|
||||
destroy() {
|
||||
clearTimeout(timer)
|
||||
element.removeEventListener("pointerdown", longPress)
|
||||
},
|
||||
const getAccentStyle = color => {
|
||||
if (!color) {
|
||||
return ""
|
||||
}
|
||||
let style = ""
|
||||
style += `--accent-bg-color:${hexToRGBA(color, 0.15)};`
|
||||
style += `--accent-border-color:${hexToRGBA(color, 0.35)};`
|
||||
return style
|
||||
}
|
||||
</script>
|
||||
|
||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||
<span
|
||||
class="btn-wrap"
|
||||
<button
|
||||
class="spectrum-ActionButton spectrum-ActionButton--size{size}"
|
||||
class:spectrum-ActionButton--quiet={quiet}
|
||||
class:is-selected={selected}
|
||||
class:noPadding
|
||||
class:fullWidth
|
||||
class:active
|
||||
class:disabled
|
||||
class:accent={accentColor != null}
|
||||
on:click|preventDefault
|
||||
on:mouseover={() => (showTooltip = true)}
|
||||
on:mouseleave={() => (showTooltip = false)}
|
||||
on:focus={() => (showTooltip = true)}
|
||||
{disabled}
|
||||
style={accentStyle}
|
||||
>
|
||||
<button
|
||||
use:longPress
|
||||
class:spectrum-ActionButton--quiet={quiet}
|
||||
class:spectrum-ActionButton--emphasized={emphasized}
|
||||
class:is-selected={selected}
|
||||
class:noPadding
|
||||
class:fullWidth
|
||||
class="spectrum-ActionButton spectrum-ActionButton--size{size}"
|
||||
class:active
|
||||
class:disabled
|
||||
{disabled}
|
||||
on:longPress
|
||||
on:click|preventDefault
|
||||
>
|
||||
{#if longPressable}
|
||||
<svg
|
||||
class="spectrum-Icon spectrum-UIIcon-CornerTriangle100 spectrum-ActionButton-hold"
|
||||
focusable="false"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<use xlink:href="#spectrum-css-icon-CornerTriangle100" />
|
||||
</svg>
|
||||
{/if}
|
||||
{#if icon}
|
||||
<svg
|
||||
class="spectrum-Icon spectrum-Icon--sizeS"
|
||||
focusable="false"
|
||||
aria-hidden="true"
|
||||
aria-label={icon}
|
||||
>
|
||||
<use xlink:href="#spectrum-icon-18-{icon}" />
|
||||
</svg>
|
||||
{/if}
|
||||
{#if $$slots}
|
||||
<span class="spectrum-ActionButton-label"><slot /></span>
|
||||
{/if}
|
||||
{#if tooltip && showTooltip}
|
||||
<div class="tooltip" in:fade={{ duration: 130, delay: 250 }}>
|
||||
<Tooltip textWrapping direction="bottom" text={tooltip} />
|
||||
</div>
|
||||
{/if}
|
||||
</button>
|
||||
</span>
|
||||
{#if icon}
|
||||
<svg
|
||||
class="spectrum-Icon spectrum-Icon--sizeS"
|
||||
focusable="false"
|
||||
aria-hidden="true"
|
||||
aria-label={icon}
|
||||
>
|
||||
<use xlink:href="#spectrum-icon-18-{icon}" />
|
||||
</svg>
|
||||
{/if}
|
||||
{#if $$slots}
|
||||
<span class="spectrum-ActionButton-label"><slot /></span>
|
||||
{/if}
|
||||
{#if tooltip && showTooltip}
|
||||
<div class="tooltip" in:fade={{ duration: 130, delay: 250 }}>
|
||||
<Tooltip textWrapping direction="bottom" text={tooltip} />
|
||||
</div>
|
||||
{/if}
|
||||
</button>
|
||||
|
||||
<style>
|
||||
button {
|
||||
transition: filter 130ms ease-out, background 130ms ease-out,
|
||||
border 130ms ease-out, color 130ms ease-out;
|
||||
}
|
||||
.fullWidth {
|
||||
width: 100%;
|
||||
}
|
||||
|
@ -104,9 +82,7 @@
|
|||
margin-left: 0;
|
||||
transition: color ease-out 130ms;
|
||||
}
|
||||
.is-selected:not(.spectrum-ActionButton--emphasized):not(
|
||||
.spectrum-ActionButton--quiet
|
||||
) {
|
||||
.is-selected:not(.spectrum-ActionButton--quiet) {
|
||||
background: var(--spectrum-global-color-gray-300);
|
||||
border-color: var(--spectrum-global-color-gray-500);
|
||||
}
|
||||
|
@ -115,12 +91,13 @@
|
|||
}
|
||||
.spectrum-ActionButton--quiet.is-selected {
|
||||
color: var(--spectrum-global-color-gray-900);
|
||||
background: var(--spectrum-global-color-gray-300);
|
||||
}
|
||||
.noPadding {
|
||||
padding: 0;
|
||||
min-width: 0;
|
||||
}
|
||||
.is-selected:not(.emphasized) .spectrum-Icon {
|
||||
.is-selected .spectrum-Icon {
|
||||
color: var(--spectrum-global-color-gray-900);
|
||||
}
|
||||
.is-selected.disabled .spectrum-Icon {
|
||||
|
@ -137,4 +114,12 @@
|
|||
text-align: center;
|
||||
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>
|
||||
|
|
|
@ -1,14 +1,20 @@
|
|||
<script>
|
||||
import { setContext } from "svelte"
|
||||
import { setContext, getContext } from "svelte"
|
||||
import Popover from "../Popover/Popover.svelte"
|
||||
import Menu from "../Menu/Menu.svelte"
|
||||
|
||||
export let disabled = false
|
||||
export let align = "left"
|
||||
export let portalTarget
|
||||
export let openOnHover = false
|
||||
export let animate
|
||||
export let offset
|
||||
|
||||
const actionMenuContext = getContext("actionMenu")
|
||||
|
||||
let anchor
|
||||
let dropdown
|
||||
let timeout
|
||||
|
||||
// This is needed because display: contents is considered "invisible".
|
||||
// It should only ever be an action button, so should be fine.
|
||||
|
@ -16,11 +22,19 @@
|
|||
anchor = node.firstChild
|
||||
}
|
||||
|
||||
export const show = () => {
|
||||
cancelHide()
|
||||
dropdown.show()
|
||||
}
|
||||
|
||||
export const hide = () => {
|
||||
dropdown.hide()
|
||||
}
|
||||
export const show = () => {
|
||||
dropdown.show()
|
||||
|
||||
// Hides this menu and all parent menus
|
||||
const hideAll = () => {
|
||||
hide()
|
||||
actionMenuContext?.hide()
|
||||
}
|
||||
|
||||
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>
|
||||
|
||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||
<!-- 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" />
|
||||
</div>
|
||||
<Popover
|
||||
|
@ -43,9 +70,13 @@
|
|||
{anchor}
|
||||
{align}
|
||||
{portalTarget}
|
||||
{animate}
|
||||
{offset}
|
||||
resizable={false}
|
||||
on:open
|
||||
on:close
|
||||
on:mouseenter={openOnHover ? cancelHide : null}
|
||||
on:mouseleave={openOnHover ? queueHide : null}
|
||||
>
|
||||
<Menu>
|
||||
<slot />
|
||||
|
|
|
@ -151,9 +151,9 @@ export default function positionDropdown(element, opts) {
|
|||
// Determine X strategy
|
||||
if (align === "right") {
|
||||
applyXStrategy(Strategies.EndToEnd)
|
||||
} else if (align === "right-outside") {
|
||||
} else if (align === "right-outside" || align === "right-context-menu") {
|
||||
applyXStrategy(Strategies.StartToEnd)
|
||||
} else if (align === "left-outside") {
|
||||
} else if (align === "left-outside" || align === "left-context-menu") {
|
||||
applyXStrategy(Strategies.EndToStart)
|
||||
} else if (align === "center") {
|
||||
applyXStrategy(Strategies.MidPoint)
|
||||
|
@ -164,6 +164,12 @@ export default function positionDropdown(element, opts) {
|
|||
// Determine Y strategy
|
||||
if (align === "right-outside" || align === "left-outside") {
|
||||
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 {
|
||||
applyYStrategy(Strategies.StartToEnd)
|
||||
}
|
||||
|
@ -240,7 +246,7 @@ export default function positionDropdown(element, opts) {
|
|||
}
|
||||
|
||||
// Apply initial styles which don't need to change
|
||||
element.style.position = "absolute"
|
||||
element.style.position = "fixed"
|
||||
element.style.zIndex = "9999"
|
||||
|
||||
// Set up a scroll listener
|
||||
|
|
|
@ -17,6 +17,8 @@
|
|||
export let tooltip = undefined
|
||||
export let newStyles = true
|
||||
export let id
|
||||
export let ref
|
||||
export let reverse = false
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
</script>
|
||||
|
@ -25,6 +27,7 @@
|
|||
<button
|
||||
{id}
|
||||
{type}
|
||||
bind:this={ref}
|
||||
class:spectrum-Button--cta={cta}
|
||||
class:spectrum-Button--primary={primary}
|
||||
class:spectrum-Button--secondary={secondary}
|
||||
|
@ -41,6 +44,9 @@
|
|||
}
|
||||
}}
|
||||
>
|
||||
{#if $$slots && reverse}
|
||||
<span class="spectrum-Button-label"><slot /></span>
|
||||
{/if}
|
||||
{#if icon}
|
||||
<svg
|
||||
class="spectrum-Icon spectrum-Icon--size{size.toUpperCase()}"
|
||||
|
@ -51,7 +57,7 @@
|
|||
<use xlink:href="#spectrum-icon-18-{icon}" />
|
||||
</svg>
|
||||
{/if}
|
||||
{#if $$slots}
|
||||
{#if $$slots && !reverse}
|
||||
<span class="spectrum-Button-label"><slot /></span>
|
||||
{/if}
|
||||
</button>
|
||||
|
@ -91,4 +97,11 @@
|
|||
.spectrum-Button--secondary.new-styles.is-disabled {
|
||||
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>
|
||||
|
|
|
@ -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>
|
|
@ -19,6 +19,7 @@
|
|||
{disabled}
|
||||
on:change={onChange}
|
||||
on:click
|
||||
on:click|stopPropagation
|
||||
{id}
|
||||
type="checkbox"
|
||||
class="spectrum-Switch-input"
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<script>
|
||||
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 placeholder = null
|
||||
|
@ -68,10 +68,13 @@
|
|||
return type === "number" ? "decimal" : "text"
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
onMount(async () => {
|
||||
if (disabled) return
|
||||
focus = autofocus
|
||||
if (focus) field.focus()
|
||||
if (focus) {
|
||||
await tick()
|
||||
field.focus()
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
|
|
|
@ -60,10 +60,11 @@
|
|||
.newStyles {
|
||||
color: var(--spectrum-global-color-gray-700);
|
||||
}
|
||||
|
||||
svg {
|
||||
transition: color var(--spectrum-global-animation-duration-100, 130ms);
|
||||
}
|
||||
svg.hoverable {
|
||||
pointer-events: all;
|
||||
transition: color var(--spectrum-global-animation-duration-100, 130ms);
|
||||
}
|
||||
svg.hoverable:hover {
|
||||
color: var(--hover-color) !important;
|
||||
|
|
|
@ -1,55 +1,57 @@
|
|||
<script>
|
||||
import Body from "../Typography/Body.svelte"
|
||||
import IconAvatar from "../Icon/IconAvatar.svelte"
|
||||
import Label from "../Label/Label.svelte"
|
||||
import Avatar from "../Avatar/Avatar.svelte"
|
||||
import Icon from "../Icon/Icon.svelte"
|
||||
|
||||
export let icon = null
|
||||
export let iconBackground = null
|
||||
export let iconColor = null
|
||||
export let avatar = false
|
||||
export let title = null
|
||||
export let subtitle = null
|
||||
export let url = null
|
||||
export let hoverable = false
|
||||
|
||||
$: initials = avatar ? title?.[0] : null
|
||||
export let showArrow = false
|
||||
</script>
|
||||
|
||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<div class="list-item" class:hoverable on:click>
|
||||
<a
|
||||
href={url}
|
||||
class="list-item"
|
||||
class:hoverable={hoverable || url != null}
|
||||
on:click
|
||||
>
|
||||
<div class="left">
|
||||
{#if icon}
|
||||
<IconAvatar {icon} color={iconColor} background={iconBackground} />
|
||||
<Icon name={icon} color={iconColor} />
|
||||
{/if}
|
||||
{#if avatar}
|
||||
<Avatar {initials} />
|
||||
{/if}
|
||||
{#if title}
|
||||
<Body>{title}</Body>
|
||||
{/if}
|
||||
{#if subtitle}
|
||||
<Label>{subtitle}</Label>
|
||||
<div class="list-item__text">
|
||||
{#if title}
|
||||
<div class="list-item__title">
|
||||
{title}
|
||||
</div>
|
||||
{/if}
|
||||
{#if subtitle}
|
||||
<div class="list-item__subtitle">
|
||||
{subtitle}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
<div class="right">
|
||||
<slot name="right" />
|
||||
{#if showArrow}
|
||||
<Icon name="ChevronRight" />
|
||||
{/if}
|
||||
</div>
|
||||
{#if $$slots.default}
|
||||
<div class="right">
|
||||
<slot />
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</a>
|
||||
|
||||
<style>
|
||||
.list-item {
|
||||
padding: 0 16px;
|
||||
height: 56px;
|
||||
background: var(--spectrum-global-color-gray-50);
|
||||
padding: var(--spacing-m);
|
||||
background: var(--spectrum-global-color-gray-75);
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
border: 1px solid var(--spectrum-global-color-gray-300);
|
||||
transition: background 130ms ease-out;
|
||||
gap: var(--spacing-m);
|
||||
color: var(--spectrum-global-color-gray-800);
|
||||
}
|
||||
.list-item:not(:first-child) {
|
||||
border-top: none;
|
||||
|
@ -64,14 +66,15 @@
|
|||
}
|
||||
.hoverable:hover {
|
||||
cursor: pointer;
|
||||
background: var(--spectrum-global-color-gray-75);
|
||||
background: var(--spectrum-global-color-gray-200);
|
||||
}
|
||||
|
||||
.left,
|
||||
.right {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: var(--spacing-xl);
|
||||
gap: var(--spacing-s);
|
||||
}
|
||||
.left {
|
||||
width: 0;
|
||||
|
@ -79,17 +82,20 @@
|
|||
}
|
||||
.right {
|
||||
flex: 0 0 auto;
|
||||
color: var(--spectrum-global-color-gray-600);
|
||||
}
|
||||
.list-item :global(.spectrum-Icon),
|
||||
.list-item :global(.spectrum-Avatar) {
|
||||
flex: 0 0 auto;
|
||||
|
||||
.list-item__text {
|
||||
flex: 1 1 auto;
|
||||
width: 0;
|
||||
}
|
||||
.list-item :global(.spectrum-Body) {
|
||||
color: var(--spectrum-global-color-gray-900);
|
||||
}
|
||||
.list-item :global(.spectrum-Body) {
|
||||
.list-item__title,
|
||||
.list-item__subtitle {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
.list-item__subtitle {
|
||||
color: var(--spectrum-global-color-gray-600);
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -27,7 +27,7 @@
|
|||
|
||||
const onClick = () => {
|
||||
if (actionMenu && !noClose) {
|
||||
actionMenu.hide()
|
||||
actionMenu.hideAll()
|
||||
}
|
||||
dispatch("click")
|
||||
}
|
||||
|
@ -35,7 +35,7 @@
|
|||
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<li
|
||||
on:click|preventDefault={disabled ? null : onClick}
|
||||
on:click={disabled ? null : onClick}
|
||||
class="spectrum-Menu-item"
|
||||
class:is-disabled={disabled}
|
||||
role="menuitem"
|
||||
|
@ -47,8 +47,9 @@
|
|||
</div>
|
||||
{/if}
|
||||
<span class="spectrum-Menu-itemLabel"><slot /></span>
|
||||
{#if keys?.length}
|
||||
{#if keys?.length || $$slots.right}
|
||||
<div class="keys">
|
||||
<slot name="right" />
|
||||
{#each keys as key}
|
||||
<div class="key">
|
||||
{#if key.startsWith("!")}
|
||||
|
|
|
@ -30,7 +30,9 @@
|
|||
export let custom = false
|
||||
|
||||
const { hide, cancel } = getContext(Context.Modal)
|
||||
|
||||
let loading = false
|
||||
|
||||
$: confirmDisabled = disabled || loading
|
||||
|
||||
async function secondary(e) {
|
||||
|
@ -90,7 +92,7 @@
|
|||
|
||||
<!-- TODO: Remove content-grid class once Layout components are in bbui -->
|
||||
<section class="spectrum-Dialog-content content-grid">
|
||||
<slot />
|
||||
<slot {loading} />
|
||||
</section>
|
||||
{#if showCancelButton || showConfirmButton || $$slots.footer}
|
||||
<div
|
||||
|
|
|
@ -27,11 +27,7 @@
|
|||
<div class="spectrum-Toast-body" class:actionBody={!!action}>
|
||||
<div class="wrap spectrum-Toast-content">{message || ""}</div>
|
||||
{#if action}
|
||||
<ActionButton
|
||||
quiet
|
||||
emphasized
|
||||
on:click={() => action(() => dispatch("dismiss"))}
|
||||
>
|
||||
<ActionButton quiet on:click={() => action(() => dispatch("dismiss"))}>
|
||||
<div style="color: white; font-weight: 600;">{actionMessage}</div>
|
||||
</ActionButton>
|
||||
{/if}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<script>
|
||||
import "@spectrum-css/popover/dist/index-vars.css"
|
||||
import Portal from "svelte-portal"
|
||||
import { createEventDispatcher, getContext } from "svelte"
|
||||
import { createEventDispatcher, getContext, onDestroy } from "svelte"
|
||||
import positionDropdown from "../Actions/position_dropdown"
|
||||
import clickOutside from "../Actions/click_outside"
|
||||
import { fly } from "svelte/transition"
|
||||
|
@ -28,7 +28,24 @@
|
|||
export let resizable = true
|
||||
export let wrap = false
|
||||
|
||||
const animationDuration = 260
|
||||
|
||||
let timeout
|
||||
let blockPointerEvents = false
|
||||
|
||||
$: 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 = () => {
|
||||
dispatch("open")
|
||||
|
@ -77,6 +94,10 @@
|
|||
hide()
|
||||
}
|
||||
}
|
||||
|
||||
onDestroy(() => {
|
||||
clearTimeout(timeout)
|
||||
})
|
||||
</script>
|
||||
|
||||
{#if open}
|
||||
|
@ -104,9 +125,13 @@
|
|||
class="spectrum-Popover is-open"
|
||||
class:customZindex
|
||||
class:hidden={!showPopover}
|
||||
class:blockPointerEvents
|
||||
role="presentation"
|
||||
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:mouseleave
|
||||
>
|
||||
|
@ -121,6 +146,12 @@
|
|||
border-color: var(--spectrum-global-color-gray-300);
|
||||
overflow: auto;
|
||||
transition: opacity 260ms ease-out;
|
||||
filter: none;
|
||||
-webkit-filter: none;
|
||||
box-shadow: 0 1px 4px var(--drop-shadow);
|
||||
}
|
||||
.blockPointerEvents {
|
||||
pointer-events: none;
|
||||
}
|
||||
.hidden {
|
||||
opacity: 0;
|
||||
|
|
|
@ -228,3 +228,13 @@ export const getDateDisplayValue = (
|
|||
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})`
|
||||
}
|
||||
|
|
|
@ -39,6 +39,7 @@ export { default as ActionGroup } from "./ActionGroup/ActionGroup.svelte"
|
|||
export { default as ActionMenu } from "./ActionMenu/ActionMenu.svelte"
|
||||
export { default as Button } from "./Button/Button.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 Icon } from "./Icon/Icon.svelte"
|
||||
export { default as IconAvatar } from "./Icon/IconAvatar.svelte"
|
||||
|
|
|
@ -12,13 +12,17 @@
|
|||
import { Icon, notifications, Modal, Toggle } from "@budibase/bbui"
|
||||
import { ActionStepID } from "constants/backend/automations"
|
||||
import UndoRedoControl from "components/common/UndoRedoControl.svelte"
|
||||
import { sdk } from "@budibase/shared-core"
|
||||
|
||||
export let automation
|
||||
|
||||
let testDataModal
|
||||
let confirmDeleteDialog
|
||||
let scrolling = false
|
||||
|
||||
$: blocks = getBlocks(automation).filter(x => x.stepId !== ActionStepID.LOOP)
|
||||
$: isRowAction = sdk.automations.isRowAction(automation)
|
||||
|
||||
const getBlocks = automation => {
|
||||
let blocks = []
|
||||
if (automation.definition.trigger) {
|
||||
|
@ -74,17 +78,19 @@
|
|||
Test details
|
||||
</div>
|
||||
</div>
|
||||
<div class="setting-spacing">
|
||||
<Toggle
|
||||
text={automation.disabled ? "Paused" : "Activated"}
|
||||
on:change={automationStore.actions.toggleDisabled(
|
||||
automation._id,
|
||||
automation.disabled
|
||||
)}
|
||||
disabled={!$selectedAutomation?.definition?.trigger}
|
||||
value={!automation.disabled}
|
||||
/>
|
||||
</div>
|
||||
{#if !isRowAction}
|
||||
<div class="setting-spacing">
|
||||
<Toggle
|
||||
text={automation.disabled ? "Paused" : "Activated"}
|
||||
on:change={automationStore.actions.toggleDisabled(
|
||||
automation._id,
|
||||
automation.disabled
|
||||
)}
|
||||
disabled={!$selectedAutomation?.definition?.trigger}
|
||||
value={!automation.disabled}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
<div class="canvas" on:scroll={handleScroll}>
|
||||
|
|
|
@ -190,7 +190,7 @@
|
|||
{#if isTrigger && triggerInfo}
|
||||
<InlineAlert
|
||||
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 lastStep}
|
||||
|
|
|
@ -9,6 +9,7 @@
|
|||
import { sdk } from "@budibase/shared-core"
|
||||
import ConfirmDialog from "components/common/ConfirmDialog.svelte"
|
||||
import UpdateAutomationModal from "components/automation/AutomationPanel/UpdateAutomationModal.svelte"
|
||||
import UpdateRowActionModal from "components/automation/AutomationPanel/UpdateRowActionModal.svelte"
|
||||
import NavItem from "components/common/NavItem.svelte"
|
||||
|
||||
export let automation
|
||||
|
@ -16,12 +17,16 @@
|
|||
|
||||
let confirmDeleteDialog
|
||||
let updateAutomationDialog
|
||||
let updateRowActionDialog
|
||||
|
||||
$: isRowAction = sdk.automations.isRowAction(automation)
|
||||
|
||||
async function deleteAutomation() {
|
||||
try {
|
||||
await automationStore.actions.delete(automation)
|
||||
notifications.success("Automation deleted successfully")
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
notifications.error("Error deleting automation")
|
||||
}
|
||||
}
|
||||
|
@ -36,42 +41,7 @@
|
|||
}
|
||||
|
||||
const getContextMenuItems = () => {
|
||||
const isRowAction = sdk.automations.isRowAction(automation)
|
||||
const result = []
|
||||
if (!isRowAction) {
|
||||
result.push(
|
||||
...[
|
||||
{
|
||||
icon: "Delete",
|
||||
name: "Delete",
|
||||
keyBind: null,
|
||||
visible: true,
|
||||
disabled: false,
|
||||
callback: confirmDeleteDialog.show,
|
||||
},
|
||||
{
|
||||
icon: "Edit",
|
||||
name: "Edit",
|
||||
keyBind: null,
|
||||
visible: true,
|
||||
disabled: !automation.definition.trigger,
|
||||
callback: updateAutomationDialog.show,
|
||||
},
|
||||
{
|
||||
icon: "Duplicate",
|
||||
name: "Duplicate",
|
||||
keyBind: null,
|
||||
visible: true,
|
||||
disabled:
|
||||
!automation.definition.trigger ||
|
||||
automation.definition.trigger?.name === "Webhook",
|
||||
callback: duplicateAutomation,
|
||||
},
|
||||
]
|
||||
)
|
||||
}
|
||||
|
||||
result.push({
|
||||
const pause = {
|
||||
icon: automation.disabled ? "CheckmarkCircle" : "Cancel",
|
||||
name: automation.disabled ? "Activate" : "Pause",
|
||||
keyBind: null,
|
||||
|
@ -83,8 +53,50 @@
|
|||
automation.disabled
|
||||
)
|
||||
},
|
||||
})
|
||||
return result
|
||||
}
|
||||
const del = {
|
||||
icon: "Delete",
|
||||
name: "Delete",
|
||||
keyBind: null,
|
||||
visible: true,
|
||||
disabled: false,
|
||||
callback: confirmDeleteDialog.show,
|
||||
}
|
||||
if (!isRowAction) {
|
||||
return [
|
||||
{
|
||||
icon: "Edit",
|
||||
name: "Edit",
|
||||
keyBind: null,
|
||||
visible: true,
|
||||
disabled: !automation.definition.trigger,
|
||||
callback: updateAutomationDialog.show,
|
||||
},
|
||||
{
|
||||
icon: "Duplicate",
|
||||
name: "Duplicate",
|
||||
keyBind: null,
|
||||
visible: true,
|
||||
disabled:
|
||||
!automation.definition.trigger ||
|
||||
automation.definition.trigger?.name === "Webhook",
|
||||
callback: duplicateAutomation,
|
||||
},
|
||||
pause,
|
||||
del,
|
||||
]
|
||||
} else {
|
||||
return [
|
||||
{
|
||||
icon: "Edit",
|
||||
name: "Edit",
|
||||
keyBind: null,
|
||||
visible: true,
|
||||
callback: updateRowActionDialog.show,
|
||||
},
|
||||
del,
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
const openContextMenu = e => {
|
||||
|
@ -99,7 +111,9 @@
|
|||
<NavItem
|
||||
on:contextmenu={openContextMenu}
|
||||
{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}
|
||||
selected={automation._id === $selectedAutomation?._id}
|
||||
hovering={automation._id === $contextMenuStore.id}
|
||||
|
@ -107,9 +121,7 @@
|
|||
selectedBy={$userSelectedResourceMap[automation._id]}
|
||||
disabled={automation.disabled}
|
||||
>
|
||||
<div class="icon">
|
||||
<Icon on:click={openContextMenu} size="S" hoverable name="MoreSmallList" />
|
||||
</div>
|
||||
<Icon on:click={openContextMenu} size="S" hoverable name="MoreSmallList" />
|
||||
</NavItem>
|
||||
|
||||
<ConfirmDialog
|
||||
|
@ -122,13 +134,9 @@
|
|||
<i>{automation.name}?</i>
|
||||
This action cannot be undone.
|
||||
</ConfirmDialog>
|
||||
<UpdateAutomationModal {automation} bind:this={updateAutomationDialog} />
|
||||
|
||||
<style>
|
||||
div.icon {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
}
|
||||
</style>
|
||||
{#if isRowAction}
|
||||
<UpdateRowActionModal {automation} bind:this={updateRowActionDialog} />
|
||||
{:else}
|
||||
<UpdateAutomationModal {automation} bind:this={updateAutomationDialog} />
|
||||
{/if}
|
||||
|
|
|
@ -3,13 +3,21 @@
|
|||
import { Modal, notifications, Layout } from "@budibase/bbui"
|
||||
import NavHeader from "components/common/NavHeader.svelte"
|
||||
import { onMount } from "svelte"
|
||||
import { automationStore } from "stores/builder"
|
||||
import { automationStore, tables } from "stores/builder"
|
||||
import AutomationNavItem from "./AutomationNavItem.svelte"
|
||||
import { TriggerStepID } from "constants/backend/automations"
|
||||
|
||||
export let modal
|
||||
export let webhookModal
|
||||
let searchString
|
||||
|
||||
const dsTriggers = [
|
||||
TriggerStepID.ROW_SAVED,
|
||||
TriggerStepID.ROW_UPDATED,
|
||||
TriggerStepID.ROW_DELETED,
|
||||
TriggerStepID.ROW_ACTION,
|
||||
]
|
||||
|
||||
$: filteredAutomations = $automationStore.automations
|
||||
.filter(automation => {
|
||||
return (
|
||||
|
@ -29,19 +37,47 @@
|
|||
return lowerA > lowerB ? 1 : -1
|
||||
})
|
||||
|
||||
$: groupedAutomations = filteredAutomations.reduce((acc, auto) => {
|
||||
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
|
||||
}, {})
|
||||
$: groupedAutomations = groupAutomations(filteredAutomations)
|
||||
|
||||
$: 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 () => {
|
||||
try {
|
||||
await automationStore.actions.fetch()
|
||||
|
@ -88,16 +124,22 @@
|
|||
|
||||
<style>
|
||||
.nav-group {
|
||||
padding-top: var(--spacing-l);
|
||||
padding-top: 24px;
|
||||
}
|
||||
.nav-group:first-child {
|
||||
padding-top: var(--spacing-s);
|
||||
}
|
||||
.nav-group-header {
|
||||
color: var(--spectrum-global-color-gray-600);
|
||||
padding: 0px calc(var(--spacing-l) + 4px);
|
||||
padding-bottom: var(--spacing-l);
|
||||
padding-bottom: var(--spacing-m);
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.side-bar {
|
||||
flex: 0 0 260px;
|
||||
display: flex;
|
||||
|
|
|
@ -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>
|
|
@ -62,6 +62,7 @@
|
|||
} from "@budibase/types"
|
||||
import { FIELDS } from "constants/backend"
|
||||
import PropField from "./PropField.svelte"
|
||||
import { utils } from "@budibase/shared-core"
|
||||
|
||||
export let block
|
||||
export let testData
|
||||
|
@ -96,8 +97,14 @@
|
|||
$: memoEnvVariables.set($environment.variables)
|
||||
$: memoBlock.set(block)
|
||||
|
||||
$: filters = lookForFilters(schemaProperties) || []
|
||||
$: tempFilters = filters
|
||||
$: filters = lookForFilters(schemaProperties)
|
||||
$: filterCount =
|
||||
filters?.groups?.reduce((acc, group) => {
|
||||
acc = acc += group?.filters?.length || 0
|
||||
return acc
|
||||
}, 0) || 0
|
||||
|
||||
$: tempFilters = cloneDeep(filters)
|
||||
$: stepId = $memoBlock.stepId
|
||||
|
||||
$: automationBindings = getAvailableBindings(
|
||||
|
@ -791,14 +798,13 @@
|
|||
break
|
||||
}
|
||||
}
|
||||
return filters || []
|
||||
return utils.processSearchFilters(filters)
|
||||
}
|
||||
|
||||
function saveFilters(key) {
|
||||
const filters = QueryUtils.buildQuery(tempFilters)
|
||||
|
||||
const query = QueryUtils.buildQuery(tempFilters)
|
||||
onChange({
|
||||
[key]: filters,
|
||||
[key]: query,
|
||||
[`${key}-def`]: tempFilters, // need to store the builder definition in the automation
|
||||
})
|
||||
|
||||
|
@ -1027,18 +1033,24 @@
|
|||
</div>
|
||||
</div>
|
||||
{:else if value.customType === AutomationCustomIOType.FILTERS || value.customType === AutomationCustomIOType.TRIGGER_FILTER}
|
||||
<ActionButton fullWidth on:click={drawer.show}
|
||||
>{filters.length > 0
|
||||
? "Update Filter"
|
||||
: "No Filter set"}</ActionButton
|
||||
<ActionButton fullWidth on:click={drawer.show}>
|
||||
{filterCount > 0 ? "Update Filter" : "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)}>
|
||||
Save
|
||||
</Button>
|
||||
|
||||
<DrawerContent slot="body">
|
||||
<FilterBuilder
|
||||
{filters}
|
||||
filters={tempFilters}
|
||||
{bindings}
|
||||
{schemaFields}
|
||||
datasource={{ type: "table", tableId }}
|
||||
|
|
|
@ -233,6 +233,14 @@
|
|||
)
|
||||
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>
|
||||
|
||||
{#each schemaFields || [] as [field, schema]}
|
||||
|
@ -257,7 +265,7 @@
|
|||
panel={AutomationBindingPanel}
|
||||
type={schema.type}
|
||||
{schema}
|
||||
value={editableRow[field]}
|
||||
value={drawerValue(editableRow[field])}
|
||||
on:change={e =>
|
||||
onChange({
|
||||
row: {
|
||||
|
|
|
@ -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}
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -1,13 +1,15 @@
|
|||
<script>
|
||||
import { ActionButton, Modal } from "@budibase/bbui"
|
||||
import { Button, Modal } from "@budibase/bbui"
|
||||
import EditRolesModal from "../modals/EditRoles.svelte"
|
||||
|
||||
let modal
|
||||
</script>
|
||||
|
||||
<ActionButton icon="UsersLock" quiet on:click={modal.show}>
|
||||
Edit roles
|
||||
</ActionButton>
|
||||
<Modal bind:this={modal}>
|
||||
<div>
|
||||
<Button secondary icon="UsersLock" on:click on:click={modal.show}>
|
||||
Edit roles
|
||||
</Button>
|
||||
</div>
|
||||
<Modal bind:this={modal} on:show on:hide>
|
||||
<EditRolesModal />
|
||||
</Modal>
|
||||
|
|
|
@ -1,23 +1,31 @@
|
|||
<script>
|
||||
import { ActionButton, Modal } from "@budibase/bbui"
|
||||
import { ActionButton } from "@budibase/bbui"
|
||||
import { permissions } from "stores/builder"
|
||||
import ManageAccessModal from "../modals/ManageAccessModal.svelte"
|
||||
import DetailPopover from "components/common/DetailPopover.svelte"
|
||||
import EditRolesButton from "./EditRolesButton.svelte"
|
||||
|
||||
export let resourceId
|
||||
export let disabled = false
|
||||
|
||||
let modal
|
||||
let resourcePermissions
|
||||
let showPopover = true
|
||||
|
||||
async function openModal() {
|
||||
resourcePermissions = await permissions.forResourceDetailed(resourceId)
|
||||
modal.show()
|
||||
$: fetchPermissions(resourceId)
|
||||
|
||||
const fetchPermissions = async id => {
|
||||
resourcePermissions = await permissions.forResourceDetailed(id)
|
||||
}
|
||||
</script>
|
||||
|
||||
<ActionButton icon="LockClosed" quiet on:click={openModal} {disabled}>
|
||||
Access
|
||||
</ActionButton>
|
||||
<Modal bind:this={modal}>
|
||||
<ManageAccessModal {resourceId} permissions={resourcePermissions} />
|
||||
</Modal>
|
||||
<DetailPopover title="Manage access" {showPopover}>
|
||||
<svelte:fragment slot="anchor" let:open>
|
||||
<ActionButton icon="LockClosed" selected={open} quiet>Access</ActionButton>
|
||||
</svelte:fragment>
|
||||
{#if resourcePermissions}
|
||||
<ManageAccessModal {resourceId} permissions={resourcePermissions} />
|
||||
{/if}
|
||||
<EditRolesButton
|
||||
on:show={() => (showPopover = false)}
|
||||
on:hide={() => (showPopover = true)}
|
||||
/>
|
||||
</DetailPopover>
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
import { getUserBindings } from "dataBinding"
|
||||
import { makePropSafe } from "@budibase/string-templates"
|
||||
import { search } from "@budibase/frontend-core"
|
||||
import { utils } from "@budibase/shared-core"
|
||||
import { tables } from "stores/builder"
|
||||
|
||||
export let schema
|
||||
|
@ -16,15 +17,19 @@
|
|||
|
||||
let drawer
|
||||
|
||||
$: tempValue = filters || []
|
||||
$: localFilters = utils.processSearchFilters(filters)
|
||||
|
||||
$: schemaFields = search.getFields(
|
||||
$tables.list,
|
||||
Object.values(schema || {}),
|
||||
{ allowLinks: true }
|
||||
)
|
||||
|
||||
$: text = getText(filters)
|
||||
$: selected = tempValue.filter(x => !x.onEmptyFilter)?.length > 0
|
||||
$: filterCount =
|
||||
localFilters?.groups?.reduce((acc, group) => {
|
||||
return (acc += group.filters.filter(filter => filter.field).length)
|
||||
}, 0) || 0
|
||||
|
||||
$: bindings = [
|
||||
{
|
||||
type: "context",
|
||||
|
@ -38,28 +43,33 @@
|
|||
},
|
||||
...getUserBindings(),
|
||||
]
|
||||
const getText = filters => {
|
||||
const count = filters?.filter(filter => filter.field)?.length
|
||||
return count ? `Filter (${count})` : "Filter"
|
||||
}
|
||||
</script>
|
||||
|
||||
<ActionButton icon="Filter" quiet {disabled} on:click={drawer.show} {selected}>
|
||||
{text}
|
||||
<ActionButton
|
||||
icon="Filter"
|
||||
quiet
|
||||
{disabled}
|
||||
on:click={drawer.show}
|
||||
selected={filterCount > 0}
|
||||
accentColor="#004EA6"
|
||||
>
|
||||
{filterCount ? `Filter: ${filterCount}` : "Filter"}
|
||||
</ActionButton>
|
||||
|
||||
<Drawer
|
||||
bind:this={drawer}
|
||||
title="Filtering"
|
||||
on:drawerHide
|
||||
on:drawerShow
|
||||
on:drawerShow={() => {
|
||||
localFilters = utils.processSearchFilters(filters)
|
||||
}}
|
||||
forceModal
|
||||
>
|
||||
<Button
|
||||
cta
|
||||
slot="buttons"
|
||||
on:click={() => {
|
||||
dispatch("change", tempValue)
|
||||
dispatch("change", localFilters)
|
||||
drawer.hide()
|
||||
}}
|
||||
>
|
||||
|
@ -67,10 +77,10 @@
|
|||
</Button>
|
||||
<DrawerContent slot="body">
|
||||
<FilterBuilder
|
||||
{filters}
|
||||
filters={localFilters}
|
||||
{schemaFields}
|
||||
datasource={{ type: "table", tableId }}
|
||||
on:change={e => (tempValue = e.detail)}
|
||||
on:change={e => (localFilters = e.detail)}
|
||||
{bindings}
|
||||
/>
|
||||
</DrawerContent>
|
||||
|
|
|
@ -1,20 +1,19 @@
|
|||
<script>
|
||||
import { getContext } from "svelte"
|
||||
import { Icon, notifications, ActionButton, Popover } from "@budibase/bbui"
|
||||
import { getColumnIcon } from "../lib/utils"
|
||||
import ToggleActionButtonGroup from "./ToggleActionButtonGroup.svelte"
|
||||
import ToggleActionButtonGroup from "components/common/ToggleActionButtonGroup.svelte"
|
||||
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 { FieldPermissions } from "../../../constants"
|
||||
import { FieldPermissions } from "./GridColumnsSettingButton.svelte"
|
||||
|
||||
export let permissions = [FieldPermissions.WRITABLE, FieldPermissions.HIDDEN]
|
||||
export let disabledPermissions = []
|
||||
export let columns
|
||||
export let fromRelationshipField
|
||||
export let canSetRelationshipSchemas
|
||||
|
||||
const { datasource, dispatch, config } = getContext("grid")
|
||||
|
||||
$: canSetRelationshipSchemas = $config.canSetRelationshipSchemas
|
||||
const { datasource, dispatch } = getContext("grid")
|
||||
|
||||
let relationshipPanelAnchor
|
||||
let relationshipFieldName
|
||||
|
@ -153,9 +152,6 @@
|
|||
await datasource.actions.saveSchemaMutations()
|
||||
} catch (e) {
|
||||
notifications.error(e.message)
|
||||
} finally {
|
||||
await datasource.actions.resetSchemaMutations()
|
||||
await datasource.actions.refreshDefinition()
|
||||
}
|
||||
dispatch(visible ? "show-column" : "hide-column")
|
||||
}
|
||||
|
@ -177,7 +173,7 @@
|
|||
<div class="columns">
|
||||
{#each displayColumns as column}
|
||||
<div class="column">
|
||||
<Icon size="S" name={getColumnIcon(column)} />
|
||||
<Icon size="S" name={SchemaUtils.getColumnIcon(column)} />
|
||||
<div class="column-label" title={column.label}>
|
||||
{column.label}
|
||||
</div>
|
||||
|
@ -198,6 +194,7 @@
|
|||
size="S"
|
||||
icon="ChevronRight"
|
||||
quiet
|
||||
selected={relationshipFieldName === column.name}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
|
@ -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>
|
|
@ -1,8 +1,17 @@
|
|||
<script context="module">
|
||||
export const FieldPermissions = {
|
||||
WRITABLE: "writable",
|
||||
READONLY: "readonly",
|
||||
HIDDEN: "hidden",
|
||||
}
|
||||
</script>
|
||||
|
||||
<script>
|
||||
import { getContext } from "svelte"
|
||||
import { ActionButton, Popover } from "@budibase/bbui"
|
||||
import ColumnsSettingContent from "./ColumnsSettingContent.svelte"
|
||||
import { FieldPermissions } from "../../../constants"
|
||||
import { isEnabled } from "helpers/featureFlags"
|
||||
import { FeatureFlag } from "@budibase/types"
|
||||
|
||||
const { tableColumns, datasource } = getContext("grid")
|
||||
|
||||
|
@ -12,7 +21,7 @@
|
|||
$: anyRestricted = $tableColumns.filter(
|
||||
col => !col.visible || col.readonly
|
||||
).length
|
||||
$: text = anyRestricted ? `Columns (${anyRestricted} restricted)` : "Columns"
|
||||
$: text = anyRestricted ? `Columns: ${anyRestricted} restricted` : "Columns"
|
||||
$: permissions =
|
||||
$datasource.type === "viewV2"
|
||||
? [
|
||||
|
@ -31,11 +40,16 @@
|
|||
on:click={() => (open = !open)}
|
||||
selected={open || anyRestricted}
|
||||
disabled={!$tableColumns.length}
|
||||
accentColor="#674D00"
|
||||
>
|
||||
{text}
|
||||
</ActionButton>
|
||||
</div>
|
||||
|
||||
<Popover bind:open {anchor} align="left">
|
||||
<ColumnsSettingContent columns={$tableColumns} {permissions} />
|
||||
<ColumnsSettingContent
|
||||
columns={$tableColumns}
|
||||
canSetRelationshipSchemas={isEnabled(FeatureFlag.ENRICHED_RELATIONSHIPS)}
|
||||
{permissions}
|
||||
/>
|
||||
</Popover>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -4,14 +4,8 @@
|
|||
|
||||
const { datasource } = getContext("grid")
|
||||
|
||||
$: resourceId = getResourceID($datasource)
|
||||
|
||||
const getResourceID = datasource => {
|
||||
if (!datasource) {
|
||||
return null
|
||||
}
|
||||
return datasource.type === "table" ? datasource.tableId : datasource.id
|
||||
}
|
||||
$: ds = $datasource
|
||||
$: resourceId = ds?.type === "table" ? ds.tableId : ds?.id
|
||||
</script>
|
||||
|
||||
<ManageAccessButton {resourceId} />
|
||||
|
|
|
@ -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>
|
|
@ -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>
|
|
@ -1,34 +1,34 @@
|
|||
<script>
|
||||
import { getContext } from "svelte"
|
||||
import { ActionButton, Popover, Label } from "@budibase/bbui"
|
||||
import {
|
||||
DefaultColumnWidth,
|
||||
LargeRowHeight,
|
||||
MediumRowHeight,
|
||||
SmallRowHeight,
|
||||
} from "../lib/constants"
|
||||
|
||||
const { columns, rowHeight, definition, fixedRowHeight, datasource } =
|
||||
getContext("grid")
|
||||
const {
|
||||
Constants,
|
||||
columns,
|
||||
rowHeight,
|
||||
definition,
|
||||
fixedRowHeight,
|
||||
datasource,
|
||||
} = getContext("grid")
|
||||
|
||||
// Some constants for column width options
|
||||
const smallColSize = 120
|
||||
const mediumColSize = DefaultColumnWidth
|
||||
const largeColSize = DefaultColumnWidth * 1.5
|
||||
const mediumColSize = Constants.DefaultColumnWidth
|
||||
const largeColSize = Constants.DefaultColumnWidth * 1.5
|
||||
|
||||
// Row height sizes
|
||||
const rowSizeOptions = [
|
||||
{
|
||||
label: "Small",
|
||||
size: SmallRowHeight,
|
||||
size: Constants.SmallRowHeight,
|
||||
},
|
||||
{
|
||||
label: "Medium",
|
||||
size: MediumRowHeight,
|
||||
size: Constants.MediumRowHeight,
|
||||
},
|
||||
{
|
||||
label: "Large",
|
||||
size: LargeRowHeight,
|
||||
size: Constants.LargeRowHeight,
|
||||
},
|
||||
]
|
||||
|
|
@ -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 |
|
@ -4,6 +4,7 @@
|
|||
Button,
|
||||
Label,
|
||||
Select,
|
||||
Multiselect,
|
||||
Toggle,
|
||||
Icon,
|
||||
DatePicker,
|
||||
|
@ -25,6 +26,7 @@
|
|||
import { createEventDispatcher, getContext, onMount } from "svelte"
|
||||
import { cloneDeep } from "lodash/fp"
|
||||
import { tables, datasources } from "stores/builder"
|
||||
import { licensing } from "stores/portal"
|
||||
import { TableNames, UNEDITABLE_USER_FIELDS } from "constants"
|
||||
import {
|
||||
FIELDS,
|
||||
|
@ -34,6 +36,7 @@
|
|||
} from "constants/backend"
|
||||
import { getAutoColumnInformation, buildAutoColumn } from "helpers/utils"
|
||||
import ConfirmDialog from "components/common/ConfirmDialog.svelte"
|
||||
import AIFieldConfiguration from "components/common/AIFieldConfiguration.svelte"
|
||||
import ModalBindableInput from "components/common/bindings/ModalBindableInput.svelte"
|
||||
import { getBindings } from "components/backend/DataTable/formula"
|
||||
import JSONSchemaModal from "./JSONSchemaModal.svelte"
|
||||
|
@ -49,18 +52,13 @@
|
|||
import { isEnabled } from "helpers/featureFlags"
|
||||
import { getUserBindings } from "dataBinding"
|
||||
|
||||
const AUTO_TYPE = FieldType.AUTO
|
||||
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
|
||||
export let field
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
const { dispatch: gridDispatch, rows } = getContext("grid")
|
||||
|
||||
export let field
|
||||
const SafeID = `${makePropSafe("user")}.${makePropSafe("_id")}`
|
||||
const SingleUserDefault = `{{ ${SafeID} }}`
|
||||
const MultiUserDefault = `{{ js "${btoa(`return [$("${SafeID}")]`)}" }}`
|
||||
|
||||
let mounted = false
|
||||
let originalName
|
||||
|
@ -103,13 +101,15 @@
|
|||
let optionsValid = true
|
||||
|
||||
$: rowGoldenSample = RowUtils.generateGoldenSample($rows)
|
||||
$: aiEnabled =
|
||||
$licensing.customAIConfigsEnabled || $licensing.budibaseAIEnabled
|
||||
$: if (primaryDisplay) {
|
||||
editableColumn.constraints.presence = { allowEmpty: false }
|
||||
}
|
||||
$: {
|
||||
// this parses any changes the user has made when creating a new internal relationship
|
||||
// into what we expect the schema to look like
|
||||
if (editableColumn.type === LINK_TYPE) {
|
||||
if (editableColumn.type === FieldType.LINK) {
|
||||
relationshipTableIdPrimary = table._id
|
||||
if (relationshipPart1 === PrettyRelationshipDefinitions.ONE) {
|
||||
relationshipOpts2 = relationshipOpts2.filter(
|
||||
|
@ -137,15 +137,16 @@
|
|||
}
|
||||
$: initialiseField(field, savingColumn)
|
||||
$: checkConstraints(editableColumn)
|
||||
$: required = hasDefault
|
||||
? false
|
||||
: !!editableColumn?.constraints?.presence || primaryDisplay
|
||||
$: required =
|
||||
primaryDisplay ||
|
||||
editableColumn?.constraints?.presence === true ||
|
||||
editableColumn?.constraints?.presence?.allowEmpty === false
|
||||
$: uneditable =
|
||||
$tables.selected?._id === TableNames.USERS &&
|
||||
UNEDITABLE_USER_FIELDS.includes(editableColumn.name)
|
||||
$: invalid =
|
||||
!editableColumn?.name ||
|
||||
(editableColumn?.type === LINK_TYPE && !editableColumn?.tableId) ||
|
||||
(editableColumn?.type === FieldType.LINK && !editableColumn?.tableId) ||
|
||||
Object.keys(errors).length !== 0 ||
|
||||
!optionsValid
|
||||
$: errors = checkErrors(editableColumn)
|
||||
|
@ -168,12 +169,12 @@
|
|||
// used to select what different options can be displayed for column type
|
||||
$: canBeDisplay =
|
||||
canBeDisplayColumn(editableColumn) && !editableColumn.autocolumn
|
||||
$: canHaveDefault =
|
||||
isEnabled("DEFAULT_VALUES") && canHaveDefaultColumn(editableColumn.type)
|
||||
$: defaultValuesEnabled = isEnabled("DEFAULT_VALUES")
|
||||
$: canHaveDefault = !required && canHaveDefaultColumn(editableColumn.type)
|
||||
$: canBeRequired =
|
||||
editableColumn?.type !== LINK_TYPE &&
|
||||
editableColumn?.type !== FieldType.LINK &&
|
||||
!uneditable &&
|
||||
editableColumn?.type !== AUTO_TYPE &&
|
||||
editableColumn?.type !== FieldType.AUTO &&
|
||||
!editableColumn.autocolumn
|
||||
$: hasDefault =
|
||||
editableColumn?.default != null && editableColumn?.default !== ""
|
||||
|
@ -188,7 +189,6 @@
|
|||
(originalName &&
|
||||
SWITCHABLE_TYPES[field.type] &&
|
||||
!editableColumn?.autocolumn)
|
||||
|
||||
$: allowedTypes = getAllowedTypes(datasource).map(t => ({
|
||||
fieldId: makeFieldId(t.type, t.subtype),
|
||||
...t,
|
||||
|
@ -206,6 +206,11 @@
|
|||
},
|
||||
...getUserBindings(),
|
||||
]
|
||||
$: sanitiseDefaultValue(
|
||||
editableColumn.type,
|
||||
editableColumn.constraints?.inclusion || [],
|
||||
editableColumn.default
|
||||
)
|
||||
|
||||
const fieldDefinitions = Object.values(FIELDS).reduce(
|
||||
// Storing the fields by complex field id
|
||||
|
@ -218,7 +223,7 @@
|
|||
|
||||
function makeFieldId(type, subtype, autocolumn) {
|
||||
// don't make field IDs for auto types
|
||||
if (type === AUTO_TYPE || autocolumn) {
|
||||
if (type === FieldType.AUTO || autocolumn) {
|
||||
return type.toUpperCase()
|
||||
} else if (
|
||||
type === FieldType.BB_REFERENCE ||
|
||||
|
@ -243,7 +248,7 @@
|
|||
// 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
|
||||
// for the tableId
|
||||
if (editableColumn.type === LINK_TYPE && editableColumn.tableId) {
|
||||
if (editableColumn.type === FieldType.LINK && editableColumn.tableId) {
|
||||
relationshipTableIdPrimary = table._id
|
||||
relationshipTableIdSecondary = editableColumn.tableId
|
||||
if (editableColumn.relationshipType in relationshipMap) {
|
||||
|
@ -284,17 +289,33 @@
|
|||
|
||||
delete saveColumn.fieldId
|
||||
|
||||
if (saveColumn.type === AUTO_TYPE) {
|
||||
if (saveColumn.type === FieldType.AUTO) {
|
||||
saveColumn = buildAutoColumn(
|
||||
$tables.selected.name,
|
||||
saveColumn.name,
|
||||
saveColumn.subtype
|
||||
)
|
||||
}
|
||||
if (saveColumn.type !== LINK_TYPE) {
|
||||
if (saveColumn.type !== FieldType.LINK) {
|
||||
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 {
|
||||
await tables.saveField({
|
||||
originalName,
|
||||
|
@ -362,9 +383,9 @@
|
|||
editableColumn.subtype = definition.subtype
|
||||
|
||||
// Default relationships many to many
|
||||
if (editableColumn.type === LINK_TYPE) {
|
||||
if (editableColumn.type === FieldType.LINK) {
|
||||
editableColumn.relationshipType = RelationshipType.MANY_TO_MANY
|
||||
} else if (editableColumn.type === FORMULA_TYPE) {
|
||||
} else if (editableColumn.type === FieldType.FORMULA) {
|
||||
editableColumn.formulaType = "dynamic"
|
||||
}
|
||||
}
|
||||
|
@ -430,6 +451,7 @@
|
|||
FIELDS.BOOLEAN,
|
||||
FIELDS.DATETIME,
|
||||
FIELDS.LINK,
|
||||
...(aiEnabled ? [FIELDS.AI] : []),
|
||||
FIELDS.LONGFORM,
|
||||
FIELDS.USER,
|
||||
FIELDS.USERS,
|
||||
|
@ -483,17 +505,23 @@
|
|||
fieldToCheck.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 = {}
|
||||
}
|
||||
// some number types made server-side will be missing constraints
|
||||
if (
|
||||
fieldToCheck.type === NUMBER_TYPE &&
|
||||
fieldToCheck.type === FieldType.NUMBER &&
|
||||
!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 = {}
|
||||
}
|
||||
}
|
||||
|
@ -541,6 +569,20 @@
|
|||
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(() => {
|
||||
mounted = true
|
||||
})
|
||||
|
@ -554,13 +596,13 @@
|
|||
on:input={e => {
|
||||
if (
|
||||
!uneditable &&
|
||||
!(linkEditDisabled && editableColumn.type === LINK_TYPE)
|
||||
!(linkEditDisabled && editableColumn.type === FieldType.LINK)
|
||||
) {
|
||||
editableColumn.name = e.target.value
|
||||
}
|
||||
}}
|
||||
disabled={uneditable ||
|
||||
(linkEditDisabled && editableColumn.type === LINK_TYPE)}
|
||||
(linkEditDisabled && editableColumn.type === FieldType.LINK)}
|
||||
error={errors?.name}
|
||||
/>
|
||||
{/if}
|
||||
|
@ -574,7 +616,7 @@
|
|||
getOptionValue={field => field.fieldId}
|
||||
getOptionIcon={field => field.icon}
|
||||
isOptionEnabled={option => {
|
||||
if (option.type === AUTO_TYPE) {
|
||||
if (option.type === FieldType.AUTO) {
|
||||
return availableAutoColumnKeys?.length > 0
|
||||
}
|
||||
return true
|
||||
|
@ -617,7 +659,7 @@
|
|||
bind:optionColors={editableColumn.optionColors}
|
||||
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="label-length">
|
||||
<Label size="M">Earliest</Label>
|
||||
|
@ -704,7 +746,7 @@
|
|||
{tableOptions}
|
||||
{errors}
|
||||
/>
|
||||
{:else if editableColumn.type === FORMULA_TYPE}
|
||||
{:else if editableColumn.type === FieldType.FORMULA}
|
||||
{#if !externalTable}
|
||||
<div class="split-label">
|
||||
<div class="label-length">
|
||||
|
@ -747,12 +789,19 @@
|
|||
/>
|
||||
</div>
|
||||
</div>
|
||||
{:else if editableColumn.type === JSON_TYPE}
|
||||
<Button primary text on:click={openJsonSchemaEditor}
|
||||
>Open schema editor</Button
|
||||
>
|
||||
{:else if editableColumn.type === FieldType.AI}
|
||||
<AIFieldConfiguration
|
||||
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 editableColumn.type === AUTO_TYPE || editableColumn.autocolumn}
|
||||
{#if editableColumn.type === FieldType.AUTO || editableColumn.autocolumn}
|
||||
<Select
|
||||
label="Auto column type"
|
||||
value={editableColumn.subtype}
|
||||
|
@ -779,27 +828,51 @@
|
|||
</div>
|
||||
{/if}
|
||||
|
||||
{#if canHaveDefault}
|
||||
<div>
|
||||
<ModalBindableInput
|
||||
panel={ServerBindingPanel}
|
||||
title="Default"
|
||||
label="Default"
|
||||
{#if defaultValuesEnabled}
|
||||
{#if editableColumn.type === FieldType.OPTIONS}
|
||||
<Select
|
||||
disabled={!canHaveDefault}
|
||||
options={editableColumn.constraints?.inclusion || []}
|
||||
label="Default value"
|
||||
value={editableColumn.default}
|
||||
on:change={e => {
|
||||
editableColumn = {
|
||||
...editableColumn,
|
||||
default: e.detail,
|
||||
}
|
||||
|
||||
if (e.detail) {
|
||||
setRequired(false)
|
||||
}
|
||||
}}
|
||||
on:change={e => (editableColumn.default = e.detail)}
|
||||
placeholder="None"
|
||||
/>
|
||||
{:else if editableColumn.type === FieldType.ARRAY}
|
||||
<Multiselect
|
||||
disabled={!canHaveDefault}
|
||||
options={editableColumn.constraints?.inclusion || []}
|
||||
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}
|
||||
allowJS
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
</Layout>
|
||||
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
import { FIELDS } from "constants/backend"
|
||||
|
||||
const FORMULA_TYPE = FIELDS.FORMULA.type
|
||||
const AI_TYPE = FIELDS.AI.type
|
||||
|
||||
export let row = {}
|
||||
|
||||
|
@ -60,7 +61,7 @@
|
|||
}}
|
||||
>
|
||||
{#each tableSchema as [key, meta]}
|
||||
{#if !meta.autocolumn && meta.type !== FORMULA_TYPE}
|
||||
{#if !meta.autocolumn && meta.type !== FORMULA_TYPE && meta.type !== AI_TYPE}
|
||||
<div>
|
||||
<RowFieldControl error={errors[key]} {meta} bind:value={row[key]} />
|
||||
</div>
|
||||
|
|
|
@ -7,13 +7,9 @@
|
|||
Select,
|
||||
notifications,
|
||||
Body,
|
||||
ModalContent,
|
||||
Tags,
|
||||
Tag,
|
||||
Icon,
|
||||
} from "@budibase/bbui"
|
||||
import { capitalise } from "helpers"
|
||||
import { getFormattedPlanName } from "helpers/planTitle"
|
||||
import { get } from "svelte/store"
|
||||
|
||||
export let resourceId
|
||||
|
@ -21,6 +17,32 @@
|
|||
|
||||
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) {
|
||||
try {
|
||||
if (role === inheritedRoleId) {
|
||||
|
@ -45,38 +67,9 @@
|
|||
}
|
||||
}
|
||||
|
||||
$: computedPermissions = Object.entries(permissions.permissions).reduce(
|
||||
(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() {
|
||||
async function loadDependantInfo(resourceId) {
|
||||
const dependantsInfo = await permissionsStore.getDependantsInfo(resourceId)
|
||||
|
||||
const resourceByType = dependantsInfo?.resourceByType
|
||||
|
||||
if (resourceByType) {
|
||||
const total = Object.values(resourceByType).reduce((p, c) => p + c, 0)
|
||||
let resourceDisplay =
|
||||
|
@ -91,51 +84,35 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
loadDependantInfo()
|
||||
</script>
|
||||
|
||||
<ModalContent showCancelButton={false} showConfirmButton={false}>
|
||||
<span slot="header">
|
||||
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>Role</Label>
|
||||
{#each Object.keys(computedPermissions) as level}
|
||||
<Input value={capitalise(level)} disabled />
|
||||
<Select
|
||||
disabled={requiresPlanToModify}
|
||||
placeholder={false}
|
||||
value={computedPermissions[level].selectedValue}
|
||||
on:change={e => changePermission(level, e.detail)}
|
||||
options={computedPermissions[level].options}
|
||||
getOptionLabel={x => x.name}
|
||||
getOptionValue={x => x._id}
|
||||
/>
|
||||
{/each}
|
||||
</div>
|
||||
<Body size="S">Specify the minimum access level role for this data.</Body>
|
||||
<div class="row">
|
||||
<Label extraSmall grey>Level</Label>
|
||||
<Label extraSmall grey>Role</Label>
|
||||
{#each Object.keys(computedPermissions) as level}
|
||||
<Input value={capitalise(level)} disabled />
|
||||
<Select
|
||||
placeholder={false}
|
||||
value={computedPermissions[level].selectedValue}
|
||||
on:change={e => changePermission(level, e.detail)}
|
||||
options={computedPermissions[level].options}
|
||||
getOptionLabel={x => x.name}
|
||||
getOptionValue={x => x._id}
|
||||
/>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
{#if dependantsInfoMessage}
|
||||
<div class="inheriting-resources">
|
||||
<Icon name="Alert" />
|
||||
<Body size="S">
|
||||
<i>
|
||||
{dependantsInfoMessage}
|
||||
</i>
|
||||
</Body>
|
||||
</div>
|
||||
{/if}
|
||||
</ModalContent>
|
||||
{#if dependantsInfoMessage}
|
||||
<div class="inheriting-resources">
|
||||
<Icon name="Alert" />
|
||||
<Body size="S">
|
||||
<i>
|
||||
{dependantsInfoMessage}
|
||||
</i>
|
||||
</Body>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
.row {
|
||||
|
@ -143,11 +120,6 @@
|
|||
grid-template-columns: 1fr 1fr;
|
||||
grid-gap: var(--spacing-s);
|
||||
}
|
||||
|
||||
.lock-tag {
|
||||
padding-left: var(--spacing-s);
|
||||
}
|
||||
|
||||
.inheriting-resources {
|
||||
display: flex;
|
||||
gap: var(--spacing-s);
|
||||
|
|
|
@ -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>
|
|
@ -39,9 +39,7 @@
|
|||
|
||||
const selectTable = tableId => {
|
||||
tables.select(tableId)
|
||||
if (!$isActive("./table/:tableId")) {
|
||||
$goto(`./table/${tableId}`)
|
||||
}
|
||||
$goto(`./table/${tableId}`)
|
||||
}
|
||||
|
||||
function openNode(datasource) {
|
||||
|
|
|
@ -104,7 +104,7 @@
|
|||
</InlineAlert>
|
||||
</div>
|
||||
{/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} />
|
||||
</div>
|
||||
</ConfirmDialog>
|
||||
|
|
|
@ -20,14 +20,6 @@
|
|||
|
||||
const getContextMenuItems = () => {
|
||||
return [
|
||||
{
|
||||
icon: "Delete",
|
||||
name: "Delete",
|
||||
keyBind: null,
|
||||
visible: true,
|
||||
disabled: false,
|
||||
callback: deleteConfirmationModal.show,
|
||||
},
|
||||
{
|
||||
icon: "Edit",
|
||||
name: "Edit",
|
||||
|
@ -36,6 +28,14 @@
|
|||
disabled: false,
|
||||
callback: editModal.show,
|
||||
},
|
||||
{
|
||||
icon: "Delete",
|
||||
name: "Delete",
|
||||
keyBind: null,
|
||||
visible: true,
|
||||
disabled: false,
|
||||
callback: deleteConfirmationModal.show,
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
|
|
|
@ -1,33 +1,15 @@
|
|||
<script>
|
||||
import { goto } from "@roxi/routify"
|
||||
import TableNavItem from "./TableNavItem/TableNavItem.svelte"
|
||||
import ViewNavItem from "./ViewNavItem/ViewNavItem.svelte"
|
||||
import { alphabetical } from "./utils"
|
||||
|
||||
export let tables
|
||||
export let selectTable
|
||||
|
||||
$: sortedTables = tables.sort(alphabetical)
|
||||
|
||||
const alphabetical = (a, b) => {
|
||||
return a.name?.toLowerCase() > b.name?.toLowerCase() ? 1 : -1
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="hierarchy-items-container">
|
||||
{#each sortedTables as table, idx}
|
||||
<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}
|
||||
</div>
|
||||
|
|
|
@ -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} />
|
|
@ -66,3 +66,7 @@ export const parseFile = e => {
|
|||
reader.readAsText(file)
|
||||
})
|
||||
}
|
||||
|
||||
export const alphabetical = (a, b) => {
|
||||
return a.name?.toLowerCase() > b.name?.toLowerCase() ? 1 : -1
|
||||
}
|
||||
|
|
|
@ -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}
|
|
@ -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>
|
|
@ -9,7 +9,7 @@
|
|||
export let options
|
||||
</script>
|
||||
|
||||
<div class="permissionPicker">
|
||||
<div>
|
||||
{#each options as option}
|
||||
<AbsTooltip text={option.tooltip} type={TooltipType.Info}>
|
||||
<ActionButton
|
||||
|
@ -26,15 +26,14 @@
|
|||
</div>
|
||||
|
||||
<style>
|
||||
.permissionPicker {
|
||||
div {
|
||||
display: flex;
|
||||
gap: var(--spacing-xs);
|
||||
}
|
||||
|
||||
.permissionPicker :global(.spectrum-Icon) {
|
||||
div :global(.spectrum-Icon) {
|
||||
width: 14px;
|
||||
}
|
||||
.permissionPicker :global(.spectrum-ActionButton) {
|
||||
div :global(.spectrum-ActionButton) {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
}
|
|
@ -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>
|
|
@ -33,7 +33,7 @@
|
|||
|
||||
const getSchemaFields = resourceId => {
|
||||
const { schema } = getSchemaForDatasourcePlus(resourceId)
|
||||
return Object.values(schema || {})
|
||||
return Object.values(schema || {}).filter(field => !field.readonly)
|
||||
}
|
||||
|
||||
const onFieldsChanged = e => {
|
||||
|
|
|
@ -25,3 +25,4 @@ export { default as OpenModal } from "./OpenModal.svelte"
|
|||
export { default as CloseModal } from "./CloseModal.svelte"
|
||||
export { default as ClearRowSelection } from "./ClearRowSelection.svelte"
|
||||
export { default as DownloadFile } from "./DownloadFile.svelte"
|
||||
export { default as RowAction } from "./RowAction.svelte"
|
||||
|
|
|
@ -178,6 +178,11 @@
|
|||
"name": "Download File",
|
||||
"type": "data",
|
||||
"component": "DownloadFile"
|
||||
},
|
||||
{
|
||||
"name": "Row Action",
|
||||
"type": "data",
|
||||
"component": "RowAction"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
@ -2,10 +2,11 @@
|
|||
import DraggableList from "../DraggableList/DraggableList.svelte"
|
||||
import ButtonSetting from "./ButtonSetting.svelte"
|
||||
import { createEventDispatcher } from "svelte"
|
||||
import { Helpers } from "@budibase/bbui"
|
||||
import { Helpers, Menu, MenuItem, Popover } from "@budibase/bbui"
|
||||
import { componentStore } from "stores/builder"
|
||||
import { getEventContextBindings } from "dataBinding"
|
||||
import { cloneDeep, isEqual } from "lodash/fp"
|
||||
import { getRowActionButtonTemplates } from "templates/rowActions"
|
||||
|
||||
export let componentInstance
|
||||
export let componentBindings
|
||||
|
@ -17,13 +18,14 @@
|
|||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
let focusItem
|
||||
let cachedValue
|
||||
let rowActionTemplates = []
|
||||
let anchor
|
||||
let popover
|
||||
|
||||
$: if (!isEqual(value, cachedValue)) {
|
||||
cachedValue = cloneDeep(value)
|
||||
}
|
||||
|
||||
$: buttonList = sanitizeValue(cachedValue) || []
|
||||
$: buttonCount = buttonList.length
|
||||
$: eventContextBindings = getEventContextBindings({
|
||||
|
@ -73,17 +75,32 @@
|
|||
_instanceName: Helpers.uuid(),
|
||||
text: cfg.text,
|
||||
type: cfg.type || "primary",
|
||||
},
|
||||
{}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
const addButton = () => {
|
||||
const addCustomButton = () => {
|
||||
const newButton = buildPseudoInstance({
|
||||
text: `Button ${buttonCount + 1}`,
|
||||
})
|
||||
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 => {
|
||||
|
@ -105,12 +122,11 @@
|
|||
listItemKey={"_id"}
|
||||
listType={ButtonSetting}
|
||||
listTypeProps={itemProps}
|
||||
focus={focusItem}
|
||||
draggable={buttonCount > 1}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
<div
|
||||
bind:this={anchor}
|
||||
class="list-footer"
|
||||
class:disabled={!canAddButtons}
|
||||
on:click={addButton}
|
||||
|
@ -120,6 +136,17 @@
|
|||
</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>
|
||||
.button-configuration :global(.spectrum-ActionButton) {
|
||||
width: 100%;
|
||||
|
|
|
@ -1,87 +1,32 @@
|
|||
<script>
|
||||
import DrawerBindableInput from "components/common/bindings/DrawerBindableInput.svelte"
|
||||
import ClientBindingPanel from "components/common/bindings/ClientBindingPanel.svelte"
|
||||
|
||||
import { dataFilters } from "@budibase/shared-core"
|
||||
import { FilterBuilder } from "@budibase/frontend-core"
|
||||
import { CoreFilterBuilder } from "@budibase/frontend-core"
|
||||
import { tables } from "stores/builder"
|
||||
|
||||
import { createEventDispatcher, onMount } from "svelte"
|
||||
import {
|
||||
runtimeToReadableBinding,
|
||||
readableToRuntimeBinding,
|
||||
} from "dataBinding"
|
||||
|
||||
export let schemaFields
|
||||
export let filters = []
|
||||
export let filters
|
||||
export let bindings = []
|
||||
export let panel = ClientBindingPanel
|
||||
export let allowBindings = true
|
||||
export let datasource
|
||||
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>
|
||||
|
||||
<FilterBuilder
|
||||
bind:filters={rawFilters}
|
||||
<CoreFilterBuilder
|
||||
toReadable={runtimeToReadableBinding}
|
||||
toRuntime={readableToRuntimeBinding}
|
||||
behaviourFilters={true}
|
||||
tables={$tables.list}
|
||||
{filters}
|
||||
{panel}
|
||||
{schemaFields}
|
||||
{datasource}
|
||||
{allowBindings}
|
||||
{showFilterEmptyDropdown}
|
||||
>
|
||||
<div slot="filtering-hero-content" />
|
||||
|
||||
<DrawerBindableInput
|
||||
let:filter
|
||||
slot="binding"
|
||||
disabled={filter.noValue}
|
||||
title={filter.field}
|
||||
value={filter.value}
|
||||
placeholder="Value"
|
||||
{panel}
|
||||
{bindings}
|
||||
on:change={event => {
|
||||
const indexToUpdate = rawFilters.findIndex(f => f.id === filter.id)
|
||||
rawFilters[indexToUpdate] = {
|
||||
...rawFilters[indexToUpdate],
|
||||
value: event.detail,
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</FilterBuilder>
|
||||
{bindings}
|
||||
on:change
|
||||
/>
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
Button,
|
||||
Drawer,
|
||||
DrawerContent,
|
||||
Helpers,
|
||||
} from "@budibase/bbui"
|
||||
import { createEventDispatcher } from "svelte"
|
||||
import { getDatasourceForProvider, getSchemaForDatasource } from "dataBinding"
|
||||
|
@ -21,7 +22,7 @@
|
|||
|
||||
let drawer
|
||||
|
||||
$: tempValue = value
|
||||
$: localFilters = Helpers.cloneDeep(value)
|
||||
$: datasource = getDatasourceForProvider($selectedScreen, componentInstance)
|
||||
$: dsSchema = getSchemaForDatasource($selectedScreen, datasource)?.schema
|
||||
$: schemaFields = search.getFields(
|
||||
|
@ -29,19 +30,24 @@
|
|||
Object.values(schema || dsSchema || {}),
|
||||
{ allowLinks: true }
|
||||
)
|
||||
$: text = getText(value?.filter(filter => filter.field))
|
||||
|
||||
$: text = getText(value?.groups)
|
||||
|
||||
async function saveFilter() {
|
||||
dispatch("change", tempValue)
|
||||
dispatch("change", localFilters)
|
||||
notifications.success("Filters saved")
|
||||
drawer.hide()
|
||||
}
|
||||
|
||||
const getText = filters => {
|
||||
if (!filters?.length) {
|
||||
const getText = (filterGroups = []) => {
|
||||
const allFilters = filterGroups.reduce((acc, group) => {
|
||||
return (acc += group.filters.filter(filter => filter.field).length)
|
||||
}, 0)
|
||||
|
||||
if (allFilters === 0) {
|
||||
return "No filters set"
|
||||
} else {
|
||||
return `${filters.length} filter${filters.length === 1 ? "" : "s"} set`
|
||||
return `${allFilters} filter${allFilters === 1 ? "" : "s"} set`
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
@ -49,15 +55,25 @@
|
|||
<div class="filter-editor">
|
||||
<ActionButton on:click={drawer.show}>{text}</ActionButton>
|
||||
</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>
|
||||
<DrawerContent slot="body">
|
||||
<FilterBuilder
|
||||
filters={value}
|
||||
filters={localFilters}
|
||||
{bindings}
|
||||
{schemaFields}
|
||||
{datasource}
|
||||
on:change={e => (tempValue = e.detail)}
|
||||
on:change={e => {
|
||||
localFilters = e.detail
|
||||
}}
|
||||
/>
|
||||
</DrawerContent>
|
||||
</Drawer>
|
||||
|
|
|
@ -54,6 +54,8 @@
|
|||
_instanceName: `Step ${currentStep + 1}`,
|
||||
title: savedInstance.title ?? defaults?.title,
|
||||
buttons: savedInstance.buttons || defaults?.buttons,
|
||||
buttonsCollapsed: savedInstance.buttonsCollapsed,
|
||||
buttonsCollapsedText: savedInstance.buttonsCollapsedText,
|
||||
fields: savedInstance.fields,
|
||||
desc: savedInstance.desc,
|
||||
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
import FieldSetting from "./FieldSetting.svelte"
|
||||
import PrimaryColumnFieldSetting from "./PrimaryColumnFieldSetting.svelte"
|
||||
import getColumns from "./getColumns.js"
|
||||
import InfoDisplay from "pages/builder/app/[application]/design/[screenId]/[componentId]/_components/Component/InfoDisplay.svelte"
|
||||
|
||||
export let value
|
||||
export let componentInstance
|
||||
|
@ -60,16 +61,25 @@
|
|||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
<DraggableList
|
||||
on:change={e => columns.updateSortable(e.detail)}
|
||||
on:itemChange={e => columns.update(e.detail)}
|
||||
items={columns.sortable}
|
||||
listItemKey={"_id"}
|
||||
listType={FieldSetting}
|
||||
listTypeProps={{
|
||||
bindings,
|
||||
}}
|
||||
/>
|
||||
|
||||
{#if columns?.sortable?.length}
|
||||
<DraggableList
|
||||
on:change={e => columns.updateSortable(e.detail)}
|
||||
on:itemChange={e => columns.update(e.detail)}
|
||||
items={columns.sortable}
|
||||
listItemKey={"_id"}
|
||||
listType={FieldSetting}
|
||||
listTypeProps={{
|
||||
bindings,
|
||||
}}
|
||||
/>
|
||||
{:else}
|
||||
<InfoDisplay
|
||||
body={datasource?.type !== "custom"
|
||||
? "No available columns"
|
||||
: "No available columns for JSON/CSV data sources"}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
.right-content {
|
||||
|
|
|
@ -243,11 +243,14 @@
|
|||
{/if}
|
||||
{:else if (type === FieldType.BB_REFERENCE || type === FieldType.BB_REFERENCE_SINGLE) && condition.valueType === type}
|
||||
<FilterUsers
|
||||
bind:value={condition.referenceValue}
|
||||
value={condition.referenceValue}
|
||||
multiselect={[
|
||||
Constants.OperatorOptions.In.value,
|
||||
Constants.OperatorOptions.ContainsAny.value,
|
||||
].includes(condition.operator)}
|
||||
on:change={e => {
|
||||
condition.referenceValue = e.detail
|
||||
}}
|
||||
disabled={condition.noValue}
|
||||
type={condition.valueType}
|
||||
/>
|
||||
|
|
|
@ -6,6 +6,7 @@ export const TriggerStepID = {
|
|||
WEBHOOK: "WEBHOOK",
|
||||
APP: "APP",
|
||||
CRON: "CRON",
|
||||
ROW_ACTION: "ROW_ACTION",
|
||||
}
|
||||
|
||||
export const ActionStepID = {
|
||||
|
|
|
@ -159,6 +159,12 @@ export const FIELDS = {
|
|||
icon: TypeIconMap[FieldType.FORMULA],
|
||||
constraints: {},
|
||||
},
|
||||
AI: {
|
||||
name: "AI",
|
||||
type: FieldType.AI,
|
||||
icon: TypeIconMap[FieldType.AI],
|
||||
constraints: {},
|
||||
},
|
||||
JSON: {
|
||||
name: "JSON",
|
||||
type: FieldType.JSON,
|
||||
|
|
|
@ -72,3 +72,9 @@ export const PlanModel = {
|
|||
}
|
||||
|
||||
export const ChangelogURL = "https://docs.budibase.com/changelog"
|
||||
|
||||
export const AutoScreenTypes = {
|
||||
BLANK: "blank",
|
||||
TABLE: "table",
|
||||
FORM: "form",
|
||||
}
|
||||
|
|
|
@ -213,7 +213,7 @@ export const getComponentText = component => {
|
|||
return component._instanceName
|
||||
}
|
||||
const type =
|
||||
component._component.replace("@budibase/standard-components/", "") ||
|
||||
component._component?.replace("@budibase/standard-components/", "") ||
|
||||
"component"
|
||||
return capitalise(type)
|
||||
}
|
||||
|
|
|
@ -12,7 +12,7 @@
|
|||
stateKey: "selectedViewId",
|
||||
validate: id => $viewsV2.list?.some(view => view.id === id),
|
||||
update: viewsV2.select,
|
||||
fallbackUrl: "../../",
|
||||
fallbackUrl: "../",
|
||||
store: viewsV2,
|
||||
routify,
|
||||
decode: decodeURIComponent,
|
|
@ -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>
|
|
@ -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>
|
|
@ -20,6 +20,7 @@
|
|||
}
|
||||
notifications.success("View deleted")
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
notifications.error("Error deleting view")
|
||||
}
|
||||
}
|
|
@ -39,7 +39,7 @@
|
|||
</script>
|
||||
|
||||
<Modal bind:this={editorModal} on:show={initForm}>
|
||||
<ModalContent title="Edit View" onConfirm={save} confirmText="Save">
|
||||
<Input label="View Name" thin bind:value={updatedName} />
|
||||
<ModalContent title="Edit view" onConfirm={save} confirmText="Save">
|
||||
<Input label="Name" thin bind:value={updatedName} />
|
||||
</ModalContent>
|
||||
</Modal>
|
|
@ -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>
|
|
@ -3,6 +3,7 @@
|
|||
import { tables, builderStore } from "stores/builder"
|
||||
import * as routify from "@roxi/routify"
|
||||
import { onDestroy } from "svelte"
|
||||
import ViewNavBar from "./_components/ViewNavBar.svelte"
|
||||
|
||||
$: tableId = $tables.selectedTableId
|
||||
$: builderStore.selectResource(tableId)
|
||||
|
@ -20,4 +21,17 @@
|
|||
onDestroy(stopSyncing)
|
||||
</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>
|
||||
|
|
|
@ -1,7 +1,92 @@
|
|||
<script>
|
||||
import TableDataTable from "components/backend/DataTable/TableDataTable.svelte"
|
||||
import { tables } from "stores/builder"
|
||||
import { Banner } from "@budibase/bbui"
|
||||
import { Banner, notifications } from "@budibase/bbui"
|
||||
import {
|
||||
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 => {
|
||||
// Check for duplicates
|
||||
|
@ -17,17 +102,6 @@
|
|||
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>
|
||||
|
||||
{#if $tables?.selected?.name}
|
||||
|
@ -40,7 +114,63 @@
|
|||
</Banner>
|
||||
</div>
|
||||
{/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}
|
||||
<i>Create your first table to start building</i>
|
||||
{/if}
|
||||
|
|
|
@ -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)}
|
||||
/>
|
|
@ -1,7 +0,0 @@
|
|||
<script>
|
||||
import { redirect } from "@roxi/routify"
|
||||
|
||||
$redirect("../../")
|
||||
</script>
|
||||
|
||||
<!-- routify:options index=false -->
|
|
@ -1,7 +0,0 @@
|
|||
<script>
|
||||
import { redirect } from "@roxi/routify"
|
||||
|
||||
$redirect("../")
|
||||
</script>
|
||||
|
||||
<!-- routify:options index=false -->
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -1,5 +0,0 @@
|
|||
<script>
|
||||
import ViewV2DataTable from "components/backend/DataTable/ViewV2DataTable.svelte"
|
||||
</script>
|
||||
|
||||
<ViewV2DataTable />
|
|
@ -1,5 +0,0 @@
|
|||
<script>
|
||||
import { redirect } from "@roxi/routify"
|
||||
|
||||
$redirect("../")
|
||||
</script>
|
|
@ -8,6 +8,7 @@
|
|||
import InfoDisplay from "./InfoDisplay.svelte"
|
||||
import analytics, { Events } from "analytics"
|
||||
import { shouldDisplaySetting } from "@budibase/frontend-core"
|
||||
import { getContext, setContext } from "svelte"
|
||||
|
||||
export let componentDefinition
|
||||
export let componentInstance
|
||||
|
@ -19,6 +20,16 @@
|
|||
export let includeHidden = false
|
||||
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(
|
||||
componentInstance,
|
||||
componentDefinition,
|
||||
|
|
|
@ -10,12 +10,16 @@
|
|||
navigationStore,
|
||||
permissions as permissionsStore,
|
||||
builderStore,
|
||||
datasources,
|
||||
appStore,
|
||||
} from "stores/builder"
|
||||
import { auth } from "stores/portal"
|
||||
import { goto } from "@roxi/routify"
|
||||
import { TOUR_KEYS } from "components/portal/onboarding/tours.js"
|
||||
import * as screenTemplating from "templates/screenTemplating"
|
||||
import { Roles } from "constants/backend"
|
||||
import { AutoScreenTypes } from "constants"
|
||||
import { makeTableOption, makeViewOption } from "./utils"
|
||||
|
||||
let mode
|
||||
|
||||
|
@ -23,20 +27,33 @@
|
|||
let datasourceModal
|
||||
let formTypeModal
|
||||
let tableTypeModal
|
||||
|
||||
let selectedTablesAndViews = []
|
||||
let permissions = {}
|
||||
let hasPreselectedDatasource = false
|
||||
|
||||
$: screens = $screenStore.screens
|
||||
|
||||
export const show = newMode => {
|
||||
export const show = (newMode, preselectedDatasource) => {
|
||||
mode = newMode
|
||||
selectedTablesAndViews = []
|
||||
permissions = {}
|
||||
hasPreselectedDatasource = preselectedDatasource != null
|
||||
|
||||
if (mode === "table" || mode === "form") {
|
||||
datasourceModal.show()
|
||||
} else if (mode === "blank") {
|
||||
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()
|
||||
}
|
||||
} else if (mode === AutoScreenTypes.BLANK) {
|
||||
screenDetailsModal.show()
|
||||
} else {
|
||||
throw new Error("Invalid mode provided")
|
||||
|
@ -77,44 +94,49 @@
|
|||
}
|
||||
|
||||
const onSelectDatasources = async () => {
|
||||
if (mode === "form") {
|
||||
if (mode === AutoScreenTypes.FORM) {
|
||||
formTypeModal.show()
|
||||
} else if (mode === "table") {
|
||||
} else if (mode === AutoScreenTypes.TABLE) {
|
||||
tableTypeModal.show()
|
||||
}
|
||||
}
|
||||
|
||||
const createBlankScreen = async ({ route }) => {
|
||||
const screenTemplates = screenTemplating.blank({ route, screens })
|
||||
|
||||
const newScreens = await createScreens(screenTemplates)
|
||||
loadNewScreen(newScreens[0])
|
||||
}
|
||||
|
||||
const createTableScreen = async type => {
|
||||
const screenTemplates = selectedTablesAndViews.flatMap(tableOrView =>
|
||||
screenTemplating.table({
|
||||
screens,
|
||||
tableOrView,
|
||||
type,
|
||||
permissions: permissions[tableOrView.id],
|
||||
})
|
||||
)
|
||||
|
||||
const screenTemplates = (
|
||||
await Promise.all(
|
||||
selectedTablesAndViews.map(tableOrView =>
|
||||
screenTemplating.table({
|
||||
screens,
|
||||
tableOrView,
|
||||
type,
|
||||
permissions: permissions[tableOrView.id],
|
||||
})
|
||||
)
|
||||
)
|
||||
).flat()
|
||||
const newScreens = await createScreens(screenTemplates)
|
||||
loadNewScreen(newScreens[0])
|
||||
}
|
||||
|
||||
const createFormScreen = async type => {
|
||||
const screenTemplates = selectedTablesAndViews.flatMap(tableOrView =>
|
||||
screenTemplating.form({
|
||||
screens,
|
||||
tableOrView,
|
||||
type,
|
||||
permissions: permissions[tableOrView.id],
|
||||
})
|
||||
)
|
||||
|
||||
const screenTemplates = (
|
||||
await Promise.all(
|
||||
selectedTablesAndViews.map(tableOrView =>
|
||||
screenTemplating.form({
|
||||
screens,
|
||||
tableOrView,
|
||||
type,
|
||||
permissions: permissions[tableOrView.id],
|
||||
})
|
||||
)
|
||||
)
|
||||
).flat()
|
||||
const newScreens = await createScreens(screenTemplates)
|
||||
|
||||
if (type === "update" || type === "create") {
|
||||
|
@ -136,9 +158,11 @@
|
|||
if (screen?.props?._children.length) {
|
||||
// Focus on the main component for the screen type
|
||||
const mainComponent = screen?.props?._children?.[0]._id
|
||||
$goto(`./${screen._id}/${mainComponent}`)
|
||||
$goto(
|
||||
`/builder/app/${$appStore.appId}/design/${screen._id}/${mainComponent}`
|
||||
)
|
||||
} else {
|
||||
$goto(`./${screen._id}`)
|
||||
$goto(`/builder/app/${$appStore.appId}/design/${screen._id}`)
|
||||
}
|
||||
|
||||
screenStore.select(screen._id)
|
||||
|
@ -214,6 +238,7 @@
|
|||
tableTypeModal.hide()
|
||||
datasourceModal.show()
|
||||
}}
|
||||
showCancelButton={!hasPreselectedDatasource}
|
||||
/>
|
||||
</Modal>
|
||||
|
||||
|
@ -230,5 +255,6 @@
|
|||
formTypeModal.hide()
|
||||
datasourceModal.show()
|
||||
}}
|
||||
showCancelButton={!hasPreselectedDatasource}
|
||||
/>
|
||||
</Modal>
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
<script>
|
||||
import { Body, ModalContent, Layout, notifications } from "@budibase/bbui"
|
||||
import { Body, ModalContent, Layout } from "@budibase/bbui"
|
||||
import { datasources as datasourcesStore } from "stores/builder"
|
||||
import ICONS from "components/backend/DatasourceNavigator/icons"
|
||||
import { IntegrationNames } from "constants"
|
||||
import { createEventDispatcher, onMount } from "svelte"
|
||||
import { createEventDispatcher } from "svelte"
|
||||
import TableOrViewOption from "./TableOrViewOption.svelte"
|
||||
import * as format from "helpers/data/format"
|
||||
import { makeTableOption, makeViewOption } from "./utils"
|
||||
|
||||
export let onConfirm
|
||||
export let selectedTablesAndViews
|
||||
|
@ -16,17 +16,10 @@
|
|||
const views = Object.values(table.views || {}).filter(
|
||||
view => view.version === 2
|
||||
)
|
||||
|
||||
return views.map(view => ({
|
||||
icon: "Remove",
|
||||
name: view.name,
|
||||
id: view.id,
|
||||
tableSelectFormat: format.tableSelect.viewV2(view),
|
||||
datasourceSelectFormat: format.datasourceSelect.viewV2(view),
|
||||
}))
|
||||
return views.map(makeViewOption)
|
||||
}
|
||||
|
||||
const getTablesAndViews = datasource => {
|
||||
const getTablesAndViews = (datasource, datasources) => {
|
||||
let tablesAndViews = []
|
||||
const tables = Array.isArray(datasource.entities)
|
||||
? datasource.entities
|
||||
|
@ -37,16 +30,7 @@
|
|||
continue
|
||||
}
|
||||
|
||||
const formattedTable = {
|
||||
icon: "Table",
|
||||
name: table.name,
|
||||
id: table._id,
|
||||
tableSelectFormat: format.tableSelect.table(table),
|
||||
datasourceSelectFormat: format.datasourceSelect.table(
|
||||
table,
|
||||
$datasourcesStore.list
|
||||
),
|
||||
}
|
||||
const formattedTable = makeTableOption(table, datasources)
|
||||
|
||||
tablesAndViews = tablesAndViews.concat([
|
||||
formattedTable,
|
||||
|
@ -71,7 +55,7 @@
|
|||
const datasource = {
|
||||
name: rawDatasource.name,
|
||||
iconComponent: ICONS[rawDatasource.source],
|
||||
tablesAndViews: getTablesAndViews(rawDatasource),
|
||||
tablesAndViews: getTablesAndViews(rawDatasource, rawDatasources),
|
||||
}
|
||||
|
||||
datasources.push(datasource)
|
||||
|
@ -85,14 +69,6 @@
|
|||
const toggleSelection = tableOrView => {
|
||||
dispatch("toggle", tableOrView)
|
||||
}
|
||||
|
||||
onMount(async () => {
|
||||
try {
|
||||
await datasourcesStore.fetch()
|
||||
} catch (error) {
|
||||
notifications.error("Error fetching datasources")
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<ModalContent
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
export let types
|
||||
export let onCancel = () => {}
|
||||
export let onConfirm = () => {}
|
||||
export let showCancelButton = true
|
||||
</script>
|
||||
|
||||
<ModalContent
|
||||
|
@ -17,6 +18,7 @@
|
|||
{onCancel}
|
||||
disabled={!selectedType}
|
||||
size="L"
|
||||
{showCancelButton}
|
||||
>
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||
|
@ -65,9 +67,9 @@
|
|||
box-sizing: border-box;
|
||||
padding: var(--spacing-m) var(--spacing-xl);
|
||||
flex-grow: 1;
|
||||
gap: var(--spacing-s);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.image {
|
||||
|
|
|
@ -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),
|
||||
})
|
|
@ -2,9 +2,7 @@ import { writable } from "svelte/store"
|
|||
|
||||
export default class BudiStore {
|
||||
constructor(init, opts) {
|
||||
const store = writable({
|
||||
...init,
|
||||
})
|
||||
const store = writable({ ...init })
|
||||
|
||||
/**
|
||||
* Internal Svelte store
|
||||
|
@ -23,6 +21,7 @@ export default class BudiStore {
|
|||
* *Store modification should be kept to a minimum
|
||||
*/
|
||||
this.update = this.store.update
|
||||
this.set = this.store.set
|
||||
|
||||
/**
|
||||
* Optional debug mode to output the store updates to console
|
||||
|
|
|
@ -6,6 +6,8 @@ import { createHistoryStore } from "stores/builder/history"
|
|||
import { notifications } from "@budibase/bbui"
|
||||
import { updateReferencesInObject } from "dataBinding"
|
||||
import { AutomationTriggerStepId } from "@budibase/types"
|
||||
import { sdk } from "@budibase/shared-core"
|
||||
import { rowActions } from "./rowActions"
|
||||
import {
|
||||
updateBindingsInSteps,
|
||||
getNewStepName,
|
||||
|
@ -127,10 +129,18 @@ const automationActions = store => ({
|
|||
return response.automation
|
||||
},
|
||||
delete: async automation => {
|
||||
await API.deleteAutomation({
|
||||
automationId: automation?._id,
|
||||
automationRev: automation?._rev,
|
||||
})
|
||||
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({
|
||||
automationId: automation?._id,
|
||||
automationRev: automation?._rev,
|
||||
})
|
||||
}
|
||||
|
||||
store.update(state => {
|
||||
// Remove the automation
|
||||
|
|
|
@ -31,6 +31,7 @@ import {
|
|||
import BudiStore from "../BudiStore"
|
||||
import { Utils } from "@budibase/frontend-core"
|
||||
import { FieldType } from "@budibase/types"
|
||||
import { utils } from "@budibase/shared-core"
|
||||
|
||||
export const INITIAL_COMPONENTS_STATE = {
|
||||
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
|
||||
}
|
||||
|
||||
|
@ -405,7 +425,13 @@ export class ComponentStore extends BudiStore {
|
|||
screen: get(selectedScreen),
|
||||
useDefaultValues: true,
|
||||
})
|
||||
this.migrateSettings(instance)
|
||||
|
||||
try {
|
||||
this.migrateSettings(instance)
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
throw e
|
||||
}
|
||||
|
||||
// Custom post processing for creation only
|
||||
let extras = {}
|
||||
|
|
|
@ -29,6 +29,7 @@ import { integrations } from "./integrations"
|
|||
import { sortedIntegrations } from "./sortedIntegrations"
|
||||
import { queries } from "./queries"
|
||||
import { flags } from "./flags"
|
||||
import { rowActions } from "./rowActions"
|
||||
import componentTreeNodesStore from "./componentTreeNodes"
|
||||
|
||||
export {
|
||||
|
@ -65,6 +66,7 @@ export {
|
|||
flags,
|
||||
hoverStore,
|
||||
snippets,
|
||||
rowActions,
|
||||
}
|
||||
|
||||
export const reset = () => {
|
||||
|
@ -74,6 +76,7 @@ export const reset = () => {
|
|||
componentStore.reset()
|
||||
layoutStore.reset()
|
||||
navigationStore.reset()
|
||||
rowActions.reset()
|
||||
}
|
||||
|
||||
const refreshBuilderData = async () => {
|
||||
|
|
|
@ -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
Loading…
Reference in New Issue