Merge v3-ui.

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

View File

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

View File

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

View File

@ -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) => {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -39,6 +39,7 @@ export { default as ActionGroup } from "./ActionGroup/ActionGroup.svelte"
export { default as ActionMenu } from "./ActionMenu/ActionMenu.svelte"
export { default as 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"

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -62,6 +62,7 @@
} from "@budibase/types"
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 }}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,13 +1,15 @@
<script>
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>

View File

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

View File

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

View File

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

View File

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

View File

@ -1,8 +1,17 @@
<script context="module">
export const FieldPermissions = {
WRITABLE: "writable",
READONLY: "readonly",
HIDDEN: "hidden",
}
</script>
<script>
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>

View File

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

View File

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

View File

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

View File

@ -4,14 +4,8 @@
const { datasource } = getContext("grid")
$: 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} />

View File

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

View File

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

View File

@ -1,34 +1,34 @@
<script>
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,
},
]

View File

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

After

Width:  |  Height:  |  Size: 2.5 KiB

View File

@ -4,6 +4,7 @@
Button,
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>

View File

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

View File

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

View File

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

View File

@ -39,9 +39,7 @@
const selectTable = tableId => {
tables.select(tableId)
if (!$isActive("./table/:tableId")) {
$goto(`./table/${tableId}`)
}
$goto(`./table/${tableId}`)
}
function openNode(datasource) {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -9,7 +9,7 @@
export let options
</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;
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -3,6 +3,7 @@
import { tables, builderStore } from "stores/builder"
import * 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>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -8,6 +8,7 @@
import InfoDisplay from "./InfoDisplay.svelte"
import 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,

View File

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

View File

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

View File

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

View File

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

View File

@ -2,9 +2,7 @@ import { writable } from "svelte/store"
export default class BudiStore {
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

View File

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

View File

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

View File

@ -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 () => {

View File

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

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