Merge branch 'master' into prefill-cards
This commit is contained in:
commit
8498dd7c3d
|
@ -71,8 +71,8 @@ const handleMouseDown = e => {
|
||||||
|
|
||||||
// Clear any previous listeners in case of multiple down events, and register
|
// Clear any previous listeners in case of multiple down events, and register
|
||||||
// a single mouse up listener
|
// a single mouse up listener
|
||||||
document.removeEventListener("mouseup", handleMouseUp)
|
document.removeEventListener("click", handleMouseUp)
|
||||||
document.addEventListener("mouseup", handleMouseUp, true)
|
document.addEventListener("click", handleMouseUp, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Global singleton listeners for our events
|
// Global singleton listeners for our events
|
||||||
|
|
|
@ -0,0 +1,267 @@
|
||||||
|
<script>
|
||||||
|
import { onMount, createEventDispatcher } from "svelte"
|
||||||
|
import Atrament from "atrament"
|
||||||
|
import Icon from "../../Icon/Icon.svelte"
|
||||||
|
|
||||||
|
const dispatch = createEventDispatcher()
|
||||||
|
|
||||||
|
let last
|
||||||
|
|
||||||
|
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 darkMode
|
||||||
|
|
||||||
|
export function toDataUrl() {
|
||||||
|
// PNG to preserve transparency
|
||||||
|
return canvasRef.toDataURL("image/png")
|
||||||
|
}
|
||||||
|
|
||||||
|
export function toFile() {
|
||||||
|
const data = canvasContext
|
||||||
|
.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,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function clearCanvas() {
|
||||||
|
return canvasContext.clearRect(0, 0, canvasWidth, canvasHeight)
|
||||||
|
}
|
||||||
|
|
||||||
|
let canvasRef
|
||||||
|
let canvasContext
|
||||||
|
let canvasWrap
|
||||||
|
let canvasWidth
|
||||||
|
let canvasHeight
|
||||||
|
let signature
|
||||||
|
|
||||||
|
let updated = false
|
||||||
|
let signatureFile
|
||||||
|
let urlFailed
|
||||||
|
|
||||||
|
$: if (value) {
|
||||||
|
signatureFile = value
|
||||||
|
}
|
||||||
|
|
||||||
|
$: if (signatureFile?.url) {
|
||||||
|
updated = false
|
||||||
|
}
|
||||||
|
|
||||||
|
$: if (last) {
|
||||||
|
dispatch("update")
|
||||||
|
}
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
if (!editable) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const getPos = e => {
|
||||||
|
var rect = canvasRef.getBoundingClientRect()
|
||||||
|
const canvasX = e.offsetX || e.targetTouches?.[0].pageX - rect.left
|
||||||
|
const canvasY = e.offsetY || e.targetTouches?.[0].pageY - rect.top
|
||||||
|
|
||||||
|
return { x: canvasX, y: canvasY }
|
||||||
|
}
|
||||||
|
|
||||||
|
const checkUp = e => {
|
||||||
|
last = getPos(e)
|
||||||
|
}
|
||||||
|
|
||||||
|
canvasRef.addEventListener("pointerdown", e => {
|
||||||
|
const current = getPos(e)
|
||||||
|
//If the cursor didn't move at all, block the default pointerdown
|
||||||
|
if (last?.x === current?.x && last?.y === current?.y) {
|
||||||
|
e.preventDefault()
|
||||||
|
e.stopImmediatePropagation()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
document.addEventListener("pointerup", checkUp)
|
||||||
|
|
||||||
|
signature = new Atrament(canvasRef, {
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
color: "white",
|
||||||
|
})
|
||||||
|
|
||||||
|
signature.weight = 4
|
||||||
|
signature.smoothing = 2
|
||||||
|
|
||||||
|
canvasWrap.style.width = `${width}px`
|
||||||
|
canvasWrap.style.height = `${height}px`
|
||||||
|
|
||||||
|
const { width: wrapWidth, height: wrapHeight } =
|
||||||
|
canvasWrap.getBoundingClientRect()
|
||||||
|
|
||||||
|
canvasHeight = wrapHeight
|
||||||
|
canvasWidth = wrapWidth
|
||||||
|
|
||||||
|
canvasContext = canvasRef.getContext("2d")
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
signature.destroy()
|
||||||
|
document.removeEventListener("pointerup", checkUp)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="signature" class:light={!darkMode} 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 signatureFile?.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 && signatureFile?.url}
|
||||||
|
<!-- svelte-ignore a11y-missing-attribute -->
|
||||||
|
{#if !urlFailed}
|
||||||
|
<img
|
||||||
|
src={signatureFile?.url}
|
||||||
|
on:error={() => {
|
||||||
|
urlFailed = true
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{:else}
|
||||||
|
Could not load signature
|
||||||
|
{/if}
|
||||||
|
{:else}
|
||||||
|
<div bind:this={canvasWrap} class="canvas-wrap">
|
||||||
|
<canvas
|
||||||
|
id="signature-canvas"
|
||||||
|
bind:this={canvasRef}
|
||||||
|
style="--max-sig-width: {width}px; --max-sig-height: {height}px"
|
||||||
|
/>
|
||||||
|
{#if editable}
|
||||||
|
<div class="indicator-overlay">
|
||||||
|
<div class="sign-here">
|
||||||
|
<Icon name="Close" />
|
||||||
|
<hr />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.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;
|
||||||
|
}
|
||||||
|
.sign-here {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: var(--spectrum-global-dimension-size-150);
|
||||||
|
}
|
||||||
|
.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-canvas {
|
||||||
|
max-width: var(--max-sig-width);
|
||||||
|
max-height: var(--max-sig-height);
|
||||||
|
}
|
||||||
|
.signature.light img,
|
||||||
|
.signature.light #signature-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>
|
|
@ -16,3 +16,4 @@ export { default as CoreStepper } from "./Stepper.svelte"
|
||||||
export { default as CoreRichTextField } from "./RichTextField.svelte"
|
export { default as CoreRichTextField } from "./RichTextField.svelte"
|
||||||
export { default as CoreSlider } from "./Slider.svelte"
|
export { default as CoreSlider } from "./Slider.svelte"
|
||||||
export { default as CoreFile } from "./File.svelte"
|
export { default as CoreFile } from "./File.svelte"
|
||||||
|
export { default as CoreSignature } from "./Signature.svelte"
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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 custom = false
|
||||||
|
|
||||||
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={custom}
|
||||||
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={!custom}>
|
||||||
{#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;
|
||||||
|
|
|
@ -364,6 +364,7 @@
|
||||||
value.customType !== "cron" &&
|
value.customType !== "cron" &&
|
||||||
value.customType !== "triggerSchema" &&
|
value.customType !== "triggerSchema" &&
|
||||||
value.customType !== "automationFields" &&
|
value.customType !== "automationFields" &&
|
||||||
|
value.type !== "signature_single" &&
|
||||||
value.type !== "attachment" &&
|
value.type !== "attachment" &&
|
||||||
value.type !== "attachment_single"
|
value.type !== "attachment_single"
|
||||||
)
|
)
|
||||||
|
@ -456,7 +457,7 @@
|
||||||
value={inputData[key]}
|
value={inputData[key]}
|
||||||
options={Object.keys(table?.schema || {})}
|
options={Object.keys(table?.schema || {})}
|
||||||
/>
|
/>
|
||||||
{:else if value.type === "attachment"}
|
{:else if value.type === "attachment" || value.type === "signature_single"}
|
||||||
<div class="attachment-field-wrapper">
|
<div class="attachment-field-wrapper">
|
||||||
<div class="label-wrapper">
|
<div class="label-wrapper">
|
||||||
<Label>{label}</Label>
|
<Label>{label}</Label>
|
||||||
|
|
|
@ -24,6 +24,11 @@
|
||||||
|
|
||||||
let table
|
let table
|
||||||
let schemaFields
|
let schemaFields
|
||||||
|
let attachmentTypes = [
|
||||||
|
FieldType.ATTACHMENTS,
|
||||||
|
FieldType.ATTACHMENT_SINGLE,
|
||||||
|
FieldType.SIGNATURE_SINGLE,
|
||||||
|
]
|
||||||
|
|
||||||
$: {
|
$: {
|
||||||
table = $tables.list.find(table => table._id === value?.tableId)
|
table = $tables.list.find(table => table._id === value?.tableId)
|
||||||
|
@ -120,15 +125,9 @@
|
||||||
{#if schemaFields.length}
|
{#if schemaFields.length}
|
||||||
{#each schemaFields as [field, schema]}
|
{#each schemaFields as [field, schema]}
|
||||||
{#if !schema.autocolumn}
|
{#if !schema.autocolumn}
|
||||||
<div
|
<div class:schema-fields={!attachmentTypes.includes(schema.type)}>
|
||||||
class:schema-fields={schema.type !== FieldType.ATTACHMENTS &&
|
|
||||||
schema.type !== FieldType.ATTACHMENT_SINGLE}
|
|
||||||
>
|
|
||||||
<Label>{field}</Label>
|
<Label>{field}</Label>
|
||||||
<div
|
<div class:field-width={!attachmentTypes.includes(schema.type)}>
|
||||||
class:field-width={schema.type !== FieldType.ATTACHMENTS &&
|
|
||||||
schema.type !== FieldType.ATTACHMENT_SINGLE}
|
|
||||||
>
|
|
||||||
{#if isTestModal}
|
{#if isTestModal}
|
||||||
<RowSelectorTypes
|
<RowSelectorTypes
|
||||||
{isTestModal}
|
{isTestModal}
|
||||||
|
|
|
@ -21,6 +21,12 @@
|
||||||
return clone
|
return clone
|
||||||
})
|
})
|
||||||
|
|
||||||
|
let attachmentTypes = [
|
||||||
|
FieldType.ATTACHMENTS,
|
||||||
|
FieldType.ATTACHMENT_SINGLE,
|
||||||
|
FieldType.SIGNATURE_SINGLE,
|
||||||
|
]
|
||||||
|
|
||||||
function schemaHasOptions(schema) {
|
function schemaHasOptions(schema) {
|
||||||
return !!schema.constraints?.inclusion?.length
|
return !!schema.constraints?.inclusion?.length
|
||||||
}
|
}
|
||||||
|
@ -29,7 +35,8 @@
|
||||||
let params = {}
|
let params = {}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
schema.type === FieldType.ATTACHMENT_SINGLE &&
|
(schema.type === FieldType.ATTACHMENT_SINGLE ||
|
||||||
|
schema.type === FieldType.SIGNATURE_SINGLE) &&
|
||||||
Object.keys(keyValueObj).length === 0
|
Object.keys(keyValueObj).length === 0
|
||||||
) {
|
) {
|
||||||
return []
|
return []
|
||||||
|
@ -100,16 +107,20 @@
|
||||||
on:change={e => onChange(e, field)}
|
on:change={e => onChange(e, field)}
|
||||||
useLabel={false}
|
useLabel={false}
|
||||||
/>
|
/>
|
||||||
{:else if schema.type === FieldType.ATTACHMENTS || schema.type === FieldType.ATTACHMENT_SINGLE}
|
{:else if attachmentTypes.includes(schema.type)}
|
||||||
<div class="attachment-field-spacinng">
|
<div class="attachment-field-spacinng">
|
||||||
<KeyValueBuilder
|
<KeyValueBuilder
|
||||||
on:change={e =>
|
on:change={e =>
|
||||||
onChange(
|
onChange(
|
||||||
{
|
{
|
||||||
detail:
|
detail:
|
||||||
schema.type === FieldType.ATTACHMENT_SINGLE
|
schema.type === FieldType.ATTACHMENT_SINGLE ||
|
||||||
|
schema.type === FieldType.SIGNATURE_SINGLE
|
||||||
? e.detail.length > 0
|
? e.detail.length > 0
|
||||||
? { url: e.detail[0].name, filename: e.detail[0].value }
|
? {
|
||||||
|
url: e.detail[0].name,
|
||||||
|
filename: e.detail[0].value,
|
||||||
|
}
|
||||||
: {}
|
: {}
|
||||||
: e.detail.map(({ name, value }) => ({
|
: e.detail.map(({ name, value }) => ({
|
||||||
url: name,
|
url: name,
|
||||||
|
@ -125,7 +136,8 @@
|
||||||
customButtonText={"Add attachment"}
|
customButtonText={"Add attachment"}
|
||||||
keyPlaceholder={"URL"}
|
keyPlaceholder={"URL"}
|
||||||
valuePlaceholder={"Filename"}
|
valuePlaceholder={"Filename"}
|
||||||
actionButtonDisabled={schema.type === FieldType.ATTACHMENT_SINGLE &&
|
actionButtonDisabled={(schema.type === FieldType.ATTACHMENT_SINGLE ||
|
||||||
|
schema.type === FieldType.SIGNATURE) &&
|
||||||
Object.keys(value[field]).length >= 1}
|
Object.keys(value[field]).length >= 1}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
<script>
|
<script>
|
||||||
|
import { API } from "api"
|
||||||
import {
|
import {
|
||||||
Input,
|
Input,
|
||||||
Select,
|
Select,
|
||||||
|
@ -8,11 +9,16 @@
|
||||||
Label,
|
Label,
|
||||||
RichTextField,
|
RichTextField,
|
||||||
TextArea,
|
TextArea,
|
||||||
|
CoreSignature,
|
||||||
|
ActionButton,
|
||||||
|
notifications,
|
||||||
} from "@budibase/bbui"
|
} from "@budibase/bbui"
|
||||||
import Dropzone from "components/common/Dropzone.svelte"
|
import Dropzone from "components/common/Dropzone.svelte"
|
||||||
import { capitalise } from "helpers"
|
import { capitalise } from "helpers"
|
||||||
import LinkedRowSelector from "components/common/LinkedRowSelector.svelte"
|
import LinkedRowSelector from "components/common/LinkedRowSelector.svelte"
|
||||||
import Editor from "../../integration/QueryEditor.svelte"
|
import Editor from "../../integration/QueryEditor.svelte"
|
||||||
|
import { SignatureModal } from "@budibase/frontend-core/src/components"
|
||||||
|
import { themeStore } from "stores/portal"
|
||||||
|
|
||||||
export let meta
|
export let meta
|
||||||
export let value
|
export let value
|
||||||
|
@ -38,8 +44,35 @@
|
||||||
|
|
||||||
const timeStamp = resolveTimeStamp(value)
|
const timeStamp = resolveTimeStamp(value)
|
||||||
const isTimeStamp = !!timeStamp || meta?.timeOnly
|
const isTimeStamp = !!timeStamp || meta?.timeOnly
|
||||||
|
|
||||||
|
$: currentTheme = $themeStore?.theme
|
||||||
|
$: darkMode = !currentTheme.includes("light")
|
||||||
|
|
||||||
|
let signatureModal
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<SignatureModal
|
||||||
|
{darkMode}
|
||||||
|
onConfirm={async sigCanvas => {
|
||||||
|
const signatureFile = sigCanvas.toFile()
|
||||||
|
|
||||||
|
let attachRequest = new FormData()
|
||||||
|
attachRequest.append("file", signatureFile)
|
||||||
|
|
||||||
|
try {
|
||||||
|
const uploadReq = await API.uploadBuilderAttachment(attachRequest)
|
||||||
|
const [signatureAttachment] = uploadReq
|
||||||
|
value = signatureAttachment
|
||||||
|
} catch (error) {
|
||||||
|
$notifications.error(error.message || "Failed to save signature")
|
||||||
|
value = []
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
title={meta.name}
|
||||||
|
{value}
|
||||||
|
bind:this={signatureModal}
|
||||||
|
/>
|
||||||
|
|
||||||
{#if type === "options" && meta.constraints.inclusion.length !== 0}
|
{#if type === "options" && meta.constraints.inclusion.length !== 0}
|
||||||
<Select
|
<Select
|
||||||
{label}
|
{label}
|
||||||
|
@ -58,7 +91,51 @@
|
||||||
bind:value
|
bind:value
|
||||||
/>
|
/>
|
||||||
{:else if type === "attachment"}
|
{:else if type === "attachment"}
|
||||||
<Dropzone {label} {error} bind:value />
|
<Dropzone
|
||||||
|
compact
|
||||||
|
{label}
|
||||||
|
{error}
|
||||||
|
{value}
|
||||||
|
on:change={e => {
|
||||||
|
value = e.detail
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{:else if type === "attachment_single"}
|
||||||
|
<Dropzone
|
||||||
|
compact
|
||||||
|
{label}
|
||||||
|
{error}
|
||||||
|
value={value ? [value] : []}
|
||||||
|
on:change={e => {
|
||||||
|
value = e.detail?.[0]
|
||||||
|
}}
|
||||||
|
maximum={1}
|
||||||
|
/>
|
||||||
|
{:else if type === "signature_single"}
|
||||||
|
<div class="signature">
|
||||||
|
<Label>{label}</Label>
|
||||||
|
<div class="sig-wrap" class:display={value}>
|
||||||
|
{#if value}
|
||||||
|
<CoreSignature
|
||||||
|
{darkMode}
|
||||||
|
{value}
|
||||||
|
editable={false}
|
||||||
|
on:clear={() => {
|
||||||
|
value = null
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{:else}
|
||||||
|
<ActionButton
|
||||||
|
fullWidth
|
||||||
|
on:click={() => {
|
||||||
|
signatureModal.show()
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Add signature
|
||||||
|
</ActionButton>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
{:else if type === "boolean"}
|
{:else if type === "boolean"}
|
||||||
<Toggle text={label} {error} bind:value />
|
<Toggle text={label} {error} bind:value />
|
||||||
{:else if type === "array" && meta.constraints.inclusion.length !== 0}
|
{:else if type === "array" && meta.constraints.inclusion.length !== 0}
|
||||||
|
@ -94,3 +171,22 @@
|
||||||
{:else}
|
{:else}
|
||||||
<Input {label} {type} {error} bind:value disabled={readonly} />
|
<Input {label} {type} {error} bind:value disabled={readonly} />
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.signature :global(label.spectrum-FieldLabel) {
|
||||||
|
padding-top: var(--spectrum-fieldlabel-padding-top);
|
||||||
|
padding-bottom: var(--spectrum-fieldlabel-padding-bottom);
|
||||||
|
}
|
||||||
|
.sig-wrap.display {
|
||||||
|
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>
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
<script>
|
<script>
|
||||||
import { datasources, tables, integrations, appStore } from "stores/builder"
|
import { datasources, tables, integrations, appStore } from "stores/builder"
|
||||||
import { admin } from "stores/portal"
|
import { themeStore, admin } 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"
|
||||||
|
@ -38,6 +38,9 @@
|
||||||
})
|
})
|
||||||
$: relationshipsEnabled = relationshipSupport(tableDatasource)
|
$: relationshipsEnabled = relationshipSupport(tableDatasource)
|
||||||
|
|
||||||
|
$: currentTheme = $themeStore?.theme
|
||||||
|
$: darkMode = !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
|
||||||
|
@ -56,6 +59,7 @@
|
||||||
<div class="wrapper">
|
<div class="wrapper">
|
||||||
<Grid
|
<Grid
|
||||||
{API}
|
{API}
|
||||||
|
{darkMode}
|
||||||
datasource={gridDatasource}
|
datasource={gridDatasource}
|
||||||
canAddRows={!isUsersTable}
|
canAddRows={!isUsersTable}
|
||||||
canDeleteRows={!isUsersTable}
|
canDeleteRows={!isUsersTable}
|
||||||
|
|
|
@ -9,6 +9,7 @@ const MAX_DEPTH = 1
|
||||||
const TYPES_TO_SKIP = [
|
const TYPES_TO_SKIP = [
|
||||||
FieldType.FORMULA,
|
FieldType.FORMULA,
|
||||||
FieldType.LONGFORM,
|
FieldType.LONGFORM,
|
||||||
|
FieldType.SIGNATURE_SINGLE,
|
||||||
FieldType.ATTACHMENTS,
|
FieldType.ATTACHMENTS,
|
||||||
//https://github.com/Budibase/budibase/issues/3030
|
//https://github.com/Budibase/budibase/issues/3030
|
||||||
FieldType.INTERNAL,
|
FieldType.INTERNAL,
|
||||||
|
|
|
@ -412,6 +412,7 @@
|
||||||
FIELDS.FORMULA,
|
FIELDS.FORMULA,
|
||||||
FIELDS.JSON,
|
FIELDS.JSON,
|
||||||
FIELDS.BARCODEQR,
|
FIELDS.BARCODEQR,
|
||||||
|
FIELDS.SIGNATURE_SINGLE,
|
||||||
FIELDS.BIGINT,
|
FIELDS.BIGINT,
|
||||||
FIELDS.AUTO,
|
FIELDS.AUTO,
|
||||||
]
|
]
|
||||||
|
|
|
@ -54,6 +54,10 @@
|
||||||
label: "Attachment",
|
label: "Attachment",
|
||||||
value: FieldType.ATTACHMENT_SINGLE,
|
value: FieldType.ATTACHMENT_SINGLE,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
label: "Signature",
|
||||||
|
value: FieldType.SIGNATURE_SINGLE,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
label: "Attachment list",
|
label: "Attachment list",
|
||||||
value: FieldType.ATTACHMENTS,
|
value: FieldType.ATTACHMENTS,
|
||||||
|
|
|
@ -28,6 +28,12 @@
|
||||||
let bindingDrawer
|
let bindingDrawer
|
||||||
let currentVal = value
|
let currentVal = value
|
||||||
|
|
||||||
|
let attachmentTypes = [
|
||||||
|
FieldType.ATTACHMENT_SINGLE,
|
||||||
|
FieldType.ATTACHMENTS,
|
||||||
|
FieldType.SIGNATURE_SINGLE,
|
||||||
|
]
|
||||||
|
|
||||||
$: readableValue = runtimeToReadableBinding(bindings, value)
|
$: readableValue = runtimeToReadableBinding(bindings, value)
|
||||||
$: tempValue = readableValue
|
$: tempValue = readableValue
|
||||||
$: isJS = isJSBinding(value)
|
$: isJS = isJSBinding(value)
|
||||||
|
@ -105,6 +111,7 @@
|
||||||
boolean: isValidBoolean,
|
boolean: isValidBoolean,
|
||||||
attachment: false,
|
attachment: false,
|
||||||
attachment_single: false,
|
attachment_single: false,
|
||||||
|
signature_single: false,
|
||||||
}
|
}
|
||||||
|
|
||||||
const isValid = value => {
|
const isValid = value => {
|
||||||
|
@ -126,6 +133,7 @@
|
||||||
"bigint",
|
"bigint",
|
||||||
"barcodeqr",
|
"barcodeqr",
|
||||||
"attachment",
|
"attachment",
|
||||||
|
"signature_single",
|
||||||
"attachment_single",
|
"attachment_single",
|
||||||
].includes(type)
|
].includes(type)
|
||||||
) {
|
) {
|
||||||
|
@ -169,7 +177,7 @@
|
||||||
{updateOnChange}
|
{updateOnChange}
|
||||||
/>
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
{#if !disabled && type !== "formula" && !disabled && type !== FieldType.ATTACHMENTS && !disabled && type !== FieldType.ATTACHMENT_SINGLE}
|
{#if !disabled && type !== "formula" && !disabled && !attachmentTypes.includes(type)}
|
||||||
<div
|
<div
|
||||||
class={`icon ${getIconClass(value, type)}`}
|
class={`icon ${getIconClass(value, type)}`}
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
|
|
|
@ -76,6 +76,7 @@ const componentMap = {
|
||||||
"field/array": FormFieldSelect,
|
"field/array": FormFieldSelect,
|
||||||
"field/json": FormFieldSelect,
|
"field/json": FormFieldSelect,
|
||||||
"field/barcodeqr": FormFieldSelect,
|
"field/barcodeqr": FormFieldSelect,
|
||||||
|
"field/signature_single": FormFieldSelect,
|
||||||
"field/bb_reference": FormFieldSelect,
|
"field/bb_reference": FormFieldSelect,
|
||||||
// Some validation types are the same as others, so not all types are
|
// Some validation types are the same as others, so not all types are
|
||||||
// explicitly listed here. e.g. options uses string validation
|
// explicitly listed here. e.g. options uses string validation
|
||||||
|
@ -85,6 +86,8 @@ const componentMap = {
|
||||||
"validation/boolean": ValidationEditor,
|
"validation/boolean": ValidationEditor,
|
||||||
"validation/datetime": ValidationEditor,
|
"validation/datetime": ValidationEditor,
|
||||||
"validation/attachment": ValidationEditor,
|
"validation/attachment": ValidationEditor,
|
||||||
|
"validation/attachment_single": ValidationEditor,
|
||||||
|
"validation/signature_single": ValidationEditor,
|
||||||
"validation/link": ValidationEditor,
|
"validation/link": ValidationEditor,
|
||||||
"validation/bb_reference": ValidationEditor,
|
"validation/bb_reference": ValidationEditor,
|
||||||
}
|
}
|
||||||
|
|
|
@ -41,6 +41,7 @@ export const FieldTypeToComponentMap = {
|
||||||
[FieldType.BOOLEAN]: "booleanfield",
|
[FieldType.BOOLEAN]: "booleanfield",
|
||||||
[FieldType.LONGFORM]: "longformfield",
|
[FieldType.LONGFORM]: "longformfield",
|
||||||
[FieldType.DATETIME]: "datetimefield",
|
[FieldType.DATETIME]: "datetimefield",
|
||||||
|
[FieldType.SIGNATURE_SINGLE]: "signaturesinglefield",
|
||||||
[FieldType.ATTACHMENTS]: "attachmentfield",
|
[FieldType.ATTACHMENTS]: "attachmentfield",
|
||||||
[FieldType.ATTACHMENT_SINGLE]: "attachmentsinglefield",
|
[FieldType.ATTACHMENT_SINGLE]: "attachmentsinglefield",
|
||||||
[FieldType.LINK]: "relationshipfield",
|
[FieldType.LINK]: "relationshipfield",
|
||||||
|
|
|
@ -108,6 +108,8 @@
|
||||||
Constraints.MaxFileSize,
|
Constraints.MaxFileSize,
|
||||||
Constraints.MaxUploadSize,
|
Constraints.MaxUploadSize,
|
||||||
],
|
],
|
||||||
|
["attachment_single"]: [Constraints.Required, Constraints.MaxUploadSize],
|
||||||
|
["signature_single"]: [Constraints.Required],
|
||||||
["link"]: [
|
["link"]: [
|
||||||
Constraints.Required,
|
Constraints.Required,
|
||||||
Constraints.Contains,
|
Constraints.Contains,
|
||||||
|
|
|
@ -127,6 +127,14 @@ export const FIELDS = {
|
||||||
presence: false,
|
presence: false,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
SIGNATURE_SINGLE: {
|
||||||
|
name: "Signature",
|
||||||
|
type: FieldType.SIGNATURE_SINGLE,
|
||||||
|
icon: "AnnotatePen",
|
||||||
|
constraints: {
|
||||||
|
presence: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
LINK: {
|
LINK: {
|
||||||
name: "Relationship",
|
name: "Relationship",
|
||||||
type: FieldType.LINK,
|
type: FieldType.LINK,
|
||||||
|
|
|
@ -71,6 +71,7 @@
|
||||||
"multifieldselect",
|
"multifieldselect",
|
||||||
"s3upload",
|
"s3upload",
|
||||||
"codescanner",
|
"codescanner",
|
||||||
|
"signaturesinglefield",
|
||||||
"bbreferencesinglefield",
|
"bbreferencesinglefield",
|
||||||
"bbreferencefield"
|
"bbreferencefield"
|
||||||
]
|
]
|
||||||
|
|
|
@ -4115,6 +4115,55 @@
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"signaturesinglefield": {
|
||||||
|
"name": "Signature",
|
||||||
|
"icon": "AnnotatePen",
|
||||||
|
"styles": ["size"],
|
||||||
|
"size": {
|
||||||
|
"width": 400,
|
||||||
|
"height": 50
|
||||||
|
},
|
||||||
|
"settings": [
|
||||||
|
{
|
||||||
|
"type": "field/signature_single",
|
||||||
|
"label": "Field",
|
||||||
|
"key": "field",
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "text",
|
||||||
|
"label": "Label",
|
||||||
|
"key": "label"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "text",
|
||||||
|
"label": "Help text",
|
||||||
|
"key": "helpText"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "boolean",
|
||||||
|
"label": "Disabled",
|
||||||
|
"key": "disabled",
|
||||||
|
"defaultValue": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "event",
|
||||||
|
"label": "On change",
|
||||||
|
"key": "onChange",
|
||||||
|
"context": [
|
||||||
|
{
|
||||||
|
"label": "Field Value",
|
||||||
|
"key": "value"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "validation/signature_single",
|
||||||
|
"label": "Validation",
|
||||||
|
"key": "validation"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
"embeddedmap": {
|
"embeddedmap": {
|
||||||
"name": "Embedded Map",
|
"name": "Embedded Map",
|
||||||
"icon": "Location",
|
"icon": "Location",
|
||||||
|
@ -4380,7 +4429,7 @@
|
||||||
"defaultValue": false
|
"defaultValue": false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "validation/attachment",
|
"type": "validation/attachment_single",
|
||||||
"label": "Validation",
|
"label": "Validation",
|
||||||
"key": "validation"
|
"key": "validation"
|
||||||
},
|
},
|
||||||
|
|
|
@ -34,7 +34,8 @@
|
||||||
"screenfull": "^6.0.1",
|
"screenfull": "^6.0.1",
|
||||||
"shortid": "^2.2.15",
|
"shortid": "^2.2.15",
|
||||||
"svelte-apexcharts": "^1.0.2",
|
"svelte-apexcharts": "^1.0.2",
|
||||||
"svelte-spa-router": "^4.0.1"
|
"svelte-spa-router": "^4.0.1",
|
||||||
|
"atrament": "^4.3.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@rollup/plugin-alias": "^5.1.0",
|
"@rollup/plugin-alias": "^5.1.0",
|
||||||
|
|
|
@ -193,6 +193,9 @@
|
||||||
$: pad = pad || (interactive && hasChildren && inDndPath)
|
$: pad = pad || (interactive && hasChildren && inDndPath)
|
||||||
$: $dndIsDragging, (pad = false)
|
$: $dndIsDragging, (pad = false)
|
||||||
|
|
||||||
|
$: currentTheme = $context?.device?.theme
|
||||||
|
$: darkMode = !currentTheme?.includes("light")
|
||||||
|
|
||||||
// Update component context
|
// Update component context
|
||||||
$: store.set({
|
$: store.set({
|
||||||
id,
|
id,
|
||||||
|
@ -222,6 +225,7 @@
|
||||||
parent: id,
|
parent: id,
|
||||||
ancestors: [...($component?.ancestors ?? []), instance._component],
|
ancestors: [...($component?.ancestors ?? []), instance._component],
|
||||||
path: [...($component?.path ?? []), id],
|
path: [...($component?.path ?? []), id],
|
||||||
|
darkMode,
|
||||||
})
|
})
|
||||||
|
|
||||||
const initialise = (instance, force = false) => {
|
const initialise = (instance, force = false) => {
|
||||||
|
|
|
@ -37,8 +37,10 @@
|
||||||
|
|
||||||
let grid
|
let grid
|
||||||
let gridContext
|
let gridContext
|
||||||
let minHeight
|
let minHeight = 0
|
||||||
|
|
||||||
|
$: currentTheme = $context?.device?.theme
|
||||||
|
$: darkMode = !currentTheme?.includes("light")
|
||||||
$: parsedColumns = getParsedColumns(columns)
|
$: parsedColumns = getParsedColumns(columns)
|
||||||
$: columnWhitelist = parsedColumns.filter(x => x.active).map(x => x.field)
|
$: columnWhitelist = parsedColumns.filter(x => x.active).map(x => x.field)
|
||||||
$: schemaOverrides = getSchemaOverrides(parsedColumns)
|
$: schemaOverrides = getSchemaOverrides(parsedColumns)
|
||||||
|
@ -154,6 +156,7 @@
|
||||||
{API}
|
{API}
|
||||||
{stripeRows}
|
{stripeRows}
|
||||||
{quiet}
|
{quiet}
|
||||||
|
{darkMode}
|
||||||
{initialFilter}
|
{initialFilter}
|
||||||
{initialSortColumn}
|
{initialSortColumn}
|
||||||
{initialSortOrder}
|
{initialSortOrder}
|
||||||
|
|
|
@ -15,6 +15,7 @@
|
||||||
[FieldType.BOOLEAN]: "booleanfield",
|
[FieldType.BOOLEAN]: "booleanfield",
|
||||||
[FieldType.LONGFORM]: "longformfield",
|
[FieldType.LONGFORM]: "longformfield",
|
||||||
[FieldType.DATETIME]: "datetimefield",
|
[FieldType.DATETIME]: "datetimefield",
|
||||||
|
[FieldType.SIGNATURE_SINGLE]: "signaturesinglefield",
|
||||||
[FieldType.ATTACHMENTS]: "attachmentfield",
|
[FieldType.ATTACHMENTS]: "attachmentfield",
|
||||||
[FieldType.ATTACHMENT_SINGLE]: "attachmentsinglefield",
|
[FieldType.ATTACHMENT_SINGLE]: "attachmentsinglefield",
|
||||||
[FieldType.LINK]: "relationshipfield",
|
[FieldType.LINK]: "relationshipfield",
|
||||||
|
|
|
@ -0,0 +1,129 @@
|
||||||
|
<script>
|
||||||
|
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,
|
||||||
|
})
|
||||||
|
const [signatureAttachment] = resp
|
||||||
|
updateValue = signatureAttachment
|
||||||
|
} else {
|
||||||
|
updateValue = null
|
||||||
|
}
|
||||||
|
|
||||||
|
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 changed = fieldApi.setValue(null)
|
||||||
|
if (onChange && changed) {
|
||||||
|
onChange({ value: null })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$: currentTheme = $context?.device?.theme
|
||||||
|
$: darkMode = !currentTheme?.includes("light")
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<SignatureModal
|
||||||
|
onConfirm={saveSignature}
|
||||||
|
title={label || fieldSchema?.name || ""}
|
||||||
|
value={fieldState?.value}
|
||||||
|
{darkMode}
|
||||||
|
bind:this={modal}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Field
|
||||||
|
{label}
|
||||||
|
{field}
|
||||||
|
{disabled}
|
||||||
|
{readonly}
|
||||||
|
{validation}
|
||||||
|
{span}
|
||||||
|
{helpText}
|
||||||
|
type="signature_single"
|
||||||
|
bind:fieldState
|
||||||
|
bind:fieldApi
|
||||||
|
bind:fieldSchema
|
||||||
|
defaultValue={[]}
|
||||||
|
>
|
||||||
|
{#if fieldState}
|
||||||
|
{#if (Array.isArray(fieldState?.value) && !fieldState?.value?.length) || !fieldState?.value}
|
||||||
|
<ActionButton
|
||||||
|
fullWidth
|
||||||
|
disabled={fieldState.disabled}
|
||||||
|
on:click={() => {
|
||||||
|
if (!$builderStore.inBuilder) {
|
||||||
|
modal.show()
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Add signature
|
||||||
|
</ActionButton>
|
||||||
|
{:else}
|
||||||
|
<div class="signature-field">
|
||||||
|
<CoreSignature
|
||||||
|
{darkMode}
|
||||||
|
disabled={$builderStore.inBuilder || fieldState.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>
|
|
@ -16,5 +16,6 @@ export { default as formstep } from "./FormStep.svelte"
|
||||||
export { default as jsonfield } from "./JSONField.svelte"
|
export { default as jsonfield } from "./JSONField.svelte"
|
||||||
export { default as s3upload } from "./S3Upload.svelte"
|
export { default as s3upload } from "./S3Upload.svelte"
|
||||||
export { default as codescanner } from "./CodeScannerField.svelte"
|
export { default as codescanner } from "./CodeScannerField.svelte"
|
||||||
|
export { default as signaturesinglefield } from "./SignatureField.svelte"
|
||||||
export { default as bbreferencefield } from "./BBReferenceField.svelte"
|
export { default as bbreferencefield } from "./BBReferenceField.svelte"
|
||||||
export { default as bbreferencesinglefield } from "./BBReferenceSingleField.svelte"
|
export { default as bbreferencesinglefield } from "./BBReferenceSingleField.svelte"
|
||||||
|
|
|
@ -200,6 +200,17 @@ const parseType = (value, type) => {
|
||||||
return value
|
return value
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Parse attachment/signature single, treating no key as null
|
||||||
|
if (
|
||||||
|
type === FieldTypes.ATTACHMENT_SINGLE ||
|
||||||
|
type === FieldTypes.SIGNATURE_SINGLE
|
||||||
|
) {
|
||||||
|
if (!value?.key) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
||||||
// Parse links, treating no elements as null
|
// Parse links, treating no elements as null
|
||||||
if (type === FieldTypes.LINK) {
|
if (type === FieldTypes.LINK) {
|
||||||
if (!Array.isArray(value) || !value.length) {
|
if (!Array.isArray(value) || !value.length) {
|
||||||
|
@ -246,10 +257,8 @@ const maxLengthHandler = (value, rule) => {
|
||||||
// Evaluates a max file size (MB) constraint
|
// Evaluates a max file size (MB) constraint
|
||||||
const maxFileSizeHandler = (value, rule) => {
|
const maxFileSizeHandler = (value, rule) => {
|
||||||
const limit = parseType(rule.value, "number")
|
const limit = parseType(rule.value, "number")
|
||||||
return (
|
const check = attachment => attachment.size / 1000000 > limit
|
||||||
value == null ||
|
return value == null || !(value?.key ? check(value) : value.some(check))
|
||||||
!value.some(attachment => attachment.size / 1000000 > limit)
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Evaluates a max total upload size (MB) constraint
|
// Evaluates a max total upload size (MB) constraint
|
||||||
|
@ -257,8 +266,11 @@ const maxUploadSizeHandler = (value, rule) => {
|
||||||
const limit = parseType(rule.value, "number")
|
const limit = parseType(rule.value, "number")
|
||||||
return (
|
return (
|
||||||
value == null ||
|
value == null ||
|
||||||
value.reduce((acc, currentItem) => acc + currentItem.size, 0) / 1000000 <=
|
(value?.key
|
||||||
limit
|
? value.size / 1000000 <= limit
|
||||||
|
: value.reduce((acc, currentItem) => acc + currentItem.size, 0) /
|
||||||
|
1000000 <=
|
||||||
|
limit)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,59 @@
|
||||||
|
<script>
|
||||||
|
import { Modal, ModalContent, Body, CoreSignature } from "@budibase/bbui"
|
||||||
|
|
||||||
|
export let onConfirm = () => {}
|
||||||
|
export let value
|
||||||
|
export let title
|
||||||
|
export let darkMode
|
||||||
|
|
||||||
|
export const show = () => {
|
||||||
|
edited = false
|
||||||
|
modal.show()
|
||||||
|
}
|
||||||
|
|
||||||
|
let modal
|
||||||
|
let canvas
|
||||||
|
let edited = false
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Modal bind:this={modal}>
|
||||||
|
<ModalContent
|
||||||
|
showConfirmButton
|
||||||
|
showCancelButton={false}
|
||||||
|
showCloseIcon={false}
|
||||||
|
custom
|
||||||
|
disabled={!edited}
|
||||||
|
showDivider={false}
|
||||||
|
onConfirm={() => {
|
||||||
|
onConfirm(canvas)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div slot="header">
|
||||||
|
<Body>{title}</Body>
|
||||||
|
</div>
|
||||||
|
<div class="signature-wrap modal">
|
||||||
|
<CoreSignature
|
||||||
|
{darkMode}
|
||||||
|
{value}
|
||||||
|
saveIcon={false}
|
||||||
|
bind:this={canvas}
|
||||||
|
on:update={() => {
|
||||||
|
edited = true
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</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>
|
|
@ -8,7 +8,6 @@
|
||||||
export let onChange
|
export let onChange
|
||||||
export let readonly = false
|
export let readonly = false
|
||||||
export let api
|
export let api
|
||||||
export let invertX = false
|
|
||||||
export let schema
|
export let schema
|
||||||
export let maximum
|
export let maximum
|
||||||
|
|
||||||
|
@ -92,13 +91,7 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#if isOpen}
|
{#if isOpen}
|
||||||
<GridPopover
|
<GridPopover open={isOpen} {anchor} maxHeight={null} on:close={close}>
|
||||||
open={isOpen}
|
|
||||||
{anchor}
|
|
||||||
{invertX}
|
|
||||||
maxHeight={null}
|
|
||||||
on:close={close}
|
|
||||||
>
|
|
||||||
<div class="dropzone">
|
<div class="dropzone">
|
||||||
<Dropzone
|
<Dropzone
|
||||||
{value}
|
{value}
|
||||||
|
|
|
@ -18,8 +18,6 @@
|
||||||
export let row
|
export let row
|
||||||
export let cellId
|
export let cellId
|
||||||
export let updateValue = rows.actions.updateValue
|
export let updateValue = rows.actions.updateValue
|
||||||
export let invertX = false
|
|
||||||
export let invertY = false
|
|
||||||
export let contentLines = 1
|
export let contentLines = 1
|
||||||
export let hidden = false
|
export let hidden = false
|
||||||
|
|
||||||
|
@ -93,8 +91,6 @@
|
||||||
onChange={cellAPI.setValue}
|
onChange={cellAPI.setValue}
|
||||||
{focused}
|
{focused}
|
||||||
{readonly}
|
{readonly}
|
||||||
{invertY}
|
|
||||||
{invertX}
|
|
||||||
{contentLines}
|
{contentLines}
|
||||||
/>
|
/>
|
||||||
<slot />
|
<slot />
|
||||||
|
|
|
@ -10,7 +10,6 @@
|
||||||
export let focused = false
|
export let focused = false
|
||||||
export let readonly = false
|
export let readonly = false
|
||||||
export let api
|
export let api
|
||||||
export let invertX = false
|
|
||||||
|
|
||||||
let isOpen
|
let isOpen
|
||||||
let anchor
|
let anchor
|
||||||
|
@ -111,7 +110,7 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#if isOpen}
|
{#if isOpen}
|
||||||
<GridPopover {anchor} {invertX} maxHeight={null} on:close={close}>
|
<GridPopover {anchor} maxHeight={null} on:close={close}>
|
||||||
<CoreDatePickerPopoverContents
|
<CoreDatePickerPopoverContents
|
||||||
value={parsedValue}
|
value={parsedValue}
|
||||||
useKeyboardShortcuts={false}
|
useKeyboardShortcuts={false}
|
||||||
|
|
|
@ -8,7 +8,6 @@
|
||||||
export let onChange
|
export let onChange
|
||||||
export let readonly = false
|
export let readonly = false
|
||||||
export let api
|
export let api
|
||||||
export let invertX = false
|
|
||||||
|
|
||||||
let textarea
|
let textarea
|
||||||
let isOpen = false
|
let isOpen = false
|
||||||
|
@ -67,7 +66,7 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#if isOpen}
|
{#if isOpen}
|
||||||
<GridPopover {anchor} {invertX} on:close={close}>
|
<GridPopover {anchor} on:close={close}>
|
||||||
<textarea
|
<textarea
|
||||||
bind:this={textarea}
|
bind:this={textarea}
|
||||||
value={value || ""}
|
value={value || ""}
|
||||||
|
|
|
@ -11,7 +11,6 @@
|
||||||
export let multi = false
|
export let multi = false
|
||||||
export let readonly = false
|
export let readonly = false
|
||||||
export let api
|
export let api
|
||||||
export let invertX
|
|
||||||
export let contentLines = 1
|
export let contentLines = 1
|
||||||
|
|
||||||
let isOpen = false
|
let isOpen = false
|
||||||
|
@ -120,7 +119,7 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#if isOpen}
|
{#if isOpen}
|
||||||
<GridPopover {anchor} {invertX} on:close={close}>
|
<GridPopover {anchor} on:close={close}>
|
||||||
<div class="options">
|
<div class="options">
|
||||||
{#each options as option, idx}
|
{#each options as option, idx}
|
||||||
{@const color = optionColors[option] || getOptionColor(option)}
|
{@const color = optionColors[option] || getOptionColor(option)}
|
||||||
|
|
|
@ -13,7 +13,6 @@
|
||||||
export let focused
|
export let focused
|
||||||
export let schema
|
export let schema
|
||||||
export let onChange
|
export let onChange
|
||||||
export let invertX = false
|
|
||||||
export let contentLines = 1
|
export let contentLines = 1
|
||||||
export let searchFunction = API.searchTable
|
export let searchFunction = API.searchTable
|
||||||
export let primaryDisplay
|
export let primaryDisplay
|
||||||
|
@ -275,7 +274,7 @@
|
||||||
<!-- 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 -->
|
||||||
{#if isOpen}
|
{#if isOpen}
|
||||||
<GridPopover open={isOpen} {anchor} {invertX} on:close={close}>
|
<GridPopover open={isOpen} {anchor} on:close={close}>
|
||||||
<div class="dropdown" on:wheel|stopPropagation>
|
<div class="dropdown" on:wheel|stopPropagation>
|
||||||
<div class="search">
|
<div class="search">
|
||||||
<Input
|
<Input
|
||||||
|
|
|
@ -0,0 +1,162 @@
|
||||||
|
<script>
|
||||||
|
import { onMount, getContext } from "svelte"
|
||||||
|
import { SignatureModal } from "@budibase/frontend-core/src/components"
|
||||||
|
import { CoreSignature, ActionButton } from "@budibase/bbui"
|
||||||
|
import GridPopover from "../overlays/GridPopover.svelte"
|
||||||
|
|
||||||
|
export let schema
|
||||||
|
export let value
|
||||||
|
export let focused = false
|
||||||
|
export let onChange
|
||||||
|
export let readonly = false
|
||||||
|
export let api
|
||||||
|
|
||||||
|
const { API, notifications, props } = getContext("grid")
|
||||||
|
|
||||||
|
let isOpen = false
|
||||||
|
let modal
|
||||||
|
let anchor
|
||||||
|
|
||||||
|
$: editable = focused && !readonly
|
||||||
|
$: {
|
||||||
|
if (!focused) {
|
||||||
|
close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const onKeyDown = () => {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
const open = () => {
|
||||||
|
isOpen = true
|
||||||
|
}
|
||||||
|
|
||||||
|
const close = () => {
|
||||||
|
isOpen = false
|
||||||
|
}
|
||||||
|
|
||||||
|
const deleteSignature = async () => {
|
||||||
|
onChange(null)
|
||||||
|
}
|
||||||
|
|
||||||
|
const saveSignature = async sigCanvas => {
|
||||||
|
const signatureFile = sigCanvas.toFile()
|
||||||
|
|
||||||
|
let attachRequest = new FormData()
|
||||||
|
attachRequest.append("file", signatureFile)
|
||||||
|
|
||||||
|
try {
|
||||||
|
const uploadReq = await API.uploadBuilderAttachment(attachRequest)
|
||||||
|
const [signatureAttachment] = uploadReq
|
||||||
|
onChange(signatureAttachment)
|
||||||
|
} catch (error) {
|
||||||
|
$notifications.error(error.message || "Failed to save signature")
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
api = {
|
||||||
|
focus: () => open(),
|
||||||
|
blur: () => close(),
|
||||||
|
isActive: () => isOpen,
|
||||||
|
onKeyDown,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||||
|
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||||
|
<div
|
||||||
|
class="signature-cell"
|
||||||
|
class:light={!$props?.darkMode}
|
||||||
|
class:editable
|
||||||
|
bind:this={anchor}
|
||||||
|
on:click={editable ? open : null}
|
||||||
|
>
|
||||||
|
{#if value?.url}
|
||||||
|
<!-- svelte-ignore a11y-missing-attribute -->
|
||||||
|
<img src={value?.url} />
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<SignatureModal
|
||||||
|
onConfirm={saveSignature}
|
||||||
|
title={schema?.name}
|
||||||
|
{value}
|
||||||
|
darkMode={$props.darkMode}
|
||||||
|
bind:this={modal}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{#if isOpen}
|
||||||
|
<GridPopover open={isOpen} {anchor} maxHeight={null} on:close={close}>
|
||||||
|
<div class="signature" class:empty={!value}>
|
||||||
|
{#if value?.key}
|
||||||
|
<div class="signature-wrap">
|
||||||
|
<CoreSignature
|
||||||
|
darkMode={$props.darkMode}
|
||||||
|
editable={false}
|
||||||
|
{value}
|
||||||
|
on:change={saveSignature}
|
||||||
|
on:clear={deleteSignature}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
<div class="add-signature">
|
||||||
|
<ActionButton
|
||||||
|
fullWidth
|
||||||
|
on:click={() => {
|
||||||
|
modal.show()
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Add signature
|
||||||
|
</ActionButton>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</GridPopover>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.signature {
|
||||||
|
min-width: 320px;
|
||||||
|
padding: var(--cell-padding);
|
||||||
|
background: var(--grid-background-alt);
|
||||||
|
border: var(--cell-border);
|
||||||
|
}
|
||||||
|
.signature.empty {
|
||||||
|
width: 100%;
|
||||||
|
min-width: unset;
|
||||||
|
}
|
||||||
|
.signature-cell.light img {
|
||||||
|
-webkit-filter: invert(100%);
|
||||||
|
filter: invert(100%);
|
||||||
|
}
|
||||||
|
.signature-cell {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: stretch;
|
||||||
|
max-width: 320px;
|
||||||
|
padding-left: var(--cell-padding);
|
||||||
|
padding-right: var(--cell-padding);
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
align-self: stretch;
|
||||||
|
overflow: hidden;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
.signature-cell.editable:hover {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.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>
|
|
@ -54,6 +54,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 darkMode
|
||||||
export let isCloud = null
|
export let isCloud = null
|
||||||
|
|
||||||
// Unique identifier for DOM nodes inside this instance
|
// Unique identifier for DOM nodes inside this instance
|
||||||
|
@ -109,6 +110,7 @@
|
||||||
notifySuccess,
|
notifySuccess,
|
||||||
notifyError,
|
notifyError,
|
||||||
buttons,
|
buttons,
|
||||||
|
darkMode,
|
||||||
isCloud,
|
isCloud,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
@ -9,7 +9,6 @@
|
||||||
bounds,
|
bounds,
|
||||||
renderedRows,
|
renderedRows,
|
||||||
visibleColumns,
|
visibleColumns,
|
||||||
rowVerticalInversionIndex,
|
|
||||||
hoveredRowId,
|
hoveredRowId,
|
||||||
dispatch,
|
dispatch,
|
||||||
isDragging,
|
isDragging,
|
||||||
|
@ -41,11 +40,7 @@
|
||||||
<div bind:this={body} class="grid-body">
|
<div bind:this={body} class="grid-body">
|
||||||
<GridScrollWrapper scrollHorizontally scrollVertically attachHandlers>
|
<GridScrollWrapper scrollHorizontally scrollVertically attachHandlers>
|
||||||
{#each $renderedRows as row, idx}
|
{#each $renderedRows as row, idx}
|
||||||
<GridRow
|
<GridRow {row} top={idx === 0} />
|
||||||
{row}
|
|
||||||
top={idx === 0}
|
|
||||||
invertY={idx >= $rowVerticalInversionIndex}
|
|
||||||
/>
|
|
||||||
{/each}
|
{/each}
|
||||||
{#if $config.canAddRows}
|
{#if $config.canAddRows}
|
||||||
<div
|
<div
|
||||||
|
|
|
@ -5,7 +5,6 @@
|
||||||
|
|
||||||
export let row
|
export let row
|
||||||
export let top = false
|
export let top = false
|
||||||
export let invertY = false
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
focusedCellId,
|
focusedCellId,
|
||||||
|
@ -15,7 +14,6 @@
|
||||||
hoveredRowId,
|
hoveredRowId,
|
||||||
selectedCellMap,
|
selectedCellMap,
|
||||||
focusedRow,
|
focusedRow,
|
||||||
columnHorizontalInversionIndex,
|
|
||||||
contentLines,
|
contentLines,
|
||||||
isDragging,
|
isDragging,
|
||||||
dispatch,
|
dispatch,
|
||||||
|
@ -38,15 +36,13 @@
|
||||||
on:mouseleave={$isDragging ? null : () => ($hoveredRowId = null)}
|
on:mouseleave={$isDragging ? null : () => ($hoveredRowId = null)}
|
||||||
on:click={() => dispatch("rowclick", rows.actions.cleanRow(row))}
|
on:click={() => dispatch("rowclick", rows.actions.cleanRow(row))}
|
||||||
>
|
>
|
||||||
{#each $visibleColumns as column, columnIdx}
|
{#each $visibleColumns as column}
|
||||||
{@const cellId = getCellID(row._id, column.name)}
|
{@const cellId = getCellID(row._id, column.name)}
|
||||||
<DataCell
|
<DataCell
|
||||||
{cellId}
|
{cellId}
|
||||||
{column}
|
{column}
|
||||||
{row}
|
{row}
|
||||||
{invertY}
|
|
||||||
{rowFocused}
|
{rowFocused}
|
||||||
invertX={columnIdx >= $columnHorizontalInversionIndex}
|
|
||||||
highlighted={rowHovered || rowFocused || reorderSource === column.name}
|
highlighted={rowHovered || rowFocused || reorderSource === column.name}
|
||||||
selected={rowSelected}
|
selected={rowSelected}
|
||||||
rowIdx={row.__idx}
|
rowIdx={row.__idx}
|
||||||
|
|
|
@ -24,8 +24,6 @@
|
||||||
rowHeight,
|
rowHeight,
|
||||||
hasNextPage,
|
hasNextPage,
|
||||||
maxScrollTop,
|
maxScrollTop,
|
||||||
rowVerticalInversionIndex,
|
|
||||||
columnHorizontalInversionIndex,
|
|
||||||
selectedRows,
|
selectedRows,
|
||||||
loaded,
|
loaded,
|
||||||
refreshing,
|
refreshing,
|
||||||
|
@ -43,17 +41,9 @@
|
||||||
$: firstColumn = $stickyColumn || $visibleColumns[0]
|
$: firstColumn = $stickyColumn || $visibleColumns[0]
|
||||||
$: width = GutterWidth + ($stickyColumn?.width || 0)
|
$: width = GutterWidth + ($stickyColumn?.width || 0)
|
||||||
$: $datasource, (visible = false)
|
$: $datasource, (visible = false)
|
||||||
$: invertY = shouldInvertY(offset, $rowVerticalInversionIndex, $renderedRows)
|
|
||||||
$: selectedRowCount = Object.values($selectedRows).length
|
$: selectedRowCount = Object.values($selectedRows).length
|
||||||
$: hasNoRows = !$rows.length
|
$: hasNoRows = !$rows.length
|
||||||
|
|
||||||
const shouldInvertY = (offset, inversionIndex, rows) => {
|
|
||||||
if (offset === 0) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return rows.length >= inversionIndex
|
|
||||||
}
|
|
||||||
|
|
||||||
const addRow = async () => {
|
const addRow = async () => {
|
||||||
// Blur the active cell and tick to let final value updates propagate
|
// Blur the active cell and tick to let final value updates propagate
|
||||||
isAdding = true
|
isAdding = true
|
||||||
|
@ -205,7 +195,6 @@
|
||||||
width={$stickyColumn.width}
|
width={$stickyColumn.width}
|
||||||
{updateValue}
|
{updateValue}
|
||||||
topRow={offset === 0}
|
topRow={offset === 0}
|
||||||
{invertY}
|
|
||||||
>
|
>
|
||||||
{#if $stickyColumn?.schema?.autocolumn}
|
{#if $stickyColumn?.schema?.autocolumn}
|
||||||
<div class="readonly-overlay">Can't edit auto column</div>
|
<div class="readonly-overlay">Can't edit auto column</div>
|
||||||
|
@ -219,7 +208,7 @@
|
||||||
<div class="normal-columns" transition:fade|local={{ duration: 130 }}>
|
<div class="normal-columns" transition:fade|local={{ duration: 130 }}>
|
||||||
<GridScrollWrapper scrollHorizontally attachHandlers>
|
<GridScrollWrapper scrollHorizontally attachHandlers>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
{#each $visibleColumns as column, columnIdx}
|
{#each $visibleColumns as column}
|
||||||
{@const cellId = `new-${column.name}`}
|
{@const cellId = `new-${column.name}`}
|
||||||
<DataCell
|
<DataCell
|
||||||
{cellId}
|
{cellId}
|
||||||
|
@ -230,8 +219,6 @@
|
||||||
focused={$focusedCellId === cellId}
|
focused={$focusedCellId === cellId}
|
||||||
width={column.width}
|
width={column.width}
|
||||||
topRow={offset === 0}
|
topRow={offset === 0}
|
||||||
invertX={columnIdx >= $columnHorizontalInversionIndex}
|
|
||||||
{invertY}
|
|
||||||
hidden={!$columnRenderMap[column.name]}
|
hidden={!$columnRenderMap[column.name]}
|
||||||
>
|
>
|
||||||
{#if column?.schema?.autocolumn}
|
{#if column?.schema?.autocolumn}
|
||||||
|
|
|
@ -13,6 +13,7 @@ import JSONCell from "../cells/JSONCell.svelte"
|
||||||
import AttachmentCell from "../cells/AttachmentCell.svelte"
|
import AttachmentCell from "../cells/AttachmentCell.svelte"
|
||||||
import AttachmentSingleCell from "../cells/AttachmentSingleCell.svelte"
|
import AttachmentSingleCell from "../cells/AttachmentSingleCell.svelte"
|
||||||
import BBReferenceCell from "../cells/BBReferenceCell.svelte"
|
import BBReferenceCell from "../cells/BBReferenceCell.svelte"
|
||||||
|
import SignatureCell from "../cells/SignatureCell.svelte"
|
||||||
import BBReferenceSingleCell from "../cells/BBReferenceSingleCell.svelte"
|
import BBReferenceSingleCell from "../cells/BBReferenceSingleCell.svelte"
|
||||||
|
|
||||||
const TypeComponentMap = {
|
const TypeComponentMap = {
|
||||||
|
@ -20,6 +21,7 @@ const TypeComponentMap = {
|
||||||
[FieldType.OPTIONS]: OptionsCell,
|
[FieldType.OPTIONS]: OptionsCell,
|
||||||
[FieldType.DATETIME]: DateCell,
|
[FieldType.DATETIME]: DateCell,
|
||||||
[FieldType.BARCODEQR]: TextCell,
|
[FieldType.BARCODEQR]: TextCell,
|
||||||
|
[FieldType.SIGNATURE_SINGLE]: SignatureCell,
|
||||||
[FieldType.LONGFORM]: LongFormCell,
|
[FieldType.LONGFORM]: LongFormCell,
|
||||||
[FieldType.ARRAY]: MultiSelectCell,
|
[FieldType.ARRAY]: MultiSelectCell,
|
||||||
[FieldType.NUMBER]: NumberCell,
|
[FieldType.NUMBER]: NumberCell,
|
||||||
|
|
|
@ -1,9 +1,5 @@
|
||||||
import { derived } from "svelte/store"
|
import { derived } from "svelte/store"
|
||||||
import {
|
import { MinColumnWidth } from "../lib/constants"
|
||||||
MaxCellRenderOverflow,
|
|
||||||
MinColumnWidth,
|
|
||||||
ScrollBarSize,
|
|
||||||
} from "../lib/constants"
|
|
||||||
|
|
||||||
export const deriveStores = context => {
|
export const deriveStores = context => {
|
||||||
const {
|
const {
|
||||||
|
@ -85,51 +81,10 @@ export const deriveStores = context => {
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
// Determine the row index at which we should start vertically inverting cell
|
|
||||||
// dropdowns
|
|
||||||
const rowVerticalInversionIndex = derived(
|
|
||||||
[height, rowHeight, scrollTop],
|
|
||||||
([$height, $rowHeight, $scrollTop]) => {
|
|
||||||
const offset = $scrollTop % $rowHeight
|
|
||||||
|
|
||||||
// Compute the last row index with space to render popovers below it
|
|
||||||
const minBottom =
|
|
||||||
$height - ScrollBarSize * 3 - MaxCellRenderOverflow + offset
|
|
||||||
const lastIdx = Math.floor(minBottom / $rowHeight)
|
|
||||||
|
|
||||||
// Compute the first row index with space to render popovers above it
|
|
||||||
const minTop = MaxCellRenderOverflow + offset
|
|
||||||
const firstIdx = Math.ceil(minTop / $rowHeight)
|
|
||||||
|
|
||||||
// Use the greater of the two indices so that we prefer content below,
|
|
||||||
// unless there is room to render the entire popover above
|
|
||||||
return Math.max(lastIdx, firstIdx)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
// Determine the column index at which we should start horizontally inverting
|
|
||||||
// cell dropdowns
|
|
||||||
const columnHorizontalInversionIndex = derived(
|
|
||||||
[visibleColumns, scrollLeft, width],
|
|
||||||
([$visibleColumns, $scrollLeft, $width]) => {
|
|
||||||
const cutoff = $width + $scrollLeft - ScrollBarSize * 3
|
|
||||||
let inversionIdx = $visibleColumns.length
|
|
||||||
for (let i = $visibleColumns.length - 1; i >= 0; i--, inversionIdx--) {
|
|
||||||
const rightEdge = $visibleColumns[i].left + $visibleColumns[i].width
|
|
||||||
if (rightEdge + MaxCellRenderOverflow <= cutoff) {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return inversionIdx
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
scrolledRowCount,
|
scrolledRowCount,
|
||||||
visualRowCapacity,
|
visualRowCapacity,
|
||||||
renderedRows,
|
renderedRows,
|
||||||
columnRenderMap,
|
columnRenderMap,
|
||||||
rowVerticalInversionIndex,
|
|
||||||
columnHorizontalInversionIndex,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -121,6 +121,7 @@ export const TypeIconMap = {
|
||||||
[FieldType.OPTIONS]: "Dropdown",
|
[FieldType.OPTIONS]: "Dropdown",
|
||||||
[FieldType.DATETIME]: "Calendar",
|
[FieldType.DATETIME]: "Calendar",
|
||||||
[FieldType.BARCODEQR]: "Camera",
|
[FieldType.BARCODEQR]: "Camera",
|
||||||
|
[FieldType.SIGNATURE_SINGLE]: "AnnotatePen",
|
||||||
[FieldType.LONGFORM]: "TextAlignLeft",
|
[FieldType.LONGFORM]: "TextAlignLeft",
|
||||||
[FieldType.ARRAY]: "Duplicate",
|
[FieldType.ARRAY]: "Duplicate",
|
||||||
[FieldType.NUMBER]: "123",
|
[FieldType.NUMBER]: "123",
|
||||||
|
|
|
@ -309,6 +309,11 @@ describe.each([
|
||||||
name: "attachments",
|
name: "attachments",
|
||||||
constraints: { type: "array", presence: false },
|
constraints: { type: "array", presence: false },
|
||||||
}
|
}
|
||||||
|
const signature: FieldSchema = {
|
||||||
|
type: FieldType.SIGNATURE_SINGLE,
|
||||||
|
name: "signature",
|
||||||
|
constraints: { presence: false },
|
||||||
|
}
|
||||||
const bool: FieldSchema = {
|
const bool: FieldSchema = {
|
||||||
type: FieldType.BOOLEAN,
|
type: FieldType.BOOLEAN,
|
||||||
name: "boolean",
|
name: "boolean",
|
||||||
|
@ -375,6 +380,8 @@ describe.each([
|
||||||
attachmentListUndefined: attachmentList,
|
attachmentListUndefined: attachmentList,
|
||||||
attachmentListEmpty: attachmentList,
|
attachmentListEmpty: attachmentList,
|
||||||
attachmentListEmptyArrayStr: attachmentList,
|
attachmentListEmptyArrayStr: attachmentList,
|
||||||
|
signatureNull: signature,
|
||||||
|
signatureUndefined: signature,
|
||||||
arrayFieldEmptyArrayStr: arrayField,
|
arrayFieldEmptyArrayStr: arrayField,
|
||||||
arrayFieldArrayStrKnown: arrayField,
|
arrayFieldArrayStrKnown: arrayField,
|
||||||
arrayFieldNull: arrayField,
|
arrayFieldNull: arrayField,
|
||||||
|
@ -416,6 +423,8 @@ describe.each([
|
||||||
attachmentListUndefined: undefined,
|
attachmentListUndefined: undefined,
|
||||||
attachmentListEmpty: "",
|
attachmentListEmpty: "",
|
||||||
attachmentListEmptyArrayStr: "[]",
|
attachmentListEmptyArrayStr: "[]",
|
||||||
|
signatureNull: null,
|
||||||
|
signatureUndefined: undefined,
|
||||||
arrayFieldEmptyArrayStr: "[]",
|
arrayFieldEmptyArrayStr: "[]",
|
||||||
arrayFieldUndefined: undefined,
|
arrayFieldUndefined: undefined,
|
||||||
arrayFieldNull: null,
|
arrayFieldNull: null,
|
||||||
|
@ -450,6 +459,8 @@ describe.each([
|
||||||
expect(row.attachmentListUndefined).toBe(undefined)
|
expect(row.attachmentListUndefined).toBe(undefined)
|
||||||
expect(row.attachmentListEmpty).toEqual([])
|
expect(row.attachmentListEmpty).toEqual([])
|
||||||
expect(row.attachmentListEmptyArrayStr).toEqual([])
|
expect(row.attachmentListEmptyArrayStr).toEqual([])
|
||||||
|
expect(row.signatureNull).toEqual(null)
|
||||||
|
expect(row.signatureUndefined).toBe(undefined)
|
||||||
expect(row.arrayFieldEmptyArrayStr).toEqual([])
|
expect(row.arrayFieldEmptyArrayStr).toEqual([])
|
||||||
expect(row.arrayFieldNull).toEqual([])
|
expect(row.arrayFieldNull).toEqual([])
|
||||||
expect(row.arrayFieldUndefined).toEqual(undefined)
|
expect(row.arrayFieldUndefined).toEqual(undefined)
|
||||||
|
@ -894,70 +905,91 @@ describe.each([
|
||||||
})
|
})
|
||||||
|
|
||||||
isInternal &&
|
isInternal &&
|
||||||
describe("attachments", () => {
|
describe("attachments and signatures", () => {
|
||||||
it("should allow enriching single attachment rows", async () => {
|
const coreAttachmentEnrichment = async (
|
||||||
const table = await config.api.table.save(
|
schema: any,
|
||||||
|
field: string,
|
||||||
|
attachmentCfg: string | string[]
|
||||||
|
) => {
|
||||||
|
const testTable = await config.api.table.save(
|
||||||
defaultTable({
|
defaultTable({
|
||||||
schema: {
|
schema,
|
||||||
attachment: {
|
|
||||||
type: FieldType.ATTACHMENT_SINGLE,
|
|
||||||
name: "attachment",
|
|
||||||
constraints: { presence: false },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
const attachmentId = `${uuid.v4()}.csv`
|
const attachmentToStoreKey = (attachmentId: string) => {
|
||||||
const row = await config.api.row.save(table._id!, {
|
return {
|
||||||
|
key: `${config.getAppId()}/attachments/${attachmentId}`,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const draftRow = {
|
||||||
name: "test",
|
name: "test",
|
||||||
description: "test",
|
description: "test",
|
||||||
attachment: {
|
[field]:
|
||||||
key: `${config.getAppId()}/attachments/${attachmentId}`,
|
typeof attachmentCfg === "string"
|
||||||
},
|
? attachmentToStoreKey(attachmentCfg)
|
||||||
|
: attachmentCfg.map(attachmentToStoreKey),
|
||||||
|
tableId: testTable._id,
|
||||||
|
}
|
||||||
|
const row = await config.api.row.save(testTable._id!, draftRow)
|
||||||
|
|
||||||
tableId: table._id,
|
|
||||||
})
|
|
||||||
await config.withEnv({ SELF_HOSTED: "true" }, async () => {
|
await config.withEnv({ SELF_HOSTED: "true" }, async () => {
|
||||||
return context.doInAppContext(config.getAppId(), async () => {
|
return context.doInAppContext(config.getAppId(), async () => {
|
||||||
const enriched = await outputProcessing(table, [row])
|
const enriched: Row[] = await outputProcessing(table, [row])
|
||||||
expect((enriched as Row[])[0].attachment.url.split("?")[0]).toBe(
|
const [targetRow] = enriched
|
||||||
`/files/signed/prod-budi-app-assets/${config.getProdAppId()}/attachments/${attachmentId}`
|
const attachmentEntries = Array.isArray(targetRow[field])
|
||||||
)
|
? targetRow[field]
|
||||||
|
: [targetRow[field]]
|
||||||
|
|
||||||
|
for (const entry of attachmentEntries) {
|
||||||
|
const attachmentId = entry.key.split("/").pop()
|
||||||
|
expect(entry.url.split("?")[0]).toBe(
|
||||||
|
`/files/signed/prod-budi-app-assets/${config.getProdAppId()}/attachments/${attachmentId}`
|
||||||
|
)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
it("should allow enriching single attachment rows", async () => {
|
||||||
|
await coreAttachmentEnrichment(
|
||||||
|
{
|
||||||
|
attachment: {
|
||||||
|
type: FieldType.ATTACHMENT_SINGLE,
|
||||||
|
name: "attachment",
|
||||||
|
constraints: { presence: false },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"attachment",
|
||||||
|
`${uuid.v4()}.csv`
|
||||||
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
it("should allow enriching attachment list rows", async () => {
|
it("should allow enriching attachment list rows", async () => {
|
||||||
const table = await config.api.table.save(
|
await coreAttachmentEnrichment(
|
||||||
defaultTable({
|
{
|
||||||
schema: {
|
attachments: {
|
||||||
attachment: {
|
type: FieldType.ATTACHMENTS,
|
||||||
type: FieldType.ATTACHMENTS,
|
name: "attachments",
|
||||||
name: "attachment",
|
constraints: { type: "array", presence: false },
|
||||||
constraints: { type: "array", presence: false },
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
})
|
},
|
||||||
|
"attachments",
|
||||||
|
[`${uuid.v4()}.csv`]
|
||||||
)
|
)
|
||||||
const attachmentId = `${uuid.v4()}.csv`
|
})
|
||||||
const row = await config.api.row.save(table._id!, {
|
|
||||||
name: "test",
|
it("should allow enriching signature rows", async () => {
|
||||||
description: "test",
|
await coreAttachmentEnrichment(
|
||||||
attachment: [
|
{
|
||||||
{
|
signature: {
|
||||||
key: `${config.getAppId()}/attachments/${attachmentId}`,
|
type: FieldType.SIGNATURE_SINGLE,
|
||||||
|
name: "signature",
|
||||||
|
constraints: { presence: false },
|
||||||
},
|
},
|
||||||
],
|
},
|
||||||
tableId: table._id,
|
"signature",
|
||||||
})
|
`${uuid.v4()}.png`
|
||||||
await config.withEnv({ SELF_HOSTED: "true" }, async () => {
|
)
|
||||||
return context.doInAppContext(config.getAppId(), async () => {
|
|
||||||
const enriched = await outputProcessing(table, [row])
|
|
||||||
expect((enriched as Row[])[0].attachment[0].url.split("?")[0]).toBe(
|
|
||||||
`/files/signed/prod-budi-app-assets/${config.getProdAppId()}/attachments/${attachmentId}`
|
|
||||||
)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
@ -21,9 +21,6 @@ import _ from "lodash"
|
||||||
import tk from "timekeeper"
|
import tk from "timekeeper"
|
||||||
import { encodeJSBinding } from "@budibase/string-templates"
|
import { encodeJSBinding } from "@budibase/string-templates"
|
||||||
|
|
||||||
const serverTime = new Date("2024-05-06T00:00:00.000Z")
|
|
||||||
tk.freeze(serverTime)
|
|
||||||
|
|
||||||
describe.each([
|
describe.each([
|
||||||
["lucene", undefined],
|
["lucene", undefined],
|
||||||
["sqs", undefined],
|
["sqs", undefined],
|
||||||
|
@ -251,8 +248,14 @@ describe.each([
|
||||||
describe("bindings", () => {
|
describe("bindings", () => {
|
||||||
let globalUsers: any = []
|
let globalUsers: any = []
|
||||||
|
|
||||||
const future = new Date(serverTime.getTime())
|
const serverTime = new Date()
|
||||||
future.setDate(future.getDate() + 30)
|
|
||||||
|
// In MariaDB and MySQL we only store dates to second precision, so we need
|
||||||
|
// to remove milliseconds from the server time to ensure searches work as
|
||||||
|
// expected.
|
||||||
|
serverTime.setMilliseconds(0)
|
||||||
|
|
||||||
|
const future = new Date(serverTime.getTime() + 1000 * 60 * 60 * 24 * 30)
|
||||||
|
|
||||||
const rows = (currentUser: User) => {
|
const rows = (currentUser: User) => {
|
||||||
return [
|
return [
|
||||||
|
@ -358,20 +361,22 @@ describe.each([
|
||||||
})
|
})
|
||||||
|
|
||||||
it("should parse the date binding and return all rows after the resolved value", async () => {
|
it("should parse the date binding and return all rows after the resolved value", async () => {
|
||||||
await expectQuery({
|
await tk.withFreeze(serverTime, async () => {
|
||||||
range: {
|
await expectQuery({
|
||||||
appointment: {
|
range: {
|
||||||
low: "{{ [now] }}",
|
appointment: {
|
||||||
high: "9999-00-00T00:00:00.000Z",
|
low: "{{ [now] }}",
|
||||||
|
high: "9999-00-00T00:00:00.000Z",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
}).toContainExactly([
|
||||||
}).toContainExactly([
|
{
|
||||||
{
|
name: config.getUser().firstName,
|
||||||
name: config.getUser().firstName,
|
appointment: future.toISOString(),
|
||||||
appointment: future.toISOString(),
|
},
|
||||||
},
|
{ name: "serverDate", appointment: serverTime.toISOString() },
|
||||||
{ name: "serverDate", appointment: serverTime.toISOString() },
|
])
|
||||||
])
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
it("should parse the date binding and return all rows before the resolved value", async () => {
|
it("should parse the date binding and return all rows before the resolved value", async () => {
|
||||||
|
@ -407,8 +412,7 @@ describe.each([
|
||||||
})
|
})
|
||||||
|
|
||||||
it("should parse the encoded js binding. Return rows with appointments 2 weeks in the past", async () => {
|
it("should parse the encoded js binding. Return rows with appointments 2 weeks in the past", async () => {
|
||||||
const jsBinding =
|
const jsBinding = `const currentTime = new Date(${Date.now()})\ncurrentTime.setDate(currentTime.getDate()-14);\nreturn currentTime.toISOString();`
|
||||||
"const currentTime = new Date()\ncurrentTime.setDate(currentTime.getDate()-14);\nreturn currentTime.toISOString();"
|
|
||||||
const encodedBinding = encodeJSBinding(jsBinding)
|
const encodedBinding = encodeJSBinding(jsBinding)
|
||||||
|
|
||||||
await expectQuery({
|
await expectQuery({
|
||||||
|
|
|
@ -113,7 +113,8 @@ export async function sendAutomationAttachmentsToStorage(
|
||||||
const schema = table.schema[prop]
|
const schema = table.schema[prop]
|
||||||
if (
|
if (
|
||||||
schema?.type === FieldType.ATTACHMENTS ||
|
schema?.type === FieldType.ATTACHMENTS ||
|
||||||
schema?.type === FieldType.ATTACHMENT_SINGLE
|
schema?.type === FieldType.ATTACHMENT_SINGLE ||
|
||||||
|
schema?.type === FieldType.SIGNATURE_SINGLE
|
||||||
) {
|
) {
|
||||||
attachmentRows[prop] = value
|
attachmentRows[prop] = value
|
||||||
}
|
}
|
||||||
|
|
|
@ -125,6 +125,7 @@ function generateSchema(
|
||||||
break
|
break
|
||||||
case FieldType.ATTACHMENTS:
|
case FieldType.ATTACHMENTS:
|
||||||
case FieldType.ATTACHMENT_SINGLE:
|
case FieldType.ATTACHMENT_SINGLE:
|
||||||
|
case FieldType.SIGNATURE_SINGLE:
|
||||||
case FieldType.AUTO:
|
case FieldType.AUTO:
|
||||||
case FieldType.JSON:
|
case FieldType.JSON:
|
||||||
case FieldType.INTERNAL:
|
case FieldType.INTERNAL:
|
||||||
|
|
|
@ -72,6 +72,7 @@ const isTypeAllowed: Record<FieldType, boolean> = {
|
||||||
[FieldType.JSON]: false,
|
[FieldType.JSON]: false,
|
||||||
[FieldType.INTERNAL]: false,
|
[FieldType.INTERNAL]: false,
|
||||||
[FieldType.BIGINT]: false,
|
[FieldType.BIGINT]: false,
|
||||||
|
[FieldType.SIGNATURE_SINGLE]: false,
|
||||||
}
|
}
|
||||||
|
|
||||||
const ALLOWED_TYPES = Object.entries(isTypeAllowed)
|
const ALLOWED_TYPES = Object.entries(isTypeAllowed)
|
||||||
|
|
|
@ -381,6 +381,7 @@ function copyExistingPropsOver(
|
||||||
case FieldType.ARRAY:
|
case FieldType.ARRAY:
|
||||||
case FieldType.ATTACHMENTS:
|
case FieldType.ATTACHMENTS:
|
||||||
case FieldType.ATTACHMENT_SINGLE:
|
case FieldType.ATTACHMENT_SINGLE:
|
||||||
|
case FieldType.SIGNATURE_SINGLE:
|
||||||
case FieldType.JSON:
|
case FieldType.JSON:
|
||||||
case FieldType.BB_REFERENCE:
|
case FieldType.BB_REFERENCE:
|
||||||
case FieldType.BB_REFERENCE_SINGLE:
|
case FieldType.BB_REFERENCE_SINGLE:
|
||||||
|
|
|
@ -68,7 +68,8 @@ export async function updateAttachmentColumns(prodAppId: string, db: Database) {
|
||||||
rewriteAttachmentUrl(prodAppId, attachment)
|
rewriteAttachmentUrl(prodAppId, attachment)
|
||||||
)
|
)
|
||||||
} else if (
|
} else if (
|
||||||
columnType === FieldType.ATTACHMENT_SINGLE &&
|
(columnType === FieldType.ATTACHMENT_SINGLE ||
|
||||||
|
columnType === FieldType.SIGNATURE_SINGLE) &&
|
||||||
row[column]
|
row[column]
|
||||||
) {
|
) {
|
||||||
row[column] = rewriteAttachmentUrl(prodAppId, row[column])
|
row[column] = rewriteAttachmentUrl(prodAppId, row[column])
|
||||||
|
|
|
@ -32,7 +32,8 @@ export async function getRowsWithAttachments(appId: string, table: Table) {
|
||||||
for (let [key, column] of Object.entries(table.schema)) {
|
for (let [key, column] of Object.entries(table.schema)) {
|
||||||
if (
|
if (
|
||||||
column.type === FieldType.ATTACHMENTS ||
|
column.type === FieldType.ATTACHMENTS ||
|
||||||
column.type === FieldType.ATTACHMENT_SINGLE
|
column.type === FieldType.ATTACHMENT_SINGLE ||
|
||||||
|
column.type === FieldType.SIGNATURE_SINGLE
|
||||||
) {
|
) {
|
||||||
attachmentCols.push(key)
|
attachmentCols.push(key)
|
||||||
}
|
}
|
||||||
|
|
|
@ -42,6 +42,7 @@ const FieldTypeMap: Record<FieldType, SQLiteType> = {
|
||||||
[FieldType.BARCODEQR]: SQLiteType.BLOB,
|
[FieldType.BARCODEQR]: SQLiteType.BLOB,
|
||||||
[FieldType.ATTACHMENTS]: SQLiteType.BLOB,
|
[FieldType.ATTACHMENTS]: SQLiteType.BLOB,
|
||||||
[FieldType.ATTACHMENT_SINGLE]: SQLiteType.BLOB,
|
[FieldType.ATTACHMENT_SINGLE]: SQLiteType.BLOB,
|
||||||
|
[FieldType.SIGNATURE_SINGLE]: SQLiteType.BLOB,
|
||||||
[FieldType.ARRAY]: SQLiteType.BLOB,
|
[FieldType.ARRAY]: SQLiteType.BLOB,
|
||||||
[FieldType.LINK]: SQLiteType.BLOB,
|
[FieldType.LINK]: SQLiteType.BLOB,
|
||||||
[FieldType.BIGINT]: SQLiteType.TEXT,
|
[FieldType.BIGINT]: SQLiteType.TEXT,
|
||||||
|
|
|
@ -23,13 +23,35 @@ describe("should be able to re-write attachment URLs", () => {
|
||||||
await config.init()
|
await config.init()
|
||||||
})
|
})
|
||||||
|
|
||||||
it("should update URLs on a number of rows over the limit", async () => {
|
const coreBehaviour = async (tblSchema: any, row: any) => {
|
||||||
const table = await config.api.table.save({
|
const table = await config.api.table.save({
|
||||||
name: "photos",
|
name: "photos",
|
||||||
type: "table",
|
type: "table",
|
||||||
sourceId: INTERNAL_TABLE_SOURCE_ID,
|
sourceId: INTERNAL_TABLE_SOURCE_ID,
|
||||||
sourceType: TableSourceType.INTERNAL,
|
sourceType: TableSourceType.INTERNAL,
|
||||||
schema: {
|
schema: tblSchema,
|
||||||
|
})
|
||||||
|
|
||||||
|
for (let i = 0; i < FIND_LIMIT * 4; i++) {
|
||||||
|
await config.api.row.save(table._id!, {
|
||||||
|
...row,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const db = dbCore.getDB(config.getAppId())
|
||||||
|
await sdk.backups.updateAttachmentColumns(db.name, db)
|
||||||
|
|
||||||
|
return {
|
||||||
|
db,
|
||||||
|
rows: (await sdk.rows.getAllInternalRows(db.name)).filter(
|
||||||
|
row => row.tableId === table._id
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
it("Attachment field, should update URLs on a number of rows over the limit", async () => {
|
||||||
|
const { rows, db } = await coreBehaviour(
|
||||||
|
{
|
||||||
photo: {
|
photo: {
|
||||||
type: FieldType.ATTACHMENT_SINGLE,
|
type: FieldType.ATTACHMENT_SINGLE,
|
||||||
name: "photo",
|
name: "photo",
|
||||||
|
@ -43,21 +65,11 @@ describe("should be able to re-write attachment URLs", () => {
|
||||||
name: "otherCol",
|
name: "otherCol",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
})
|
{
|
||||||
|
|
||||||
for (let i = 0; i < FIND_LIMIT * 4; i++) {
|
|
||||||
await config.api.row.save(table._id!, {
|
|
||||||
photo: { ...attachment },
|
photo: { ...attachment },
|
||||||
gallery: [{ ...attachment }, { ...attachment }],
|
gallery: [{ ...attachment }, { ...attachment }],
|
||||||
otherCol: "string",
|
otherCol: "string",
|
||||||
})
|
}
|
||||||
}
|
|
||||||
|
|
||||||
const db = dbCore.getDB(config.getAppId())
|
|
||||||
await sdk.backups.updateAttachmentColumns(db.name, db)
|
|
||||||
|
|
||||||
const rows = (await sdk.rows.getAllInternalRows(db.name)).filter(
|
|
||||||
row => row.tableId === table._id
|
|
||||||
)
|
)
|
||||||
for (const row of rows) {
|
for (const row of rows) {
|
||||||
expect(row.otherCol).toBe("string")
|
expect(row.otherCol).toBe("string")
|
||||||
|
@ -69,4 +81,27 @@ describe("should be able to re-write attachment URLs", () => {
|
||||||
expect(row.gallery[1].key).toBe(`${db.name}/attachments/a.png`)
|
expect(row.gallery[1].key).toBe(`${db.name}/attachments/a.png`)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
it("Signature field, should update URLs on a number of rows over the limit", async () => {
|
||||||
|
const { rows, db } = await coreBehaviour(
|
||||||
|
{
|
||||||
|
signature: {
|
||||||
|
type: FieldType.SIGNATURE_SINGLE,
|
||||||
|
name: "signature",
|
||||||
|
},
|
||||||
|
otherCol: {
|
||||||
|
type: FieldType.STRING,
|
||||||
|
name: "otherCol",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
signature: { ...attachment },
|
||||||
|
otherCol: "string",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
for (const row of rows) {
|
||||||
|
expect(row.otherCol).toBe("string")
|
||||||
|
expect(row.signature.url).toBe("")
|
||||||
|
expect(row.signature.key).toBe(`${db.name}/attachments/a.png`)
|
||||||
|
}
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
|
@ -31,7 +31,8 @@ export class AttachmentCleanup {
|
||||||
): string[] {
|
): string[] {
|
||||||
if (
|
if (
|
||||||
type !== FieldType.ATTACHMENTS &&
|
type !== FieldType.ATTACHMENTS &&
|
||||||
type !== FieldType.ATTACHMENT_SINGLE
|
type !== FieldType.ATTACHMENT_SINGLE &&
|
||||||
|
type !== FieldType.SIGNATURE_SINGLE
|
||||||
) {
|
) {
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
|
@ -62,7 +63,8 @@ export class AttachmentCleanup {
|
||||||
for (let [key, schema] of Object.entries(tableSchema)) {
|
for (let [key, schema] of Object.entries(tableSchema)) {
|
||||||
if (
|
if (
|
||||||
schema.type !== FieldType.ATTACHMENTS &&
|
schema.type !== FieldType.ATTACHMENTS &&
|
||||||
schema.type !== FieldType.ATTACHMENT_SINGLE
|
schema.type !== FieldType.ATTACHMENT_SINGLE &&
|
||||||
|
schema.type !== FieldType.SIGNATURE_SINGLE
|
||||||
) {
|
) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
@ -100,10 +102,12 @@ export class AttachmentCleanup {
|
||||||
for (let [key, schema] of Object.entries(table.schema)) {
|
for (let [key, schema] of Object.entries(table.schema)) {
|
||||||
if (
|
if (
|
||||||
schema.type !== FieldType.ATTACHMENTS &&
|
schema.type !== FieldType.ATTACHMENTS &&
|
||||||
schema.type !== FieldType.ATTACHMENT_SINGLE
|
schema.type !== FieldType.ATTACHMENT_SINGLE &&
|
||||||
|
schema.type !== FieldType.SIGNATURE_SINGLE
|
||||||
) {
|
) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
rows.forEach(row => {
|
rows.forEach(row => {
|
||||||
files = files.concat(
|
files = files.concat(
|
||||||
AttachmentCleanup.extractAttachmentKeys(schema.type, row[key])
|
AttachmentCleanup.extractAttachmentKeys(schema.type, row[key])
|
||||||
|
@ -120,7 +124,8 @@ export class AttachmentCleanup {
|
||||||
for (let [key, schema] of Object.entries(table.schema)) {
|
for (let [key, schema] of Object.entries(table.schema)) {
|
||||||
if (
|
if (
|
||||||
schema.type !== FieldType.ATTACHMENTS &&
|
schema.type !== FieldType.ATTACHMENTS &&
|
||||||
schema.type !== FieldType.ATTACHMENT_SINGLE
|
schema.type !== FieldType.ATTACHMENT_SINGLE &&
|
||||||
|
schema.type !== FieldType.SIGNATURE_SINGLE
|
||||||
) {
|
) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
|
@ -158,7 +158,10 @@ export async function inputProcessing(
|
||||||
delete attachment.url
|
delete attachment.url
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
} else if (field.type === FieldType.ATTACHMENT_SINGLE) {
|
} else if (
|
||||||
|
field.type === FieldType.ATTACHMENT_SINGLE ||
|
||||||
|
field.type === FieldType.SIGNATURE_SINGLE
|
||||||
|
) {
|
||||||
const attachment = clonedRow[key]
|
const attachment = clonedRow[key]
|
||||||
if (attachment?.url) {
|
if (attachment?.url) {
|
||||||
delete clonedRow[key].url
|
delete clonedRow[key].url
|
||||||
|
@ -230,7 +233,8 @@ export async function outputProcessing<T extends Row[] | Row>(
|
||||||
for (let [property, column] of Object.entries(table.schema)) {
|
for (let [property, column] of Object.entries(table.schema)) {
|
||||||
if (
|
if (
|
||||||
column.type === FieldType.ATTACHMENTS ||
|
column.type === FieldType.ATTACHMENTS ||
|
||||||
column.type === FieldType.ATTACHMENT_SINGLE
|
column.type === FieldType.ATTACHMENT_SINGLE ||
|
||||||
|
column.type === FieldType.SIGNATURE_SINGLE
|
||||||
) {
|
) {
|
||||||
for (let row of enriched) {
|
for (let row of enriched) {
|
||||||
if (row[property] == null) {
|
if (row[property] == null) {
|
||||||
|
|
|
@ -1,22 +1,30 @@
|
||||||
import { AttachmentCleanup } from "../attachments"
|
import { AttachmentCleanup } from "../attachments"
|
||||||
import { FieldType, Table, Row, TableSourceType } from "@budibase/types"
|
import { FieldType, Table, Row, TableSourceType } from "@budibase/types"
|
||||||
import { DEFAULT_BB_DATASOURCE_ID } from "../../../constants"
|
import { DEFAULT_BB_DATASOURCE_ID } from "../../../constants"
|
||||||
import { objectStore } from "@budibase/backend-core"
|
import { objectStore, db, context } from "@budibase/backend-core"
|
||||||
|
import * as uuid from "uuid"
|
||||||
|
|
||||||
const BUCKET = "prod-budi-app-assets"
|
const BUCKET = "prod-budi-app-assets"
|
||||||
const FILE_NAME = "file/thing.jpg"
|
const FILE_NAME = "file/thing.jpg"
|
||||||
|
const DEV_APPID = "abc_dev_123"
|
||||||
|
const PROD_APPID = "abc_123"
|
||||||
|
|
||||||
jest.mock("@budibase/backend-core", () => {
|
jest.mock("@budibase/backend-core", () => {
|
||||||
const actual = jest.requireActual("@budibase/backend-core")
|
const actual = jest.requireActual("@budibase/backend-core")
|
||||||
return {
|
return {
|
||||||
...actual,
|
...actual,
|
||||||
|
context: {
|
||||||
|
...actual.context,
|
||||||
|
getAppId: jest.fn(),
|
||||||
|
},
|
||||||
objectStore: {
|
objectStore: {
|
||||||
deleteFiles: jest.fn(),
|
deleteFiles: jest.fn(),
|
||||||
ObjectStoreBuckets: actual.objectStore.ObjectStoreBuckets,
|
ObjectStoreBuckets: actual.objectStore.ObjectStoreBuckets,
|
||||||
},
|
},
|
||||||
db: {
|
db: {
|
||||||
isProdAppID: () => jest.fn(() => false),
|
isProdAppID: jest.fn(),
|
||||||
dbExists: () => jest.fn(() => false),
|
getProdAppID: jest.fn(),
|
||||||
|
dbExists: jest.fn(),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
@ -27,12 +35,18 @@ const mockedDeleteFiles = objectStore.deleteFiles as jest.MockedFunction<
|
||||||
|
|
||||||
const rowGenerators: [
|
const rowGenerators: [
|
||||||
string,
|
string,
|
||||||
FieldType.ATTACHMENT_SINGLE | FieldType.ATTACHMENTS,
|
(
|
||||||
|
| FieldType.ATTACHMENT_SINGLE
|
||||||
|
| FieldType.ATTACHMENTS
|
||||||
|
| FieldType.SIGNATURE_SINGLE
|
||||||
|
),
|
||||||
|
string,
|
||||||
(fileKey?: string) => Row
|
(fileKey?: string) => Row
|
||||||
][] = [
|
][] = [
|
||||||
[
|
[
|
||||||
"row with a attachment list column",
|
"row with a attachment list column",
|
||||||
FieldType.ATTACHMENTS,
|
FieldType.ATTACHMENTS,
|
||||||
|
"attach",
|
||||||
function rowWithAttachments(fileKey: string = FILE_NAME): Row {
|
function rowWithAttachments(fileKey: string = FILE_NAME): Row {
|
||||||
return {
|
return {
|
||||||
attach: [
|
attach: [
|
||||||
|
@ -48,6 +62,7 @@ const rowGenerators: [
|
||||||
[
|
[
|
||||||
"row with a single attachment column",
|
"row with a single attachment column",
|
||||||
FieldType.ATTACHMENT_SINGLE,
|
FieldType.ATTACHMENT_SINGLE,
|
||||||
|
"attach",
|
||||||
function rowWithAttachments(fileKey: string = FILE_NAME): Row {
|
function rowWithAttachments(fileKey: string = FILE_NAME): Row {
|
||||||
return {
|
return {
|
||||||
attach: {
|
attach: {
|
||||||
|
@ -58,11 +73,25 @@ const rowGenerators: [
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
[
|
||||||
|
"row with a single signature column",
|
||||||
|
FieldType.SIGNATURE_SINGLE,
|
||||||
|
"signature",
|
||||||
|
function rowWithSignature(): Row {
|
||||||
|
return {
|
||||||
|
signature: {
|
||||||
|
size: 1,
|
||||||
|
extension: "png",
|
||||||
|
key: `${uuid.v4()}.png`,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
],
|
||||||
]
|
]
|
||||||
|
|
||||||
describe.each(rowGenerators)(
|
describe.each(rowGenerators)(
|
||||||
"attachment cleanup",
|
"attachment cleanup",
|
||||||
(_, attachmentFieldType, rowGenerator) => {
|
(_, attachmentFieldType, colKey, rowGenerator) => {
|
||||||
function tableGenerator(): Table {
|
function tableGenerator(): Table {
|
||||||
return {
|
return {
|
||||||
name: "table",
|
name: "table",
|
||||||
|
@ -75,97 +104,158 @@ describe.each(rowGenerators)(
|
||||||
type: attachmentFieldType,
|
type: attachmentFieldType,
|
||||||
constraints: {},
|
constraints: {},
|
||||||
},
|
},
|
||||||
|
signature: {
|
||||||
|
name: "signature",
|
||||||
|
type: FieldType.SIGNATURE_SINGLE,
|
||||||
|
constraints: {},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const getRowKeys = (row: any, col: string) => {
|
||||||
|
return Array.isArray(row[col])
|
||||||
|
? row[col].map((entry: any) => entry.key)
|
||||||
|
: [row[col]?.key]
|
||||||
|
}
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
mockedDeleteFiles.mockClear()
|
mockedDeleteFiles.mockClear()
|
||||||
|
jest.resetAllMocks()
|
||||||
|
|
||||||
|
jest.spyOn(context, "getAppId").mockReturnValue(DEV_APPID)
|
||||||
|
jest.spyOn(db, "isProdAppID").mockReturnValue(false)
|
||||||
|
jest.spyOn(db, "getProdAppID").mockReturnValue(PROD_APPID)
|
||||||
|
jest.spyOn(db, "dbExists").mockReturnValue(Promise.resolve(false))
|
||||||
})
|
})
|
||||||
|
|
||||||
it("should be able to cleanup a table update", async () => {
|
// Ignore calls to prune attachments when app is in production.
|
||||||
|
it(`${attachmentFieldType} - should not attempt to delete attachments/signatures if a published app exists`, async () => {
|
||||||
|
jest.spyOn(db, "dbExists").mockReturnValue(Promise.resolve(true))
|
||||||
const originalTable = tableGenerator()
|
const originalTable = tableGenerator()
|
||||||
delete originalTable.schema["attach"]
|
delete originalTable.schema[colKey]
|
||||||
await AttachmentCleanup.tableUpdate(originalTable, [rowGenerator()], {
|
await AttachmentCleanup.tableUpdate(originalTable, [rowGenerator()], {
|
||||||
oldTable: tableGenerator(),
|
oldTable: tableGenerator(),
|
||||||
})
|
})
|
||||||
expect(mockedDeleteFiles).toHaveBeenCalledWith(BUCKET, [FILE_NAME])
|
|
||||||
})
|
|
||||||
|
|
||||||
it("should be able to cleanup a table deletion", async () => {
|
|
||||||
await AttachmentCleanup.tableDelete(tableGenerator(), [rowGenerator()])
|
|
||||||
expect(mockedDeleteFiles).toHaveBeenCalledWith(BUCKET, [FILE_NAME])
|
|
||||||
})
|
|
||||||
|
|
||||||
it("should handle table column renaming", async () => {
|
|
||||||
const updatedTable = tableGenerator()
|
|
||||||
updatedTable.schema.attach2 = updatedTable.schema.attach
|
|
||||||
delete updatedTable.schema.attach
|
|
||||||
await AttachmentCleanup.tableUpdate(updatedTable, [rowGenerator()], {
|
|
||||||
oldTable: tableGenerator(),
|
|
||||||
rename: { old: "attach", updated: "attach2" },
|
|
||||||
})
|
|
||||||
expect(mockedDeleteFiles).not.toHaveBeenCalled()
|
expect(mockedDeleteFiles).not.toHaveBeenCalled()
|
||||||
})
|
})
|
||||||
|
|
||||||
it("shouldn't cleanup if no table changes", async () => {
|
it(`${attachmentFieldType} - should be able to cleanup a table update`, async () => {
|
||||||
|
const originalTable = tableGenerator()
|
||||||
|
delete originalTable.schema[colKey]
|
||||||
|
const targetRow = rowGenerator()
|
||||||
|
|
||||||
|
await AttachmentCleanup.tableUpdate(originalTable, [targetRow], {
|
||||||
|
oldTable: tableGenerator(),
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(mockedDeleteFiles).toHaveBeenCalledWith(
|
||||||
|
BUCKET,
|
||||||
|
getRowKeys(targetRow, colKey)
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it(`${attachmentFieldType} - should be able to cleanup a table deletion`, async () => {
|
||||||
|
const targetRow = rowGenerator()
|
||||||
|
await AttachmentCleanup.tableDelete(tableGenerator(), [targetRow])
|
||||||
|
expect(mockedDeleteFiles).toHaveBeenCalledWith(
|
||||||
|
BUCKET,
|
||||||
|
getRowKeys(targetRow, colKey)
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it(`${attachmentFieldType} - should handle table column renaming`, async () => {
|
||||||
|
const updatedTable = tableGenerator()
|
||||||
|
updatedTable.schema.col2 = updatedTable.schema[colKey]
|
||||||
|
delete updatedTable.schema.attach
|
||||||
|
await AttachmentCleanup.tableUpdate(updatedTable, [rowGenerator()], {
|
||||||
|
oldTable: tableGenerator(),
|
||||||
|
rename: { old: colKey, updated: "col2" },
|
||||||
|
})
|
||||||
|
expect(mockedDeleteFiles).not.toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
|
||||||
|
it(`${attachmentFieldType} - shouldn't cleanup if no table changes`, async () => {
|
||||||
await AttachmentCleanup.tableUpdate(tableGenerator(), [rowGenerator()], {
|
await AttachmentCleanup.tableUpdate(tableGenerator(), [rowGenerator()], {
|
||||||
oldTable: tableGenerator(),
|
oldTable: tableGenerator(),
|
||||||
})
|
})
|
||||||
expect(mockedDeleteFiles).not.toHaveBeenCalled()
|
expect(mockedDeleteFiles).not.toHaveBeenCalled()
|
||||||
})
|
})
|
||||||
|
|
||||||
it("should handle row updates", async () => {
|
it(`${attachmentFieldType} - should handle row updates`, async () => {
|
||||||
const updatedRow = rowGenerator()
|
const updatedRow = rowGenerator()
|
||||||
delete updatedRow.attach
|
delete updatedRow[colKey]
|
||||||
|
|
||||||
|
const targetRow = rowGenerator()
|
||||||
await AttachmentCleanup.rowUpdate(tableGenerator(), {
|
await AttachmentCleanup.rowUpdate(tableGenerator(), {
|
||||||
row: updatedRow,
|
row: updatedRow,
|
||||||
oldRow: rowGenerator(),
|
oldRow: targetRow,
|
||||||
})
|
})
|
||||||
expect(mockedDeleteFiles).toHaveBeenCalledWith(BUCKET, [FILE_NAME])
|
|
||||||
|
expect(mockedDeleteFiles).toHaveBeenCalledWith(
|
||||||
|
BUCKET,
|
||||||
|
getRowKeys(targetRow, colKey)
|
||||||
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
it("should handle row deletion", async () => {
|
it(`${attachmentFieldType} - should handle row deletion`, async () => {
|
||||||
await AttachmentCleanup.rowDelete(tableGenerator(), [rowGenerator()])
|
const targetRow = rowGenerator()
|
||||||
expect(mockedDeleteFiles).toHaveBeenCalledWith(BUCKET, [FILE_NAME])
|
await AttachmentCleanup.rowDelete(tableGenerator(), [targetRow])
|
||||||
|
expect(mockedDeleteFiles).toHaveBeenCalledWith(
|
||||||
|
BUCKET,
|
||||||
|
getRowKeys(targetRow, colKey)
|
||||||
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
it("should handle row deletion and not throw when attachments are undefined", async () => {
|
it(`${attachmentFieldType} - should handle row deletion, prune signature`, async () => {
|
||||||
|
const targetRow = rowGenerator()
|
||||||
|
await AttachmentCleanup.rowDelete(tableGenerator(), [targetRow])
|
||||||
|
expect(mockedDeleteFiles).toHaveBeenCalledWith(
|
||||||
|
BUCKET,
|
||||||
|
getRowKeys(targetRow, colKey)
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it(`${attachmentFieldType} - should handle row deletion and not throw when attachments are undefined`, async () => {
|
||||||
await AttachmentCleanup.rowDelete(tableGenerator(), [
|
await AttachmentCleanup.rowDelete(tableGenerator(), [
|
||||||
{
|
{
|
||||||
multipleAttachments: undefined,
|
[colKey]: undefined,
|
||||||
},
|
},
|
||||||
])
|
])
|
||||||
})
|
})
|
||||||
|
|
||||||
it("shouldn't cleanup attachments if row not updated", async () => {
|
it(`${attachmentFieldType} - shouldn't cleanup attachments if row not updated`, async () => {
|
||||||
|
const targetRow = rowGenerator()
|
||||||
await AttachmentCleanup.rowUpdate(tableGenerator(), {
|
await AttachmentCleanup.rowUpdate(tableGenerator(), {
|
||||||
row: rowGenerator(),
|
row: targetRow,
|
||||||
oldRow: rowGenerator(),
|
oldRow: targetRow,
|
||||||
})
|
})
|
||||||
expect(mockedDeleteFiles).not.toHaveBeenCalled()
|
expect(mockedDeleteFiles).not.toHaveBeenCalled()
|
||||||
})
|
})
|
||||||
|
|
||||||
it("should be able to cleanup a column and not throw when attachments are undefined", async () => {
|
it(`${attachmentFieldType} - should be able to cleanup a column and not throw when attachments are undefined`, async () => {
|
||||||
const originalTable = tableGenerator()
|
const originalTable = tableGenerator()
|
||||||
delete originalTable.schema["attach"]
|
delete originalTable.schema[colKey]
|
||||||
|
const row1 = rowGenerator("file 1")
|
||||||
|
const row2 = rowGenerator("file 2")
|
||||||
await AttachmentCleanup.tableUpdate(
|
await AttachmentCleanup.tableUpdate(
|
||||||
originalTable,
|
originalTable,
|
||||||
[rowGenerator("file 1"), { attach: undefined }, rowGenerator("file 2")],
|
[row1, { [colKey]: undefined }, row2],
|
||||||
{
|
{
|
||||||
oldTable: tableGenerator(),
|
oldTable: tableGenerator(),
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
const expectedKeys = [row1, row2].reduce((acc: string[], row) => {
|
||||||
|
acc = [...acc, ...getRowKeys(row, colKey)]
|
||||||
|
return acc
|
||||||
|
}, [])
|
||||||
expect(mockedDeleteFiles).toHaveBeenCalledTimes(1)
|
expect(mockedDeleteFiles).toHaveBeenCalledTimes(1)
|
||||||
expect(mockedDeleteFiles).toHaveBeenCalledWith(BUCKET, [
|
expect(mockedDeleteFiles).toHaveBeenCalledWith(BUCKET, expectedKeys)
|
||||||
"file 1",
|
|
||||||
"file 2",
|
|
||||||
])
|
|
||||||
})
|
})
|
||||||
|
|
||||||
it("should be able to cleanup a column and not throw when ALL attachments are undefined", async () => {
|
it(`${attachmentFieldType} - should be able to cleanup a column and not throw when ALL attachments are undefined`, async () => {
|
||||||
const originalTable = tableGenerator()
|
const originalTable = tableGenerator()
|
||||||
delete originalTable.schema["attach"]
|
delete originalTable.schema[colKey]
|
||||||
await AttachmentCleanup.tableUpdate(
|
await AttachmentCleanup.tableUpdate(
|
||||||
originalTable,
|
originalTable,
|
||||||
[{}, { attach: undefined }],
|
[{}, { attach: undefined }],
|
||||||
|
|
|
@ -151,7 +151,8 @@ export function parse(rows: Rows, schema: TableSchema): Rows {
|
||||||
parsedRow[columnName] = parsedValue?._id
|
parsedRow[columnName] = parsedValue?._id
|
||||||
} else if (
|
} else if (
|
||||||
(columnType === FieldType.ATTACHMENTS ||
|
(columnType === FieldType.ATTACHMENTS ||
|
||||||
columnType === FieldType.ATTACHMENT_SINGLE) &&
|
columnType === FieldType.ATTACHMENT_SINGLE ||
|
||||||
|
columnType === FieldType.SIGNATURE_SINGLE) &&
|
||||||
typeof columnData === "string"
|
typeof columnData === "string"
|
||||||
) {
|
) {
|
||||||
parsedRow[columnName] = parseCsvExport(columnData)
|
parsedRow[columnName] = parseCsvExport(columnData)
|
||||||
|
|
|
@ -15,6 +15,7 @@ const allowDisplayColumnByType: Record<FieldType, boolean> = {
|
||||||
[FieldType.ARRAY]: false,
|
[FieldType.ARRAY]: false,
|
||||||
[FieldType.ATTACHMENTS]: false,
|
[FieldType.ATTACHMENTS]: false,
|
||||||
[FieldType.ATTACHMENT_SINGLE]: false,
|
[FieldType.ATTACHMENT_SINGLE]: false,
|
||||||
|
[FieldType.SIGNATURE_SINGLE]: false,
|
||||||
[FieldType.LINK]: false,
|
[FieldType.LINK]: false,
|
||||||
[FieldType.JSON]: false,
|
[FieldType.JSON]: false,
|
||||||
[FieldType.BB_REFERENCE]: false,
|
[FieldType.BB_REFERENCE]: false,
|
||||||
|
@ -33,10 +34,10 @@ const allowSortColumnByType: Record<FieldType, boolean> = {
|
||||||
[FieldType.BIGINT]: true,
|
[FieldType.BIGINT]: true,
|
||||||
[FieldType.BOOLEAN]: true,
|
[FieldType.BOOLEAN]: true,
|
||||||
[FieldType.JSON]: true,
|
[FieldType.JSON]: true,
|
||||||
|
|
||||||
[FieldType.FORMULA]: false,
|
[FieldType.FORMULA]: false,
|
||||||
[FieldType.ATTACHMENTS]: false,
|
[FieldType.ATTACHMENTS]: false,
|
||||||
[FieldType.ATTACHMENT_SINGLE]: false,
|
[FieldType.ATTACHMENT_SINGLE]: false,
|
||||||
|
[FieldType.SIGNATURE_SINGLE]: false,
|
||||||
[FieldType.ARRAY]: false,
|
[FieldType.ARRAY]: false,
|
||||||
[FieldType.LINK]: false,
|
[FieldType.LINK]: false,
|
||||||
[FieldType.BB_REFERENCE]: false,
|
[FieldType.BB_REFERENCE]: false,
|
||||||
|
|
|
@ -93,6 +93,11 @@ export enum FieldType {
|
||||||
* type is found. The column will contain the contents of any barcode scanned.
|
* type is found. The column will contain the contents of any barcode scanned.
|
||||||
*/
|
*/
|
||||||
BARCODEQR = "barcodeqr",
|
BARCODEQR = "barcodeqr",
|
||||||
|
/**
|
||||||
|
* a JSON type, called Signature within Budibase. This type functions much the same as ATTACHMENTS but restricted
|
||||||
|
* only to signatures.
|
||||||
|
*/
|
||||||
|
SIGNATURE_SINGLE = "signature_single",
|
||||||
/**
|
/**
|
||||||
* a string type, this allows representing very large integers, but they are held/managed within Budibase as
|
* a string type, this allows representing very large integers, but they are held/managed within Budibase as
|
||||||
* strings. When stored in external databases Budibase will attempt to use a real big integer type and depend
|
* strings. When stored in external databases Budibase will attempt to use a real big integer type and depend
|
||||||
|
|
Loading…
Reference in New Issue