Merge branch 'master' into chore/run_oss_checks

This commit is contained in:
Adria Navarro 2023-11-20 10:08:57 +01:00 committed by GitHub
commit 18b48f2c6b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 307 additions and 27 deletions

View File

@ -21,6 +21,7 @@
export let allowHelpers = true export let allowHelpers = true
export let updateOnChange = true export let updateOnChange = true
export let drawerLeft export let drawerLeft
export let disableBindings = false
const dispatch = createEventDispatcher() const dispatch = createEventDispatcher()
let bindingDrawer let bindingDrawer
@ -62,7 +63,7 @@
{placeholder} {placeholder}
{updateOnChange} {updateOnChange}
/> />
{#if !disabled} {#if !disabled && !disableBindings}
<div <div
class="icon" class="icon"
on:click={() => { on:click={() => {

View File

@ -21,7 +21,8 @@
$: schemaComponents = getContextProviderComponents( $: schemaComponents = getContextProviderComponents(
$currentAsset, $currentAsset,
$store.selectedComponentId, $store.selectedComponentId,
"schema" "schema",
{ includeSelf: nested }
) )
$: providerOptions = getProviderOptions(formComponents, schemaComponents) $: providerOptions = getProviderOptions(formComponents, schemaComponents)
$: schemaFields = getSchemaFields(parameters?.tableId) $: schemaFields = getSchemaFields(parameters?.tableId)

View File

@ -4,10 +4,15 @@
import { createEventDispatcher } from "svelte" import { createEventDispatcher } from "svelte"
import { store } from "builderStore" import { store } from "builderStore"
import { Helpers } from "@budibase/bbui" import { Helpers } from "@budibase/bbui"
import { getEventContextBindings } from "builderStore/dataBinding"
export let componentInstance
export let componentBindings export let componentBindings
export let bindings export let bindings
export let value export let value
export let key
export let nested
export let max
const dispatch = createEventDispatcher() const dispatch = createEventDispatcher()
@ -15,12 +20,18 @@
$: buttonList = sanitizeValue(value) || [] $: buttonList = sanitizeValue(value) || []
$: buttonCount = buttonList.length $: buttonCount = buttonList.length
$: eventContextBindings = getEventContextBindings({
componentInstance,
settingKey: key,
})
$: allBindings = [...bindings, ...eventContextBindings]
$: itemProps = { $: itemProps = {
componentBindings: componentBindings || [], componentBindings: componentBindings || [],
bindings, bindings: allBindings,
removeButton, removeButton,
canRemove: buttonCount > 1, nested,
} }
$: canAddButtons = max == null || buttonList.length < max
const sanitizeValue = val => { const sanitizeValue = val => {
return val?.map(button => { return val?.map(button => {
@ -86,11 +97,16 @@
focus={focusItem} focus={focusItem}
draggable={buttonCount > 1} draggable={buttonCount > 1}
/> />
{/if}
<div class="list-footer" on:click={addButton}> <div
class="list-footer"
class:disabled={!canAddButtons}
on:click={addButton}
class:empty={!buttonCount}
>
<div class="add-button">Add button</div> <div class="add-button">Add button</div>
</div> </div>
{/if}
</div> </div>
<style> <style>
@ -120,15 +136,21 @@
var(--spectrum-table-border-color, var(--spectrum-alias-border-color-mid)); var(--spectrum-table-border-color, var(--spectrum-alias-border-color-mid));
cursor: pointer; cursor: pointer;
} }
.list-footer.empty {
.add-button { border-radius: 4px;
margin: var(--spacing-s); }
.list-footer.disabled {
color: var(--spectrum-global-color-gray-500);
pointer-events: none;
} }
.list-footer:hover { .list-footer:hover {
background-color: var( background-color: var(
--spectrum-table-row-background-color-hover, --spectrum-table-row-background-color-hover,
var(--spectrum-alias-highlight-hover) var(--spectrum-alias-highlight-hover)
); );
} }
.add-button {
margin: var(--spacing-s);
}
</style> </style>

View File

@ -9,11 +9,33 @@
export let bindings export let bindings
export let anchor export let anchor
export let removeButton export let removeButton
export let canRemove export let nested
$: readableText = isJSBinding(item.text) $: readableText = isJSBinding(item.text)
? "(JavaScript function)" ? "(JavaScript function)"
: runtimeToReadableBinding([...bindings, componentBindings], item.text) : runtimeToReadableBinding([...bindings, componentBindings], item.text)
// If this is a nested setting (for example inside a grid or form block) then
// we need to mark all the settings of the actual buttons as nested too, to
// allow us to reference context provided by the block.
// We will need to update this in future if the normal button component
// gets broken into multiple settings sections, as we assume a flat array.
const updatedNestedFlags = settings => {
if (!nested || !settings?.length) {
return settings
}
let newSettings = settings.map(setting => ({
...setting,
nested: true,
}))
// We need to prevent bindings for the button names because of how grid
// blocks work. This is an edge case but unavoidable.
let name = newSettings.find(x => x.key === "text")
if (name) {
name.disableBindings = true
}
return newSettings
}
</script> </script>
<div class="list-item-body"> <div class="list-item-body">
@ -24,12 +46,12 @@
{componentBindings} {componentBindings}
{bindings} {bindings}
on:change on:change
parseSettings={updatedNestedFlags}
/> />
<div class="field-label">{readableText || "Button"}</div> <div class="field-label">{readableText || "Button"}</div>
</div> </div>
<div class="list-item-right"> <div class="list-item-right">
<Icon <Icon
disabled={!canRemove}
size="S" size="S"
name="Close" name="Close"
hoverable hoverable

View File

@ -18,6 +18,7 @@
export let value export let value
const dispatch = createEventDispatcher() const dispatch = createEventDispatcher()
let sanitisedFields let sanitisedFields
let fieldList let fieldList
let schema let schema

View File

@ -23,6 +23,7 @@
export let highlighted = false export let highlighted = false
export let propertyFocus = false export let propertyFocus = false
export let info = null export let info = null
export let disableBindings = false
$: nullishValue = value == null || value === "" $: nullishValue = value == null || value === ""
$: allBindings = getAllBindings(bindings, componentBindings, nested) $: allBindings = getAllBindings(bindings, componentBindings, nested)
@ -99,6 +100,7 @@
{nested} {nested}
{key} {key}
{type} {type}
{disableBindings}
{...props} {...props}
on:drawerHide on:drawerHide
on:drawerShow on:drawerShow

View File

@ -179,6 +179,7 @@
highlighted={$store.highlightedSettingKey === setting.key} highlighted={$store.highlightedSettingKey === setting.key}
propertyFocus={$store.propertyFocus === setting.key} propertyFocus={$store.propertyFocus === setting.key}
info={setting.info} info={setting.info}
disableBindings={setting.disableBindings}
props={{ props={{
// Generic settings // Generic settings
placeholder: setting.placeholder || null, placeholder: setting.placeholder || null,

View File

@ -270,7 +270,6 @@
{ {
"type": "buttonConfiguration", "type": "buttonConfiguration",
"key": "buttons", "key": "buttons",
"nested": true,
"defaultValue": [ "defaultValue": [
{ {
"type": "cta", "type": "cta",
@ -6339,8 +6338,29 @@
"label": "High contrast", "label": "High contrast",
"key": "stripeRows", "key": "stripeRows",
"defaultValue": false "defaultValue": false
},
{
"section": true,
"name": "Buttons",
"settings": [
{
"type": "buttonConfiguration",
"key": "buttons",
"nested": true,
"max": 3,
"context": [
{
"label": "Clicked row",
"key": "row"
} }
] ]
}
]
}
],
"context": {
"type": "schema"
}
}, },
"bbreferencefield": { "bbreferencefield": {
"devComment": "As bb reference is only used for user subtype for now, we are using user for icon and labels", "devComment": "As bb reference is only used for user subtype for now, we are using user for icon and labels",

View File

@ -2,6 +2,7 @@
// NOTE: this is not a block - it's just named as such to avoid confusing users, // NOTE: this is not a block - it's just named as such to avoid confusing users,
// because it functions similarly to one // because it functions similarly to one
import { getContext } from "svelte" import { getContext } from "svelte"
import { get } from "svelte/store"
import { Grid } from "@budibase/frontend-core" import { Grid } from "@budibase/frontend-core"
// table is actually any datasource, but called table for legacy compatibility // table is actually any datasource, but called table for legacy compatibility
@ -16,12 +17,21 @@
export let fixedRowHeight = null export let fixedRowHeight = null
export let columns = null export let columns = null
export let onRowClick = null export let onRowClick = null
export let buttons = null
const context = getContext("context")
const component = getContext("component") const component = getContext("component")
const { styleable, API, builderStore, notificationStore } = getContext("sdk") const {
styleable,
API,
builderStore,
notificationStore,
enrichButtonActions,
} = getContext("sdk")
$: columnWhitelist = columns?.map(col => col.name) $: columnWhitelist = columns?.map(col => col.name)
$: schemaOverrides = getSchemaOverrides(columns) $: schemaOverrides = getSchemaOverrides(columns)
$: enrichedButtons = enrichButtons(buttons)
const getSchemaOverrides = columns => { const getSchemaOverrides = columns => {
let overrides = {} let overrides = {}
@ -33,6 +43,25 @@
}) })
return overrides return overrides
} }
const enrichButtons = buttons => {
if (!buttons?.length) {
return null
}
return buttons.map(settings => ({
size: "M",
text: settings.text,
type: settings.type,
onClick: async row => {
// We add a fake context binding in here, which allows us to pretend
// that the grid provides a "schema" binding - that lets us use the
// clicked row in things like save row actions
const enrichedContext = { ...get(context), [get(component).id]: row }
const fn = enrichButtonActions(settings.onClick, enrichedContext)
return await fn?.({ row })
},
}))
}
</script> </script>
<div <div
@ -58,6 +87,7 @@
showControls={false} showControls={false}
notifySuccess={notificationStore.actions.success} notifySuccess={notificationStore.actions.success}
notifyError={notificationStore.actions.error} notifyError={notificationStore.actions.error}
buttons={enrichedButtons}
on:rowclick={e => onRowClick?.({ row: e.detail })} on:rowclick={e => onRowClick?.({ row: e.detail })}
/> />
</div> </div>

View File

@ -14,6 +14,7 @@ import {
dndIsDragging, dndIsDragging,
confirmationStore, confirmationStore,
roleStore, roleStore,
stateStore,
} from "stores" } from "stores"
import { styleable } from "utils/styleable" import { styleable } from "utils/styleable"
import { linkable } from "utils/linkable" import { linkable } from "utils/linkable"
@ -24,9 +25,13 @@ import BlockComponent from "components/BlockComponent.svelte"
import { ActionTypes } from "./constants" import { ActionTypes } from "./constants"
import { fetchDatasourceSchema } from "./utils/schema.js" import { fetchDatasourceSchema } from "./utils/schema.js"
import { getAPIKey } from "./utils/api.js" import { getAPIKey } from "./utils/api.js"
import { enrichButtonActions } from "./utils/buttonActions.js"
import { processStringSync, makePropSafe } from "@budibase/string-templates"
export default { export default {
API, API,
// Stores
authStore, authStore,
notificationStore, notificationStore,
routeStore, routeStore,
@ -41,13 +46,23 @@ export default {
currentRole, currentRole,
confirmationStore, confirmationStore,
roleStore, roleStore,
stateStore,
// Utils
styleable, styleable,
linkable, linkable,
getAction, getAction,
fetchDatasourceSchema, fetchDatasourceSchema,
Provider,
ActionTypes,
getAPIKey, getAPIKey,
enrichButtonActions,
processStringSync,
makePropSafe,
// Components
Provider,
Block, Block,
BlockComponent, BlockComponent,
// Constants
ActionTypes,
} }

View File

@ -15,7 +15,7 @@
$: style = getStyle(width, selectedUser) $: style = getStyle(width, selectedUser)
const getStyle = (width, selectedUser) => { const getStyle = (width, selectedUser) => {
let style = `flex: 0 0 ${width}px;` let style = width === "auto" ? "width: auto;" : `flex: 0 0 ${width}px;`
if (selectedUser) { if (selectedUser) {
style += `--user-color:${selectedUser.color};` style += `--user-color:${selectedUser.color};`
} }

View File

@ -0,0 +1,144 @@
<script>
import { getContext, onMount } from "svelte"
import { Button } from "@budibase/bbui"
import GridCell from "../cells/GridCell.svelte"
import GridScrollWrapper from "./GridScrollWrapper.svelte"
const {
renderedRows,
hoveredRowId,
props,
width,
rows,
focusedRow,
selectedRows,
visibleColumns,
scroll,
isDragging,
buttonColumnWidth,
} = getContext("grid")
let measureContainer
$: buttons = $props.buttons?.slice(0, 3) || []
$: columnsWidth = $visibleColumns.reduce(
(total, col) => (total += col.width),
0
)
$: end = columnsWidth - 1 - $scroll.left
$: left = Math.min($width - $buttonColumnWidth, end)
const handleClick = async (button, row) => {
await button.onClick?.(rows.actions.cleanRow(row))
// Refresh the row in case it changed
await rows.actions.refreshRow(row._id)
}
onMount(() => {
const observer = new ResizeObserver(entries => {
const width = entries?.[0]?.contentRect?.width ?? 0
buttonColumnWidth.set(width)
})
observer.observe(measureContainer)
})
</script>
<!-- Hidden copy of buttons to measure -->
<div class="measure" bind:this={measureContainer}>
<GridCell width="auto">
<div class="buttons">
{#each buttons as button}
<Button size="S">
{button.text || "Button"}
</Button>
{/each}
</div>
</GridCell>
</div>
<div
class="button-column"
style="left:{left}px"
class:hidden={$buttonColumnWidth === 0}
>
<div class="content" on:mouseleave={() => ($hoveredRowId = null)}>
<GridScrollWrapper scrollVertically attachHandlers>
{#each $renderedRows as row}
{@const rowSelected = !!$selectedRows[row._id]}
{@const rowHovered = $hoveredRowId === row._id}
{@const rowFocused = $focusedRow?._id === row._id}
<div
class="row"
on:mouseenter={$isDragging ? null : () => ($hoveredRowId = row._id)}
on:mouseleave={$isDragging ? null : () => ($hoveredRowId = null)}
>
<GridCell
width="auto"
rowIdx={row.__idx}
selected={rowSelected}
highlighted={rowHovered || rowFocused}
>
<div class="buttons">
{#each buttons as button}
<Button
newStyles
size="S"
cta={button.type === "cta"}
primary={button.type === "primary"}
secondary={button.type === "secondary"}
warning={button.type === "warning"}
overBackground={button.type === "overBackground"}
on:click={() => handleClick(button, row)}
>
{button.text || "Button"}
</Button>
{/each}
</div>
</GridCell>
</div>
{/each}
</GridScrollWrapper>
</div>
</div>
<style>
.button-column {
display: flex;
flex-direction: column;
background: var(--cell-background);
position: absolute;
top: 0;
}
.button-column.hidden {
opacity: 0;
}
.content {
position: relative;
flex: 1 1 auto;
}
.row {
display: flex;
flex-direction: row;
justify-content: flex-start;
align-items: stretch;
}
.buttons {
display: flex;
align-items: center;
padding: 0 var(--cell-padding);
gap: var(--cell-padding);
height: inherit;
}
/* Add left cell border */
.button-column :global(.cell) {
border-left: var(--cell-border);
}
/* Hidden copy of buttons to measure width against */
.measure {
position: absolute;
opacity: 0;
pointer-events: none;
}
</style>

View File

@ -48,6 +48,7 @@
export let fixedRowHeight = null export let fixedRowHeight = null
export let notifySuccess = null export let notifySuccess = null
export let notifyError = null export let notifyError = null
export let buttons = null
// Unique identifier for DOM nodes inside this instance // Unique identifier for DOM nodes inside this instance
const rand = Math.random() const rand = Math.random()
@ -99,6 +100,7 @@
fixedRowHeight, fixedRowHeight,
notifySuccess, notifySuccess,
notifyError, notifyError,
buttons,
}) })
// Set context for children to consume // Set context for children to consume

View File

@ -3,6 +3,7 @@
import GridScrollWrapper from "./GridScrollWrapper.svelte" import GridScrollWrapper from "./GridScrollWrapper.svelte"
import GridRow from "./GridRow.svelte" import GridRow from "./GridRow.svelte"
import { BlankRowID } from "../lib/constants" import { BlankRowID } from "../lib/constants"
import ButtonColumn from "./ButtonColumn.svelte"
const { const {
bounds, bounds,
@ -13,6 +14,7 @@
dispatch, dispatch,
isDragging, isDragging,
config, config,
props,
} = getContext("grid") } = getContext("grid")
let body let body
@ -54,6 +56,9 @@
/> />
{/if} {/if}
</GridScrollWrapper> </GridScrollWrapper>
{#if $props.buttons?.length}
<ButtonColumn />
{/if}
</div> </div>
<style> <style>

View File

@ -314,8 +314,12 @@ export const createActions = context => {
// Refreshes a specific row // Refreshes a specific row
const refreshRow = async id => { const refreshRow = async id => {
try {
const row = await datasource.actions.getRow(id) const row = await datasource.actions.getRow(id)
replaceRow(id, row) replaceRow(id, row)
} catch {
// Do nothing - we probably just don't support refreshing individual rows
}
} }
// Refreshes all data // Refreshes all data

View File

@ -20,8 +20,15 @@ export const createStores = () => {
} }
export const deriveStores = context => { export const deriveStores = context => {
const { rows, visibleColumns, stickyColumn, rowHeight, width, height } = const {
context rows,
visibleColumns,
stickyColumn,
rowHeight,
width,
height,
buttonColumnWidth,
} = context
// Memoize store primitives // Memoize store primitives
const stickyColumnWidth = derived(stickyColumn, $col => $col?.width || 0, 0) const stickyColumnWidth = derived(stickyColumn, $col => $col?.width || 0, 0)
@ -40,9 +47,10 @@ export const deriveStores = context => {
// Derive horizontal limits // Derive horizontal limits
const contentWidth = derived( const contentWidth = derived(
[visibleColumns, stickyColumnWidth], [visibleColumns, stickyColumnWidth, buttonColumnWidth],
([$visibleColumns, $stickyColumnWidth]) => { ([$visibleColumns, $stickyColumnWidth, $buttonColumnWidth]) => {
let width = GutterWidth + Padding + $stickyColumnWidth const space = Math.max(Padding, $buttonColumnWidth - 1)
let width = GutterWidth + space + $stickyColumnWidth
$visibleColumns.forEach(col => { $visibleColumns.forEach(col => {
width += col.width width += col.width
}) })

View File

@ -18,6 +18,7 @@ export const createStores = context => {
const previousFocusedRowId = writable(null) const previousFocusedRowId = writable(null)
const gridFocused = writable(false) const gridFocused = writable(false)
const isDragging = writable(false) const isDragging = writable(false)
const buttonColumnWidth = writable(0)
// Derive the current focused row ID // Derive the current focused row ID
const focusedRowId = derived( const focusedRowId = derived(
@ -51,6 +52,7 @@ export const createStores = context => {
rowHeight, rowHeight,
gridFocused, gridFocused,
isDragging, isDragging,
buttonColumnWidth,
selectedRows: { selectedRows: {
...selectedRows, ...selectedRows,
actions: { actions: {