commit
1b9916a89d
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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>
|
|
@ -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,
|
||||
},
|
||||
]
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -0,0 +1,8 @@
|
|||
<script>
|
||||
import Provider from "./Provider.svelte"
|
||||
import { stateStore } from "../store"
|
||||
</script>
|
||||
|
||||
<Provider key="state" data={$stateStore}>
|
||||
<slot />
|
||||
</Provider>
|
|
@ -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"
|
||||
|
|
|
@ -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()
|
|
@ -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 = {
|
||||
|
|
|
@ -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)
|
||||
|
|
Loading…
Reference in New Issue