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:
parent
9c4cfe16f1
commit
2ec1ccc6a5
|
|
@ -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!",
|
||||
// );
|
||||
// });
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
40
app/client/src/assets/icons/widget/codeScanner/flip.svg
Normal file
40
app/client/src/assets/icons/widget/codeScanner/flip.svg
Normal 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 |
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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> = {};
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
};
|
||||
|
|
@ -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} </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} </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;
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -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"],
|
||||
},
|
||||
],
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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"],
|
||||
},
|
||||
],
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user