Merge pull request #1774 from Budibase/confirmable-actions

Confirmable actions
This commit is contained in:
Andrew Kingston 2021-06-21 12:56:47 +01:00 committed by GitHub
commit 983ac8e76b
15 changed files with 228 additions and 103 deletions

View File

@ -27,9 +27,17 @@
visible = false visible = false
} }
export function cancel() {
if (!visible) {
return
}
dispatch("cancel")
hide()
}
function handleKey(e) { function handleKey(e) {
if (visible && e.key === "Escape") { if (visible && e.key === "Escape") {
hide() cancel()
} }
} }
@ -41,7 +49,7 @@
} }
} }
setContext(Context.Modal, { show, hide }) setContext(Context.Modal, { show, hide, cancel })
</script> </script>
<svelte:window on:keydown={handleKey} /> <svelte:window on:keydown={handleKey} />
@ -56,15 +64,17 @@
<Portal target=".modal-container"> <Portal target=".modal-container">
<div <div
class="spectrum-Underlay is-open" class="spectrum-Underlay is-open"
transition:fade|local={{ duration: 200 }} in:fade={{ duration: 200 }}
on:mousedown|self={hide} out:fade|local={{ duration: 200 }}
on:mousedown|self={cancel}
> >
<div class="modal-wrapper" on:mousedown|self={hide}> <div class="modal-wrapper" on:mousedown|self={cancel}>
<div class="modal-inner-wrapper" on:mousedown|self={hide}> <div class="modal-inner-wrapper" on:mousedown|self={cancel}>
<div <div
use:focusFirstInput use:focusFirstInput
class="spectrum-Modal is-open" 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 /> <slot />
</div> </div>

View File

@ -16,7 +16,7 @@
export let onConfirm = undefined export let onConfirm = undefined
export let disabled = false export let disabled = false
const { hide } = getContext(Context.Modal) const { hide, cancel } = getContext(Context.Modal)
let loading = false let loading = false
$: confirmDisabled = disabled || loading $: confirmDisabled = disabled || loading
@ -56,7 +56,7 @@
> >
<slot name="footer" /> <slot name="footer" />
{#if showCancelButton} {#if showCancelButton}
<Button group secondary on:click={hide}>{cancelText}</Button> <Button group secondary on:click={cancel}>{cancelText}</Button>
{/if} {/if}
{#if showConfirmButton} {#if showConfirmButton}
<Button <Button

View File

@ -62,6 +62,7 @@ function generateTitleContainer(table, title, formId, repeaterId) {
tableId: table._id, tableId: table._id,
rowId: `{{ ${makePropSafe(repeaterId)}.${makePropSafe("_id")} }}`, rowId: `{{ ${makePropSafe(repeaterId)}.${makePropSafe("_id")} }}`,
revId: `{{ ${makePropSafe(repeaterId)}.${makePropSafe("_rev")} }}`, revId: `{{ ${makePropSafe(repeaterId)}.${makePropSafe("_rev")} }}`,
confirm: true,
}, },
"##eventHandlerType": "Delete Row", "##eventHandlerType": "Delete Row",
}, },

View File

@ -1,5 +1,5 @@
<script> <script>
import { Select, Label } from "@budibase/bbui" import { Select, Label, Checkbox, Input } from "@budibase/bbui"
import { store, currentAsset } from "builderStore" import { store, currentAsset } from "builderStore"
import { tables } from "stores/backend" import { tables } from "stores/backend"
import { getBindableProperties } from "builderStore/dataBinding" import { getBindableProperties } from "builderStore/dataBinding"
@ -35,6 +35,17 @@
value={parameters.revId} value={parameters.revId}
on:change={value => (parameters.revId = value.detail)} 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> </div>
<style> <style>
@ -42,8 +53,8 @@
display: grid; display: grid;
column-gap: var(--spacing-l); column-gap: var(--spacing-l);
row-gap: var(--spacing-s); row-gap: var(--spacing-s);
grid-template-columns: auto 1fr; grid-template-columns: 60px 1fr;
align-items: baseline; align-items: center;
max-width: 800px; max-width: 800px;
margin: 0 auto; margin: 0 auto;
} }

View File

@ -1,5 +1,5 @@
<script> <script>
import { Select, Layout } from "@budibase/bbui" import { Select, Layout, Input, Checkbox } from "@budibase/bbui"
import { store, currentAsset } from "builderStore" import { store, currentAsset } from "builderStore"
import { datasources, integrations, queries } from "stores/backend" import { datasources, integrations, queries } from "stores/backend"
import { getBindableProperties } from "builderStore/dataBinding" import { getBindableProperties } from "builderStore/dataBinding"
@ -25,7 +25,7 @@
} }
</script> </script>
<Layout> <Layout gap="XS">
<Select <Select
label="Datasource" label="Datasource"
bind:value={parameters.datasourceId} bind:value={parameters.datasourceId}
@ -44,22 +44,34 @@
getOptionLabel={query => query.name} getOptionLabel={query => query.name}
getOptionValue={query => query._id} getOptionValue={query => query._id}
/> />
{/if}
{#if query?.parameters?.length > 0} {#if parameters.queryId}
<div> <Checkbox text="Require confirmation" bind:value={parameters.confirm} />
<ParameterBuilder
bind:customParams={parameters.queryParams} {#if parameters.confirm}
parameters={query.parameters} <Input
bindings={bindableProperties} label="Confirm text"
/> placeholder="Are you sure you want to execute this query?"
<IntegrationQueryEditor bind:value={parameters.confirmText}
height={200} />
{query} {/if}
schema={fetchQueryDefinition(query)}
editable={false} {#if query?.parameters?.length > 0}
{datasource} <div>
/> <ParameterBuilder
</div> bind:customParams={parameters.queryParams}
parameters={query.parameters}
bindings={bindableProperties}
/>
<IntegrationQueryEditor
height={200}
{query}
schema={fetchQueryDefinition(query)}
editable={false}
{datasource}
/>
</div>
{/if}
{/if}
{/if} {/if}
</Layout> </Layout>

View File

@ -79,7 +79,7 @@
on:click={() => removeField(field[0])} on:click={() => removeField(field[0])}
/> />
{/each} {/each}
<div> <div style="margin-top: 10px">
<Button icon="AddCircle" secondary on:click={addField}> <Button icon="AddCircle" secondary on:click={addField}>
Add Add
{fieldLabel} {fieldLabel}

View File

@ -1,5 +1,5 @@
<script> <script>
import { Select, Label, Body } from "@budibase/bbui" import { Select, Label, Body, Checkbox, Input } from "@budibase/bbui"
import { store, currentAsset } from "builderStore" import { store, currentAsset } from "builderStore"
import { tables } from "stores/backend" import { tables } from "stores/backend"
import { import {
@ -33,7 +33,8 @@
optional.<br /> optional.<br />
You can always add or override fields manually. You can always add or override fields manually.
</Body> </Body>
<div class="fields">
<div class="params">
<Label small>Data Source</Label> <Label small>Data Source</Label>
<Select <Select
bind:value={parameters.providerId} bind:value={parameters.providerId}
@ -51,37 +52,58 @@
getOptionValue={option => option._id} 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 <SaveFields
parameterFields={parameters.fields} parameterFields={parameters.fields}
{schemaFields} {schemaFields}
on:change={onFieldsChanged} on:change={onFieldsChanged}
/> />
{/if} </div>
</div> {/if}
</div> </div>
<style> <style>
.root { .root {
width: 100%;
max-width: 800px; max-width: 800px;
margin: 0 auto; margin: 0 auto;
display: flex;
flex-direction: column;
justify-content: flex-start;
align-items: stretch;
gap: var(--spacing-xl);
} }
.root :global(p) { .root :global(p) {
line-height: 1.5; 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 { .fields {
display: grid; display: grid;
column-gap: var(--spacing-l); column-gap: var(--spacing-l);
row-gap: var(--spacing-s); row-gap: var(--spacing-s);
grid-template-columns: auto 1fr auto 1fr auto; grid-template-columns: 60px 1fr auto 1fr auto;
align-items: center; align-items: center;
} }
.fields :global(> div:nth-child(2)),
.fields :global(> div:nth-child(4)) {
grid-column-start: 2;
grid-column-end: 6;
}
</style> </style>

View File

@ -1,5 +1,5 @@
<script> <script>
import { Select, Label, Input } from "@budibase/bbui" import { Select, Label, Input, Checkbox } from "@budibase/bbui"
import { automationStore } from "builderStore" import { automationStore } from "builderStore"
import SaveFields from "./SaveFields.svelte" import SaveFields from "./SaveFields.svelte"
@ -72,7 +72,7 @@
</div> </div>
</div> </div>
<div class="fields"> <div class="params">
<Label small>Automation</Label> <Label small>Automation</Label>
{#if automationStatus === AUTOMATION_STATUS.EXISTING} {#if automationStatus === AUTOMATION_STATUS.EXISTING}
@ -90,6 +90,19 @@
/> />
{/if} {/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} {#key parameters.automationId}
<SaveFields <SaveFields
schemaFields={selectedSchema} schemaFields={selectedSchema}
@ -107,16 +120,21 @@
margin: 0 auto; margin: 0 auto;
} }
.fields { .params {
display: grid; display: grid;
column-gap: var(--spacing-l); column-gap: var(--spacing-l);
row-gap: var(--spacing-s); row-gap: var(--spacing-s);
grid-template-columns: auto 1fr auto 1fr auto; grid-template-columns: 60px 1fr;
align-items: baseline; align-items: center;
} }
.fields :global(> div:nth-child(2)) { .fields {
grid-column: 2 / span 4; 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, .radios,

View File

@ -24,15 +24,12 @@
<style> <style>
.root { .root {
display: flex; display: grid;
flex-direction: row; column-gap: var(--spacing-l);
row-gap: var(--spacing-s);
grid-template-columns: 60px 1fr;
align-items: center; align-items: center;
max-width: 800px; max-width: 800px;
margin: 0 auto; margin: 0 auto;
} }
.root :global(> div) {
flex: 1;
margin-left: var(--spacing-l);
}
</style> </style>

View File

@ -3,6 +3,7 @@
import { setContext, onMount } from "svelte" import { setContext, onMount } from "svelte"
import Component from "./Component.svelte" import Component from "./Component.svelte"
import NotificationDisplay from "./NotificationDisplay.svelte" import NotificationDisplay from "./NotificationDisplay.svelte"
import ConfirmationDisplay from "./ConfirmationDisplay.svelte"
import Provider from "./Provider.svelte" import Provider from "./Provider.svelte"
import SDK from "../sdk" import SDK from "../sdk"
import { import {
@ -70,6 +71,7 @@
{/key} {/key}
</div> </div>
<NotificationDisplay /> <NotificationDisplay />
<ConfirmationDisplay />
<!-- Key block needs to be outside the if statement or it breaks --> <!-- Key block needs to be outside the if statement or it breaks -->
{#key $builderStore.selectedComponentId} {#key $builderStore.selectedComponentId}
{#if $builderStore.inBuilder} {#if $builderStore.inBuilder}

View File

@ -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}

View File

@ -1,40 +1,11 @@
import * as API from "../api" import * as API from "../api"
import { writable, get } from "svelte/store" import { writable, get } from "svelte/store"
import { initialise } from "./initialise"
import { routeStore } from "./routes"
import { builderStore } from "./builder" import { builderStore } from "./builder"
import { TableNames } from "../constants" import { TableNames } from "../constants"
const createAuthStore = () => { const createAuthStore = () => {
const store = writable(null) 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 // Fetches the user object if someone is logged in and has reloaded the page
const fetchUser = async () => { const fetchUser = async () => {
// Fetch the first user if inside the builder // Fetch the first user if inside the builder
@ -54,7 +25,7 @@ const createAuthStore = () => {
return { return {
subscribe: store.subscribe, subscribe: store.subscribe,
actions: { logIn, logOut, fetchUser }, actions: { fetchUser },
} }
} }

View File

@ -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()

View File

@ -4,6 +4,7 @@ export { routeStore } from "./routes"
export { screenStore } from "./screens" export { screenStore } from "./screens"
export { builderStore } from "./builder" export { builderStore } from "./builder"
export { dataSourceStore } from "./dataSource" export { dataSourceStore } from "./dataSource"
export { confirmationStore } from "./confirmation"
// Context stores are layered and duplicated, so it is not a singleton // Context stores are layered and duplicated, so it is not a singleton
export { createContextStore } from "./context" export { createContextStore } from "./context"

View File

@ -1,5 +1,5 @@
import { get } from "svelte/store" 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 { saveRow, deleteRow, executeQuery, triggerAutomation } from "../api"
import { ActionTypes } from "../constants" 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 = { const handlerMap = {
["Save Row"]: saveRowHandler, ["Save Row"]: saveRowHandler,
["Delete Row"]: deleteRowHandler, ["Delete Row"]: deleteRowHandler,
@ -85,13 +76,19 @@ const handlerMap = {
["Trigger Automation"]: triggerAutomationHandler, ["Trigger Automation"]: triggerAutomationHandler,
["Validate Form"]: validateFormHandler, ["Validate Form"]: validateFormHandler,
["Refresh Datasource"]: refreshDatasourceHandler, ["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 * Parses an array of actions and returns a function which will execute the
* actions in the current context. * actions in the current context.
* A handler returning `false` is a flag to stop execution of handlers
*/ */
export const enrichButtonActions = (actions, context) => { export const enrichButtonActions = (actions, context) => {
// Prevent button actions in the builder preview // Prevent button actions in the builder preview
@ -102,15 +99,44 @@ export const enrichButtonActions = (actions, context) => {
return async () => { return async () => {
for (let i = 0; i < handlers.length; i++) { for (let i = 0; i < handlers.length; i++) {
try { try {
const result = await handlers[i](actions[i], context) const action = actions[i]
// A handler returning `false` is a flag to stop execution of handlers const callback = async () => handlers[i](action, context)
if (result === false) {
// 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 return
} }
// For non-confirmable actions, execute the handler immediately
else {
const result = await callback()
if (result === false) {
return
}
}
} catch (error) { } catch (error) {
console.error("Error while executing button handler") console.error("Error while executing button handler")
console.error(error) console.error(error)
// Stop executing on an error // Stop executing further actions on error
return return
} }
} }