Initial commit of QR Reader field

This commit is contained in:
Dean 2022-10-05 09:28:07 +01:00
parent 41ea9b9c2a
commit 5836c2b3c4
18 changed files with 265 additions and 2 deletions

View File

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

View File

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

View File

@ -124,6 +124,10 @@
label: "Multi-select", label: "Multi-select",
value: FIELDS.ARRAY.type, value: FIELDS.ARRAY.type,
}, },
{
label: "Code",
value: FIELDS.CODE.type,
},
] ]
</script> </script>

View File

@ -51,6 +51,7 @@ const componentMap = {
"field/link": FormFieldSelect, "field/link": FormFieldSelect,
"field/array": FormFieldSelect, "field/array": FormFieldSelect,
"field/json": FormFieldSelect, "field/json": FormFieldSelect,
"field/code": 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
"validation/string": ValidationEditor, "validation/string": ValidationEditor,

View File

@ -26,7 +26,7 @@
let entries = Object.entries(schema ?? {}) let entries = Object.entries(schema ?? {})
let types = [] let types = []
if (type === "field/options") { if ((type === "field/options", type === "field/code")) {
// allow options to be used on both options and string fields // allow options to be used on both options and string fields
types = [type, "field/string"] types = [type, "field/string"]
} else { } else {

View File

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

View File

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

View File

@ -3157,6 +3157,42 @@
} }
] ]
}, },
"codescanner": {
"name": "Code Scanner",
"icon": "Camera",
"styles": [
"size"
],
"draggable": true,
"illegalChildren": [
"section"
],
"settings": [
{
"type": "text",
"label": "Label",
"key": "label",
"defaultValue": true
},
{
"type": "field/code",
"label": "Field",
"key": "field",
"required": true
},
{
"type": "boolean",
"label": "Disabled",
"key": "disabled",
"defaultValue": false
},
{
"type": "validation/string",
"label": "Validation",
"key": "validation"
}
]
},
"embeddedmap": { "embeddedmap": {
"name": "Embedded Map", "name": "Embedded Map",
"icon": "Location", "icon": "Location",

View File

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

View File

@ -0,0 +1,141 @@
<script>
import { ModalContent, Modal, Select } from "@budibase/bbui"
import { Input, Button, StatusLight } from "@budibase/bbui"
import { Html5Qrcode } from "html5-qrcode"
export let code = ""
let videoEle
let camModal
let manualMode = false
let enabled = false
let cameraInit = false
let html5QrCode
let cameraId
let cameraDevices = []
let selectedCam
const checkCamera = async () => {
return new Promise(resolve => {
Html5Qrcode.getCameras()
.then(devices => {
if (devices && devices.length) {
cameraDevices = devices
cameraId = devices[0].id
resolve({ enabled: true })
}
})
.catch(() => {
resolve({ enabled: false })
})
})
}
$: if (enabled && videoEle && !cameraInit) {
html5QrCode = new Html5Qrcode("reader")
html5QrCode
.start(
cameraId,
{
fps: 25,
qrbox: { width: 250, height: 250 },
},
(decodedText, decodedResult) => {
code = decodedText
console.log(decodedText, decodedResult)
}
)
.catch(err => {
console.log("There was a problem scanning the image", err)
})
}
const showReaderModal = async () => {
camModal.show()
const camStatus = await checkCamera()
enabled = camStatus.enabled
}
const hideReaderModal = async () => {
camModal.hide()
await html5QrCode.stop()
}
</script>
<div class="scanner-video-wrapper">
{#if code}
<div class="scanner-value">
<StatusLight positive />
{code}
</div>
{/if}
<Button primary icon="Camera" on:click={showReaderModal}>Scan Code</Button>
</div>
<div class="modal-wrap">
<Select
on:change={e => console.log(e)}
value={selectedCam}
options={cameraDevices}
getOptionLabel={() => cameraDevices}
/>
<Modal bind:this={camModal} on:hide={hideReaderModal}>
<ModalContent
title="Scan Code"
showCancelButton={false}
showConfirmButton={false}
>
<div id="reader" bind:this={videoEle} />
<div class="code-wrap">
{#if manualMode}
<Input label="Enter" bind:value={code} />
{/if}
{#if code}
<div class="scanner-value">
<StatusLight positive />
{code}
</div>
{/if}
{#if !code && enabled && videoEle && cameraInit}
<div class="scanner-value">
<StatusLight neutral />
Searching for code...
</div>
{/if}
</div>
<div slot="footer">
<div class="footer-buttons">
<Button
group
secondary
newStyles
on:click={() => {
manualMode = !manualMode
}}
>
Enter Manually
</Button>
<Button group cta disabled={!code}>Confirm</Button>
</div>
</div>
</ModalContent>
</Modal>
</div>
<style>
.footer-buttons {
display: flex;
grid-area: buttonGroup;
gap: var(--spectrum-global-dimension-static-size-200);
}
.scanner-value {
padding-top: var(
--spectrum-fieldlabel-side-m-padding-top,
var(--spectrum-global-dimension-size-100)
);
display: flex;
margin-bottom: var(--spacing-m);
}
</style>

View File

@ -0,0 +1,55 @@
<script>
import Field from "./Field.svelte"
import CodeScanner from "../CodeScanner.svelte"
export let field
export let label
export let type = "code"
export let disabled = false
export let validation
export let defaultValue = ""
export let onChange
let fieldState
let fieldApi
let scannedCode
let loaded = false
const handleInput = () => {
const changed = fieldApi.setValue(scannedCode)
if (onChange && changed) {
onChange({ value: scannedCode })
}
}
$: if (!loaded && !scannedCode && fieldState?.value) {
scannedCode = fieldState.value + ""
loaded = true
}
/*
QR Nimiq has rollup issues?
QR qrcodejs 12b bundle?
https://github.com/davidshimjs/qrcodejs
BOTH html5-qrcode has a 330k bundle
https://github.com/mebjas/html5-qrcode
BOTH zxing 360k bundle size
https://github.com/zxing-js/library
*/
</script>
<Field
{label}
{field}
{disabled}
{validation}
{defaultValue}
{type}
bind:fieldState
bind:fieldApi
>
{#if fieldState}
<CodeScanner bind:code={scannedCode} on:input={handleInput} />
{/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 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"

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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