feat: Code Scanner Enhancements (#17929)

* feat: Add Scanner Variant - Click to scan and always on to code scanenr

* feat: Enhancements for code scanner's property pane - update help text, hide properties on always on

* feat: Add Image Mirror button to code scanner

* feat: Update code scanner video fit to contain

* feat: Add DSL migrations for Code Scanner

* feat: Make always on the default scanner layout, rename scannerVarient to scannerLayout

* feat: Stop scanning and detecting codes in background for code scanner widget

* test: Add Cypress tests for different scanner layouts for code scanner

* refactor: fix minor code callouts here and there

* refactor: Restructure cypress test suite

* feat: Increase code scanner delay to avoid unintended detections

* refactor: combine two different conditions into one ternary

* feat: Remove one cy test case

Co-authored-by: Aishwarya UR <aishwarya@appsmith.com>
This commit is contained in:
Dhruvik Neharia 2022-11-04 11:45:45 +05:30 committed by GitHub
parent 9c4cfe16f1
commit 2ec1ccc6a5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 561 additions and 114 deletions

View File

@ -2,62 +2,227 @@ const explorer = require("../../../../../locators/explorerlocators.json");
const widgetsPage = require("../../../../../locators/Widgets.json");
const commonlocators = require("../../../../../locators/commonlocators.json");
const publish = require("../../../../../locators/publishWidgetspage.json");
const widgetName = "codescannerwidget";
const codeScannerVideoOnPublishPage = `${publish.codescannerwidget} ${commonlocators.codeScannerVideo}`;
const codeScannerDisabledSVGIconOnPublishPage = `${publish.codescannerwidget} ${commonlocators.codeScannerDisabledSVGIcon}`;
describe("Code Scanner widget", () => {
it("1. Drag & drop Code Scanner/Text widgets", () => {
describe("Code Scanner widget's functionality", () => {
it("1 => Check if code scanner widget can be dropped on the canvas", () => {
// Drop the widget
cy.get(explorer.addWidget).click();
cy.dragAndDropToCanvas(widgetName, { x: 300, y: 300 });
cy.dragAndDropToCanvas(widgetName, { x: 300, y: 100 });
// Widget should be on the canvas
cy.get(widgetsPage.codescannerwidget).should("exist");
cy.dragAndDropToCanvas("textwidget", { x: 300, y: 500 });
});
it("2 => Check if the default scanner layout is ALWAYS_ON", () => {
// Drop a text widget to test the code scanner value binding
cy.dragAndDropToCanvas("textwidget", { x: 300, y: 600 });
cy.openPropertyPane("textwidget");
cy.updateCodeInput(".t--property-control-text", `{{CodeScanner1.value}}`);
cy.moveToContentTab();
cy.updateCodeInput(
".t--property-control-text",
`{{CodeScanner1.scannerLayout}}`,
);
cy.wait(200);
// Check the value of scanner layout
cy.get(commonlocators.TextInside).should("have.text", "ALWAYS_ON");
});
it("2. Code Scanner functionality to check disabled widget", function() {
cy.openPropertyPane(widgetName);
cy.togglebar(commonlocators.disableCheckbox);
cy.PublishtheApp();
cy.get(publish.codescannerwidget + " " + "button").should("be.disabled");
cy.get(publish.backToEditor).click();
describe("3 => Checks for the 'Always On' Scanner Layout", () => {
describe("3.1 => Checks for the disabled property", () => {
describe("3.1.1 => Check if the scanner can be disabled", () => {
it("3.1.1.1 => Disabled icon should be visible", () => {
cy.openPropertyPane(widgetName);
cy.moveToContentTab();
// Disable and publish
cy.togglebar(commonlocators.disableCheckbox);
cy.PublishtheApp();
// Disabled icon should be there
cy.get(codeScannerDisabledSVGIconOnPublishPage).should("exist");
});
it("3.1.1.2 => Scanner should not be scanning and streaming video", () => {
// Video should NOT be streaming
cy.get(codeScannerVideoOnPublishPage).should("not.exist");
// Back to editor
cy.get(publish.backToEditor).click();
});
});
describe("3.1.2 => Check if the scanner can be enabled", () => {
it("3.1.2.1 => Disabled icon should not be visible", () => {
cy.openPropertyPane(widgetName);
cy.moveToContentTab();
// Enable and publish
cy.togglebarDisable(commonlocators.disableCheckbox);
cy.PublishtheApp();
// Disabled icon should NOT be visible
cy.get(codeScannerDisabledSVGIconOnPublishPage).should("not.exist");
});
it("3.1.2.2 => Should be scanning and streaming video", () => {
// Video should be streaming
cy.get(codeScannerVideoOnPublishPage).should("exist");
// Back to editor
cy.get(publish.backToEditor).click();
});
});
});
describe("3.2 => Checks for the visible property", () => {
it("3.2.1 => Widget should be invisible on the canvas", () => {
cy.openPropertyPane(widgetName);
cy.moveToContentTab();
// Visibilty OFF and publish
cy.togglebarDisable(commonlocators.visibleCheckbox);
cy.PublishtheApp();
// Video should NOT be streaming
cy.get(codeScannerVideoOnPublishPage).should("not.exist");
// Back to editor
cy.get(publish.backToEditor).click();
});
it("3.2.2 => Widget should be visible on the canvas", () => {
cy.openPropertyPane(widgetName);
cy.moveToContentTab();
// Visibilty ON and publish
cy.togglebar(commonlocators.visibleCheckbox);
cy.PublishtheApp();
// Video should be streaming
cy.get(codeScannerVideoOnPublishPage).should("be.visible");
// Back to editor
cy.get(publish.backToEditor).click();
});
});
});
it("3. Code Scanner functionality to check enabled widget", function() {
cy.openPropertyPane(widgetName);
cy.togglebarDisable(commonlocators.disableCheckbox);
cy.PublishtheApp();
cy.get(publish.codescannerwidget + " " + "button").should("be.enabled");
cy.get(publish.backToEditor).click();
});
describe("4 => Checks for 'Click to Scan' Scanner Layout", () => {
it("4.1 => Check if scanner layout can be changed from Always On to Click to Scan", () => {
cy.openPropertyPane(widgetName);
cy.moveToContentTab();
it("4. Code Scanner functionality to uncheck visible widget", function() {
cy.openPropertyPane(widgetName);
cy.togglebarDisable(commonlocators.visibleCheckbox);
cy.PublishtheApp();
cy.get(publish.codescannerwidget + " " + "button").should("not.exist");
cy.get(publish.backToEditor).click();
});
// Select scanner layout as CLICK_TO_SCAN
cy.get(
`${commonlocators.codeScannerScannerLayout} .t--button-tab-CLICK_TO_SCAN`,
)
.last()
.click({
force: true,
});
it("5. Code Scanner functionality to check visible widget", function() {
cy.openPropertyPane(widgetName);
cy.togglebar(commonlocators.visibleCheckbox);
cy.PublishtheApp();
cy.get(publish.codescannerwidget + " " + "button").should("be.visible");
cy.get(publish.backToEditor).click();
});
cy.wait(200);
// Disabling this test for now.
// Check out - https://github.com/appsmithorg/appsmith/pull/15990#issuecomment-1241598309
// it("6. Open the Code Scanner modal and Scan a QR using fake webcam video.", function() {
// // Open
// cy.get(widgetsPage.codescannerwidget).click();
// //eslint-disable-next-line cypress/no-unnecessary-waiting
// cy.wait(2000);
// // Check if the QR code was read
// cy.get(".t--widget-textwidget").should(
// "contain",
// "Hello Cypress, this is from Appsmith!",
// );
// });
// Check if previously dropped text widget with value {{CodeScanner1.scannerLayout}} is updated
cy.get(commonlocators.TextInside).should("have.text", "CLICK_TO_SCAN");
// Publish
cy.PublishtheApp();
// Check if a button is added to the canvas
cy.get(publish.codescannerwidget + " " + "button").should("be.visible");
cy.get(publish.codescannerwidget + " " + "button").should("be.enabled");
// and video should not be streaming
cy.get(codeScannerVideoOnPublishPage).should("not.exist");
// Back to editor
cy.get(publish.backToEditor).click();
});
describe("4.2 => Checks for the disabled property", () => {
it("4.2.1 => Button on the canvas should be disabled", () => {
cy.openPropertyPane(widgetName);
cy.moveToContentTab();
// Disable and publish
cy.togglebar(commonlocators.disableCheckbox);
cy.PublishtheApp();
// Button should be disabled
cy.get(publish.codescannerwidget + " " + "button").should(
"be.disabled",
);
// Back to editor
cy.get(publish.backToEditor).click();
});
it("4.2.2 => Button on the canvas should be enabled again", () => {
cy.openPropertyPane(widgetName);
cy.moveToContentTab();
// Enable and publish
cy.togglebarDisable(commonlocators.disableCheckbox);
cy.PublishtheApp();
// Button should be enabled
cy.get(publish.codescannerwidget + " " + "button").should("be.enabled");
// Back to editor
cy.get(publish.backToEditor).click();
});
});
describe("4.3 => Checks for the visible property", () => {
it("4.3.1 => Button on the canvas should be invisible", () => {
cy.openPropertyPane(widgetName);
cy.moveToContentTab();
// Visibilty OFF and publish
cy.togglebarDisable(commonlocators.visibleCheckbox);
cy.PublishtheApp();
// Button should NOT be visible
cy.get(publish.codescannerwidget + " " + "button").should("not.exist");
// Back to editor
cy.get(publish.backToEditor).click();
});
it("4.3.2 => Button on the canvas should be visible again", () => {
cy.openPropertyPane(widgetName);
cy.moveToContentTab();
// Visibilty ON and publish
cy.togglebar(commonlocators.visibleCheckbox);
cy.PublishtheApp();
// Button should be visible
cy.get(publish.codescannerwidget + " " + "button").should("be.visible");
// Back to editor
cy.get(publish.backToEditor).click();
});
});
});
});
// Disabling this test for now.
// Check out - https://github.com/appsmithorg/appsmith/pull/15990#issuecomment-1241598309
// it("6. Open the Code Scanner modal and Scan a QR using fake webcam video.", () => {
// // Open
// cy.get(widgetsPage.codescannerwidget).click();
// //eslint-disable-next-line cypress/no-unnecessary-waiting
// cy.wait(2000);
// // Check if the QR code was read
// cy.get(".t--widget-textwidget").should(
// "contain",
// "Hello Cypress, this is from Appsmith!",
// );
// });

View File

@ -189,5 +189,8 @@
"textWidgetContainer": ".t--text-widget-container",
"propertyStyle": "li:contains('STYLE')",
"propertyContent": "li:contains('CONTENT')",
"cancelActionExecution": ".t--cancel-action-button"
"cancelActionExecution": ".t--cancel-action-button",
"codeScannerScannerLayout": ".t--property-control-scannerlayout",
"codeScannerVideo": ".code-scanner-camera-container video",
"codeScannerDisabledSVGIcon": ".code-scanner-camera-container div[disabled] svg"
}

View File

@ -22,6 +22,7 @@
"@sentry/react": "^6.2.4",
"@sentry/tracing": "^6.2.4",
"@tinymce/tinymce-react": "^3.13.0",
"@types/react-page-visibility": "^6.4.1",
"@uppy/core": "^1.16.0",
"@uppy/dashboard": "^1.16.0",
"@uppy/file-input": "^1.4.22",
@ -123,6 +124,7 @@
"react-media-recorder": "^1.6.1",
"react-mentions": "^4.1.1",
"react-modal": "^3.15.1",
"react-page-visibility": "^7.0.0",
"react-paginating": "^1.4.0",
"react-player": "^2.3.1",
"react-qr-barcode-scanner": "^1.0.6",

View File

@ -0,0 +1,40 @@
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" width="20" height="20" x="0" y="0" viewBox="0 0 512 512">
<g>
<g id="Flip">
<g>
<path
d="m460.429 449.927-150-60c-5.695-2.278-9.429-7.793-9.429-13.927v-240c0-6.134 3.734-11.649 9.429-13.927l150-60c4.622-1.847 9.858-1.285 13.98 1.506s6.591 7.443 6.591 12.421v360c0 4.978-2.469 9.63-6.591 12.421-4.075 2.759-9.306 3.375-13.98 1.506zm-129.429-84.082 120 48v-315.69l-120 48z"
fill="#fff" stroke-width="10px" stroke="white"></path>
</g>
<g>
<path
d="m37.591 448.421c-4.122-2.791-6.591-7.443-6.591-12.421v-360c0-4.978 2.469-9.63 6.591-12.421 4.123-2.791 9.36-3.354 13.98-1.506l150 60c5.695 2.278 9.429 7.793 9.429 13.927v240c0 6.134-3.734 11.649-9.429 13.927l-150 60c-4.669 1.867-9.9 1.256-13.98-1.506zm23.409-350.266v315.689l120-48v-219.689zm135 277.845h.01z"
fill="#fff" stroke-width="10px" stroke="white"></path>
</g>
<g>
<path d="m256 512c-8.284 0-15-6.716-15-15v-31c0-8.284 6.716-15 15-15s15 6.716 15 15v31c0 8.284-6.716 15-15 15z"
fill="#fff" stroke-width="10px" stroke="white"></path>
</g>
<g>
<path d="m256 421c-8.284 0-15-6.716-15-15v-30c0-8.284 6.716-15 15-15s15 6.716 15 15v30c0 8.284-6.716 15-15 15z"
fill="#fff" stroke-width="10px" stroke="white"></path>
</g>
<g>
<path d="m256 331c-8.284 0-15-6.716-15-15v-30c0-8.284 6.716-15 15-15s15 6.716 15 15v30c0 8.284-6.716 15-15 15z"
fill="#fff" stroke-width="10px" stroke="white"></path>
</g>
<g>
<path d="m256 241c-8.284 0-15-6.716-15-15v-30c0-8.284 6.716-15 15-15s15 6.716 15 15v30c0 8.284-6.716 15-15 15z"
fill="#fff" stroke-width="10px" stroke="white"></path>
</g>
<g>
<path d="m256 151c-8.284 0-15-6.716-15-15v-30c0-8.284 6.716-15 15-15s15 6.716 15 15v30c0 8.284-6.716 15-15 15z"
fill="#fff" stroke-width="10px" stroke="white"></path>
</g>
<g>
<path d="m256 61c-8.284 0-15-6.716-15-15v-31c0-8.284 6.716-15 15-15s15 6.716 15 15v31c0 8.284-6.716 15-15 15z"
fill="#fff" stroke-width="10px" stroke="white"></path>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.2 KiB

View File

@ -70,7 +70,7 @@ export const layoutConfigurations: LayoutConfigurations = {
FLUID: { minWidth: -1, maxWidth: -1 },
};
export const LATEST_PAGE_VERSION = 65;
export const LATEST_PAGE_VERSION = 66;
export const GridDefaults = {
DEFAULT_CELL_SIZE: 1,

View File

@ -20,6 +20,7 @@ import * as mapChartReskinningMigrations from "./migrations/MapChartReskinningMi
import { LATEST_PAGE_VERSION } from "constants/WidgetConstants";
import { originalDSLForDSLMigrations } from "./testDSLs";
import * as rateWidgetMigrations from "./migrations/RateWidgetMigrations";
import * as codeScannerWidgetMigrations from "./migrations/CodeScannerWidgetMigrations";
type Migration = {
functionLookup: {
@ -634,6 +635,15 @@ const migrations: Migration[] = [
],
version: 64,
},
{
functionLookup: [
{
moduleObj: codeScannerWidgetMigrations,
functionName: "migrateCodeScannerLayout",
},
],
version: 65,
},
];
const mockFnObj: Record<number, any> = {};

View File

@ -63,6 +63,7 @@ import { migrateChartWidgetReskinningData } from "./migrations/ChartWidgetReskin
import { MigrateSelectTypeWidgetDefaultValue } from "./migrations/SelectWidget";
import { migrateMapChartWidgetReskinningData } from "./migrations/MapChartReskinningMigrations";
import { migrateRateWidgetDisabledState } from "./migrations/RateWidgetMigrations";
import { migrateCodeScannerLayout } from "./migrations/CodeScannerWidgetMigrations";
/**
* adds logBlackList key for all list widget children
@ -1127,6 +1128,11 @@ export const transformDSL = (
if (currentDSL.version === 64) {
currentDSL = migrateRateWidgetDisabledState(currentDSL);
currentDSL.version = 65;
}
if (currentDSL.version === 65) {
currentDSL = migrateCodeScannerLayout(currentDSL);
currentDSL.version = LATEST_PAGE_VERSION;
}

View File

@ -0,0 +1,18 @@
import { WidgetProps } from "widgets/BaseWidget";
import { DSLWidget } from "widgets/constants";
export const migrateCodeScannerLayout = (currentDSL: DSLWidget) => {
currentDSL.children = currentDSL.children?.map((child: WidgetProps) => {
if (child.type === "CODE_SCANNER_WIDGET") {
if (!child.scannerLayout) {
child.scannerLayout = "CLICK_TO_SCAN";
}
} else if (child.children && child.children.length > 0) {
child = migrateCodeScannerLayout(child);
}
return child;
});
return currentDSL;
};

View File

@ -3,7 +3,7 @@ import { ComponentProps } from "widgets/BaseComponent";
import { BaseButton } from "widgets/ButtonWidget/component";
import Modal from "react-modal";
import BarcodeScannerComponent from "react-qr-barcode-scanner";
import styled, { createGlobalStyle } from "styled-components";
import styled, { createGlobalStyle, css } from "styled-components";
import CloseIcon from "assets/icons/ads/cross.svg";
import { getBrowserInfo, getPlatformOS, PLATFORM_OS } from "utils/helpers";
import { Button, Icon, Menu, MenuItem, Position } from "@blueprintjs/core";
@ -16,11 +16,23 @@ import { Popover2 } from "@blueprintjs/popover2";
import Interweave from "interweave";
import { Alignment } from "@blueprintjs/core";
import { IconName } from "@blueprintjs/icons";
import { ButtonPlacement } from "components/constants";
import {
ButtonBorderRadius,
ButtonBorderRadiusTypes,
ButtonPlacement,
ButtonVariant,
ButtonVariantTypes,
} from "components/constants";
import { ScannerLayout } from "../constants";
import { ThemeProp } from "widgets/constants";
import { ReactComponent as FlipImageIcon } from "assets/icons/widget/codeScanner/flip.svg";
import { usePageVisibility } from "react-page-visibility";
const CodeScannerGlobalStyles = createGlobalStyle<{
borderRadius?: string;
boxShadow?: string;
disabled: boolean;
scannerLayout: ScannerLayout;
}>`
.code-scanner-content {
position: fixed;
@ -74,6 +86,11 @@ const CodeScannerGlobalStyles = createGlobalStyle<{
.code-scanner-camera-container {
border-radius: ${({ borderRadius }) => borderRadius};
background: ${({ disabled }) =>
disabled ? "var(--wds-color-bg-disabled)" : "#000"};
border-radius: ${({ borderRadius }) => borderRadius};
box-shadow: ${({ boxShadow, scannerLayout }) =>
scannerLayout === ScannerLayout.ALWAYS_ON ? boxShadow : "none"};
overflow: hidden;
height: 100%;
position: relative;
@ -94,16 +111,47 @@ const CodeScannerGlobalStyles = createGlobalStyle<{
z-index: 1;
border-radius: ${({ borderRadius }) => borderRadius};
}
&.mirror-video {
video {
transform: scaleX(-1);
}
}
}
.code-scanner-camera-container video {
height: 100%;
position: relative;
object-fit: cover;
object-fit: contain;
border-radius: ${({ borderRadius }) => borderRadius};
}
`;
const overlayerMixin = css`
position: absolute;
height: 100%;
width: 100%;
object-fit: contain;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
display: flex;
align-items: center;
justify-content: center;
`;
export interface DisabledOverlayerProps {
disabled: boolean;
}
const DisabledOverlayer = styled.div<DisabledOverlayerProps>`
${overlayerMixin};
display: ${({ disabled }) => (disabled ? `flex` : `none`)};
height: 100%;
z-index: 2;
background: var(--wds-color-bg-disabled);
`;
const DeviceButtonContainer = styled.div`
position: relative;
`;
@ -140,7 +188,7 @@ const ControlPanelOverlayer = styled.div<ControlPanelOverlayerProps>`
const MediaInputsContainer = styled.div`
display: flex;
flex: 1;
justify-content: flex-end;
justify-content: space-between;
& .bp3-minimal {
height: 30px;
@ -170,7 +218,10 @@ const TooltipStyles = createGlobalStyle`
}
`;
const ErrorMessageWrapper = styled.div`
const ErrorMessageWrapper = styled.div<{
borderRadius?: string;
boxShadow?: string;
}>`
height: 100%;
width: 100%;
padding: 0.5em 0;
@ -180,6 +231,37 @@ const ErrorMessageWrapper = styled.div`
align-items: center;
justify-content: center;
flex-direction: column;
background-color: black;
border-radius: ${({ borderRadius }) => borderRadius};
box-shadow: ${({ boxShadow }) => boxShadow};
`;
export interface StyledButtonProps {
variant: ButtonVariant;
borderRadius: ButtonBorderRadius;
}
const StyledButton = styled(Button)<ThemeProp & StyledButtonProps>`
z-index: 1;
height: 32px;
width: 32px;
margin: 0 1%;
box-shadow: none !important;
${({ borderRadius }) =>
borderRadius === ButtonBorderRadiusTypes.CIRCLE &&
`
border-radius: 50%;
`}
border: ${({ variant }) =>
variant === ButtonVariantTypes.SECONDARY ? `1px solid white` : `none`};
background: ${({ theme, variant }) =>
variant === ButtonVariantTypes.PRIMARY
? theme.colors.button.primary.primary.bgColor
: `none`} !important;
&:hover {
background: rgba(167, 182, 194, 0.3) !important;
}
`;
// Device menus (microphone, camera)
@ -246,6 +328,7 @@ export interface ControlPanelProps {
appLayoutType?: SupportedLayouts;
onMediaInputChange: (mediaDeviceInfo: MediaDeviceInfo) => void;
updateDeviceInputs: () => void;
handleImageMirror: () => void;
}
function ControlPanel(props: ControlPanelProps) {
@ -302,6 +385,12 @@ function ControlPanel(props: ControlPanelProps) {
<ControlPanelContainer>
<ControlPanelOverlayer appLayoutType={appLayoutType}>
<MediaInputsContainer>
<StyledButton
borderRadius={ButtonBorderRadiusTypes.SHARP}
icon={<Icon color="white" icon={<FlipImageIcon />} iconSize={20} />}
onClick={props.handleImageMirror}
variant={ButtonVariantTypes.TERTIARY}
/>
{renderMediaDeviceSelectors()}
</MediaInputsContainer>
</ControlPanelOverlayer>
@ -313,12 +402,19 @@ function CodeScannerComponent(props: CodeScannerComponentProps) {
const [modalIsOpen, setIsOpen] = useState(false);
const [videoInputs, setVideoInputs] = useState<MediaDeviceInfo[]>([]);
const [error, setError] = useState<string>("");
const [isImageMirrored, setIsImageMirrored] = useState(false);
const [videoConstraints, setVideoConstraints] = useState<
MediaTrackConstraints
>({
facingMode: "environment",
});
/**
* Check if the tab is active.
* If not, stop scanning and detecting codes in background.
*/
const isTabActive = usePageVisibility();
const openModal = () => {
setIsOpen(true);
};
@ -368,6 +464,10 @@ function CodeScannerComponent(props: CodeScannerComponentProps) {
setError((error as DOMException).message);
}, []);
const handleImageMirror = () => {
setIsImageMirrored(!isImageMirrored);
};
const renderComponent = () => {
const handleOnResult = (err: any, result: any) => {
if (!!result) {
@ -382,54 +482,86 @@ function CodeScannerComponent(props: CodeScannerComponentProps) {
}
};
return (
<>
<CodeScannerGlobalStyles
borderRadius={props.borderRadius}
boxShadow={props.boxShadow}
/>
const errorMessage = (
<ErrorMessageWrapper
borderRadius={props.borderRadius}
boxShadow={props.boxShadow}
>
<CameraOfflineIcon />
<span className="error-text">{error}&ensp;</span>
{error === "Permission denied" && (
<a
href="https://support.google.com/chrome/answer/2693767"
rel="noreferrer"
target="_blank"
>
Know more
</a>
)}
</ErrorMessageWrapper>
);
<Modal
className="code-scanner-content"
isOpen={modalIsOpen}
onRequestClose={closeModal}
overlayClassName="code-scanner-overlay"
>
{error && (
<ErrorMessageWrapper>
<CameraOfflineIcon />
<span className="error-text">{error}&ensp;</span>
{error === "Permission denied" && (
<a
href="https://support.google.com/chrome/answer/2693767"
rel="noreferrer"
target="_blank"
>
Know more
</a>
)}
</ErrorMessageWrapper>
)}
{modalIsOpen && !error && (
<div className="code-scanner-camera-container">
const codeScannerCameraContainer = (
<div
className={`code-scanner-camera-container ${
isImageMirrored ? "mirror-video" : ""
}`}
>
{props.isDisabled ? (
<DisabledOverlayer disabled={props.isDisabled}>
<CameraOfflineIcon />
</DisabledOverlayer>
) : (
<>
{isTabActive && (
<BarcodeScannerComponent
delay={1000}
key={JSON.stringify(videoConstraints)}
onError={handleCameraErrors}
onUpdate={handleOnResult}
videoConstraints={videoConstraints}
/>
<ControlPanel
appLayoutType={appLayout?.type}
onMediaInputChange={handleMediaDeviceChange}
updateDeviceInputs={updateDeviceInputs}
videoInputs={videoInputs}
/>
</div>
)}
)}
<button className="code-scanner-close" onClick={closeModal} />
</Modal>
<ControlPanel
appLayoutType={appLayout?.type}
handleImageMirror={handleImageMirror}
onMediaInputChange={handleMediaDeviceChange}
updateDeviceInputs={updateDeviceInputs}
videoInputs={videoInputs}
/>
</>
)}
</div>
);
const scanAlways = error ? errorMessage : codeScannerCameraContainer;
const scanInAModal = (
<Modal
className="code-scanner-content"
isOpen={modalIsOpen}
onRequestClose={closeModal}
overlayClassName="code-scanner-overlay"
>
{error ? errorMessage : modalIsOpen && codeScannerCameraContainer}
<button className="code-scanner-close" onClick={closeModal} />
</Modal>
);
return (
<>
<CodeScannerGlobalStyles
borderRadius={props.borderRadius}
boxShadow={props.boxShadow}
disabled={props.isDisabled}
scannerLayout={props.scannerLayout}
/>
{props.scannerLayout === ScannerLayout.ALWAYS_ON
? scanAlways
: scanInAModal}
</>
);
};
@ -450,23 +582,24 @@ function CodeScannerComponent(props: CodeScannerComponentProps) {
return (
<>
{!props.tooltip ? (
baseButtonWrapper
) : (
<ToolTipWrapper>
<TooltipStyles />
<Popover2
autoFocus={false}
content={<Interweave content={props.tooltip} />}
hoverOpenDelay={200}
interactionKind="hover"
portalClassName="iconBtnTooltipContainer"
position={Position.TOP}
>
{baseButtonWrapper}
</Popover2>
</ToolTipWrapper>
)}
{props.scannerLayout !== ScannerLayout.ALWAYS_ON &&
(!props.tooltip ? (
baseButtonWrapper
) : (
<ToolTipWrapper>
<TooltipStyles />
<Popover2
autoFocus={false}
content={<Interweave content={props.tooltip} />}
hoverOpenDelay={200}
interactionKind="hover"
portalClassName="iconBtnTooltipContainer"
position={Position.TOP}
>
{baseButtonWrapper}
</Popover2>
</ToolTipWrapper>
))}
{renderComponent()}
</>
@ -483,6 +616,7 @@ export interface CodeScannerComponentProps extends ComponentProps {
iconAlign?: Alignment;
placement?: ButtonPlacement;
onCodeDetected: (value: string) => void;
scannerLayout: ScannerLayout;
}
export default CodeScannerComponent;

View File

@ -14,4 +14,10 @@ export interface CodeScannerWidgetProps extends WidgetProps {
iconName?: IconName;
iconAlign?: Alignment;
placement?: ButtonPlacement;
scannerLayout: ScannerLayout;
}
export enum ScannerLayout {
ALWAYS_ON = "ALWAYS_ON",
CLICK_TO_SCAN = "CLICK_TO_SCAN",
}

View File

@ -1,6 +1,7 @@
import IconSVG from "./icon.svg";
import Widget from "./widget";
import { ButtonPlacementTypes } from "components/constants";
import { ScannerLayout } from "./constants";
export const CONFIG = {
type: Widget.getWidgetType(),
@ -14,11 +15,12 @@ export const CONFIG = {
"barcode reader",
],
defaults: {
rows: 4,
rows: 33,
label: "Scan a QR/Barcode",
columns: 16,
columns: 25,
widgetName: "CodeScanner",
isDefaultClickDisabled: true,
scannerLayout: ScannerLayout.ALWAYS_ON,
version: 1,
isRequired: false,
isDisabled: false,

View File

@ -47,6 +47,7 @@ class CodeScannerWidget extends BaseWidget<
label={this.props.label}
onCodeDetected={this.onCodeDetected}
placement={this.props.placement}
scannerLayout={this.props.scannerLayout}
tooltip={this.props.tooltip}
widgetId={this.props.widgetId}
/>

View File

@ -1,10 +1,35 @@
import { ValidationTypes } from "constants/WidgetValidation";
import { PropertyPaneConfig } from "constants/PropertyControlConstants";
import {
CodeScannerWidgetProps,
ScannerLayout,
} from "widgets/CodeScannerWidget/constants";
export default [
{
sectionName: "Label",
sectionName: "Basic",
children: [
{
propertyName: "scannerLayout",
label: "Scanner Layout",
controlType: "ICON_TABS",
fullWidth: true,
helpText:
'Sets how the code scanner will look and behave. If set to "Always on", the scanner will be visible and scanning all the time. If set to "Click to Scan", the scanner will pop up inside a modal and start scanning when the user clicks on the button.',
options: [
{
label: "Always On",
value: ScannerLayout.ALWAYS_ON,
},
{
label: "Click to Scan",
value: ScannerLayout.CLICK_TO_SCAN,
},
],
isJSConvertible: false,
isBindProperty: false,
isTriggerProperty: false,
},
{
propertyName: "label",
label: "Text",
@ -15,6 +40,9 @@ export default [
isBindProperty: true,
isTriggerProperty: false,
validation: { type: ValidationTypes.TEXT },
hidden: (props: CodeScannerWidgetProps) =>
props.scannerLayout === ScannerLayout.ALWAYS_ON,
dependencies: ["scannerLayout"],
},
],
},
@ -61,6 +89,9 @@ export default [
isBindProperty: true,
isTriggerProperty: false,
validation: { type: ValidationTypes.TEXT },
hidden: (props: CodeScannerWidgetProps) =>
props.scannerLayout === ScannerLayout.ALWAYS_ON,
dependencies: ["scannerLayout"],
},
],
},

View File

@ -1,6 +1,10 @@
import { ValidationTypes } from "constants/WidgetValidation";
import { ButtonPlacementTypes } from "components/constants";
import { updateStyles } from "../propertyUtils";
import {
CodeScannerWidgetProps,
ScannerLayout,
} from "widgets/CodeScannerWidget/constants";
export default [
{
@ -14,10 +18,12 @@ export default [
isBindProperty: false,
isTriggerProperty: false,
updateHook: updateStyles,
dependencies: ["iconAlign"],
dependencies: ["iconAlign", "scannerLayout"],
validation: {
type: ValidationTypes.TEXT,
},
hidden: (props: CodeScannerWidgetProps) =>
props.scannerLayout === ScannerLayout.ALWAYS_ON,
},
{
propertyName: "iconAlign",
@ -43,6 +49,9 @@ export default [
allowedValues: ["center", "left", "right"],
},
},
hidden: (props: CodeScannerWidgetProps) =>
props.scannerLayout === ScannerLayout.ALWAYS_ON,
dependencies: ["scannerLayout"],
},
{
propertyName: "placement",
@ -79,6 +88,9 @@ export default [
default: ButtonPlacementTypes.CENTER,
},
},
hidden: (props: CodeScannerWidgetProps) =>
props.scannerLayout === ScannerLayout.ALWAYS_ON,
dependencies: ["scannerLayout"],
},
],
},
@ -99,6 +111,9 @@ export default [
regex: /^(?![<|{{]).+/,
},
},
hidden: (props: CodeScannerWidgetProps) =>
props.scannerLayout === ScannerLayout.ALWAYS_ON,
dependencies: ["scannerLayout"],
},
],
},

View File

@ -3268,6 +3268,13 @@
dependencies:
"@types/react" "*"
"@types/react-page-visibility@^6.4.1":
version "6.4.1"
resolved "https://registry.yarnpkg.com/@types/react-page-visibility/-/react-page-visibility-6.4.1.tgz#21c3bc4a3f310d38d188916cadc55f2bde65f27d"
integrity sha512-vNlYAqKhB2SU1HmF9ARFTFZN0NSPzWn8HSjBpFqYuQlJhsb/aSYeIZdygeqfSjAg0PZ70id2IFWHGULJwe59Aw==
dependencies:
"@types/react" "*"
"@types/react-redux@^7.0.1":
version "7.1.9"
resolved "https://registry.npmjs.org/@types/react-redux/-/react-redux-7.1.9.tgz"
@ -12855,6 +12862,13 @@ react-modal@^3.15.1:
react-lifecycles-compat "^3.0.0"
warning "^4.0.3"
react-page-visibility@^7.0.0:
version "7.0.0"
resolved "https://registry.yarnpkg.com/react-page-visibility/-/react-page-visibility-7.0.0.tgz#13dfe604790d061e70b900038bad1ca769a36cbc"
integrity sha512-d4Kq/8TtJSr8dQc8EJeAZcSKTrGzC5OPTm6UrMur9BnwP0fgTawI9+Nd+ZGB7vwCfn2yZS0qDF9DR3/QYTGazw==
dependencies:
prop-types "^15.7.2"
react-paginating@^1.4.0:
version "1.4.0"
resolved "https://registry.npmjs.org/react-paginating/-/react-paginating-1.4.0.tgz"