diff --git a/packages/bbui/src/Modal/Modal.svelte b/packages/bbui/src/Modal/Modal.svelte index 12c12e3717..3ed9b93fa5 100644 --- a/packages/bbui/src/Modal/Modal.svelte +++ b/packages/bbui/src/Modal/Modal.svelte @@ -27,9 +27,17 @@ visible = false } + export function cancel() { + if (!visible) { + return + } + dispatch("cancel") + hide() + } + function handleKey(e) { if (visible && e.key === "Escape") { - hide() + cancel() } } @@ -41,7 +49,7 @@ } } - setContext(Context.Modal, { show, hide }) + setContext(Context.Modal, { show, hide, cancel }) </script> <svelte:window on:keydown={handleKey} /> @@ -56,15 +64,17 @@ <Portal target=".modal-container"> <div class="spectrum-Underlay is-open" - transition:fade|local={{ duration: 200 }} - on:mousedown|self={hide} + in:fade={{ duration: 200 }} + out:fade|local={{ duration: 200 }} + on:mousedown|self={cancel} > - <div class="modal-wrapper" on:mousedown|self={hide}> - <div class="modal-inner-wrapper" on:mousedown|self={hide}> + <div class="modal-wrapper" on:mousedown|self={cancel}> + <div class="modal-inner-wrapper" on:mousedown|self={cancel}> <div use:focusFirstInput class="spectrum-Modal is-open" - transition:fly|local={{ y: 30, duration: 200 }} + in:fly={{ y: 30, duration: 200 }} + out:fly|local={{ y: 30, duration: 200 }} > <slot /> </div> diff --git a/packages/bbui/src/Modal/ModalContent.svelte b/packages/bbui/src/Modal/ModalContent.svelte index 16338b1ed2..dae9b9e8af 100644 --- a/packages/bbui/src/Modal/ModalContent.svelte +++ b/packages/bbui/src/Modal/ModalContent.svelte @@ -16,7 +16,7 @@ export let onConfirm = undefined export let disabled = false - const { hide } = getContext(Context.Modal) + const { hide, cancel } = getContext(Context.Modal) let loading = false $: confirmDisabled = disabled || loading @@ -56,7 +56,7 @@ > <slot name="footer" /> {#if showCancelButton} - <Button group secondary on:click={hide}>{cancelText}</Button> + <Button group secondary on:click={cancel}>{cancelText}</Button> {/if} {#if showConfirmButton} <Button diff --git a/packages/builder/src/builderStore/store/screenTemplates/rowDetailScreen.js b/packages/builder/src/builderStore/store/screenTemplates/rowDetailScreen.js index 7e61d68f59..495389d95f 100644 --- a/packages/builder/src/builderStore/store/screenTemplates/rowDetailScreen.js +++ b/packages/builder/src/builderStore/store/screenTemplates/rowDetailScreen.js @@ -62,6 +62,7 @@ function generateTitleContainer(table, title, formId, repeaterId) { tableId: table._id, rowId: `{{ ${makePropSafe(repeaterId)}.${makePropSafe("_id")} }}`, revId: `{{ ${makePropSafe(repeaterId)}.${makePropSafe("_rev")} }}`, + confirm: true, }, "##eventHandlerType": "Delete Row", }, diff --git a/packages/builder/src/components/design/PropertiesPanel/PropertyControls/EventsEditor/actions/DeleteRow.svelte b/packages/builder/src/components/design/PropertiesPanel/PropertyControls/EventsEditor/actions/DeleteRow.svelte index 5092ca0773..aa657beea5 100644 --- a/packages/builder/src/components/design/PropertiesPanel/PropertyControls/EventsEditor/actions/DeleteRow.svelte +++ b/packages/builder/src/components/design/PropertiesPanel/PropertyControls/EventsEditor/actions/DeleteRow.svelte @@ -1,5 +1,5 @@ <script> - import { Select, Label } from "@budibase/bbui" + import { Select, Label, Checkbox, Input } from "@budibase/bbui" import { store, currentAsset } from "builderStore" import { tables } from "stores/backend" import { getBindableProperties } from "builderStore/dataBinding" @@ -35,6 +35,17 @@ value={parameters.revId} on:change={value => (parameters.revId = value.detail)} /> + + <Label small /> + <Checkbox text="Require confirmation" bind:value={parameters.confirm} /> + + {#if parameters.confirm} + <Label small>Confirm text</Label> + <Input + placeholder="Are you sure you want to delete this row?" + bind:value={parameters.confirmText} + /> + {/if} </div> <style> @@ -42,8 +53,8 @@ display: grid; column-gap: var(--spacing-l); row-gap: var(--spacing-s); - grid-template-columns: auto 1fr; - align-items: baseline; + grid-template-columns: 60px 1fr; + align-items: center; max-width: 800px; margin: 0 auto; } diff --git a/packages/builder/src/components/design/PropertiesPanel/PropertyControls/EventsEditor/actions/ExecuteQuery.svelte b/packages/builder/src/components/design/PropertiesPanel/PropertyControls/EventsEditor/actions/ExecuteQuery.svelte index 44f349a324..5d298e4b9f 100644 --- a/packages/builder/src/components/design/PropertiesPanel/PropertyControls/EventsEditor/actions/ExecuteQuery.svelte +++ b/packages/builder/src/components/design/PropertiesPanel/PropertyControls/EventsEditor/actions/ExecuteQuery.svelte @@ -1,5 +1,5 @@ <script> - import { Select, Layout } from "@budibase/bbui" + import { Select, Layout, Input, Checkbox } from "@budibase/bbui" import { store, currentAsset } from "builderStore" import { datasources, integrations, queries } from "stores/backend" import { getBindableProperties } from "builderStore/dataBinding" @@ -25,7 +25,7 @@ } </script> -<Layout> +<Layout gap="XS"> <Select label="Datasource" bind:value={parameters.datasourceId} @@ -44,22 +44,34 @@ getOptionLabel={query => query.name} getOptionValue={query => query._id} /> - {/if} - {#if query?.parameters?.length > 0} - <div> - <ParameterBuilder - bind:customParams={parameters.queryParams} - parameters={query.parameters} - bindings={bindableProperties} - /> - <IntegrationQueryEditor - height={200} - {query} - schema={fetchQueryDefinition(query)} - editable={false} - {datasource} - /> - </div> + {#if parameters.queryId} + <Checkbox text="Require confirmation" bind:value={parameters.confirm} /> + + {#if parameters.confirm} + <Input + label="Confirm text" + placeholder="Are you sure you want to execute this query?" + bind:value={parameters.confirmText} + /> + {/if} + + {#if query?.parameters?.length > 0} + <div> + <ParameterBuilder + bind:customParams={parameters.queryParams} + parameters={query.parameters} + bindings={bindableProperties} + /> + <IntegrationQueryEditor + height={200} + {query} + schema={fetchQueryDefinition(query)} + editable={false} + {datasource} + /> + </div> + {/if} + {/if} {/if} </Layout> diff --git a/packages/builder/src/components/design/PropertiesPanel/PropertyControls/EventsEditor/actions/SaveFields.svelte b/packages/builder/src/components/design/PropertiesPanel/PropertyControls/EventsEditor/actions/SaveFields.svelte index f4a4bea334..bf88ed2caf 100644 --- a/packages/builder/src/components/design/PropertiesPanel/PropertyControls/EventsEditor/actions/SaveFields.svelte +++ b/packages/builder/src/components/design/PropertiesPanel/PropertyControls/EventsEditor/actions/SaveFields.svelte @@ -79,7 +79,7 @@ on:click={() => removeField(field[0])} /> {/each} - <div> + <div style="margin-top: 10px"> <Button icon="AddCircle" secondary on:click={addField}> Add {fieldLabel} diff --git a/packages/builder/src/components/design/PropertiesPanel/PropertyControls/EventsEditor/actions/SaveRow.svelte b/packages/builder/src/components/design/PropertiesPanel/PropertyControls/EventsEditor/actions/SaveRow.svelte index 664129ee02..a2c3b6d49e 100644 --- a/packages/builder/src/components/design/PropertiesPanel/PropertyControls/EventsEditor/actions/SaveRow.svelte +++ b/packages/builder/src/components/design/PropertiesPanel/PropertyControls/EventsEditor/actions/SaveRow.svelte @@ -1,5 +1,5 @@ <script> - import { Select, Label, Body } from "@budibase/bbui" + import { Select, Label, Body, Checkbox, Input } from "@budibase/bbui" import { store, currentAsset } from "builderStore" import { tables } from "stores/backend" import { @@ -33,7 +33,8 @@ optional.<br /> You can always add or override fields manually. </Body> - <div class="fields"> + + <div class="params"> <Label small>Data Source</Label> <Select bind:value={parameters.providerId} @@ -51,37 +52,58 @@ getOptionValue={option => option._id} /> - {#if parameters.tableId} + <Label small /> + <Checkbox text="Require confirmation" bind:value={parameters.confirm} /> + + {#if parameters.confirm} + <Label small>Confirm text</Label> + <Input + placeholder="Are you sure you want to save this row?" + bind:value={parameters.confirmText} + /> + {/if} + </div> + + {#if parameters.tableId} + <div class="fields"> <SaveFields parameterFields={parameters.fields} {schemaFields} on:change={onFieldsChanged} /> - {/if} - </div> + </div> + {/if} </div> <style> .root { + width: 100%; max-width: 800px; margin: 0 auto; + display: flex; + flex-direction: column; + justify-content: flex-start; + align-items: stretch; + gap: var(--spacing-xl); } .root :global(p) { line-height: 1.5; } + .params { + display: grid; + column-gap: var(--spacing-l); + row-gap: var(--spacing-s); + grid-template-columns: 60px 1fr; + align-items: center; + } + .fields { display: grid; column-gap: var(--spacing-l); row-gap: var(--spacing-s); - grid-template-columns: auto 1fr auto 1fr auto; + grid-template-columns: 60px 1fr auto 1fr auto; align-items: center; } - - .fields :global(> div:nth-child(2)), - .fields :global(> div:nth-child(4)) { - grid-column-start: 2; - grid-column-end: 6; - } </style> diff --git a/packages/builder/src/components/design/PropertiesPanel/PropertyControls/EventsEditor/actions/TriggerAutomation.svelte b/packages/builder/src/components/design/PropertiesPanel/PropertyControls/EventsEditor/actions/TriggerAutomation.svelte index e40c182131..545a805cc5 100644 --- a/packages/builder/src/components/design/PropertiesPanel/PropertyControls/EventsEditor/actions/TriggerAutomation.svelte +++ b/packages/builder/src/components/design/PropertiesPanel/PropertyControls/EventsEditor/actions/TriggerAutomation.svelte @@ -1,5 +1,5 @@ <script> - import { Select, Label, Input } from "@budibase/bbui" + import { Select, Label, Input, Checkbox } from "@budibase/bbui" import { automationStore } from "builderStore" import SaveFields from "./SaveFields.svelte" @@ -72,7 +72,7 @@ </div> </div> - <div class="fields"> + <div class="params"> <Label small>Automation</Label> {#if automationStatus === AUTOMATION_STATUS.EXISTING} @@ -90,6 +90,19 @@ /> {/if} + <Label small /> + <Checkbox text="Require confirmation" bind:value={parameters.confirm} /> + + {#if parameters.confirm} + <Label small>Confirm text</Label> + <Input + placeholder="Are you sure you want to trigger this automation?" + bind:value={parameters.confirmText} + /> + {/if} + </div> + + <div class="fields"> {#key parameters.automationId} <SaveFields schemaFields={selectedSchema} @@ -107,16 +120,21 @@ margin: 0 auto; } - .fields { + .params { display: grid; column-gap: var(--spacing-l); row-gap: var(--spacing-s); - grid-template-columns: auto 1fr auto 1fr auto; - align-items: baseline; + grid-template-columns: 60px 1fr; + align-items: center; } - .fields :global(> div:nth-child(2)) { - grid-column: 2 / span 4; + .fields { + margin-top: var(--spacing-l); + display: grid; + column-gap: var(--spacing-l); + row-gap: var(--spacing-s); + grid-template-columns: 60px 1fr auto 1fr auto; + align-items: center; } .radios, diff --git a/packages/builder/src/components/design/PropertiesPanel/PropertyControls/EventsEditor/actions/ValidateForm.svelte b/packages/builder/src/components/design/PropertiesPanel/PropertyControls/EventsEditor/actions/ValidateForm.svelte index 7a02a7203f..462597f265 100644 --- a/packages/builder/src/components/design/PropertiesPanel/PropertyControls/EventsEditor/actions/ValidateForm.svelte +++ b/packages/builder/src/components/design/PropertiesPanel/PropertyControls/EventsEditor/actions/ValidateForm.svelte @@ -24,15 +24,12 @@ <style> .root { - display: flex; - flex-direction: row; + display: grid; + column-gap: var(--spacing-l); + row-gap: var(--spacing-s); + grid-template-columns: 60px 1fr; align-items: center; max-width: 800px; margin: 0 auto; } - - .root :global(> div) { - flex: 1; - margin-left: var(--spacing-l); - } </style> diff --git a/packages/client/src/components/ClientApp.svelte b/packages/client/src/components/ClientApp.svelte index 5e0efe5451..c2bc0caaa3 100644 --- a/packages/client/src/components/ClientApp.svelte +++ b/packages/client/src/components/ClientApp.svelte @@ -3,6 +3,7 @@ import { setContext, onMount } from "svelte" import Component from "./Component.svelte" import NotificationDisplay from "./NotificationDisplay.svelte" + import ConfirmationDisplay from "./ConfirmationDisplay.svelte" import Provider from "./Provider.svelte" import SDK from "../sdk" import { @@ -70,6 +71,7 @@ {/key} </div> <NotificationDisplay /> + <ConfirmationDisplay /> <!-- Key block needs to be outside the if statement or it breaks --> {#key $builderStore.selectedComponentId} {#if $builderStore.inBuilder} diff --git a/packages/client/src/components/ConfirmationDisplay.svelte b/packages/client/src/components/ConfirmationDisplay.svelte new file mode 100644 index 0000000000..454cf009a5 --- /dev/null +++ b/packages/client/src/components/ConfirmationDisplay.svelte @@ -0,0 +1,15 @@ +<script> + import { confirmationStore } from "../store" + import { Modal, ModalContent } from "@budibase/bbui" +</script> + +{#if $confirmationStore.showConfirmation} + <Modal fixed on:cancel={confirmationStore.actions.cancel}> + <ModalContent + title={$confirmationStore.title} + onConfirm={confirmationStore.actions.confirm} + > + {$confirmationStore.text} + </ModalContent> + </Modal> +{/if} diff --git a/packages/client/src/store/auth.js b/packages/client/src/store/auth.js index 9e01a5648f..9829d2e350 100644 --- a/packages/client/src/store/auth.js +++ b/packages/client/src/store/auth.js @@ -1,40 +1,11 @@ import * as API from "../api" import { writable, get } from "svelte/store" -import { initialise } from "./initialise" -import { routeStore } from "./routes" import { builderStore } from "./builder" import { TableNames } from "../constants" const createAuthStore = () => { const store = writable(null) - const goToDefaultRoute = () => { - // Setting the active route forces an update of the active screen ID, - // even if we're on the same URL - routeStore.actions.setActiveRoute("/") - - // Navigating updates the URL to reflect this route - routeStore.actions.navigate("/") - } - - // Logs a user in - const logIn = async ({ email, password }) => { - const auth = await API.logIn({ email, password }) - if (auth.success) { - await fetchUser() - await initialise() - goToDefaultRoute() - } - } - - // Logs a user out - const logOut = async () => { - store.set(null) - window.document.cookie = `budibase:auth=; budibase:currentapp=; Path=/; Expires=Thu, 01 Jan 1970 00:00:01 GMT;` - await initialise() - goToDefaultRoute() - } - // Fetches the user object if someone is logged in and has reloaded the page const fetchUser = async () => { // Fetch the first user if inside the builder @@ -54,7 +25,7 @@ const createAuthStore = () => { return { subscribe: store.subscribe, - actions: { logIn, logOut, fetchUser }, + actions: { fetchUser }, } } diff --git a/packages/client/src/store/confirmation.js b/packages/client/src/store/confirmation.js new file mode 100644 index 0000000000..497b021b04 --- /dev/null +++ b/packages/client/src/store/confirmation.js @@ -0,0 +1,39 @@ +import { writable, get } from "svelte/store" + +const initialState = { + showConfirmation: false, + title: null, + text: null, + callback: null, +} + +const createConfirmationStore = () => { + const store = writable(initialState) + + const showConfirmation = (title, text, callback) => { + store.set({ + showConfirmation: true, + title, + text, + callback, + }) + } + const confirm = async () => { + const state = get(store) + if (!state.showConfirmation || !state.callback) { + return + } + store.set(initialState) + await state.callback() + } + const cancel = () => { + store.set(initialState) + } + + return { + subscribe: store.subscribe, + actions: { showConfirmation, confirm, cancel }, + } +} + +export const confirmationStore = createConfirmationStore() diff --git a/packages/client/src/store/index.js b/packages/client/src/store/index.js index ebf89e14e3..9f2dbfdcfb 100644 --- a/packages/client/src/store/index.js +++ b/packages/client/src/store/index.js @@ -4,6 +4,7 @@ export { routeStore } from "./routes" export { screenStore } from "./screens" export { builderStore } from "./builder" export { dataSourceStore } from "./dataSource" +export { confirmationStore } from "./confirmation" // Context stores are layered and duplicated, so it is not a singleton export { createContextStore } from "./context" diff --git a/packages/client/src/utils/buttonActions.js b/packages/client/src/utils/buttonActions.js index 4d2865d586..6a72c494f0 100644 --- a/packages/client/src/utils/buttonActions.js +++ b/packages/client/src/utils/buttonActions.js @@ -1,5 +1,5 @@ import { get } from "svelte/store" -import { routeStore, builderStore, authStore } from "../store" +import { routeStore, builderStore, confirmationStore } from "../store" import { saveRow, deleteRow, executeQuery, triggerAutomation } from "../api" import { ActionTypes } from "../constants" @@ -68,15 +68,6 @@ const refreshDatasourceHandler = async (action, context) => { ) } -const loginHandler = async action => { - const { email, password } = action.parameters - await authStore.actions.logIn({ email, password }) -} - -const logoutHandler = async () => { - await authStore.actions.logOut() -} - const handlerMap = { ["Save Row"]: saveRowHandler, ["Delete Row"]: deleteRowHandler, @@ -85,13 +76,19 @@ const handlerMap = { ["Trigger Automation"]: triggerAutomationHandler, ["Validate Form"]: validateFormHandler, ["Refresh Datasource"]: refreshDatasourceHandler, - ["Log In"]: loginHandler, - ["Log Out"]: logoutHandler, +} + +const confirmTextMap = { + ["Delete Row"]: "Are you sure you want to delete this row?", + ["Save Row"]: "Are you sure you want to save this row?", + ["Execute Query"]: "Are you sure you want to execute this query?", + ["Trigger Automation"]: "Are you sure you want to trigger this automation?", } /** * Parses an array of actions and returns a function which will execute the * actions in the current context. + * A handler returning `false` is a flag to stop execution of handlers */ export const enrichButtonActions = (actions, context) => { // Prevent button actions in the builder preview @@ -102,15 +99,44 @@ export const enrichButtonActions = (actions, context) => { return async () => { for (let i = 0; i < handlers.length; i++) { try { - const result = await handlers[i](actions[i], context) - // A handler returning `false` is a flag to stop execution of handlers - if (result === false) { + const action = actions[i] + const callback = async () => handlers[i](action, context) + + // If this action is confirmable, show confirmation and await a + // callback to execute further actions + if (action.parameters?.confirm) { + const defaultText = confirmTextMap[action["##eventHandlerType"]] + const confirmText = action.parameters?.confirmText || defaultText + confirmationStore.actions.showConfirmation( + action["##eventHandlerType"], + confirmText, + async () => { + // When confirmed, execute this action immediately, + // then execute the rest of the actions in the chain + const result = await callback() + if (result !== false) { + const next = enrichButtonActions(actions.slice(i + 1), context) + await next() + } + } + ) + + // Stop enriching actions when encountering a confirmable action, + // as the callback continues the action chain return } + + // For non-confirmable actions, execute the handler immediately + else { + const result = await callback() + if (result === false) { + return + } + } } catch (error) { console.error("Error while executing button handler") console.error(error) - // Stop executing on an error + // Stop executing further actions on error return } }