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}
on:change={onChange}
on:click
on:click|stopPropagation
{id}
type="checkbox"
class="spectrum-Switch-input"

View File

@ -1,55 +1,50 @@
<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 hoverable = false
$: initials = avatar ? title?.[0] : null
export let url = null
</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={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>
{/if}
</div>
{#if $$slots.default}
<div class="right">
<slot />
<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>
{/if}
</div>
</div>
<div class="right">
<slot name="right" />
<Icon name="ChevronRight" />
</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 +59,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-l);
}
.left {
width: 0;
@ -79,17 +75,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

@ -1,5 +1,5 @@
<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"
@ -11,7 +11,6 @@
$: fetchPermissions(resourceId)
const fetchPermissions = async id => {
console.log("getting perms for", id)
resourcePermissions = await permissions.forResourceDetailed(id)
}
</script>

View File

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

View File

@ -8,9 +8,14 @@
const { datasource } = getContext("grid")
let popover
$: 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) {
@ -53,7 +58,7 @@
}
</script>
<DetailPopover title="Generate">
<DetailPopover title="Generate" bind:this={popover}>
<svelte:fragment slot="anchor" let:open>
<ActionButton icon="MagicWand" selected={open}>Generate</ActionButton>
</svelte:fragment>

View File

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

View File

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

View File

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

View File

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

View File

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