Add majority of frontend implementation of row actions

This commit is contained in:
Andrew Kingston 2024-08-21 16:33:51 +01:00
parent 1991610b47
commit c7c6597424
No known key found for this signature in database
12 changed files with 311 additions and 62 deletions

View File

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

View File

@ -1,55 +1,50 @@
<script> <script>
import Body from "../Typography/Body.svelte" import Icon from "../Icon/Icon.svelte"
import IconAvatar from "../Icon/IconAvatar.svelte"
import Label from "../Label/Label.svelte"
import Avatar from "../Avatar/Avatar.svelte"
export let icon = null export let icon = null
export let iconBackground = null
export let iconColor = null export let iconColor = null
export let avatar = false
export let title = null export let title = null
export let subtitle = null export let subtitle = null
export let hoverable = false export let url = null
$: initials = avatar ? title?.[0] : null
</script> </script>
<!-- svelte-ignore a11y-no-static-element-interactions --> <!-- svelte-ignore a11y-no-static-element-interactions -->
<!-- svelte-ignore a11y-click-events-have-key-events --> <!-- svelte-ignore a11y-click-events-have-key-events -->
<div class="list-item" class:hoverable on:click> <a href={url} class="list-item" class:hoverable={url != null} on:click>
<div class="left"> <div class="left">
{#if icon} {#if icon}
<IconAvatar {icon} color={iconColor} background={iconBackground} /> <Icon name={icon} color={iconColor} />
{/if} {/if}
{#if avatar} <div class="list-item__text">
<Avatar {initials} /> {#if title}
{/if} <div class="list-item__title">
{#if title} {title}
<Body>{title}</Body> </div>
{/if} {/if}
{#if subtitle} {#if subtitle}
<Label>{subtitle}</Label> <div class="list-item__subtitle">
{/if} {subtitle}
</div> </div>
{#if $$slots.default} {/if}
<div class="right">
<slot />
</div> </div>
{/if} </div>
</div> <div class="right">
<slot name="right" />
<Icon name="ChevronRight" />
</div>
</a>
<style> <style>
.list-item { .list-item {
padding: 0 16px; padding: var(--spacing-m);
height: 56px; background: var(--spectrum-global-color-gray-75);
background: var(--spectrum-global-color-gray-50);
display: flex; display: flex;
flex-direction: row; flex-direction: row;
justify-content: space-between; justify-content: space-between;
border: 1px solid var(--spectrum-global-color-gray-300); border: 1px solid var(--spectrum-global-color-gray-300);
transition: background 130ms ease-out; transition: background 130ms ease-out;
gap: var(--spacing-m); gap: var(--spacing-m);
color: var(--spectrum-global-color-gray-800);
} }
.list-item:not(:first-child) { .list-item:not(:first-child) {
border-top: none; border-top: none;
@ -64,14 +59,15 @@
} }
.hoverable:hover { .hoverable:hover {
cursor: pointer; cursor: pointer;
background: var(--spectrum-global-color-gray-75); background: var(--spectrum-global-color-gray-200);
} }
.left, .left,
.right { .right {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
align-items: center; align-items: center;
gap: var(--spacing-xl); gap: var(--spacing-l);
} }
.left { .left {
width: 0; width: 0;
@ -79,17 +75,20 @@
} }
.right { .right {
flex: 0 0 auto; flex: 0 0 auto;
color: var(--spectrum-global-color-gray-600);
} }
.list-item :global(.spectrum-Icon),
.list-item :global(.spectrum-Avatar) { .list-item__text {
flex: 0 0 auto; flex: 1 1 auto;
width: 0;
} }
.list-item :global(.spectrum-Body) { .list-item__title,
color: var(--spectrum-global-color-gray-900); .list-item__subtitle {
}
.list-item :global(.spectrum-Body) {
white-space: nowrap; white-space: nowrap;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
} }
.list-item__subtitle {
color: var(--spectrum-global-color-gray-600);
}
</style> </style>

View File

@ -1,5 +1,5 @@
<script> <script>
import { ActionButton, Modal } from "@budibase/bbui" import { ActionButton } from "@budibase/bbui"
import { permissions } from "stores/builder" import { permissions } from "stores/builder"
import ManageAccessModal from "../modals/ManageAccessModal.svelte" import ManageAccessModal from "../modals/ManageAccessModal.svelte"
import DetailPopover from "components/common/DetailPopover.svelte" import DetailPopover from "components/common/DetailPopover.svelte"
@ -11,7 +11,6 @@
$: fetchPermissions(resourceId) $: fetchPermissions(resourceId)
const fetchPermissions = async id => { const fetchPermissions = async id => {
console.log("getting perms for", id)
resourcePermissions = await permissions.forResourceDetailed(id) resourcePermissions = await permissions.forResourceDetailed(id)
} }
</script> </script>

View File

@ -1,12 +1,68 @@
<script> <script>
import { ActionButton } from "@budibase/bbui" import { ActionButton, List, ListItem, Button } from "@budibase/bbui"
import DetailPopover from "components/common/DetailPopover.svelte" 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
)
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("generate-automation")
}
</script> </script>
<DetailPopover title="Automations"> <DetailPopover title="Automations" minWidth={400} bind:this={popover}>
<svelte:fragment slot="anchor" let:open> <svelte:fragment slot="anchor" let:open>
<ActionButton icon="JourneyVoyager" selected={open} quiet> <ActionButton icon="JourneyVoyager" selected={open} quiet
Automations >Automations</ActionButton
</ActionButton> >
</svelte:fragment> </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}`}
/>
{/each}
</List>
{/if}
<div>
<Button secondary icon="JourneyVoyager" on:click={generateAutomation}>
Generate new automation
</Button>
</div>
</DetailPopover> </DetailPopover>

View File

@ -8,9 +8,14 @@
const { datasource } = getContext("grid") const { datasource } = getContext("grid")
let popover
$: triggers = $automationStore.blockDefinitions.CREATABLE_TRIGGER $: triggers = $automationStore.blockDefinitions.CREATABLE_TRIGGER
$: table = $tables.list.find(table => table._id === $datasource.tableId) $: table = $tables.list.find(table => table._id === $datasource.tableId)
export const show = () => popover?.show()
export const hide = () => popover?.hide()
async function createAutomation(type) { async function createAutomation(type) {
const triggerType = triggers[type] const triggerType = triggers[type]
if (!triggerType) { if (!triggerType) {
@ -53,7 +58,7 @@
} }
</script> </script>
<DetailPopover title="Generate"> <DetailPopover title="Generate" bind:this={popover}>
<svelte:fragment slot="anchor" let:open> <svelte:fragment slot="anchor" let:open>
<ActionButton icon="MagicWand" selected={open}>Generate</ActionButton> <ActionButton icon="MagicWand" selected={open}>Generate</ActionButton>
</svelte:fragment> </svelte:fragment>

View File

@ -1,12 +1,110 @@
<script> <script>
import { ActionButton } from "@budibase/bbui" import {
ActionButton,
List,
ListItem,
Button,
Toggle,
notifications,
} from "@budibase/bbui"
import DetailPopover from "components/common/DetailPopover.svelte" import DetailPopover from "components/common/DetailPopover.svelte"
import { getContext } from "svelte"
import { appStore, automationStore } from "stores/builder"
import { API } from "api"
import { goto, url } from "@roxi/routify"
import { derived } from "svelte/store"
import { getSequentialName } from "helpers/duplicate"
const { datasource } = getContext("grid")
let rowActions = []
$: ds = $datasource
$: tableId = ds?.tableId
$: isView = ds?.type === "viewV2"
$: fetchRowActions(tableId)
$: console.log(rowActions)
$: activeCount = 0
$: suffix = isView ? activeCount : rowActions.length
const rowActionUrl = derived([url, appStore], ([$url, $appStore]) => {
return ({ automationId }) => {
return $url(`/builder/app/${$appStore.appId}/automation/${automationId}`)
}
})
const fetchRowActions = async tableId => {
if (!tableId) {
rowActions = []
return
}
const res = await API.rowActions.fetch(tableId)
rowActions = Object.values(res || {})
}
const createRowAction = async () => {
try {
const name = getSequentialName(rowActions, "New row action ", {
getName: x => x.name,
})
const res = await API.rowActions.create({
name,
tableId,
})
console.log(res)
await automationStore.actions.fetch()
notifications.success("Row action created successfully")
$goto($rowActionUrl(res))
} catch (error) {
console.log(error)
}
}
</script> </script>
<DetailPopover title="Row Actions"> <DetailPopover title="Row Actions" minWidth={400} maxWidth={400}>
<svelte:fragment slot="anchor" let:open> <svelte:fragment slot="anchor" let:open>
<ActionButton icon="Engagement" selected={open} quiet> <ActionButton icon="Engagement" selected={open} quiet>
Row Actions Row Actions ({suffix})
</ActionButton> </ActionButton>
</svelte:fragment> </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 !rowActions.length}
<br />
You haven't created any row actions.
{:else}
<List>
{#each rowActions as action}
<ListItem title={action.name} url={$rowActionUrl(action)}>
<svelte:fragment slot="right">
{#if isView}
<span>
<Toggle />
</span>
{/if}
</svelte:fragment>
</ListItem>
{/each}
</List>
{/if}
<div>
<Button secondary icon="Engagement" on:click={createRowAction}>
Create row action
</Button>
</div>
</DetailPopover> </DetailPopover>
<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

@ -1,7 +1,7 @@
<script> <script>
import { ActionButton } from "@budibase/bbui" import { ActionButton, List, ListItem } from "@budibase/bbui"
import DetailPopover from "components/common/DetailPopover.svelte" import DetailPopover from "components/common/DetailPopover.svelte"
import { screenStore } from "stores/builder" import { screenStore, appStore } from "stores/builder"
import { getContext } from "svelte" import { getContext } from "svelte"
const { datasource } = getContext("grid") const { datasource } = getContext("grid")
@ -20,10 +20,21 @@
$: console.log(connectedScreens) $: console.log(connectedScreens)
</script> </script>
<DetailPopover title="Screens"> <DetailPopover title="Screens" minWidth={400}>
<svelte:fragment slot="anchor" let:open> <svelte:fragment slot="anchor" let:open>
<ActionButton icon="WebPage" selected={open} quiet>Screens</ActionButton> <ActionButton icon="WebPage" selected={open} quiet>Screens</ActionButton>
</svelte:fragment> </svelte:fragment>
The following screens are connected to this data: {#if !connectedScreens.length}
{connectedScreens.map(screen => screen.routing.route)} 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}`}
/>
{/each}
</List>
{/if}
</DetailPopover> </DetailPopover>

View File

@ -3,6 +3,8 @@
export let title export let title
export let align = "left" export let align = "left"
export let minWidth
export let maxWidth
let popover let popover
let anchor let anchor
@ -18,7 +20,14 @@
<slot name="anchor" {open} /> <slot name="anchor" {open} />
</div> </div>
<Popover bind:this={popover} bind:open {anchor} {align} minWidth={300}> <Popover
bind:this={popover}
bind:open
{anchor}
{align}
minWidth={minWidth || 300}
{maxWidth}
>
<div class="detail-popover"> <div class="detail-popover">
<div class="detail-popover__header"> <div class="detail-popover__header">
<div class="detail-popover__title"> <div class="detail-popover__title">
@ -34,7 +43,6 @@
<style> <style>
.detail-popover { .detail-popover {
--padding: var(--spacing-l);
background-color: var(--spectrum-alias-background-color-primary); background-color: var(--spectrum-alias-background-color-primary);
} }
.detail-popover__header { .detail-popover__header {
@ -43,17 +51,17 @@
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
border-bottom: 1px solid var(--spectrum-global-color-gray-300); border-bottom: 1px solid var(--spectrum-global-color-gray-300);
padding: var(--padding); padding: var(--spacing-l) var(--spacing-xl);
} }
.detail-popover__title { .detail-popover__title {
font-size: 16px; font-size: 16px;
font-weight: 600; font-weight: 600;
} }
.detail-popover__body { .detail-popover__body {
padding: var(--padding); padding: var(--spacing-xl) var(--spacing-xl);
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: stretch; align-items: stretch;
gap: var(--padding); gap: var(--spacing-xl);
} }
</style> </style>

View File

@ -43,7 +43,6 @@
<GridColumnsSettingButton /> <GridColumnsSettingButton />
<GridManageAccessButton /> <GridManageAccessButton />
<GridRowActionsButton /> <GridRowActionsButton />
<GridAutomationsButton />
<GridScreensButton /> <GridScreensButton />
</svelte:fragment> </svelte:fragment>
<svelte:fragment slot="controls-right"> <svelte:fragment slot="controls-right">

View File

@ -16,6 +16,7 @@
import GridUsersTableButton from "components/backend/DataTable/buttons/grid/GridUsersTableButton.svelte" import GridUsersTableButton from "components/backend/DataTable/buttons/grid/GridUsersTableButton.svelte"
import GridGenerateButton from "components/backend/DataTable/buttons/grid/GridGenerateButton.svelte" import GridGenerateButton from "components/backend/DataTable/buttons/grid/GridGenerateButton.svelte"
import GridScreensButton from "components/backend/DataTable/buttons/grid/GridScreensButton.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 GridRowActionsButton from "components/backend/DataTable/buttons/grid/GridRowActionsButton.svelte"
import { DB_TYPE_EXTERNAL } from "constants/backend" import { DB_TYPE_EXTERNAL } from "constants/backend"
@ -27,6 +28,8 @@
status: { displayName: "Status", disabled: true }, status: { displayName: "Status", disabled: true },
} }
let generateButton
$: autoColumnStatus = verifyAutocolumns($tables?.selected) $: autoColumnStatus = verifyAutocolumns($tables?.selected)
$: duplicates = Object.values(autoColumnStatus).reduce((acc, status) => { $: duplicates = Object.values(autoColumnStatus).reduce((acc, status) => {
if (status.length > 1) { if (status.length > 1) {
@ -113,7 +116,12 @@
{#if relationshipsEnabled} {#if relationshipsEnabled}
<GridRelationshipButton /> <GridRelationshipButton />
{/if} {/if}
<GridRowActionsButton /> {#if !isUsersTable}
<GridRowActionsButton />
{/if}
<GridAutomationsButton
on:generate-automation={() => generateButton?.show()}
/>
<GridScreensButton /> <GridScreensButton />
{#if !isUsersTable} {#if !isUsersTable}
<GridImportButton /> <GridImportButton />
@ -122,7 +130,7 @@
</svelte:fragment> </svelte:fragment>
<svelte:fragment slot="controls-right"> <svelte:fragment slot="controls-right">
<GridGenerateButton /> <GridGenerateButton bind:this={generateButton} />
</svelte:fragment> </svelte:fragment>
<!-- Content for editing columns --> <!-- Content for editing columns -->

View File

@ -34,6 +34,7 @@ import { buildEventEndpoints } from "./events"
import { buildAuditLogsEndpoints } from "./auditLogs" import { buildAuditLogsEndpoints } from "./auditLogs"
import { buildLogsEndpoints } from "./logs" import { buildLogsEndpoints } from "./logs"
import { buildMigrationEndpoints } from "./migrations" import { buildMigrationEndpoints } from "./migrations"
import { buildRowActionEndpoints } from "./rowActions"
/** /**
* Random identifier to uniquely identify a session in a tab. This is * Random identifier to uniquely identify a session in a tab. This is
@ -301,5 +302,6 @@ export const createAPIClient = config => {
...buildLogsEndpoints(API), ...buildLogsEndpoints(API),
...buildMigrationEndpoints(API), ...buildMigrationEndpoints(API),
viewV2: buildViewV2Endpoints(API), viewV2: buildViewV2Endpoints(API),
rowActions: buildRowActionEndpoints(API),
} }
} }

View File

@ -0,0 +1,63 @@
export const buildRowActionEndpoints = API => ({
/**
* Gets the available row actions for a table.
* @param tableId the ID of the table
*/
fetch: async tableId => {
const res = await API.get({
url: `/api/tables/${tableId}/actions`,
})
return res?.actions || {}
},
/**
* Creates a row action.
* @param name the name of the row action
* @param tableId the ID of the table
*/
create: async ({ name, tableId }) => {
return await API.post({
url: `/api/tables/${tableId}/actions`,
body: {
name,
},
})
},
/**
* Updates a row action.
* @param name the new name of the row action
* @param tableId the ID of the table
* @param rowActionId the ID of the row action to update
*/
update: async ({ tableId, rowActionId, name }) => {
return await API.post({
url: `/api/tables/${tableId}/actions/${rowActionId}`,
body: {
name,
},
})
},
/**
* Deletes a row action.
* @param tableId the ID of the table
* @param rowActionId the ID of the row action to delete
*/
delete: async ({ tableId, rowActionId }) => {
return await API.delete({
url: `/api/tables/${tableId}/actions/${rowActionId}`,
})
},
/**
* Triggers a row action.
* @param tableId the ID of the table
* @param rowActionId the ID of the row action to trigger
*/
trigger: async ({ tableId, rowActionid }) => {
return await API.post({
url: `/api/tables/${tableId}/actions/${rowActionId}/trigger`,
})
},
})