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 icon} + + {/if} + {#if $$slots} + + {/if} + {#if tooltip && showTooltip} +
+ +
+ {/if} + diff --git a/packages/bbui/src/ActionMenu/ActionMenu.svelte b/packages/bbui/src/ActionMenu/ActionMenu.svelte index 75ddd679da..f27854bc04 100644 --- a/packages/bbui/src/ActionMenu/ActionMenu.svelte +++ b/packages/bbui/src/ActionMenu/ActionMenu.svelte @@ -1,14 +1,20 @@ -
+
diff --git a/packages/bbui/src/Actions/position_dropdown.js b/packages/bbui/src/Actions/position_dropdown.js index 21635592d2..e95c7dd1b6 100644 --- a/packages/bbui/src/Actions/position_dropdown.js +++ b/packages/bbui/src/Actions/position_dropdown.js @@ -151,9 +151,9 @@ export default function positionDropdown(element, opts) { // Determine X strategy if (align === "right") { applyXStrategy(Strategies.EndToEnd) - } else if (align === "right-outside") { + } else if (align === "right-outside" || align === "right-context-menu") { applyXStrategy(Strategies.StartToEnd) - } else if (align === "left-outside") { + } else if (align === "left-outside" || align === "left-context-menu") { applyXStrategy(Strategies.EndToStart) } else if (align === "center") { applyXStrategy(Strategies.MidPoint) @@ -164,6 +164,12 @@ export default function positionDropdown(element, opts) { // Determine Y strategy if (align === "right-outside" || align === "left-outside") { applyYStrategy(Strategies.MidPoint) + } else if ( + align === "right-context-menu" || + align === "left-context-menu" + ) { + applyYStrategy(Strategies.StartToStart) + styles.top -= 5 // Manual adjustment for action menu padding } else { applyYStrategy(Strategies.StartToEnd) } @@ -240,7 +246,7 @@ export default function positionDropdown(element, opts) { } // Apply initial styles which don't need to change - element.style.position = "absolute" + element.style.position = "fixed" element.style.zIndex = "9999" // Set up a scroll listener diff --git a/packages/bbui/src/Button/Button.svelte b/packages/bbui/src/Button/Button.svelte index 9e49d84d44..0a8917c3c1 100644 --- a/packages/bbui/src/Button/Button.svelte +++ b/packages/bbui/src/Button/Button.svelte @@ -17,6 +17,8 @@ export let tooltip = undefined export let newStyles = true export let id + export let ref + export let reverse = false const dispatch = createEventDispatcher() @@ -25,6 +27,7 @@ @@ -91,4 +97,11 @@ .spectrum-Button--secondary.new-styles.is-disabled { color: var(--spectrum-global-color-gray-500); } + .spectrum-Button .spectrum-Button-label + .spectrum-Icon { + margin-left: var(--spectrum-button-primary-icon-gap); + margin-right: calc( + -1 * (var(--spectrum-button-primary-textonly-padding-left-adjusted) - + var(--spectrum-button-primary-padding-left-adjusted)) + ); + } diff --git a/packages/bbui/src/ButtonGroup/CollapsedButtonGroup.svelte b/packages/bbui/src/ButtonGroup/CollapsedButtonGroup.svelte new file mode 100644 index 0000000000..d7aad5ccff --- /dev/null +++ b/packages/bbui/src/ButtonGroup/CollapsedButtonGroup.svelte @@ -0,0 +1,57 @@ + + + + + + {#each buttons as button} + handleClick(button)} disabled={button.disabled}> + {button.text || "Button"} + + {/each} + + diff --git a/packages/bbui/src/Form/Core/Switch.svelte b/packages/bbui/src/Form/Core/Switch.svelte index deffc19167..d7110a6e67 100644 --- a/packages/bbui/src/Form/Core/Switch.svelte +++ b/packages/bbui/src/Form/Core/Switch.svelte @@ -19,6 +19,7 @@ {disabled} on:change={onChange} on:click + on:click|stopPropagation {id} type="checkbox" class="spectrum-Switch-input" diff --git a/packages/bbui/src/Form/Core/TextField.svelte b/packages/bbui/src/Form/Core/TextField.svelte index 3335d3567b..917bb2a452 100644 --- a/packages/bbui/src/Form/Core/TextField.svelte +++ b/packages/bbui/src/Form/Core/TextField.svelte @@ -1,6 +1,6 @@ diff --git a/packages/bbui/src/Icon/Icon.svelte b/packages/bbui/src/Icon/Icon.svelte index 6ae1f4ca67..73ad8edd10 100644 --- a/packages/bbui/src/Icon/Icon.svelte +++ b/packages/bbui/src/Icon/Icon.svelte @@ -60,10 +60,11 @@ .newStyles { color: var(--spectrum-global-color-gray-700); } - + svg { + transition: color var(--spectrum-global-animation-duration-100, 130ms); + } svg.hoverable { pointer-events: all; - transition: color var(--spectrum-global-animation-duration-100, 130ms); } svg.hoverable:hover { color: var(--hover-color) !important; diff --git a/packages/bbui/src/List/ListItem.svelte b/packages/bbui/src/List/ListItem.svelte index 76b242cf9c..2b1f6b030f 100644 --- a/packages/bbui/src/List/ListItem.svelte +++ b/packages/bbui/src/List/ListItem.svelte @@ -1,55 +1,57 @@ - - - + diff --git a/packages/bbui/src/Menu/Item.svelte b/packages/bbui/src/Menu/Item.svelte index 05a33adda9..5e5f6d840c 100644 --- a/packages/bbui/src/Menu/Item.svelte +++ b/packages/bbui/src/Menu/Item.svelte @@ -27,7 +27,7 @@ const onClick = () => { if (actionMenu && !noClose) { - actionMenu.hide() + actionMenu.hideAll() } dispatch("click") } @@ -35,7 +35,7 @@
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 - - +
+ +
+ 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} +
+ +
+
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 @@ + + + + + +
+ magic wand + 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} +
+ +
+
+ + + + + + + + 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} +
+ +
+
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. -
- - - {#each Object.keys(computedPermissions) as level} - - + - diff --git a/packages/builder/src/components/backend/DatasourceNavigator/DatasourceNavigator.svelte b/packages/builder/src/components/backend/DatasourceNavigator/DatasourceNavigator.svelte index e0745c15a1..a6078e38fb 100644 --- a/packages/builder/src/components/backend/DatasourceNavigator/DatasourceNavigator.svelte +++ b/packages/builder/src/components/backend/DatasourceNavigator/DatasourceNavigator.svelte @@ -39,9 +39,7 @@ const selectTable = tableId => { tables.select(tableId) - if (!$isActive("./table/:tableId")) { - $goto(`./table/${tableId}`) - } + $goto(`./table/${tableId}`) } function openNode(datasource) { diff --git a/packages/builder/src/components/backend/TableNavigator/TableNavItem/DeleteConfirmationModal.svelte b/packages/builder/src/components/backend/TableNavigator/TableNavItem/DeleteConfirmationModal.svelte index 03da9f3fd3..c0e030845f 100644 --- a/packages/builder/src/components/backend/TableNavigator/TableNavItem/DeleteConfirmationModal.svelte +++ b/packages/builder/src/components/backend/TableNavigator/TableNavItem/DeleteConfirmationModal.svelte @@ -104,7 +104,7 @@
{/if} -

Please enter the app name below to confirm.

+

Please enter the table name below to confirm.

diff --git a/packages/builder/src/components/backend/TableNavigator/TableNavItem/TableNavItem.svelte b/packages/builder/src/components/backend/TableNavigator/TableNavItem/TableNavItem.svelte index ab79a8fff0..6b64096e2e 100644 --- a/packages/builder/src/components/backend/TableNavigator/TableNavItem/TableNavItem.svelte +++ b/packages/builder/src/components/backend/TableNavigator/TableNavItem/TableNavItem.svelte @@ -20,14 +20,6 @@ const getContextMenuItems = () => { return [ - { - icon: "Delete", - name: "Delete", - keyBind: null, - visible: true, - disabled: false, - callback: deleteConfirmationModal.show, - }, { icon: "Edit", name: "Edit", @@ -36,6 +28,14 @@ disabled: false, callback: editModal.show, }, + { + icon: "Delete", + name: "Delete", + keyBind: null, + visible: true, + disabled: false, + callback: deleteConfirmationModal.show, + }, ] } diff --git a/packages/builder/src/components/backend/TableNavigator/TableNavigator.svelte b/packages/builder/src/components/backend/TableNavigator/TableNavigator.svelte index f21230d7a6..f97bd2487b 100644 --- a/packages/builder/src/components/backend/TableNavigator/TableNavigator.svelte +++ b/packages/builder/src/components/backend/TableNavigator/TableNavigator.svelte @@ -1,33 +1,15 @@
{#each sortedTables as table, idx} selectTable(table._id)} /> - {#each [...Object.entries(table.views || {})].sort() as [name, view], idx (idx)} - { - if (view.version === 2) { - $goto(`./view/v2/${encodeURIComponent(view.id)}`) - } else { - $goto(`./view/v1/${encodeURIComponent(name)}`) - } - }} - /> - {/each} {/each}
diff --git a/packages/builder/src/components/backend/TableNavigator/ViewNavItem/ViewNavItem.svelte b/packages/builder/src/components/backend/TableNavigator/ViewNavItem/ViewNavItem.svelte deleted file mode 100644 index ba30cea165..0000000000 --- a/packages/builder/src/components/backend/TableNavigator/ViewNavItem/ViewNavItem.svelte +++ /dev/null @@ -1,71 +0,0 @@ - - - - - - - diff --git a/packages/builder/src/components/backend/TableNavigator/utils.js b/packages/builder/src/components/backend/TableNavigator/utils.js index ae7aaa0f0a..a76a14c1a4 100644 --- a/packages/builder/src/components/backend/TableNavigator/utils.js +++ b/packages/builder/src/components/backend/TableNavigator/utils.js @@ -66,3 +66,7 @@ export const parseFile = e => { reader.readAsText(file) }) } + +export const alphabetical = (a, b) => { + return a.name?.toLowerCase() > b.name?.toLowerCase() ? 1 : -1 +} diff --git a/packages/builder/src/components/common/DetailPopover.svelte b/packages/builder/src/components/common/DetailPopover.svelte new file mode 100644 index 0000000000..bc891bfe42 --- /dev/null +++ b/packages/builder/src/components/common/DetailPopover.svelte @@ -0,0 +1,75 @@ + + + + +
+ +
+ + +
+
+
+ {title} +
+ +
+
+ +
+
+
+ + 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 @@ + + + + +{#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 @@