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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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 { 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 },
}
}

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 { 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"

View File

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