diff --git a/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/Widgets/CodeScanner/CodeScanner_spec.js b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/Widgets/CodeScanner/CodeScanner_spec.js index 9e36a07251..787ca5083e 100644 --- a/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/Widgets/CodeScanner/CodeScanner_spec.js +++ b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/Widgets/CodeScanner/CodeScanner_spec.js @@ -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!", +// ); +// }); diff --git a/app/client/cypress/locators/commonlocators.json b/app/client/cypress/locators/commonlocators.json index ed5622cfeb..c5a62b979e 100644 --- a/app/client/cypress/locators/commonlocators.json +++ b/app/client/cypress/locators/commonlocators.json @@ -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" } diff --git a/app/client/package.json b/app/client/package.json index c3e2380870..c082750844 100644 --- a/app/client/package.json +++ b/app/client/package.json @@ -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", diff --git a/app/client/src/assets/icons/widget/codeScanner/flip.svg b/app/client/src/assets/icons/widget/codeScanner/flip.svg new file mode 100644 index 0000000000..22d97e4935 --- /dev/null +++ b/app/client/src/assets/icons/widget/codeScanner/flip.svg @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/client/src/constants/WidgetConstants.tsx b/app/client/src/constants/WidgetConstants.tsx index e589725e85..15c54f9418 100644 --- a/app/client/src/constants/WidgetConstants.tsx +++ b/app/client/src/constants/WidgetConstants.tsx @@ -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, diff --git a/app/client/src/utils/DSLMigration.test.ts b/app/client/src/utils/DSLMigration.test.ts index 83cd53b48e..2f1621b9ba 100644 --- a/app/client/src/utils/DSLMigration.test.ts +++ b/app/client/src/utils/DSLMigration.test.ts @@ -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 = {}; diff --git a/app/client/src/utils/DSLMigrations.ts b/app/client/src/utils/DSLMigrations.ts index 0eef63ca7f..a37eedeaf1 100644 --- a/app/client/src/utils/DSLMigrations.ts +++ b/app/client/src/utils/DSLMigrations.ts @@ -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; } diff --git a/app/client/src/utils/migrations/CodeScannerWidgetMigrations.ts b/app/client/src/utils/migrations/CodeScannerWidgetMigrations.ts new file mode 100644 index 0000000000..e0d1ebc6eb --- /dev/null +++ b/app/client/src/utils/migrations/CodeScannerWidgetMigrations.ts @@ -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; +}; diff --git a/app/client/src/widgets/CodeScannerWidget/component/index.tsx b/app/client/src/widgets/CodeScannerWidget/component/index.tsx index aacfb1df60..0371d520e5 100644 --- a/app/client/src/widgets/CodeScannerWidget/component/index.tsx +++ b/app/client/src/widgets/CodeScannerWidget/component/index.tsx @@ -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` + ${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` 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)` + 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) { + } iconSize={20} />} + onClick={props.handleImageMirror} + variant={ButtonVariantTypes.TERTIARY} + /> {renderMediaDeviceSelectors()} @@ -313,12 +402,19 @@ function CodeScannerComponent(props: CodeScannerComponentProps) { const [modalIsOpen, setIsOpen] = useState(false); const [videoInputs, setVideoInputs] = useState([]); const [error, setError] = useState(""); + 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 ( - <> - + const errorMessage = ( + + + {error} + {error === "Permission denied" && ( + + Know more + + )} + + ); - - {error && ( - - - {error} - {error === "Permission denied" && ( - - Know more - - )} - - )} - - {modalIsOpen && !error && ( - + const codeScannerCameraContainer = ( + + {props.isDisabled ? ( + + + + ) : ( + <> + {isTabActive && ( - - - )} + )} - - + + > + )} + + ); + + const scanAlways = error ? errorMessage : codeScannerCameraContainer; + + const scanInAModal = ( + + {error ? errorMessage : modalIsOpen && codeScannerCameraContainer} + + + + ); + + return ( + <> + + + {props.scannerLayout === ScannerLayout.ALWAYS_ON + ? scanAlways + : scanInAModal} > ); }; @@ -450,23 +582,24 @@ function CodeScannerComponent(props: CodeScannerComponentProps) { return ( <> - {!props.tooltip ? ( - baseButtonWrapper - ) : ( - - - } - hoverOpenDelay={200} - interactionKind="hover" - portalClassName="iconBtnTooltipContainer" - position={Position.TOP} - > - {baseButtonWrapper} - - - )} + {props.scannerLayout !== ScannerLayout.ALWAYS_ON && + (!props.tooltip ? ( + baseButtonWrapper + ) : ( + + + } + hoverOpenDelay={200} + interactionKind="hover" + portalClassName="iconBtnTooltipContainer" + position={Position.TOP} + > + {baseButtonWrapper} + + + ))} {renderComponent()} > @@ -483,6 +616,7 @@ export interface CodeScannerComponentProps extends ComponentProps { iconAlign?: Alignment; placement?: ButtonPlacement; onCodeDetected: (value: string) => void; + scannerLayout: ScannerLayout; } export default CodeScannerComponent; diff --git a/app/client/src/widgets/CodeScannerWidget/constants.ts b/app/client/src/widgets/CodeScannerWidget/constants.ts index 484326f18f..7ce680e65f 100644 --- a/app/client/src/widgets/CodeScannerWidget/constants.ts +++ b/app/client/src/widgets/CodeScannerWidget/constants.ts @@ -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", } diff --git a/app/client/src/widgets/CodeScannerWidget/index.ts b/app/client/src/widgets/CodeScannerWidget/index.ts index 73c6d4c657..084704696c 100644 --- a/app/client/src/widgets/CodeScannerWidget/index.ts +++ b/app/client/src/widgets/CodeScannerWidget/index.ts @@ -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, diff --git a/app/client/src/widgets/CodeScannerWidget/widget/index.tsx b/app/client/src/widgets/CodeScannerWidget/widget/index.tsx index dc69fbc482..86b958fce2 100644 --- a/app/client/src/widgets/CodeScannerWidget/widget/index.tsx +++ b/app/client/src/widgets/CodeScannerWidget/widget/index.tsx @@ -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} /> diff --git a/app/client/src/widgets/CodeScannerWidget/widget/propertyConfig/contentConfig.ts b/app/client/src/widgets/CodeScannerWidget/widget/propertyConfig/contentConfig.ts index a9b1714a9f..d1df555e74 100644 --- a/app/client/src/widgets/CodeScannerWidget/widget/propertyConfig/contentConfig.ts +++ b/app/client/src/widgets/CodeScannerWidget/widget/propertyConfig/contentConfig.ts @@ -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"], }, ], }, diff --git a/app/client/src/widgets/CodeScannerWidget/widget/propertyConfig/styleConfig.ts b/app/client/src/widgets/CodeScannerWidget/widget/propertyConfig/styleConfig.ts index b8fd4cd2ce..6bab5d77b1 100644 --- a/app/client/src/widgets/CodeScannerWidget/widget/propertyConfig/styleConfig.ts +++ b/app/client/src/widgets/CodeScannerWidget/widget/propertyConfig/styleConfig.ts @@ -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"], }, ], }, diff --git a/app/client/yarn.lock b/app/client/yarn.lock index 783b471f25..d8bce1deb9 100644 --- a/app/client/yarn.lock +++ b/app/client/yarn.lock @@ -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"