budibase/packages/builder/src/stores/builder/screenComponent.ts

282 lines
7.6 KiB
TypeScript

import { derived } from "svelte/store"
import { tables } from "./tables"
import { selectedScreen } from "./screens"
import { viewsV2 } from "./viewsV2"
import {
UIDatasourceType,
Component,
UIComponentError,
ComponentDefinition,
DependsOnComponentSetting,
} from "@budibase/types"
import { queries } from "./queries"
import { views } from "./views"
import { findAllComponents } from "@/helpers/components"
import { bindings } from "@/helpers"
import { getBindableProperties } from "@/dataBinding"
import { componentStore } from "./components"
import { getSettingsDefinition } from "@budibase/frontend-core"
function reduceBy<TItem extends {}, TKey extends keyof TItem>(
key: TKey,
list: TItem[]
): Record<string, TItem> {
return list.reduce<Record<string, TItem>>((result, item) => {
result[item[key] as string] = item
return result
}, {})
}
const friendlyNameByType: Partial<Record<UIDatasourceType, string>> = {
viewV2: "view",
}
const validationKeyByType: Record<UIDatasourceType, string | null> = {
table: "tableId",
view: "name",
viewV2: "id",
query: "_id",
custom: null,
link: "rowId",
field: "value",
jsonarray: "value",
}
export const screenComponentsList = derived(
[selectedScreen],
([$selectedScreen]): Component[] => {
if (!$selectedScreen) {
return []
}
return findAllComponents($selectedScreen.props)
}
)
export const screenComponentErrorList = derived(
[selectedScreen, tables, views, viewsV2, queries, componentStore],
([
$selectedScreen,
$tables,
$views,
$viewsV2,
$queries,
$componentStore,
]): UIComponentError[] => {
if (!$selectedScreen) {
return []
}
const datasources = {
...reduceBy("_id", $tables.list),
...reduceBy("name", $views.list),
...reduceBy("id", $viewsV2.list),
...reduceBy("_id", $queries.list),
}
const { components: definitions } = $componentStore
const errors: UIComponentError[] = []
function checkComponentErrors(component: Component, ancestors: string[]) {
errors.push(...getInvalidDatasources(component, datasources, definitions))
errors.push(...getMissingRequiredSettings(component, definitions))
errors.push(...getMissingAncestors(component, definitions, ancestors))
for (const child of component._children || []) {
checkComponentErrors(child, [...ancestors, component._component])
}
}
checkComponentErrors($selectedScreen?.props, [])
return errors
}
)
function getInvalidDatasources(
component: Component,
datasources: Record<string, any>,
definitions: Record<string, ComponentDefinition>
) {
const result: UIComponentError[] = []
const datasourceTypes = ["table", "dataSource"]
const possibleSettings = definitions[component._component]?.settings?.filter(
s => datasourceTypes.includes(s.type)
)
if (possibleSettings) {
for (const setting of possibleSettings) {
const componentSettings = component[setting.key]
if (!componentSettings) {
continue
}
const { label } = componentSettings
const type = componentSettings.type as UIDatasourceType
const validationKey = validationKeyByType[type]
if (!validationKey) {
continue
}
const componentBindings = getBindableProperties(screen, component._id)
const componentDatasources = {
...reduceBy("rowId", bindings.extractRelationships(componentBindings)),
...reduceBy("value", bindings.extractFields(componentBindings)),
...reduceBy(
"value",
bindings.extractJSONArrayFields(componentBindings)
),
}
const resourceId = componentSettings[validationKey]
if (!{ ...datasources, ...componentDatasources }[resourceId]) {
const friendlyTypeName = friendlyNameByType[type] ?? type
result.push({
componentId: component._id!,
key: setting.key,
label: setting.label || setting.key,
message: `The ${friendlyTypeName} named "${label}" could not be found`,
errorType: "setting",
cause: "invalid",
})
}
}
}
return result
}
function parseDependsOn(dependsOn: DependsOnComponentSetting | undefined): {
key?: string
value?: string
} {
if (dependsOn === undefined) {
return {}
}
if (typeof dependsOn === "string") {
return { key: dependsOn }
}
return { key: dependsOn.setting, value: dependsOn.value }
}
function getMissingRequiredSettings(
component: Component,
definitions: Record<string, ComponentDefinition>
) {
const result: UIComponentError[] = []
const definition = definitions[component._component]
const settings = getSettingsDefinition(definition)
const missingRequiredSettings = settings.filter(setting => {
let empty = component[setting.key] == null || component[setting.key] === ""
let missing = setting.required && empty
// Check if this setting depends on another, as it may not be required
if (setting.dependsOn) {
const { key: dependsOnKey, value: dependsOnValue } = parseDependsOn(
setting.dependsOn
)
const realDependentValue =
component[dependsOnKey as keyof typeof component]
const { key: sectionDependsOnKey, value: sectionDependsOnValue } =
parseDependsOn(setting.sectionDependsOn)
const sectionRealDependentValue =
component[sectionDependsOnKey as keyof typeof component]
if (dependsOnValue == null && realDependentValue == null) {
return false
}
if (dependsOnValue != null && dependsOnValue !== realDependentValue) {
return false
}
if (
sectionDependsOnValue != null &&
sectionDependsOnValue !== sectionRealDependentValue
) {
return false
}
}
return missing
})
if (missingRequiredSettings?.length) {
result.push(
...missingRequiredSettings.map<UIComponentError>(s => ({
componentId: component._id!,
key: s.key,
label: s.label || s.key,
message: `Add the <mark>${s.label}</mark> setting to start using your component`,
errorType: "setting",
cause: "missing",
}))
)
}
return result
}
const BudibasePrefix = "@budibase/standard-components/"
function getMissingAncestors(
component: Component,
definitions: Record<string, ComponentDefinition>,
ancestors: string[]
): UIComponentError[] {
const definition = definitions[component._component]
if (!definition?.requiredAncestors?.length) {
return []
}
const result: UIComponentError[] = []
const missingAncestors = definition.requiredAncestors.filter(
ancestor => !ancestors.includes(`${BudibasePrefix}${ancestor}`)
)
if (missingAncestors.length) {
const pluralise = (name: string) => {
return name.endsWith("s") ? `${name}'` : `${name}s`
}
result.push(
...missingAncestors.map<UIComponentError>(ancestor => {
const ancestorDefinition = definitions[`${BudibasePrefix}${ancestor}`]
return {
componentId: component._id!,
message: `${pluralise(definition.name)} need to be inside a
<mark>${ancestorDefinition.name}</mark>`,
errorType: "ancestor-setting",
ancestor: {
name: ancestorDefinition.name,
fullType: `${BudibasePrefix}${ancestor}`,
},
}
})
)
}
return result
}
export const screenComponentErrors = derived(
[screenComponentErrorList],
([$list]): Record<string, UIComponentError[]> => {
return $list.reduce<Record<string, UIComponentError[]>>((obj, error) => {
obj[error.componentId] ??= []
obj[error.componentId].push(error)
return obj
}, {})
}
)