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> <script>
import { onMount } from "svelte" import { onMount } from "svelte"
import Icon from "../../Icon/Icon.svelte"
import { createEventDispatcher } from "svelte"
export let canvasWidth const dispatch = createEventDispatcher()
export let canvasHeight
let canvasRef export let value
let canvas export let disabled = false
let mouseDown = false export let editable = true
let lastOffsetX, lastOffsetY export let width = 400
export let height = 220
let touching = false export let saveIcon = false
let touchmove = false export let isDark
let debug = true
let debugData
export function toDataUrl() { export function toDataUrl() {
return canvasRef.toDataURL() // PNG to preserve transparency
return canvasRef.toDataURL("image/png")
} }
export function clear() { export function toFile() {
return canvas.clearRect(0, 0, canvas.width, canvas.height) 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 return lastOffsetX != x || lastOffsetY != y
} }
// Needs touch handling
const handleDraw = e => { const handleDraw = e => {
// e.touches[0] there should ol
e.preventDefault() e.preventDefault()
if (disabled || !editable) {
return
}
var rect = canvasRef.getBoundingClientRect() var rect = canvasRef.getBoundingClientRect()
const canvasX = e.offsetX || e.targetTouches?.[0].pageX - rect.left const canvasX = e.offsetX || e.targetTouches?.[0].pageX - rect.left
const canvasY = e.offsetY || e.targetTouches?.[0].pageY - rect.top const canvasY = e.offsetY || e.targetTouches?.[0].pageY - rect.top
const coords = { x: Math.round(canvasX), y: Math.round(canvasY) } const coords = { x: Math.round(canvasX), y: Math.round(canvasY) }
draw(coords.x, coords.y) 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) => { const draw = (xPos, yPos) => {
if (mouseDown && updated(xPos, yPos)) { if (drawing && updatedPos(xPos, yPos)) {
canvas.miterLimit = 3 canvas.miterLimit = 2
canvas.lineWidth = 3 canvas.lineWidth = 2
canvas.lineJoin = "round" canvas.lineJoin = "round"
canvas.lineCap = "round" canvas.lineCap = "round"
canvas.strokeStyle = "white" canvas.strokeStyle = "white"
@ -72,69 +87,199 @@
} }
const stopTracking = () => { const stopTracking = () => {
mouseDown = false if (!canvas) {
return
}
canvas.closePath()
drawing = false
lastOffsetX = null lastOffsetX = null
lastOffsetY = null lastOffsetY = null
} }
const canvasMouseDown = e => { const canvasContact = e => {
// if (e.button != 0) { if (disabled || !editable || (!e.targetTouches && e.button != 0)) {
// return return
// } } else if (!updated) {
mouseDown = true updated = true
canvas.moveTo(e.offsetX, e.offsetY) clearCanvas()
}
drawing = true
canvas.beginPath() 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(() => { 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 = canvasRef.getContext("2d")
canvas.imageSmoothingEnabled = true canvas.imageSmoothingEnabled = true
canvas.imageSmoothingQuality = "high"
}) })
</script> </script>
<div> <div class="signature" class:light={!isDark} class:image-error={urlFailed}>
<div>{JSON.stringify(debugData, null, 2)}</div> {#if !disabled}
<canvas <div class="overlay">
id="canvas" {#if updated && saveIcon}
width={200} <span class="save">
height={220} <Icon
bind:this={canvasRef} name="Checkmark"
on:mousemove={handleDraw} hoverable
on:mousedown={canvasMouseDown} tooltip={"Save"}
on:mouseup={stopTracking} tooltipPosition={"top"}
on:mouseleave={stopTracking} tooltipType={"info"}
on:touchstart={e => { on:click={() => {
touching = true dispatch("change", toDataUrl())
canvasMouseDown(e) }}
}} />
on:touchend={e => { </span>
touching = false {/if}
touchmove = false {#if signature?.url && !updated}
stopTracking(e) <span class="delete">
}} <Icon
on:touchmove={e => { name="DeleteOutline"
touchmove = true hoverable
handleDraw(e) tooltip={"Delete"}
}} tooltipPosition={"top"}
on:touchleave={e => { tooltipType={"info"}
touching = false on:click={() => {
touchmove = false if (editable) {
stopTracking(e) clearCanvas()
}} }
class:touching={touching && debug} dispatch("clear")
class:touchmove={touchmove && debug} }}
/> />
</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> </div>
<style> <style>
#canvas { .indicator-overlay {
border: 1px solid blueviolet; 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 { .sign-here {
border-color: aquamarine; display: flex;
align-items: center;
justify-content: center;
gap: var(--spectrum-global-dimension-size-150);
} }
#canvas.touchmove { .sign-here hr {
border-color: rgb(227, 102, 68); 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> </style>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -35,6 +35,7 @@
let grid let grid
$: isDark = $component.isDark
$: columnWhitelist = parsedColumns $: columnWhitelist = parsedColumns
?.filter(col => col.active) ?.filter(col => col.active)
?.map(col => col.field) ?.map(col => col.field)
@ -114,6 +115,7 @@
<Grid <Grid
bind:this={grid} bind:this={grid}
datasource={table} datasource={table}
{isDark}
{API} {API}
{stripeRows} {stripeRows}
{initialFilter} {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> <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> </script>
<div>SignatureField</div> <SignatureModal
<CoreSignature /> 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 return value === true
} }
// Parse attachments, treating no elements as null // Parse attachments/signatures, treating no elements as null
if (type === FieldTypes.ATTACHMENT) { if (type === FieldTypes.ATTACHMENT || type === FieldTypes.SIGNATURE) {
if (!Array.isArray(value) || !value.length) { if (!Array.isArray(value) || !value.length) {
return null 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> <script>
import { onMount, getContext } from "svelte" 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 value
export let focused = false export let focused = false
export let onChange export let onChange
@ -9,12 +11,18 @@
export let api export let api
export let invertX = false export let invertX = false
export let invertY = false export let invertY = false
// export let schema
const { API, notifications } = getContext("grid") const { API, notifications, props } = getContext("grid")
let isOpen = false let isOpen = false
let sigCanvas let editing = false
let signature
let modal
$: if (value) {
const [attachment] = value
signature = attachment
}
$: editable = focused && !readonly $: editable = focused && !readonly
$: { $: {
@ -35,6 +43,31 @@
isOpen = false 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(() => { onMount(() => {
api = { api = {
focus: () => open(), focus: () => open(),
@ -47,44 +80,75 @@
<!-- svelte-ignore a11y-no-static-element-interactions --> <!-- svelte-ignore a11y-no-static-element-interactions -->
<!-- svelte-ignore a11y-click-events-have-key-events --> <!-- svelte-ignore a11y-click-events-have-key-events -->
<div class="signature-cell" class:editable on:click={editable ? open : null}> <div
signature cell: open {isOpen} 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> </div>
<SignatureModal
onConfirm={saveSignature}
title={schema?.name}
{value}
isDark={$props.isDark}
bind:this={modal}
/>
{#if isOpen} {#if isOpen}
<div class="signature" class:invertX class:invertY> <div class="signature" class:invertX class:invertY class:empty={!signature}>
<button {#if signature?.key}
on:click={() => { <div class="signature-wrap">
console.log(sigCanvas.toDataUrl()) <CoreSignature
}} isDark={$props.isDark}
> editable={false}
check {value}
</button> on:change={saveSignature}
<div class="field-wrap"> on:clear={deleteSignature}
<CoreSignature />
bind:this={sigCanvas} </div>
on:change={() => { {:else}
console.log("cell change") <div class="add-signature">
}} <ActionButton
/> fullWidth
</div> on:click={() => {
editing = true
modal.show()
}}
>
Add signature
</ActionButton>
</div>
{/if}
</div> </div>
{/if} {/if}
<style> <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 { .signature-cell {
/* display: flex;
padding: var(--cell-padding);
overflow: hidden;
user-select: none;
position: relative; */
flex: 1 1 auto; flex: 1 1 auto;
display: flex; display: flex;
flex-direction: row; flex-direction: row;
align-items: stretch; align-items: stretch;
padding: var(--cell-padding); max-width: 320px;
padding-left: var(--cell-padding);
padding-right: var(--cell-padding);
flex-wrap: nowrap; flex-wrap: nowrap;
gap: var(--cell-spacing);
align-self: stretch; align-self: stretch;
overflow: hidden; overflow: hidden;
user-select: none; user-select: none;
@ -96,29 +160,20 @@
position: absolute; position: absolute;
top: 100%; top: 100%;
left: 0; left: 0;
width: 320px;
background: var(--grid-background-alt); background: var(--grid-background-alt);
border: var(--cell-border); border: var(--cell-border);
padding: var(--cell-padding); 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; display: flex;
flex-direction: column; flex-direction: column;
justify-content: flex-start; justify-content: flex-start;
align-items: stretch; align-items: stretch;
background-color: var(--spectrum-global-color-gray-50); background-color: var(--spectrum-global-color-gray-50);
color: var(--spectrum-alias-text-color); color: var(--spectrum-alias-text-color);
/* font-size: var(--spectrum-alias-item-text-size-m); */
box-sizing: border-box; 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; position: relative;
} }
.signature.invertX { .signature.invertX {
left: auto; left: auto;
right: 0; right: 0;
@ -127,17 +182,4 @@
transform: translateY(-100%); transform: translateY(-100%);
top: 0; 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> </style>

View File

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

View File

@ -1,5 +1,6 @@
export { default as SplitPage } from "./SplitPage.svelte" export { default as SplitPage } from "./SplitPage.svelte"
export { default as TestimonialPage } from "./TestimonialPage.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 Testimonial } from "./Testimonial.svelte"
export { default as UserAvatar } from "./UserAvatar.svelte" export { default as UserAvatar } from "./UserAvatar.svelte"
export { default as UserAvatars } from "./UserAvatars.svelte" export { default as UserAvatars } from "./UserAvatars.svelte"

View File

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

View File

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

View File

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