Skip to content

Refactor profile picture image cropper#3060

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

Refactor profile picture image cropper#3060
samuelwei wants to merge 11 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 always render circular.
    • File uploads now accept only JPG/PNG and show allowed formats plus human-readable max-size hints.
    • Image cropper replaced with native CropperJS for improved preview, save/cancel behavior, and reliable modal teardown.
    • Pause image help text now displays resolution-only info.
    • Validation and file-format handling updated site-wide (affects displayed messages/behavior).
  • Style
    • Added cropper-specific styles for consistent cropping UI.
  • New Features
    • Human-readable file-size helper for display.
  • Tests
    • Updated tests/fixtures to match new file-mimes format and UI text.

Show profile image rounded during cropping and inside profile page
@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
  • 🔍 Trigger review

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 docstrings
  • Create stacked PR
  • Commit on current branch
🧪 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

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 96.34%. Comparing base (3c8a92f) to head (35480a3).
⚠️ Report is 24 commits behind head on develop.

Additional details and impacted files
@@              Coverage Diff              @@
##             develop    #3060      +/-   ##
=============================================
- Coverage      96.73%   96.34%   -0.40%     
  Complexity      1924     1924              
=============================================
  Files            457      278     -179     
  Lines          12988     6674    -6314     
  Branches        2079        0    -2079     
=============================================
- Hits           12564     6430    -6134     
+ Misses           424      244     -180     

☔ 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 #2972

Run Properties:  status check failed Failed #2972  •  git commit 35480a3caf: Refactor profile picture image cropper
Project PILOS
Branch Review refactor-image-cropper
Run status status check failed Failed #2972
Run duration 11m 06s
Commit git commit 35480a3caf: Refactor profile picture image cropper
Committer Samuel Weirich
View all properties for this run ↗︎

Test results
Tests that failed  Failures 3
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 604
View all changes introduced in this branch ↗︎

Tests for review

Failed  AdminUsersEditBase.cy.js • 1 failed test • Frontend tests

View Output

Test Artifacts
Admin users edit base > check view and save changes Test Replay Screenshots
Failed  UserProfileBase.cy.js • 1 failed test • Frontend tests

View Output

Test Artifacts
User Profile Base > check view and save changes Test Replay Screenshots
Failed  AdminUsersViewUserActions.cy.js • 1 failed test • Frontend tests

View Output

Test Artifacts
Admin users view user actions > switch between edit and view 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 on lines +215 to 255
function closeModal() {
if (cropper.value) {
clearKeyboardShortcuts();
cropper.value.destroy();
cropper.value = null;
}
modalVisible.value = false;
selectedFile.value = null;
}

async function onFileSelect(event) {
modalVisible.value = true;
isLoadingAction.value = true;

const file = event.files[0];

const reader = new FileReader();
reader.onload = (event) => {
reader.onload = async (event) => {
selectedFile.value = event.target.result;
cropperRef.value.replace(selectedFile.value);
isLoadingAction.value = false;

await nextTick();

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;

initKeyboardShortcuts();
},
});
};
reader.readAsDataURL(file);
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

Make async cropper initialization cancel-safe.

If the user cancels while FileReader/nextTick/Cropper ready is still pending, the stale callback can initialize Cropper with a null cropperImgRef and leave isLoadingAction stuck.

🐛 Proposed guard for stale initialization
 const cropper = ref();
 const cropperImgRef = ref(null);
+const cropperInitToken = ref(0);
 function closeModal() {
+  cropperInitToken.value += 1;
+  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) => {
+    if (initToken !== cropperInitToken.value || !modalVisible.value) {
+      return;
+    }
+
     selectedFile.value = event.target.result;
 
     await nextTick();
 
+    if (
+      initToken !== cropperInitToken.value ||
+      !modalVisible.value ||
+      !cropperImgRef.value
+    ) {
+      selectedFile.value = null;
+      isLoadingAction.value = false;
+      return;
+    }
+
     cropper.value = new Cropper(cropperImgRef.value, {
@@
       dragMode: "none",
       ready: async function () {
+        if (initToken !== cropperInitToken.value) {
+          return;
+        }
         isLoadingAction.value = false;
 
         initKeyboardShortcuts();
       },
     });
   };
+  reader.onerror = () => {
+    if (initToken === cropperInitToken.value) {
+      isLoadingAction.value = false;
+    }
+  };
   reader.readAsDataURL(file);
 }
📝 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
function closeModal() {
if (cropper.value) {
clearKeyboardShortcuts();
cropper.value.destroy();
cropper.value = null;
}
modalVisible.value = false;
selectedFile.value = null;
}
async function onFileSelect(event) {
modalVisible.value = true;
isLoadingAction.value = true;
const file = event.files[0];
const reader = new FileReader();
reader.onload = (event) => {
reader.onload = async (event) => {
selectedFile.value = event.target.result;
cropperRef.value.replace(selectedFile.value);
isLoadingAction.value = false;
await nextTick();
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;
initKeyboardShortcuts();
},
});
};
reader.readAsDataURL(file);
function closeModal() {
cropperInitToken.value += 1;
isLoadingAction.value = false;
if (cropper.value) {
clearKeyboardShortcuts();
cropper.value.destroy();
cropper.value = null;
}
modalVisible.value = false;
selectedFile.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) => {
if (initToken !== cropperInitToken.value || !modalVisible.value) {
return;
}
selectedFile.value = event.target.result;
await nextTick();
if (
initToken !== cropperInitToken.value ||
!modalVisible.value ||
!cropperImgRef.value
) {
selectedFile.value = null;
isLoadingAction.value = false;
return;
}
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 () {
if (initToken !== cropperInitToken.value) {
return;
}
isLoadingAction.value = false;
initKeyboardShortcuts();
},
});
};
reader.onerror = () => {
if (initToken === cropperInitToken.value) {
isLoadingAction.value = false;
}
};
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 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.

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

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