diff --git a/packages/bbui/src/ActionButton/ActionButton.svelte b/packages/bbui/src/ActionButton/ActionButton.svelte
index d3cec0f307..2401354fbb 100644
--- a/packages/bbui/src/ActionButton/ActionButton.svelte
+++ b/packages/bbui/src/ActionButton/ActionButton.svelte
@@ -1,15 +1,11 @@
-
- (showTooltip = true)}
on:mouseleave={() => (showTooltip = false)}
on:focus={() => (showTooltip = true)}
+ {disabled}
+ style={accentStyle}
>
-
- {#if longPressable}
-
-
-
- {/if}
- {#if icon}
-
-
-
- {/if}
- {#if $$slots}
-
- {/if}
- {#if tooltip && showTooltip}
-
-
-
- {/if}
-
-
+ {#if icon}
+
+
+
+ {/if}
+ {#if $$slots}
+
+ {/if}
+ {#if tooltip && showTooltip}
+
diff --git a/packages/builder/src/components/automation/AutomationBuilder/FlowChart/FlowItem.svelte b/packages/builder/src/components/automation/AutomationBuilder/FlowChart/FlowItem.svelte
index c88317c79f..aca3e950c3 100644
--- a/packages/builder/src/components/automation/AutomationBuilder/FlowChart/FlowItem.svelte
+++ b/packages/builder/src/components/automation/AutomationBuilder/FlowChart/FlowItem.svelte
@@ -190,7 +190,7 @@
{#if isTrigger && triggerInfo}
{/if}
{#if lastStep}
diff --git a/packages/builder/src/components/automation/AutomationPanel/AutomationNavItem.svelte b/packages/builder/src/components/automation/AutomationPanel/AutomationNavItem.svelte
index 6e4d7c0099..ec9b956190 100644
--- a/packages/builder/src/components/automation/AutomationPanel/AutomationNavItem.svelte
+++ b/packages/builder/src/components/automation/AutomationPanel/AutomationNavItem.svelte
@@ -9,6 +9,7 @@
import { sdk } from "@budibase/shared-core"
import ConfirmDialog from "components/common/ConfirmDialog.svelte"
import UpdateAutomationModal from "components/automation/AutomationPanel/UpdateAutomationModal.svelte"
+ import UpdateRowActionModal from "components/automation/AutomationPanel/UpdateRowActionModal.svelte"
import NavItem from "components/common/NavItem.svelte"
export let automation
@@ -16,12 +17,16 @@
let confirmDeleteDialog
let updateAutomationDialog
+ let updateRowActionDialog
+
+ $: isRowAction = sdk.automations.isRowAction(automation)
async function deleteAutomation() {
try {
await automationStore.actions.delete(automation)
notifications.success("Automation deleted successfully")
} catch (error) {
+ console.error(error)
notifications.error("Error deleting automation")
}
}
@@ -36,42 +41,7 @@
}
const getContextMenuItems = () => {
- const isRowAction = sdk.automations.isRowAction(automation)
- const result = []
- if (!isRowAction) {
- result.push(
- ...[
- {
- icon: "Delete",
- name: "Delete",
- keyBind: null,
- visible: true,
- disabled: false,
- callback: confirmDeleteDialog.show,
- },
- {
- icon: "Edit",
- name: "Edit",
- keyBind: null,
- visible: true,
- disabled: !automation.definition.trigger,
- callback: updateAutomationDialog.show,
- },
- {
- icon: "Duplicate",
- name: "Duplicate",
- keyBind: null,
- visible: true,
- disabled:
- !automation.definition.trigger ||
- automation.definition.trigger?.name === "Webhook",
- callback: duplicateAutomation,
- },
- ]
- )
- }
-
- result.push({
+ const pause = {
icon: automation.disabled ? "CheckmarkCircle" : "Cancel",
name: automation.disabled ? "Activate" : "Pause",
keyBind: null,
@@ -83,8 +53,50 @@
automation.disabled
)
},
- })
- return result
+ }
+ const del = {
+ icon: "Delete",
+ name: "Delete",
+ keyBind: null,
+ visible: true,
+ disabled: false,
+ callback: confirmDeleteDialog.show,
+ }
+ if (!isRowAction) {
+ return [
+ {
+ icon: "Edit",
+ name: "Edit",
+ keyBind: null,
+ visible: true,
+ disabled: !automation.definition.trigger,
+ callback: updateAutomationDialog.show,
+ },
+ {
+ icon: "Duplicate",
+ name: "Duplicate",
+ keyBind: null,
+ visible: true,
+ disabled:
+ !automation.definition.trigger ||
+ automation.definition.trigger?.name === "Webhook",
+ callback: duplicateAutomation,
+ },
+ pause,
+ del,
+ ]
+ } else {
+ return [
+ {
+ icon: "Edit",
+ name: "Edit",
+ keyBind: null,
+ visible: true,
+ callback: updateRowActionDialog.show,
+ },
+ del,
+ ]
+ }
}
const openContextMenu = e => {
@@ -99,7 +111,9 @@
-
-
-
+
{automation.name}?
This action cannot be undone.
-
-
+{#if isRowAction}
+
+{:else}
+
+{/if}
diff --git a/packages/builder/src/components/automation/AutomationPanel/AutomationPanel.svelte b/packages/builder/src/components/automation/AutomationPanel/AutomationPanel.svelte
index 58eebfdd3e..6b96c4ebf5 100644
--- a/packages/builder/src/components/automation/AutomationPanel/AutomationPanel.svelte
+++ b/packages/builder/src/components/automation/AutomationPanel/AutomationPanel.svelte
@@ -3,13 +3,21 @@
import { Modal, notifications, Layout } from "@budibase/bbui"
import NavHeader from "components/common/NavHeader.svelte"
import { onMount } from "svelte"
- import { automationStore } from "stores/builder"
+ import { automationStore, tables } from "stores/builder"
import AutomationNavItem from "./AutomationNavItem.svelte"
+ import { TriggerStepID } from "constants/backend/automations"
export let modal
export let webhookModal
let searchString
+ const dsTriggers = [
+ TriggerStepID.ROW_SAVED,
+ TriggerStepID.ROW_UPDATED,
+ TriggerStepID.ROW_DELETED,
+ TriggerStepID.ROW_ACTION,
+ ]
+
$: filteredAutomations = $automationStore.automations
.filter(automation => {
return (
@@ -29,19 +37,47 @@
return lowerA > lowerB ? 1 : -1
})
- $: groupedAutomations = filteredAutomations.reduce((acc, auto) => {
- const catName = auto.definition?.trigger?.event || "No Trigger"
- acc[catName] ??= {
- icon: auto.definition?.trigger?.icon || "AlertCircle",
- name: (auto.definition?.trigger?.name || "No Trigger").toUpperCase(),
- entries: [],
- }
- acc[catName].entries.push(auto)
- return acc
- }, {})
+ $: groupedAutomations = groupAutomations(filteredAutomations)
$: showNoResults = searchString && !filteredAutomations.length
+ const groupAutomations = automations => {
+ let groups = {}
+
+ for (let auto of automations) {
+ let category = null
+ let dataTrigger = false
+
+ // Group by datasource if possible
+ if (dsTriggers.includes(auto.definition?.trigger?.stepId)) {
+ if (auto.definition.trigger.inputs?.tableId) {
+ const tableId = auto.definition.trigger.inputs?.tableId
+ category = $tables.list.find(x => x._id === tableId)?.name
+ }
+ }
+ // Otherwise group by trigger
+ if (!category) {
+ category = auto.definition?.trigger?.name || "No Trigger"
+ } else {
+ dataTrigger = true
+ }
+ groups[category] ??= {
+ icon: auto.definition?.trigger?.icon || "AlertCircle",
+ name: category.toUpperCase(),
+ entries: [],
+ dataTrigger,
+ }
+ groups[category].entries.push(auto)
+ }
+
+ return Object.values(groups).sort((a, b) => {
+ if (a.dataTrigger === b.dataTrigger) {
+ return a.name < b.name ? -1 : 1
+ }
+ return a.dataTrigger ? -1 : 1
+ })
+ }
+
onMount(async () => {
try {
await automationStore.actions.fetch()
@@ -88,16 +124,22 @@
diff --git a/packages/builder/src/components/backend/DataTable/RelationshipDataTable.svelte b/packages/builder/src/components/backend/DataTable/RelationshipDataTable.svelte
deleted file mode 100644
index f107acc315..0000000000
--- a/packages/builder/src/components/backend/DataTable/RelationshipDataTable.svelte
+++ /dev/null
@@ -1,44 +0,0 @@
-
-
-{#if row && row._id === rowId}
-
-{/if}
diff --git a/packages/builder/src/components/backend/DataTable/TableDataTable.svelte b/packages/builder/src/components/backend/DataTable/TableDataTable.svelte
deleted file mode 100644
index 525421f996..0000000000
--- a/packages/builder/src/components/backend/DataTable/TableDataTable.svelte
+++ /dev/null
@@ -1,120 +0,0 @@
-
-
-
-
-
- {#if isUsersTable && $appStore.features.disableUserMetadata}
-
- {/if}
-
-
-
- {#if !isUsersTable}
-
- {/if}
-
- {#if !isUsersTable}
-
- {/if}
- {#if relationshipsEnabled}
-
- {/if}
- {#if isUsersTable}
-
- {:else}
-
- {/if}
-
- {#if isUsersTable}
-
- {:else}
-
- {/if}
-
-
-
-
-
-
-
-
-
-
-
diff --git a/packages/builder/src/components/backend/DataTable/ViewDataTable.svelte b/packages/builder/src/components/backend/DataTable/ViewDataTable.svelte
deleted file mode 100644
index 684cbd6cf4..0000000000
--- a/packages/builder/src/components/backend/DataTable/ViewDataTable.svelte
+++ /dev/null
@@ -1,80 +0,0 @@
-
-
-
-
-
- {#if view.calculation}
-
- {/if}
-
-
-
-
diff --git a/packages/builder/src/components/backend/DataTable/ViewV2DataTable.svelte b/packages/builder/src/components/backend/DataTable/ViewV2DataTable.svelte
deleted file mode 100644
index b56c5f6568..0000000000
--- a/packages/builder/src/components/backend/DataTable/ViewV2DataTable.svelte
+++ /dev/null
@@ -1,55 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/packages/builder/src/components/backend/DataTable/buttons/EditRolesButton.svelte b/packages/builder/src/components/backend/DataTable/buttons/EditRolesButton.svelte
index 87ca2fa142..3b31de170c 100644
--- a/packages/builder/src/components/backend/DataTable/buttons/EditRolesButton.svelte
+++ b/packages/builder/src/components/backend/DataTable/buttons/EditRolesButton.svelte
@@ -1,13 +1,15 @@
-
- Edit roles
-
-
+
+
+ Edit roles
+
+
+
diff --git a/packages/builder/src/components/backend/DataTable/buttons/ManageAccessButton.svelte b/packages/builder/src/components/backend/DataTable/buttons/ManageAccessButton.svelte
index aa8eefe89c..22a55ecc03 100644
--- a/packages/builder/src/components/backend/DataTable/buttons/ManageAccessButton.svelte
+++ b/packages/builder/src/components/backend/DataTable/buttons/ManageAccessButton.svelte
@@ -1,23 +1,31 @@
-
- Access
-
-
-
-
+
+
+ Access
+
+ {#if resourcePermissions}
+
+ {/if}
+ (showPopover = false)}
+ on:hide={() => (showPopover = true)}
+ />
+
diff --git a/packages/builder/src/components/backend/DataTable/buttons/TableFilterButton.svelte b/packages/builder/src/components/backend/DataTable/buttons/TableFilterButton.svelte
index 6c6ce8c56a..a467801214 100644
--- a/packages/builder/src/components/backend/DataTable/buttons/TableFilterButton.svelte
+++ b/packages/builder/src/components/backend/DataTable/buttons/TableFilterButton.svelte
@@ -51,6 +51,7 @@
{disabled}
on:click={drawer.show}
selected={filterCount > 0}
+ accentColor="#004EA6"
>
{filterCount ? `Filter (${filterCount})` : "Filter"}
diff --git a/packages/frontend-core/src/components/grid/controls/ColumnsSettingContent.svelte b/packages/builder/src/components/backend/DataTable/buttons/grid/ColumnsSettingContent.svelte
similarity index 93%
rename from packages/frontend-core/src/components/grid/controls/ColumnsSettingContent.svelte
rename to packages/builder/src/components/backend/DataTable/buttons/grid/ColumnsSettingContent.svelte
index 456eb50a9c..99f2c16e8f 100644
--- a/packages/frontend-core/src/components/grid/controls/ColumnsSettingContent.svelte
+++ b/packages/builder/src/components/backend/DataTable/buttons/grid/ColumnsSettingContent.svelte
@@ -1,20 +1,19 @@
+
+
+
+
+ Automations{automationCount ? `: ${automationCount}` : ""}
+
+
+ {#if !connectedAutomations.length}
+ There aren't any automations connected to this data.
+ {:else}
+ The following automations are connected to this data.
+
+ {#each connectedAutomations as automation}
+
+ {/each}
+
+ {/if}
+
+
+ Generate automation
+
+
+
diff --git a/packages/frontend-core/src/components/grid/controls/ColumnsSettingButton.svelte b/packages/builder/src/components/backend/DataTable/buttons/grid/GridColumnsSettingButton.svelte
similarity index 66%
rename from packages/frontend-core/src/components/grid/controls/ColumnsSettingButton.svelte
rename to packages/builder/src/components/backend/DataTable/buttons/grid/GridColumnsSettingButton.svelte
index b4940c8903..909ed00d55 100644
--- a/packages/frontend-core/src/components/grid/controls/ColumnsSettingButton.svelte
+++ b/packages/builder/src/components/backend/DataTable/buttons/grid/GridColumnsSettingButton.svelte
@@ -1,10 +1,18 @@
+
+
-
-
-
(open = !open)}
- selected={open}
- >
- Generate
-
-
-
-
-
- {
- open = false
- createAutomation(TriggerStepID.ROW_SAVED)
- }}
- >
- Automation: when row is created
-
- {
- open = false
- createAutomation(TriggerStepID.ROW_UPDATED)
- }}
- >
- Automation: when row is updated
-
-
-
-
-
diff --git a/packages/builder/src/components/backend/DataTable/buttons/grid/GridCreateViewButton.svelte b/packages/builder/src/components/backend/DataTable/buttons/grid/GridCreateViewButton.svelte
deleted file mode 100644
index d4f4bcd1b1..0000000000
--- a/packages/builder/src/components/backend/DataTable/buttons/grid/GridCreateViewButton.svelte
+++ /dev/null
@@ -1,29 +0,0 @@
-
-
-
-
- Create view
-
-
-
-
-
diff --git a/packages/builder/src/components/backend/DataTable/buttons/grid/GridGenerateButton.svelte b/packages/builder/src/components/backend/DataTable/buttons/grid/GridGenerateButton.svelte
new file mode 100644
index 0000000000..5cc3aca19e
--- /dev/null
+++ b/packages/builder/src/components/backend/DataTable/buttons/grid/GridGenerateButton.svelte
@@ -0,0 +1,191 @@
+
+
+
+
+
+
+
+ Generate
+
+
+
+
+ {#if $datasource.type === "table"}
+ Generate a new app screen or automation from this data.
+ {:else}
+ Generate a new app screen from this data.
+ {/if}
+
+
+
App screens
+
+
+ startScreenWizard(AutoScreenTypes.TABLE)}
+ iconColor="var(--spectrum-global-color-gray-600)"
+ />
+
+
+ startScreenWizard(AutoScreenTypes.FORM)}
+ iconColor="var(--spectrum-global-color-gray-600)"
+ />
+
+
+
+
+ {#if $datasource.type === "table"}
+
+
Automation triggers (When a...)
+
+
+ createAutomation(TriggerStepID.ROW_SAVED)}
+ iconColor="var(--spectrum-global-color-gray-600)"
+ />
+
+
+ createAutomation(TriggerStepID.ROW_UPDATED)}
+ iconColor="var(--spectrum-global-color-gray-600)"
+ />
+
+
+ createAutomation(TriggerStepID.ROW_DELETED)}
+ iconColor="var(--spectrum-global-color-gray-600)"
+ />
+
+
+
+ {/if}
+
+
+
+
+
diff --git a/packages/builder/src/components/backend/DataTable/buttons/grid/GridManageAccessButton.svelte b/packages/builder/src/components/backend/DataTable/buttons/grid/GridManageAccessButton.svelte
index 0cd008bab1..c5bc966332 100644
--- a/packages/builder/src/components/backend/DataTable/buttons/grid/GridManageAccessButton.svelte
+++ b/packages/builder/src/components/backend/DataTable/buttons/grid/GridManageAccessButton.svelte
@@ -4,14 +4,8 @@
const { datasource } = getContext("grid")
- $: resourceId = getResourceID($datasource)
-
- const getResourceID = datasource => {
- if (!datasource) {
- return null
- }
- return datasource.type === "table" ? datasource.tableId : datasource.id
- }
+ $: ds = $datasource
+ $: resourceId = ds?.type === "table" ? ds.tableId : ds?.id
diff --git a/packages/builder/src/components/backend/DataTable/buttons/grid/GridRowActionsButton.svelte b/packages/builder/src/components/backend/DataTable/buttons/grid/GridRowActionsButton.svelte
new file mode 100644
index 0000000000..3f0d6f11c5
--- /dev/null
+++ b/packages/builder/src/components/backend/DataTable/buttons/grid/GridRowActionsButton.svelte
@@ -0,0 +1,146 @@
+
+
+
+
+
+ Row actions{actionCount ? `: ${actionCount}` : ""}
+
+
+ A row action is a user-triggered automation for a chosen row.
+ {#if isView && rowActions.length}
+
+ Use the toggle to enable/disable row actions for this view.
+
+ {/if}
+ {#if !tableRowActions.length}
+
+ You haven't created any row actions.
+ {:else}
+
+ {#each tableRowActions as action}
+
+
+ {#if isView}
+
+ toggleAction(action, e.detail)}
+ />
+
+ {/if}
+
+
+ {/each}
+
+ {/if}
+
+
+ Create row action
+
+
+
+
+
+
+
+
+
+
+
diff --git a/packages/builder/src/components/backend/DataTable/buttons/grid/GridScreensButton.svelte b/packages/builder/src/components/backend/DataTable/buttons/grid/GridScreensButton.svelte
new file mode 100644
index 0000000000..701a286112
--- /dev/null
+++ b/packages/builder/src/components/backend/DataTable/buttons/grid/GridScreensButton.svelte
@@ -0,0 +1,59 @@
+
+
+
+
+
+ Screens{screenCount ? `: ${screenCount}` : ""}
+
+
+ {#if !connectedScreens.length}
+ There aren't any screens connected to this data.
+ {:else}
+ The following screens are connected to this data.
+
+ {#each connectedScreens as screen}
+
+ {/each}
+
+ {/if}
+
+
+ Generate app screen
+
+
+
diff --git a/packages/frontend-core/src/components/grid/controls/SizeButton.svelte b/packages/builder/src/components/backend/DataTable/buttons/grid/GridSizeButton.svelte
similarity index 86%
rename from packages/frontend-core/src/components/grid/controls/SizeButton.svelte
rename to packages/builder/src/components/backend/DataTable/buttons/grid/GridSizeButton.svelte
index 320aa47345..f9499e54b7 100644
--- a/packages/frontend-core/src/components/grid/controls/SizeButton.svelte
+++ b/packages/builder/src/components/backend/DataTable/buttons/grid/GridSizeButton.svelte
@@ -1,34 +1,34 @@
-
-
- Manage Access
- {#if requiresPlanToModify}
-
-
- {getFormattedPlanName(requiresPlanToModify)}
-
-
- {/if}
-
- Specify the minimum access level role for this data.
-
- Level
- Role
- {#each Object.keys(computedPermissions) as level}
-
- changePermission(level, e.detail)}
- options={computedPermissions[level].options}
- getOptionLabel={x => x.name}
- getOptionValue={x => x._id}
- />
- {/each}
-
+Specify the minimum access level role for this data.
+
+ Level
+ Role
+ {#each Object.keys(computedPermissions) as level}
+
+ changePermission(level, e.detail)}
+ options={computedPermissions[level].options}
+ getOptionLabel={x => x.name}
+ getOptionValue={x => x._id}
+ />
+ {/each}
+
- {#if dependantsInfoMessage}
-
-
-
-
- {dependantsInfoMessage}
-
-
-
- {/if}
-
+{#if dependantsInfoMessage}
+
+
+
+
+ {dependantsInfoMessage}
+
+
+
+{/if}
diff --git a/packages/frontend-core/src/components/grid/controls/ToggleActionButtonGroup.svelte b/packages/builder/src/components/common/ToggleActionButtonGroup.svelte
similarity index 82%
rename from packages/frontend-core/src/components/grid/controls/ToggleActionButtonGroup.svelte
rename to packages/builder/src/components/common/ToggleActionButtonGroup.svelte
index 497e77c2c9..8a5778534f 100644
--- a/packages/frontend-core/src/components/grid/controls/ToggleActionButtonGroup.svelte
+++ b/packages/builder/src/components/common/ToggleActionButtonGroup.svelte
@@ -9,7 +9,7 @@
export let options
-
+
{#each options as option}
diff --git a/packages/builder/src/components/design/settings/controls/ButtonActionEditor/actions/index.js b/packages/builder/src/components/design/settings/controls/ButtonActionEditor/actions/index.js
index 606ee41d02..b171b34111 100644
--- a/packages/builder/src/components/design/settings/controls/ButtonActionEditor/actions/index.js
+++ b/packages/builder/src/components/design/settings/controls/ButtonActionEditor/actions/index.js
@@ -25,3 +25,4 @@ export { default as OpenModal } from "./OpenModal.svelte"
export { default as CloseModal } from "./CloseModal.svelte"
export { default as ClearRowSelection } from "./ClearRowSelection.svelte"
export { default as DownloadFile } from "./DownloadFile.svelte"
+export { default as RowAction } from "./RowAction.svelte"
diff --git a/packages/builder/src/components/design/settings/controls/ButtonActionEditor/manifest.json b/packages/builder/src/components/design/settings/controls/ButtonActionEditor/manifest.json
index 4022926e7f..631e3119e8 100644
--- a/packages/builder/src/components/design/settings/controls/ButtonActionEditor/manifest.json
+++ b/packages/builder/src/components/design/settings/controls/ButtonActionEditor/manifest.json
@@ -178,6 +178,11 @@
"name": "Download File",
"type": "data",
"component": "DownloadFile"
+ },
+ {
+ "name": "Row Action",
+ "type": "data",
+ "component": "RowAction"
}
]
}
diff --git a/packages/builder/src/components/design/settings/controls/ButtonConfiguration/ButtonConfiguration.svelte b/packages/builder/src/components/design/settings/controls/ButtonConfiguration/ButtonConfiguration.svelte
index db2289345f..6f3a13a745 100644
--- a/packages/builder/src/components/design/settings/controls/ButtonConfiguration/ButtonConfiguration.svelte
+++ b/packages/builder/src/components/design/settings/controls/ButtonConfiguration/ButtonConfiguration.svelte
@@ -2,10 +2,11 @@
import DraggableList from "../DraggableList/DraggableList.svelte"
import ButtonSetting from "./ButtonSetting.svelte"
import { createEventDispatcher } from "svelte"
- import { Helpers } from "@budibase/bbui"
+ import { Helpers, Menu, MenuItem, Popover } from "@budibase/bbui"
import { componentStore } from "stores/builder"
import { getEventContextBindings } from "dataBinding"
import { cloneDeep, isEqual } from "lodash/fp"
+ import { getRowActionButtonTemplates } from "templates/rowActions"
export let componentInstance
export let componentBindings
@@ -17,13 +18,14 @@
const dispatch = createEventDispatcher()
- let focusItem
let cachedValue
+ let rowActionTemplates = []
+ let anchor
+ let popover
$: if (!isEqual(value, cachedValue)) {
cachedValue = cloneDeep(value)
}
-
$: buttonList = sanitizeValue(cachedValue) || []
$: buttonCount = buttonList.length
$: eventContextBindings = getEventContextBindings({
@@ -73,17 +75,32 @@
_instanceName: Helpers.uuid(),
text: cfg.text,
type: cfg.type || "primary",
- },
- {}
+ }
)
}
- const addButton = () => {
+ const addCustomButton = () => {
const newButton = buildPseudoInstance({
text: `Button ${buttonCount + 1}`,
})
dispatch("change", [...buttonList, newButton])
- focusItem = newButton._id
+ popover.hide()
+ }
+
+ const addRowActionTemplate = template => {
+ dispatch("change", [...buttonList, template])
+ popover.hide()
+ }
+
+ const addButton = async () => {
+ rowActionTemplates = await getRowActionButtonTemplates({
+ component: componentInstance,
+ })
+ if (rowActionTemplates.length) {
+ popover.show()
+ } else {
+ addCustomButton()
+ }
}
const removeButton = id => {
@@ -105,12 +122,11 @@
listItemKey={"_id"}
listType={ButtonSetting}
listTypeProps={itemProps}
- focus={focusItem}
draggable={buttonCount > 1}
/>
{/if}
-
+
+
+ Custom button
+ {#each rowActionTemplates as template}
+ addRowActionTemplate(template)}>
+ {template.text}
+
+ {/each}
+
+
+
diff --git a/packages/builder/src/components/backend/TableNavigator/ViewNavItem/DeleteConfirmationModal.svelte b/packages/builder/src/pages/builder/app/[application]/data/table/[tableId]/_components/DeleteViewModal.svelte
similarity index 100%
rename from packages/builder/src/components/backend/TableNavigator/ViewNavItem/DeleteConfirmationModal.svelte
rename to packages/builder/src/pages/builder/app/[application]/data/table/[tableId]/_components/DeleteViewModal.svelte
diff --git a/packages/builder/src/components/backend/TableNavigator/ViewNavItem/EditViewModal.svelte b/packages/builder/src/pages/builder/app/[application]/data/table/[tableId]/_components/EditViewModal.svelte
similarity index 87%
rename from packages/builder/src/components/backend/TableNavigator/ViewNavItem/EditViewModal.svelte
rename to packages/builder/src/pages/builder/app/[application]/data/table/[tableId]/_components/EditViewModal.svelte
index 0809d55884..0f39fa063d 100644
--- a/packages/builder/src/components/backend/TableNavigator/ViewNavItem/EditViewModal.svelte
+++ b/packages/builder/src/pages/builder/app/[application]/data/table/[tableId]/_components/EditViewModal.svelte
@@ -39,7 +39,7 @@
-
-
+
+
diff --git a/packages/builder/src/pages/builder/app/[application]/data/table/[tableId]/_components/ViewNavBar.svelte b/packages/builder/src/pages/builder/app/[application]/data/table/[tableId]/_components/ViewNavBar.svelte
new file mode 100644
index 0000000000..d74fb90cb2
--- /dev/null
+++ b/packages/builder/src/pages/builder/app/[application]/data/table/[tableId]/_components/ViewNavBar.svelte
@@ -0,0 +1,379 @@
+
+
+
+
+
+
+ {table.name}
+
+ {#if tableSelectedBy}
+
+ {/if}
+ {#if tableEditable}
+
+ {/if}
+
+ {#if hasViews}
+
+ {/if}
+ {#if !hasViews && tableEditable}
+
+
+ To create subsets of data, control access and more, create a view.
+
+ {/if}
+ {#if overflowedViews.length}
+
+
+
+ {overflowedViews.length} more
+
+
+ {#each overflowedViews as view}
+
+
+ editOverflowView(view)}>
+ Edit
+
+ deleteOverflowView(view)}>
+ Delete
+
+
+ {/each}
+
+ {/if}
+ {#if hasViews}
+
+ {/if}
+
+
+{#if table && tableEditable}
+
+
+{/if}
+
+{#if editableView}
+
+
+{/if}
+
+
diff --git a/packages/builder/src/pages/builder/app/[application]/data/table/[tableId]/_layout.svelte b/packages/builder/src/pages/builder/app/[application]/data/table/[tableId]/_layout.svelte
index 8c60dbdd69..da05196c04 100644
--- a/packages/builder/src/pages/builder/app/[application]/data/table/[tableId]/_layout.svelte
+++ b/packages/builder/src/pages/builder/app/[application]/data/table/[tableId]/_layout.svelte
@@ -3,6 +3,7 @@
import { tables, builderStore } from "stores/builder"
import * as routify from "@roxi/routify"
import { onDestroy } from "svelte"
+ import ViewNavBar from "./_components/ViewNavBar.svelte"
$: tableId = $tables.selectedTableId
$: builderStore.selectResource(tableId)
@@ -20,4 +21,17 @@
onDestroy(stopSyncing)
-
+
+
+
+
+
+
diff --git a/packages/builder/src/pages/builder/app/[application]/data/table/[tableId]/index.svelte b/packages/builder/src/pages/builder/app/[application]/data/table/[tableId]/index.svelte
index d79a0bc0ad..b9b58cbfce 100644
--- a/packages/builder/src/pages/builder/app/[application]/data/table/[tableId]/index.svelte
+++ b/packages/builder/src/pages/builder/app/[application]/data/table/[tableId]/index.svelte
@@ -1,7 +1,89 @@
{#if $tables?.selected?.name}
@@ -40,7 +111,61 @@
{/if}
-
+
+
+
+ {#if isUsersTable && $appStore.features.disableUserMetadata}
+
+ {/if}
+
+ {#if relationshipsEnabled}
+
+ {/if}
+ {#if !isUsersTable}
+
+ generateButton?.show()} />
+ generateButton?.show()}
+ />
+
+ {/if}
+
+
+
+ {#if !isUsersTable}
+
+ {/if}
+
+
+
+
+
+
+
+
+
+
+
+ {#if isUsersTable}
+
+ {:else}
+
+ {/if}
+
{:else}
Create your first table to start building
{/if}
diff --git a/packages/builder/src/pages/builder/app/[application]/data/table/[tableId]/relationship/[rowId]/[field]/index.svelte b/packages/builder/src/pages/builder/app/[application]/data/table/[tableId]/relationship/[rowId]/[field]/index.svelte
deleted file mode 100644
index 39dbcb9d11..0000000000
--- a/packages/builder/src/pages/builder/app/[application]/data/table/[tableId]/relationship/[rowId]/[field]/index.svelte
+++ /dev/null
@@ -1,10 +0,0 @@
-
-
-
diff --git a/packages/builder/src/pages/builder/app/[application]/data/table/[tableId]/relationship/[rowId]/index.svelte b/packages/builder/src/pages/builder/app/[application]/data/table/[tableId]/relationship/[rowId]/index.svelte
deleted file mode 100644
index 348ed0b5bf..0000000000
--- a/packages/builder/src/pages/builder/app/[application]/data/table/[tableId]/relationship/[rowId]/index.svelte
+++ /dev/null
@@ -1,7 +0,0 @@
-
-
-
diff --git a/packages/builder/src/pages/builder/app/[application]/data/table/[tableId]/relationship/index.svelte b/packages/builder/src/pages/builder/app/[application]/data/table/[tableId]/relationship/index.svelte
deleted file mode 100644
index cecec0ab53..0000000000
--- a/packages/builder/src/pages/builder/app/[application]/data/table/[tableId]/relationship/index.svelte
+++ /dev/null
@@ -1,7 +0,0 @@
-
-
-
diff --git a/packages/builder/src/pages/builder/app/[application]/data/view/v1/[viewName]/_layout.svelte b/packages/builder/src/pages/builder/app/[application]/data/table/[tableId]/v1/[viewName]/_layout.svelte
similarity index 100%
rename from packages/builder/src/pages/builder/app/[application]/data/view/v1/[viewName]/_layout.svelte
rename to packages/builder/src/pages/builder/app/[application]/data/table/[tableId]/v1/[viewName]/_layout.svelte
diff --git a/packages/builder/src/pages/builder/app/[application]/data/table/[tableId]/v1/[viewName]/index.svelte b/packages/builder/src/pages/builder/app/[application]/data/table/[tableId]/v1/[viewName]/index.svelte
new file mode 100644
index 0000000000..2c822569b7
--- /dev/null
+++ b/packages/builder/src/pages/builder/app/[application]/data/table/[tableId]/v1/[viewName]/index.svelte
@@ -0,0 +1,91 @@
+
+
+
+ {#if view}
+
+
+
+ {#if view.calculation}
+
+ {/if}
+
+
+
+
+ {:else}
Create your first table to start building {/if}
+
+
+
diff --git a/packages/builder/src/pages/builder/app/[application]/data/view/v1/index.svelte b/packages/builder/src/pages/builder/app/[application]/data/table/[tableId]/v1/index.svelte
similarity index 100%
rename from packages/builder/src/pages/builder/app/[application]/data/view/v1/index.svelte
rename to packages/builder/src/pages/builder/app/[application]/data/table/[tableId]/v1/index.svelte
diff --git a/packages/builder/src/pages/builder/app/[application]/data/view/index.svelte b/packages/builder/src/pages/builder/app/[application]/data/view/index.svelte
deleted file mode 100644
index 623cd224db..0000000000
--- a/packages/builder/src/pages/builder/app/[application]/data/view/index.svelte
+++ /dev/null
@@ -1,19 +0,0 @@
-
diff --git a/packages/builder/src/pages/builder/app/[application]/data/view/v1/[viewName]/index.svelte b/packages/builder/src/pages/builder/app/[application]/data/view/v1/[viewName]/index.svelte
deleted file mode 100644
index 51149b602d..0000000000
--- a/packages/builder/src/pages/builder/app/[application]/data/view/v1/[viewName]/index.svelte
+++ /dev/null
@@ -1,18 +0,0 @@
-
-
-{#if selectedView}
-
-{:else}
Create your first table to start building {/if}
-
-
diff --git a/packages/builder/src/pages/builder/app/[application]/data/view/v2/[viewId]/index.svelte b/packages/builder/src/pages/builder/app/[application]/data/view/v2/[viewId]/index.svelte
deleted file mode 100644
index c2281710ba..0000000000
--- a/packages/builder/src/pages/builder/app/[application]/data/view/v2/[viewId]/index.svelte
+++ /dev/null
@@ -1,5 +0,0 @@
-
-
-
diff --git a/packages/builder/src/pages/builder/app/[application]/data/view/v2/index.svelte b/packages/builder/src/pages/builder/app/[application]/data/view/v2/index.svelte
deleted file mode 100644
index c11ca87023..0000000000
--- a/packages/builder/src/pages/builder/app/[application]/data/view/v2/index.svelte
+++ /dev/null
@@ -1,5 +0,0 @@
-
diff --git a/packages/builder/src/pages/builder/app/[application]/design/[screenId]/[componentId]/_components/Component/ComponentSettingsSection.svelte b/packages/builder/src/pages/builder/app/[application]/design/[screenId]/[componentId]/_components/Component/ComponentSettingsSection.svelte
index d5a696c6bf..d40f28e65a 100644
--- a/packages/builder/src/pages/builder/app/[application]/design/[screenId]/[componentId]/_components/Component/ComponentSettingsSection.svelte
+++ b/packages/builder/src/pages/builder/app/[application]/design/[screenId]/[componentId]/_components/Component/ComponentSettingsSection.svelte
@@ -8,6 +8,7 @@
import InfoDisplay from "./InfoDisplay.svelte"
import analytics, { Events } from "analytics"
import { shouldDisplaySetting } from "@budibase/frontend-core"
+ import { getContext, setContext } from "svelte"
export let componentDefinition
export let componentInstance
@@ -19,6 +20,16 @@
export let includeHidden = false
export let tag
+ // Sometimes we render component settings using a complicated nested
+ // component instance technique. This results in instances with IDs that
+ // don't exist anywhere in the tree. Therefore we need to keep track of
+ // what the real component tree ID is so we can always find it.
+ const rootId = getContext("rootId")
+ if (!rootId) {
+ setContext("rootId", componentInstance._id)
+ }
+ $: componentInstance._rootId = rootId || componentInstance._id
+
$: sections = getSections(
componentInstance,
componentDefinition,
diff --git a/packages/builder/src/pages/builder/app/[application]/design/_components/NewScreen/CreateScreenModal.svelte b/packages/builder/src/pages/builder/app/[application]/design/_components/NewScreen/CreateScreenModal.svelte
index 554a2c80f5..47d7c765d6 100644
--- a/packages/builder/src/pages/builder/app/[application]/design/_components/NewScreen/CreateScreenModal.svelte
+++ b/packages/builder/src/pages/builder/app/[application]/design/_components/NewScreen/CreateScreenModal.svelte
@@ -10,12 +10,16 @@
navigationStore,
permissions as permissionsStore,
builderStore,
+ datasources,
+ appStore,
} from "stores/builder"
import { auth } from "stores/portal"
import { goto } from "@roxi/routify"
import { TOUR_KEYS } from "components/portal/onboarding/tours.js"
import * as screenTemplating from "templates/screenTemplating"
import { Roles } from "constants/backend"
+ import { AutoScreenTypes } from "constants"
+ import { makeTableOption, makeViewOption } from "./utils"
let mode
@@ -23,20 +27,33 @@
let datasourceModal
let formTypeModal
let tableTypeModal
-
let selectedTablesAndViews = []
let permissions = {}
+ let hasPreselectedDatasource = false
$: screens = $screenStore.screens
- export const show = newMode => {
+ export const show = (newMode, preselectedDatasource) => {
mode = newMode
selectedTablesAndViews = []
permissions = {}
+ hasPreselectedDatasource = preselectedDatasource != null
- if (mode === "table" || mode === "form") {
- datasourceModal.show()
- } else if (mode === "blank") {
+ if (mode === AutoScreenTypes.TABLE || mode === AutoScreenTypes.FORM) {
+ if (preselectedDatasource) {
+ // If preselecting a datasource, skip a step
+ const isTable = preselectedDatasource.type === "table"
+ const tableOrView = isTable
+ ? makeTableOption(preselectedDatasource, $datasources.list)
+ : makeViewOption(preselectedDatasource)
+ fetchPermission(tableOrView.id)
+ selectedTablesAndViews.push(tableOrView)
+ onSelectDatasources()
+ } else {
+ // Otherwise choose a datasource
+ datasourceModal.show()
+ }
+ } else if (mode === AutoScreenTypes.BLANK) {
screenDetailsModal.show()
} else {
throw new Error("Invalid mode provided")
@@ -77,44 +94,49 @@
}
const onSelectDatasources = async () => {
- if (mode === "form") {
+ if (mode === AutoScreenTypes.FORM) {
formTypeModal.show()
- } else if (mode === "table") {
+ } else if (mode === AutoScreenTypes.TABLE) {
tableTypeModal.show()
}
}
const createBlankScreen = async ({ route }) => {
const screenTemplates = screenTemplating.blank({ route, screens })
-
const newScreens = await createScreens(screenTemplates)
loadNewScreen(newScreens[0])
}
const createTableScreen = async type => {
- const screenTemplates = selectedTablesAndViews.flatMap(tableOrView =>
- screenTemplating.table({
- screens,
- tableOrView,
- type,
- permissions: permissions[tableOrView.id],
- })
- )
-
+ const screenTemplates = (
+ await Promise.all(
+ selectedTablesAndViews.map(tableOrView =>
+ screenTemplating.table({
+ screens,
+ tableOrView,
+ type,
+ permissions: permissions[tableOrView.id],
+ })
+ )
+ )
+ ).flat()
const newScreens = await createScreens(screenTemplates)
loadNewScreen(newScreens[0])
}
const createFormScreen = async type => {
- const screenTemplates = selectedTablesAndViews.flatMap(tableOrView =>
- screenTemplating.form({
- screens,
- tableOrView,
- type,
- permissions: permissions[tableOrView.id],
- })
- )
-
+ const screenTemplates = (
+ await Promise.all(
+ selectedTablesAndViews.map(tableOrView =>
+ screenTemplating.form({
+ screens,
+ tableOrView,
+ type,
+ permissions: permissions[tableOrView.id],
+ })
+ )
+ )
+ ).flat()
const newScreens = await createScreens(screenTemplates)
if (type === "update" || type === "create") {
@@ -136,9 +158,11 @@
if (screen?.props?._children.length) {
// Focus on the main component for the screen type
const mainComponent = screen?.props?._children?.[0]._id
- $goto(`./${screen._id}/${mainComponent}`)
+ $goto(
+ `/builder/app/${$appStore.appId}/design/${screen._id}/${mainComponent}`
+ )
} else {
- $goto(`./${screen._id}`)
+ $goto(`/builder/app/${$appStore.appId}/design/${screen._id}`)
}
screenStore.select(screen._id)
@@ -214,6 +238,7 @@
tableTypeModal.hide()
datasourceModal.show()
}}
+ showCancelButton={!hasPreselectedDatasource}
/>
@@ -230,5 +255,6 @@
formTypeModal.hide()
datasourceModal.show()
}}
+ showCancelButton={!hasPreselectedDatasource}
/>
diff --git a/packages/builder/src/pages/builder/app/[application]/design/_components/NewScreen/DatasourceModal.svelte b/packages/builder/src/pages/builder/app/[application]/design/_components/NewScreen/DatasourceModal.svelte
index d036d6a905..d32e8e3fc7 100644
--- a/packages/builder/src/pages/builder/app/[application]/design/_components/NewScreen/DatasourceModal.svelte
+++ b/packages/builder/src/pages/builder/app/[application]/design/_components/NewScreen/DatasourceModal.svelte
@@ -1,11 +1,11 @@
{}
export let onConfirm = () => {}
+ export let showCancelButton = true
@@ -65,9 +67,9 @@
box-sizing: border-box;
padding: var(--spacing-m) var(--spacing-xl);
flex-grow: 1;
- gap: var(--spacing-s);
display: flex;
flex-direction: column;
+ justify-content: center;
}
.image {
diff --git a/packages/builder/src/pages/builder/app/[application]/design/_components/NewScreen/utils.js b/packages/builder/src/pages/builder/app/[application]/design/_components/NewScreen/utils.js
new file mode 100644
index 0000000000..1f2461d2cb
--- /dev/null
+++ b/packages/builder/src/pages/builder/app/[application]/design/_components/NewScreen/utils.js
@@ -0,0 +1,17 @@
+import * as format from "helpers/data/format"
+
+export const makeViewOption = view => ({
+ icon: "Remove",
+ name: view.name,
+ id: view.id,
+ tableSelectFormat: format.tableSelect.viewV2(view),
+ datasourceSelectFormat: format.datasourceSelect.viewV2(view),
+})
+
+export const makeTableOption = (table, datasources) => ({
+ icon: "Table",
+ name: table.name,
+ id: table._id,
+ tableSelectFormat: format.tableSelect.table(table),
+ datasourceSelectFormat: format.datasourceSelect.table(table, datasources),
+})
diff --git a/packages/builder/src/stores/BudiStore.js b/packages/builder/src/stores/BudiStore.js
index 1acf299921..e854aa6588 100644
--- a/packages/builder/src/stores/BudiStore.js
+++ b/packages/builder/src/stores/BudiStore.js
@@ -2,9 +2,7 @@ import { writable } from "svelte/store"
export default class BudiStore {
constructor(init, opts) {
- const store = writable({
- ...init,
- })
+ const store = writable({ ...init })
/**
* Internal Svelte store
@@ -23,6 +21,7 @@ export default class BudiStore {
* *Store modification should be kept to a minimum
*/
this.update = this.store.update
+ this.set = this.store.set
/**
* Optional debug mode to output the store updates to console
diff --git a/packages/builder/src/stores/builder/automations.js b/packages/builder/src/stores/builder/automations.js
index fdb0991911..d6d89567a7 100644
--- a/packages/builder/src/stores/builder/automations.js
+++ b/packages/builder/src/stores/builder/automations.js
@@ -6,6 +6,8 @@ import { createHistoryStore } from "stores/builder/history"
import { notifications } from "@budibase/bbui"
import { updateReferencesInObject } from "dataBinding"
import { AutomationTriggerStepId } from "@budibase/types"
+import { sdk } from "@budibase/shared-core"
+import { rowActions } from "./rowActions"
const initialAutomationState = {
automations: [],
@@ -123,10 +125,18 @@ const automationActions = store => ({
return response.automation
},
delete: async automation => {
- await API.deleteAutomation({
- automationId: automation?._id,
- automationRev: automation?._rev,
- })
+ const isRowAction = sdk.automations.isRowAction(automation)
+ if (isRowAction) {
+ await rowActions.delete(
+ automation.definition.trigger.inputs.tableId,
+ automation.definition.trigger.inputs.rowActionId
+ )
+ } else {
+ await API.deleteAutomation({
+ automationId: automation?._id,
+ automationRev: automation?._rev,
+ })
+ }
store.update(state => {
// Remove the automation
diff --git a/packages/builder/src/stores/builder/index.js b/packages/builder/src/stores/builder/index.js
index aa0062dd7d..dbde739951 100644
--- a/packages/builder/src/stores/builder/index.js
+++ b/packages/builder/src/stores/builder/index.js
@@ -29,6 +29,7 @@ import { integrations } from "./integrations"
import { sortedIntegrations } from "./sortedIntegrations"
import { queries } from "./queries"
import { flags } from "./flags"
+import { rowActions } from "./rowActions"
import componentTreeNodesStore from "./componentTreeNodes"
export {
@@ -65,6 +66,7 @@ export {
flags,
hoverStore,
snippets,
+ rowActions,
}
export const reset = () => {
@@ -74,6 +76,7 @@ export const reset = () => {
componentStore.reset()
layoutStore.reset()
navigationStore.reset()
+ rowActions.reset()
}
const refreshBuilderData = async () => {
diff --git a/packages/builder/src/stores/builder/rowActions.js b/packages/builder/src/stores/builder/rowActions.js
new file mode 100644
index 0000000000..a8532b0c72
--- /dev/null
+++ b/packages/builder/src/stores/builder/rowActions.js
@@ -0,0 +1,149 @@
+import { get, derived } from "svelte/store"
+import BudiStore from "stores/BudiStore"
+import { tables } from "./tables"
+import { viewsV2 } from "./viewsV2"
+import { automationStore } from "./automations"
+import { API } from "api"
+import { getSequentialName } from "helpers/duplicate"
+
+const initialState = {}
+
+export class RowActionStore extends BudiStore {
+ constructor() {
+ super(initialState)
+ }
+
+ reset = () => {
+ this.store.set(initialState)
+ }
+
+ refreshRowActions = async sourceId => {
+ if (!sourceId) {
+ return
+ }
+
+ // Get the underlying table ID for this source ID
+ let tableId = get(tables).list.find(table => table._id === sourceId)?._id
+ if (!tableId) {
+ const view = get(viewsV2).list.find(view => view.id === sourceId)
+ tableId = view?.tableId
+ }
+ if (!tableId) {
+ return
+ }
+
+ // Fetch row actions for this table
+ const res = await API.rowActions.fetch(tableId)
+ const actions = Object.values(res || {})
+ this.update(state => ({
+ ...state,
+ [tableId]: actions,
+ }))
+ }
+
+ createRowAction = async (tableId, viewId, name) => {
+ if (!tableId) {
+ return
+ }
+
+ // Get a unique name for this action
+ if (!name) {
+ const existingRowActions = get(this.store)[tableId] || []
+ name = getSequentialName(existingRowActions, "New row action ", {
+ getName: x => x.name,
+ })
+ }
+
+ // Create the action
+ const res = await API.rowActions.create({
+ name,
+ tableId,
+ })
+
+ // Enable action on this view if adding via a view
+ if (viewId) {
+ await Promise.all([
+ this.enableView(tableId, viewId, res.id),
+ automationStore.actions.fetch(),
+ ])
+ } else {
+ await Promise.all([
+ this.refreshRowActions(tableId),
+ automationStore.actions.fetch(),
+ ])
+ }
+
+ return res
+ }
+
+ enableView = async (tableId, viewId, rowActionId) => {
+ await API.rowActions.enableView({
+ tableId,
+ viewId,
+ rowActionId,
+ })
+ await this.refreshRowActions(tableId)
+ }
+
+ disableView = async (tableId, viewId, rowActionId) => {
+ await API.rowActions.disableView({
+ tableId,
+ viewId,
+ rowActionId,
+ })
+ await this.refreshRowActions(tableId)
+ }
+
+ rename = async (tableId, rowActionId, name) => {
+ await API.rowActions.update({
+ tableId,
+ rowActionId,
+ name,
+ })
+ await this.refreshRowActions(tableId)
+ automationStore.actions.fetch()
+ }
+
+ delete = async (tableId, rowActionId) => {
+ await API.rowActions.delete({
+ tableId,
+ rowActionId,
+ })
+ await this.refreshRowActions(tableId)
+ // We don't need to refresh automations as we can only delete row actions
+ // from the automations store, so we already handle the state update there
+ }
+
+ trigger = async (sourceId, rowActionId, rowId) => {
+ await API.rowActions.trigger({
+ sourceId,
+ rowActionId,
+ rowId,
+ })
+ }
+}
+
+const store = new RowActionStore()
+const derivedStore = derived(store, $store => {
+ let map = {}
+
+ // Generate an entry for every view as well
+ Object.keys($store || {}).forEach(tableId => {
+ map[tableId] = $store[tableId]
+ for (let action of $store[tableId]) {
+ for (let viewId of action.allowedViews || []) {
+ if (!map[viewId]) {
+ map[viewId] = []
+ }
+ map[viewId].push(action)
+ }
+ }
+ })
+
+ return map
+})
+
+export const rowActions = {
+ ...store,
+ subscribe: derivedStore.subscribe,
+}
diff --git a/packages/builder/src/stores/builder/views.js b/packages/builder/src/stores/builder/views.js
index b9986889db..1d2463e2e2 100644
--- a/packages/builder/src/stores/builder/views.js
+++ b/packages/builder/src/stores/builder/views.js
@@ -41,6 +41,7 @@ export function createViewsStore() {
const save = async view => {
const savedView = await API.saveView(view)
+ select(view.name)
// Update tables
tables.update(state => {
diff --git a/packages/builder/src/templates/BaseStructure.js b/packages/builder/src/templates/BaseStructure.js
index 71daca9d1b..0398b1f268 100644
--- a/packages/builder/src/templates/BaseStructure.js
+++ b/packages/builder/src/templates/BaseStructure.js
@@ -24,9 +24,9 @@ export class BaseStructure {
if (this._children.length !== 0) {
for (let child of this._children) {
if (this._isScreen) {
- structure.props._children.push(child.json())
+ structure.props._children.push(child.json?.() || child)
} else {
- structure._children.push(child.json())
+ structure._children.push(child.json?.() || child)
}
}
}
diff --git a/packages/builder/src/templates/rowActions.js b/packages/builder/src/templates/rowActions.js
new file mode 100644
index 0000000000..639db09d00
--- /dev/null
+++ b/packages/builder/src/templates/rowActions.js
@@ -0,0 +1,99 @@
+import { get } from "svelte/store"
+import { getDatasourceForProvider } from "dataBinding"
+import { rowActions, selectedScreen, componentStore } from "stores/builder"
+import { Helpers } from "@budibase/bbui"
+import { findComponent } from "helpers/components"
+
+export const getRowActionButtonTemplates = async ({
+ screen,
+ component,
+ instance,
+}) => {
+ // Find root component instance if not specified
+ if (!instance) {
+ if (!component) {
+ return []
+ }
+ if (!screen) {
+ screen = get(selectedScreen)
+ }
+ const id = component._rootId
+ instance = findComponent(screen?.props, id)
+ }
+ if (!instance) {
+ return []
+ }
+
+ // The row ID binding depends on what component this is.
+ // Therefore we need to whitelist this to only function for certain components.
+ const type = instance?._component
+ const isGridBlock = type?.endsWith("/gridblock")
+ const isFormBlock =
+ type?.endsWith("/formblock") || type?.endsWith("/multistepformblock")
+ if (!isGridBlock && !isFormBlock) {
+ return []
+ }
+
+ // Check we have a valid datasource that can contain row actions
+ const ds = getDatasourceForProvider(screen, instance)
+ if (ds?.type !== "table" && ds?.type !== "viewV2") {
+ return []
+ }
+ const resourceId = ds.id || ds.tableId
+ if (!resourceId) {
+ return []
+ }
+ await rowActions.refreshRowActions(resourceId)
+ const enabledActions = get(rowActions)[resourceId] || []
+
+ // Generate the row ID binding depending on the component
+ let rowIdBinding
+ if (isGridBlock) {
+ rowIdBinding = `{{ [${instance._id}].[_id] }}`
+ } else if (isFormBlock) {
+ rowIdBinding = `{{ [${instance._id}-repeater].[_id] }}`
+ }
+
+ // Create templates
+ return enabledActions.map(action => {
+ // Create a button instance
+ const button = componentStore.createInstance(
+ `@budibase/standard-components/button`,
+ {
+ _instanceName: Helpers.uuid(),
+ text: action.name,
+ type: "primary",
+ quiet: true,
+ }
+ )
+
+ // Row action button action
+ const onClick = [
+ {
+ parameters: {
+ rowActionId: action.id,
+ resourceId,
+ rowId: rowIdBinding,
+ },
+ "##eventHandlerType": "Row Action",
+ id: Helpers.uuid(),
+ },
+ ]
+
+ // For form blocks we need to manually refresh the form after running the action
+ if (isFormBlock) {
+ onClick.push({
+ parameters: {
+ componentId: `${instance._id}-provider`,
+ },
+ "##eventHandlerType": "Refresh Data Provider",
+ id: Helpers.uuid(),
+ })
+ }
+
+ return {
+ ...button,
+ onClick,
+ }
+ })
+}
diff --git a/packages/builder/src/templates/screenTemplating/form.js b/packages/builder/src/templates/screenTemplating/form.js
index d0b7580280..e0fe815896 100644
--- a/packages/builder/src/templates/screenTemplating/form.js
+++ b/packages/builder/src/templates/screenTemplating/form.js
@@ -1,6 +1,9 @@
import { Screen } from "./Screen"
import { Component } from "../Component"
import getValidRoute from "./getValidRoute"
+import { componentStore } from "stores/builder"
+import { Helpers } from "@budibase/bbui"
+import { getRowActionButtonTemplates } from "templates/rowActions"
export const getTypeSpecificRoute = (tableOrView, type) => {
if (type === "create") {
@@ -32,27 +35,55 @@ const getActionType = type => {
}
}
-const form = ({ tableOrView, type, permissions, screens }) => {
+const getTitle = type => {
+ if (type === "create") {
+ return "Create row"
+ } else if (type === "update") {
+ return "Update row"
+ }
+ return "Row details"
+}
+
+const form = async ({ tableOrView, type, permissions, screens }) => {
+ const id = Helpers.uuid()
const typeSpecificRoute = getTypeSpecificRoute(tableOrView, type)
const role = getRole(permissions, type)
- const multistepFormBlock = new Component(
- "@budibase/standard-components/multistepformblock"
- )
+ let formBlock = new Component("@budibase/standard-components/formblock", id)
.customProps({
- actionType: getActionType(type),
dataSource: tableOrView.tableSelectFormat,
- steps: [{}],
+ actionType: getActionType(type),
+ title: getTitle(type),
rowId: type === "new" ? undefined : `{{ url.id }}`,
+ buttonPosition: "bottom",
})
- .instanceName(`${tableOrView.name} - Multistep Form block`)
+ .instanceName(`${tableOrView.name} - Form block`)
+ .json()
+
+ // Add default button config
+ componentStore.migrateSettings(formBlock)
+
+ // Add row action buttons if required
+ if (type !== "create") {
+ const rowActionButtons = await getRowActionButtonTemplates({
+ instance: formBlock,
+ })
+ if (rowActionButtons.length) {
+ formBlock.buttons = [...(formBlock.buttons || []), ...rowActionButtons]
+
+ // Collapse buttons if more than 3 row actions
+ if (rowActionButtons.length > 3) {
+ formBlock.buttonsCollapsed = true
+ }
+ }
+ }
const template = new Screen()
.route(getValidRoute(screens, typeSpecificRoute, role))
.instanceName(`${tableOrView.name} - Form`)
.role(role)
.autoTableId(tableOrView.id)
- .addChild(multistepFormBlock)
+ .addChild(formBlock)
.json()
return [
diff --git a/packages/builder/src/templates/screenTemplating/table/index.js b/packages/builder/src/templates/screenTemplating/table/index.js
index 40cfde6f15..943d93f456 100644
--- a/packages/builder/src/templates/screenTemplating/table/index.js
+++ b/packages/builder/src/templates/screenTemplating/table/index.js
@@ -3,20 +3,20 @@ import modal from "./modal"
import sidePanel from "./sidePanel"
import newScreen from "./newScreen"
-const createScreen = ({ tableOrView, type, permissions, screens }) => {
+const createScreen = async ({ tableOrView, type, permissions, screens }) => {
if (type === "inline") {
- return inline({ tableOrView, permissions, screens })
+ return await inline({ tableOrView, permissions, screens })
}
if (type === "modal") {
- return modal({ tableOrView, permissions, screens })
+ return await modal({ tableOrView, permissions, screens })
}
if (type === "sidePanel") {
- return sidePanel({ tableOrView, permissions, screens })
+ return await sidePanel({ tableOrView, permissions, screens })
}
if (type === "newScreen") {
- return newScreen({ tableOrView, permissions, screens })
+ return await newScreen({ tableOrView, permissions, screens })
}
throw new Error(`Unrecognized table type ${type}`)
diff --git a/packages/builder/src/templates/screenTemplating/table/inline.js b/packages/builder/src/templates/screenTemplating/table/inline.js
index d218aa998a..b7e0dd58b8 100644
--- a/packages/builder/src/templates/screenTemplating/table/inline.js
+++ b/packages/builder/src/templates/screenTemplating/table/inline.js
@@ -2,8 +2,9 @@ import { Screen } from "../Screen"
import { Component } from "../../Component"
import { capitalise } from "helpers"
import getValidRoute from "../getValidRoute"
+import { getRowActionButtonTemplates } from "templates/rowActions"
-const inline = ({ tableOrView, permissions, screens }) => {
+const inline = async ({ tableOrView, permissions, screens }) => {
const heading = new Component("@budibase/standard-components/heading")
.instanceName("Table heading")
.customProps({
@@ -12,7 +13,7 @@ const inline = ({ tableOrView, permissions, screens }) => {
.gridDesktopColSpan(1, 13)
.gridDesktopRowSpan(1, 3)
- const tableBlock = new Component("@budibase/standard-components/gridblock")
+ let tableBlock = new Component("@budibase/standard-components/gridblock")
.instanceName(`${tableOrView.name} - Table`)
.customProps({
table: tableOrView.datasourceSelectFormat,
@@ -20,6 +21,17 @@ const inline = ({ tableOrView, permissions, screens }) => {
.gridDesktopColSpan(1, 13)
.gridDesktopRowSpan(3, 21)
+ // Add row actions to table
+ const rowActionButtons = await getRowActionButtonTemplates({
+ instance: tableBlock.json(),
+ })
+ if (rowActionButtons.length) {
+ tableBlock = tableBlock.customProps({
+ buttons: rowActionButtons,
+ buttonsCollapsed: rowActionButtons.length > 1,
+ })
+ }
+
const screenTemplate = new Screen()
.route(getValidRoute(screens, tableOrView.name, permissions.write))
.instanceName(`${tableOrView.name} - List`)
diff --git a/packages/builder/src/templates/screenTemplating/table/modal.js b/packages/builder/src/templates/screenTemplating/table/modal.js
index caf1582fbb..6a97979b31 100644
--- a/packages/builder/src/templates/screenTemplating/table/modal.js
+++ b/packages/builder/src/templates/screenTemplating/table/modal.js
@@ -5,8 +5,9 @@ import { makePropSafe as safe } from "@budibase/string-templates"
import { Utils } from "@budibase/frontend-core"
import { capitalise } from "helpers"
import getValidRoute from "../getValidRoute"
+import { getRowActionButtonTemplates } from "templates/rowActions"
-const modal = ({ tableOrView, permissions, screens }) => {
+const modal = async ({ tableOrView, permissions, screens }) => {
/*
Create Row
*/
@@ -56,7 +57,7 @@ const modal = ({ tableOrView, permissions, screens }) => {
createFormBlock.instanceName("Create row form block").customProps({
dataSource: tableOrView.tableSelectFormat,
labelPosition: "left",
- buttonPosition: "top",
+ buttonPosition: "bottom",
actionType: "Create",
title: "Create row",
buttons: Utils.buildFormBlockButtonConfig({
@@ -81,23 +82,34 @@ const modal = ({ tableOrView, permissions, screens }) => {
size: "large",
})
- const editFormBlock = new Component("@budibase/standard-components/formblock")
- editFormBlock.instanceName("Edit row form block").customProps({
- dataSource: tableOrView.tableSelectFormat,
- labelPosition: "left",
- buttonPosition: "top",
- actionType: "Update",
- title: "Edit",
- rowId: `{{ ${safe("state")}.${safe(stateKey)} }}`,
- buttons: Utils.buildFormBlockButtonConfig({
- _id: editFormBlock._json._id,
- showDeleteButton: true,
- showSaveButton: true,
- saveButtonLabel: "Save",
- deleteButtonLabel: "Delete",
- actionType: "Update",
+ let editFormBlock = new Component("@budibase/standard-components/formblock")
+ .instanceName("Edit row form block")
+ .customProps({
dataSource: tableOrView.tableSelectFormat,
- }),
+ labelPosition: "left",
+ buttonPosition: "bottom",
+ actionType: "Update",
+ title: "Edit",
+ rowId: `{{ ${safe("state")}.${safe(stateKey)} }}`,
+ })
+
+ // Generate button config including row actions
+ let buttons = Utils.buildFormBlockButtonConfig({
+ _id: editFormBlock._json._id,
+ showDeleteButton: true,
+ showSaveButton: true,
+ saveButtonLabel: "Save",
+ deleteButtonLabel: "Delete",
+ actionType: "Update",
+ dataSource: tableOrView.tableSelectFormat,
+ })
+ const rowActionButtons = await getRowActionButtonTemplates({
+ instance: editFormBlock.json(),
+ })
+ buttons = [...(buttons || []), ...rowActionButtons]
+ editFormBlock = editFormBlock.customProps({
+ buttons,
+ buttonsCollapsed: buttons.length > 5,
})
detailsModal.addChild(editFormBlock)
diff --git a/packages/builder/src/templates/screenTemplating/table/newScreen.js b/packages/builder/src/templates/screenTemplating/table/newScreen.js
index 34026bca7f..b8b06d3d44 100644
--- a/packages/builder/src/templates/screenTemplating/table/newScreen.js
+++ b/packages/builder/src/templates/screenTemplating/table/newScreen.js
@@ -4,6 +4,7 @@ import { capitalise } from "helpers"
import { makePropSafe as safe } from "@budibase/string-templates"
import getValidRoute from "../getValidRoute"
import { Helpers } from "@budibase/bbui"
+import { getRowActionButtonTemplates } from "templates/rowActions"
const getTableScreenTemplate = ({
route,
@@ -92,7 +93,7 @@ const getTableScreenTemplate = ({
}
}
-const getUpdateScreenTemplate = ({
+const getUpdateScreenTemplate = async ({
route,
tableScreenRoute,
tableOrView,
@@ -102,27 +103,11 @@ const getUpdateScreenTemplate = ({
const formId = `${formBlockId}-form`
const repeaterId = `${formBlockId}-repeater`
- const backButton = new Component("@budibase/standard-components/button")
- .instanceName("Back button")
- .customProps({
- type: "primary",
- icon: "ri-arrow-go-back-fill",
- text: "Back",
- onClick: [
- {
- "##eventHandlerType": "Navigate To",
- parameters: {
- type: "url",
- url: tableScreenRoute,
- },
- },
- ],
- })
-
const deleteButton = new Component("@budibase/standard-components/button")
.instanceName("Delete button")
.customProps({
- type: "secondary",
+ type: "warning",
+ quiet: true,
text: "Delete",
onClick: [
{
@@ -173,7 +158,7 @@ const getUpdateScreenTemplate = ({
],
})
- const updateFormBlock = new Component(
+ let updateFormBlock = new Component(
"@budibase/standard-components/formblock",
formBlockId
)
@@ -181,12 +166,22 @@ const getUpdateScreenTemplate = ({
.customProps({
dataSource: tableOrView.tableSelectFormat,
labelPosition: "left",
- buttonPosition: "top",
+ buttonPosition: "bottom",
actionType: "Update",
title: `Update ${tableOrView.name} row`,
- buttons: [backButton.json(), saveButton.json(), deleteButton.json()],
})
+ // Generate button config including row actions
+ let buttons = [saveButton.json(), deleteButton.json()]
+ const rowActionButtons = await getRowActionButtonTemplates({
+ instance: updateFormBlock.json(),
+ })
+ buttons = [...(buttons || []), ...rowActionButtons]
+ updateFormBlock = updateFormBlock.customProps({
+ buttons,
+ buttonsCollapsed: buttons.length > 5,
+ })
+
const template = new Screen()
.route(route)
.instanceName(`Update row`)
@@ -210,23 +205,6 @@ const getCreateScreenTemplate = ({
const formBlockId = Helpers.uuid()
const formId = `${formBlockId}-form`
- const backButton = new Component("@budibase/standard-components/button")
- .instanceName("Back button")
- .customProps({
- type: "primary",
- icon: "ri-arrow-go-back-fill",
- text: "Back",
- onClick: [
- {
- "##eventHandlerType": "Navigate To",
- parameters: {
- type: "url",
- url: tableScreenRoute,
- },
- },
- ],
- })
-
const saveButton = new Component("@budibase/standard-components/button")
.instanceName("Save button")
.customProps({
@@ -264,10 +242,10 @@ const getCreateScreenTemplate = ({
.customProps({
dataSource: tableOrView.tableSelectFormat,
labelPosition: "left",
- buttonPosition: "top",
+ buttonPosition: "bottom",
actionType: "Create",
title: `Create ${tableOrView.name} row`,
- buttons: [backButton.json(), saveButton.json()],
+ buttons: [saveButton.json()],
})
const template = new Screen()
@@ -284,7 +262,7 @@ const getCreateScreenTemplate = ({
}
}
-const newScreen = ({ tableOrView, permissions, screens }) => {
+const newScreen = async ({ tableOrView, permissions, screens }) => {
const tableScreenRoute = getValidRoute(
screens,
tableOrView.name,
@@ -312,7 +290,7 @@ const newScreen = ({ tableOrView, permissions, screens }) => {
gridLayout: true,
})
- const updateScreenTemplate = getUpdateScreenTemplate({
+ const updateScreenTemplate = await getUpdateScreenTemplate({
route: updateScreenRoute,
tableScreenRoute,
tableOrView,
diff --git a/packages/builder/src/templates/screenTemplating/table/sidePanel.js b/packages/builder/src/templates/screenTemplating/table/sidePanel.js
index ef88597733..139b117d1b 100644
--- a/packages/builder/src/templates/screenTemplating/table/sidePanel.js
+++ b/packages/builder/src/templates/screenTemplating/table/sidePanel.js
@@ -5,8 +5,9 @@ import { makePropSafe as safe } from "@budibase/string-templates"
import { Utils } from "@budibase/frontend-core"
import { capitalise } from "helpers"
import getValidRoute from "../getValidRoute"
+import { getRowActionButtonTemplates } from "templates/rowActions"
-const sidePanel = ({ tableOrView, permissions, screens }) => {
+const sidePanel = async ({ tableOrView, permissions, screens }) => {
/*
Create Row
*/
@@ -54,7 +55,7 @@ const sidePanel = ({ tableOrView, permissions, screens }) => {
createFormBlock.instanceName("Create row form block").customProps({
dataSource: tableOrView.tableSelectFormat,
labelPosition: "left",
- buttonPosition: "top",
+ buttonPosition: "bottom",
actionType: "Create",
title: "Create row",
buttons: Utils.buildFormBlockButtonConfig({
@@ -77,23 +78,33 @@ const sidePanel = ({ tableOrView, permissions, screens }) => {
"@budibase/standard-components/sidepanel"
).instanceName("Edit row side panel")
- const editFormBlock = new Component("@budibase/standard-components/formblock")
+ let editFormBlock = new Component("@budibase/standard-components/formblock")
editFormBlock.instanceName("Edit row form block").customProps({
dataSource: tableOrView.tableSelectFormat,
labelPosition: "left",
- buttonPosition: "top",
+ buttonPosition: "bottom",
actionType: "Update",
title: "Edit",
rowId: `{{ ${safe("state")}.${safe(stateKey)} }}`,
- buttons: Utils.buildFormBlockButtonConfig({
- _id: editFormBlock._json._id,
- showDeleteButton: true,
- showSaveButton: true,
- saveButtonLabel: "Save",
- deleteButtonLabel: "Delete",
- actionType: "Update",
- dataSource: tableOrView.tableSelectFormat,
- }),
+ })
+
+ // Generate button config including row actions
+ let buttons = Utils.buildFormBlockButtonConfig({
+ _id: editFormBlock._json._id,
+ showDeleteButton: true,
+ showSaveButton: true,
+ saveButtonLabel: "Save",
+ deleteButtonLabel: "Delete",
+ actionType: "Update",
+ dataSource: tableOrView.tableSelectFormat,
+ })
+ const rowActionButtons = await getRowActionButtonTemplates({
+ instance: editFormBlock.json(),
+ })
+ buttons = [...(buttons || []), ...rowActionButtons]
+ editFormBlock = editFormBlock.customProps({
+ buttons,
+ buttonsCollapsed: buttons.length > 5,
})
detailsSidePanel.addChild(editFormBlock)
diff --git a/packages/client/manifest.json b/packages/client/manifest.json
index e0ab01ccf3..17aebc5225 100644
--- a/packages/client/manifest.json
+++ b/packages/client/manifest.json
@@ -447,6 +447,18 @@
"section": true,
"name": "Layout",
"settings": [
+ {
+ "type": "boolean",
+ "label": "Collapse",
+ "key": "collapsed"
+ },
+ {
+ "type": "text",
+ "label": "Collapsed text",
+ "key": "collapsedText",
+ "dependsOn": "collapsed",
+ "placeholder": "Action"
+ },
{
"type": "select",
"label": "Direction",
@@ -467,7 +479,11 @@
"barTitle": "Row layout"
}
],
- "defaultValue": "row"
+ "defaultValue": "row",
+ "dependsOn": {
+ "setting": "collapsed",
+ "invert": true
+ }
},
{
"type": "select",
@@ -557,7 +573,11 @@
"barTitle": "Grow container"
}
],
- "defaultValue": "shrink"
+ "defaultValue": "shrink",
+ "dependsOn": {
+ "setting": "collapsed",
+ "invert": true
+ }
},
{
"type": "select",
@@ -583,7 +603,11 @@
"value": "L"
}
],
- "defaultValue": "M"
+ "defaultValue": "M",
+ "dependsOn": {
+ "setting": "collapsed",
+ "invert": true
+ }
},
{
"type": "boolean",
@@ -591,7 +615,11 @@
"key": "wrap",
"showInBar": true,
"barIcon": "ModernGridView",
- "barTitle": "Wrap"
+ "barTitle": "Wrap",
+ "dependsOn": {
+ "setting": "collapsed",
+ "invert": true
+ }
}
]
}
@@ -6971,6 +6999,10 @@
{
"type": "ChangeFormStep",
"suffix": "form"
+ },
+ {
+ "type": "RefreshDatasource",
+ "suffix": "provider"
}
],
"context": [
@@ -7041,6 +7073,18 @@
"key": "buttons",
"wide": true,
"nested": true
+ },
+ {
+ "type": "boolean",
+ "label": "Collapse",
+ "key": "buttonsCollapsed"
+ },
+ {
+ "type": "text",
+ "label": "Collapsed text",
+ "key": "buttonsCollapsedText",
+ "dependsOn": "buttonsCollapsed",
+ "placeholder": "Action"
}
]
},
@@ -7153,6 +7197,18 @@
"key": "buttons",
"nested": true,
"resetOn": ["actionType", "dataSource"]
+ },
+ {
+ "type": "boolean",
+ "label": "Collapse",
+ "key": "buttonsCollapsed"
+ },
+ {
+ "type": "text",
+ "label": "Collapsed text",
+ "key": "buttonsCollapsedText",
+ "dependsOn": "buttonsCollapsed",
+ "placeholder": "Action"
}
]
},
@@ -7190,6 +7246,10 @@
{
"type": "ScrollTo",
"suffix": "form"
+ },
+ {
+ "type": "RefreshDatasource",
+ "suffix": "provider"
}
],
"context": [
@@ -7537,13 +7597,24 @@
"type": "buttonConfiguration",
"key": "buttons",
"nested": true,
- "max": 3,
"context": [
{
"label": "Clicked row",
"key": "row"
}
]
+ },
+ {
+ "type": "boolean",
+ "label": "Collapse",
+ "key": "buttonsCollapsed"
+ },
+ {
+ "type": "text",
+ "label": "Collapsed text",
+ "key": "buttonsCollapsedText",
+ "dependsOn": "buttonsCollapsed",
+ "placeholder": "Action"
}
]
}
diff --git a/packages/client/src/components/app/ButtonGroup.svelte b/packages/client/src/components/app/ButtonGroup.svelte
index b3523cdd21..6907f1b129 100644
--- a/packages/client/src/components/app/ButtonGroup.svelte
+++ b/packages/client/src/components/app/ButtonGroup.svelte
@@ -1,12 +1,31 @@
@@ -25,20 +44,27 @@
},
}}
>
- {#each buttons as { text, type, quiet, disabled, onClick, size, icon, gap }}
-
- {/each}
+ {:else}
+ {#each buttons as { text, type, quiet, disabled, onClick, size, icon, gap }}
+
+ {/each}
+ {/if}
diff --git a/packages/client/src/components/app/GridBlock.svelte b/packages/client/src/components/app/GridBlock.svelte
index 30a35b0713..543bade382 100644
--- a/packages/client/src/components/app/GridBlock.svelte
+++ b/packages/client/src/components/app/GridBlock.svelte
@@ -19,6 +19,8 @@
export let columns = null
export let onRowClick = null
export let buttons = null
+ export let buttonsCollapsed = false
+ export let buttonsCollapsedText = null
const context = getContext("context")
const component = getContext("component")
@@ -181,6 +183,8 @@
notifySuccess={notificationStore.actions.success}
notifyError={notificationStore.actions.error}
buttons={enrichedButtons}
+ {buttonsCollapsed}
+ {buttonsCollapsedText}
isCloud={$environmentStore.cloud}
on:rowclick={e => onRowClick?.({ row: e.detail })}
/>
diff --git a/packages/client/src/components/app/blocks/MultiStepFormblock.svelte b/packages/client/src/components/app/blocks/MultiStepFormblock.svelte
index bcc62b5229..6cecf57a2c 100644
--- a/packages/client/src/components/app/blocks/MultiStepFormblock.svelte
+++ b/packages/client/src/components/app/blocks/MultiStepFormblock.svelte
@@ -77,7 +77,7 @@
const enrichSteps = (steps, schema, id) => {
const safeSteps = steps?.length ? steps : [{}]
return safeSteps.map((step, idx) => {
- const { title, desc, fields, buttons } = step
+ const { title, fields, buttons } = step
const defaultProps = Utils.buildMultiStepFormBlockDefaultProps({
_id: id,
stepCount: safeSteps.length,
@@ -86,10 +86,10 @@
dataSource,
})
return {
+ ...step,
_stepId: Helpers.uuid(),
fields: getDefaultFields(fields || [], schema),
title: title ?? defaultProps.title,
- desc,
buttons: buttons || defaultProps.buttons,
}
})
@@ -172,7 +172,11 @@
{#if buttonPosition === "bottom"}
{/if}
diff --git a/packages/client/src/components/app/blocks/form/FormBlock.svelte b/packages/client/src/components/app/blocks/form/FormBlock.svelte
index e3aa20ffa6..656aa5933b 100644
--- a/packages/client/src/components/app/blocks/form/FormBlock.svelte
+++ b/packages/client/src/components/app/blocks/form/FormBlock.svelte
@@ -18,6 +18,8 @@
export let actionUrl
export let noRowsMessage
export let notificationOverride
+ export let buttonsCollapsed
+ export let buttonsCollapsedText
// Legacy
export let showDeleteButton
@@ -118,5 +120,7 @@
{notificationOverride}
buttons={buttonsOrDefault}
buttonPosition={buttons ? buttonPosition : "top"}
+ {buttonsCollapsed}
+ {buttonsCollapsedText}
/>
diff --git a/packages/client/src/components/app/blocks/form/InnerFormBlock.svelte b/packages/client/src/components/app/blocks/form/InnerFormBlock.svelte
index 0227107dd2..cf833642d2 100644
--- a/packages/client/src/components/app/blocks/form/InnerFormBlock.svelte
+++ b/packages/client/src/components/app/blocks/form/InnerFormBlock.svelte
@@ -13,6 +13,8 @@
export let description
export let buttons
export let buttonPosition = "bottom"
+ export let buttonsCollapsed
+ export let buttonsCollapsedText
export let schema
const context = getContext("context")
@@ -81,6 +83,8 @@
type="buttongroup"
props={{
buttons,
+ collapsed: buttonsCollapsed,
+ collapsedText: buttonsCollapsedText,
}}
order={0}
/>
@@ -104,10 +108,12 @@
type="buttongroup"
props={{
buttons,
+ collapsed: buttonsCollapsed,
+ collapsedText: buttonsCollapsedText,
}}
styles={{
normal: {
- "margin-top": "16",
+ "margin-top": "24",
},
}}
order={1}
diff --git a/packages/client/src/utils/buttonActions.js b/packages/client/src/utils/buttonActions.js
index 8f0cb575a7..847a32116d 100644
--- a/packages/client/src/utils/buttonActions.js
+++ b/packages/client/src/utils/buttonActions.js
@@ -493,6 +493,15 @@ const downloadFileHandler = async action => {
}
}
+const rowActionHandler = async action => {
+ const { resourceId, rowId, rowActionId } = action.parameters
+ await API.rowActions.trigger({
+ rowActionId,
+ sourceId: resourceId,
+ rowId,
+ })
+}
+
const handlerMap = {
["Fetch Row"]: fetchRowHandler,
["Save Row"]: saveRowHandler,
@@ -514,6 +523,7 @@ const handlerMap = {
["Open Modal"]: openModalHandler,
["Close Modal"]: closeModalHandler,
["Download File"]: downloadFileHandler,
+ ["Row Action"]: rowActionHandler,
}
const confirmTextMap = {
diff --git a/packages/frontend-core/src/api/index.js b/packages/frontend-core/src/api/index.js
index 066ab16f6e..9558ca36cb 100644
--- a/packages/frontend-core/src/api/index.js
+++ b/packages/frontend-core/src/api/index.js
@@ -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),
}
}
diff --git a/packages/frontend-core/src/api/rowActions.js b/packages/frontend-core/src/api/rowActions.js
new file mode 100644
index 0000000000..071af953ef
--- /dev/null
+++ b/packages/frontend-core/src/api/rowActions.js
@@ -0,0 +1,90 @@
+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.put({
+ 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}`,
+ })
+ },
+
+ /**
+ * Enables a row action for a certain view
+ * @param tableId the ID of the parent table
+ * @param rowActionId the ID of the row action
+ * @param viewId the ID of the view
+ */
+ enableView: async ({ tableId, rowActionId, viewId }) => {
+ return await API.post({
+ url: `/api/tables/${tableId}/actions/${rowActionId}/permissions/${viewId}`,
+ })
+ },
+
+ /**
+ * Disables a row action for a certain view
+ * @param tableId the ID of the parent table
+ * @param rowActionId the ID of the row action
+ * @param viewId the ID of the view
+ */
+ disableView: async ({ tableId, rowActionId, viewId }) => {
+ return await API.delete({
+ url: `/api/tables/${tableId}/actions/${rowActionId}/permissions/${viewId}`,
+ })
+ },
+
+ /**
+ * Triggers a row action.
+ * @param tableId the ID of the table
+ * @param rowActionId the ID of the row action to trigger
+ */
+ trigger: async ({ sourceId, rowActionId, rowId }) => {
+ return await API.post({
+ url: `/api/tables/${sourceId}/actions/${rowActionId}/trigger`,
+ body: {
+ rowId,
+ },
+ })
+ },
+})
diff --git a/packages/frontend-core/src/components/grid/cells/GridCell.svelte b/packages/frontend-core/src/components/grid/cells/GridCell.svelte
index 7b5deccb35..68f98648b4 100644
--- a/packages/frontend-core/src/components/grid/cells/GridCell.svelte
+++ b/packages/frontend-core/src/components/grid/cells/GridCell.svelte
@@ -16,7 +16,12 @@
$: style = getStyle(width, selectedUser, metadata)
const getStyle = (width, selectedUser, metadata) => {
- let style = width === "auto" ? "width: auto;" : `flex: 0 0 ${width}px;`
+ let style
+ if (width === "auto" || width === "100%") {
+ style = `width: ${width};`
+ } else {
+ style = `flex: 0 0 ${width}px;`
+ }
if (selectedUser) {
style += `--user-color :${selectedUser.color};`
}
diff --git a/packages/frontend-core/src/components/grid/cells/HeaderCell.svelte b/packages/frontend-core/src/components/grid/cells/HeaderCell.svelte
index 3b6aa5d424..1e6e49c354 100644
--- a/packages/frontend-core/src/components/grid/cells/HeaderCell.svelte
+++ b/packages/frontend-core/src/components/grid/cells/HeaderCell.svelte
@@ -3,7 +3,7 @@
import { canBeDisplayColumn, canBeSortColumn } from "@budibase/shared-core"
import { Icon, Menu, MenuItem, Modal } from "@budibase/bbui"
import GridCell from "./GridCell.svelte"
- import { getColumnIcon } from "../lib/utils"
+ import { getColumnIcon } from "../../../utils/schema"
import MigrationModal from "../controls/MigrationModal.svelte"
import { debounce } from "../../../utils/utils"
import { FieldType, FormulaType } from "@budibase/types"
diff --git a/packages/frontend-core/src/components/grid/cells/RelationshipCell.svelte b/packages/frontend-core/src/components/grid/cells/RelationshipCell.svelte
index 73c8a99cc2..afdc3d2f30 100644
--- a/packages/frontend-core/src/components/grid/cells/RelationshipCell.svelte
+++ b/packages/frontend-core/src/components/grid/cells/RelationshipCell.svelte
@@ -29,6 +29,7 @@
let searching = false
let container
let anchor
+ let relationshipAnchor
let relationshipFields
$: fieldValue = parseValue(value)
@@ -57,6 +58,7 @@
}, {})
$: showRelationshipFields =
+ relationshipAnchor &&
relationshipFields &&
Object.keys(relationshipFields).length &&
focused &&
@@ -206,6 +208,7 @@
// Toggles whether a row is included in the relationship or not
const toggleRow = async row => {
+ hideRelationshipFields()
if (fieldValue?.some(x => x._id === row._id)) {
// If the row is already included, remove it and update the candidate
// row to be the same position if possible
@@ -242,11 +245,13 @@
return value
}
- const displayRelationshipFields = relationship => {
+ const displayRelationshipFields = (e, relationship) => {
+ relationshipAnchor = e.target
relationshipFields = relationFields[relationship._id]
}
const hideRelationshipFields = () => {
+ relationshipAnchor = null
relationshipFields = undefined
}
@@ -281,7 +286,7 @@
displayRelationshipFields(relationship)}
+ on:mouseenter={e => displayRelationshipFields(e, relationship)}
on:focus={() => {}}
on:mouseleave={() => hideRelationshipFields()}
>
@@ -359,7 +364,12 @@
{/if}
{#if showRelationshipFields}
-
+
{#each Object.entries(relationshipFields) as [fieldName, fieldValue]}
@@ -461,10 +471,8 @@
height: 20px;
max-width: 100%;
}
- .values.wrap .badge:hover {
+ .values.wrap .badge.extra-info:hover {
filter: brightness(1.25);
- }
- .values.wrap .badge.extra-info {
cursor: pointer;
}
@@ -543,15 +551,19 @@
.relationship-fields {
margin: var(--spacing-m) var(--spacing-l);
display: grid;
- grid-template-columns: minmax(auto, 50%) auto;
+ grid-template-columns: auto 1fr;
grid-row-gap: var(--spacing-m);
- grid-column-gap: var(--spacing-m);
+ grid-column-gap: var(--spacing-xl);
}
.relationship-field-name {
text-transform: uppercase;
color: var(--spectrum-global-color-gray-600);
- font-size: var(--font-size-xs);
+ font-size: 12px;
+ overflow: hidden;
+ white-space: nowrap;
+ text-overflow: ellipsis;
+ max-width: 120px;
}
.relationship-field-value {
overflow: hidden;
diff --git a/packages/frontend-core/src/components/grid/layout/ButtonColumn.svelte b/packages/frontend-core/src/components/grid/layout/ButtonColumn.svelte
index f448460a96..0e2cf7951e 100644
--- a/packages/frontend-core/src/components/grid/layout/ButtonColumn.svelte
+++ b/packages/frontend-core/src/components/grid/layout/ButtonColumn.svelte
@@ -1,6 +1,6 @@