Merge pull request #2502 from Budibase/lab-day-state

App state
This commit is contained in:
Andrew Kingston 2021-09-01 16:08:13 +01:00 committed by GitHub
commit 1b9916a89d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 233 additions and 22 deletions

View File

@ -1,6 +1,10 @@
import { cloneDeep } from "lodash/fp"
import { get } from "svelte/store"
import { findComponent, findComponentPath } from "./storeUtils"
import {
findComponent,
findComponentPath,
findAllMatchingComponents,
} from "./storeUtils"
import { store } from "builderStore"
import { tables as tablesStore, queries as queriesStores } from "stores/backend"
import { makePropSafe } from "@budibase/string-templates"
@ -18,7 +22,9 @@ export const getBindableProperties = (asset, componentId) => {
const userBindings = getUserBindings()
const urlBindings = getUrlBindings(asset)
const deviceBindings = getDeviceBindings()
const stateBindings = getStateBindings()
return [
...stateBindings,
...deviceBindings,
...urlBindings,
...contextBindings,
@ -256,6 +262,18 @@ const getDeviceBindings = () => {
return bindings
}
/**
* Gets all state bindings that are globally available.
*/
const getStateBindings = () => {
const safeState = makePropSafe("state")
return getAllStateVariables().map(key => ({
type: "context",
runtimeBinding: `${safeState}.${makePropSafe(key)}`,
readableBinding: `State.${key}`,
}))
}
/**
* Gets all bindable properties from URL parameters.
*/
@ -458,3 +476,49 @@ export function runtimeToReadableBinding(bindableProperties, textWithBindings) {
"readableBinding"
)
}
/**
* Returns an array of the keys of any state variables which are set anywhere
* in the app.
*/
export const getAllStateVariables = () => {
let allComponents = []
// Find all onClick settings in all layouts
get(store).layouts.forEach(layout => {
const components = findAllMatchingComponents(
layout.props,
c => c.onClick != null
)
allComponents = allComponents.concat(components || [])
})
// Find all onClick settings in all screens
get(store).screens.forEach(screen => {
const components = findAllMatchingComponents(
screen.props,
c => c.onClick != null
)
allComponents = allComponents.concat(components || [])
})
// Add state bindings for all state actions
let bindingSet = new Set()
allComponents.forEach(component => {
if (!Array.isArray(component.onClick)) {
return
}
component.onClick.forEach(action => {
if (
action["##eventHandlerType"] === "Update State" &&
action.parameters?.type === "set" &&
action.parameters?.key &&
action.parameters?.value
) {
bindingSet.add(action.parameters.key)
}
})
})
return Array.from(bindingSet)
}

View File

@ -0,0 +1,65 @@
<script>
import { Select, Label, Combobox, Checkbox, Body } from "@budibase/bbui"
import { onMount } from "svelte"
import DrawerBindableInput from "components/common/bindings/DrawerBindableInput.svelte"
import { getAllStateVariables } from "builderStore/dataBinding"
export let parameters
export let bindings = []
const keyOptions = getAllStateVariables()
const typeOptions = [
{
label: "Set value",
value: "set",
},
{
label: "Delete value",
value: "delete",
},
]
onMount(() => {
if (!parameters.type) {
parameters.type = "set"
}
})
</script>
<div class="root">
<Label small>Type</Label>
<Select
placeholder={null}
bind:value={parameters.type}
options={typeOptions}
/>
<Label small>Key</Label>
<Combobox bind:value={parameters.key} options={keyOptions} />
{#if parameters.type === "set"}
<Label small>Value</Label>
<DrawerBindableInput
{bindings}
value={parameters.value}
on:change={e => (parameters.value = e.detail)}
/>
<div />
<Checkbox bind:value={parameters.persist} text="Persist this value" />
<div />
<Body size="XS">
Persisted values will remain even after reloading the page or closing the
browser.
</Body>
{/if}
</div>
<style>
.root {
display: grid;
column-gap: var(--spacing-l);
row-gap: var(--spacing-s);
grid-template-columns: 60px 1fr;
align-items: center;
max-width: 400px;
margin: 0 auto;
}
</style>

View File

@ -8,6 +8,7 @@ import LogOut from "./LogOut.svelte"
import ClearForm from "./ClearForm.svelte"
import CloseScreenModal from "./CloseScreenModal.svelte"
import ChangeFormStep from "./ChangeFormStep.svelte"
import UpdateStateStep from "./UpdateState.svelte"
// Defines which actions are available to configure in the front end.
// Unfortunately the "name" property is used as the identifier so please don't
@ -57,4 +58,8 @@ export default [
name: "Change Form Step",
component: ChangeFormStep,
},
{
name: "Update State",
component: UpdateStateStep,
},
]

View File

@ -22,6 +22,7 @@
import ErrorSVG from "../../../builder/assets/error.svg"
import UserBindingsProvider from "./UserBindingsProvider.svelte"
import DeviceBindingsProvider from "./DeviceBindingsProvider.svelte"
import StateBindingsProvider from "./StateBindingsProvider.svelte"
// Provide contexts
setContext("sdk", SDK)
@ -85,28 +86,30 @@
{:else if $screenStore.activeLayout}
<UserBindingsProvider>
<DeviceBindingsProvider>
<div id="app-root" class:preview={$builderStore.inBuilder}>
{#key $screenStore.activeLayout._id}
<Component instance={$screenStore.activeLayout.props} />
<StateBindingsProvider>
<div id="app-root" class:preview={$builderStore.inBuilder}>
{#key $screenStore.activeLayout._id}
<Component instance={$screenStore.activeLayout.props} />
{/key}
</div>
<NotificationDisplay />
<ConfirmationDisplay />
<PeekScreenDisplay />
<!-- Key block needs to be outside the if statement or it breaks -->
{#key $builderStore.selectedComponentId}
{#if $builderStore.inBuilder}
<SettingsBar />
{/if}
{/key}
</div>
<NotificationDisplay />
<ConfirmationDisplay />
<PeekScreenDisplay />
<!-- Key block needs to be outside the if statement or it breaks -->
{#key $builderStore.selectedComponentId}
<!--
We don't want to key these by componentID as they control their own
re-mounting to avoid flashes.
-->
{#if $builderStore.inBuilder}
<SettingsBar />
<SelectionIndicator />
<HoverIndicator />
{/if}
{/key}
<!--
We don't want to key these by componentID as they control their own
re-mounting to avoid flashes.
-->
{#if $builderStore.inBuilder}
<SelectionIndicator />
<HoverIndicator />
{/if}
</StateBindingsProvider>
</DeviceBindingsProvider>
</UserBindingsProvider>
{/if}

View File

@ -0,0 +1,8 @@
<script>
import Provider from "./Provider.svelte"
import { stateStore } from "../store"
</script>
<Provider key="state" data={$stateStore}>
<slot />
</Provider>

View File

@ -7,6 +7,7 @@ export { builderStore } from "./builder"
export { dataSourceStore } from "./dataSource"
export { confirmationStore } from "./confirmation"
export { peekStore } from "./peek"
export { stateStore } from "./state"
// Context stores are layered and duplicated, so it is not a singleton
export { createContextStore } from "./context"

View File

@ -0,0 +1,54 @@
import { writable, get, derived } from "svelte/store"
import { localStorageStore } from "../../../builder/src/builderStore/store/localStorage"
import { appStore } from "./app"
const createStateStore = () => {
const localStorageKey = `${get(appStore).appId}.state`
const persistentStore = localStorageStore(localStorageKey, {})
// Initialise the temp store to mirror the persistent store
const tempStore = writable(get(persistentStore))
// Sets a value to state, optionally persistent
const setValue = (key, value, persist = false) => {
const storeToSave = persist ? persistentStore : tempStore
const storeToClear = persist ? tempStore : persistentStore
storeToSave.update(state => {
state[key] = value
return state
})
storeToClear.update(state => {
delete state[key]
return state
})
}
// Delete a certain key from both stores
const deleteValue = key => {
const stores = [tempStore, persistentStore]
stores.forEach(store => {
store.update(state => {
delete state[key]
return state
})
})
}
// Derive the combination of both persisted and non persisted stores
const store = derived(
[tempStore, persistentStore],
([$tempStore, $persistentStore]) => {
return {
...$tempStore,
...$persistentStore,
}
}
)
return {
subscribe: store.subscribe,
actions: { setValue, deleteValue },
}
}
export const stateStore = createStateStore()

View File

@ -5,6 +5,7 @@ import {
confirmationStore,
authStore,
peekStore,
stateStore,
} from "../store"
import { saveRow, deleteRow, executeQuery, triggerAutomation } from "../api"
import { ActionTypes } from "../constants"
@ -122,6 +123,15 @@ const closeScreenModalHandler = () => {
window.dispatchEvent(new Event("close-screen-modal"))
}
const updateStateHandler = action => {
const { type, key, value, persist } = action.parameters
if (type === "set") {
stateStore.actions.setValue(key, value, persist)
} else if (type === "delete") {
stateStore.actions.deleteValue(key)
}
}
const handlerMap = {
["Save Row"]: saveRowHandler,
["Delete Row"]: deleteRowHandler,
@ -134,6 +144,7 @@ const handlerMap = {
["Clear Form"]: clearFormHandler,
["Close Screen Modal"]: closeScreenModalHandler,
["Change Form Step"]: changeFormStepHandler,
["Update State"]: updateStateHandler,
}
const confirmTextMap = {

View File

@ -105,12 +105,12 @@ export const luceneQuery = (docs, query) => {
// Process an equal match (fails if the value is different)
const equalMatch = match("equal", (key, value, doc) => {
return doc[key] !== value
return value != null && value !== "" && doc[key] !== value
})
// Process a not-equal match (fails if the value is the same)
const notEqualMatch = match("notEqual", (key, value, doc) => {
return doc[key] === value
return value != null && value !== "" && doc[key] === value
})
// Process an empty match (fails if the value is not empty)