Skip to content

Refactor profile picture image cropper#3060

Open
samuelwei wants to merge 14 commits intodevelopfrom
refactor-image-cropper
Open

Refactor profile picture image cropper#3060
samuelwei wants to merge 14 commits intodevelopfrom
refactor-image-cropper

Conversation

@samuelwei
Copy link
Copy Markdown
Collaborator

@samuelwei samuelwei commented Apr 15, 2026

Type

  • Bugfix
  • Feature
  • Documentation
  • Refactoring (e.g. Style updates, Test implementation, etc.)
  • Other (please describe):

Checklist

  • Code updated to current develop branch head
  • Passes CI checks
  • Is a part of an issue
  • Tests added for the bugfix or newly implemented feature, describe below why if not
  • Changelog is updated
  • Documentation of code and features exists

Changes

  • Replace vue-cropperjs with cropperjs
  • Show profile image rounded during cropping and inside profile page
  • A11y: Preserve keyboard focus when adding, removing, reverting profile image changes
  • A11y: Add keyboard shortcuts (arrow keys) to modify image cropped area

Other information

Summary by CodeRabbit

  • Changes
    • Avatars now always render circular; file uploads default to JPG/PNG and show allowed formats with human-readable max-size hints; pause-image help shows resolution-only.
  • Style
    • Added CropperJS-specific styles for a consistent circular crop UI.
  • New Features
    • Keyboard/focus support for avatar cropper; new file-size formatting helper.
  • Tests
    • Frontend tests updated to use pixel-level image comparison and updated fixtures/translation keys.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 15, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • ✅ Review completed - (🔄 Check again to review again)

Walkthrough

Replaced the Vue wrapper with upstream CropperJS (dependency + scoped CSS), refactored profile image selector to instantiate/destroy Cropper programmatically, removed shape prop from avatar, migrated many file validation rules to Laravel's fluent File/Rule validators, added file-size helper and updated i18n/UX for file inputs.

Changes

Cohort / File(s) Summary
Dependency
package.json
Removed vue-cropperjs, added cropperjs runtime dependency.
Cropper styles
resources/css/app/_cropperjs.css, resources/css/app/app.css
Added CropperJS scoped CSS and imported it into the app stylesheet.
Profile image & avatar UI
resources/js/components/UserProfileImageSelector.vue, resources/js/components/UserAvatar.vue, resources/js/views/AdminUsersNew.vue
Replaced VueCropper with programmatic new Cropper(...), added cropper lifecycle/teardown, tightened file accept filters, removed shape prop and hardcoded avatar shape, removed direct CSS side-effect import.
File input UX / helpers
resources/js/composables/useFileHelpers.js, resources/js/components/SettingsFileSelector.vue, resources/js/components/SettingsImageSelector.vue, resources/js/components/RoomTabFilesUploadButton.vue, resources/js/components/RoomTabStreamingConfigButton.vue, resources/js/views/AdminSettings.vue, resources/js/views/AdminStreamingSettings.vue
Added useFileHelpers() with fileSize() formatting; surfaced allowed extensions and formatted max size in UI; adjusted accept bindings and numeric literal formatting.
Validation (Laravel requests)
app/Http/Requests/.../*.php
app/Http/Requests/StoreRoomFileRequest.php, app/Http/Requests/UpdateRoomStreamingConfigRequest.php, app/Http/Requests/UpdateRoomTypeStreamingSettingsRequest.php, app/Http/Requests/UpdateSettingsRequest.php, app/Http/Requests/UpdateStreamingSettingsRequest.php, app/Http/Requests/UserRequest.php
Replaced string-based image/mimes/max/extensions/dimensions rules with Illuminate\Validation\Rules\File and Rule::dimensions() fluent validators; added corresponding imports.
Config & translations
config/bigbluebutton.php, lang/en/app.php, lang/en/rooms.php, lang/en/admin.php
Changed allowed_file_mimes config from comma string to array; moved/renamed translation keys (validationfile, pause_image_formatpause_image_resolution); added crop aria strings.
Tests & fixtures
tests/.../ApplicationTest.php, tests/.../SettingsTest.php, tests/Frontend/fixtures/config.json, tests/Frontend/e2e/RoomsViewFilesFileActions.cy.js
Updated tests and fixtures to expect bbb.file_mimes as an array and adjusted i18n assertions/size formatting.
Minor UI tweak
resources/js/components/UserTabProfile.vue
Changed uploaded cropped image filename from image.pngimage.jpg when appending blob to FormData.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Suggested labels

frontend, refactor, UI

Suggested reviewers

  • Sabr1n4W
  • danielmachill
🚥 Pre-merge checks | ✅ 1 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Description check ⚠️ Warning The PR description is incomplete. Required sections are missing or unmarked, including issue reference, CI checks status, issue linkage, tests documentation, changelog update, and feature documentation. Fill in the 'Fixes #' section with issue numbers, mark completed checklist items, verify CI passes, document tests added/why not needed, confirm changelog/documentation updates.
✅ Passed checks (1 passed)
Check name Status Explanation
Title check ✅ Passed The title 'Refactor profile picture image cropper' is clear, concise, and accurately summarizes the main change from the developer's perspective—replacing the image cropper library.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch refactor-image-cropper

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
resources/js/components/UserProfileImageSelector.vue (1)

173-177: ⚠️ Potential issue | 🟡 Minor

Verify fillColor value.

The fillColor: "#ffff" appears to be a typo. Valid CSS color values are "#fff" (3-digit) or "#ffffff" (6-digit). While some parsers may accept 4-digit hex as RGBA shorthand, this could cause unexpected transparency.

🔧 Proposed fix
   const oc = cropper.value.getCroppedCanvas({
     width: 100,
     height: 100,
-    fillColor: "#ffff",
+    fillColor: "#fff",
   });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@resources/js/components/UserProfileImageSelector.vue` around lines 173 - 177,
The fillColor passed to cropper.value.getCroppedCanvas uses an invalid/ambiguous
hex "#ffff"; update the call in the code that constructs oc
(cropper.value.getCroppedCanvas) to use a valid CSS hex like "#fff" or "#ffffff"
(choose one consistent with your color scheme) so the generated canvas
background is deterministic and not accidentally transparent.
🧹 Nitpick comments (1)
resources/js/components/UserProfileImageSelector.vue (1)

112-119: Use template ref instead of document.getElementById.

Using document.getElementById is not idiomatic in Vue and can cause issues if multiple instances of this component exist simultaneously. Consider using a template ref instead.

♻️ Proposed fix using template ref

Update the template:

-      <img
-        :src="selectedFile"
-        width="100%"
-        id="cropper"
-        :alt="$t('admin.users.image.title')"
-      />
+      <img
+        ref="cropperImageRef"
+        :src="selectedFile"
+        width="100%"
+        :alt="$t('admin.users.image.title')"
+      />

Update the script:

 const cropper = ref();
+const cropperImageRef = ref(null);
-    cropper.value = new Cropper(document.getElementById("cropper"), {
+    cropper.value = new Cropper(cropperImageRef.value, {

Additionally, the static analysis tool indicates that id should go before width if you keep using the id attribute.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@resources/js/components/UserProfileImageSelector.vue` around lines 112 - 119,
The template currently relies on an element id ("cropper") and likely uses
document.getElementById to access the image, which is not idiomatic and breaks
with multiple component instances; change the <img> to use a Vue template ref
(e.g., ref="cropperImg") and update any code that calls
document.getElementById("cropper") to use this.$refs.cropperImg (or the
Composition API ref) instead; also remove or keep the id attribute (preferably
remove it) or if kept follow static analysis ordering by placing id before width
in the <img> attributes. Ensure references to "selectedFile" remain unchanged
and update any methods that manipulate the cropper DOM to use the ref.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In `@resources/js/components/UserProfileImageSelector.vue`:
- Around line 173-177: The fillColor passed to cropper.value.getCroppedCanvas
uses an invalid/ambiguous hex "#ffff"; update the call in the code that
constructs oc (cropper.value.getCroppedCanvas) to use a valid CSS hex like
"#fff" or "#ffffff" (choose one consistent with your color scheme) so the
generated canvas background is deterministic and not accidentally transparent.

---

Nitpick comments:
In `@resources/js/components/UserProfileImageSelector.vue`:
- Around line 112-119: The template currently relies on an element id
("cropper") and likely uses document.getElementById to access the image, which
is not idiomatic and breaks with multiple component instances; change the <img>
to use a Vue template ref (e.g., ref="cropperImg") and update any code that
calls document.getElementById("cropper") to use this.$refs.cropperImg (or the
Composition API ref) instead; also remove or keep the id attribute (preferably
remove it) or if kept follow static analysis ordering by placing id before width
in the <img> attributes. Ensure references to "selectedFile" remain unchanged
and update any methods that manipulate the cropper DOM to use the ref.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 7ed00fd2-f410-40b5-bbc4-122b8cefb1ce

📥 Commits

Reviewing files that changed from the base of the PR and between e086f05 and e0a7d52.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (6)
  • package.json
  • resources/css/app/_cropperjs.css
  • resources/css/app/app.css
  • resources/js/components/UserAvatar.vue
  • resources/js/components/UserProfileImageSelector.vue
  • resources/js/views/AdminUsersNew.vue
💤 Files with no reviewable changes (1)
  • resources/js/views/AdminUsersNew.vue

@codecov
Copy link
Copy Markdown

codecov Bot commented Apr 15, 2026

Codecov Report

❌ Patch coverage is 51.70732% with 99 lines in your changes missing coverage. Please review.
✅ Project coverage is 96.02%. Comparing base (bf0c41d) to head (48e1aec).

Files with missing lines Patch % Lines
...sources/js/components/UserProfileImageSelector.vue 43.27% 97 Missing ⚠️
resources/js/composables/useFileHelpers.js 83.33% 2 Missing ⚠️
Additional details and impacted files
@@              Coverage Diff              @@
##             develop    #3060      +/-   ##
=============================================
- Coverage      96.73%   96.02%   -0.72%     
  Complexity      1924     1924              
=============================================
  Files            457      458       +1     
  Lines          13002    13175     +173     
  Branches        2088     2118      +30     
=============================================
+ Hits           12578    12651      +73     
- Misses           424      524     +100     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@cypress
Copy link
Copy Markdown

cypress Bot commented Apr 15, 2026

PILOS    Run #2976

Run Properties:  status check failed Failed #2976  •  git commit 48e1aecb03: Refactor profile picture image cropper
Project PILOS
Branch Review refactor-image-cropper
Run status status check failed Failed #2976
Run duration 07m 05s
Commit git commit 48e1aecb03: Refactor profile picture image cropper
Committer Samuel Weirich
View all properties for this run ↗︎

Test results
Tests that failed  Failures 1
Tests that were flaky  Flaky 0
Tests that did not run due to a developer annotating a test with .skip  Pending 0
Tests that did not run due to a failure in a mocha hook  Skipped 0
Tests that passed  Passing 621
View all changes introduced in this branch ↗︎

Tests for review

Failed  e2e/RoomsJoinWithLobby.cy.js • 1 failed test • System tests

View Output

Test Artifacts
Room Join with lobby settings > Lobby enabled for guests only Test Replay Screenshots

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
resources/js/components/UserProfileImageSelector.vue (1)

173-177: ⚠️ Potential issue | 🟡 Minor

Likely typo in fillColor value.

"#ffff" is 4 hex digits. This is likely meant to be "#fff" (3 digits) or "#ffffff" (6 digits) for white. While 4-digit hex is valid CSS (RGBA shorthand), it's unusual here and may not be intentional.

🔧 Proposed fix
   const oc = cropper.value.getCroppedCanvas({
     width: 100,
     height: 100,
-    fillColor: "#ffff",
+    fillColor: "#fff",
   });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@resources/js/components/UserProfileImageSelector.vue` around lines 173 - 177,
In the getCroppedCanvas call where oc is created, fix the fillColor value which
is currently "#ffff" (likely a typo); update the fillColor option passed to
cropper.value.getCroppedCanvas to a proper white hex such as "#fff" or "#ffffff"
(refer to the oc variable and the getCroppedCanvas invocation) so the canvas is
filled with the intended white color.
🧹 Nitpick comments (2)
resources/js/components/UserProfileImageSelector.vue (2)

205-237: Consider adding error handling for robustness.

If file reading fails or the Cropper fails to initialize, isLoadingAction will remain true, leaving the UI in a stuck state. Consider adding error handling.

♻️ Suggested error handling pattern
 async function onFileSelect(event) {
   modalVisible.value = true;
   isLoadingAction.value = true;
   const file = event.files[0];

   const reader = new FileReader();
+  reader.onerror = () => {
+    isLoadingAction.value = false;
+    closeModal();
+    // Optionally show error toast/notification
+  };
   reader.onload = async (event) => {
     selectedFile.value = event.target.result;

     await nextTick();

-    cropper.value = new Cropper(document.getElementById("cropper"), {
+    try {
+      cropper.value = new Cropper(cropperImg.value, {
         // ... options
-    });
+      });
+    } catch (e) {
+      isLoadingAction.value = false;
+      closeModal();
+    }
   };
   reader.readAsDataURL(file);
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@resources/js/components/UserProfileImageSelector.vue` around lines 205 - 237,
The onFileSelect handler can leave isLoadingAction true if file reading or
Cropper initialization fails; add try/catch around the async work (FileReader
read and Cropper creation) so any thrown error sets isLoadingAction.value =
false and modalVisible.value = false (or closes the modal) and logs or surfaces
the error; specifically wrap the FileReader onload handler and the new
Cropper(...) creation/ready logic to catch initialization errors, ensure
selectedFile.value is only set on success, and in the catch block clean up
cropper.value if partially created and call
document.querySelector('[data-test="dialog-cancel-button"]').focus() (or
appropriate UI reset) to avoid a stuck loading state.

227-233: Consider using a template ref for focus management.

document.querySelector('[data-test="dialog-cancel-button"]') queries the entire document, which could select the wrong element if multiple instances exist. Consider adding a template ref to the cancel button for scoped access.

♻️ Proposed approach

Add a ref to the cancel button:

         <Button
+          ref="cancelButton"
           :label="$t('app.cancel')"
           severity="secondary"

In script:

+const cancelButton = ref(null);

Then focus using the ref:

-        document.querySelector('[data-test="dialog-cancel-button"]').focus();
+        cancelButton.value?.$el?.focus();
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@resources/js/components/UserProfileImageSelector.vue` around lines 227 - 233,
The ready method currently uses
document.querySelector('[data-test="dialog-cancel-button"]') which can collide
across instances; add a template ref (e.g., cancelButtonRef) to the cancel
button in the component template and replace the global query in the ready()
function with a scoped ref access: after await nextTick(), check
cancelButtonRef.value and call .focus() on it (with a null guard). Update any
related imports/`ref` declarations in the script setup so cancelButtonRef is
reactive and accessible in ready().
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@resources/js/components/UserProfileImageSelector.vue`:
- Around line 112-119: Replace the hardcoded id usage for the image element and
any document.getElementById("cropper") calls with a Vue template ref (e.g.,
ref="cropperRef") so DOM access is scoped per-component; update the template
image tag that uses selectedFile to remove the id and add the ref, and update
the Cropper initialization code (wherever Cropper is constructed) to use the
template ref (this.$refs.cropperRef or the setup/ref equivalent) instead of
getElementById; apply the same change to the other occurrence noted (line ~216),
and ensure attribute ordering no longer relies on an id being present.

---

Outside diff comments:
In `@resources/js/components/UserProfileImageSelector.vue`:
- Around line 173-177: In the getCroppedCanvas call where oc is created, fix the
fillColor value which is currently "#ffff" (likely a typo); update the fillColor
option passed to cropper.value.getCroppedCanvas to a proper white hex such as
"#fff" or "#ffffff" (refer to the oc variable and the getCroppedCanvas
invocation) so the canvas is filled with the intended white color.

---

Nitpick comments:
In `@resources/js/components/UserProfileImageSelector.vue`:
- Around line 205-237: The onFileSelect handler can leave isLoadingAction true
if file reading or Cropper initialization fails; add try/catch around the async
work (FileReader read and Cropper creation) so any thrown error sets
isLoadingAction.value = false and modalVisible.value = false (or closes the
modal) and logs or surfaces the error; specifically wrap the FileReader onload
handler and the new Cropper(...) creation/ready logic to catch initialization
errors, ensure selectedFile.value is only set on success, and in the catch block
clean up cropper.value if partially created and call
document.querySelector('[data-test="dialog-cancel-button"]').focus() (or
appropriate UI reset) to avoid a stuck loading state.
- Around line 227-233: The ready method currently uses
document.querySelector('[data-test="dialog-cancel-button"]') which can collide
across instances; add a template ref (e.g., cancelButtonRef) to the cancel
button in the component template and replace the global query in the ready()
function with a scoped ref access: after await nextTick(), check
cancelButtonRef.value and call .focus() on it (with a null guard). Update any
related imports/`ref` declarations in the script setup so cancelButtonRef is
reactive and accessible in ready().
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: ae44d204-0039-410e-812d-e0a1512e6be0

📥 Commits

Reviewing files that changed from the base of the PR and between e0a7d52 and 63a8c5c.

📒 Files selected for processing (2)
  • resources/css/app/_cropperjs.css
  • resources/js/components/UserProfileImageSelector.vue
✅ Files skipped from review due to trivial changes (1)
  • resources/css/app/_cropperjs.css

Comment thread resources/js/components/UserProfileImageSelector.vue Outdated
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
tests/Frontend/e2e/RoomsViewFilesFileActions.cy.js (1)

20-27: ⚠️ Potential issue | 🟠 Major

Fix the expected max-size interpolation string.

Line 26 won’t match the usual i18n fallback format because the size value should be quoted.

🧪 Proposed test fix
-      .should("include.text", 'app.file.max_size_{"size":30 MB')
+      .should("include.text", 'app.file.max_size_{"size":"30 MB"}')
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/Frontend/e2e/RoomsViewFilesFileActions.cy.js` around lines 20 - 27, The
assertion expecting the i18n fallback for max size is missing quotes around the
interpolated size; update the check on the
cy.get('[data-test="room-files-upload-dialog"]') chain so the expected string is
'app.file.max_size_{"size":"30 MB"}' (i.e., add surrounding quotes for the size
value and include the closing brace) so the .should("include.text", ...) matches
the usual i18n format.
resources/js/views/AdminStreamingSettings.vue (1)

52-64: ⚠️ Potential issue | 🟡 Minor

Align CSS file upload limit with backend validation.

The backend enforces a 500 KB maximum for CSS files (app/Http/Requests/UpdateStreamingSettingsRequest.php line: max('500kb')), but the frontend allows 5 MB. This mismatch will cause file upload failures for users who select valid files under the frontend limit but over the backend limit. The default pause image (correctly set to 5 MB on both frontend and backend) shows this was likely unintentional.

Proposed fix
-                  :max-file-size="5_000_000"
+                  :max-file-size="500_000"
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@resources/js/views/AdminStreamingSettings.vue` around lines 52 - 64, The CSS
file upload limit in the SettingsFileSelector for streaming CSS is out of sync
with backend validation: change the max-file-size passed to the
SettingsFileSelector (the prop currently set via
v-model/file-url="settings.css_file" and related bindings) from 5_000_000 to
500_000 so the frontend enforces the same 500 KB limit as
UpdateStreamingSettingsRequest; leave the pause image/upload limit unchanged if
it already uses 5_000_000.
🧹 Nitpick comments (3)
resources/js/components/UserProfileImageSelector.vue (1)

214-243: Handle FileReader error / cropper init failure.

If reader fails (or new Cropper(...) throws synchronously before ready fires), isLoadingAction stays true and the modal remains open with a spinner and no way to recover other than the cancel button. Add an onerror handler that closes the modal, and consider wrapping the Cropper construction in try/catch to reset loading state.

♻️ Proposed fix
   const reader = new FileReader();
   reader.onload = async (event) => {
     selectedFile.value = event.target.result;

     await nextTick();

-    cropper.value = new Cropper(cropperImgRef.value, {
+    try {
+      cropper.value = new Cropper(cropperImgRef.value, {
         aspectRatio: 1,
         autoCropArea: 0.9,
         background: false,
         guides: false,
         center: false,
         rotatable: false,
         zoomable: false,
         movable: false,
         viewMode: 1,
         dragMode: "none",
         ready: async function () {
           isLoadingAction.value = false;
         },
-    });
+      });
+    } catch (e) {
+      isLoadingAction.value = false;
+      closeModal();
+      throw e;
+    }
   };
+  reader.onerror = () => {
+    isLoadingAction.value = false;
+    closeModal();
+  };
   reader.readAsDataURL(file);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@resources/js/components/UserProfileImageSelector.vue` around lines 214 - 243,
The onFileSelect handler currently sets modalVisible and isLoadingAction but
lacks error handling; add a reader.onerror callback that sets
isLoadingAction.value = false and modalVisible.value = false (or shows an error
state) and logs the error, and wrap the new Cropper(...) instantiation inside a
try/catch in onFileSelect (or inside reader.onload) so that if Cropper throws
synchronously you also reset isLoadingAction.value and modalVisible.value and
clean up any partially created cropper.value; reference reader, reader.onerror,
onFileSelect, cropper.value, Cropper, isLoadingAction.value and
modalVisible.value when implementing.
config/bigbluebutton.php (1)

13-13: Handle empty/whitespace env values when parsing BBB_ALLOWED_FILE_MIMES.

explode(',', '') yields [''] and tokens aren’t trimmed, so an empty or whitespace-padded override silently produces invalid entries that downstream File::types(...)/extensions(...) will then include in the allowed list.

♻️ Proposed fix
-    'allowed_file_mimes' => explode(',', env('BBB_ALLOWED_FILE_MIMES', 'pdf,doc,docx,xls,xlsx,ppt,pptx,txt,rtf,odt,ods,odp,odg,odc,odi,jpg,jpeg,png')),
+    'allowed_file_mimes' => array_values(array_filter(array_map('trim', explode(',', env('BBB_ALLOWED_FILE_MIMES', 'pdf,doc,docx,xls,xlsx,ppt,pptx,txt,rtf,odt,ods,odp,odg,odc,odi,jpg,jpeg,png'))), 'strlen')),
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@config/bigbluebutton.php` at line 13, The config currently uses explode(',',
env('BBB_ALLOWED_FILE_MIMES', '...')) which yields [''] for empty env and
doesn't trim tokens; update the allowed_file_mimes parsing to split on commas,
trim each token, and filter out empty/whitespace-only entries (e.g., via
array_map('trim') + array_filter or preg_split with PREG_SPLIT_NO_EMPTY and
trim) so that allowed_file_mimes contains only valid non-empty extensions before
passing to File::types(...) / extensions(...).
resources/js/views/AdminSettings.vue (1)

1471-1477: Add a computed normalizer for bbb.file_mimes before passing to SettingsFileSelector.

While the backend always sends this setting as an array via explode() in config/bigbluebutton.php, the Visual test fixture contains a comma-separated string. Since SettingsFileSelector requires an array (uses .join() on the prop), add defensive normalization here—the only place in the codebase where this setting is dynamically bound to the component:

♻️ Proposed normalization
+                  :allowed-extensions="bbbFileMimes"
-                  :allowed-extensions="
-                    settingsStore.getSetting('bbb.file_mimes')
-                  "

Add near the other computed values:

const bbbFileMimes = computed(() => {
  const value = settingsStore.getSetting("bbb.file_mimes");

  if (Array.isArray(value)) {
    return value;
  }

  return String(value ?? "")
    .split(",")
    .map((extension) => extension.trim())
    .filter(Boolean);
});
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@resources/js/views/AdminSettings.vue` around lines 1471 - 1477, Add a
defensive computed normalizer named bbbFileMimes that reads
settingsStore.getSetting('bbb.file_mimes') and returns an array (if already an
array return it; otherwise split the string on commas, trim each entry and
filter out empties), then replace the direct binding to
settingsStore.getSetting('bbb.file_mimes') passed into SettingsFileSelector with
the computed bbbFileMimes so the prop always receives an array compatible with
SettingsFileSelector (which uses .join()).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@lang/en/app.php`:
- Around line 144-149: The localization moved file-related messages under
app.file which breaks existing callers expecting app.validation.too_large and
app.validation.invalid_type; either restore alias keys under app.validation
(e.g., add validation.too_large and validation.invalid_type that reference the
same strings as file.too_large and file.invalid_type) or update the callers
(SettingsFileSelector.vue, SettingsImageSelector.vue and any E2E tests
referencing app.validation.too_large) to use app.file.too_large and
app.file.invalid_type so no missing-key errors occur; choose the alias approach
if you want backward compatibility with minimal code changes.

In `@resources/js/components/UserProfileImageSelector.vue`:
- Around line 15-16: Change the attribute hyphenation and order to satisfy the
frontend style checks: rename the prop binding :invalidFileSizeMessage to
:invalid-file-size-message to match :invalid-file-type-message; in the element
that currently has `@click` and autofocus (around where autofocus is on line 104)
move the autofocus attribute to appear before the `@click` handler; and in the
element with ref and width (around line 119) move the ref attribute so it
appears before the width attribute. Ensure you update the exact attributes in
UserProfileImageSelector.vue (the elements using :invalid-file-size-message,
autofocus/@click, and ref/width) without altering other logic.
- Line 127: Remove the unused import useTemplateRef from the import list in
UserProfileImageSelector.vue; keep the existing refs (e.g., cropperImgRef
defined via const cropperImgRef = ref(null) and used as ref="cropperImgRef") and
only import nextTick, ref, and watch so the unused-symbol lint error is
resolved.

In `@resources/js/composables/useFileHelpers.js`:
- Around line 10-28: The fileSize function should validate and normalize inputs
and promote values that round up across unit boundaries: coerce sizeInBytes to a
Number and if it's NaN or <= 0 return "0 B"; keep the units array and compute
exponent as before, compute value = sizeInBytes / Math.pow(1000, exponent), then
if after rounding (e.g., value.toFixed(0)) the value would be >= 1000 and
exponent < units.length - 1, increment exponent and recompute value = value /
1000 so boundaries like 999_999 become "1 MB"; finally return the rounded value
with the units[exponent]. Ensure negative/invalid inputs never produce undefined
units by returning "0 B".

---

Outside diff comments:
In `@resources/js/views/AdminStreamingSettings.vue`:
- Around line 52-64: The CSS file upload limit in the SettingsFileSelector for
streaming CSS is out of sync with backend validation: change the max-file-size
passed to the SettingsFileSelector (the prop currently set via
v-model/file-url="settings.css_file" and related bindings) from 5_000_000 to
500_000 so the frontend enforces the same 500 KB limit as
UpdateStreamingSettingsRequest; leave the pause image/upload limit unchanged if
it already uses 5_000_000.

In `@tests/Frontend/e2e/RoomsViewFilesFileActions.cy.js`:
- Around line 20-27: The assertion expecting the i18n fallback for max size is
missing quotes around the interpolated size; update the check on the
cy.get('[data-test="room-files-upload-dialog"]') chain so the expected string is
'app.file.max_size_{"size":"30 MB"}' (i.e., add surrounding quotes for the size
value and include the closing brace) so the .should("include.text", ...) matches
the usual i18n format.

---

Nitpick comments:
In `@config/bigbluebutton.php`:
- Line 13: The config currently uses explode(',', env('BBB_ALLOWED_FILE_MIMES',
'...')) which yields [''] for empty env and doesn't trim tokens; update the
allowed_file_mimes parsing to split on commas, trim each token, and filter out
empty/whitespace-only entries (e.g., via array_map('trim') + array_filter or
preg_split with PREG_SPLIT_NO_EMPTY and trim) so that allowed_file_mimes
contains only valid non-empty extensions before passing to File::types(...) /
extensions(...).

In `@resources/js/components/UserProfileImageSelector.vue`:
- Around line 214-243: The onFileSelect handler currently sets modalVisible and
isLoadingAction but lacks error handling; add a reader.onerror callback that
sets isLoadingAction.value = false and modalVisible.value = false (or shows an
error state) and logs the error, and wrap the new Cropper(...) instantiation
inside a try/catch in onFileSelect (or inside reader.onload) so that if Cropper
throws synchronously you also reset isLoadingAction.value and modalVisible.value
and clean up any partially created cropper.value; reference reader,
reader.onerror, onFileSelect, cropper.value, Cropper, isLoadingAction.value and
modalVisible.value when implementing.

In `@resources/js/views/AdminSettings.vue`:
- Around line 1471-1477: Add a defensive computed normalizer named bbbFileMimes
that reads settingsStore.getSetting('bbb.file_mimes') and returns an array (if
already an array return it; otherwise split the string on commas, trim each
entry and filter out empties), then replace the direct binding to
settingsStore.getSetting('bbb.file_mimes') passed into SettingsFileSelector with
the computed bbbFileMimes so the prop always receives an array compatible with
SettingsFileSelector (which uses .join()).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 1a593afc-bcc8-481f-b052-495ec7270062

📥 Commits

Reviewing files that changed from the base of the PR and between 63a8c5c and 2f4acc0.

📒 Files selected for processing (22)
  • app/Http/Requests/StoreRoomFileRequest.php
  • app/Http/Requests/UpdateRoomStreamingConfigRequest.php
  • app/Http/Requests/UpdateRoomTypeStreamingSettingsRequest.php
  • app/Http/Requests/UpdateSettingsRequest.php
  • app/Http/Requests/UpdateStreamingSettingsRequest.php
  • app/Http/Requests/UserRequest.php
  • config/bigbluebutton.php
  • lang/en/app.php
  • lang/en/rooms.php
  • resources/js/components/AdminStreamingRoomTypeEditButton.vue
  • resources/js/components/RoomTabFilesUploadButton.vue
  • resources/js/components/RoomTabStreamingConfigButton.vue
  • resources/js/components/SettingsFileSelector.vue
  • resources/js/components/SettingsImageSelector.vue
  • resources/js/components/UserProfileImageSelector.vue
  • resources/js/composables/useFileHelpers.js
  • resources/js/views/AdminSettings.vue
  • resources/js/views/AdminStreamingSettings.vue
  • tests/Backend/Feature/api/v1/ApplicationTest.php
  • tests/Backend/Feature/api/v1/SettingsTest.php
  • tests/Frontend/e2e/RoomsViewFilesFileActions.cy.js
  • tests/Frontend/fixtures/config.json
✅ Files skipped from review due to trivial changes (2)
  • resources/js/components/AdminStreamingRoomTypeEditButton.vue
  • tests/Frontend/fixtures/config.json

Comment thread lang/en/app.php
Comment thread resources/js/components/UserProfileImageSelector.vue Outdated
Comment thread resources/js/components/UserProfileImageSelector.vue Outdated
Comment thread resources/js/composables/useFileHelpers.js Outdated
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

♻️ Duplicate comments (1)
resources/js/components/UserProfileImageSelector.vue (1)

15-15: ⚠️ Potential issue | 🔴 Critical

Resolve the still-present frontend lint/style failures.

Line 127 is a CI failure because useTemplateRef is unused; Lines 15, 104, and 119 still have the style warnings already reported.

🛠️ Proposed fix
-        :invalidFileSizeMessage="$t('app.validation.too_large')"
+        :invalid-file-size-message="$t('app.validation.too_large')"
           :disabled="isSavingAction"
           data-test="dialog-cancel-button"
-          `@click`="closeModal"
           autofocus
+          `@click`="closeModal"
       <img
         :src="selectedFile"
-        width="100%"
         ref="cropperImgRef"
+        width="100%"
         :alt="$t('admin.users.image.title')"
-import { nextTick, ref, useTemplateRef, watch } from "vue";
+import { nextTick, ref, watch } from "vue";

Also applies to: 103-104, 117-119, 127-127

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@resources/js/components/UserProfileImageSelector.vue` at line 15, Remove or
use the unused template helper and any other unused bindings reported by the
linter: either delete the unused import/variable useTemplateRef from the
component setup or actually use it where intended, and similarly remove or use
any other unused variables/computeds flagged at the same locations; also fix the
style warnings around the prop binding
:invalidFileSizeMessage="$t('app.validation.too_large')" by aligning it with the
project formatting (consistent spacing/line breaks) so the component passes
frontend linting.
🧹 Nitpick comments (1)
resources/js/components/UserProfileImageSelector.vue (1)

285-338: Scope handle lookup to this cropper instance.

document.getElementsByClassName(...)[0] can select another cropper’s handles or return undefined; the following setAttribute/addEventListener calls will then target the wrong DOM or throw.

♿ Safer scoped lookup pattern
 function initKeyboardShortcuts() {
   const dimensions = cropper.value.getData();
   scaleMove.value = Math.min(dimensions.width, dimensions.height) / 100;
+  const cropperRoot = cropper.value.container;
 
   // Get all handles
-  moveHandle.value = document.getElementsByClassName("cropper-move")[0];
-  topLeftHandle.value = document.getElementsByClassName(
-    "cropper-point point-nw",
-  )[0];
-  topRightHandle.value = document.getElementsByClassName(
-    "cropper-point point-ne",
-  )[0];
-  bottomLeftHandle.value = document.getElementsByClassName(
-    "cropper-point point-sw",
-  )[0];
-  bottomRightHandle.value = document.getElementsByClassName(
-    "cropper-point point-se",
-  )[0];
+  moveHandle.value = cropperRoot?.querySelector(".cropper-move");
+  topLeftHandle.value = cropperRoot?.querySelector(".cropper-point.point-nw");
+  topRightHandle.value = cropperRoot?.querySelector(".cropper-point.point-ne");
+  bottomLeftHandle.value = cropperRoot?.querySelector(".cropper-point.point-sw");
+  bottomRightHandle.value = cropperRoot?.querySelector(".cropper-point.point-se");
+
+  if (
+    !moveHandle.value ||
+    !topLeftHandle.value ||
+    !topRightHandle.value ||
+    !bottomLeftHandle.value ||
+    !bottomRightHandle.value
+  ) {
+    return;
+  }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@resources/js/components/UserProfileImageSelector.vue` around lines 285 - 338,
The element lookups in initKeyboardShortcuts currently use
document.getElementsByClassName(...) which can find handles from other cropper
instances or return undefined; change these to query the current cropper's
container (e.g., obtain the cropper DOM container from cropper.value via its API
or stored element, assign it to a local container variable) and call
container.getElementsByClassName(...) for moveHandle, topLeftHandle,
topRightHandle, bottomLeftHandle, bottomRightHandle; also guard each resulting
element (check for null/undefined) before calling setAttribute or
addEventListener so you never operate on the wrong or missing node.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@resources/js/components/UserProfileImageSelector.vue`:
- Around line 341-443: Each keyboard handler (moveEventListener,
topLeftEventListener, topRightEventListener, bottomLeftEventListener,
bottomRightEventListener) should early-return for non-arrow keys and call
e.preventDefault() when an Arrow key is handled so the page/dialog doesn't
scroll; update each function to first check if e.key startsWith "Arrow" (or
matches "ArrowUp"/"ArrowDown"/"ArrowLeft"/"ArrowRight"), call
e.preventDefault(), then run the existing switch logic and
cropper.value.setCropBoxData(cropDimensions).
- Around line 215-255: The onFileSelect/FileReader/Cropper path can finish after
the user cancels, so make the async cropper initialization cancel-safe: when
creating the FileReader and in reader.onload (inside onFileSelect), check that
modalVisible.value is still true, selectedFile.value still matches the file, and
cropperImgRef.value is non-null before calling new Cropper; also store the
FileReader instance so closeModal can call reader.abort() (or set a cancellation
flag) and ensure closeModal clears isLoadingAction.value and prevents any late
ready callback from initializing cropper.value or calling initKeyboardShortcuts;
update the ready handler to re-check modalVisible and cropperImgRef before
setting isLoadingAction/value or calling initKeyboardShortcuts.

---

Duplicate comments:
In `@resources/js/components/UserProfileImageSelector.vue`:
- Line 15: Remove or use the unused template helper and any other unused
bindings reported by the linter: either delete the unused import/variable
useTemplateRef from the component setup or actually use it where intended, and
similarly remove or use any other unused variables/computeds flagged at the same
locations; also fix the style warnings around the prop binding
:invalidFileSizeMessage="$t('app.validation.too_large')" by aligning it with the
project formatting (consistent spacing/line breaks) so the component passes
frontend linting.

---

Nitpick comments:
In `@resources/js/components/UserProfileImageSelector.vue`:
- Around line 285-338: The element lookups in initKeyboardShortcuts currently
use document.getElementsByClassName(...) which can find handles from other
cropper instances or return undefined; change these to query the current
cropper's container (e.g., obtain the cropper DOM container from cropper.value
via its API or stored element, assign it to a local container variable) and call
container.getElementsByClassName(...) for moveHandle, topLeftHandle,
topRightHandle, bottomLeftHandle, bottomRightHandle; also guard each resulting
element (check for null/undefined) before calling setAttribute or
addEventListener so you never operate on the wrong or missing node.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 3d0c47ee-a96b-4fde-abe9-b8867ff4a3c7

📥 Commits

Reviewing files that changed from the base of the PR and between 2f4acc0 and 4ef538c.

📒 Files selected for processing (3)
  • lang/en/admin.php
  • resources/js/components/UserProfileImageSelector.vue
  • resources/js/components/UserTabProfile.vue
✅ Files skipped from review due to trivial changes (1)
  • lang/en/admin.php

Comment thread resources/js/components/UserProfileImageSelector.vue
Comment thread resources/js/components/UserProfileImageSelector.vue
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

♻️ Duplicate comments (1)
resources/js/components/UserProfileImageSelector.vue (1)

164-165: ⚠️ Potential issue | 🟠 Major

Make cropper initialization cancel-safe.

If the dialog is cancelled while FileReader, nextTick, or Cropper’s ready callback is still pending, stale callbacks can create a Cropper against a missing image ref or leave loading state stuck.

Guard stale async initialization
 const cropper = ref();
 const cropperImgRef = ref(null);
+const cropperInitToken = ref(0);
+const activeReader = ref(null);
 function closeModal() {
+  cropperInitToken.value += 1;
+  if (activeReader.value?.readyState === FileReader.LOADING) {
+    activeReader.value.abort();
+  }
+  activeReader.value = null;
+  isLoadingAction.value = false;
+
   if (cropper.value) {
     clearKeyboardShortcuts();
     cropper.value.destroy();
     cropper.value = null;
   }
@@
 async function onFileSelect(event) {
+  const initToken = ++cropperInitToken.value;
   modalVisible.value = true;
   isLoadingAction.value = true;
 
   const file = event.files[0];
 
   const reader = new FileReader();
-  reader.onload = async (event) => {
-    selectedFile.value = event.target.result;
+  activeReader.value = reader;
+  reader.onload = async (loadEvent) => {
+    if (initToken !== cropperInitToken.value || !modalVisible.value) {
+      return;
+    }
+
+    selectedFile.value = loadEvent.target.result;
 
     await nextTick();
 
+    if (
+      initToken !== cropperInitToken.value ||
+      !modalVisible.value ||
+      !cropperImgRef.value
+    ) {
+      isLoadingAction.value = false;
+      return;
+    }
+
     cropper.value = new Cropper(cropperImgRef.value, {
@@
       dragMode: "none",
       ready: async function () {
+        if (initToken !== cropperInitToken.value || !modalVisible.value) {
+          return;
+        }
+
         isLoadingAction.value = false;
 
         initKeyboardShortcuts();
       },
     });
   };
+  reader.onerror = reader.onabort = () => {
+    if (initToken === cropperInitToken.value) {
+      isLoadingAction.value = false;
+    }
+  };
   reader.readAsDataURL(file);
 }

Also applies to: 215-255

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@resources/js/components/UserProfileImageSelector.vue` around lines 164 - 165,
The async Cropper initialization must be made cancel-safe: introduce a local
cancellation flag/token used in the image load flow that is set when the dialog
is closed/cancelled, abort any in-progress FileReader, and return early from
pending callbacks; before calling new Cropper(...) or accessing
cropperImgRef.value validate the ref and the token, and in Cropper's ready
callback check the token before touching state; also ensure cropper (ref) is
destroyed and loading state cleared on cancel/cleanup (affects the functions
using cropper, cropperImgRef, FileReader, nextTick, and Cropper.ready in the
component).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@resources/js/components/UserProfileImageSelector.vue`:
- Around line 164-165: The async Cropper initialization must be made
cancel-safe: introduce a local cancellation flag/token used in the image load
flow that is set when the dialog is closed/cancelled, abort any in-progress
FileReader, and return early from pending callbacks; before calling new
Cropper(...) or accessing cropperImgRef.value validate the ref and the token,
and in Cropper's ready callback check the token before touching state; also
ensure cropper (ref) is destroyed and loading state cleared on cancel/cleanup
(affects the functions using cropper, cropperImgRef, FileReader, nextTick, and
Cropper.ready in the component).

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: dff2f633-ecf0-43ba-9189-56d6ee10126e

📥 Commits

Reviewing files that changed from the base of the PR and between 4ef538c and a3e6986.

📒 Files selected for processing (6)
  • resources/js/components/RoomTabFilesUploadButton.vue
  • resources/js/components/SettingsFileSelector.vue
  • resources/js/components/SettingsImageSelector.vue
  • resources/js/components/UserProfileImageSelector.vue
  • resources/js/composables/useFileHelpers.js
  • tests/Frontend/e2e/RoomsViewFilesFileActions.cy.js
🚧 Files skipped from review as they are similar to previous changes (3)
  • resources/js/components/SettingsFileSelector.vue
  • tests/Frontend/e2e/RoomsViewFilesFileActions.cy.js
  • resources/js/composables/useFileHelpers.js

@samuelwei samuelwei force-pushed the refactor-image-cropper branch from d50146e to e579e80 Compare April 21, 2026 11:44
@samuelwei samuelwei force-pushed the refactor-image-cropper branch from e579e80 to 48e1aec Compare April 21, 2026 13:27
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🧹 Nitpick comments (3)
resources/js/components/UserProfileImageSelector.vue (1)

299-311: Nit: pass the class name as a single token, not a compound selector string.

document.getElementsByClassName("cropper-point point-nw") happens to work (the argument is interpreted as a space-separated class list — elements must have both classes), but it's easy to misread as a CSS selector. Using querySelector(".cropper-point.point-nw") scoped to the cropper's container is clearer and less coupled to global DOM state.

-  moveHandle.value = document.getElementsByClassName("cropper-move")[0];
-  topLeftHandle.value = document.getElementsByClassName(
-    "cropper-point point-nw",
-  )[0];
+  const root = cropperImgRef.value?.closest(".cropper-container") ?? document;
+  moveHandle.value = root.querySelector(".cropper-move");
+  topLeftHandle.value = root.querySelector(".cropper-point.point-nw");
   // …etc
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@resources/js/components/UserProfileImageSelector.vue` around lines 299 - 311,
The code is using document.getElementsByClassName with compound class strings
(e.g., "cropper-point point-nw") and global document access; change these to
select elements more clearly and scoped to the cropper container: either call
getElementsByClassName with a single token (e.g., "cropper-point") and filter
for the second class, or better, use
container.querySelector(".cropper-point.point-nw") (or
container.querySelector(".cropper-move") for moveHandle) so you avoid global DOM
coupling; update where moveHandle, topLeftHandle, topRightHandle,
bottomLeftHandle, and bottomRightHandle are assigned to use the scoped
querySelector approach.
tests/Frontend/support/utils/fileHelper.js (2)

43-56: Add onerror handling to avoid test hangs on decode failures.

_base64ToImageData only resolves from image.onload. If the base64 input is malformed or decoding fails, the promise never settles and tests will hang until Cypress/Mocha times out, with no useful diagnostic. Also, no reject path is plumbed through the returned promise.

♻️ Proposed fix
 export async function _base64ToImageData(base64) {
-  return await new Promise((res) => {
+  return await new Promise((resolve, reject) => {
     const image = new Image();
     image.src = base64;

     image.onload = () => {
       const { naturalWidth: width, naturalHeight: height } = image;
       const canvas = new OffscreenCanvas(width, height);
       const ctx = canvas.getContext("2d");
       ctx.drawImage(image, 0, 0);
-      res(ctx.getImageData(0, 0, width, height));
+      resolve(ctx.getImageData(0, 0, width, height));
     };
+    image.onerror = () =>
+      reject(new Error("Failed to decode base64 image for comparison"));
   });
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/Frontend/support/utils/fileHelper.js` around lines 43 - 56,
_base64ToImageData currently only resolves on image.onload so malformed base64
will leave the promise pending; update the Promise executor to accept (res, rej)
and add an image.onerror handler that calls rej(new Error(...)) with a
descriptive message (include the failing base64 or its prefix), and also guard
for ctx being null before calling getImageData and reject if the 2D context is
unavailable; ensure both onload and onerror clean up handlers if needed so the
promise always settles.

23-40: Minor cleanups in _compareBase64Images.

  • The variable is named diff2 (likely a leftover from iteration); rename to diff for clarity.
  • The failure message embeds the full base64 strings, which for typical profile images can be many KB and clutter the test output. Consider omitting the data URIs (or truncating them) and instead logging the diff pixel count.
♻️ Proposed fix
-  const diff2 = pixelmatch(
+  const diff = pixelmatch(
     expectedImage.data,
     actualImage.data,
     null,
     expectedImage.width,
     expectedImage.height,
     {
       threshold: 0.1,
     },
   );

-  expect(diff2).to.eql(
-    0,
-    "Expected Image: " +
-      expectedImageBase64 +
-      " Actual Image: " +
-      actualImageBase64,
-  );
+  expect(diff).to.eql(0, `Images differ by ${diff} pixels`);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/Frontend/support/utils/fileHelper.js` around lines 23 - 40, In
_compareBase64Images, rename the local variable diff2 to diff for clarity and
update the assertion message to avoid embedding full base64 blobs; instead
include the diff pixel count and a short preview (e.g., first 64 chars) of
expectedImageBase64 and actualImageBase64 or omit them entirely. Change the
expect call on pixelmatch's result (now diff) to report something like "Image
mismatch: X differing pixels. Expected (prefix): <...> Actual (prefix): <...>"
so test output is concise and useful.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@config/bigbluebutton.php`:
- Line 13: The config value for 'allowed_file_mimes' should be normalized so an
empty BBB_ALLOWED_FILE_MIMES does not produce ['']; instead, take
env('BBB_ALLOWED_FILE_MIMES', '...'), explode on ',', trim each entry, filter
out empty strings (and optionally normalize case), and assign the resulting
array to 'allowed_file_mimes' so downstream consumers like
File::types()/File::extensions() and the frontend accept/label do not receive an
empty entry.

In `@resources/js/components/UserProfileImageSelector.vue`:
- Around line 186-227: save() can leave isSavingAction.value true if
getCroppedCanvas() returns null or toBlob never calls back; update save() to
early-check oc (if null) and set isSavingAction.value = false and return, and
guard the toBlob callback to handle a null blob (emit null/error and set
isSavingAction.value = false). Also add a short timeout/fallback to clear
isSavingAction if toBlob callback doesn't fire. Update closeModal() to always
reset isSavingAction.value and isLoadingAction.value to false (and
cancel/destroy any pending cropper operations) so canceling the modal can't
leave action flags stuck; reference functions: save(), closeModal(), and the
cropper.value.toBlob callback when making these changes.
- Around line 294-348: initKeyboardShortcuts() queries DOM handles (moveHandle,
topLeftHandle, topRightHandle, bottomLeftHandle, bottomRightHandle) and calls
setAttribute/addEventListener without null checks, which can throw if a handle
is missing; update initKeyboardShortcuts to guard every DOM access by either
using optional chaining (e.g., moveHandle.value?.setAttribute(...),
moveHandle.value?.addEventListener(...)) or explicit null checks and
early-returns, and preferably scope element queries to the cropper instance
container (use cropperImgRef.value.closest(".cropper-container") before
querySelector/getElementsByClassName) to avoid cross-instance collisions; mirror
the defensive pattern used in closeKeyboardShortcuts() so missing handles don’t
break the ready callback or leave isLoadingAction true.

In `@tests/Frontend/e2e/UserProfileBase.cy.js`:
- Around line 388-403: The cy.fixture(...).then(...) callback currently calls
uploadedFile.arrayBuffer() (a native Promise) but does not return it, so Cypress
won't wait; update the callback in the test to return the promise chain: inside
the cy.fixture("files/profileImagePreview.jpg", "base64").then(...) handler
return uploadedFile.arrayBuffer().then(...) (or convert the outer callback to
async and return await uploadedFile.arrayBuffer()), then continue to compute
actualImageBase64 with _arrayBufferToBase64 and await/return the
_compareBase64Images(...) Promise so Cypress waits for the full comparison
(references: cy.fixture, uploadedFile, uploadedFile.arrayBuffer,
_arrayBufferToBase64, _compareBase64Images).

---

Nitpick comments:
In `@resources/js/components/UserProfileImageSelector.vue`:
- Around line 299-311: The code is using document.getElementsByClassName with
compound class strings (e.g., "cropper-point point-nw") and global document
access; change these to select elements more clearly and scoped to the cropper
container: either call getElementsByClassName with a single token (e.g.,
"cropper-point") and filter for the second class, or better, use
container.querySelector(".cropper-point.point-nw") (or
container.querySelector(".cropper-move") for moveHandle) so you avoid global DOM
coupling; update where moveHandle, topLeftHandle, topRightHandle,
bottomLeftHandle, and bottomRightHandle are assigned to use the scoped
querySelector approach.

In `@tests/Frontend/support/utils/fileHelper.js`:
- Around line 43-56: _base64ToImageData currently only resolves on image.onload
so malformed base64 will leave the promise pending; update the Promise executor
to accept (res, rej) and add an image.onerror handler that calls rej(new
Error(...)) with a descriptive message (include the failing base64 or its
prefix), and also guard for ctx being null before calling getImageData and
reject if the 2D context is unavailable; ensure both onload and onerror clean up
handlers if needed so the promise always settles.
- Around line 23-40: In _compareBase64Images, rename the local variable diff2 to
diff for clarity and update the assertion message to avoid embedding full base64
blobs; instead include the diff pixel count and a short preview (e.g., first 64
chars) of expectedImageBase64 and actualImageBase64 or omit them entirely.
Change the expect call on pixelmatch's result (now diff) to report something
like "Image mismatch: X differing pixels. Expected (prefix): <...> Actual
(prefix): <...>" so test output is concise and useful.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 47da78b8-d07b-4c76-b8ed-120d46f696dc

📥 Commits

Reviewing files that changed from the base of the PR and between 4ef538c and 48e1aec.

⛔ Files ignored due to path filters (2)
  • package-lock.json is excluded by !**/package-lock.json
  • tests/Frontend/fixtures/files/profileImagePreview.jpg is excluded by !**/*.jpg
📒 Files selected for processing (34)
  • CHANGELOG.md
  • app/Http/Requests/StoreRoomFileRequest.php
  • app/Http/Requests/UpdateRoomStreamingConfigRequest.php
  • app/Http/Requests/UpdateRoomTypeStreamingSettingsRequest.php
  • app/Http/Requests/UpdateSettingsRequest.php
  • app/Http/Requests/UpdateStreamingSettingsRequest.php
  • app/Http/Requests/UserRequest.php
  • config/bigbluebutton.php
  • lang/en/admin.php
  • lang/en/app.php
  • lang/en/rooms.php
  • package.json
  • resources/css/app/_cropperjs.css
  • resources/css/app/app.css
  • resources/js/components/AdminStreamingRoomTypeEditButton.vue
  • resources/js/components/RoomTabFilesUploadButton.vue
  • resources/js/components/RoomTabStreamingConfigButton.vue
  • resources/js/components/SettingsFileSelector.vue
  • resources/js/components/SettingsImageSelector.vue
  • resources/js/components/UserAvatar.vue
  • resources/js/components/UserProfileImageSelector.vue
  • resources/js/components/UserTabProfile.vue
  • resources/js/composables/useFileHelpers.js
  • resources/js/views/AdminSettings.vue
  • resources/js/views/AdminStreamingSettings.vue
  • resources/js/views/AdminUsersNew.vue
  • tests/Backend/Feature/api/v1/ApplicationTest.php
  • tests/Backend/Feature/api/v1/SettingsTest.php
  • tests/Frontend/e2e/AdminUsersEditBase.cy.js
  • tests/Frontend/e2e/AdminUsersViewUserActions.cy.js
  • tests/Frontend/e2e/RoomsViewFilesFileActions.cy.js
  • tests/Frontend/e2e/UserProfileBase.cy.js
  • tests/Frontend/fixtures/config.json
  • tests/Frontend/support/utils/fileHelper.js
💤 Files with no reviewable changes (1)
  • resources/js/views/AdminUsersNew.vue
✅ Files skipped from review due to trivial changes (14)
  • resources/css/app/app.css
  • resources/js/components/AdminStreamingRoomTypeEditButton.vue
  • resources/js/components/UserTabProfile.vue
  • resources/js/components/RoomTabStreamingConfigButton.vue
  • resources/js/views/AdminStreamingSettings.vue
  • lang/en/admin.php
  • tests/Frontend/fixtures/config.json
  • resources/js/components/UserAvatar.vue
  • app/Http/Requests/UpdateRoomStreamingConfigRequest.php
  • CHANGELOG.md
  • tests/Frontend/e2e/RoomsViewFilesFileActions.cy.js
  • resources/js/composables/useFileHelpers.js
  • resources/css/app/_cropperjs.css
  • app/Http/Requests/UpdateSettingsRequest.php
🚧 Files skipped from review as they are similar to previous changes (11)
  • resources/js/components/SettingsImageSelector.vue
  • tests/Backend/Feature/api/v1/SettingsTest.php
  • package.json
  • resources/js/components/SettingsFileSelector.vue
  • app/Http/Requests/StoreRoomFileRequest.php
  • app/Http/Requests/UpdateRoomTypeStreamingSettingsRequest.php
  • lang/en/rooms.php
  • app/Http/Requests/UserRequest.php
  • lang/en/app.php
  • tests/Backend/Feature/api/v1/ApplicationTest.php
  • app/Http/Requests/UpdateStreamingSettingsRequest.php

Comment thread config/bigbluebutton.php
],
'max_filesize' => (int) env('BBB_MAX_FILESIZE', 30),
'allowed_file_mimes' => env('BBB_ALLOWED_FILE_MIMES', 'pdf,doc,docx,xls,xlsx,ppt,pptx,txt,rtf,odt,ods,odp,odg,odc,odi,jpg,jpeg,png'),
'allowed_file_mimes' => explode(',', env('BBB_ALLOWED_FILE_MIMES', 'pdf,doc,docx,xls,xlsx,ppt,pptx,txt,rtf,odt,ods,odp,odg,odc,odi,jpg,jpeg,png')),
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Edge case: empty BBB_ALLOWED_FILE_MIMES produces [''] instead of [].

If an operator sets BBB_ALLOWED_FILE_MIMES= (empty), explode(',', '') returns [''], which will propagate an empty entry into File::types()/File::extensions() on the backend and into the frontend accept="." / "Allowed formats: " display. Consider normalizing and trimming entries, and dropping empties.

🛡️ Suggested hardening
-    'allowed_file_mimes' => explode(',', env('BBB_ALLOWED_FILE_MIMES', 'pdf,doc,docx,xls,xlsx,ppt,pptx,txt,rtf,odt,ods,odp,odg,odc,odi,jpg,jpeg,png')),
+    'allowed_file_mimes' => array_values(array_filter(array_map(
+        'trim',
+        explode(',', (string) env('BBB_ALLOWED_FILE_MIMES', 'pdf,doc,docx,xls,xlsx,ppt,pptx,txt,rtf,odt,ods,odp,odg,odc,odi,jpg,jpeg,png'))
+    ), 'strlen')),
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
'allowed_file_mimes' => explode(',', env('BBB_ALLOWED_FILE_MIMES', 'pdf,doc,docx,xls,xlsx,ppt,pptx,txt,rtf,odt,ods,odp,odg,odc,odi,jpg,jpeg,png')),
'allowed_file_mimes' => array_values(array_filter(array_map(
'trim',
explode(',', (string) env('BBB_ALLOWED_FILE_MIMES', 'pdf,doc,docx,xls,xlsx,ppt,pptx,txt,rtf,odt,ods,odp,odg,odc,odi,jpg,jpeg,png'))
), 'strlen')),
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@config/bigbluebutton.php` at line 13, The config value for
'allowed_file_mimes' should be normalized so an empty BBB_ALLOWED_FILE_MIMES
does not produce ['']; instead, take env('BBB_ALLOWED_FILE_MIMES', '...'),
explode on ',', trim each entry, filter out empty strings (and optionally
normalize case), and assign the resulting array to 'allowed_file_mimes' so
downstream consumers like File::types()/File::extensions() and the frontend
accept/label do not receive an empty entry.

Comment on lines 186 to 227
async function save() {
isLoadingAction.value = true;
const oc = cropperRef.value.getCroppedCanvas({
isSavingAction.value = true;
const oc = cropper.value.getCroppedCanvas({
width: 100,
height: 100,
fillColor: "#ffff",
});

croppedImage.value = oc.toDataURL("image/jpeg");
oc.toBlob((blob) => {
emit("newImage", blob);
isLoadingAction.value = false;
modalVisible.value = false;
}, "image/jpeg");
croppedImage.value = oc.toDataURL("image/jpeg", 1);
oc.toBlob(
(blob) => {
emit("newImage", blob);
isSavingAction.value = false;
closeModal();
},
"image/jpeg",
1,
);
}

/**
* Reset other previously uploaded images
*/
function resetFileUpload() {
async function resetFileUpload() {
croppedImage.value = null;
emit("newImage", null);
selectedFile.value = null;

await nextTick();

document.querySelector("[data-test='upload-file-button']")?.focus();
}

function closeModal() {
if (cropper.value) {
clearKeyboardShortcuts();
cropper.value.destroy();
cropper.value = null;
}
modalVisible.value = false;
selectedFile.value = null;
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

save() and closeModal() can leave action flags stuck.

  • save() sets isSavingAction.value = true, but cropper.value.getCroppedCanvas() can return null (e.g. tainted canvas / memory limits on iOS) and oc.toBlob may pass null or never call the callback. Nothing here catches either case, so the save button stays in its loading state indefinitely.
  • closeModal() does not reset isLoadingAction / isSavingAction. If the user cancels while FileReader.onload / cropper ready is still pending (or while toBlob is running), flags remain true and the Dialog/button may appear stuck in a weird state next time the modal opens.
🐛 Proposed fix
 async function save() {
   isSavingAction.value = true;
-  const oc = cropper.value.getCroppedCanvas({
+  const oc = cropper.value?.getCroppedCanvas({
     width: 100,
     height: 100,
     fillColor: "#ffff",
   });
+  if (!oc) {
+    isSavingAction.value = false;
+    return;
+  }

   croppedImage.value = oc.toDataURL("image/jpeg", 1);
   oc.toBlob(
     (blob) => {
       emit("newImage", blob);
       isSavingAction.value = false;
       closeModal();
     },
     "image/jpeg",
     1,
   );
 }

 function closeModal() {
   if (cropper.value) {
     clearKeyboardShortcuts();
     cropper.value.destroy();
     cropper.value = null;
   }
   modalVisible.value = false;
   selectedFile.value = null;
+  isLoadingAction.value = false;
+  isSavingAction.value = false;
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@resources/js/components/UserProfileImageSelector.vue` around lines 186 - 227,
save() can leave isSavingAction.value true if getCroppedCanvas() returns null or
toBlob never calls back; update save() to early-check oc (if null) and set
isSavingAction.value = false and return, and guard the toBlob callback to handle
a null blob (emit null/error and set isSavingAction.value = false). Also add a
short timeout/fallback to clear isSavingAction if toBlob callback doesn't fire.
Update closeModal() to always reset isSavingAction.value and
isLoadingAction.value to false (and cancel/destroy any pending cropper
operations) so canceling the modal can't leave action flags stuck; reference
functions: save(), closeModal(), and the cropper.value.toBlob callback when
making these changes.

Comment on lines +294 to +348
function initKeyboardShortcuts() {
const dimensions = cropper.value.getData();
scaleMove.value = Math.min(dimensions.width, dimensions.height) / 100;

// Get all handles
moveHandle.value = document.getElementsByClassName("cropper-move")[0];
topLeftHandle.value = document.getElementsByClassName(
"cropper-point point-nw",
)[0];
topRightHandle.value = document.getElementsByClassName(
"cropper-point point-ne",
)[0];
bottomLeftHandle.value = document.getElementsByClassName(
"cropper-point point-sw",
)[0];
bottomRightHandle.value = document.getElementsByClassName(
"cropper-point point-se",
)[0];

// Add tabindex
moveHandle.value.setAttribute("tabindex", 0);
topLeftHandle.value.setAttribute("tabindex", 0);
topRightHandle.value.setAttribute("tabindex", 0);
bottomLeftHandle.value.setAttribute("tabindex", 0);
bottomRightHandle.value.setAttribute("tabindex", 0);

// Add aria-labels
moveHandle.value.setAttribute(
"aria-label",
t("admin.users.image.aria_crop_selection.move"),
);
topLeftHandle.value.setAttribute(
"aria-label",
t("admin.users.image.aria_crop_selection.top_left"),
);
topRightHandle.value.setAttribute(
"aria-label",
t("admin.users.image.aria_crop_selection.top_right"),
);
bottomLeftHandle.value.setAttribute(
"aria-label",
t("admin.users.image.aria_crop_selection.bottom_left"),
);
bottomRightHandle.value.setAttribute(
"aria-label",
t("admin.users.image.aria_crop_selection.bottom_right"),
);

// Add event listeners
moveHandle.value.addEventListener("keydown", moveEventListener);
topLeftHandle.value.addEventListener("keydown", topLeftEventListener);
topRightHandle.value.addEventListener("keydown", topRightEventListener);
bottomLeftHandle.value.addEventListener("keydown", bottomLeftEventListener);
bottomRightHandle.value.addEventListener("keydown", bottomRightEventListener);
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Inspect cropperjs v1 source to verify when the `cropper-move` class is applied
# to the face / drag box, and whether it is present when dragMode === 'none'.
fd -t f 'cropper.js$' | xargs -r rg -n -C2 'cropper-move|CLASS_MOVE|ACTION_MOVE'
fd -t f -e js -e mjs . node_modules/cropperjs 2>/dev/null | head -n 50

Repository: THM-Health/PILOS

Length of output: 42


🏁 Script executed:

# Find the UserProfileImageSelector.vue file and examine the cropper setup
fd -t f 'UserProfileImageSelector.vue' | head -5

Repository: THM-Health/PILOS

Length of output: 112


🏁 Script executed:

# Check package.json to find cropperjs version
fd -t f 'package.json' | head -5 | xargs cat | grep -A2 -B2 'cropperjs\|cropper'

Repository: THM-Health/PILOS

Length of output: 207


🏁 Script executed:

# Look for the cropper initialization to understand dragMode and movable settings
fd -t f 'UserProfileImageSelector.vue' | xargs grep -n 'dragMode\|movable\|cropper:' | head -20

Repository: THM-Health/PILOS

Length of output: 113


🏁 Script executed:

# Read the complete UserProfileImageSelector.vue file focusing on the initKeyboardShortcuts function
cat resources/js/components/UserProfileImageSelector.vue | sed -n '290,360p'

Repository: THM-Health/PILOS

Length of output: 2266


🏁 Script executed:

# Also check if there are event listeners added with null checks elsewhere in the component
grep -n 'addEventListener\|removeEventListener' resources/js/components/UserProfileImageSelector.vue

Repository: THM-Health/PILOS

Length of output: 796


🏁 Script executed:

# Check if there's a cropper.js file in node_modules or if we need to look at the CDN docs
find . -name 'cropper.js' -o -name 'cropper.min.js' 2>/dev/null | head -5

Repository: THM-Health/PILOS

Length of output: 42


🏁 Script executed:

# Look for any test files or examples that show DOM structure with dragMode: "none"
find . -type f \( -name '*.vue' -o -name '*.js' -o -name '*.html' \) | xargs grep -l 'dragMode.*none' 2>/dev/null | head -5

Repository: THM-Health/PILOS

Length of output: 114


🏁 Script executed:

# Check if there are any other components or code that guards against missing cropper elements
grep -r 'cropper-move\|cropper-point' --include='*.vue' --include='*.js' . 2>/dev/null

Repository: THM-Health/PILOS

Length of output: 527


🌐 Web query:

cropperjs v1.6.2 dragMode none cropper-move element DOM structure

💡 Result:

In Cropper.js v1.6.2, setting dragMode: 'none' disables dragging actions on the cropper—no canvas movement or new crop box creation on drag. The generated DOM structure wraps the target image in a container with classes like 'cropper-container', containing 'cropper-canvas', 'cropper-drag-canvas', 'cropper-crop-box', etc. The 'cropper-move' class is used on elements (like canvas or crop box) to indicate and style the move drag mode via CSS cursor: move, but with dragMode 'none', no dragging occurs even if the class is present. Official docs confirm dragMode options: 'none' (do nothing), 'move' (move canvas), 'crop' (new crop box). Use setDragMode('none') to change dynamically. For disabling crop entirely, combine with autoCrop: false and dragMode: 'move'. v2.0+ uses custom elements and 'action' property instead.

Citations:


🌐 Web query:

cropperjs v1.6.2 cropper-point handles elements created

💡 Result:

In Cropper.js v1.6.2, no elements with class "cropper-point" are created for handles. Cropper.js v1.x uses plain DIV elements with classes like "cropper-point point-se" (for southeast resize handle), "cropper-point point-e", "cropper-point point-n", etc., positioned absolutely at the crop box corners and edges for resizing and dragging. These are dynamically created by the library's JavaScript when initializing the cropper instance and rendering the crop box, as part of the internal template insertion into the container. The handles enable crop box resizing (e.g., n-resize, e-resize, ne-resize) and movement. This structure is confirmed by the v1.6.2 GitHub tag and demo inspection at https://fengyuanchen.github.io/cropperjs/v1/. Note: Custom web components like appear in v2.x, not v1.6.2.

Citations:


🏁 Script executed:

# Look for the actual cropper initialization and check if there are any try-catch blocks around initKeyboardShortcuts
grep -n -A 20 'ready.*function\|ready:' resources/js/components/UserProfileImageSelector.vue | head -40

Repository: THM-Health/PILOS

Length of output: 617


🏁 Script executed:

# Check the complete initKeyboardShortcuts function and see if there are any guards we missed
sed -n '294,348p' resources/js/components/UserProfileImageSelector.vue | cat -n

Repository: THM-Health/PILOS

Length of output: 2374


🏁 Script executed:

# Look for error handling or try-catch blocks in the entire component
grep -n 'try\|catch\|finally' resources/js/components/UserProfileImageSelector.vue

Repository: THM-Health/PILOS

Length of output: 42


Guard all handle element queries against missing DOM elements; use optional chaining or null checks.

The initKeyboardShortcuts() function queries for five cropper handle elements and immediately calls setAttribute() and addEventListener() on them without any null or undefined checks. If any handle element is missing from the DOM—whether due to a missing .cropper-move class (which may not reliably exist when dragMode: "none" is configured) or unexpected DOM structure changes—the code will throw a TypeError and crash the ready callback, leaving isLoadingAction stuck at true.

This is particularly inconsistent because the cleanup code in closeKeyboardShortcuts() already uses optional chaining (?.) on these same references, showing a defensive pattern was partially implemented. Apply the same protection throughout:

  • Use optional chaining (handle?.setAttribute()) or null checks before each attribute/event operation
  • Or refactor to skip initialization if any required handle is missing
  • Consider scoping queries to cropperImgRef.value.closest(".cropper-container") instead of document.getElementsByClassName() to avoid conflicts if multiple instances exist on the same page
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@resources/js/components/UserProfileImageSelector.vue` around lines 294 - 348,
initKeyboardShortcuts() queries DOM handles (moveHandle, topLeftHandle,
topRightHandle, bottomLeftHandle, bottomRightHandle) and calls
setAttribute/addEventListener without null checks, which can throw if a handle
is missing; update initKeyboardShortcuts to guard every DOM access by either
using optional chaining (e.g., moveHandle.value?.setAttribute(...),
moveHandle.value?.addEventListener(...)) or explicit null checks and
early-returns, and preferably scope element queries to the cropper instance
container (use cropperImgRef.value.closest(".cropper-container") before
querySelector/getElementsByClassName) to avoid cross-instance collisions; mirror
the defensive pattern used in closeKeyboardShortcuts() so missing handles don’t
break the ready callback or leave isLoadingAction true.

Comment on lines 388 to 403
const uploadedFile = formData.get("image");
expect(uploadedFile.name).to.eql("image.png");
expect(uploadedFile.name).to.eql("image.jpg");
expect(uploadedFile.type).to.eql("image/jpeg");
cy.fixture("files/profileImagePreview.jpg", "base64").then((content) => {
uploadedFile.arrayBuffer().then((arrayBuffer) => {
const base64 = _arrayBufferToBase64(arrayBuffer);
expect(content).to.eql(base64);
});
});
cy.fixture("files/profileImagePreview.jpg", "base64").then(
(referenceImageBase64) => {
uploadedFile.arrayBuffer().then(async (actualImageBuffer) => {
const actualImageBase64 = _arrayBufferToBase64(actualImageBuffer);

await _compareBase64Images(
"data:image/jpeg;base64," + referenceImageBase64,
"data:image/jpeg;base64," + actualImageBase64,
);
});
},
);
});
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Return the promise from the cy.fixture().then(...) callback so Cypress awaits it.

uploadedFile.arrayBuffer() returns a native Promise (not a chainable Cypress command). Because the outer .then callback doesn't return this promise, Cypress may proceed to the next command (and potentially end the test) before _compareBase64Images finishes, turning real mismatches into unhandled rejections rather than failed assertions.

♻️ Proposed fix
-      cy.fixture("files/profileImagePreview.jpg", "base64").then(
-        (referenceImageBase64) => {
-          uploadedFile.arrayBuffer().then(async (actualImageBuffer) => {
+      cy.fixture("files/profileImagePreview.jpg", "base64").then(
+        (referenceImageBase64) =>
+          uploadedFile.arrayBuffer().then(async (actualImageBuffer) => {
             const actualImageBase64 = _arrayBufferToBase64(actualImageBuffer);

             await _compareBase64Images(
               "data:image/jpeg;base64," + referenceImageBase64,
               "data:image/jpeg;base64," + actualImageBase64,
             );
-          });
-        },
+          }),
       );
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/Frontend/e2e/UserProfileBase.cy.js` around lines 388 - 403, The
cy.fixture(...).then(...) callback currently calls uploadedFile.arrayBuffer() (a
native Promise) but does not return it, so Cypress won't wait; update the
callback in the test to return the promise chain: inside the
cy.fixture("files/profileImagePreview.jpg", "base64").then(...) handler return
uploadedFile.arrayBuffer().then(...) (or convert the outer callback to async and
return await uploadedFile.arrayBuffer()), then continue to compute
actualImageBase64 with _arrayBufferToBase64 and await/return the
_compareBase64Images(...) Promise so Cypress waits for the full comparison
(references: cy.fixture, uploadedFile, uploadedFile.arrayBuffer,
_arrayBufferToBase64, _compareBase64Images).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant