diff --git a/.github/actions/create-native-bundle/action.yml b/.github/actions/create-native-bundle/action.yml index 4ae5ea850..227ac6cfa 100644 --- a/.github/actions/create-native-bundle/action.yml +++ b/.github/actions/create-native-bundle/action.yml @@ -23,16 +23,16 @@ runs: with: args: unzip -qq project.zip - name: "Rename extracted directory" - run: mv Native-Mobile-Resources-mx-version-10 Native-Mobile-Resources-mx10 + run: mv Native-Mobile-Resources-mx-version-10 NMR-mx10 shell: bash - name: "Extract deployment package" uses: montudor/action-zip@v1.0.0 with: - args: unzip -qq ${{ inputs.mda-file }} -d Native-Mobile-Resources-mx10/deployment + args: unzip -qq ${{ inputs.mda-file }} -d NMR-mx10/deployment - name: "Create bundle for ${{ inputs.platform }}" run: | mkdir -p ${{ inputs.platform }}/assets - cd Native-Mobile-Resources-mx10/deployment/native && \ + cd NMR-mx10/deployment/native && \ /tmp/mxbuild/modeler/tools/node/linux-x64/node \ /tmp/mxbuild/modeler/tools/node/node_modules/react-native/cli.js \ bundle --verbose --platform ${{ inputs.platform }} --dev false \ diff --git a/.github/scripts/determine-widget-scope.sh b/.github/scripts/determine-widget-scope.sh index 08bd9b0bf..4983cd507 100644 --- a/.github/scripts/determine-widget-scope.sh +++ b/.github/scripts/determine-widget-scope.sh @@ -7,6 +7,7 @@ input_workspace="$2" before_commit="$3" current_commit="$4" +# List of all native widgets # Dynamically discover all native widgets from the packages/pluggableWidgets directory # This ensures we don't miss any widgets when new ones are added script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" @@ -43,39 +44,65 @@ if [ "$event_name" == "pull_request" ]; then selected_workspaces=$(echo $selected_workspaces | xargs) # Build the final scope and widgets output + # Note: widgets output is used for both BUILDING and the widget TEST MATRIX + # When only JS actions change, widgets_to_test is empty (no widget tests needed) + # but we still need to build all widgets for the test project if [[ -n "$selected_workspaces" ]] && [[ "$js_actions_changed" == "true" ]]; then # Both widgets and JS actions changed # Convert space-separated widget names to JSON array format widget_array=$(echo "$selected_workspaces" | sed 's/ /","/g') echo "scope=--all --include '$selected_workspaces mobile-resources-native nanoflow-actions-native'" >> $GITHUB_OUTPUT - echo "widgets=[\"$widget_array\",\"mobile-resources-native\",\"nanoflow-actions-native\"]" >> $GITHUB_OUTPUT + echo "widgets=[\"$widget_array\"]" >> $GITHUB_OUTPUT + echo "widgets_to_test=[\"$widget_array\"]" >> $GITHUB_OUTPUT + echo "js_actions_changed=true" >> $GITHUB_OUTPUT elif [[ -n "$selected_workspaces" ]] && [[ "$js_actions_changed" == "false" ]]; then # Only widgets changed widget_array=$(echo "$selected_workspaces" | sed 's/ /","/g') echo "scope=--all --include '$selected_workspaces'" >> $GITHUB_OUTPUT echo "widgets=[\"$widget_array\"]" >> $GITHUB_OUTPUT + echo "widgets_to_test=[\"$widget_array\"]" >> $GITHUB_OUTPUT + echo "js_actions_changed=false" >> $GITHUB_OUTPUT elif [[ -z "$selected_workspaces" ]] && [[ "$js_actions_changed" == "true" ]]; then - # Only JS actions changed - echo "scope=--all --include 'mobile-resources-native nanoflow-actions-native'" >> $GITHUB_OUTPUT - echo "widgets=[\"mobile-resources-native\",\"nanoflow-actions-native\"]" >> $GITHUB_OUTPUT + # Only JS actions changed - need to build ALL widgets because JS action tests + # require the full test project with all widgets to function properly + # But widget tests should NOT run (empty widgets_to_test) + echo "scope=--all --include '*-native mobile-resources-native nanoflow-actions-native'" >> $GITHUB_OUTPUT + echo "widgets=${all_widgets}" >> $GITHUB_OUTPUT + echo "widgets_to_test=[]" >> $GITHUB_OUTPUT + echo "js_actions_changed=true" >> $GITHUB_OUTPUT else # No specific changes detected in widgets or JS actions, run everything echo "scope=--all --include '*-native mobile-resources-native nanoflow-actions-native'" >> $GITHUB_OUTPUT - echo "widgets=${all_widgets_and_js}" >> $GITHUB_OUTPUT + echo "widgets=${all_widgets}" >> $GITHUB_OUTPUT + echo "widgets_to_test=${all_widgets}" >> $GITHUB_OUTPUT + echo "js_actions_changed=true" >> $GITHUB_OUTPUT fi else if [ -n "$input_workspace" ] && [ "$input_workspace" != "*-native" ] && [ "$input_workspace" != "js-actions" ]; then + # Specific widget(s) selected selected_workspaces=$(echo "$input_workspace" | sed 's/,/ /g') echo "scope=--all --include '${selected_workspaces}'" >> $GITHUB_OUTPUT echo "widgets=[\"$input_workspace\"]" >> $GITHUB_OUTPUT + echo "widgets_to_test=[\"$input_workspace\"]" >> $GITHUB_OUTPUT + echo "js_actions_changed=false" >> $GITHUB_OUTPUT elif [ "$input_workspace" == "js-actions" ]; then - echo "scope=--all --include 'mobile-resources-native nanoflow-actions-native'" >> $GITHUB_OUTPUT - echo "widgets=[\"mobile-resources-native\",\"nanoflow-actions-native\"]" >> $GITHUB_OUTPUT + # JS actions selected - need to build ALL widgets because JS action tests + # require the full test project with all widgets to function properly + # But widget tests should NOT run (empty widgets_to_test) + echo "scope=--all --include '*-native mobile-resources-native nanoflow-actions-native'" >> $GITHUB_OUTPUT + echo "widgets=${all_widgets}" >> $GITHUB_OUTPUT + echo "widgets_to_test=[]" >> $GITHUB_OUTPUT + echo "js_actions_changed=true" >> $GITHUB_OUTPUT else + # All widgets (*-native) or default - run everything echo "scope=--all --include '*-native mobile-resources-native nanoflow-actions-native'" >> $GITHUB_OUTPUT - echo "widgets=${all_widgets_and_js}" >> $GITHUB_OUTPUT + echo "widgets=${all_widgets}" >> $GITHUB_OUTPUT + echo "widgets_to_test=${all_widgets}" >> $GITHUB_OUTPUT + echo "js_actions_changed=true" >> $GITHUB_OUTPUT fi fi -echo "Determined scope: $(cat $GITHUB_OUTPUT | grep scope= | cut -d= -f2)" -echo "Widgets: $(cat $GITHUB_OUTPUT | grep widgets= | cut -d= -f2)" \ No newline at end of file +echo "Determined scope: $(cat $GITHUB_OUTPUT | grep 'scope=' | cut -d= -f2)" +echo "Widgets to build: $(cat $GITHUB_OUTPUT | grep '^widgets=' | cut -d= -f2)" +echo "Widgets to test: $(cat $GITHUB_OUTPUT | grep 'widgets_to_test=' | cut -d= -f2)" +echo "JS actions changed: $(cat $GITHUB_OUTPUT | grep 'js_actions_changed=' | cut -d= -f2)" \ No newline at end of file diff --git a/.github/workflows/NativePipeline.yml b/.github/workflows/NativePipeline.yml index f8b9bd203..5d67363ca 100644 --- a/.github/workflows/NativePipeline.yml +++ b/.github/workflows/NativePipeline.yml @@ -88,6 +88,8 @@ jobs: outputs: scope: ${{ steps.scope.outputs.scope }} widgets: ${{ steps.scope.outputs.widgets }} + widgets_to_test: ${{ steps.scope.outputs.widgets_to_test }} + js_actions_changed: ${{ steps.scope.outputs.js_actions_changed }} steps: - name: "Check out code" uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 @@ -101,7 +103,9 @@ jobs: - name: "Debug Scope Output" run: | echo "Scope is: ${{ steps.scope.outputs.scope }}" - echo "Widgets or js actions are: ${{ steps.scope.outputs.widgets }}" + echo "Widgets to build: ${{ steps.scope.outputs.widgets }}" + echo "Widgets to test: ${{ steps.scope.outputs.widgets_to_test }}" + echo "JS actions changed: ${{ steps.scope.outputs.js_actions_changed }}" mendix-version: runs-on: ubuntu-24.04 outputs: @@ -214,23 +218,20 @@ jobs: run: yarn install --immutable - name: "Force rebuild resources" run: | - # Build JS actions if needed - if [ "${{ github.event.inputs.workspace }}" = "js-actions" ] || \ + # Build JS actions if needed (when js_actions_changed is true OR when workspace explicitly includes them) + if [ "${{ needs.scope.outputs.js_actions_changed }}" = "true" ] || \ + [ "${{ github.event.inputs.workspace }}" = "js-actions" ] || \ [ "${{ github.event.inputs.workspace }}" = "*-native" ] || \ [ "${{ github.event_name }}" = "schedule" ]; then yarn workspace mobile-resources-native run build yarn workspace nanoflow-actions-native run build fi - # Build widgets if needed (any specific widget, *-native, or nightly) - if [ "${{ github.event.inputs.workspace }}" != "js-actions" ] || \ - [ "${{ github.event.inputs.workspace }}" = "*-native" ] || \ - [ "${{ github.event_name }}" = "schedule" ]; then + # Build widgets from scope widgets=$(echo '${{ needs.scope.outputs.widgets }}' | jq -r '.[]') for w in $widgets; do yarn workspace $w run build done - fi - name: "Unit test" run: yarn workspaces foreach ${{ needs.scope.outputs.scope }} run test - name: "Upload JS actions resources artifact" @@ -269,8 +270,8 @@ jobs: uses: montudor/action-zip@0852c26906e00f8a315c704958823928d8018b28 # v1.0.0 with: args: unzip -qq project.zip -d . - - name: "Rename extracted directory" # Doing this since mxbuild fails with - path too long - run: mv Native-Mobile-Resources-mx-version-10 Native-Mobile-Resources-mx10 + - name: "Rename test project directory" + run: mv Native-Mobile-Resources-mx-version-10 NMR-mx10 - name: "Download resources artifact" uses: actions/download-artifact@65a9edc5881444af0b9093a5e628f2fe47ea3b2e # v4.1.7 with: @@ -284,28 +285,28 @@ jobs: run: | if compgen -G 'resources/pluggableWidgets/**/dist/*/*.mpk' > /dev/null; then for oldPath in resources/pluggableWidgets/**/dist/*/*.mpk; do - newPath=Native-Mobile-Resources-mx10/widgets/$(basename $oldPath) + newPath=NMR-mx10/widgets/$(basename $oldPath) mv -f $oldPath $newPath done - mx update-widgets --loose-version-check Native-Mobile-Resources-mx10/NativeComponentsTestProject.mpr + mx update-widgets --loose-version-check NMR-mx10/NativeComponentsTestProject.mpr fi - name: "Move mobile-resources" shell: bash run: | if compgen -G 'resources/jsActions/mobile-resources-native/dist/*' > /dev/null; then - rm -rf Native-Mobile-Resources-mx10/javascriptsource/nativemobileresources/actions/node_modules - mv -f resources/jsActions/mobile-resources-native/dist/* Native-Mobile-Resources-mx10/javascriptsource/nativemobileresources/actions/ + rm -rf NMR-mx10/javascriptsource/nativemobileresources/actions/node_modules + mv -f resources/jsActions/mobile-resources-native/dist/* NMR-mx10/javascriptsource/nativemobileresources/actions/ fi - name: "Move nanoflow-actions" shell: bash run: | if compgen -G 'resources/jsActions/nanoflow-actions-native/dist/*' > /dev/null; then - rm -rf Native-Mobile-Resources-mx10/javascriptsource/nanoflowcommons/actions/node_modules - mv -f resources/jsActions/nanoflow-actions-native/dist/* Native-Mobile-Resources-mx10/javascriptsource/nanoflowcommons/actions/ + rm -rf NMR-mx10/javascriptsource/nanoflowcommons/actions/node_modules + mv -f resources/jsActions/nanoflow-actions-native/dist/* NMR-mx10/javascriptsource/nanoflowcommons/actions/ fi - name: "Force rebuild test project" run: | - mxbuild -o automation.mda --loose-version-check Native-Mobile-Resources-mx10/NativeComponentsTestProject.mpr + mxbuild -o automation.mda --loose-version-check NMR-mx10/NativeComponentsTestProject.mpr - name: "Upload MDA" uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 #v4 with: @@ -490,14 +491,14 @@ jobs: android-widget-tests: needs: [scope, mendix-version, project, android-app] - # Run if project succeeds and either android-app succeeds OR we're using custom artifacts (android-app was skipped) - if: ${{ (github.event.inputs.workspace != 'js-actions' || github.event_name == 'schedule') && always() && needs.project.result == 'success' && (needs.android-app.result == 'success' || needs.android-app.result == 'skipped') }} + # Run if widgets need testing (widgets_to_test is not empty) and project succeeds + if: ${{ needs.scope.outputs.widgets_to_test != '[]' && always() && needs.project.result == 'success' && (needs.android-app.result == 'success' || needs.android-app.result == 'skipped') }} runs-on: ubuntu-24.04 timeout-minutes: 60 strategy: max-parallel: 5 matrix: - widget: ${{ fromJson(needs.scope.outputs.widgets) }} + widget: ${{ fromJson(needs.scope.outputs.widgets_to_test) }} fail-fast: false steps: - name: "Check out code" @@ -566,14 +567,14 @@ jobs: ios-widget-tests: needs: [scope, mendix-version, project, ios-app] - # Run if project succeeds and either ios-app succeeds OR we're using custom artifacts (ios-app was skipped) - if: ${{ (github.event.inputs.workspace != 'js-actions' || github.event_name == 'schedule') && always() && needs.project.result == 'success' && (needs.ios-app.result == 'success' || needs.ios-app.result == 'skipped') }} + # Run if widgets need testing (widgets_to_test is not empty) and project succeeds + if: ${{ needs.scope.outputs.widgets_to_test != '[]' && always() && needs.project.result == 'success' && (needs.ios-app.result == 'success' || needs.ios-app.result == 'skipped') }} runs-on: macos-15 timeout-minutes: 60 strategy: max-parallel: 5 matrix: - widget: ${{ fromJson(needs.scope.outputs.widgets) }} + widget: ${{ fromJson(needs.scope.outputs.widgets_to_test) }} fail-fast: false steps: - name: "Force cleanup workspace" @@ -662,8 +663,8 @@ jobs: android-js-tests: needs: [scope, mendix-version, project, android-app] - # Run if project succeeds and either android-app succeeds OR we're using custom artifacts (android-app was skipped) - if: ${{ (github.event.inputs.workspace == '*-native' || github.event_name == 'schedule' || github.event.inputs.workspace == 'js-actions' || contains(needs.scope.outputs.widgets, 'mobile-resources-native') || contains(needs.scope.outputs.widgets, 'nanoflow-actions-native')) && always() && needs.project.result == 'success' && (needs.android-app.result == 'success' || needs.android-app.result == 'skipped') }} + # Run if JS actions changed and project succeeds and either android-app succeeds OR we're using custom artifacts (android-app was skipped) + if: ${{ needs.scope.outputs.js_actions_changed == 'true' && always() && needs.project.result == 'success' && (needs.android-app.result == 'success' || needs.android-app.result == 'skipped') }} runs-on: ubuntu-24.04 timeout-minutes: 90 steps: @@ -732,8 +733,8 @@ jobs: ios-js-tests: needs: [scope, mendix-version, project, ios-app] - # Run if project succeeds and either ios-app succeeds OR we're using custom artifacts (ios-app was skipped) - if: ${{ (github.event.inputs.workspace == '*-native' || github.event_name == 'schedule' || github.event.inputs.workspace == 'js-actions' || contains(needs.scope.outputs.widgets, 'mobile-resources-native') || contains(needs.scope.outputs.widgets, 'nanoflow-actions-native')) && always() && needs.project.result == 'success' && (needs.ios-app.result == 'success' || needs.ios-app.result == 'skipped') }} + # Run if JS actions changed and project succeeds and either ios-app succeeds OR we're using custom artifacts (ios-app was skipped) + if: ${{ needs.scope.outputs.js_actions_changed == 'true' && always() && needs.project.result == 'success' && (needs.ios-app.result == 'success' || needs.ios-app.result == 'skipped') }} runs-on: macos-15 timeout-minutes: 90 steps: @@ -830,7 +831,7 @@ jobs: - name: "Download Android screenshots" run: | - widgets=$(echo '${{ needs.scope.outputs.widgets }}' | jq -r '.[]') + widgets=$(echo '${{ needs.scope.outputs.widgets_to_test }}' | jq -r '.[]') mkdir -p images/actual/android/ for widget in $widgets; do echo "Downloading android-screenshots-${widget}" @@ -844,7 +845,7 @@ jobs: - name: "Download iOS screenshots" run: | - widgets=$(echo '${{ needs.scope.outputs.widgets }}' | jq -r '.[]') + widgets=$(echo '${{ needs.scope.outputs.widgets_to_test }}' | jq -r '.[]') mkdir -p images/actual/ios/ for widget in $widgets; do echo "Downloading ios-screenshots-${widget}" diff --git a/configs/e2e/mendix-versions.json b/configs/e2e/mendix-versions.json index 1bb0404c1..4a000998d 100644 --- a/configs/e2e/mendix-versions.json +++ b/configs/e2e/mendix-versions.json @@ -1,3 +1,3 @@ { - "latest": "10.24.4.77222" + "latest": "10.24.14.90436" } diff --git a/packages/jsActions/nanoflow-actions-native/CHANGELOG.md b/packages/jsActions/nanoflow-actions-native/CHANGELOG.md index 5bf32878f..b19e0f991 100644 --- a/packages/jsActions/nanoflow-actions-native/CHANGELOG.md +++ b/packages/jsActions/nanoflow-actions-native/CHANGELOG.md @@ -6,10 +6,13 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), ## [Unreleased] +### Fixed + +- We've fixed a synchronization issue with Base64 images generated by the Signature widget. + ## [5.2.0] Nanoflow Commons - 2026-1-23 - We've migrated from using @react-native-community/geolocation to react-native-permissions for handling location permissions. -- ## [5.1.6] Nanoflow Commons - 2025-12-05 diff --git a/packages/jsActions/nanoflow-actions-native/e2e/specs/maestro/Confirmation.yaml b/packages/jsActions/nanoflow-actions-native/e2e/specs/maestro/Confirmation.yaml index 7a9600401..37a06d42e 100644 --- a/packages/jsActions/nanoflow-actions-native/e2e/specs/maestro/Confirmation.yaml +++ b/packages/jsActions/nanoflow-actions-native/e2e/specs/maestro/Confirmation.yaml @@ -14,7 +14,9 @@ appId: "${APP_ID}" id: "container1" - inputText: "11" - tapOn: "Show confirmation " -- assertVisible: "Yolo" +- extendedWaitUntil: + visible: "Yolo" + timeout: 10000 - assertVisible: "11" - tapOn: "OK" - assertVisible: "Confirmed!" diff --git a/packages/jsActions/nanoflow-actions-native/src/other/Base64DecodeToImage.ts b/packages/jsActions/nanoflow-actions-native/src/other/Base64DecodeToImage.ts index f7243f73d..280defed3 100644 --- a/packages/jsActions/nanoflow-actions-native/src/other/Base64DecodeToImage.ts +++ b/packages/jsActions/nanoflow-actions-native/src/other/Base64DecodeToImage.ts @@ -6,6 +6,7 @@ // - the code between BEGIN EXTRA CODE and END EXTRA CODE // Other code you write will be lost the next time you deploy the project. import { Base64 } from "js-base64"; +import RNBlobUtil from "react-native-blob-util"; // BEGIN EXTRA CODE // END EXTRA CODE @@ -16,7 +17,7 @@ import { Base64 } from "js-base64"; * @param {MxObject} image * @returns {Promise.} */ -export function Base64DecodeToImage(base64: string, image: mendix.lib.MxObject): Promise { +export async function Base64DecodeToImage(base64: string, image: mendix.lib.MxObject): Promise { // BEGIN USER CODE if (!base64) { @@ -26,8 +27,57 @@ export function Base64DecodeToImage(base64: string, image: mendix.lib.MxObject): throw new Error("image should not be null"); } - const blob = new Blob([Base64.toUint8Array(base64)], { type: "image/png" }); + // Native platform + if (navigator && navigator.product === "ReactNative") { + try { + // Remove data URI prefix if present (e.g., "data:image/png;base64,") + let cleanBase64 = base64; + if (base64.includes(",")) { + cleanBase64 = base64.split(",")[1]; + } + + // Remove any whitespace/newlines + cleanBase64 = cleanBase64.replace(/\s/g, ""); + + // Validate base64 format + if (!/^[A-Za-z0-9+/]*={0,2}$/.test(cleanBase64)) { + throw new Error("Invalid base64 format"); + } + + // Create a temporary file path + const tempPath = `${RNBlobUtil.fs.dirs.CacheDir}/temp_image_${Date.now()}.png`; + + // Write Base64 data to a temporary file + await RNBlobUtil.fs.writeFile(tempPath, cleanBase64, "base64"); + // Fetch the file as a blob + const res = await fetch(`file://${tempPath}`); + const blob = await res.blob(); + + return new Promise((resolve, reject) => { + mx.data.saveDocument( + image.getGuid(), + "camera image", + {}, + blob, + () => { + RNBlobUtil.fs.unlink(tempPath).catch(() => {}); + resolve(true); + }, + error => { + RNBlobUtil.fs.unlink(tempPath).catch(() => {}); + reject(error); + } + ); + }); + } catch (error) { + console.error("Failed to decode base64 to image:", error); + return false; + } + } + + // Other platforms + const blob = new Blob([Base64.toUint8Array(base64)], { type: "image/png" }); return new Promise((resolve, reject) => { mx.data.saveDocument(image.getGuid(), "camera image", {}, blob, () => resolve(true), reject); });