1
0
Fork 0
mirror of synced 2024-09-10 06:26:02 +12:00

Full support for signature field type and some feedback changes

This commit is contained in:
Dean 2024-04-05 12:50:09 +01:00
parent fc35bfd83b
commit 1e5506b8c3
21 changed files with 572 additions and 159 deletions

View file

@ -1,65 +1,80 @@
<!--
Should this just be Canvas.svelte?
A drawing zone?
Shift it somewhere else?
width, height, toBase64, toFileObj
-->
<script>
import { onMount } from "svelte"
import Icon from "../../Icon/Icon.svelte"
import { createEventDispatcher } from "svelte"
export let canvasWidth
export let canvasHeight
const dispatch = createEventDispatcher()
let canvasRef
let canvas
let mouseDown = false
let lastOffsetX, lastOffsetY
let touching = false
let touchmove = false
let debug = true
let debugData
export let value
export let disabled = false
export let editable = true
export let width = 400
export let height = 220
export let saveIcon = false
export let isDark
export function toDataUrl() {
return canvasRef.toDataURL()
// PNG to preserve transparency
return canvasRef.toDataURL("image/png")
}
export function clear() {
return canvas.clearRect(0, 0, canvas.width, canvas.height)
export function toFile() {
const data = canvas
.getImageData(0, 0, width, height)
.data.some(channel => channel !== 0)
if (!data) {
return
}
let dataURIParts = toDataUrl().split(",")
if (!dataURIParts.length) {
console.error("Could not retrieve signature data")
}
// Pull out the base64 encoded byte data
let binaryVal = atob(dataURIParts[1])
let blobArray = new Uint8Array(binaryVal.length)
let pos = 0
while (pos < binaryVal.length) {
blobArray[pos] = binaryVal.charCodeAt(pos)
pos++
}
const signatureBlob = new Blob([blobArray], {
type: "image/png",
})
return new File([signatureBlob], "signature.png", {
type: signatureBlob.type,
})
}
const updated = (x, y) => {
export function clearCanvas() {
return canvas.clearRect(0, 0, canvasWidth, canvasHeight)
}
const updatedPos = (x, y) => {
return lastOffsetX != x || lastOffsetY != y
}
// Needs touch handling
const handleDraw = e => {
// e.touches[0] there should ol
e.preventDefault()
if (disabled || !editable) {
return
}
var rect = canvasRef.getBoundingClientRect()
const canvasX = e.offsetX || e.targetTouches?.[0].pageX - rect.left
const canvasY = e.offsetY || e.targetTouches?.[0].pageY - rect.top
const coords = { x: Math.round(canvasX), y: Math.round(canvasY) }
draw(coords.x, coords.y)
debugData = {
coords,
t0x: Math.round(e.touches?.[0].clientX),
t0y: Math.round(e.touches?.[0].clientY),
mouseOffx: e.offsetX,
mouseOffy: e.offsetY,
}
}
const draw = (xPos, yPos) => {
if (mouseDown && updated(xPos, yPos)) {
canvas.miterLimit = 3
canvas.lineWidth = 3
if (drawing && updatedPos(xPos, yPos)) {
canvas.miterLimit = 2
canvas.lineWidth = 2
canvas.lineJoin = "round"
canvas.lineCap = "round"
canvas.strokeStyle = "white"
@ -72,69 +87,199 @@
}
const stopTracking = () => {
mouseDown = false
if (!canvas) {
return
}
canvas.closePath()
drawing = false
lastOffsetX = null
lastOffsetY = null
}
const canvasMouseDown = e => {
// if (e.button != 0) {
// return
// }
mouseDown = true
canvas.moveTo(e.offsetX, e.offsetY)
const canvasContact = e => {
if (disabled || !editable || (!e.targetTouches && e.button != 0)) {
return
} else if (!updated) {
updated = true
clearCanvas()
}
drawing = true
canvas.beginPath()
canvas.moveTo(e.offsetX, e.offsetY)
}
let canvasRef
let canvas
let canvasWrap
let drawing = false
let updated = false
let lastOffsetX, lastOffsetY
let canvasWidth
let canvasHeight
let signature
let urlFailed
$: if (value) {
const [attachment] = value || []
signature = attachment
}
$: if (signature?.url) {
updated = false
}
onMount(() => {
if (!editable) {
return
}
canvasWrap.style.width = `${width}px`
canvasWrap.style.height = `${height}px`
const { width: wrapWidth, height: wrapHeight } =
canvasWrap.getBoundingClientRect()
canvasHeight = wrapHeight
canvasWidth = wrapWidth
canvas = canvasRef.getContext("2d")
canvas.imageSmoothingEnabled = true
canvas.imageSmoothingQuality = "high"
})
</script>
<div>
<div>{JSON.stringify(debugData, null, 2)}</div>
<canvas
id="canvas"
width={200}
height={220}
bind:this={canvasRef}
on:mousemove={handleDraw}
on:mousedown={canvasMouseDown}
on:mouseup={stopTracking}
on:mouseleave={stopTracking}
on:touchstart={e => {
touching = true
canvasMouseDown(e)
}}
on:touchend={e => {
touching = false
touchmove = false
stopTracking(e)
}}
on:touchmove={e => {
touchmove = true
handleDraw(e)
}}
on:touchleave={e => {
touching = false
touchmove = false
stopTracking(e)
}}
class:touching={touching && debug}
class:touchmove={touchmove && debug}
/>
<div class="signature" class:light={!isDark} class:image-error={urlFailed}>
{#if !disabled}
<div class="overlay">
{#if updated && saveIcon}
<span class="save">
<Icon
name="Checkmark"
hoverable
tooltip={"Save"}
tooltipPosition={"top"}
tooltipType={"info"}
on:click={() => {
dispatch("change", toDataUrl())
}}
/>
</span>
{/if}
{#if signature?.url && !updated}
<span class="delete">
<Icon
name="DeleteOutline"
hoverable
tooltip={"Delete"}
tooltipPosition={"top"}
tooltipType={"info"}
on:click={() => {
if (editable) {
clearCanvas()
}
dispatch("clear")
}}
/>
</span>
{/if}
</div>
{/if}
{#if !editable && signature?.url}
<!-- svelte-ignore a11y-missing-attribute -->
{#if !urlFailed}
<img
src={signature?.url}
on:error={() => {
urlFailed = true
}}
/>
{:else}
Could not load signature
{/if}
{:else}
<div bind:this={canvasWrap} class="canvas-wrap">
<canvas
id="canvas"
width={canvasWidth}
height={canvasHeight}
bind:this={canvasRef}
on:mousemove={handleDraw}
on:mousedown={canvasContact}
on:mouseup={stopTracking}
on:mouseleave={stopTracking}
on:touchstart={canvasContact}
on:touchend={stopTracking}
on:touchmove={handleDraw}
on:touchleave={stopTracking}
/>
{#if editable}
<div class="indicator-overlay">
<div class="sign-here">
<Icon name="Close" />
<hr />
</div>
</div>
{/if}
</div>
{/if}
</div>
<style>
#canvas {
border: 1px solid blueviolet;
.indicator-overlay {
position: absolute;
width: 100%;
height: 100%;
top: 0px;
display: flex;
flex-direction: column;
justify-content: end;
padding: var(--spectrum-global-dimension-size-150);
box-sizing: border-box;
pointer-events: none;
}
#canvas.touching {
border-color: aquamarine;
.sign-here {
display: flex;
align-items: center;
justify-content: center;
gap: var(--spectrum-global-dimension-size-150);
}
#canvas.touchmove {
border-color: rgb(227, 102, 68);
.sign-here hr {
border: 0;
border-top: 2px solid var(--spectrum-global-color-gray-200);
width: 100%;
}
.canvas-wrap {
position: relative;
margin: auto;
}
.signature img {
max-width: 100%;
}
.signature.light img,
.signature.light #canvas {
-webkit-filter: invert(100%);
filter: invert(100%);
}
.signature.image-error .overlay {
padding-top: 0px;
}
.signature {
width: 100%;
height: 100%;
position: relative;
text-align: center;
}
.overlay {
position: absolute;
width: 100%;
height: 100%;
pointer-events: none;
padding: var(--spectrum-global-dimension-size-150);
text-align: right;
z-index: 2;
box-sizing: border-box;
}
.save,
.delete {
display: inline-block;
}
</style>

View file

@ -173,6 +173,7 @@
}
.spectrum-Modal {
border: 2px solid var(--spectrum-global-color-gray-200);
overflow: visible;
max-height: none;
margin: 40px 0;

View file

@ -27,6 +27,7 @@
export let secondaryButtonText = undefined
export let secondaryAction = undefined
export let secondaryButtonWarning = false
export let enableGrid = true
const { hide, cancel } = getContext(Context.Modal)
let loading = false
@ -63,12 +64,13 @@
class:spectrum-Dialog--medium={size === "M"}
class:spectrum-Dialog--large={size === "L"}
class:spectrum-Dialog--extraLarge={size === "XL"}
class:no-grid={!enableGrid}
style="position: relative;"
role="dialog"
tabindex="-1"
aria-modal="true"
>
<div class="spectrum-Dialog-grid">
<div class="modal-core" class:spectrum-Dialog-grid={enableGrid}>
{#if title || $$slots.header}
<h1
class="spectrum-Dialog-heading spectrum-Dialog-heading--noHeader"
@ -153,6 +155,25 @@
.spectrum-Dialog-content {
overflow: visible;
}
.no-grid .spectrum-Dialog-content {
border-top: 2px solid var(--spectrum-global-color-gray-200);
border-bottom: 2px solid var(--spectrum-global-color-gray-200);
}
.no-grid .spectrum-Dialog-heading {
margin-top: 12px;
margin-left: 12px;
}
.spectrum-Dialog.no-grid {
width: 100%;
}
.spectrum-Dialog.no-grid .spectrum-Dialog-buttonGroup {
padding: 12px;
}
.spectrum-Dialog-heading {
font-family: var(--font-accent);
font-weight: 600;

View file

@ -1,5 +1,6 @@
<script>
import { datasources, tables, integrations, appStore } from "stores/builder"
import { themeStore } from "stores/portal"
import EditRolesButton from "./buttons/EditRolesButton.svelte"
import { TableNames } from "constants"
import { Grid } from "@budibase/frontend-core"
@ -37,6 +38,9 @@
})
$: relationshipsEnabled = relationshipSupport(tableDatasource)
$: currentTheme = $themeStore?.theme
$: isDark = !currentTheme.includes("light")
const relationshipSupport = datasource => {
const integration = $integrations[datasource?.source]
return !isInternal && integration?.relationships !== false
@ -55,6 +59,7 @@
<div class="wrapper">
<Grid
{API}
{isDark}
datasource={gridDatasource}
canAddRows={!isUsersTable}
canDeleteRows={!isUsersTable}

View file

@ -12,6 +12,7 @@ const TYPES_TO_SKIP = [
FIELDS.FORMULA.type,
FIELDS.LONGFORM.type,
FIELDS.ATTACHMENT.type,
FIELDS.SIGNATURE.type,
internalType,
]

View file

@ -82,6 +82,7 @@ const componentMap = {
"validation/boolean": ValidationEditor,
"validation/datetime": ValidationEditor,
"validation/attachment": ValidationEditor,
"validation/signature": ValidationEditor,
"validation/link": ValidationEditor,
"validation/bb_reference": ValidationEditor,
}

View file

@ -108,6 +108,7 @@
Constraints.MaxFileSize,
Constraints.MaxUploadSize,
],
["signature"]: [Constraints.Required],
["link"]: [
Constraints.Required,
Constraints.Contains,

View file

@ -114,7 +114,6 @@ export const FIELDS = {
presence: false,
},
},
// USE the single approach like Adrias update
SIGNATURE: {
name: "Signature",
type: FieldType.SIGNATURE,

View file

@ -4156,7 +4156,7 @@
]
},
{
"type": "validation/string",
"type": "validation/signature",
"label": "Validation",
"key": "validation"
}

View file

@ -23,6 +23,7 @@
appStore,
dndComponentPath,
dndIsDragging,
themeStore,
} from "stores"
import { Helpers } from "@budibase/bbui"
import { getActiveConditions, reduceConditionActions } from "utils/conditions"
@ -192,6 +193,7 @@
let pad = false
$: pad = pad || (interactive && hasChildren && inDndPath)
$: $dndIsDragging, (pad = false)
$: isDark = !$themeStore.theme?.includes("light")
// Update component context
$: store.set({
@ -222,6 +224,7 @@
parent: id,
ancestors: [...($component?.ancestors ?? []), instance._component],
path: [...($component?.path ?? []), id],
isDark,
})
const initialise = (instance, force = false) => {

View file

@ -35,6 +35,7 @@
let grid
$: isDark = $component.isDark
$: columnWhitelist = parsedColumns
?.filter(col => col.active)
?.map(col => col.field)
@ -114,6 +115,7 @@
<Grid
bind:this={grid}
datasource={table}
{isDark}
{API}
{stripeRows}
{initialFilter}

View file

@ -1,8 +0,0 @@
<script>
import SignatureField from "./SignatureField.svelte"
</script>
<div>
Sig Wrap
<SignatureField />
</div>

View file

@ -1,6 +1,140 @@
<script>
import { CoreSignature } from "@budibase/bbui"
import { CoreSignature, ActionButton } from "@budibase/bbui"
import { getContext } from "svelte"
import Field from "./Field.svelte"
import { SignatureModal } from "@budibase/frontend-core/src/components"
export let field
export let label
export let disabled = false
export let readonly = false
export let validation
export let onChange
export let span
export let helpText = null
let fieldState
let fieldApi
let fieldSchema
let modal
const { API, notificationStore, builderStore } = getContext("sdk")
const context = getContext("context")
const formContext = getContext("form")
const saveSignature = async canvas => {
try {
const signatureFile = canvas.toFile()
let updateValue
if (signatureFile) {
let attachRequest = new FormData()
attachRequest.append("file", signatureFile)
const resp = await API.uploadAttachment({
data: attachRequest,
tableId: formContext?.dataSource?.tableId,
})
updateValue = resp
} else {
updateValue = []
}
const changed = fieldApi.setValue(updateValue)
if (onChange && changed) {
onChange({ value: updateValue })
}
} catch (error) {
notificationStore.actions.error(
`There was a problem saving your signature`
)
console.error(error)
}
}
const deleteSignature = async () => {
const deleteRequest = fieldState?.value.map(item => item.key)
const changed = fieldApi.setValue([])
if (onChange && changed) {
onChange({ value: [] })
}
try {
await API.deleteAttachments({
keys: deleteRequest,
tableId: formContext?.dataSource?.tableId,
})
} catch (error) {
notificationStore.actions.error(
`There was a problem deleting your signature`
)
console.error(error)
}
}
$: currentTheme = $context?.device?.theme
$: isDark = !currentTheme?.includes("light")
</script>
<div>SignatureField</div>
<CoreSignature />
<SignatureModal
onConfirm={saveSignature}
title={fieldSchema?.name}
value={fieldState?.value}
{isDark}
bind:this={modal}
/>
<Field
{label}
{field}
disabled={$builderStore.inBuilder || disabled}
{readonly}
{validation}
{span}
{helpText}
type="signature"
bind:fieldState
bind:fieldApi
bind:fieldSchema
defaultValue={[]}
>
{#if fieldState}
{#if (Array.isArray(fieldState?.value) && !fieldState?.value?.length) || !fieldState?.value}
<ActionButton
fullWidth
on:click={() => {
modal.show()
}}
>
Add signature
</ActionButton>
{:else}
<div class="signature-field">
<CoreSignature
{isDark}
disabled={$builderStore.inBuilder || disabled}
editable={false}
value={fieldState?.value}
on:clear={deleteSignature}
/>
</div>
{/if}
{/if}
</Field>
<style>
.signature-field {
min-height: 50px;
justify-content: center;
width: 100%;
display: flex;
flex-direction: column;
background-color: var(--spectrum-global-color-gray-50);
box-sizing: border-box;
border: var(--spectrum-alias-border-size-thin)
var(--spectrum-alias-border-color) solid;
border-radius: var(--spectrum-alias-border-radius-regular);
}
</style>

View file

@ -191,8 +191,8 @@ const parseType = (value, type) => {
return value === true
}
// Parse attachments, treating no elements as null
if (type === FieldTypes.ATTACHMENT) {
// Parse attachments/signatures, treating no elements as null
if (type === FieldTypes.ATTACHMENT || type === FieldTypes.SIGNATURE) {
if (!Array.isArray(value) || !value.length) {
return null
}

View file

@ -0,0 +1,48 @@
<script>
import { Modal, ModalContent, Body, CoreSignature } from "@budibase/bbui"
export let onConfirm = () => {}
export let value
export let title
export let isDark
export const show = () => {
modal.show()
}
let modal
let canvas
</script>
<Modal bind:this={modal}>
<ModalContent
showConfirmButton
showCancelButton={false}
showCloseIcon={false}
enableGrid={false}
showDivider={false}
onConfirm={() => {
onConfirm(canvas)
}}
>
<div slot="header">
<Body>{title}</Body>
</div>
<div class="signature-wrap modal">
<CoreSignature {isDark} {value} saveIcon={false} bind:this={canvas} />
</div>
</ModalContent>
</Modal>
<style>
.signature-wrap {
display: flex;
flex-direction: column;
justify-content: flex-start;
align-items: stretch;
background-color: var(--spectrum-global-color-gray-50);
color: var(--spectrum-alias-text-color);
box-sizing: border-box;
position: relative;
}
</style>

View file

@ -1,7 +1,9 @@
<script>
import { onMount, getContext } from "svelte"
import { CoreSignature } from "@budibase/bbui"
import { SignatureModal } from "@budibase/frontend-core/src/components"
import { CoreSignature, ActionButton } from "@budibase/bbui"
export let schema
export let value
export let focused = false
export let onChange
@ -9,12 +11,18 @@
export let api
export let invertX = false
export let invertY = false
// export let schema
const { API, notifications } = getContext("grid")
const { API, notifications, props } = getContext("grid")
let isOpen = false
let sigCanvas
let editing = false
let signature
let modal
$: if (value) {
const [attachment] = value
signature = attachment
}
$: editable = focused && !readonly
$: {
@ -35,6 +43,31 @@
isOpen = false
}
const deleteSignature = async () => {
onChange([])
const deleteRequest = value.map(item => item.key)
try {
await API.deleteBuilderAttachments(deleteRequest)
} catch (e) {
$notifications.error(error.message || "Failed to delete signature")
}
}
const saveSignature = async sigCanvas => {
const signatureFile = sigCanvas.toFile()
let attachRequest = new FormData()
attachRequest.append("file", signatureFile)
try {
const uploadReq = await API.uploadBuilderAttachment(attachRequest)
onChange(uploadReq)
} catch (error) {
$notifications.error(error.message || "Failed to upload attachment")
return []
}
}
onMount(() => {
api = {
focus: () => open(),
@ -47,44 +80,75 @@
<!-- svelte-ignore a11y-no-static-element-interactions -->
<!-- svelte-ignore a11y-click-events-have-key-events -->
<div class="signature-cell" class:editable on:click={editable ? open : null}>
signature cell: open {isOpen}
<div
class="signature-cell"
class:light={!($props?.isDark || undefined)}
class:editable
on:click={editable ? open : null}
>
{#if signature?.url}
<!-- svelte-ignore a11y-missing-attribute -->
<img src={signature?.url} />
{/if}
</div>
<SignatureModal
onConfirm={saveSignature}
title={schema?.name}
{value}
isDark={$props.isDark}
bind:this={modal}
/>
{#if isOpen}
<div class="signature" class:invertX class:invertY>
<button
on:click={() => {
console.log(sigCanvas.toDataUrl())
}}
>
check
</button>
<div class="field-wrap">
<CoreSignature
bind:this={sigCanvas}
on:change={() => {
console.log("cell change")
}}
/>
</div>
<div class="signature" class:invertX class:invertY class:empty={!signature}>
{#if signature?.key}
<div class="signature-wrap">
<CoreSignature
isDark={$props.isDark}
editable={false}
{value}
on:change={saveSignature}
on:clear={deleteSignature}
/>
</div>
{:else}
<div class="add-signature">
<ActionButton
fullWidth
on:click={() => {
editing = true
modal.show()
}}
>
Add signature
</ActionButton>
</div>
{/if}
</div>
{/if}
<style>
.signature {
min-width: 320px;
}
.signature.empty {
width: 100%;
min-width: unset;
}
.signature-cell.light img {
-webkit-filter: invert(100%);
filter: invert(100%);
}
.signature-cell {
/* display: flex;
padding: var(--cell-padding);
overflow: hidden;
user-select: none;
position: relative; */
flex: 1 1 auto;
display: flex;
flex-direction: row;
align-items: stretch;
padding: var(--cell-padding);
max-width: 320px;
padding-left: var(--cell-padding);
padding-right: var(--cell-padding);
flex-wrap: nowrap;
gap: var(--cell-spacing);
align-self: stretch;
overflow: hidden;
user-select: none;
@ -96,29 +160,20 @@
position: absolute;
top: 100%;
left: 0;
width: 320px;
background: var(--grid-background-alt);
border: var(--cell-border);
padding: var(--cell-padding);
box-shadow: 0 0 20px -4px rgba(0, 0, 0, 0.15);
border-bottom-left-radius: 2px;
border-bottom-right-radius: 2px;
}
.field-wrap {
.signature-wrap {
display: flex;
flex-direction: column;
justify-content: flex-start;
align-items: stretch;
background-color: var(--spectrum-global-color-gray-50);
color: var(--spectrum-alias-text-color);
/* font-size: var(--spectrum-alias-item-text-size-m); */
box-sizing: border-box;
border: var(--spectrum-alias-border-size-thin)
var(--spectrum-alias-border-color) solid;
border-radius: var(--spectrum-alias-border-radius-regular);
position: relative;
}
.signature.invertX {
left: auto;
right: 0;
@ -127,17 +182,4 @@
transform: translateY(-100%);
top: 0;
}
/* .attachment-cell {
flex: 1 1 auto;
display: flex;
flex-direction: row;
align-items: stretch;
padding: var(--cell-padding);
flex-wrap: nowrap;
gap: var(--cell-spacing);
align-self: stretch;
overflow: hidden;
user-select: none;
}
*/
</style>

View file

@ -49,6 +49,7 @@
export let notifySuccess = null
export let notifyError = null
export let buttons = null
export let isDark
// Unique identifier for DOM nodes inside this instance
const rand = Math.random()
@ -101,6 +102,7 @@
notifySuccess,
notifyError,
buttons,
isDark,
})
// Set context for children to consume

View file

@ -1,5 +1,6 @@
export { default as SplitPage } from "./SplitPage.svelte"
export { default as TestimonialPage } from "./TestimonialPage.svelte"
export { default as SignatureModal } from "./SignatureModal.svelte"
export { default as Testimonial } from "./Testimonial.svelte"
export { default as UserAvatar } from "./UserAvatar.svelte"
export { default as UserAvatars } from "./UserAvatars.svelte"

View file

@ -175,13 +175,15 @@ export async function validate({
errors[fieldName] = [`${fieldName} is required`]
}
} else if (
(type === FieldType.ATTACHMENT || type === FieldType.JSON) &&
(type === FieldType.ATTACHMENT ||
type === FieldType.SIGNATURE ||
type === FieldType.JSON) &&
typeof row[fieldName] === "string"
) {
// this should only happen if there is an error
try {
const json = JSON.parse(row[fieldName])
if (type === FieldType.ATTACHMENT) {
if (type === FieldType.ATTACHMENT || type === FieldType.SIGNATURE) {
if (Array.isArray(json)) {
row[fieldName] = json
} else {

View file

@ -34,7 +34,10 @@ export class AttachmentCleanup {
let files: string[] = []
const tableSchema = opts.oldTable?.schema || table.schema
for (let [key, schema] of Object.entries(tableSchema)) {
if (schema.type !== FieldType.ATTACHMENT) {
if (
schema.type !== FieldType.ATTACHMENT &&
schema.type !== FieldType.SIGNATURE
) {
continue
}
const columnRemoved = opts.oldTable && !table.schema[key]
@ -68,9 +71,13 @@ export class AttachmentCleanup {
return AttachmentCleanup.coreCleanup(() => {
let files: string[] = []
for (let [key, schema] of Object.entries(table.schema)) {
if (schema.type !== FieldType.ATTACHMENT) {
if (
schema.type !== FieldType.ATTACHMENT &&
schema.type !== FieldType.SIGNATURE
) {
continue
}
rows.forEach(row => {
if (!Array.isArray(row[key])) {
return

View file

@ -147,8 +147,11 @@ export async function inputProcessing(
clonedRow[key] = coerce(value, field.type)
}
// remove any attachment urls, they are generated on read
if (field.type === FieldType.ATTACHMENT) {
// remove any attachment/signature urls, they are generated on read
if (
field.type === FieldType.ATTACHMENT ||
field.type === FieldType.SIGNATURE
) {
const attachments = clonedRow[key]
if (attachments?.length) {
attachments.forEach((attachment: RowAttachment) => {
@ -216,7 +219,10 @@ export async function outputProcessing<T extends Row[] | Row>(
// process complex types: attachements, bb references...
for (let [property, column] of Object.entries(table.schema)) {
if (column.type === FieldType.ATTACHMENT) {
if (
column.type === FieldType.ATTACHMENT ||
column.type === FieldType.SIGNATURE
) {
for (let row of enriched) {
if (row[property] == null || !Array.isArray(row[property])) {
continue