Merge pull request #8153 from Budibase/feature/qr-barcode-reader

Feature/qr barcode reader
This commit is contained in:
deanhannigan 2022-10-13 16:43:54 +01:00 committed by GitHub
commit aa1293f15a
19 changed files with 387 additions and 7 deletions

View File

@ -138,6 +138,7 @@ const fieldTypeToComponentMap = {
attachment: "attachmentfield",
link: "relationshipfield",
json: "jsonfield",
barcodeqr: "codescanner",
}
export function makeDatasourceFormComponents(datasource) {

View File

@ -261,6 +261,7 @@
} else {
return [
FIELDS.STRING,
FIELDS.BARCODEQR,
FIELDS.LONGFORM,
FIELDS.OPTIONS,
FIELDS.DATETIME,

View File

@ -124,6 +124,14 @@
label: "Multi-select",
value: FIELDS.ARRAY.type,
},
{
label: "Barcode/QR",
value: FIELDS.BARCODEQR.type,
},
{
label: "Long Form Text",
value: FIELDS.LONGFORM.type,
},
]
</script>

View File

@ -51,6 +51,7 @@ const componentMap = {
"field/link": FormFieldSelect,
"field/array": FormFieldSelect,
"field/json": FormFieldSelect,
"field/barcode/qr": FormFieldSelect,
// Some validation types are the same as others, so not all types are
// explicitly listed here. e.g. options uses string validation
"validation/string": ValidationEditor,

View File

@ -24,18 +24,17 @@
const getOptions = (schema, type) => {
let entries = Object.entries(schema ?? {})
let types = []
if (type === "field/options") {
if (type === "field/options" || type === "field/barcode/qr") {
// allow options to be used on both options and string fields
types = [type, "field/string"]
} else {
types = [type]
}
types = types.map(type => type.split("/")[1])
entries = entries.filter(entry => types.includes(entry[1].type))
types = types.map(type => type.slice(type.indexOf("/") + 1))
entries = entries.filter(entry => types.includes(entry[1].type))
return entries.map(entry => entry[0])
}
</script>

View File

@ -8,6 +8,15 @@ export const FIELDS = {
presence: false,
},
},
BARCODEQR: {
name: "Barcode/QR",
type: "barcodeqr",
constraints: {
type: "string",
length: {},
presence: false,
},
},
LONGFORM: {
name: "Long Form Text",
type: "longform",
@ -148,6 +157,7 @@ export const ALLOWABLE_STRING_OPTIONS = [
FIELDS.STRING,
FIELDS.OPTIONS,
FIELDS.LONGFORM,
FIELDS.BARCODEQR,
]
export const ALLOWABLE_STRING_TYPES = ALLOWABLE_STRING_OPTIONS.map(
opt => opt.type

View File

@ -66,7 +66,8 @@
"relationshipfield",
"datetimefield",
"multifieldselect",
"s3upload"
"s3upload",
"codescanner"
]
},
{

View File

@ -3157,6 +3157,56 @@
}
]
},
"codescanner": {
"name": "Barcode/QR Scanner",
"icon": "Camera",
"styles": [
"size"
],
"illegalChildren": [
"section"
],
"settings": [
{
"type": "field/barcode/qr",
"label": "Field",
"key": "field",
"required": true
},
{
"type": "text",
"label": "Label",
"key": "label"
},
{
"type": "text",
"label": "Button text",
"key": "scanButtonText"
},
{
"type": "text",
"label": "Default value",
"key": "defaultValue"
},
{
"type": "boolean",
"label": "Disabled",
"key": "disabled",
"defaultValue": false
},
{
"type": "boolean",
"label": "Allow manual entry",
"key": "allowManualEntry",
"defaultValue": false
},
{
"type": "validation/string",
"label": "Validation",
"key": "validation"
}
]
},
"embeddedmap": {
"name": "Embedded Map",
"icon": "Location",
@ -4399,7 +4449,9 @@
"formblock": {
"name": "Form Block",
"icon": "Form",
"styles": ["size"],
"styles": [
"size"
],
"block": true,
"info": "Form blocks are only compatible with internal or SQL tables",
"settings": [
@ -4407,7 +4459,11 @@
"type": "select",
"label": "Type",
"key": "actionType",
"options": ["Create", "Update", "View"],
"options": [
"Create",
"Update",
"View"
],
"defaultValue": "Create"
},
{

View File

@ -33,6 +33,7 @@
"apexcharts": "^3.22.1",
"dayjs": "^1.10.5",
"downloadjs": "1.4.7",
"html5-qrcode": "^2.2.1",
"leaflet": "^1.7.1",
"regexparam": "^1.3.0",
"sanitize-html": "^2.7.0",

View File

@ -27,6 +27,15 @@ export default {
file: `./dist/budibase-client.js`,
},
],
onwarn(warning, warn) {
if (
warning.code === "THIS_IS_UNDEFINED" ||
warning.code === "CIRCULAR_DEPENDENCY"
) {
return
}
warn(warning)
},
plugins: [
alias({
entries: [

View File

@ -0,0 +1,234 @@
<script>
import { ModalContent, Modal, Icon, ActionButton } from "@budibase/bbui"
import { Input, Button, StatusLight } from "@budibase/bbui"
import { Html5Qrcode } from "html5-qrcode"
import { createEventDispatcher } from "svelte"
export let value
export let disabled = false
export let allowManualEntry = false
export let scanButtonText = "Scan code"
const dispatch = createEventDispatcher()
let videoEle
let camModal
let manualMode = false
let cameraEnabled
let cameraStarted = false
let html5QrCode
let cameraSetting = { facingMode: "environment" }
let cameraConfig = {
fps: 25,
qrbox: { width: 250, height: 250 },
}
const onScanSuccess = decodedText => {
if (value != decodedText) {
dispatch("change", decodedText)
}
}
const initReader = async () => {
if (html5QrCode) {
html5QrCode.stop()
}
html5QrCode = new Html5Qrcode("reader")
return new Promise(resolve => {
html5QrCode
.start(cameraSetting, cameraConfig, onScanSuccess)
.then(() => {
resolve({ initialised: true })
})
.catch(err => {
console.log("There was a problem scanning the image", err)
resolve({ initialised: false })
})
})
}
const checkCamera = async () => {
return new Promise(resolve => {
Html5Qrcode.getCameras()
.then(devices => {
if (devices && devices.length) {
resolve({ enabled: true })
}
})
.catch(e => {
console.error(e)
resolve({ enabled: false })
})
})
}
const start = async () => {
const status = await initReader()
cameraStarted = status.initialised
}
$: if (cameraEnabled && videoEle && !cameraStarted) {
start()
}
const showReaderModal = async () => {
camModal.show()
const camStatus = await checkCamera()
cameraEnabled = camStatus.enabled
}
const hideReaderModal = async () => {
cameraEnabled = undefined
cameraStarted = false
if (html5QrCode) {
await html5QrCode.stop()
html5QrCode = undefined
}
camModal.hide()
}
</script>
<div class="scanner-video-wrapper">
{#if value && !manualMode}
<div class="scanner-value field-display">
<StatusLight positive />
{value}
</div>
{/if}
{#if allowManualEntry && manualMode}
<div class="manual-input">
<Input
bind:value
on:change={() => {
dispatch("change", value)
}}
/>
</div>
{/if}
{#if value}
<ActionButton
on:click={() => {
dispatch("change", "")
}}
{disabled}
>
Clear
</ActionButton>
{:else}
<ActionButton
icon="Camera"
on:click={() => {
showReaderModal()
}}
{disabled}
>
{scanButtonText}
</ActionButton>
{/if}
</div>
<div class="modal-wrap">
<Modal bind:this={camModal} on:hide={hideReaderModal}>
<ModalContent
title={scanButtonText}
showConfirmButton={false}
showCancelButton={false}
>
<div id="reader" class="container" bind:this={videoEle}>
<div class="camera-placeholder">
<Icon size="XXL" name="Camera" />
{#if cameraEnabled === false}
<div>Your camera is disabled.</div>
{/if}
</div>
</div>
{#if cameraEnabled === true}
<div class="code-wrap">
{#if value}
<div class="scanner-value">
<StatusLight positive />
{value}
</div>
{:else}
<div class="scanner-value">
<StatusLight neutral />
Searching for code...
</div>
{/if}
</div>
{/if}
<div slot="footer">
<div class="footer-buttons">
{#if allowManualEntry && !manualMode}
<Button
group
secondary
newStyles
on:click={() => {
manualMode = true
camModal.hide()
}}
>
Enter manually
</Button>
{/if}
<Button
group
cta
on:click={() => {
camModal.hide()
}}
>
Confirm
</Button>
</div>
</div>
</ModalContent>
</Modal>
</div>
<style>
#reader :global(video) {
border-radius: 4px;
border: 2px solid var(--spectrum-global-color-gray-300);
overflow: hidden;
}
.field-display :global(.spectrum-Tags-item) {
margin: 0px;
}
.footer-buttons {
display: flex;
grid-area: buttonGroup;
gap: var(--spectrum-global-dimension-static-size-200);
}
.scanner-value {
display: flex;
}
.field-display {
padding-top: var(
--spectrum-fieldlabel-side-m-padding-top,
var(--spectrum-global-dimension-size-100)
);
margin-bottom: var(--spacing-m);
}
.camera-placeholder {
display: flex;
align-items: center;
justify-content: center;
border-radius: 4px;
border: 2px solid var(--spectrum-global-color-gray-300);
background-color: var(--spectrum-global-color-gray-200);
flex-direction: column;
gap: var(--spectrum-global-dimension-static-size-200);
}
.container,
.camera-placeholder {
width: 100%;
min-height: 240px;
}
.manual-input {
padding-bottom: var(--spacing-m);
}
</style>

View File

@ -0,0 +1,47 @@
<script>
import Field from "./Field.svelte"
import CodeScanner from "./CodeScanner.svelte"
export let field
export let label
export let type = "barcodeqr"
export let disabled = false
export let validation
export let defaultValue = ""
export let onChange
export let allowManualEntry
export let scanButtonText
let fieldState
let fieldApi
$: scanText = scanButtonText || "Scan code"
const handleUpdate = e => {
const changed = fieldApi.setValue(e.detail)
if (onChange && changed) {
onChange({ value: e.detail })
}
}
</script>
<Field
{label}
{field}
{disabled}
{validation}
{defaultValue}
{type}
bind:fieldState
bind:fieldApi
>
{#if fieldState}
<CodeScanner
value={fieldState.value}
on:change={handleUpdate}
disabled={fieldState.disabled}
{allowManualEntry}
scanButtonText={scanText}
/>
{/if}
</Field>

View File

@ -13,3 +13,4 @@ export { default as passwordfield } from "./PasswordField.svelte"
export { default as formstep } from "./FormStep.svelte"
export { default as jsonfield } from "./JSONField.svelte"
export { default as s3upload } from "./S3Upload.svelte"
export { default as codescanner } from "./CodeScannerField.svelte"

View File

@ -1,5 +1,6 @@
export const FieldTypes = {
STRING: "string",
BARCODEQR: "barcodeqr",
LONGFORM: "longform",
OPTIONS: "options",
NUMBER: "number",

View File

@ -31,6 +31,7 @@ exports.NoEmptyFilterStrings = [
exports.FieldTypes = {
STRING: "string",
BARCODEQR: "barcodeqr",
LONGFORM: "longform",
OPTIONS: "options",
NUMBER: "number",
@ -51,6 +52,7 @@ exports.CanSwitchTypes = [
exports.FieldTypes.STRING,
exports.FieldTypes.OPTIONS,
exports.FieldTypes.LONGFORM,
exports.FieldTypes.BARCODEQR,
],
[exports.FieldTypes.BOOLEAN, exports.FieldTypes.NUMBER],
]

View File

@ -40,6 +40,7 @@ function generateSchema(
case FieldTypes.STRING:
case FieldTypes.OPTIONS:
case FieldTypes.LONGFORM:
case FieldTypes.BARCODEQR:
schema.text(key)
break
case FieldTypes.NUMBER:

View File

@ -4,6 +4,7 @@ const { FieldTypes } = require("../constants")
const VALIDATORS = {
[FieldTypes.STRING]: () => true,
[FieldTypes.OPTIONS]: () => true,
[FieldTypes.BARCODEQR]: () => true,
[FieldTypes.NUMBER]: attribute => {
// allow not to be present
if (!attribute) {

View File

@ -48,6 +48,11 @@ const TYPE_TRANSFORM_MAP = {
[null]: "",
[undefined]: undefined,
},
[FieldTypes.BARCODEQR]: {
"": "",
[null]: "",
[undefined]: undefined,
},
[FieldTypes.FORMULA]: {
"": "",
[null]: "",

View File

@ -24,6 +24,7 @@ export enum QueryType {
export enum DatasourceFieldType {
STRING = "string",
CODE = "code",
LONGFORM = "longForm",
BOOLEAN = "boolean",
NUMBER = "number",