Merge pull request #1774 from Budibase/confirmable-actions
Confirmable actions
This commit is contained in:
commit
983ac8e76b
|
@ -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>
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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",
|
||||
},
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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,6 +44,16 @@
|
|||
getOptionLabel={query => query.name}
|
||||
getOptionValue={query => query._id}
|
||||
/>
|
||||
|
||||
{#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}
|
||||
|
@ -62,4 +72,6 @@
|
|||
/>
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
{/if}
|
||||
</Layout>
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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}
|
||||
/>
|
||||
|
||||
<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>
|
||||
{/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>
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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}
|
|
@ -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 },
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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()
|
|
@ -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"
|
||||
|
|
|
@ -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
|
||||
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
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue