Add row actions to tables in data section

This commit is contained in:
Andrew Kingston 2024-09-02 08:45:49 +01:00
parent 9fc837b04a
commit c5e27b860f
No known key found for this signature in database
6 changed files with 93 additions and 29 deletions

View File

@ -15,7 +15,6 @@
let anchor let anchor
let dropdown let dropdown
let timeout let timeout
let open
// This is needed because display: contents is considered "invisible". // This is needed because display: contents is considered "invisible".
// It should only ever be an action button, so should be fine. // It should only ever be an action button, so should be fine.
@ -23,7 +22,7 @@
anchor = node.firstChild anchor = node.firstChild
} }
export const show = () => { export const show = e => {
cancelHide() cancelHide()
dropdown.show() dropdown.show()
} }
@ -64,11 +63,10 @@
on:mouseenter={openOnHover ? show : null} on:mouseenter={openOnHover ? show : null}
on:mouseleave={openOnHover ? queueHide : null} on:mouseleave={openOnHover ? queueHide : null}
> >
<slot name="control" {open} /> <slot name="control" />
</div> </div>
<Popover <Popover
bind:this={dropdown} bind:this={dropdown}
bind:open
{anchor} {anchor}
{align} {align}
{portalTarget} {portalTarget}

View File

@ -169,7 +169,7 @@ export default function positionDropdown(element, opts) {
align === "left-context-menu" align === "left-context-menu"
) { ) {
applyYStrategy(Strategies.StartToStart) applyYStrategy(Strategies.StartToStart)
styles.top -= 4 // Manual adjustment for action menu padding styles.top -= 5 // Manual adjustment for action menu padding
} else { } else {
applyYStrategy(Strategies.StartToEnd) applyYStrategy(Strategies.StartToEnd)
} }

View File

@ -1,5 +1,5 @@
<script> <script>
import { Banner } from "@budibase/bbui" import { Banner, Menu, MenuItem, Popover } from "@budibase/bbui"
import { datasources, tables, integrations, appStore } from "stores/builder" import { datasources, tables, integrations, appStore } from "stores/builder"
import { themeStore, admin } from "stores/portal" import { themeStore, admin } from "stores/portal"
import { TableNames } from "constants" import { TableNames } from "constants"
@ -28,7 +28,12 @@
status: { displayName: "Status", disabled: true }, status: { displayName: "Status", disabled: true },
} }
let rowActions = []
let generateButton let generateButton
let rowActionPopover
let rowActionRow
let rowActionAnchor
let refreshRow
$: autoColumnStatus = verifyAutocolumns($tables?.selected) $: autoColumnStatus = verifyAutocolumns($tables?.selected)
$: duplicates = Object.values(autoColumnStatus).reduce((acc, status) => { $: duplicates = Object.values(autoColumnStatus).reduce((acc, status) => {
@ -53,6 +58,20 @@
$: relationshipsEnabled = relationshipSupport(tableDatasource) $: relationshipsEnabled = relationshipSupport(tableDatasource)
$: currentTheme = $themeStore?.theme $: currentTheme = $themeStore?.theme
$: darkMode = !currentTheme.includes("light") $: darkMode = !currentTheme.includes("light")
$: buttons = [
{
text: "Actions",
type: "cta",
icon: "ChevronDown",
onClick: async (e, row, refresh) => {
rowActionRow = row
rowActionAnchor = e.currentTarget
rowActionPopover.show()
refreshRow = refresh
},
},
]
$: fetchRowActions(id)
const relationshipSupport = datasource => { const relationshipSupport = datasource => {
const integration = $integrations[datasource?.source] const integration = $integrations[datasource?.source]
@ -82,6 +101,25 @@
return acc return acc
}, {}) }, {})
} }
const fetchRowActions = async tableId => {
if (!tableId) {
rowActions = []
return
}
const res = await API.rowActions.fetch(tableId)
rowActions = Object.values(res || {})
}
const runRowAction = async action => {
await API.rowActions.trigger({
rowActionId: action.id,
tableId: id,
rowId: rowActionRow._id,
})
await refreshRow()
rowActionPopover.hide()
}
</script> </script>
{#if $tables?.selected?.name} {#if $tables?.selected?.name}
@ -105,6 +143,7 @@
schemaOverrides={isUsersTable ? userSchemaOverrides : null} schemaOverrides={isUsersTable ? userSchemaOverrides : null}
showAvatars={false} showAvatars={false}
isCloud={$admin.cloud} isCloud={$admin.cloud}
buttons={rowActions.length ? buttons : null}
on:updatedatasource={handleGridTableUpdate} on:updatedatasource={handleGridTableUpdate}
> >
<!-- Controls --> <!-- Controls -->
@ -153,6 +192,19 @@
<i>Create your first table to start building</i> <i>Create your first table to start building</i>
{/if} {/if}
<Popover
bind:this={rowActionPopover}
align="right"
anchor={rowActionAnchor}
offset={5}
>
<Menu>
{#each rowActions as action}
<MenuItem on:click={() => runRowAction(action)}>{action.name}</MenuItem>
{/each}
</Menu>
</Popover>
<style> <style>
i { i {
font-size: var(--font-size-m); font-size: var(--font-size-m);

View File

@ -115,13 +115,15 @@
text: settings.text, text: settings.text,
type: settings.type, type: settings.type,
icon: settings.icon, icon: settings.icon,
onClick: async row => { onClick: async (_, row, refresh) => {
// Create a fake, ephemeral context to run the buttons actions with // Create a fake, ephemeral context to run the buttons actions with
const id = get(component).id const id = get(component).id
const gridContext = createContextStore(context) const gridContext = createContextStore(context)
gridContext.actions.provideData(id, row) gridContext.actions.provideData(id, row)
const fn = enrichButtonActions(settings.onClick, get(gridContext)) const fn = enrichButtonActions(settings.onClick, get(gridContext))
return await fn?.({ row }) const res = await fn?.({ row })
await refresh()
return res
}, },
})) }))
} }

View File

@ -55,9 +55,12 @@ export const buildRowActionEndpoints = API => ({
* @param tableId the ID of the table * @param tableId the ID of the table
* @param rowActionId the ID of the row action to trigger * @param rowActionId the ID of the row action to trigger
*/ */
trigger: async ({ tableId, rowActionId }) => { trigger: async ({ tableId, rowActionId, rowId }) => {
return await API.post({ return await API.post({
url: `/api/tables/${tableId}/actions/${rowActionId}/trigger`, url: `/api/tables/${tableId}/actions/${rowActionId}/trigger`,
body: {
rowId,
},
}) })
}, },
}) })

View File

@ -18,6 +18,7 @@
isDragging, isDragging,
buttonColumnWidth, buttonColumnWidth,
showVScrollbar, showVScrollbar,
showHScrollbar,
dispatch, dispatch,
} = getContext("grid") } = getContext("grid")
@ -32,10 +33,12 @@
$: gridEnd = $width - $buttonColumnWidth - 1 $: gridEnd = $width - $buttonColumnWidth - 1
$: left = Math.min(columnEnd, gridEnd) $: left = Math.min(columnEnd, gridEnd)
const handleClick = async (button, row) => { const handleClick = async (e, button, row) => {
await button.onClick?.(rows.actions.cleanRow(row)) await button.onClick?.(
// Refresh the row in case it changed e,
await rows.actions.refreshRow(row._id) rows.actions.cleanRow(row),
async () => await rows.actions.refreshRow(row._id)
)
} }
onMount(() => { onMount(() => {
@ -72,23 +75,29 @@
highlighted={rowHovered || rowFocused} highlighted={rowHovered || rowFocused}
metadata={row.__metadata?.row} metadata={row.__metadata?.row}
> >
<div class="buttons" class:offset={$showVScrollbar}> <div
class="buttons"
class:offset={$showVScrollbar && $showHScrollbar}
>
{#each buttons as button} {#each buttons as button}
<!-- svelte-ignore a11y-click-events-have-key-events -->
<span on:click={e => handleClick(e, button, row)}>
<Button <Button
newStyles newStyles
size="S" size="S"
icon={button.icon}
cta={button.type === "cta"} cta={button.type === "cta"}
primary={button.type === "primary"} primary={button.type === "primary"}
secondary={button.type === "secondary"} secondary={button.type === "secondary"}
warning={button.type === "warning"} warning={button.type === "warning"}
overBackground={button.type === "overBackground"} overBackground={button.type === "overBackground"}
on:click={() => handleClick(button, row)}
> >
{#if button.icon} {#if button.icon}
<i class="{button.icon} S" /> <i class="{button.icon} S" />
{/if} {/if}
{button.text || "Button"} {button.text || "Button"}
</Button> </Button>
</span>
{/each} {/each}
</div> </div>
</GridCell> </GridCell>