Full support for signature field type and some feedback changes
This commit is contained in:
parent
fc35bfd83b
commit
1e5506b8c3
|
@ -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
|
||||
}
|
||||
|
||||
const updated = (x, y) => {
|
||||
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,
|
||||
})
|
||||
}
|
||||
|
||||
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>
|
||||
<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={200}
|
||||
height={220}
|
||||
width={canvasWidth}
|
||||
height={canvasHeight}
|
||||
bind:this={canvasRef}
|
||||
on:mousemove={handleDraw}
|
||||
on:mousedown={canvasMouseDown}
|
||||
on:mousedown={canvasContact}
|
||||
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}
|
||||
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>
|
||||
|
|
|
@ -173,6 +173,7 @@
|
|||
}
|
||||
|
||||
.spectrum-Modal {
|
||||
border: 2px solid var(--spectrum-global-color-gray-200);
|
||||
overflow: visible;
|
||||
max-height: none;
|
||||
margin: 40px 0;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -12,6 +12,7 @@ const TYPES_TO_SKIP = [
|
|||
FIELDS.FORMULA.type,
|
||||
FIELDS.LONGFORM.type,
|
||||
FIELDS.ATTACHMENT.type,
|
||||
FIELDS.SIGNATURE.type,
|
||||
internalType,
|
||||
]
|
||||
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
|
|
|
@ -108,6 +108,7 @@
|
|||
Constraints.MaxFileSize,
|
||||
Constraints.MaxUploadSize,
|
||||
],
|
||||
["signature"]: [Constraints.Required],
|
||||
["link"]: [
|
||||
Constraints.Required,
|
||||
Constraints.Contains,
|
||||
|
|
|
@ -114,7 +114,6 @@ export const FIELDS = {
|
|||
presence: false,
|
||||
},
|
||||
},
|
||||
// USE the single approach like Adrias update
|
||||
SIGNATURE: {
|
||||
name: "Signature",
|
||||
type: FieldType.SIGNATURE,
|
||||
|
|
|
@ -4156,7 +4156,7 @@
|
|||
]
|
||||
},
|
||||
{
|
||||
"type": "validation/string",
|
||||
"type": "validation/signature",
|
||||
"label": "Validation",
|
||||
"key": "validation"
|
||||
}
|
||||
|
|
|
@ -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) => {
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -1,8 +0,0 @@
|
|||
<script>
|
||||
import SignatureField from "./SignatureField.svelte"
|
||||
</script>
|
||||
|
||||
<div>
|
||||
Sig Wrap
|
||||
<SignatureField />
|
||||
</div>
|
|
@ -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>
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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>
|
|
@ -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">
|
||||
<div class="signature" class:invertX class:invertY class:empty={!signature}>
|
||||
{#if signature?.key}
|
||||
<div class="signature-wrap">
|
||||
<CoreSignature
|
||||
bind:this={sigCanvas}
|
||||
on:change={() => {
|
||||
console.log("cell change")
|
||||
}}
|
||||
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>
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue