budibase/packages/builder/src/components/automation/SetupPanel/RowSelector.svelte

360 lines
8.8 KiB
Svelte

<script>
import { tables } from "@/stores/builder"
import {
ActionButton,
Popover,
Icon,
TooltipPosition,
TooltipType,
} from "@budibase/bbui"
import { createEventDispatcher } from "svelte"
import { FieldType } from "@budibase/types"
import RowSelectorTypes from "./RowSelectorTypes.svelte"
import { FIELDS } from "@/constants/backend"
import { capitalise } from "@/helpers"
import { memo } from "@budibase/frontend-core"
import PropField from "./PropField.svelte"
import { cloneDeep, isPlainObject, mergeWith } from "lodash"
const dispatch = createEventDispatcher()
export let row
export let meta
export let bindings
export let isTestModal
export let context = {}
export let componentWidth
export let fullWidth = false
const typeToField = Object.values(FIELDS).reduce((acc, field) => {
acc[field.type] = field
return acc
}, {})
const memoStore = memo({
row,
meta,
})
let table
// Row Schema Fields
let schemaFields
let attachmentTypes = [
FieldType.ATTACHMENTS,
FieldType.ATTACHMENT_SINGLE,
FieldType.SIGNATURE_SINGLE,
]
let customPopover
let popoverAnchor
let editableRow = {}
let editableFields = {}
// Avoid unnecessary updates
$: memoStore.set({
row,
meta,
})
$: parsedBindings = bindings.map(binding => {
let clone = Object.assign({}, binding)
clone.icon = clone.icon ?? "ShareAndroid"
return clone
})
$: tableId = $memoStore?.row?.tableId
$: initData(tableId, $memoStore?.meta?.fields, $memoStore?.row)
const initData = (tableId, metaFields, row) => {
if (!tableId) {
return
}
// Refesh the editable fields
editableFields = cloneDeep(metaFields || {})
// Refresh all the row data
editableRow = cloneDeep(row || {})
table = $tables.list.find(table => table._id === tableId)
schemaFields = Object.entries(table?.schema ?? {})
.filter(entry => {
const [, field] = entry
return field.type !== "formula" && !field.autocolumn
})
.sort(([nameA], [nameB]) => {
return nameA < nameB ? -1 : 1
})
if (table) {
editableRow["tableId"] = tableId
// Parse out any data not in the schema.
for (const column in editableFields) {
if (!Object.hasOwn(table?.schema, column)) {
delete editableFields[column]
}
}
}
// Go through the table schema and build out the editable content
for (const entry of schemaFields) {
const [key, fieldSchema] = entry
const emptyField =
editableRow[key] == null || editableRow[key]?.length === 0
// Put non-empty elements into the update and add their key to the fields list.
if (!emptyField && !Object.hasOwn(editableFields, key)) {
editableFields = {
...editableFields,
[key]: {},
}
}
// Legacy - clearRelationships
// Init the field and add it to the update.
if (emptyField) {
if (editableFields[key]?.clearRelationships === true) {
const emptyField = coerce(
!Object.hasOwn($memoStore?.row, key) ? "" : $memoStore?.row[key],
fieldSchema.type
)
// remove this and place the field in the editable row.
delete editableFields[key]?.clearRelationships
// Default the field
editableRow = {
...editableRow,
[key]: emptyField,
}
} else {
// Purge from the update as its presence is not necessary.
delete editableRow[key]
}
}
}
// Parse all known row schema keys
const schemaKeys = [
"tableId",
...schemaFields.map(entry => {
const [key] = entry
return key
}),
]
// Purge any row keys that are not present in the schema.
for (const rowKey of Object.keys(editableRow)) {
if (!schemaKeys.includes(rowKey)) {
delete editableRow[rowKey]
delete editableFields[rowKey]
}
}
}
// Row coerce
const coerce = (value, type) => {
const re = new RegExp(/{{([^{].*?)}}/g)
if (typeof value === "string" && re.test(value)) {
return value
}
if (type === "number") {
if (typeof value === "number") {
return value
}
return Number(value)
}
if (type === "options" || type === "boolean") {
return value
}
if (type === "array") {
if (!value) {
return []
}
if (Array.isArray(value)) {
return value
}
return value.split(",").map(x => x.trim())
}
if (type === "link") {
if (!value) {
return []
} else if (Array.isArray(value)) {
return value
}
return value.split(",").map(x => x.trim())
}
if (type === "json") {
return value.value
}
return value
}
const isFullWidth = type => {
return (
attachmentTypes.includes(type) ||
type === FieldType.JSON ||
type === FieldType.LONGFORM
)
}
const onChange = update => {
const customizer = (objValue, srcValue) => {
if (isPlainObject(objValue) && isPlainObject(srcValue)) {
const result = mergeWith({}, objValue, srcValue, customizer)
let outcome = Object.keys(result).reduce((acc, key) => {
if (result[key] !== null) {
acc[key] = result[key]
}
return acc
}, {})
return outcome
}
return srcValue
}
const result = mergeWith(
{},
{
row: editableRow,
meta: {
fields: editableFields,
},
},
update,
customizer
)
dispatch("change", result)
}
</script>
{#each schemaFields || [] as [field, schema]}
{#if !schema.autocolumn && Object.hasOwn(editableFields, field)}
<PropField
label={field}
fullWidth={fullWidth || isFullWidth(schema.type)}
{componentWidth}
>
<div class="prop-control-wrap">
<RowSelectorTypes
{isTestModal}
{field}
{schema}
bindings={parsedBindings}
value={editableRow}
meta={{
fields: editableFields,
}}
{onChange}
{context}
/>
</div>
</PropField>
{/if}
{/each}
{#if table && schemaFields}
<div
class="add-fields-btn"
class:empty={Object.is(editableFields, {})}
bind:this={popoverAnchor}
>
<PropField {componentWidth} {fullWidth}>
<div class="prop-control-wrap">
<ActionButton
on:click={() => {
customPopover.show()
}}
disabled={!schemaFields}
>
Edit fields
</ActionButton>
{#if schemaFields.length}
<ActionButton
on:click={() => {
dispatch("change", {
meta: { fields: {} },
row: {},
})
}}
>
Clear
</ActionButton>
{/if}
</div>
</PropField>
</div>
{/if}
<Popover
align="center"
bind:this={customPopover}
anchor={editableFields ? popoverAnchor : null}
useAnchorWidth
maxHeight={300}
resizable={false}
offset={10}
>
<!-- svelte-ignore a11y-click-events-have-key-events -->
<!-- svelte-ignore a11y-no-noninteractive-element-interactions -->
<ul class="spectrum-Menu" role="listbox">
{#each schemaFields || [] as [field, schema]}
{#if !schema.autocolumn}
<li
class="table_field spectrum-Menu-item"
class:is-selected={Object.hasOwn(editableFields, field)}
on:click={() => {
if (Object.hasOwn(editableFields, field)) {
delete editableFields[field]
onChange({
meta: { fields: editableFields },
row: { [field]: null },
})
} else {
editableFields[field] = {}
onChange({ meta: { fields: editableFields } })
}
}}
>
<Icon
name={typeToField?.[schema.type]?.icon}
color={"var(--spectrum-global-color-gray-600)"}
tooltip={capitalise(schema.type)}
tooltipType={TooltipType.Info}
tooltipPosition={TooltipPosition.Left}
/>
<div class="field_name spectrum-Menu-itemLabel">{field}</div>
<svg
class="spectrum-Icon spectrum-UIIcon-Checkmark100 spectrum-Menu-checkmark spectrum-Menu-itemIcon"
focusable="false"
aria-hidden="true"
>
<use xlink:href="#spectrum-css-icon-Checkmark100" />
</svg>
</li>
{/if}
{/each}
</ul>
</Popover>
<style>
.table_field {
display: flex;
padding: var(--spacing-s) var(--spacing-l);
gap: var(--spacing-s);
}
/* Override for general json field override */
.prop-control-wrap :global(.icon.json-slot-icon) {
right: 1px !important;
}
</style>