Conversation
|
Note Reviews pausedIt 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 Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
WalkthroughReplaced the Vue wrapper with upstream CropperJS (dependency + scoped CSS), refactored profile image selector to instantiate/destroy Cropper programmatically, removed Changes
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Suggested labels
Suggested reviewers
🚥 Pre-merge checks | ✅ 1 | ❌ 2❌ Failed checks (2 warnings)
✅ Passed checks (1 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
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 | 🟡 MinorVerify
fillColorvalue.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 ofdocument.getElementById.Using
document.getElementByIdis 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
idshould go beforewidthif 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
⛔ Files ignored due to path filters (1)
package-lock.jsonis excluded by!**/package-lock.json
📒 Files selected for processing (6)
package.jsonresources/css/app/_cropperjs.cssresources/css/app/app.cssresources/js/components/UserAvatar.vueresources/js/components/UserProfileImageSelector.vueresources/js/views/AdminUsersNew.vue
💤 Files with no reviewable changes (1)
- resources/js/views/AdminUsersNew.vue
Codecov Report❌ Patch coverage is
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. 🚀 New features to boost your workflow:
|
PILOS
|
||||||||||||||||||||||||||||||||||
| Project |
PILOS
|
| Branch Review |
refactor-image-cropper
|
| Run status |
|
| Run duration | 07m 05s |
| Commit |
|
| Committer | Samuel Weirich |
| View all properties for this run ↗︎ | |
| Test results | |
|---|---|
|
|
1
|
|
|
0
|
|
|
0
|
|
|
0
|
|
|
621
|
| View all changes introduced in this branch ↗︎ | |
Tests for review

e2e/RoomsJoinWithLobby.cy.js • 1 failed test • System tests
| Test | Artifacts | |
|---|---|---|
| Room Join with lobby settings > Lobby enabled for guests only |
Test Replay
Screenshots
|
|
There was a problem hiding this comment.
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 | 🟡 MinorLikely typo in
fillColorvalue.
"#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,
isLoadingActionwill remaintrue, 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
📒 Files selected for processing (2)
resources/css/app/_cropperjs.cssresources/js/components/UserProfileImageSelector.vue
✅ Files skipped from review due to trivial changes (1)
- resources/css/app/_cropperjs.css
There was a problem hiding this comment.
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 | 🟠 MajorFix the expected max-size interpolation string.
Line 26 won’t match the usual i18n fallback format because the
sizevalue 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 | 🟡 MinorAlign CSS file upload limit with backend validation.
The backend enforces a 500 KB maximum for CSS files (
app/Http/Requests/UpdateStreamingSettingsRequest.phpline: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: HandleFileReadererror / cropper init failure.If
readerfails (ornew Cropper(...)throws synchronously beforereadyfires),isLoadingActionstaystrueand the modal remains open with a spinner and no way to recover other than the cancel button. Add anonerrorhandler 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 parsingBBB_ALLOWED_FILE_MIMES.
explode(',', '')yields['']and tokens aren’t trimmed, so an empty or whitespace-padded override silently produces invalid entries that downstreamFile::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 forbbb.file_mimesbefore passing toSettingsFileSelector.While the backend always sends this setting as an array via
explode()inconfig/bigbluebutton.php, the Visual test fixture contains a comma-separated string. SinceSettingsFileSelectorrequires 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
📒 Files selected for processing (22)
app/Http/Requests/StoreRoomFileRequest.phpapp/Http/Requests/UpdateRoomStreamingConfigRequest.phpapp/Http/Requests/UpdateRoomTypeStreamingSettingsRequest.phpapp/Http/Requests/UpdateSettingsRequest.phpapp/Http/Requests/UpdateStreamingSettingsRequest.phpapp/Http/Requests/UserRequest.phpconfig/bigbluebutton.phplang/en/app.phplang/en/rooms.phpresources/js/components/AdminStreamingRoomTypeEditButton.vueresources/js/components/RoomTabFilesUploadButton.vueresources/js/components/RoomTabStreamingConfigButton.vueresources/js/components/SettingsFileSelector.vueresources/js/components/SettingsImageSelector.vueresources/js/components/UserProfileImageSelector.vueresources/js/composables/useFileHelpers.jsresources/js/views/AdminSettings.vueresources/js/views/AdminStreamingSettings.vuetests/Backend/Feature/api/v1/ApplicationTest.phptests/Backend/Feature/api/v1/SettingsTest.phptests/Frontend/e2e/RoomsViewFilesFileActions.cy.jstests/Frontend/fixtures/config.json
✅ Files skipped from review due to trivial changes (2)
- resources/js/components/AdminStreamingRoomTypeEditButton.vue
- tests/Frontend/fixtures/config.json
There was a problem hiding this comment.
Actionable comments posted: 2
♻️ Duplicate comments (1)
resources/js/components/UserProfileImageSelector.vue (1)
15-15:⚠️ Potential issue | 🔴 CriticalResolve the still-present frontend lint/style failures.
Line 127 is a CI failure because
useTemplateRefis 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 returnundefined; the followingsetAttribute/addEventListenercalls 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
📒 Files selected for processing (3)
lang/en/admin.phpresources/js/components/UserProfileImageSelector.vueresources/js/components/UserTabProfile.vue
✅ Files skipped from review due to trivial changes (1)
- lang/en/admin.php
There was a problem hiding this comment.
♻️ Duplicate comments (1)
resources/js/components/UserProfileImageSelector.vue (1)
164-165:⚠️ Potential issue | 🟠 MajorMake cropper initialization cancel-safe.
If the dialog is cancelled while
FileReader,nextTick, or Cropper’sreadycallback 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
📒 Files selected for processing (6)
resources/js/components/RoomTabFilesUploadButton.vueresources/js/components/SettingsFileSelector.vueresources/js/components/SettingsImageSelector.vueresources/js/components/UserProfileImageSelector.vueresources/js/composables/useFileHelpers.jstests/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
d50146e to
e579e80
Compare
Show profile image rounded during cropping and inside profile page
Show up to two decimal places
e579e80 to
48e1aec
Compare
There was a problem hiding this comment.
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. UsingquerySelector(".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: Addonerrorhandling to avoid test hangs on decode failures.
_base64ToImageDataonly resolves fromimage.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, norejectpath 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 todifffor 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
⛔ Files ignored due to path filters (2)
package-lock.jsonis excluded by!**/package-lock.jsontests/Frontend/fixtures/files/profileImagePreview.jpgis excluded by!**/*.jpg
📒 Files selected for processing (34)
CHANGELOG.mdapp/Http/Requests/StoreRoomFileRequest.phpapp/Http/Requests/UpdateRoomStreamingConfigRequest.phpapp/Http/Requests/UpdateRoomTypeStreamingSettingsRequest.phpapp/Http/Requests/UpdateSettingsRequest.phpapp/Http/Requests/UpdateStreamingSettingsRequest.phpapp/Http/Requests/UserRequest.phpconfig/bigbluebutton.phplang/en/admin.phplang/en/app.phplang/en/rooms.phppackage.jsonresources/css/app/_cropperjs.cssresources/css/app/app.cssresources/js/components/AdminStreamingRoomTypeEditButton.vueresources/js/components/RoomTabFilesUploadButton.vueresources/js/components/RoomTabStreamingConfigButton.vueresources/js/components/SettingsFileSelector.vueresources/js/components/SettingsImageSelector.vueresources/js/components/UserAvatar.vueresources/js/components/UserProfileImageSelector.vueresources/js/components/UserTabProfile.vueresources/js/composables/useFileHelpers.jsresources/js/views/AdminSettings.vueresources/js/views/AdminStreamingSettings.vueresources/js/views/AdminUsersNew.vuetests/Backend/Feature/api/v1/ApplicationTest.phptests/Backend/Feature/api/v1/SettingsTest.phptests/Frontend/e2e/AdminUsersEditBase.cy.jstests/Frontend/e2e/AdminUsersViewUserActions.cy.jstests/Frontend/e2e/RoomsViewFilesFileActions.cy.jstests/Frontend/e2e/UserProfileBase.cy.jstests/Frontend/fixtures/config.jsontests/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
| ], | ||
| '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')), |
There was a problem hiding this comment.
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.
| '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.
| 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; | ||
| } |
There was a problem hiding this comment.
save() and closeModal() can leave action flags stuck.
save()setsisSavingAction.value = true, butcropper.value.getCroppedCanvas()can returnnull(e.g. tainted canvas / memory limits on iOS) andoc.toBlobmay passnullor never call the callback. Nothing here catches either case, so the save button stays in itsloadingstate indefinitely.closeModal()does not resetisLoadingAction/isSavingAction. If the user cancels whileFileReader.onload/ cropperreadyis still pending (or whiletoBlobis running), flags remaintrueand 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.
| 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); | ||
| } |
There was a problem hiding this comment.
🧩 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 50Repository: 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 -5Repository: 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 -20Repository: 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.vueRepository: 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 -5Repository: 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 -5Repository: 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/nullRepository: 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:
- 1: https://www.npmjs.com/package/cropperjs/v/1.6.2
- 2: https://github.com/fengyuanchen/cropperjs/tree/v1.6.2
- 3: Cropper does not allow scroll when dragMode is "none" on touch screens fengyuanchen/cropper#727
- 4: Is there a way to disable cropping feature? fengyuanchen/cropperjs#706
🌐 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:
- 1: https://fengyuanchen.github.io/cropperjs/v1/
- 2: https://www.npmjs.com/package/cropperjs/v/1.6.2
- 3: https://github.com/fengyuanchen/cropperjs
- 4: https://github.com/fengyuanchen/cropperjs/tree/v1.6.2
- 5: https://cdn.jsdelivr.net/npm/cropperjs@1.6.2/dist/
🏁 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 -40Repository: 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 -nRepository: 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.vueRepository: 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 ofdocument.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.
| 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, | ||
| ); | ||
| }); | ||
| }, | ||
| ); | ||
| }); |
There was a problem hiding this comment.
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).
Type
Checklist
Changes
Other information
Summary by CodeRabbit