Merge branch 'develop' of github.com:Budibase/budibase into custom-theming
This commit is contained in:
commit
551eb629b4
|
@ -1,6 +1,10 @@
|
||||||
import { cloneDeep } from "lodash/fp"
|
import { cloneDeep } from "lodash/fp"
|
||||||
import { get } from "svelte/store"
|
import { get } from "svelte/store"
|
||||||
import { findComponent, findComponentPath } from "./storeUtils"
|
import {
|
||||||
|
findComponent,
|
||||||
|
findComponentPath,
|
||||||
|
findAllMatchingComponents,
|
||||||
|
} from "./storeUtils"
|
||||||
import { store } from "builderStore"
|
import { store } from "builderStore"
|
||||||
import { tables as tablesStore, queries as queriesStores } from "stores/backend"
|
import { tables as tablesStore, queries as queriesStores } from "stores/backend"
|
||||||
import { makePropSafe } from "@budibase/string-templates"
|
import { makePropSafe } from "@budibase/string-templates"
|
||||||
|
@ -18,7 +22,9 @@ export const getBindableProperties = (asset, componentId) => {
|
||||||
const userBindings = getUserBindings()
|
const userBindings = getUserBindings()
|
||||||
const urlBindings = getUrlBindings(asset)
|
const urlBindings = getUrlBindings(asset)
|
||||||
const deviceBindings = getDeviceBindings()
|
const deviceBindings = getDeviceBindings()
|
||||||
|
const stateBindings = getStateBindings()
|
||||||
return [
|
return [
|
||||||
|
...stateBindings,
|
||||||
...deviceBindings,
|
...deviceBindings,
|
||||||
...urlBindings,
|
...urlBindings,
|
||||||
...contextBindings,
|
...contextBindings,
|
||||||
|
@ -256,6 +262,18 @@ const getDeviceBindings = () => {
|
||||||
return bindings
|
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.
|
* Gets all bindable properties from URL parameters.
|
||||||
*/
|
*/
|
||||||
|
@ -458,3 +476,49 @@ export function runtimeToReadableBinding(bindableProperties, textWithBindings) {
|
||||||
"readableBinding"
|
"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 ClearForm from "./ClearForm.svelte"
|
||||||
import CloseScreenModal from "./CloseScreenModal.svelte"
|
import CloseScreenModal from "./CloseScreenModal.svelte"
|
||||||
import ChangeFormStep from "./ChangeFormStep.svelte"
|
import ChangeFormStep from "./ChangeFormStep.svelte"
|
||||||
|
import UpdateStateStep from "./UpdateState.svelte"
|
||||||
|
|
||||||
// Defines which actions are available to configure in the front end.
|
// Defines which actions are available to configure in the front end.
|
||||||
// Unfortunately the "name" property is used as the identifier so please don't
|
// Unfortunately the "name" property is used as the identifier so please don't
|
||||||
|
@ -57,4 +58,8 @@ export default [
|
||||||
name: "Change Form Step",
|
name: "Change Form Step",
|
||||||
component: ChangeFormStep,
|
component: ChangeFormStep,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "Update State",
|
||||||
|
component: UpdateStateStep,
|
||||||
|
},
|
||||||
]
|
]
|
||||||
|
|
|
@ -18,6 +18,7 @@
|
||||||
import PeekScreenDisplay from "components/overlay/PeekScreenDisplay.svelte"
|
import PeekScreenDisplay from "components/overlay/PeekScreenDisplay.svelte"
|
||||||
import UserBindingsProvider from "components/context/UserBindingsProvider.svelte"
|
import UserBindingsProvider from "components/context/UserBindingsProvider.svelte"
|
||||||
import DeviceBindingsProvider from "components/context/DeviceBindingsProvider.svelte"
|
import DeviceBindingsProvider from "components/context/DeviceBindingsProvider.svelte"
|
||||||
|
import StateBindingsProvider from "components/context/StateBindingsProvider.svelte"
|
||||||
import SettingsBar from "components/preview/SettingsBar.svelte"
|
import SettingsBar from "components/preview/SettingsBar.svelte"
|
||||||
import SelectionIndicator from "components/preview/SelectionIndicator.svelte"
|
import SelectionIndicator from "components/preview/SelectionIndicator.svelte"
|
||||||
import HoverIndicator from "components/preview/HoverIndicator.svelte"
|
import HoverIndicator from "components/preview/HoverIndicator.svelte"
|
||||||
|
@ -85,28 +86,30 @@
|
||||||
{:else if $screenStore.activeLayout}
|
{:else if $screenStore.activeLayout}
|
||||||
<UserBindingsProvider>
|
<UserBindingsProvider>
|
||||||
<DeviceBindingsProvider>
|
<DeviceBindingsProvider>
|
||||||
<div id="app-root" class:preview={$builderStore.inBuilder}>
|
<StateBindingsProvider>
|
||||||
{#key $screenStore.activeLayout._id}
|
<div id="app-root" class:preview={$builderStore.inBuilder}>
|
||||||
<Component instance={$screenStore.activeLayout.props} />
|
{#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}
|
{/key}
|
||||||
</div>
|
<!--
|
||||||
<NotificationDisplay />
|
We don't want to key these by componentID as they control their own
|
||||||
<ConfirmationDisplay />
|
re-mounting to avoid flashes.
|
||||||
<PeekScreenDisplay />
|
-->
|
||||||
<!-- Key block needs to be outside the if statement or it breaks -->
|
|
||||||
{#key $builderStore.selectedComponentId}
|
|
||||||
{#if $builderStore.inBuilder}
|
{#if $builderStore.inBuilder}
|
||||||
<SettingsBar />
|
<SelectionIndicator />
|
||||||
|
<HoverIndicator />
|
||||||
{/if}
|
{/if}
|
||||||
{/key}
|
</StateBindingsProvider>
|
||||||
<!--
|
|
||||||
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}
|
|
||||||
</DeviceBindingsProvider>
|
</DeviceBindingsProvider>
|
||||||
</UserBindingsProvider>
|
</UserBindingsProvider>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
|
@ -0,0 +1,8 @@
|
||||||
|
<script>
|
||||||
|
import Provider from "./Provider.svelte"
|
||||||
|
import { stateStore } from "stores"
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Provider key="state" data={$stateStore}>
|
||||||
|
<slot />
|
||||||
|
</Provider>
|
|
@ -7,6 +7,7 @@ export { builderStore } from "./builder"
|
||||||
export { dataSourceStore } from "./dataSource"
|
export { dataSourceStore } from "./dataSource"
|
||||||
export { confirmationStore } from "./confirmation"
|
export { confirmationStore } from "./confirmation"
|
||||||
export { peekStore } from "./peek"
|
export { peekStore } from "./peek"
|
||||||
|
export { stateStore } from "./state"
|
||||||
|
|
||||||
// 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"
|
||||||
|
|
|
@ -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,
|
confirmationStore,
|
||||||
authStore,
|
authStore,
|
||||||
peekStore,
|
peekStore,
|
||||||
|
stateStore,
|
||||||
} from "stores"
|
} from "stores"
|
||||||
import { saveRow, deleteRow, executeQuery, triggerAutomation } from "api"
|
import { saveRow, deleteRow, executeQuery, triggerAutomation } from "api"
|
||||||
import { ActionTypes } from "constants"
|
import { ActionTypes } from "constants"
|
||||||
|
@ -122,6 +123,15 @@ const closeScreenModalHandler = () => {
|
||||||
window.dispatchEvent(new Event("close-screen-modal"))
|
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 = {
|
const handlerMap = {
|
||||||
["Save Row"]: saveRowHandler,
|
["Save Row"]: saveRowHandler,
|
||||||
["Delete Row"]: deleteRowHandler,
|
["Delete Row"]: deleteRowHandler,
|
||||||
|
@ -134,6 +144,7 @@ const handlerMap = {
|
||||||
["Clear Form"]: clearFormHandler,
|
["Clear Form"]: clearFormHandler,
|
||||||
["Close Screen Modal"]: closeScreenModalHandler,
|
["Close Screen Modal"]: closeScreenModalHandler,
|
||||||
["Change Form Step"]: changeFormStepHandler,
|
["Change Form Step"]: changeFormStepHandler,
|
||||||
|
["Update State"]: updateStateHandler,
|
||||||
}
|
}
|
||||||
|
|
||||||
const confirmTextMap = {
|
const confirmTextMap = {
|
||||||
|
|
|
@ -105,12 +105,12 @@ export const luceneQuery = (docs, query) => {
|
||||||
|
|
||||||
// Process an equal match (fails if the value is different)
|
// Process an equal match (fails if the value is different)
|
||||||
const equalMatch = match("equal", (key, value, doc) => {
|
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)
|
// Process a not-equal match (fails if the value is the same)
|
||||||
const notEqualMatch = match("notEqual", (key, value, doc) => {
|
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)
|
// Process an empty match (fails if the value is not empty)
|
||||||
|
|
Loading…
Reference in New Issue