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} + +