Optimise client library performance with skeleton loaders (#9256)
* Treat empty string default values as nullish when considering whether field default values should be applied to the real field value * Add flag to allow not loading data immediately upon creation of a data fetch object * Use loading context inside forms to avoid wasted renders while form schema are loading * Wait for parent data providers to finish loading before loading their own data * Optimise data provider to reduce updates as much as possible * Allow forms to render content immediately again, but use the loading context to inform data providers to wait for them * Remove fetchOnCreation flag for data fetches as now redundant * Fix issue with deleting the selected button action when the next action has no parameters * Lint
This commit is contained in:
parent
dd58f79763
commit
b9cb2d9e78
|
@ -74,8 +74,18 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
const deleteAction = index => {
|
const deleteAction = index => {
|
||||||
|
// Check if we're deleting the selected action
|
||||||
|
const selectedIndex = actions.indexOf(selectedAction)
|
||||||
|
const isSelected = index === selectedIndex
|
||||||
|
|
||||||
|
// Delete the action
|
||||||
actions.splice(index, 1)
|
actions.splice(index, 1)
|
||||||
actions = actions
|
actions = actions
|
||||||
|
|
||||||
|
// Select a new action if we deleted the selected one
|
||||||
|
if (isSelected) {
|
||||||
|
selectedAction = actions?.length ? actions[0] : null
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const toggleActionList = () => {
|
const toggleActionList = () => {
|
||||||
|
|
|
@ -171,6 +171,15 @@
|
||||||
$: pad = pad || (interactive && hasChildren && inDndPath)
|
$: pad = pad || (interactive && hasChildren && inDndPath)
|
||||||
$: $dndIsDragging, (pad = false)
|
$: $dndIsDragging, (pad = false)
|
||||||
|
|
||||||
|
// Determine whether we should render a skeleton loader for this component
|
||||||
|
$: showSkeleton =
|
||||||
|
$loading &&
|
||||||
|
definition.name !== "Screenslot" &&
|
||||||
|
children.length === 0 &&
|
||||||
|
!instance._blockElementHasChildren &&
|
||||||
|
!definition.block &&
|
||||||
|
definition.skeleton !== false
|
||||||
|
|
||||||
// Update component context
|
// Update component context
|
||||||
$: store.set({
|
$: store.set({
|
||||||
id,
|
id,
|
||||||
|
@ -473,14 +482,6 @@
|
||||||
componentStore.actions.unregisterInstance(id)
|
componentStore.actions.unregisterInstance(id)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
$: showSkeleton =
|
|
||||||
$loading &&
|
|
||||||
definition.name !== "Screenslot" &&
|
|
||||||
children.length === 0 &&
|
|
||||||
!instance._blockElementHasChildren &&
|
|
||||||
!definition.block &&
|
|
||||||
definition.skeleton !== false
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if showSkeleton}
|
{#if showSkeleton}
|
||||||
|
|
|
@ -11,20 +11,23 @@
|
||||||
export let limit
|
export let limit
|
||||||
export let paginate
|
export let paginate
|
||||||
|
|
||||||
const loading = writable(false)
|
|
||||||
|
|
||||||
const { styleable, Provider, ActionTypes, API } = getContext("sdk")
|
const { styleable, Provider, ActionTypes, API } = getContext("sdk")
|
||||||
const component = getContext("component")
|
const component = getContext("component")
|
||||||
|
|
||||||
|
// Update loading state
|
||||||
|
const parentLoading = getContext("loading")
|
||||||
|
const loading = writable(true)
|
||||||
|
setContext("loading", loading)
|
||||||
|
|
||||||
// We need to manage our lucene query manually as we want to allow components
|
// We need to manage our lucene query manually as we want to allow components
|
||||||
// to extend it
|
// to extend it
|
||||||
let queryExtensions = {}
|
let queryExtensions = {}
|
||||||
$: defaultQuery = LuceneUtils.buildLuceneQuery(filter)
|
$: defaultQuery = LuceneUtils.buildLuceneQuery(filter)
|
||||||
$: query = extendQuery(defaultQuery, queryExtensions)
|
$: query = extendQuery(defaultQuery, queryExtensions)
|
||||||
|
|
||||||
// Keep our data fetch instance up to date
|
// Fetch data and refresh when needed
|
||||||
$: fetch = createFetch(dataSource)
|
$: fetch = createFetch(dataSource, $parentLoading)
|
||||||
$: fetch.update({
|
$: updateFetch({
|
||||||
query,
|
query,
|
||||||
sortColumn,
|
sortColumn,
|
||||||
sortOrder,
|
sortOrder,
|
||||||
|
@ -32,6 +35,9 @@
|
||||||
paginate,
|
paginate,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Keep loading context updated
|
||||||
|
$: loading.set($parentLoading || !$fetch.loaded)
|
||||||
|
|
||||||
// Build our action context
|
// Build our action context
|
||||||
$: actions = [
|
$: actions = [
|
||||||
{
|
{
|
||||||
|
@ -80,14 +86,21 @@
|
||||||
sortColumn: $fetch.sortColumn,
|
sortColumn: $fetch.sortColumn,
|
||||||
sortOrder: $fetch.sortOrder,
|
sortOrder: $fetch.sortOrder,
|
||||||
},
|
},
|
||||||
limit: limit,
|
limit,
|
||||||
}
|
}
|
||||||
|
|
||||||
const parentLoading = getContext("loading")
|
const createFetch = (datasource, parentLoading) => {
|
||||||
setContext("loading", loading)
|
// Return a dummy fetch if parent is still loading. We do this so that we
|
||||||
$: loading.set($parentLoading || !$fetch.loaded)
|
// can still properly subscribe to a valid fetch object and check all
|
||||||
|
// properties, but we want to avoid fetching the real data until all parents
|
||||||
|
// have finished loading.
|
||||||
|
// This logic is only needed due to skeleton loaders, as previously we
|
||||||
|
// simply blocked component rendering until data was ready.
|
||||||
|
if (parentLoading) {
|
||||||
|
return fetchData({ API })
|
||||||
|
}
|
||||||
|
|
||||||
const createFetch = datasource => {
|
// Otherwise return the real thing
|
||||||
return fetchData({
|
return fetchData({
|
||||||
API,
|
API,
|
||||||
datasource,
|
datasource,
|
||||||
|
@ -101,6 +114,14 @@
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const updateFetch = opts => {
|
||||||
|
// Only update fetch if parents have stopped loading. Otherwise we will
|
||||||
|
// trigger a fetch of the real data before parents are ready.
|
||||||
|
if (!$parentLoading) {
|
||||||
|
fetch.update(opts)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const addQueryExtension = (key, extension) => {
|
const addQueryExtension = (key, extension) => {
|
||||||
if (!key || !extension) {
|
if (!key || !extension) {
|
||||||
return
|
return
|
||||||
|
|
|
@ -1,7 +1,8 @@
|
||||||
<script>
|
<script>
|
||||||
import { getContext } from "svelte"
|
import { getContext, setContext } from "svelte"
|
||||||
import InnerForm from "./InnerForm.svelte"
|
import InnerForm from "./InnerForm.svelte"
|
||||||
import { Helpers } from "@budibase/bbui"
|
import { Helpers } from "@budibase/bbui"
|
||||||
|
import { writable } from "svelte/store"
|
||||||
|
|
||||||
export let dataSource
|
export let dataSource
|
||||||
export let theme
|
export let theme
|
||||||
|
@ -20,10 +21,17 @@
|
||||||
const context = getContext("context")
|
const context = getContext("context")
|
||||||
const { API, fetchDatasourceSchema } = getContext("sdk")
|
const { API, fetchDatasourceSchema } = getContext("sdk")
|
||||||
|
|
||||||
|
// Forms also use loading context as they require loading a schema
|
||||||
|
const parentLoading = getContext("loading")
|
||||||
|
const loading = writable(true)
|
||||||
|
setContext("loading", loading)
|
||||||
|
|
||||||
|
let loaded = false
|
||||||
let schema
|
let schema
|
||||||
let table
|
let table
|
||||||
|
|
||||||
$: fetchSchema(dataSource)
|
$: fetchSchema(dataSource)
|
||||||
|
$: loading.set($parentLoading || !loaded)
|
||||||
|
|
||||||
// Returns the closes data context which isn't a built in context
|
// Returns the closes data context which isn't a built in context
|
||||||
const getInitialValues = (type, dataSource, context) => {
|
const getInitialValues = (type, dataSource, context) => {
|
||||||
|
@ -55,11 +63,14 @@
|
||||||
}
|
}
|
||||||
const res = await fetchDatasourceSchema(dataSource)
|
const res = await fetchDatasourceSchema(dataSource)
|
||||||
schema = res || {}
|
schema = res || {}
|
||||||
|
if (!loaded) {
|
||||||
|
loaded = true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$: initialValues = getInitialValues(actionType, dataSource, $context)
|
$: initialValues = getInitialValues(actionType, dataSource, $context)
|
||||||
$: resetKey = Helpers.hashString(
|
$: resetKey = Helpers.hashString(
|
||||||
!!schema +
|
loaded +
|
||||||
JSON.stringify(initialValues) +
|
JSON.stringify(initialValues) +
|
||||||
JSON.stringify(dataSource) +
|
JSON.stringify(dataSource) +
|
||||||
disabled
|
disabled
|
||||||
|
|
|
@ -128,21 +128,15 @@
|
||||||
return fields.find(field => get(field).name === name)
|
return fields.find(field => get(field).name === name)
|
||||||
}
|
}
|
||||||
|
|
||||||
const getDefault = (defaultValue, schema, type) => {
|
// Sanitises a value by ensuring it doesn't contain any invalid data
|
||||||
// Remove any values not present in the field schema
|
const sanitiseValue = (value, schema, type) => {
|
||||||
// Convert any values supplied to string
|
// Check arrays - remove any values not present in the field schema and
|
||||||
if (Array.isArray(defaultValue) && type == "array" && schema) {
|
// convert any values supplied to strings
|
||||||
return defaultValue.reduce((acc, entry) => {
|
if (Array.isArray(value) && type === "array" && schema) {
|
||||||
let processedOption = String(entry)
|
const options = schema?.constraints.inclusion || []
|
||||||
let schemaOptions = schema.constraints.inclusion
|
return value.map(opt => String(opt)).filter(opt => options.includes(opt))
|
||||||
if (schemaOptions.indexOf(processedOption) > -1) {
|
|
||||||
acc.push(processedOption)
|
|
||||||
}
|
|
||||||
return acc
|
|
||||||
}, [])
|
|
||||||
} else {
|
|
||||||
return defaultValue
|
|
||||||
}
|
}
|
||||||
|
return value
|
||||||
}
|
}
|
||||||
|
|
||||||
const formApi = {
|
const formApi = {
|
||||||
|
@ -160,7 +154,6 @@
|
||||||
|
|
||||||
// Create validation function based on field schema
|
// Create validation function based on field schema
|
||||||
const schemaConstraints = schema?.[field]?.constraints
|
const schemaConstraints = schema?.[field]?.constraints
|
||||||
|
|
||||||
const validator = disableValidation
|
const validator = disableValidation
|
||||||
? null
|
? null
|
||||||
: createValidatorFromConstraints(
|
: createValidatorFromConstraints(
|
||||||
|
@ -170,10 +163,11 @@
|
||||||
table
|
table
|
||||||
)
|
)
|
||||||
|
|
||||||
const parsedDefault = getDefault(defaultValue, schema?.[field], type)
|
// Sanitise the default value to ensure it doesn't contain invalid data
|
||||||
|
defaultValue = sanitiseValue(defaultValue, schema?.[field], type)
|
||||||
|
|
||||||
// If we've already registered this field then keep some existing state
|
// If we've already registered this field then keep some existing state
|
||||||
let initialValue = Helpers.deepGet(initialValues, field) ?? parsedDefault
|
let initialValue = Helpers.deepGet(initialValues, field) ?? defaultValue
|
||||||
let initialError = null
|
let initialError = null
|
||||||
let fieldId = `id-${Helpers.uuid()}`
|
let fieldId = `id-${Helpers.uuid()}`
|
||||||
const existingField = getField(field)
|
const existingField = getField(field)
|
||||||
|
@ -183,7 +177,9 @@
|
||||||
|
|
||||||
// Determine the initial value for this field, reusing the current
|
// Determine the initial value for this field, reusing the current
|
||||||
// value if one exists
|
// value if one exists
|
||||||
initialValue = fieldState.value ?? initialValue
|
if (fieldState.value != null && fieldState.value !== "") {
|
||||||
|
initialValue = fieldState.value
|
||||||
|
}
|
||||||
|
|
||||||
// If this field has already been registered and we previously had an
|
// If this field has already been registered and we previously had an
|
||||||
// error set, then re-run the validator to see if we can unset it
|
// error set, then re-run the validator to see if we can unset it
|
||||||
|
@ -206,11 +202,11 @@
|
||||||
error: initialError,
|
error: initialError,
|
||||||
disabled:
|
disabled:
|
||||||
disabled || fieldDisabled || (isAutoColumn && !editAutoColumns),
|
disabled || fieldDisabled || (isAutoColumn && !editAutoColumns),
|
||||||
defaultValue: parsedDefault,
|
defaultValue,
|
||||||
validator,
|
validator,
|
||||||
lastUpdate: Date.now(),
|
lastUpdate: Date.now(),
|
||||||
},
|
},
|
||||||
fieldApi: makeFieldApi(field, parsedDefault),
|
fieldApi: makeFieldApi(field),
|
||||||
fieldSchema: schema?.[field] ?? {},
|
fieldSchema: schema?.[field] ?? {},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -225,18 +221,9 @@
|
||||||
return fieldInfo
|
return fieldInfo
|
||||||
},
|
},
|
||||||
validate: () => {
|
validate: () => {
|
||||||
let valid = true
|
return fields
|
||||||
let validationFields = fields
|
.filter(field => get(field).step === get(currentStep))
|
||||||
|
.every(field => get(field).fieldApi.validate())
|
||||||
validationFields = fields.filter(f => get(f).step === get(currentStep))
|
|
||||||
|
|
||||||
// Validate fields and check if any are invalid
|
|
||||||
validationFields.forEach(field => {
|
|
||||||
if (!get(field).fieldApi.validate()) {
|
|
||||||
valid = false
|
|
||||||
}
|
|
||||||
})
|
|
||||||
return valid
|
|
||||||
},
|
},
|
||||||
reset: () => {
|
reset: () => {
|
||||||
// Reset the form by resetting each individual field
|
// Reset the form by resetting each individual field
|
||||||
|
|
Loading…
Reference in New Issue