Skip to content

feat(condo): DOMA-13015 files for news (client-side)#7465

Open
Alllex202 wants to merge 8 commits intomainfrom
feat/condo/DOMA-13015/files-for-news
Open

feat(condo): DOMA-13015 files for news (client-side)#7465
Alllex202 wants to merge 8 commits intomainfrom
feat/condo/DOMA-13015/files-for-news

Conversation

@Alllex202
Copy link
Copy Markdown
Contributor

@Alllex202 Alllex202 commented Apr 13, 2026

Summary by CodeRabbit

Release Notes

  • New Features

    • Added media file upload and management to news creation and editing
    • Implemented automatic image and video thumbnail generation
    • Added file preview display in news articles
    • Enabled client-side file format conversion (HEIC, WebP, MOV to supported formats)
  • Bug Fixes

    • Improved caching and invalidation for file operations
    • Enhanced file access control and validation
  • Documentation & Localization

    • Added UI labels for media files in English, Spanish, and Russian

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Apr 13, 2026

📝 Walkthrough

Walkthrough

This PR introduces comprehensive file upload and management functionality to the news domain. It adds React components for file selection, preview, and display; extends GraphQL operations for creating and updating file records; modifies access control to permit null newsItem references during initial file creation; updates the news form to handle file uploads and attachment; and adds client-side file processing (format conversion via FFmpeg, canvas-based thumbnails).

Changes

Cohort / File(s) Summary
Apollo Cache Configuration
apps/condo/domains/common/utils/next/apollo.ts
Extended cache type policies with Query.allNewsItemFiles pagination handling and added NewsItemFile invalidation policy with 1-minute TTL.
File Upload & Preview Components
apps/condo/domains/news/components/FilesUploadList.tsx, FilesUploadList.module.css, ImageOrVideoPreview.tsx, DocumentsPreview.tsx
Introduced FilesUploadList with FFmpeg-based transcoding, thumbnail generation via canvas, file format conversion (HEIC→JPEG, WebP→JPEG, MOV/MP4→MP4), and utilities for managing upload state. Added preview components for images/videos (with canvas-based thumbnails) and documents (file name display).
Form Integration & Layout
apps/condo/domains/news/components/NewsForm/BaseNewsForm.tsx, InputStep/index.tsx, InputStep/InputStepFilesSelector.tsx, InputStep/InputStepForm.tsx, InputStep/InputStepPreview.tsx, InputStep/InputStepRecipientCounter.tsx, InputStep/InputStepSelector.tsx
Integrated file upload into news form workflow; added InputStepFilesSelector with file re-upload hook; removed responsive layout logic and fixed column widths; extended form state to track files and dispatch file mutations on submit; updated preview to display selected files.
News Preview & Display
apps/condo/domains/news/components/NewsPreview.tsx, apps/condo/pages/news/[id]/index.tsx
Added file attachment support to preview; split files into images/videos and documents; rendered file previews in app preview; added file fetch query and display section on news detail page.
GraphQL Operations & Types
apps/condo/domains/news/queries/NewsItemFile.graphql, apps/condo/gql/index.ts, apps/condo/gql/operation.types.ts
Added createNewsItemFile, updateNewsItemFile, and getNewsItemFiles GraphQL operations with corresponding Apollo hooks and TypeScript types.
Access Control
apps/condo/domains/news/access/NewsItemFile.js
Extended read filters to allow records with null organization when created by current user; modified manage logic to allow STAFF create without linked newsItem and removed update operation restriction.
Schema & Validation
apps/condo/domains/news/schema/NewsItemFile.js, apps/condo/domains/news/utils/testSchema/index.js
Made newsItem relationship optional; added validation preventing newsItem changes after initial set; disabled file field updates; normalized MIME-type validation; made test utility support optional newsItem.
Tests & Migration
apps/condo/domains/news/schema/NewsItemFile.test.js, apps/condo/migrations/20260413232745-0524_alter_newsitemfile_newsitem.js
Updated test cases reflecting optional newsItem; added validation tests for file immutability and newsItem change prevention; added database migration making newsItem column nullable.
Dependencies & Localization
apps/condo/package.json, apps/condo/lang/en/en.json, apps/condo/lang/es/es.json, apps/condo/lang/ru/ru.json
Added runtime dependencies (@ffmpeg/ffmpeg, @ffmpeg/util, heic2any) for file processing; added UI label translations for file field across three languages.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant FilesUploadList
    participant FileConverter
    participant S3API as S3 API
    participant GraphQL
    participant Apollo as Apollo Cache

    User->>FilesUploadList: Select file(s)
    FilesUploadList->>FileConverter: Convert format if needed<br/>(HEIC→JPEG, WebP→JPEG,<br/>MOV/MP4→MP4 via FFmpeg)
    FileConverter->>FileConverter: Generate thumbnail<br/>(canvas-based)
    FileConverter-->>FilesUploadList: Conversion complete
    FilesUploadList->>S3API: Upload converted file
    S3API-->>FilesUploadList: File URL + metadata
    FilesUploadList->>GraphQL: createNewsItemFile mutation<br/>(file, organization, user)
    GraphQL-->>FilesUploadList: Created record (id, file)
    FilesUploadList->>Apollo: Update local state +<br/>dispatch add action
    Apollo-->>User: File rendered in preview
    
    User->>User: Complete form & submit
    User->>GraphQL: Create/update NewsItem
    GraphQL-->>GraphQL: Return newsItemId
    GraphQL->>GraphQL: For each added file:<br/>updateNewsItemFile<br/>(newsItemId)
    GraphQL->>GraphQL: For each deleted file:<br/>updateNewsItemFile<br/>(deletedAt timestamp)
    GraphQL-->>Apollo: Evict allNewsItemFiles cache
    Apollo->>Apollo: Garbage collect
    Apollo-->>User: Form complete
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Poem

🐰 Hop, hop! Fresh files now play,
FFmpeg hops conversions away,
Canvas thumbnails, quick and neat,
Upload magic—complete!
Media files make news so sweet! 🎬✨

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 30.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main change: adding file attachment support to news items with client-side functionality, which is the primary focus of this comprehensive PR.

✏️ 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 feat/condo/DOMA-13015/files-for-news

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 Stylelint (17.6.0)
apps/condo/domains/news/components/FilesUploadList.module.css

ConfigurationError: Could not find "stylelint-config-rational-order". Do you need to install the package or use the "configBasedir" option?
at getModulePath (file:///usr/local/lib/node_modules/stylelint/lib/utils/getModulePath.mjs:29:9)
at loadExtendedConfig (file:///usr/local/lib/node_modules/stylelint/lib/augmentConfig.mjs:241:21)
at extendConfig (file:///usr/local/lib/node_modules/stylelint/lib/augmentConfig.mjs:208:25)
at async augmentConfigBasic (file:///usr/local/lib/node_modules/stylelint/lib/augmentConfig.mjs:73:20)
at async augmentConfigFull (file:///usr/local/lib/node_modules/stylelint/lib/augmentConfig.mjs:126:24)
at async getConfigForFile (file:///usr/local/lib/node_modules/stylelint/lib/getConfigForFile.mjs:102:26)
at async resolveOptionValue (file:///usr/local/lib/node_modules/stylelint/lib/utils/resolveOptionValue.mjs:27:24)
at async standalone (file:///usr/local/lib/node_modules/stylelint/lib/standalone.mjs:204:19)


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

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 7999df9e32

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

return await checkPermissionsInEmployedOrRelatedOrganizations(context, user, organizationId, 'canManageNewsItems')
}
return false
return true
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Enforce org permission when creating detached news files

Returning true here lets any authenticated STAFF user create a NewsItemFile without newsItem/organization checks. Because updates on organization-less files are later authorized by createdBy alone in canManageNewsItemFiles, a staff user without canManageNewsItems can create an orphan file and then attach it to a news item ID they know, bypassing organization-level manage permission checks.

Useful? React with 👍 / 👎.

id: resolvedData?.[fieldPath]?.id || null,
})
const mimetype = fileRecord?.fileMimeType
const [normalizedMimetype] = mimetype.replaceAll(' ', '').split(';')
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Check mimetype exists before normalizing it

This calls replaceAll on mimetype before verifying it is defined. If FileRecord is missing or has no fileMimeType, the hook throws a runtime TypeError instead of the intended UNKNOWN_FILE_TYPE GraphQL error, turning a handled validation case into a server error.

Useful? React with 👍 / 👎.

Comment on lines +90 to +92
setIsLoading(true)
const newsItemFiles = await getNewsItemFiles({
variables: {
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Reset reupload loading state when file copy fails

reuploadFiles sets isLoading to true and then awaits several network/file operations without try/finally. If any fetch/upload/create call rejects, setIsLoading(false) is never reached, so the edit flow can remain stuck in the loading state (LOADING!!!) and users cannot continue.

Useful? React with 👍 / 👎.

@sonarqubecloud
Copy link
Copy Markdown

Quality Gate Failed Quality Gate failed

Failed conditions
1 Security Hotspot
10.7% Duplication on New Code (required ≤ 3%)

See analysis details on SonarQube Cloud

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: 20

Caution

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

⚠️ Outside diff range comments (3)
apps/condo/domains/news/access/NewsItemFile.js (1)

70-81: ⚠️ Potential issue | 🟡 Minor

Implement cleanup mechanism for temporary orphaned NewsItemFile records.

The access control allows STAFF users to create NewsItemFile records without a linked newsItem (line 81 returns true), enabling temporary uploads during form creation. However, no cleanup job or TTL mechanism exists to remove abandoned files that remain unlinked after a reasonable period. Without cleanup, orphaned NewsItemFile records will accumulate indefinitely, consuming database storage and associated file storage.

Implement an automated cleanup process to soft-delete NewsItemFile records that meet both conditions:

  • newsItem is null (not linked to any news item)
  • Created more than a configurable TTL ago (e.g., 24-48 hours)

The cleanup can be implemented as a scheduled task using the existing task infrastructure or as a background job, similar to the pattern in notifyResidentsAboutNewsItem.js.

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

In `@apps/condo/domains/news/access/NewsItemFile.js` around lines 70 - 81, Add a
scheduled cleanup job that soft-deletes orphaned NewsItemFile rows (where
newsItem is null) older than a configurable TTL (e.g., NEWS_ITEM_FILE_TTL);
create a task module (e.g., cleanupOrphanedNewsItemFiles.js) that queries the
NewsItemFile model for records with newsItem == null and createdAt < now - TTL,
then soft-delete them using the same soft-delete method/pattern your app uses
(e.g., set deletedAt or call the existing softDelete helper) and wire the task
into the existing scheduler similar to notifyResidentsAboutNewsItem.js; expose
TTL via config/env and ensure the task logs what it deletes and is idempotent so
it can run regularly.
apps/condo/domains/news/schema/NewsItemFile.js (1)

99-106: ⚠️ Potential issue | 🟠 Major

Guard mimetype before calling replaceAll.

If FileRecord is missing or fileMimeType is empty, Line 103 throws a plain TypeError before your UNKNOWN_FILE_TYPE branch runs. That turns invalid user input into a 500.

Proposed fix
                     const fileRecord = await getByCondition('FileRecord', {
                         id: resolvedData?.[fieldPath]?.id || null,
                     })
                     const mimetype = fileRecord?.fileMimeType
-                    const [normalizedMimetype] = mimetype.replaceAll(' ', '').split(';')
-
-                    if (!normalizedMimetype) {
+                    if (!mimetype) {
                         throw new GQLError(ERRORS.UNKNOWN_FILE_TYPE, context)
                     }
+                    const [normalizedMimetype] = mimetype.replaceAll(' ', '').split(';')
 
                     if (!ALLOWED_MIME_TYPES.includes(normalizedMimetype)) {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/condo/domains/news/schema/NewsItemFile.js` around lines 99 - 106, The
code calls mimetype.replaceAll without guarding mimetype, causing a TypeError
when FileRecord or fileMimeType is missing; before splitting, check that
fileRecord?.fileMimeType (the mimetype variable) is defined/truthy and if not
throw the existing GQLError using ERRORS.UNKNOWN_FILE_TYPE with context; then
compute normalizedMimetype (e.g., after guarding, call replaceAll and split) and
proceed—update the block around getByCondition('FileRecord'), mimetype,
normalizedMimetype, and the GQLError throw to perform the presence check before
calling replaceAll.
apps/condo/domains/news/components/NewsForm/BaseNewsForm.tsx (1)

628-878: ⚠️ Potential issue | 🔴 Critical

Add missing dependencies to handleFormSubmit useCallback.

This callback reads selectAppsFormValues, condoFormValues, sharingAppsFormValues, modifiedFiles, updateNewsItemFile, client, getSelectedAndNotSkippedSharingApps, and selectedProperties, but none are included in the dependency list. This causes the callback to capture stale closures: the initial form state values (often null) prevent proper submission at line 330, file operations at lines 350 and 365 use stale file lists, sharing configs at line 382 use outdated parameters, and property lookups at line 443 reference wrong properties. Add these dependencies to the array at line 878.

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

In `@apps/condo/domains/news/components/NewsForm/BaseNewsForm.tsx` around lines
628 - 878, The useCallback for handleFormSubmit is missing several dependencies
which causes stale closures; update the dependency array of handleFormSubmit to
include selectAppsFormValues, condoFormValues, sharingAppsFormValues,
modifiedFiles, updateNewsItemFile, client, getSelectedAndNotSkippedSharingApps,
selectedProperties (in addition to the existing ones like actionName,
createOrUpdateNewsItem, updateNewsItem, etc.) so the callback sees current form
values, file lists, sharing configs and property data when it runs.
🧹 Nitpick comments (8)
apps/condo/domains/news/components/FilesPreview/DocumentsPreview.tsx (2)

7-7: Add TypeScript types for the files prop.

The files parameter lacks type annotation, resulting in implicit any. This reduces type safety and IDE support. Based on learnings, prefer type aliases for object shapes.

♻️ Proposed fix to add types
+type FileItem = {
+    id?: string
+    uid?: string
+    response?: {
+        originalName?: string
+    }
+}
+
+type DocumentsPreviewProps = {
+    files?: FileItem[]
+}
+
-export const DocumentsPreview = ({ files }) => {
+export const DocumentsPreview: React.FC<DocumentsPreviewProps> = ({ files }) => {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/condo/domains/news/components/FilesPreview/DocumentsPreview.tsx` at line
7, Add a TypeScript type alias for the file object shape (e.g., FileItem or
NewsFile) describing fields used by the component (id, name, url, mimeType,
size, etc.), then annotate the DocumentsPreview component props as ({ files }: {
files: FileItem[] }) or use an interface/type for props; update any internal
variable typings (e.g., map callbacks) to use the new FileItem type so the files
prop is no longer implicit any and IDE/type-checking work as expected.

9-15: Consider extracting inline styles to a CSS module.

The inline styles for border, padding, and border-radius would be more maintainable in a CSS module file, consistent with patterns in other components like InputStepForm.module.css.

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

In `@apps/condo/domains/news/components/FilesPreview/DocumentsPreview.tsx` around
lines 9 - 15, Extract the inline style objects from DocumentsPreview.tsx into a
CSS module (e.g., DocumentsPreview.module.css) and replace the inline props with
className references: move the container styles (width: '100%') currently on Row
and Col into a .container or .fullWidth class, and move the card styles
(borderRadius: 12, border: '1px solid `#E6E8F1`', padding: 12, width: '100%') from
the inner div into a .fileCard class; then import the module into
DocumentsPreview.tsx and apply these classes to the Row, Col and the div inside
files?.map to keep the rendering logic (files?.map, key using file?.id ||
file?.uid || ind, and Typography.Text) unchanged.
apps/condo/domains/news/components/FilesPreview/ImageOrVideoPreview.tsx (1)

85-85: Add TypeScript types for the file prop.

The file parameter lacks type annotation, resulting in implicit any. This reduces type safety and makes the expected file structure unclear to consumers.

♻️ Proposed fix to add types
+type FileItem = {
+    id?: string
+    uid?: string
+    url?: string
+    response?: {
+        url?: string
+        mimetype?: string
+    }
+}
+
+type ImageOrVideoPreviewProps = {
+    file: FileItem
+}
+
-export const ImageOrVideoPreview = ({ file }) => {
+export const ImageOrVideoPreview: React.FC<ImageOrVideoPreviewProps> = ({ file }) => {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/condo/domains/news/components/FilesPreview/ImageOrVideoPreview.tsx` at
line 85, The ImageOrVideoPreview component's parameter lacks a TypeScript type;
add an explicit prop type for file (either create a local interface like
FilePreviewProps { file: { url: string; mimetype?: string; ext?: string; name?:
string; id?: string; /* add any other fields used in the component */ } } or
import an existing domain/media type) and annotate the component signature
(e.g., export const ImageOrVideoPreview: React.FC<FilePreviewProps> = ({ file })
=> { ... }) so consumers and the compiler know the expected file shape and
optional fields used by ImageOrVideoPreview.
apps/condo/domains/news/components/FilesUploadList.module.css (2)

61-70: Consider consolidating duplicate duration styles.

.duration (lines 61-70) and .video-duration (lines 125-134) have identical styling. Consider using a single class or extracting shared styles.

Suggested consolidation
 /* Duration badge - used for both images and videos */
 .duration {
   position: absolute;
   right: 4px;
   bottom: 4px;
   padding: 2px 6px;
   color: `#fff`;
   font-size: 10px;
   background: rgb(0 0 0 / 70%);
   border-radius: 4px;
 }
-
-.video-duration {
-  position: absolute;
-  right: 4px;
-  bottom: 4px;
-  padding: 2px 6px;
-  color: white;
-  font-size: 10px;
-  background: rgb(0 0 0 / 70%);
-  border-radius: 4px;
-}

Then use .duration class for both image and video duration badges in the component.

Also applies to: 125-134

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

In `@apps/condo/domains/news/components/FilesUploadList.module.css` around lines
61 - 70, The .duration and .video-duration blocks are duplicate; remove the
redundancy by keeping a single shared CSS class (e.g., .duration) with the
background, positioning, padding, font-size, color and border-radius, delete or
collapse .video-duration, and update the component to apply .duration for both
image and video duration badges (replace references to video-duration with
duration) so both use the same style rule; ensure no other unique rules from
.video-duration are lost when consolidating.

24-24: Translate Russian comments to English.

The CSS module contains Russian comments that should be in English for consistency with the codebase:

  • Line 24: /* Контент *//* Content */
  • Line 49: /* Название *//* File name */
  • Line 60: /* Длительность *//* Duration */
  • Line 72: /* Overlay */ → (already English)
  • Line 85: /* Кнопки *//* Buttons */
  • Line 109: /* Hover */ → (already English)

Also applies to: 49-49, 60-60, 72-72, 85-85, 109-109

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

In `@apps/condo/domains/news/components/FilesUploadList.module.css` at line 24, In
FilesUploadList.module.css replace the Russian comment tokens with English
equivalents for consistency: change "/* Контент */" to "/* Content */", "/*
Название */" to "/* File name */", "/* Длительность */" to "/* Duration */", and
"/* Кнопки */" to "/* Buttons */" (leave existing English comments "/* Overlay
*/" and "/* Hover */" unchanged); update those exact comment strings in the file
so selectors and spacing remain untouched.
apps/condo/domains/news/components/NewsForm/UpdateNewsForm.tsx (1)

19-21: Import organization: consider grouping relative imports.

The blank line between the BaseNewsForm import (line 18) and the gql imports (lines 20-21) creates an unintended separation. These relative imports from parent directories should be grouped together per the import ordering guidelines.

Suggested fix
 import { SendPeriodType, BaseNewsFormProps, BaseNewsForm } from './BaseNewsForm'
-
-import { useGetNewsItemFilesQuery } from '../../../../gql'
-import { convertFilesToUploadType } from '../FilesUploadList'
+import { useGetNewsItemFilesQuery } from '../../../../gql'
+import { convertFilesToUploadType } from '../FilesUploadList'
+
 export interface IUpdateNewsForm {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/condo/domains/news/components/NewsForm/UpdateNewsForm.tsx` around lines
19 - 21, The import grouping is split by an extra blank line; move the relative
imports useGetNewsItemFilesQuery and convertFilesToUploadType so they sit with
the BaseNewsForm import (group all relative/parent-directory imports together)
in UpdateNewsForm.tsx, remove the blank line, and ensure import order follows
external libs → absolute project imports → grouped relative imports (e.g.,
BaseNewsForm, useGetNewsItemFilesQuery, convertFilesToUploadType).
apps/condo/domains/news/components/NewsForm/InputStep/InputStepFilesSelector.tsx (1)

1-15: Reorder imports to match repository grouping rules

@app/condo/* imports are currently placed before external and @open-condo/* imports. Please reorder by group and keep case-insensitive alphabetical order within each group.

As per coding guidelines, "Enforce import order groups: builtin → external → @open-condo → internal → sibling → parent with newlines between groups" and "Alphabetize imports case-insensitively within each import group".

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

In
`@apps/condo/domains/news/components/NewsForm/InputStep/InputStepFilesSelector.tsx`
around lines 1 - 15, Reorder the import block in InputStepFilesSelector.tsx so
groups follow the repo rule: builtin → external → `@open-condo` → internal (e.g.
`@app/condo`) → sibling/parent, with a blank line between groups; specifically
move the `@app/condo` imports (useCreateNewsItemFileMutation,
useGetNewsItemFilesLazyQuery, CreateNewsItemFileMutation) into the internal
group after the `@open-condo` group, ensure `@open-condo` imports (buildMeta,
uploadFiles, getClientSideSenderInfo, useAuth, useOrganization) come after
external libs (react, antd, next/config, rc-upload, etc.), and alphabetize
imports case-insensitively within each group (also order Action, DBFile,
FilesUploadList among sibling imports accordingly).
apps/condo/domains/news/components/FilesUploadList.tsx (1)

752-752: Use Typography for the file-type badge.

This text is rendered with a raw <span>. The repo standard here is to use @open-condo/ui typography components for text rendering.

As per coding guidelines "Use Typography components from @open-condo/ui for all text rendering instead of custom text elements."

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

In `@apps/condo/domains/news/components/FilesUploadList.tsx` at line 752, Replace
the raw <span> used to render file type in FilesUploadList.tsx with the
Typography component from `@open-condo/ui`: locate the conditional that returns
<span style={{ fontWeight: 'bold', color: 'grey' }}>{typeToView}</span> (the
typeToView render path) and return a <Typography> (or Text/Bold variant per
project conventions) with equivalent styling (bold and grey) instead; also add
the import for the chosen Typography component from '@open-condo/ui' if missing.
Ensure the visual style (fontWeight and color) is preserved via Typography props
or className.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@apps/condo/domains/news/components/FilesPreview/ImageOrVideoPreview.tsx`:
- Around line 10-13: The code assumes canvas.getContext('2d') always returns a
CanvasRenderingContext2D (in img.onload and in createVideoPreviewFromUrl) which
can be null; update the img.onload handler and createVideoPreviewFromUrl to
check the result of const ctx = canvas.getContext('2d') and handle the null case
(e.g., abort/return a rejected Promise or throw a descriptive error) before
using ctx, and ensure any downstream code that uses ctx is guarded so no methods
are called on a possibly null value.
- Around line 88-104: The async process() inside useDeepCompareEffect does not
catch errors from getImagePreviewFromUrl or createVideoPreviewFromUrl; wrap the
body of process() in try/catch (or attach .catch) to handle rejections, log or
otherwise handle the error, and ensure setPreview('') or a fallback is used on
failure; update the process function referenced in useDeepCompareEffect (which
calls getImagePreviewFromUrl/createVideoPreviewFromUrl and setPreview) to
prevent unhandled promise rejections.
- Around line 106-108: The <img> in ImageOrVideoPreview is missing an alt
attribute; update the JSX in the ImageOrVideoPreview component to include a
meaningful alt value (e.g., use a prop or variable like fileName, file.name, or
other available title) and fall back to an empty string if none is available
(alt={fileName ?? ''} or alt={file?.name || 'Preview image'}) so screen readers
and users with images disabled receive appropriate text.

In `@apps/condo/domains/news/components/FilesUploadList.tsx`:
- Around line 370-394: The file picker currently doesn't include 'image/webp' in
its accepted MIME types so the WebP→JPEG path in convertFile (the block handling
file.type === 'image/webp') is never used; update the file input's accept list
and any acceptedMimeTypes/ACCEPTED_MIME_TYPES array used by FilesUploadList (and
the other occurrence referenced at line ~648) to include 'image/webp' so the
picker allows WebP files and the convertFile branch becomes reachable.
- Around line 21-35: The module-level ffmpeg and ffmpegLoaded cause shared state
and mixed progress across parallel uploads; change loadFFmpeg to create and
return a fresh FFmpeg instance per upload (e.g., newFFmpeg or
createFFmpegInstance) instead of reusing the module-scoped ffmpeg/ffmpegLoaded,
attach per-upload 'log' and 'progress' listeners inside the upload flow (not
globally), and ensure cleanup/removeListener only removes the handlers bound to
that instance/closure so one upload cannot remove another upload's listener
(update places that call loadFFmpeg and the progress removal logic to use the
new per-upload instance and per-upload handler references).
- Around line 529-530: The UI message FileTooBigErrorMessage uses
MAX_UPLOAD_FILE_SIZE but the file-size validation still hardcodes 512 MB; update
the validation in FilesUploadList.tsx to compare file.size against
MAX_UPLOAD_FILE_SIZE (instead of 512 * 1024 * 1024) and replace any other
hardcoded 512MB checks (notably the other guard near the later upload handling
around the second occurrence) so both the error message and enforcement derive
from the same MAX_UPLOAD_FILE_SIZE constant.
- Around line 709-734: createAction persists the file but any error thrown while
generating preview (getImageThumbnailFromUrl or createVideoThumbnailFromUrl)
bubbles to the outer catch and marks the whole upload as failed; change the flow
so thumbnail generation is wrapped in its own try/catch: call createAction
first, then attempt thumbnail generation in a separate try block that logs the
thumbnail error (console.error) and falls back to an empty thumbnail, and always
call updateFileList and onSuccess after createAction; only call onError if
createAction itself fails. Ensure you reference createAction,
getImageThumbnailFromUrl, createVideoThumbnailFromUrl, updateFileList, onSuccess
and onError when applying the change.
- Around line 694-699: When convertFile(...) fails in FilesUploadList.tsx, don't
just console.error and rethrow — update the upload state so the item isn't left
stuck in "uploading". In the catch block for convertFile(file, onProgress) call
the component's upload-failure handler (e.g., markFileUploadFailed /
setFileStatus(file.id, 'failed') or invoke the existing onError/onUploadFail
callback) and clear any uploading flags or progress for that file, then
return/exit the flow instead of rethrowing so the UI reflects the failed state.

In `@apps/condo/domains/news/components/NewsForm/InputStep/index.tsx`:
- Around line 316-335: The effect reuploading files currently dispatches every
reuploaded NewsItemFile as modifyFiles({ type: 'add', ... }) which causes
duplicates because submit only deletes items listed in modifiedFiles.deleted;
instead initialize/sync the current files without marking them as "added": after
converting (convertFilesToUploadType) call setFiles and form.setFieldsValue({
'files': setFiles }) then call modifyFiles with a single operation that
replaces/initializes the file state (e.g., modifyFiles({ type: 'set' | 'replace'
| 'init', payload: converted }) or a dedicated sync method) rather than per-file
'add', so existing DB-backed attachments are not treated as newly added.
- Around line 328-329: The code is storing the setter function into the form
instead of the actual file list; after calling setFiles(converted) replace
form.setFieldsValue({ 'files': setFiles }) with form.setFieldsValue({ 'files':
converted }) so the form's files field contains the array of files (converted)
rather than the setFiles function; update the call around the
setFiles(converted) line in InputStep/index.tsx accordingly.

In
`@apps/condo/domains/news/components/NewsForm/InputStep/InputStepFilesSelector.tsx`:
- Around line 198-201: Replace the hardcoded Russian helper texts in the
InputStepFilesSelector component (the Typography.Text nodes) with localized
messages via intl.formatMessage and correct the typo “EXEL” to “EXCEL”; also
replace the placeholder “? МБ” with the final video size limit (use 10 МБ if
that's the project standard) and add appropriate message IDs (e.g.
news.input.files.helperImageVideo and news.input.files.helperDocument) so both
lines call intl.formatMessage({ id: '...' }) and include the finalized limits
and corrected spelling.
- Around line 90-109: The code path that calls getNewsItemFiles and
reuploadNewsItemFile can throw and never reaches setIsLoading(false), leaving
the component stuck loading; wrap the async logic inside a try/catch/finally
around the block that calls getNewsItemFiles, maps files, and awaits
reuploadNewsItemFile so you can handle errors and always call
setIsLoading(false) in the finally block, logging or handling errors in catch
and still calling setReuploadedNewsItemFiles(result) only on success (or set an
empty result on failure) to ensure consistent state.
- Around line 158-167: The fallback branch in InputStepFilesSelector assigns the
raw rc-upload file object to createInput which is not the uploaded-file payload
expected by the createNewsItemFile mutation; update the fallback to build the
same shape used in the successful-upload branch (include signature,
originalFilename, mimetype and any other fields the GraphQL input expects) by
extracting values from the rc-upload object (e.g., originFileObj, name, type) or
by reusing the upload response transform used elsewhere before calling
createNewsItemFile with that properly shaped createInput.
- Around line 36-40: The state and callback types mismatch: change
reuploadedNewsItemFiles to allow null by typing useState as
Array<CreateNewsItemFileMutation['newsItemFile']> | null and keep the initial
value null; update reuploadNewsItemFile's signature to return
Promise<CreateNewsItemFileMutation['newsItemFile'] | null> (or
Promise<NonNullable<...> | null>) and replace the bare early `return` with
`return null`, so callers and the later .filter(Boolean) usage align with the
nullable contract; adjust any related setReuploadedNewsItemFiles usage if
needed.

In `@apps/condo/domains/news/components/NewsPreview.tsx`:
- Around line 222-238: CondoAppPreview's filesByGroups useMemo assumes files is
always an array and calls files.reduce(...), which will throw when files is
undefined; update the useMemo to guard/normalize files first (e.g., treat files
= files || [] before computing) so isImageOrVideo and the reduce operate on a
safe array; refer to the filesByGroups constant, the useMemo callback and the
isImageOrVideo helper when making the change.

In `@apps/condo/domains/news/queries/NewsItemFile.graphql`:
- Around line 15-23: The GraphQL query getNewsItemFiles currently sets a default
cap ($first: Int = 10) which hides attachments beyond the 10th; remove the
default limit by changing the variable signature to use $first: Int (i.e.,
delete "= 10") so the query does not silently cap results, and then update
callers that need the full set (detail and reupload flows) to pass an explicit
high/appropriate first value or omit first if your GraphQL server returns all
when absent.

In `@apps/condo/gql/index.ts`:
- Around line 3355-3399: The generated query getNewsItemFiles
(GetNewsItemFilesDocument) defaults first: 10 with no skip, causing truncation;
update the source GraphQL (apps/condo/domains/news/queries/NewsItemFile.graphql)
to add a skip: Int = 0 parameter and include skip in the allNewsItemFiles call,
then regenerate the codegen so the exported hooks (useGetNewsItemFilesQuery,
useGetNewsItemFilesLazyQuery, useGetNewsItemFilesSuspenseQuery) accept and
forward skip via Types.GetNewsItemFilesQueryVariables to avoid silently limiting
results.

In `@apps/condo/lang/es/es.json`:
- Line 1594: The label value for the key "news.fields.files.label" is
inconsistent with other News action verbs; update its string from "Agregar
archivos multimedia" to use the same verb as the rest of the locale (e.g.,
"Añadir archivos multimedia") by editing the value for news.fields.files.label
so it matches the existing "Añadir" phrasing used across the News labels.

In `@apps/condo/migrations/20260413232745-0524_alter_newsitemfile_newsitem.js`:
- Around line 19-30: The down migration (exports.down) blindly runs ALTER TABLE
"NewsItemFile" ALTER COLUMN "newsItem" SET NOT NULL which will fail if any
NewsItemFile rows have newsItem = NULL; before applying the NOT NULL change, add
a safe step to handle or remove orphaned rows — e.g., delete or update rows
where "newsItem" IS NULL (or fail with a clear message), ensuring the migration
uses the "NewsItemFile" table and "newsItem" column and preserves/adjusts the
existing constraint "NewsItemFile_newsItem_7d5eb5a1_fk_NewsItem_id" accordingly
so the ALTER COLUMN can succeed.

In `@apps/condo/pages/news/`[id]/index.tsx:
- Around line 128-140: The attachment query currently only destructures loading
and data from useGetNewsItemFilesQuery (assigned to
newsItemFilesLoading/newsItemFilesData) and the UI's hasError check doesn't
include query errors, so failures render as "no files"; update the hook
destructuring to also get error (e.g., newsItemFilesError) and incorporate that
into the component's error state/hasError logic, then render an explicit error
UI/message for attachments when newsItemFilesError is truthy instead of the
empty media section; apply the same change to the other similar query block
around the second occurrence (the block at ~lines 308-313) so both attachment
queries expose and handle their error values.

---

Outside diff comments:
In `@apps/condo/domains/news/access/NewsItemFile.js`:
- Around line 70-81: Add a scheduled cleanup job that soft-deletes orphaned
NewsItemFile rows (where newsItem is null) older than a configurable TTL (e.g.,
NEWS_ITEM_FILE_TTL); create a task module (e.g.,
cleanupOrphanedNewsItemFiles.js) that queries the NewsItemFile model for records
with newsItem == null and createdAt < now - TTL, then soft-delete them using the
same soft-delete method/pattern your app uses (e.g., set deletedAt or call the
existing softDelete helper) and wire the task into the existing scheduler
similar to notifyResidentsAboutNewsItem.js; expose TTL via config/env and ensure
the task logs what it deletes and is idempotent so it can run regularly.

In `@apps/condo/domains/news/components/NewsForm/BaseNewsForm.tsx`:
- Around line 628-878: The useCallback for handleFormSubmit is missing several
dependencies which causes stale closures; update the dependency array of
handleFormSubmit to include selectAppsFormValues, condoFormValues,
sharingAppsFormValues, modifiedFiles, updateNewsItemFile, client,
getSelectedAndNotSkippedSharingApps, selectedProperties (in addition to the
existing ones like actionName, createOrUpdateNewsItem, updateNewsItem, etc.) so
the callback sees current form values, file lists, sharing configs and property
data when it runs.

In `@apps/condo/domains/news/schema/NewsItemFile.js`:
- Around line 99-106: The code calls mimetype.replaceAll without guarding
mimetype, causing a TypeError when FileRecord or fileMimeType is missing; before
splitting, check that fileRecord?.fileMimeType (the mimetype variable) is
defined/truthy and if not throw the existing GQLError using
ERRORS.UNKNOWN_FILE_TYPE with context; then compute normalizedMimetype (e.g.,
after guarding, call replaceAll and split) and proceed—update the block around
getByCondition('FileRecord'), mimetype, normalizedMimetype, and the GQLError
throw to perform the presence check before calling replaceAll.

---

Nitpick comments:
In `@apps/condo/domains/news/components/FilesPreview/DocumentsPreview.tsx`:
- Line 7: Add a TypeScript type alias for the file object shape (e.g., FileItem
or NewsFile) describing fields used by the component (id, name, url, mimeType,
size, etc.), then annotate the DocumentsPreview component props as ({ files }: {
files: FileItem[] }) or use an interface/type for props; update any internal
variable typings (e.g., map callbacks) to use the new FileItem type so the files
prop is no longer implicit any and IDE/type-checking work as expected.
- Around line 9-15: Extract the inline style objects from DocumentsPreview.tsx
into a CSS module (e.g., DocumentsPreview.module.css) and replace the inline
props with className references: move the container styles (width: '100%')
currently on Row and Col into a .container or .fullWidth class, and move the
card styles (borderRadius: 12, border: '1px solid `#E6E8F1`', padding: 12, width:
'100%') from the inner div into a .fileCard class; then import the module into
DocumentsPreview.tsx and apply these classes to the Row, Col and the div inside
files?.map to keep the rendering logic (files?.map, key using file?.id ||
file?.uid || ind, and Typography.Text) unchanged.

In `@apps/condo/domains/news/components/FilesPreview/ImageOrVideoPreview.tsx`:
- Line 85: The ImageOrVideoPreview component's parameter lacks a TypeScript
type; add an explicit prop type for file (either create a local interface like
FilePreviewProps { file: { url: string; mimetype?: string; ext?: string; name?:
string; id?: string; /* add any other fields used in the component */ } } or
import an existing domain/media type) and annotate the component signature
(e.g., export const ImageOrVideoPreview: React.FC<FilePreviewProps> = ({ file })
=> { ... }) so consumers and the compiler know the expected file shape and
optional fields used by ImageOrVideoPreview.

In `@apps/condo/domains/news/components/FilesUploadList.module.css`:
- Around line 61-70: The .duration and .video-duration blocks are duplicate;
remove the redundancy by keeping a single shared CSS class (e.g., .duration)
with the background, positioning, padding, font-size, color and border-radius,
delete or collapse .video-duration, and update the component to apply .duration
for both image and video duration badges (replace references to video-duration
with duration) so both use the same style rule; ensure no other unique rules
from .video-duration are lost when consolidating.
- Line 24: In FilesUploadList.module.css replace the Russian comment tokens with
English equivalents for consistency: change "/* Контент */" to "/* Content */",
"/* Название */" to "/* File name */", "/* Длительность */" to "/* Duration */",
and "/* Кнопки */" to "/* Buttons */" (leave existing English comments "/*
Overlay */" and "/* Hover */" unchanged); update those exact comment strings in
the file so selectors and spacing remain untouched.

In `@apps/condo/domains/news/components/FilesUploadList.tsx`:
- Line 752: Replace the raw <span> used to render file type in
FilesUploadList.tsx with the Typography component from `@open-condo/ui`: locate
the conditional that returns <span style={{ fontWeight: 'bold', color: 'grey'
}}>{typeToView}</span> (the typeToView render path) and return a <Typography>
(or Text/Bold variant per project conventions) with equivalent styling (bold and
grey) instead; also add the import for the chosen Typography component from
'@open-condo/ui' if missing. Ensure the visual style (fontWeight and color) is
preserved via Typography props or className.

In
`@apps/condo/domains/news/components/NewsForm/InputStep/InputStepFilesSelector.tsx`:
- Around line 1-15: Reorder the import block in InputStepFilesSelector.tsx so
groups follow the repo rule: builtin → external → `@open-condo` → internal (e.g.
`@app/condo`) → sibling/parent, with a blank line between groups; specifically
move the `@app/condo` imports (useCreateNewsItemFileMutation,
useGetNewsItemFilesLazyQuery, CreateNewsItemFileMutation) into the internal
group after the `@open-condo` group, ensure `@open-condo` imports (buildMeta,
uploadFiles, getClientSideSenderInfo, useAuth, useOrganization) come after
external libs (react, antd, next/config, rc-upload, etc.), and alphabetize
imports case-insensitively within each group (also order Action, DBFile,
FilesUploadList among sibling imports accordingly).

In `@apps/condo/domains/news/components/NewsForm/UpdateNewsForm.tsx`:
- Around line 19-21: The import grouping is split by an extra blank line; move
the relative imports useGetNewsItemFilesQuery and convertFilesToUploadType so
they sit with the BaseNewsForm import (group all relative/parent-directory
imports together) in UpdateNewsForm.tsx, remove the blank line, and ensure
import order follows external libs → absolute project imports → grouped relative
imports (e.g., BaseNewsForm, useGetNewsItemFilesQuery,
convertFilesToUploadType).
🪄 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: defaults

Review profile: CHILL

Plan: Pro

Run ID: 0e8c6725-5b3c-452a-b668-21323c59f7d5

📥 Commits

Reviewing files that changed from the base of the PR and between 978100c and 7999df9.

⛔ Files ignored due to path filters (1)
  • yarn.lock is excluded by !**/yarn.lock, !**/*.lock
📒 Files selected for processing (27)
  • apps/condo/domains/common/utils/next/apollo.ts
  • apps/condo/domains/news/access/NewsItemFile.js
  • apps/condo/domains/news/components/FilesPreview/DocumentsPreview.tsx
  • apps/condo/domains/news/components/FilesPreview/ImageOrVideoPreview.tsx
  • apps/condo/domains/news/components/FilesUploadList.module.css
  • apps/condo/domains/news/components/FilesUploadList.tsx
  • apps/condo/domains/news/components/NewsForm/BaseNewsForm.tsx
  • apps/condo/domains/news/components/NewsForm/InputStep/InputStepFilesSelector.tsx
  • apps/condo/domains/news/components/NewsForm/InputStep/InputStepForm.tsx
  • apps/condo/domains/news/components/NewsForm/InputStep/InputStepPreview.tsx
  • apps/condo/domains/news/components/NewsForm/InputStep/InputStepRecipientCounter.tsx
  • apps/condo/domains/news/components/NewsForm/InputStep/InputStepSelector.tsx
  • apps/condo/domains/news/components/NewsForm/InputStep/index.tsx
  • apps/condo/domains/news/components/NewsForm/UpdateNewsForm.tsx
  • apps/condo/domains/news/components/NewsPreview.tsx
  • apps/condo/domains/news/queries/NewsItemFile.graphql
  • apps/condo/domains/news/schema/NewsItemFile.js
  • apps/condo/domains/news/schema/NewsItemFile.test.js
  • apps/condo/domains/news/utils/testSchema/index.js
  • apps/condo/gql/index.ts
  • apps/condo/gql/operation.types.ts
  • apps/condo/lang/en/en.json
  • apps/condo/lang/es/es.json
  • apps/condo/lang/ru/ru.json
  • apps/condo/migrations/20260413232745-0524_alter_newsitemfile_newsitem.js
  • apps/condo/package.json
  • apps/condo/pages/news/[id]/index.tsx

Comment on lines +10 to +13
img.onload = () => {
const canvas = document.createElement('canvas')
const ctx = canvas.getContext('2d')

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

Handle potential null canvas context.

getContext('2d') can return null if the context identifier is not supported. Using ctx without a null check can cause runtime errors.

🛡️ Proposed fix to add null checks
 img.onload = () => {
     const canvas = document.createElement('canvas')
     const ctx = canvas.getContext('2d')
+    if (!ctx) {
+        reject(new Error('Canvas 2D context not supported'))
+        return
+    }

     const WIDTH = 1280

Apply similar fix in createVideoPreviewFromUrl:

 const video = document.createElement('video')
 const canvas = document.createElement('canvas')
 const ctx = canvas.getContext('2d')
+if (!ctx) {
+    reject(new Error('Canvas 2D context not supported'))
+    return
+}

Also applies to: 43-46

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

In `@apps/condo/domains/news/components/FilesPreview/ImageOrVideoPreview.tsx`
around lines 10 - 13, The code assumes canvas.getContext('2d') always returns a
CanvasRenderingContext2D (in img.onload and in createVideoPreviewFromUrl) which
can be null; update the img.onload handler and createVideoPreviewFromUrl to
check the result of const ctx = canvas.getContext('2d') and handle the null case
(e.g., abort/return a rejected Promise or throw a descriptive error) before
using ctx, and ensure any downstream code that uses ctx is guarded so no methods
are called on a possibly null value.

Comment on lines +88 to +104
useDeepCompareEffect(() => {
const process = async () => {
const url = file?.response?.url || file?.url

let thumb = ''
if (file.response?.mimetype?.startsWith('image/')) {
thumb = await getImagePreviewFromUrl(url)
}
if (file.response?.mimetype?.startsWith('video/')) {
thumb = await createVideoPreviewFromUrl(url)
}

setPreview(thumb)
}

process()
}, [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

Handle errors from async thumbnail generation.

The process() function is async but errors are not caught. If getImagePreviewFromUrl or createVideoPreviewFromUrl rejects, it will cause an unhandled promise rejection.

🛡️ Proposed fix to add error handling
     useDeepCompareEffect(() => {
         const process = async () => {
             const url = file?.response?.url || file?.url
+            if (!url) return

             let thumb = ''
-            if (file.response?.mimetype?.startsWith('image/')) {
-                thumb = await getImagePreviewFromUrl(url)
-            }
-            if (file.response?.mimetype?.startsWith('video/')) {
-                thumb = await createVideoPreviewFromUrl(url)
+            try {
+                if (file.response?.mimetype?.startsWith('image/')) {
+                    thumb = await getImagePreviewFromUrl(url)
+                } else if (file.response?.mimetype?.startsWith('video/')) {
+                    thumb = await createVideoPreviewFromUrl(url)
+                }
+            } catch (error) {
+                console.error('Failed to generate preview:', error)
+                return
             }

             setPreview(thumb)
         }

         process()
     }, [file])
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/condo/domains/news/components/FilesPreview/ImageOrVideoPreview.tsx`
around lines 88 - 104, The async process() inside useDeepCompareEffect does not
catch errors from getImagePreviewFromUrl or createVideoPreviewFromUrl; wrap the
body of process() in try/catch (or attach .catch) to handle rejections, log or
otherwise handle the error, and ensure setPreview('') or a fallback is used on
failure; update the process function referenced in useDeepCompareEffect (which
calls getImagePreviewFromUrl/createVideoPreviewFromUrl and setPreview) to
prevent unhandled promise rejections.

Comment on lines +106 to +108
return (
<img src={preview} style={{ borderRadius: 12, border: '1px solid #E6E8F1', width: '100%' }} />
)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Add alt attribute for accessibility.

The <img> element is missing an alt attribute, which is required for accessibility. Screen readers and users with images disabled need this information.

🛡️ Proposed fix
-        <img src={preview} style={{ borderRadius: 12, border: '1px solid `#E6E8F1`', width: '100%' }} />
+        <img src={preview} alt="" style={{ borderRadius: 12, border: '1px solid `#E6E8F1`', width: '100%' }} />

Consider passing a meaningful alt text if the file name is available:

-        <img src={preview} style={{ borderRadius: 12, border: '1px solid `#E6E8F1`', width: '100%' }} />
+        <img
+            src={preview}
+            alt={file?.response?.originalName || 'Preview'}
+            style={{ borderRadius: 12, border: '1px solid `#E6E8F1`', width: '100%' }}
+        />
📝 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
return (
<img src={preview} style={{ borderRadius: 12, border: '1px solid #E6E8F1', width: '100%' }} />
)
return (
<img src={preview} alt="" style={{ borderRadius: 12, border: '1px solid `#E6E8F1`', width: '100%' }} />
)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/condo/domains/news/components/FilesPreview/ImageOrVideoPreview.tsx`
around lines 106 - 108, The <img> in ImageOrVideoPreview is missing an alt
attribute; update the JSX in the ImageOrVideoPreview component to include a
meaningful alt value (e.g., use a prop or variable like fileName, file.name, or
other available title) and fall back to an empty string if none is available
(alt={fileName ?? ''} or alt={file?.name || 'Preview image'}) so screen readers
and users with images disabled receive appropriate text.

Comment on lines +21 to +35
let ffmpeg: FFmpeg | null = null
let ffmpegLoaded = false

const loadFFmpeg = async () => {
if (!ffmpeg) {
ffmpeg = new FFmpeg()
ffmpeg.on('log', ({ message }) => {
console.log(message)
})
}
if (!ffmpegLoaded) {
await ffmpeg.load()
ffmpegLoaded = true
}
}
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

Isolate FFmpeg state per upload.

multiple allows parallel video uploads, but every request shares the same module-level ffmpeg instance and the same progress listener set. Two concurrent conversions will report mixed progress, and one request can remove the other request’s handler on Line 424.

Also applies to: 397-425, 647-647

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

In `@apps/condo/domains/news/components/FilesUploadList.tsx` around lines 21 - 35,
The module-level ffmpeg and ffmpegLoaded cause shared state and mixed progress
across parallel uploads; change loadFFmpeg to create and return a fresh FFmpeg
instance per upload (e.g., newFFmpeg or createFFmpegInstance) instead of reusing
the module-scoped ffmpeg/ffmpegLoaded, attach per-upload 'log' and 'progress'
listeners inside the upload flow (not globally), and ensure
cleanup/removeListener only removes the handlers bound to that instance/closure
so one upload cannot remove another upload's listener (update places that call
loadFFmpeg and the progress removal logic to use the new per-upload instance and
per-upload handler references).

Comment on lines +370 to +394
// 🖼 WebP → JPEG
if (file.type === 'image/webp') {
const img = await new Promise<HTMLImageElement>((resolve, reject) => {
const i = new Image()
i.onload = () => resolve(i)
i.onerror = reject
i.src = URL.createObjectURL(file)
})

const canvas = document.createElement('canvas')
canvas.width = img.width
canvas.height = img.height
const ctx = canvas.getContext('2d')
ctx.drawImage(img, 0, 0)

const blob = await new Promise<Blob | null>((resolve) => {
canvas.toBlob(resolve, 'image/jpeg', 0.9)
})

return new File(
[blob],
file.name.replace(/\.webp$/i, '.jpeg'),
{ type: 'image/jpeg' }
)
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Add WebP to the accepted mime list.

convertFile already has a WebP → JPEG path, but the picker does not accept image/webp, so that branch is unreachable from normal file selection.

Also applies to: 648-648

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

In `@apps/condo/domains/news/components/FilesUploadList.tsx` around lines 370 -
394, The file picker currently doesn't include 'image/webp' in its accepted MIME
types so the WebP→JPEG path in convertFile (the block handling file.type ===
'image/webp') is never used; update the file input's accept list and any
acceptedMimeTypes/ACCEPTED_MIME_TYPES array used by FilesUploadList (and the
other occurrence referenced at line ~648) to include 'image/webp' so the picker
allows WebP files and the convertFile branch becomes reachable.

Comment on lines +15 to +23
query getNewsItemFiles ($where: NewsItemFileWhereInput!, $sortBy: [SortNewsItemFilesBy!] = [createdAt_ASC], $first: Int = 10) {
newsItemFiles: allNewsItemFiles(
where: $where,
sortBy: $sortBy,
first: $first
) {
id
file { publicUrl originalFilename mimetype }
}
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

Don’t cap attachment fetches at 10 by default.

This query is reused for the detail page and the reupload flow, and both paths expect the full attachment set. With first: 10 as the default, the 11th+ file silently disappears from view/edit unless every caller remembers to override it.

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

In `@apps/condo/domains/news/queries/NewsItemFile.graphql` around lines 15 - 23,
The GraphQL query getNewsItemFiles currently sets a default cap ($first: Int =
10) which hides attachments beyond the 10th; remove the default limit by
changing the variable signature to use $first: Int (i.e., delete "= 10") so the
query does not silently cap results, and then update callers that need the full
set (detail and reupload flows) to pass an explicit high/appropriate first value
or omit first if your GraphQL server returns all when absent.

Comment on lines +3355 to +3399
query getNewsItemFiles($where: NewsItemFileWhereInput!, $sortBy: [SortNewsItemFilesBy!] = [createdAt_ASC], $first: Int = 10) {
newsItemFiles: allNewsItemFiles(where: $where, sortBy: $sortBy, first: $first) {
id
file {
publicUrl
originalFilename
mimetype
}
}
}
`;

/**
* __useGetNewsItemFilesQuery__
*
* To run a query within a React component, call `useGetNewsItemFilesQuery` and pass it any options that fit your needs.
* When your component renders, `useGetNewsItemFilesQuery` returns an object from Apollo Client that contains loading, error, and data properties
* you can use to render your UI.
*
* @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options;
*
* @example
* const { data, loading, error } = useGetNewsItemFilesQuery({
* variables: {
* where: // value for 'where'
* sortBy: // value for 'sortBy'
* first: // value for 'first'
* },
* });
*/
export function useGetNewsItemFilesQuery(baseOptions: Apollo.QueryHookOptions<Types.GetNewsItemFilesQuery, Types.GetNewsItemFilesQueryVariables> & ({ variables: Types.GetNewsItemFilesQueryVariables; skip?: boolean; } | { skip: boolean; }) ) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useQuery<Types.GetNewsItemFilesQuery, Types.GetNewsItemFilesQueryVariables>(GetNewsItemFilesDocument, options);
}
export function useGetNewsItemFilesLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<Types.GetNewsItemFilesQuery, Types.GetNewsItemFilesQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useLazyQuery<Types.GetNewsItemFilesQuery, Types.GetNewsItemFilesQueryVariables>(GetNewsItemFilesDocument, options);
}
// @ts-ignore
export function useGetNewsItemFilesSuspenseQuery(baseOptions?: Apollo.SuspenseQueryHookOptions<Types.GetNewsItemFilesQuery, Types.GetNewsItemFilesQueryVariables>): Apollo.UseSuspenseQueryResult<Types.GetNewsItemFilesQuery, Types.GetNewsItemFilesQueryVariables>;
export function useGetNewsItemFilesSuspenseQuery(baseOptions?: Apollo.SkipToken | Apollo.SuspenseQueryHookOptions<Types.GetNewsItemFilesQuery, Types.GetNewsItemFilesQueryVariables>): Apollo.UseSuspenseQueryResult<Types.GetNewsItemFilesQuery | undefined, Types.GetNewsItemFilesQueryVariables>;
export function useGetNewsItemFilesSuspenseQuery(baseOptions?: Apollo.SkipToken | Apollo.SuspenseQueryHookOptions<Types.GetNewsItemFilesQuery, Types.GetNewsItemFilesQueryVariables>) {
const options = baseOptions === Apollo.skipToken ? baseOptions : {...defaultOptions, ...baseOptions}
return Apollo.useSuspenseQuery<Types.GetNewsItemFilesQuery, Types.GetNewsItemFilesQueryVariables>(GetNewsItemFilesDocument, options);
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# 1) Locate source query file and inspect current definition
fd -i 'NewsItemFile.graphql'
rg -n -C3 'query\s+getNewsItemFiles|allNewsItemFiles|first:\s*\$first|skip:\s*\$skip' apps/condo/domains/news

# 2) Check all hook callsites and whether they pass `first`
rg -n -C4 '\buseGetNewsItemFilesQuery\s*\(' --type=ts --type=tsx

# 3) Check for explicit attachment-count limits in news domain
rg -n -C3 'max.*files|files.*max|attachments?.*limit|MAX_.*FILES|limit.*attachments?' apps/condo/domains/news --type=ts --type=tsx

Repository: open-condo-software/condo

Length of output: 2366


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Find all callsites of useGetNewsItemFilesQuery (without file type restriction)
echo "=== Callsites of useGetNewsItemFilesQuery ==="
rg -n -C4 'useGetNewsItemFilesQuery\s*\(' apps/condo --type-list | head -20
rg 'useGetNewsItemFilesQuery' apps/condo -n -C5 2>/dev/null | head -100

# Check for any attachment count limits
echo -e "\n=== Attachment limits in news domain ==="
rg 'attach|file' apps/condo/domains/news -i -n --type-list 2>/dev/null | grep -i 'max\|limit' | head -20
rg -i '(max.*file|file.*max|attach.*limit|limit.*attach)' apps/condo/domains/news -n -C2 2>/dev/null | head -100

Repository: open-condo-software/condo

Length of output: 521


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Find all callsites of useGetNewsItemFilesQuery
echo "=== Callsites of useGetNewsItemFilesQuery ==="
rg 'useGetNewsItemFilesQuery' apps/condo -n -C6

# Search for file/attachment limits
echo -e "\n=== File attachment limits ==="
rg -i 'max.*attachment|attach.*max|file.*limit|limit.*file' apps/condo/domains/news -n -C2

# Check NewsItemFile.graphql for full definition
echo -e "\n=== Full NewsItemFile.graphql ==="
cat apps/condo/domains/news/queries/NewsItemFile.graphql

Repository: open-condo-software/condo

Length of output: 8663


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Get full context around the first callsite
echo "=== First callsite: news/[id]/index.tsx ==="
sed -n '131,145p' apps/condo/pages/news/[id]/index.tsx

# Get full context around the second callsite
echo -e "\n=== Second callsite: UpdateNewsForm.tsx ==="
sed -n '87,100p' apps/condo/domains/news/components/NewsForm/UpdateNewsForm.tsx

# Check if there's any upload limit or attachment validation in the form
echo -e "\n=== File upload validation/limits ==="
rg -i 'uploadfile|maxfilecount|maxfiles|filelist.*length|files.length' apps/condo/domains/news/components -n -C2 | head -50

Repository: open-condo-software/condo

Length of output: 4867


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Check for backend validation on NewsItemFile
echo "=== Backend validation for NewsItemFile ==="
find apps/condo/domains/news -name '*.ts' -o -name '*.js' | xargs grep -l 'NewsItemFile' | head -10

# Look for any limits in resolvers or hooks
echo -e "\n=== File creation/validation logic ==="
rg 'createNewsItemFile|allNewsItemFiles|MAX_.*FILE|.*\.length.*>.*10|validateFile' apps/condo/domains/news --type=ts -n -C3 | head -80

# Check if files.length is used anywhere that might indicate UI-side limit
echo -e "\n=== Files array checks in UI ==="
rg 'files\.length|fileList\.length' apps/condo/domains/news/components -n -C2 | head -50

Repository: open-condo-software/condo

Length of output: 7613


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Check NewsItemFile schema definition
echo "=== NewsItemFile schema ==="
cat apps/condo/domains/news/schema/NewsItemFile.js

# Check if there's a max file limit in constants
echo -e "\n=== Common constants for uploads ==="
cat apps/condo/domains/common/constants/uploads.ts 2>/dev/null || cat apps/condo/domains/common/constants/uploads.js 2>/dev/null || find apps/condo -name "*upload*" -path "*/constants/*" -type f -exec cat {} \;

Repository: open-condo-software/condo

Length of output: 5401


Data loss risk: getNewsItemFiles defaults to 10 results with no pagination.

Line 3355 sets first: Int = 10 and the query has no skip parameter. Both callsites (news/[id]/index.tsx and UpdateNewsForm.tsx) omit the first argument, relying entirely on the default limit. Since the backend enforces no file count limit, news items with >10 attachments will silently truncate—displaying only the first 10 files when viewed or edited.

Add a skip parameter to the source query in apps/condo/domains/news/queries/NewsItemFile.graphql, then regenerate the codegen output:

Suggested fix
- query getNewsItemFiles ($where: NewsItemFileWhereInput!, $sortBy: [SortNewsItemFilesBy!] = [createdAt_ASC], $first: Int = 10) {
-     newsItemFiles: allNewsItemFiles(
-         where: $where,
-         sortBy: $sortBy,
-         first: $first
-     ) {
+ query getNewsItemFiles (
+     $where: NewsItemFileWhereInput!,
+     $sortBy: [SortNewsItemFilesBy!] = [createdAt_ASC],
+     $first: Int = 100,
+     $skip: Int = 0
+ ) {
+     newsItemFiles: allNewsItemFiles(
+         where: $where,
+         sortBy: $sortBy,
+         first: $first,
+         skip: $skip
+     ) {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/condo/gql/index.ts` around lines 3355 - 3399, The generated query
getNewsItemFiles (GetNewsItemFilesDocument) defaults first: 10 with no skip,
causing truncation; update the source GraphQL
(apps/condo/domains/news/queries/NewsItemFile.graphql) to add a skip: Int = 0
parameter and include skip in the allNewsItemFiles call, then regenerate the
codegen so the exported hooks (useGetNewsItemFilesQuery,
useGetNewsItemFilesLazyQuery, useGetNewsItemFilesSuspenseQuery) accept and
forward skip via Types.GetNewsItemFilesQueryVariables to avoid silently limiting
results.

"news.component.RecipientCounter.willReceive.label": "recibirá \nlocales",
"news.create.title": "Nueva noticia",
"news.createNews": "Añadir noticia",
"news.fields.files.label": "Agregar archivos multimedia",
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Use consistent Spanish action verb in News form labels.

Line 1594 uses “Agregar” while nearby News actions use “Añadir”. For consistency in this locale, align this label with existing wording.

Suggested change
-  "news.fields.files.label": "Agregar archivos multimedia",
+  "news.fields.files.label": "Añadir archivos multimedia",
📝 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
"news.fields.files.label": "Agregar archivos multimedia",
"news.fields.files.label": "Añadir archivos multimedia",
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/condo/lang/es/es.json` at line 1594, The label value for the key
"news.fields.files.label" is inconsistent with other News action verbs; update
its string from "Agregar archivos multimedia" to use the same verb as the rest
of the locale (e.g., "Añadir archivos multimedia") by editing the value for
news.fields.files.label so it matches the existing "Añadir" phrasing used across
the News labels.

Comment on lines +19 to +30
exports.down = async (knex) => {
await knex.raw(`
BEGIN;
--
-- Alter field newsItem on newsitemfile
--
SET CONSTRAINTS "NewsItemFile_newsItem_7d5eb5a1_fk_NewsItem_id" IMMEDIATE; ALTER TABLE "NewsItemFile" DROP CONSTRAINT "NewsItemFile_newsItem_7d5eb5a1_fk_NewsItem_id";
ALTER TABLE "NewsItemFile" ALTER COLUMN "newsItem" SET NOT NULL;
ALTER TABLE "NewsItemFile" ADD CONSTRAINT "NewsItemFile_newsItem_7d5eb5a1_fk_NewsItem_id" FOREIGN KEY ("newsItem") REFERENCES "NewsItem" ("id") DEFERRABLE INITIALLY DEFERRED;
COMMIT;

`)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Down migration will fail if NULL newsItem records exist.

The down migration's ALTER COLUMN "newsItem" SET NOT NULL will fail if any NewsItemFile records have NULL in the newsItem column. This is expected behavior, but consider adding a comment or ensuring orphaned files are cleaned up before running the down migration.

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

In `@apps/condo/migrations/20260413232745-0524_alter_newsitemfile_newsitem.js`
around lines 19 - 30, The down migration (exports.down) blindly runs ALTER TABLE
"NewsItemFile" ALTER COLUMN "newsItem" SET NOT NULL which will fail if any
NewsItemFile rows have newsItem = NULL; before applying the NOT NULL change, add
a safe step to handle or remove orphaned rows — e.g., delete or update rows
where "newsItem" IS NULL (or fail with a clear message), ensuring the migration
uses the "NewsItemFile" table and "newsItem" column and preserves/adjusts the
existing constraint "NewsItemFile_newsItem_7d5eb5a1_fk_NewsItem_id" accordingly
so the ALTER COLUMN can succeed.

Comment on lines +128 to +140
const {
loading: newsItemFilesLoading,
data: newsItemFilesData,
} = useGetNewsItemFilesQuery({
variables: {
where: {
newsItem: {
id: newsItemId,
},
},
},
skip: !persistor || !newsItemId,
})
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

Handle attachment-query failures instead of rendering an empty media section.

useGetNewsItemFilesQuery never exposes its error, and hasError does not account for it. If this request fails, the page renders as if the news item simply has no files, which hides real data loss from the user.

Also applies to: 308-313

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

In `@apps/condo/pages/news/`[id]/index.tsx around lines 128 - 140, The attachment
query currently only destructures loading and data from useGetNewsItemFilesQuery
(assigned to newsItemFilesLoading/newsItemFilesData) and the UI's hasError check
doesn't include query errors, so failures render as "no files"; update the hook
destructuring to also get error (e.g., newsItemFilesError) and incorporate that
into the component's error state/hasError logic, then render an explicit error
UI/message for attachments when newsItemFilesError is truthy instead of the
empty media section; apply the same change to the other similar query block
around the second occurrence (the block at ~lines 308-313) so both attachment
queries expose and handle their error values.

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

Labels

None yet

Development

Successfully merging this pull request may close these issues.

1 participant