Merge branch 'master' into chore/run_oss_checks
This commit is contained in:
commit
18b48f2c6b
|
@ -21,6 +21,7 @@
|
|||
export let allowHelpers = true
|
||||
export let updateOnChange = true
|
||||
export let drawerLeft
|
||||
export let disableBindings = false
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
let bindingDrawer
|
||||
|
@ -62,7 +63,7 @@
|
|||
{placeholder}
|
||||
{updateOnChange}
|
||||
/>
|
||||
{#if !disabled}
|
||||
{#if !disabled && !disableBindings}
|
||||
<div
|
||||
class="icon"
|
||||
on:click={() => {
|
||||
|
|
|
@ -21,7 +21,8 @@
|
|||
$: schemaComponents = getContextProviderComponents(
|
||||
$currentAsset,
|
||||
$store.selectedComponentId,
|
||||
"schema"
|
||||
"schema",
|
||||
{ includeSelf: nested }
|
||||
)
|
||||
$: providerOptions = getProviderOptions(formComponents, schemaComponents)
|
||||
$: schemaFields = getSchemaFields(parameters?.tableId)
|
||||
|
|
|
@ -4,10 +4,15 @@
|
|||
import { createEventDispatcher } from "svelte"
|
||||
import { store } from "builderStore"
|
||||
import { Helpers } from "@budibase/bbui"
|
||||
import { getEventContextBindings } from "builderStore/dataBinding"
|
||||
|
||||
export let componentInstance
|
||||
export let componentBindings
|
||||
export let bindings
|
||||
export let value
|
||||
export let key
|
||||
export let nested
|
||||
export let max
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
|
@ -15,12 +20,18 @@
|
|||
|
||||
$: buttonList = sanitizeValue(value) || []
|
||||
$: buttonCount = buttonList.length
|
||||
$: eventContextBindings = getEventContextBindings({
|
||||
componentInstance,
|
||||
settingKey: key,
|
||||
})
|
||||
$: allBindings = [...bindings, ...eventContextBindings]
|
||||
$: itemProps = {
|
||||
componentBindings: componentBindings || [],
|
||||
bindings,
|
||||
bindings: allBindings,
|
||||
removeButton,
|
||||
canRemove: buttonCount > 1,
|
||||
nested,
|
||||
}
|
||||
$: canAddButtons = max == null || buttonList.length < max
|
||||
|
||||
const sanitizeValue = val => {
|
||||
return val?.map(button => {
|
||||
|
@ -86,11 +97,16 @@
|
|||
focus={focusItem}
|
||||
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>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
|
@ -120,15 +136,21 @@
|
|||
var(--spectrum-table-border-color, var(--spectrum-alias-border-color-mid));
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.add-button {
|
||||
margin: var(--spacing-s);
|
||||
.list-footer.empty {
|
||||
border-radius: 4px;
|
||||
}
|
||||
.list-footer.disabled {
|
||||
color: var(--spectrum-global-color-gray-500);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.list-footer:hover {
|
||||
background-color: var(
|
||||
--spectrum-table-row-background-color-hover,
|
||||
var(--spectrum-alias-highlight-hover)
|
||||
);
|
||||
}
|
||||
|
||||
.add-button {
|
||||
margin: var(--spacing-s);
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -9,11 +9,33 @@
|
|||
export let bindings
|
||||
export let anchor
|
||||
export let removeButton
|
||||
export let canRemove
|
||||
export let nested
|
||||
|
||||
$: readableText = isJSBinding(item.text)
|
||||
? "(JavaScript function)"
|
||||
: 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>
|
||||
|
||||
<div class="list-item-body">
|
||||
|
@ -24,12 +46,12 @@
|
|||
{componentBindings}
|
||||
{bindings}
|
||||
on:change
|
||||
parseSettings={updatedNestedFlags}
|
||||
/>
|
||||
<div class="field-label">{readableText || "Button"}</div>
|
||||
</div>
|
||||
<div class="list-item-right">
|
||||
<Icon
|
||||
disabled={!canRemove}
|
||||
size="S"
|
||||
name="Close"
|
||||
hoverable
|
||||
|
|
|
@ -18,6 +18,7 @@
|
|||
export let value
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
let sanitisedFields
|
||||
let fieldList
|
||||
let schema
|
||||
|
|
|
@ -23,6 +23,7 @@
|
|||
export let highlighted = false
|
||||
export let propertyFocus = false
|
||||
export let info = null
|
||||
export let disableBindings = false
|
||||
|
||||
$: nullishValue = value == null || value === ""
|
||||
$: allBindings = getAllBindings(bindings, componentBindings, nested)
|
||||
|
@ -99,6 +100,7 @@
|
|||
{nested}
|
||||
{key}
|
||||
{type}
|
||||
{disableBindings}
|
||||
{...props}
|
||||
on:drawerHide
|
||||
on:drawerShow
|
||||
|
|
|
@ -179,6 +179,7 @@
|
|||
highlighted={$store.highlightedSettingKey === setting.key}
|
||||
propertyFocus={$store.propertyFocus === setting.key}
|
||||
info={setting.info}
|
||||
disableBindings={setting.disableBindings}
|
||||
props={{
|
||||
// Generic settings
|
||||
placeholder: setting.placeholder || null,
|
||||
|
|
|
@ -270,7 +270,6 @@
|
|||
{
|
||||
"type": "buttonConfiguration",
|
||||
"key": "buttons",
|
||||
"nested": true,
|
||||
"defaultValue": [
|
||||
{
|
||||
"type": "cta",
|
||||
|
@ -6339,8 +6338,29 @@
|
|||
"label": "High contrast",
|
||||
"key": "stripeRows",
|
||||
"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": {
|
||||
"devComment": "As bb reference is only used for user subtype for now, we are using user for icon and labels",
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
// NOTE: this is not a block - it's just named as such to avoid confusing users,
|
||||
// because it functions similarly to one
|
||||
import { getContext } from "svelte"
|
||||
import { get } from "svelte/store"
|
||||
import { Grid } from "@budibase/frontend-core"
|
||||
|
||||
// table is actually any datasource, but called table for legacy compatibility
|
||||
|
@ -16,12 +17,21 @@
|
|||
export let fixedRowHeight = null
|
||||
export let columns = null
|
||||
export let onRowClick = null
|
||||
export let buttons = null
|
||||
|
||||
const context = getContext("context")
|
||||
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)
|
||||
$: schemaOverrides = getSchemaOverrides(columns)
|
||||
$: enrichedButtons = enrichButtons(buttons)
|
||||
|
||||
const getSchemaOverrides = columns => {
|
||||
let overrides = {}
|
||||
|
@ -33,6 +43,25 @@
|
|||
})
|
||||
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>
|
||||
|
||||
<div
|
||||
|
@ -58,6 +87,7 @@
|
|||
showControls={false}
|
||||
notifySuccess={notificationStore.actions.success}
|
||||
notifyError={notificationStore.actions.error}
|
||||
buttons={enrichedButtons}
|
||||
on:rowclick={e => onRowClick?.({ row: e.detail })}
|
||||
/>
|
||||
</div>
|
||||
|
|
|
@ -14,6 +14,7 @@ import {
|
|||
dndIsDragging,
|
||||
confirmationStore,
|
||||
roleStore,
|
||||
stateStore,
|
||||
} from "stores"
|
||||
import { styleable } from "utils/styleable"
|
||||
import { linkable } from "utils/linkable"
|
||||
|
@ -24,9 +25,13 @@ import BlockComponent from "components/BlockComponent.svelte"
|
|||
import { ActionTypes } from "./constants"
|
||||
import { fetchDatasourceSchema } from "./utils/schema.js"
|
||||
import { getAPIKey } from "./utils/api.js"
|
||||
import { enrichButtonActions } from "./utils/buttonActions.js"
|
||||
import { processStringSync, makePropSafe } from "@budibase/string-templates"
|
||||
|
||||
export default {
|
||||
API,
|
||||
|
||||
// Stores
|
||||
authStore,
|
||||
notificationStore,
|
||||
routeStore,
|
||||
|
@ -41,13 +46,23 @@ export default {
|
|||
currentRole,
|
||||
confirmationStore,
|
||||
roleStore,
|
||||
stateStore,
|
||||
|
||||
// Utils
|
||||
styleable,
|
||||
linkable,
|
||||
getAction,
|
||||
fetchDatasourceSchema,
|
||||
Provider,
|
||||
ActionTypes,
|
||||
getAPIKey,
|
||||
enrichButtonActions,
|
||||
processStringSync,
|
||||
makePropSafe,
|
||||
|
||||
// Components
|
||||
Provider,
|
||||
Block,
|
||||
BlockComponent,
|
||||
|
||||
// Constants
|
||||
ActionTypes,
|
||||
}
|
||||
|
|
|
@ -15,7 +15,7 @@
|
|||
$: style = 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) {
|
||||
style += `--user-color:${selectedUser.color};`
|
||||
}
|
||||
|
|
|
@ -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>
|
|
@ -48,6 +48,7 @@
|
|||
export let fixedRowHeight = null
|
||||
export let notifySuccess = null
|
||||
export let notifyError = null
|
||||
export let buttons = null
|
||||
|
||||
// Unique identifier for DOM nodes inside this instance
|
||||
const rand = Math.random()
|
||||
|
@ -99,6 +100,7 @@
|
|||
fixedRowHeight,
|
||||
notifySuccess,
|
||||
notifyError,
|
||||
buttons,
|
||||
})
|
||||
|
||||
// Set context for children to consume
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
import GridScrollWrapper from "./GridScrollWrapper.svelte"
|
||||
import GridRow from "./GridRow.svelte"
|
||||
import { BlankRowID } from "../lib/constants"
|
||||
import ButtonColumn from "./ButtonColumn.svelte"
|
||||
|
||||
const {
|
||||
bounds,
|
||||
|
@ -13,6 +14,7 @@
|
|||
dispatch,
|
||||
isDragging,
|
||||
config,
|
||||
props,
|
||||
} = getContext("grid")
|
||||
|
||||
let body
|
||||
|
@ -54,6 +56,9 @@
|
|||
/>
|
||||
{/if}
|
||||
</GridScrollWrapper>
|
||||
{#if $props.buttons?.length}
|
||||
<ButtonColumn />
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
|
|
|
@ -314,8 +314,12 @@ export const createActions = context => {
|
|||
|
||||
// Refreshes a specific row
|
||||
const refreshRow = async id => {
|
||||
try {
|
||||
const row = await datasource.actions.getRow(id)
|
||||
replaceRow(id, row)
|
||||
} catch {
|
||||
// Do nothing - we probably just don't support refreshing individual rows
|
||||
}
|
||||
}
|
||||
|
||||
// Refreshes all data
|
||||
|
|
|
@ -20,8 +20,15 @@ export const createStores = () => {
|
|||
}
|
||||
|
||||
export const deriveStores = context => {
|
||||
const { rows, visibleColumns, stickyColumn, rowHeight, width, height } =
|
||||
context
|
||||
const {
|
||||
rows,
|
||||
visibleColumns,
|
||||
stickyColumn,
|
||||
rowHeight,
|
||||
width,
|
||||
height,
|
||||
buttonColumnWidth,
|
||||
} = context
|
||||
|
||||
// Memoize store primitives
|
||||
const stickyColumnWidth = derived(stickyColumn, $col => $col?.width || 0, 0)
|
||||
|
@ -40,9 +47,10 @@ export const deriveStores = context => {
|
|||
|
||||
// Derive horizontal limits
|
||||
const contentWidth = derived(
|
||||
[visibleColumns, stickyColumnWidth],
|
||||
([$visibleColumns, $stickyColumnWidth]) => {
|
||||
let width = GutterWidth + Padding + $stickyColumnWidth
|
||||
[visibleColumns, stickyColumnWidth, buttonColumnWidth],
|
||||
([$visibleColumns, $stickyColumnWidth, $buttonColumnWidth]) => {
|
||||
const space = Math.max(Padding, $buttonColumnWidth - 1)
|
||||
let width = GutterWidth + space + $stickyColumnWidth
|
||||
$visibleColumns.forEach(col => {
|
||||
width += col.width
|
||||
})
|
||||
|
|
|
@ -18,6 +18,7 @@ export const createStores = context => {
|
|||
const previousFocusedRowId = writable(null)
|
||||
const gridFocused = writable(false)
|
||||
const isDragging = writable(false)
|
||||
const buttonColumnWidth = writable(0)
|
||||
|
||||
// Derive the current focused row ID
|
||||
const focusedRowId = derived(
|
||||
|
@ -51,6 +52,7 @@ export const createStores = context => {
|
|||
rowHeight,
|
||||
gridFocused,
|
||||
isDragging,
|
||||
buttonColumnWidth,
|
||||
selectedRows: {
|
||||
...selectedRows,
|
||||
actions: {
|
||||
|
|
Loading…
Reference in New Issue