diff --git a/packages/backend-core/src/constants/misc.ts b/packages/backend-core/src/constants/misc.ts index aee099e10a..e2fd975e40 100644 --- a/packages/backend-core/src/constants/misc.ts +++ b/packages/backend-core/src/constants/misc.ts @@ -28,6 +28,7 @@ export enum Config { OIDC = "oidc", OIDC_LOGOS = "logos_oidc", SCIM = "scim", + AI = "AI", } export const MIN_VALID_DATE = new Date(-2147483647000) diff --git a/packages/backend-core/src/sql/sql.ts b/packages/backend-core/src/sql/sql.ts index 949d7edf1b..2b75752aec 100644 --- a/packages/backend-core/src/sql/sql.ts +++ b/packages/backend-core/src/sql/sql.ts @@ -1351,7 +1351,8 @@ class InternalBuilder { schema.constraints?.presence === true || schema.type === FieldType.FORMULA || schema.type === FieldType.AUTO || - schema.type === FieldType.LINK + schema.type === FieldType.LINK || + schema.type === FieldType.AI ) { continue } diff --git a/packages/backend-core/tests/core/utilities/mocks/licenses.ts b/packages/backend-core/tests/core/utilities/mocks/licenses.ts index bc9a3b635c..5ba6fb36a1 100644 --- a/packages/backend-core/tests/core/utilities/mocks/licenses.ts +++ b/packages/backend-core/tests/core/utilities/mocks/licenses.ts @@ -102,6 +102,14 @@ export const useAppBuilders = () => { return useFeature(Feature.APP_BUILDERS) } +export const useBudibaseAI = () => { + return useFeature(Feature.BUDIBASE_AI) +} + +export const useAICustomConfigs = () => { + return useFeature(Feature.AI_CUSTOM_CONFIGS) +} + // QUOTAS export const setAutomationLogsQuota = (value: number) => { 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..5b6152781a 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/automation/SetupPanel/AutomationBlockSetup.svelte b/packages/builder/src/components/automation/SetupPanel/AutomationBlockSetup.svelte index bf5fd702cd..cbac1bf2ff 100644 --- a/packages/builder/src/components/automation/SetupPanel/AutomationBlockSetup.svelte +++ b/packages/builder/src/components/automation/SetupPanel/AutomationBlockSetup.svelte @@ -62,6 +62,7 @@ } from "@budibase/types" import { FIELDS } from "constants/backend" import PropField from "./PropField.svelte" + import { utils } from "@budibase/shared-core" export let block export let testData @@ -96,8 +97,14 @@ $: memoEnvVariables.set($environment.variables) $: memoBlock.set(block) - $: filters = lookForFilters(schemaProperties) || [] - $: tempFilters = filters + $: filters = lookForFilters(schemaProperties) + $: filterCount = + filters?.groups?.reduce((acc, group) => { + acc = acc += group?.filters?.length || 0 + return acc + }, 0) || 0 + + $: tempFilters = cloneDeep(filters) $: stepId = $memoBlock.stepId $: automationBindings = getAvailableBindings( @@ -791,14 +798,13 @@ break } } - return filters || [] + return utils.processSearchFilters(filters) } function saveFilters(key) { - const filters = QueryUtils.buildQuery(tempFilters) - + const query = QueryUtils.buildQuery(tempFilters) onChange({ - [key]: filters, + [key]: query, [`${key}-def`]: tempFilters, // need to store the builder definition in the automation }) @@ -1027,18 +1033,24 @@
{:else if value.customType === AutomationCustomIOType.FILTERS || value.customType === AutomationCustomIOType.TRIGGER_FILTER} - {filters.length > 0 - ? "Update Filter" - : "No Filter set"} + {filterCount > 0 ? "Update Filter" : "No Filter set"} + + { + tempFilters = filters + }} > - + { + return Array.isArray(fieldValue) ? fieldValue.join(",") : fieldValue + } {#each schemaFields || [] as [field, schema]} @@ -257,7 +265,7 @@ panel={AutomationBindingPanel} type={schema.type} {schema} - value={editableRow[field]} + value={drawerValue(editableRow[field])} on:change={e => onChange({ row: { 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 a35e1b034e..0000000000 --- a/packages/builder/src/components/backend/DataTable/ViewV2DataTable.svelte +++ /dev/null @@ -1,58 +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 91ec9a9b01..e616e27467 100644 --- a/packages/builder/src/components/backend/DataTable/buttons/TableFilterButton.svelte +++ b/packages/builder/src/components/backend/DataTable/buttons/TableFilterButton.svelte @@ -5,6 +5,7 @@ import { getUserBindings } from "dataBinding" import { makePropSafe } from "@budibase/string-templates" import { search } from "@budibase/frontend-core" + import { utils } from "@budibase/shared-core" import { tables } from "stores/builder" export let schema @@ -16,15 +17,19 @@ let drawer - $: tempValue = filters || [] + $: localFilters = utils.processSearchFilters(filters) + $: schemaFields = search.getFields( $tables.list, Object.values(schema || {}), { allowLinks: true } ) - $: text = getText(filters) - $: selected = tempValue.filter(x => !x.onEmptyFilter)?.length > 0 + $: filterCount = + localFilters?.groups?.reduce((acc, group) => { + return (acc += group.filters.filter(filter => filter.field).length) + }, 0) || 0 + $: bindings = [ { type: "context", @@ -38,28 +43,33 @@ }, ...getUserBindings(), ] - const getText = filters => { - const count = filters?.filter(filter => filter.field)?.length - return count ? `Filter (${count})` : "Filter" - } - - {text} + 0} + accentColor="#004EA6" +> + {filterCount ? `Filter: ${filterCount}` : "Filter"} { + localFilters = utils.processSearchFilters(filters) + }} forceModal > (tempValue = e.detail)} + on:change={e => (localFilters = e.detail)} {bindings} /> 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 dd12af3ff4..9e9449976b 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 64% rename from packages/frontend-core/src/components/grid/controls/ColumnsSettingButton.svelte rename to packages/builder/src/components/backend/DataTable/buttons/grid/GridColumnsSettingButton.svelte index 75108870bd..83327a48c4 100644 --- a/packages/frontend-core/src/components/grid/controls/ColumnsSettingButton.svelte +++ b/packages/builder/src/components/backend/DataTable/buttons/grid/GridColumnsSettingButton.svelte @@ -1,8 +1,17 @@ + + - -
- (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..f61e19c19d --- /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/AIFieldConfiguration.svelte b/packages/builder/src/components/common/AIFieldConfiguration.svelte new file mode 100644 index 0000000000..cb7ac77b75 --- /dev/null +++ b/packages/builder/src/components/common/AIFieldConfiguration.svelte @@ -0,0 +1,59 @@ + + + + {/if} + {/each} +{/if} 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/SaveRow.svelte b/packages/builder/src/components/design/settings/controls/ButtonActionEditor/actions/SaveRow.svelte index c23edbeb58..819ed10880 100644 --- a/packages/builder/src/components/design/settings/controls/ButtonActionEditor/actions/SaveRow.svelte +++ b/packages/builder/src/components/design/settings/controls/ButtonActionEditor/actions/SaveRow.svelte @@ -33,7 +33,7 @@ const getSchemaFields = resourceId => { const { schema } = getSchemaForDatasourcePlus(resourceId) - return Object.values(schema || {}) + return Object.values(schema || {}).filter(field => !field.readonly) } const onFieldsChanged = e => { 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 96% 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 index 774a2f987a..0b5f18b70b 100644 --- a/packages/builder/src/components/backend/TableNavigator/ViewNavItem/DeleteConfirmationModal.svelte +++ b/packages/builder/src/pages/builder/app/[application]/data/table/[tableId]/_components/DeleteViewModal.svelte @@ -20,6 +20,7 @@ } notifications.success("View deleted") } catch (error) { + console.error(error) notifications.error("Error deleting view") } } 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..bf3d073f60 --- /dev/null +++ b/packages/builder/src/pages/builder/app/[application]/data/table/[tableId]/_components/ViewNavBar.svelte @@ -0,0 +1,384 @@ + + + + +{#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..69a23f3a0e 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,92 @@ {#if $tables?.selected?.name} @@ -40,7 +114,63 @@
{/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 6627c67080..b8765ad1e7 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" import { updateBindingsInSteps, getNewStepName, @@ -127,10 +129,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/components.js b/packages/builder/src/stores/builder/components.js index e7c4b9c412..6c2c438f0c 100644 --- a/packages/builder/src/stores/builder/components.js +++ b/packages/builder/src/stores/builder/components.js @@ -31,6 +31,7 @@ import { import BudiStore from "../BudiStore" import { Utils } from "@budibase/frontend-core" import { FieldType } from "@budibase/types" +import { utils } from "@budibase/shared-core" export const INITIAL_COMPONENTS_STATE = { components: {}, @@ -196,6 +197,25 @@ export class ComponentStore extends BudiStore { } } + if (!enrichedComponent?._component) { + return migrated + } + + const def = this.getDefinition(enrichedComponent?._component) + const filterableTypes = def?.settings?.filter(setting => + setting?.type?.startsWith("filter") + ) + for (let setting of filterableTypes || []) { + const isLegacy = Array.isArray(enrichedComponent[setting.key]) + + if (isLegacy) { + const processedSetting = utils.processSearchFilters( + enrichedComponent[setting.key] + ) + enrichedComponent[setting.key] = processedSetting + migrated = true + } + } return migrated } @@ -405,7 +425,13 @@ export class ComponentStore extends BudiStore { screen: get(selectedScreen), useDefaultValues: true, }) - this.migrateSettings(instance) + + try { + this.migrateSettings(instance) + } catch (e) { + console.error(e) + throw e + } // Custom post processing for creation only let extras = {} 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..b1f4e7067f --- /dev/null +++ b/packages/builder/src/stores/builder/rowActions.js @@ -0,0 +1,151 @@ +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 => { + // We need to have all the actions for the table in order to be displayed in the crud section + map[tableId] = $store[tableId] + for (let action of $store[tableId]) { + const otherSources = (action.allowedSources || []).filter( + sourceId => sourceId !== tableId + ) + for (let source of otherSources) { + map[source] ??= [] + map[source].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/stores/builder/viewsV2.js b/packages/builder/src/stores/builder/viewsV2.js index 9bd32f4a24..7c0a940c1b 100644 --- a/packages/builder/src/stores/builder/viewsV2.js +++ b/packages/builder/src/stores/builder/viewsV2.js @@ -1,6 +1,30 @@ import { writable, derived, get } from "svelte/store" import { tables } from "./tables" import { API } from "api" +import { dataFilters } from "@budibase/shared-core" + +function convertToSearchFilters(view) { + // convert from SearchFilterGroup type + if (view?.query) { + return { + ...view, + queryUI: view.query, + query: dataFilters.buildQuery(view.query), + } + } + return view +} + +function convertToSearchFilterGroup(view) { + if (view?.queryUI) { + return { + ...view, + query: view.queryUI, + queryUI: undefined, + } + } + return view +} export function createViewsV2Store() { const store = writable({ @@ -12,7 +36,7 @@ export function createViewsV2Store() { const views = Object.values(table?.views || {}).filter(view => { return view.version === 2 }) - list = list.concat(views) + list = list.concat(views.map(view => convertToSearchFilterGroup(view))) }) return { ...$store, @@ -34,6 +58,7 @@ export function createViewsV2Store() { } const create = async view => { + view = convertToSearchFilters(view) const savedViewResponse = await API.viewV2.create(view) const savedView = savedViewResponse.data replaceView(savedView.id, savedView) @@ -41,6 +66,7 @@ export function createViewsV2Store() { } const save = async view => { + view = convertToSearchFilters(view) const res = await API.viewV2.update(view) const savedView = res?.data replaceView(view.id, savedView) @@ -51,6 +77,7 @@ export function createViewsV2Store() { if (!viewId) { return } + view = convertToSearchFilterGroup(view) const existingView = get(derivedStore).list.find(view => view.id === viewId) const tableIndex = get(tables).list.findIndex(table => { return table._id === view?.tableId || table._id === existingView?.tableId diff --git a/packages/builder/src/stores/portal/featureFlags.js b/packages/builder/src/stores/portal/featureFlags.js new file mode 100644 index 0000000000..796fe76c97 --- /dev/null +++ b/packages/builder/src/stores/portal/featureFlags.js @@ -0,0 +1,14 @@ +import { derived } from "svelte/store" +import { auth } from "stores/portal" + +export const INITIAL_FEATUREFLAG_STATE = { + SQS: false, + DEFAULT_VALUES: false, +} + +export const featureFlags = derived([auth], ([$auth]) => { + return { + ...INITIAL_FEATUREFLAG_STATE, + ...($auth?.user?.flags || {}), + } +}) diff --git a/packages/builder/src/stores/portal/index.js b/packages/builder/src/stores/portal/index.js index 8f638e0af2..d35c88671c 100644 --- a/packages/builder/src/stores/portal/index.js +++ b/packages/builder/src/stores/portal/index.js @@ -19,5 +19,6 @@ export { features } from "./features" export { themeStore } from "./theme" export { temporalStore } from "./temporal" export { navigation } from "./navigation" +export { featureFlags } from "./featureFlags" export const sideBarCollapsed = writable(false) diff --git a/packages/builder/src/stores/portal/licensing.js b/packages/builder/src/stores/portal/licensing.js index f0ce7046f2..9abc376cd0 100644 --- a/packages/builder/src/stores/portal/licensing.js +++ b/packages/builder/src/stores/portal/licensing.js @@ -131,23 +131,15 @@ export const createLicensingStore = () => { const triggerAutomationRunEnabled = license.features.includes( Constants.Features.TRIGGER_AUTOMATION_RUN ) - const perAppBuildersEnabled = license.features.includes( Constants.Features.APP_BUILDERS ) - - const isViewPermissionsEnabled = license.features.includes( - Constants.Features.VIEW_PERMISSIONS - ) - const budibaseAIEnabled = license.features.includes( Constants.Features.BUDIBASE_AI ) - const customAIConfigsEnabled = license.features.includes( Constants.Features.AI_CUSTOM_CONFIGS ) - store.update(state => { return { ...state, @@ -167,7 +159,6 @@ export const createLicensingStore = () => { enforceableSSO, syncAutomationsEnabled, triggerAutomationRunEnabled, - isViewPermissionsEnabled, perAppBuildersEnabled, } }) 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 b1552a974a..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 + } } ] } @@ -5101,7 +5129,8 @@ { "type": "filter", "label": "Filtering", - "key": "filter" + "key": "filter", + "resetOn": "dataSource" }, { "type": "field/sortable", @@ -6970,6 +6999,10 @@ { "type": "ChangeFormStep", "suffix": "form" + }, + { + "type": "RefreshDatasource", + "suffix": "provider" } ], "context": [ @@ -7040,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" } ] }, @@ -7152,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" } ] }, @@ -7189,6 +7246,10 @@ { "type": "ScrollTo", "suffix": "form" + }, + { + "type": "RefreshDatasource", + "suffix": "provider" } ], "context": [ @@ -7536,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/DataProvider.svelte b/packages/client/src/components/app/DataProvider.svelte index 3e005b02cf..7b5187ffb6 100644 --- a/packages/client/src/components/app/DataProvider.svelte +++ b/packages/client/src/components/app/DataProvider.svelte @@ -2,6 +2,7 @@ import { getContext } from "svelte" import { Pagination, ProgressCircle } from "@budibase/bbui" import { fetchData, QueryUtils } from "@budibase/frontend-core" + import { LogicalOperator, EmptyFilterOption } from "@budibase/types" export let dataSource export let filter @@ -17,9 +18,10 @@ let interval let queryExtensions = {} + $: defaultQuery = QueryUtils.buildQuery(filter) + // We need to manage our lucene query manually as we want to allow components // to extend it - $: defaultQuery = QueryUtils.buildQuery(filter) $: query = extendQuery(defaultQuery, queryExtensions) $: fetch = createFetch(dataSource) $: fetch.update({ @@ -124,17 +126,20 @@ } const extendQuery = (defaultQuery, extensions) => { - const extensionValues = Object.values(extensions || {}) - let extendedQuery = { ...defaultQuery } - extensionValues.forEach(extension => { - Object.entries(extension || {}).forEach(([operator, fields]) => { - extendedQuery[operator] = { - ...extendedQuery[operator], - ...fields, - } - }) - }) - return extendedQuery + const extended = { + [LogicalOperator.AND]: { + conditions: [ + ...(defaultQuery ? [defaultQuery] : []), + ...Object.values(extensions || {}), + ], + }, + onEmptyFilter: EmptyFilterOption.RETURN_NONE, + } + + // If there are no conditions applied at all, clear the request. + return extended[LogicalOperator.AND]?.conditions?.length > 0 + ? extended + : null } const setUpAutoRefresh = autoRefresh => { diff --git a/packages/client/src/components/app/GridBlock.svelte b/packages/client/src/components/app/GridBlock.svelte index 30a35b0713..c6b08e3869 100644 --- a/packages/client/src/components/app/GridBlock.svelte +++ b/packages/client/src/components/app/GridBlock.svelte @@ -3,6 +3,7 @@ // because it functions similarly to one import { getContext, onMount } from "svelte" import { get, derived, readable } from "svelte/store" + import { featuresStore } from "stores" import { Grid } from "@budibase/frontend-core" // table is actually any datasource, but called table for legacy compatibility @@ -19,6 +20,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,7 +184,10 @@ notifySuccess={notificationStore.actions.success} notifyError={notificationStore.actions.error} buttons={enrichedButtons} + {buttonsCollapsed} + {buttonsCollapsedText} isCloud={$environmentStore.cloud} + aiEnabled={$featuresStore.aiEnabled} 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/components/app/dynamic-filter/DynamicFilter.svelte b/packages/client/src/components/app/dynamic-filter/DynamicFilter.svelte index 516b26ee69..4ab01de17d 100644 --- a/packages/client/src/components/app/dynamic-filter/DynamicFilter.svelte +++ b/packages/client/src/components/app/dynamic-filter/DynamicFilter.svelte @@ -1,9 +1,12 @@ - - -
- Results are filtered to only those which match all of the following - constraints. -
-
diff --git a/packages/client/src/stores/features.js b/packages/client/src/stores/features.js index 2bdc2bec1a..86c16bf344 100644 --- a/packages/client/src/stores/features.js +++ b/packages/client/src/stores/features.js @@ -2,6 +2,7 @@ import { derived } from "svelte/store" import { appStore } from "./app" import { authStore } from "./auth" import { Constants } from "@budibase/frontend-core" +import { Feature } from "@budibase/types" const createFeaturesStore = () => { return derived([authStore, appStore], ([$authStore, $appStore]) => { @@ -33,8 +34,13 @@ const createFeaturesStore = () => { } } + const license = getUserLicense() + return { logoEnabled: isFreePlan(), + aiEnabled: + license?.features?.includes(Feature.AI_CUSTOM_CONFIGS) || + license?.features?.includes(Feature.BUDIBASE_AI), } }) } 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 f8315cbd2d..1ca8b7c3f4 100644 --- a/packages/frontend-core/src/api/index.js +++ b/packages/frontend-core/src/api/index.js @@ -35,6 +35,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 @@ -303,5 +304,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/api/tables.js b/packages/frontend-core/src/api/tables.js index 823e1d1675..dc9008e4eb 100644 --- a/packages/frontend-core/src/api/tables.js +++ b/packages/frontend-core/src/api/tables.js @@ -48,7 +48,7 @@ export const buildTableEndpoints = API => ({ return await API.post({ url: `/api/${tableId}/search`, body: { - query, + ...(query ? { query } : {}), bookmark, limit, sort, diff --git a/packages/frontend-core/src/components/CoreFilterBuilder.svelte b/packages/frontend-core/src/components/CoreFilterBuilder.svelte new file mode 100644 index 0000000000..c711a57e1c --- /dev/null +++ b/packages/frontend-core/src/components/CoreFilterBuilder.svelte @@ -0,0 +1,520 @@ + + +
+ + {#if fieldOptions?.length} + + + {#if editableFilters?.groups?.length} +
+ Show data which matches + + opt.label} + getOptionValue={opt => opt.value} + on:change={e => { + handleFilterChange({ + groupIdx, + group: { + logicalOperator: e.detail, + }, + }) + }} + placeholder={false} + /> + + of the following filters are matched: +
+
+ { + handleFilterChange({ + groupIdx, + addFilter: true, + }) + }} + /> + { + handleFilterChange({ + groupIdx, + deleteGroup: true, + }) + }} + /> +
+
+ +
+ {#each group.filters as filter, filterIdx} +
+ { + const updated = { ...filter, operator: e.detail } + onOperatorChange(updated) + onFilterFieldUpdate(updated, groupIdx, filterIdx) + }} + placeholder={false} + /> + + { + onFilterFieldUpdate( + { ...filter, ...e.detail }, + groupIdx, + filterIdx + ) + }} + /> + + { + handleFilterChange({ + groupIdx, + filterIdx, + deleteFilter: true, + }) + }} + /> +
+ {/each} +
+ + {/each} + + {/if} + + - {/if} -
- -
- {:else} - None of the table column can be used for filtering. - {/if} - - - - diff --git a/packages/frontend-core/src/components/FilterField.svelte b/packages/frontend-core/src/components/FilterField.svelte new file mode 100644 index 0000000000..2f034ddf3e --- /dev/null +++ b/packages/frontend-core/src/components/FilterField.svelte @@ -0,0 +1,319 @@ + + +
+ + + + + + +
+
+ {#if filter.valueType === FilterValueType.BINDING} + + {:else} +
+ {#if [FieldType.STRING, FieldType.LONGFORM, FieldType.NUMBER, FieldType.BIGINT, FieldType.FORMULA, FieldType.AI].includes(filter.type)} + + {:else if filter.type === FieldType.ARRAY || (filter.type === FieldType.OPTIONS && filter.operator === ArrayOperator.ONE_OF)} + + {:else if filter.type === FieldType.OPTIONS} + + {:else if filter.type === FieldType.BOOLEAN} + + {:else if filter.type === FieldType.DATETIME} + + {:else if [FieldType.BB_REFERENCE, FieldType.BB_REFERENCE_SINGLE].includes(filter.type)} + + {:else} + + {/if} +
+ {/if} +
+ +
+ + {#if !disabled && allowBindings && filter.field && !filter.noValue} + + +
{ + bindingDrawer.show() + }} + > + +
+ {/if} +
+
+
+ + diff --git a/packages/frontend-core/src/components/FilterUsers.svelte b/packages/frontend-core/src/components/FilterUsers.svelte index 489426df1e..4640561afd 100644 --- a/packages/frontend-core/src/components/FilterUsers.svelte +++ b/packages/frontend-core/src/components/FilterUsers.svelte @@ -27,7 +27,8 @@
option.email} diff --git a/packages/frontend-core/src/components/grid/cells/AICell.svelte b/packages/frontend-core/src/components/grid/cells/AICell.svelte new file mode 100644 index 0000000000..38e81cefd3 --- /dev/null +++ b/packages/frontend-core/src/components/grid/cells/AICell.svelte @@ -0,0 +1,99 @@ + + + + +
+
+ {value || ""} +
+
+ +{#if isOpen} + +