Skip to content

feat(widgets): Add Drive widget#2347

Draft
ecnivtwelve wants to merge 9 commits intomasterfrom
feat/widgets
Draft

feat(widgets): Add Drive widget#2347
ecnivtwelve wants to merge 9 commits intomasterfrom
feat/widgets

Conversation

@ecnivtwelve
Copy link
Copy Markdown

@ecnivtwelve ecnivtwelve commented Dec 8, 2025

Changes

  • Implements Home widgets
    • Makes Drive Widget ready to enable

Widget

Personalization

  • Added Widget personalization tab
  • Fixed header Flexbox issues with tab selector

Flags

Flag Required state Explaination
home.wallpaper-personalization.enabled true Enables personalization modal
home.widgets.enabled true Enables widget functionality

Screenshots

Home (Desktop) Modal (Mobile) Modal (Desktop)
CleanShot 2025-12-08 at 10 53 30@2x home twp localhost_8080_(iPhone SE) CleanShot 2025-12-08 at 10 55 55@2x

Summary by CodeRabbit

Release Notes

  • New Features

    • Widget system with install/uninstall functionality
    • Drive widget for browsing recent files, folders, and shared items
    • Papillon widget displaying courses, homeworks, and grades
    • Clock widget with live time and date display
    • Personalization modal now offers tabbed interface for wallpapers and widgets
  • Style

    • Enhanced widget UI with animations and improved layout styling
  • Chores

    • Updated cozy-ui-plus to version 4.0.0
    • Added internationalization support for improved language coverage

✏️ Tip: You can customize this high-level summary in your review settings.

@bundlemon
Copy link
Copy Markdown

bundlemon bot commented Dec 8, 2025

BundleMon

Files updated (5)
Status Path Size Limits
static/js/(chunkId).(hash).js
999.92KB (+125.69KB +14.38%) -
static/js/cozy.(hash).js
820.9KB (+9.79KB +1.21%) -
static/js/main.(hash).js
85.73KB (+3.79KB +4.62%) -
static/css/main.(hash).css
9.11KB (+509B +5.77%) -
static/css/cozy.(hash).css
30.53KB (+120B +0.39%) -
Unchanged files (11)
Status Path Size Limits
services/softDeleteOrRestoreAccounts.js
471KB -
services/updateAccounts.js
470.01KB -
services/deleteAccounts.js
321.54KB -
services/sourceAccountIdentifierNormalizer.js
259.66KB -
services/cliskTimeout.js
259.3KB -
services/sourceAccountIdentifierNormalizerHel
per.js
236.64KB -
services/myselfFromIdenties.js
236.47KB -
services/polyfillFetch.js
97.16KB -
static/js/lib-react.(hash).js
43.88KB -
static/js/lib-router.(hash).js
26.7KB -
services/attributesHelpers.js
15.7KB -

Total files change +139.88KB +3.3%

Groups updated (2)
Status Path Size Limits
**/*.js
6.99MB (+265.9KB +3.86%) -
**/*.css
86.5KB (+629B +0.72%) -
Unchanged groups (1)
Status Path Size Limits
**/*.{png,svg,ico}
272.78KB -

Final result: ✅

View report in BundleMon website ➡️


Current branch size history | Target branch size history

@ecnivtwelve ecnivtwelve marked this pull request as ready for review December 8, 2025 11:51
@Crash--
Copy link
Copy Markdown
Contributor

Crash-- commented Dec 19, 2025

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Dec 19, 2025

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Dec 19, 2025

Walkthrough

This pull request introduces a comprehensive widget system to the home personalization interface. Changes include adding a WidgetProvider context for managing installed widgets, implementing widget infrastructure components (Widget, WidgetTabs, WidgetListItem), creating three widget implementations (ClockWidget, DriveWidget, PapillonWidget), establishing query builders for Drive and Papillon data sources, and integrating the widget system into PersonalizationModal as a tabbed section alongside the Wallpaper tab. The Home component conditionally renders WidgetsWrapper when the feature flag is enabled. Supporting utilities, stylesheets, and localization strings (English and French) are added. Dependencies are updated to cozy-ui-plus v4.0.0 and twake-i18n v0.3.0 is added.

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ 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 'feat(widgets): Add Drive widget' accurately summarizes the main feature addition—a new Drive widget component. However, it understates the broader scope of this PR, which includes multiple widgets (Papillon, Clock), personalization UI enhancements, context providers, and localization updates. Despite this, the title is specific and relates directly to the primary deliverable.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/widgets

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

@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: 18

🧹 Nitpick comments (23)
src/components/Widgets/Atoms/widget.styl (1)

1-6: Consider documenting the fixed height constraint.

The widget has a fixed height of 162px !important. While this ensures visual consistency, it may limit flexibility for widgets with varying content sizes. Consider documenting why this fixed height is required or evaluating if a minimum height with flexible growth would be more appropriate.

src/components/Widgets/Apps/ClockWidget.jsx (1)

58-61: Consider localizing the widget title.

The title is hardcoded as "Horloge" (French). Consider using i18n for consistency with other localized strings in the application.

src/components/Widgets/widgets.styl (1)

3-3: Address the TODO comment.

The TODO comment indicates that the styling needs refinement. Consider documenting what "cleaner" means here or creating a tracking issue for the cleanup work.

Do you want me to open an issue to track this TODO, or would you like suggestions for what the "cleaner" implementation might look like?

src/components/Widgets/Apps/PapillonWidget.jsx (1)

7-7: Prefer declaring WidgetProps before usage.

While JavaScript hoisting allows this to work, declaring WidgetProps before the component improves readability and follows a more conventional pattern.

🔎 Suggested refactor
+const WidgetProps = {
+  title: 'Papillon',
+  app: 'papillon',
+  headerShown: true,
+  link: 'papillon'
+}
+
 export const PapillonWidget = ({ layoutControls, i }) => {
   return (
     <Widget {...WidgetProps}>
       <PapillonWidgetView app={WidgetProps.app} layoutControls={layoutControls} index={i} />
     </Widget>
   )
 }
 
-export const WidgetProps = {
-  title: 'Papillon',
-  app: 'papillon',
-  headerShown: true,
-  link: 'papillon'
-}
+export { WidgetProps }
src/components/Widgets/Apps/DriveWidget.jsx (1)

6-16: Redundant client prop passed to DriveWidgetView.

The component obtains a client via useClient() and passes it to DriveWidgetView. However, based on the relevant code snippets, DriveWidgetView and its child components already call useClient() internally (lines 88, 127, 164, 199 in DriveWidgetView.jsx). This makes the client prop unnecessary.

🔎 Proposed refactor
 export const DriveWidget = ({ layoutControls, i }) => {
-  const client = useClient()
-
   return (
     <Widget {...WidgetProps}>
       <DriveWidgetView
         app={WidgetProps.app}
         layoutControls={layoutControls}
         index={i}
-        client={client}
       />
     </Widget>
   )
 }

If the client prop is indeed used by DriveWidgetView, ensure it's part of the component's prop signature. Otherwise, remove the import on line 4 as well:

 import React from 'react'
 import Widget from '../Atoms/Widget'
 import { DriveWidgetView } from './Views/DriveWidgetView'
-import { useClient } from 'cozy-client'
src/components/Widgets/Atoms/Widget.jsx (1)

10-22: Consider removing unnecessary try/catch block.

The try/catch block around the JSX return provides minimal value:

  1. React's error boundaries already catch rendering errors
  2. The rethrown error doesn't add meaningful context
  3. It adds unnecessary nesting to the code

Unless there's specific error handling logic needed, this block can be removed for cleaner code.

🔎 Proposed refactor
 export const Widget = ({ children }) => {
   const { type } = useCozyTheme()

-  try {
-    return (
-      <div
-        className={`${styles[`app-widget`]} ${
-          styles[`app-widget-background--${type}`]
-        } u-flex u-flex-column u-mh-auto u-bdrs-8`}
-      >
-        <div className={`${styles[`app-widget-children`]}`}>{children}</div>
-      </div>
-    )
-  } catch (e) {
-    throw new Error('Error rendering Widget component', { cause: e })
-  }
+  return (
+    <div
+      className={`${styles[`app-widget`]} ${
+        styles[`app-widget-background--${type}`]
+      } u-flex u-flex-column u-mh-auto u-bdrs-8`}
+    >
+      <div className={`${styles[`app-widget-children`]}`}>{children}</div>
+    </div>
+  )
 }
src/components/Widgets/WidgetListItem.jsx (1)

9-15: Consider using useClient hook instead of prop drilling.

The client prop is only used to pass to AppIcon. Other widget components in this PR use useClient() directly (see relevant snippets from DriveWidget.jsx line 6, WidgetTabs.jsx line 29, etc.). For consistency, consider calling useClient() within this component instead of requiring it as a prop.

🔎 Proposed refactor
 import React from 'react'
+import { useClient } from 'cozy-client'
 import ListItem from 'cozy-ui/transpiled/react/ListItem'
 import ListItemText from 'cozy-ui/transpiled/react/ListItemText'
 import ListItemSecondaryAction from 'cozy-ui/transpiled/react/ListItemSecondaryAction'
 import Icon from 'cozy-ui/transpiled/react/Icon'
 import IconButton from 'cozy-ui/transpiled/react/IconButton'
 import AppIcon from 'cozy-ui-plus/src/AppIcon'

 const WidgetListItem = ({
   widget,
-  client,
   onAdd,
   onRemove,
   button = false
 }) => {
+  const client = useClient()
+
   return (
     <ListItem button={button} onClick={button && onAdd ? onAdd : undefined}>

This reduces coupling and makes the component more self-contained.

src/components/Widgets/Apps/Utils/subjectName.js (1)

1-265: Duplicate entries in format arrays.

Several format arrays contain duplicate values that serve no purpose:

  • Line 109: 'education civique' duplicated
  • Line 138: 'education musicale' duplicated
  • Lines 158-159: 'histoire-geographie' duplicated
  • Lines 193-194: 'mathematiques' duplicated
  • Lines 223-224: 'sciences economiques et sociales' duplicated
  • Lines 234-235: 'sciences de la vie et de la terre' duplicated

Consider removing these duplicates for cleaner data.

src/components/Personalization/PersonalizationModal.jsx (2)

34-57: Unused enabled property and inline component creation.

Two issues with the tabs configuration:

  1. The enabled: false property (lines 41, 52) is never used anywhere in the component.

  2. The component property creates JSX inline, meaning <Wallpaper client={client} /> and <Widget client={client} /> are recreated on every render. This can cause unnecessary re-renders and loss of internal component state.

🔎 Proposed fix
  const tabs = [
    {
      label: 'Wallpaper',
      icon: PaletteIcon,
      onClick: () => {
        setTabSelected(0)
      },
-     enabled: false,
-     component: <Wallpaper client={client} />
+     Component: Wallpaper
    },
    ...(flag('home.widgets.enabled')
      ? [
          {
            label: 'Widgets',
            icon: MosaicIcon,
            onClick: () => {
              setTabSelected(1)
            },
-           enabled: false,
-           component: <Widget client={client} />
+           Component: Widget
          }
        ]
      : [])
  ]

Then render as:

<tab.Component client={client} />

107-127: Consider adding unmountOnExit to Transition.

With only mountOnEnter, inactive tab components remain mounted after first view. If widgets perform expensive operations (queries, timers), they continue running in the background. Adding unmountOnExit will unmount inactive tabs.

🔎 Proposed fix
            <Transition
              in={tabSelected === index}
              timeout={0}
              key={tab.label}
              mountOnEnter
+             unmountOnExit
            >
src/components/Widgets/WidgetsWrapper.jsx (2)

49-51: Remove commented debug code.

The commented console.log on line 50 is a debug artifact that should be removed before merging.

🔎 Proposed fix
  const layoutControls = (index, command) => {
-   // console.log(index, command)
    switch (command) {

56-58: Clarify variable naming.

widgetIndex is actually the widget name/key (e.g., 'drive'), not a numeric index. This is confusing given the context. Consider renaming to widgetName or widgetKey.

src/components/Personalization/Widget.jsx (1)

17-28: Consider handling loading and error states for apps query.

The useQuery for apps doesn't destructure fetchStatus or isLoading. If apps data hasn't loaded yet, getAppBySlug returns undefined and falls back to widget.name, which is acceptable. However, explicitly handling loading state could improve UX.

src/components/Widgets/Atoms/WidgetTabs.jsx (2)

90-119: Remove duplicate key prop on nested element.

The key={tab.label} is already set on the Transition component (line 94), so the duplicate key on the inner div (line 99) is redundant.

🔎 Proposed fix
               {state => (
                 <div
-                  key={tab.label}
                   style={{
                     display: 'block',

202-212: Consider adding a null check for appLink.url.

If useAppLinkWithStoreFallback returns an undefined URL (e.g., if the app is not installed and store fallback fails), the navigation will fail silently or cause unexpected behavior.

🔎 Proposed fix
           <MenuItem
             onClick={() => {
               closeMenu()
-              window.location.href = appLink.url
+              if (appLink.url) {
+                window.location.href = appLink.url
+              }
             }}
+            disabled={!appLink.url}
           >
src/components/Widgets/Apps/Views/DriveWidgetView.jsx (3)

42-67: Hardcoded French strings should use i18n.

The tab labels ("Fichiers", "Partages", "Favoris") are hardcoded in French. For consistency with the rest of the localization system, these should use the t() function.

🔎 Proposed fix

Add the useI18n hook and use translation keys:

 export const DriveWidgetView = ({ app, layoutControls, index }) => {
+  const { t } = useI18n()
   const [openedFile, setOpenedFile] = useState({
     files: [],
     index: 0
   })

Then update the tab definitions:

             tabs={[
               {
-                label: 'Fichiers',
+                label: t('Widget.Drive.Files'),
                 icon: 'clock',
                 render: <DriveWidgetFileTab setOpenedFile={setOpenedFile} />
               },
               {
-                label: 'Partages',
+                label: t('Widget.Drive.Sharings'),
                 icon: 'share',
                 render: (
                   <DriveWidgetSharingsTab
                     setOpenedFile={setOpenedFile}
                     sharedDocumentIds={sharedDocuments}
                   />
                 )
               },
               {
-                label: 'Favoris',
+                label: t('Widget.Drive.Favorites'),
                 icon: 'star',

Note: You'll need to add the corresponding translation keys to the locale files.


271-276: Edge case: files without extensions may display incorrectly.

For files without a dot (e.g., README, Makefile), the filename will be empty and the extension will show .README. Consider handling this edge case.

🔎 Proposed fix
+      const lastDotIndex = file.name.lastIndexOf('.')
+      const hasExtension = lastDotIndex > 0
+      const filename = hasExtension ? file.name.substring(0, lastDotIndex) : file.name
+      const extension = hasExtension ? file.name.substring(lastDotIndex) : ''
       primary={
         <Filename
           variant={'subtitle1'}
-          filename={file.name.split('.').slice(0, -1).join('.')}
-          extension={'.' + file.name.split('.').pop()}
+          filename={filename}
+          extension={extension}
           midEllipsis
         />
       }

361-372: Add null safety for cozyMetadata.updatedAt access.

If folder.cozyMetadata or updatedAt is undefined, accessing it directly will throw a TypeError.

🔎 Proposed fix
       secondary={
         <Typography variant="caption" color="textSecondary">
-          {new Date(folder.cozyMetadata.updatedAt).toLocaleDateString(
+          {folder.cozyMetadata?.updatedAt ? new Date(folder.cozyMetadata.updatedAt).toLocaleDateString(
             undefined,
             {
               year: 'numeric',
               month: 'short',
               day: 'numeric'
             }
-          )}
+          ) : ''}
         </Typography>
       }
src/components/Widgets/Apps/Views/PapillonWidgetView.jsx (5)

35-51: Consider using i18n keys for tab labels.

The tab labels ('Cours', 'Devoirs', 'Notes') are hardcoded French strings. For consistency with the rest of the codebase that uses useI18n, consider using translation keys like t('Widget.Papillon.Tabs.Courses').


67-67: Missing date navigation functionality.

baseDate is initialized but never updated, so users cannot navigate to view courses on different days. Consider adding previous/next day navigation controls or a date picker.

Would you like me to suggest an implementation for date navigation controls?


102-103: Remove redundant null check.

The condition courses && courses.length > 0 is redundant since it's already verified at line 96.

🔎 Simplify the condition
   return (
     <List dense style={{ padding: 0 }}>
-      {courses &&
-        courses.length > 0 &&
-        courses.map(course => (
+      {courses.map(course => (
           <PapTimetableItem key={course.id} client={client} course={course} />
         ))}
     </List>

386-386: Consider extracting magic number to constant.

The limit of 10 grades could be extracted as a named constant (e.g., MAX_DISPLAYED_GRADES) for better maintainability and to make it configurable.


1-447: Consider architectural improvements.

While the component structure is clear, consider these optional improvements for maintainability:

  1. Extract inline styles: 50+ inline style objects could be moved to a stylesheet or styled components for better performance and maintainability.
  2. Add PropTypes or TypeScript: Public exported components lack type definitions, making the API unclear to consumers.
  3. Extract navigation utility: The repeated window.location.href pattern (lines 124, 306, 419) could be a custom hook like useNavigateToApp.
  4. Error boundaries: Consider wrapping query-dependent components in error boundaries to handle failed data fetches gracefully.
📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 99cb84a and be4d03b.

⛔ Files ignored due to path filters (1)
  • yarn.lock is excluded by !**/yarn.lock, !**/*.lock
📒 Files selected for processing (27)
  • package.json (2 hunks)
  • src/components/AppWrapper.jsx (2 hunks)
  • src/components/Home.jsx (2 hunks)
  • src/components/Personalization/Personalization.styl (1 hunks)
  • src/components/Personalization/PersonalizationModal.jsx (3 hunks)
  • src/components/Personalization/ThemeSwitcher.jsx (1 hunks)
  • src/components/Personalization/Wallpaper.styl (2 hunks)
  • src/components/Personalization/Widget.jsx (1 hunks)
  • src/components/Widgets/Apps/ClockWidget.jsx (1 hunks)
  • src/components/Widgets/Apps/DriveWidget.jsx (1 hunks)
  • src/components/Widgets/Apps/PapillonWidget.jsx (1 hunks)
  • src/components/Widgets/Apps/Queries/DriveQueries.js (1 hunks)
  • src/components/Widgets/Apps/Queries/PapillonQueries.js (1 hunks)
  • src/components/Widgets/Apps/Utils/subjectColor.js (1 hunks)
  • src/components/Widgets/Apps/Utils/subjectName.js (1 hunks)
  • src/components/Widgets/Apps/Views/DriveWidgetView.jsx (1 hunks)
  • src/components/Widgets/Apps/Views/DriveWidgetView.styl (1 hunks)
  • src/components/Widgets/Apps/Views/PapillonWidgetView.jsx (1 hunks)
  • src/components/Widgets/Atoms/Widget.jsx (1 hunks)
  • src/components/Widgets/Atoms/WidgetTabs.jsx (1 hunks)
  • src/components/Widgets/Atoms/widget.styl (1 hunks)
  • src/components/Widgets/WidgetListItem.jsx (1 hunks)
  • src/components/Widgets/WidgetsWrapper.jsx (1 hunks)
  • src/components/Widgets/widgets.styl (1 hunks)
  • src/hooks/useWidgetContext.tsx (1 hunks)
  • src/locales/en.json (1 hunks)
  • src/locales/fr.json (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (11)
src/components/Home.jsx (1)
src/components/Widgets/WidgetsWrapper.jsx (2)
  • WidgetsWrapper (32-91)
  • WidgetsWrapper (32-91)
src/components/Widgets/Apps/DriveWidget.jsx (2)
src/components/Widgets/Apps/Views/DriveWidgetView.jsx (2)
  • DriveWidgetView (28-86)
  • DriveWidgetView (28-86)
src/components/Widgets/Atoms/Widget.jsx (2)
  • Widget (7-23)
  • Widget (7-23)
src/components/Widgets/WidgetListItem.jsx (4)
src/components/Widgets/Apps/Views/DriveWidgetView.jsx (4)
  • client (89-89)
  • client (128-128)
  • client (165-165)
  • client (200-200)
src/components/Widgets/Apps/DriveWidget.jsx (1)
  • client (7-7)
src/components/Widgets/Apps/Views/PapillonWidgetView.jsx (3)
  • client (64-64)
  • client (199-199)
  • client (366-366)
src/components/Widgets/Atoms/WidgetTabs.jsx (1)
  • client (30-30)
src/components/Widgets/Apps/PapillonWidget.jsx (4)
src/components/Widgets/Atoms/Widget.jsx (2)
  • Widget (7-23)
  • Widget (7-23)
src/components/Widgets/Apps/ClockWidget.jsx (2)
  • WidgetProps (58-61)
  • WidgetProps (58-61)
src/components/Widgets/Apps/DriveWidget.jsx (2)
  • WidgetProps (21-26)
  • WidgetProps (21-26)
src/components/Widgets/Apps/Views/PapillonWidgetView.jsx (2)
  • PapillonWidgetView (24-61)
  • PapillonWidgetView (24-61)
src/components/Widgets/Apps/ClockWidget.jsx (3)
src/components/Widgets/Atoms/Widget.jsx (2)
  • Widget (7-23)
  • Widget (7-23)
src/components/Widgets/Apps/DriveWidget.jsx (2)
  • WidgetProps (21-26)
  • WidgetProps (21-26)
src/components/Widgets/Apps/PapillonWidget.jsx (2)
  • WidgetProps (13-18)
  • WidgetProps (13-18)
src/components/Personalization/Widget.jsx (2)
src/components/Widgets/WidgetsWrapper.jsx (3)
  • contextUninstallWidget (35-39)
  • AvailableWidgets (14-30)
  • AvailableWidgets (14-30)
src/hooks/useWidgetContext.tsx (1)
  • useWidgetContext (80-86)
src/components/Widgets/Apps/Queries/DriveQueries.js (1)
src/components/Widgets/Apps/Queries/PapillonQueries.js (2)
  • DEFAULT_CACHE_TIMEOUT_QUERIES (3-3)
  • defaultFetchPolicy (4-6)
src/components/Widgets/WidgetsWrapper.jsx (4)
src/components/Widgets/Apps/DriveWidget.jsx (2)
  • DriveWidget (6-19)
  • DriveWidget (6-19)
src/components/Home.jsx (1)
  • useBreakpoints (24-24)
src/hooks/useWidgetContext.tsx (1)
  • useWidgetContext (80-86)
src/components/Widgets/Atoms/WidgetTabs.jsx (1)
  • i (33-33)
src/components/Widgets/Atoms/WidgetTabs.jsx (1)
src/components/Widgets/WidgetsWrapper.jsx (1)
  • layoutControls (49-63)
src/hooks/useWidgetContext.tsx (3)
src/components/KonnectorTile.jsx (1)
  • error (145-145)
src/components/Widgets/WidgetsWrapper.jsx (1)
  • uninstallWidget (41-43)
src/components/AppWrapper.jsx (1)
  • context (42-42)
src/components/AppWrapper.jsx (3)
src/hooks/useWidgetContext.tsx (1)
  • WidgetProvider (13-78)
src/components/ConditionalWrapper.tsx (1)
  • ConditionalWrapper (7-12)
src/store/configureStore.js (1)
  • persistor (46-46)
🔇 Additional comments (30)
src/components/Personalization/ThemeSwitcher.jsx (1)

49-49: LGTM! Width constraint removal improves layout flexibility.

Removing the u-w-4 width utility allows the ThemeSwitcher to adapt to its parent container, which aligns with the broader layout reorganization in this PR.

package.json (2)

80-80: New dependency addition looks appropriate.

The addition of twake-i18n ^0.3.0 aligns with the Papillon widget functionality introduced in this PR.


56-56: Verify cozy-ui-plus v4.0.0 breaking changes before upgrading.

Unable to locate public documentation or package information for cozy-ui-plus v4.0.0. Ensure the package exists as a stable release and that all component usages are compatible before merging this upgrade. Refer to cozy-ui-plus release notes or migration guides if available.

src/components/Widgets/Atoms/widget.styl (1)

39-65: Animation transitions are well-implemented.

The tab transition states (entering, entered, exiting, exited) with smooth opacity and transform animations provide a polished user experience.

src/components/Widgets/Apps/Utils/subjectColor.js (1)

39-59: LGTM! Well-implemented deterministic color selection.

The seeded pseudo-random generator using bitwise operations provides consistent color selection for subjects. The null check for empty lists is appropriate defensive coding.

src/components/Widgets/Apps/ClockWidget.jsx (1)

10-16: Timer cleanup is correctly implemented.

The useEffect properly clears the interval on component unmount, preventing memory leaks.

src/components/Personalization/Wallpaper.styl (1)

1-53: LGTM! Styling updates maintain visual consistency.

The syntax normalization to Stylus shorthand and the addition of !important declarations ensure consistent styling overrides across the personalization UI. These changes align with the broader widget system integration.

src/components/Widgets/Apps/Queries/DriveQueries.js (1)

10-71: Query builders are well-structured and consistent.

The Drive query builders follow consistent patterns with proper caching, naming conventions, and fetch policies. The use of CozyClient's query builder API is appropriate.

src/components/Widgets/Apps/Views/DriveWidgetView.styl (1)

1-4: LGTM! Clean hover utility class.

The .underline-on-hover class provides a simple, reusable pattern for interactive text elements in the Drive widget UI.

src/components/Home.jsx (1)

20-20: LGTM!

The WidgetsWrapper integration is clean and properly gated behind the feature flag home.widgets.enabled, consistent with the PR's documented feature flag requirements.

Also applies to: 34-34

src/locales/en.json (1)

611-629: LGTM!

The translation keys are well-structured and provide clear, user-friendly messages for widget actions and empty states.

src/components/Widgets/Apps/PapillonWidget.jsx (1)

5-11: Missing app prop in component signature.

The component uses app prop on line 8 but doesn't declare it in the function parameters. This will cause app to be undefined when passed to PapillonWidgetView.

🔎 Proposed fix
-export const PapillonWidget = ({ app, layoutControls, i }) => {
+export const PapillonWidget = ({ layoutControls, i }) => {
   return (
     <Widget {...WidgetProps}>
-      <PapillonWidgetView app={app} layoutControls={layoutControls} index={i} />
+      <PapillonWidgetView app={WidgetProps.app} layoutControls={layoutControls} index={i} />
     </Widget>
   )
 }

Likely an incorrect or invalid review comment.

src/components/Widgets/widgets.styl (1)

4-4: No z-index conflicts exist. The codebase contains only one z-index declaration (z-index: 9 in this file), so there are no other UI elements to conflict with. Additionally, z-index: 9 is a moderate value, not high—modal and overlay patterns typically use values like 1000 or 9999. z-index values are only compared against other elements in the same stacking context, so the actual layering behavior depends on the stacking context hierarchy rather than the numeric value alone.

Likely an incorrect or invalid review comment.

src/components/AppWrapper.jsx (1)

130-151: Verify the provider hierarchy and PersistGate scope.

The provider nesting restructure is correct. WidgetProvider wraps CozyTheme, ThemeProvider, AlertProvider, and ReduxProvider. Inner (with I18n and realtime queries) is a child of ConditionalWrapper with PersistGate. Widget state using localForage is independent of Redux persistence. All components using useWidgetContext (Widget.jsx, WidgetsWrapper.jsx) are proper descendants of WidgetProvider with correct context access.

src/components/Personalization/Personalization.styl (1)

49-73: LGTM!

The tab transition classes are well-structured with appropriate enter/exit states and pointer-events handling to prevent interaction during transitions.

src/components/Personalization/PersonalizationModal.jsx (1)

63-80: LGTM!

The tab button rendering with conditional selected styling and icon handling is clean and functional.

src/components/Personalization/Widget.jsx (2)

58-67: LGTM!

The Switch toggle logic correctly handles install/uninstall based on current state.


9-9: Inconsistent import source; prefer cozy-ui wrapper.

Cozy UI relies on Material UI v4, and components like ListItemIcon are wrapped and themed within cozy-ui. As of version 88.0.0, MuiCozyTheme/ListItemIcon was moved to /react folder, indicating a migration away from direct Material UI imports.

The import path suggested (cozy-ui/transpiled/react/ListItemIcon) is correct for cozy-ui v88+, but verify your project's cozy-ui version. If using v87 or earlier, use cozy-ui/transpiled/react/MuiCozyTheme/ListItemIcon instead.

Proposed fix
-import { ListItemIcon } from '@material-ui/core'
+import ListItemIcon from 'cozy-ui/transpiled/react/ListItemIcon'

(Or cozy-ui/transpiled/react/MuiCozyTheme/ListItemIcon for cozy-ui v87 and earlier)

src/locales/fr.json (1)

611-629: LGTM!

The French translations are well-structured and appropriately localized. The Widget object structure mirrors the English locale with proper French translations for all action labels and empty-state messages.

src/hooks/useWidgetContext.tsx (3)

38-47: LGTM!

The installWidget and uninstallWidget callbacks correctly use functional updates to avoid stale closure issues, and the duplicate check in installWidget prevents redundant entries.


49-66: LGTM!

The moveWidget implementation correctly handles reordering with proper boundary checks using Math.max and Math.min to ensure the target position is always valid.


80-86: LGTM!

The useWidgetContext hook follows the standard pattern for context consumers, with a clear error message when used outside the provider.

src/components/Widgets/Atoms/WidgetTabs.jsx (2)

250-286: LGTM!

The UnimplementedWidgetView and LoadingWidgetView helper components are clean and reusable, providing consistent empty and loading states across widget tabs.


176-198: No changes needed. The code correctly passes the index parameter through a callback closure pattern. When layoutControls('up') is called in WidgetTabs, it invokes the callback created in WidgetsWrapper that captures the current widget index i and properly forwards it to the original layoutControls(index, command) function. This is valid JavaScript and requires no modification.

Likely an incorrect or invalid review comment.

src/components/Widgets/Apps/Views/DriveWidgetView.jsx (1)

127-159: DriveWidgetFoldersTab is exported but not used.

This component is defined and exported but not included in the DriveWidgetView tabs array. If this is intentional for future use, consider adding a brief comment. Otherwise, it may be dead code.

src/components/Widgets/Apps/Queries/PapillonQueries.js (2)

40-43: Verify sort order for homework query.

The query sorts by sourceAccountIdentifier descending first, then by dueDate descending. For a homework list, sorting primarily by dueDate would typically be more user-friendly. Is this order intentional for index optimization?


50-83: LGTM!

The buildGradesQuery and buildAccountsQuery follow consistent patterns with proper index fields and fetch policies. The flexible period filtering using period || { $gt: null } is a clean approach.

src/components/Widgets/Apps/Views/PapillonWidgetView.jsx (3)

439-439: Verify grade precision requirements.

toFixed(2) always displays grades with 2 decimal places (e.g., "15.00"). Verify this matches the expected display format—some grading systems may prefer whole numbers or 1 decimal place.


123-125: Review navigation pattern.

Using window.location.href triggers a full page reload rather than client-side navigation. This pattern is repeated throughout the file (lines 124, 306, 419). Verify this is the intended behavior for Cozy app navigation, or consider using the AppLink component directly for smoother transitions.

What is the recommended navigation pattern for Cozy apps when using cozy-client and AppLink components?

220-225: Add validation after date format transformation to catch parsing failures.

The regex transformation converts the compact date format to ISO 8601, but provides no validation that the resulting string successfully parses. JavaScript's Date object is notoriously lenient, often silently accepting invalid values and returning "valid" Date instances that behave unexpectedly. If the transformed string is malformed, new Date() will silently create an Invalid Date object. For robustness, using getTime() + isNaN() is recommended to handle both invalid objects and invalid values.

Comment on lines 1 to +2
.personalizationTabButton
background-color: var(--actionColorDisabled) !important
background-color var(--actionColorDisabled) !important
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Duplicate selector with conflicting styles.

.personalizationTabButton is defined here (line 1-2) with background-color var(--actionColorDisabled), but redefined at lines 34-44 with background-color var(--actionColorGhost). The second definition will override this one. Consider consolidating into a single selector or removing this duplicate.

🤖 Prompt for AI Agents
In src/components/Personalization/Personalization.styl (lines 1-2 and 34-44)
there is a duplicate .personalizationTabButton selector with conflicting
background-color values (var(--actionColorDisabled) at 1-2 and
var(--actionColorGhost) at 34-44) causing the latter to override the former;
consolidate into a single selector by choosing the correct background-color
value (or merge shared properties if both are needed), remove the duplicate
block, and eliminate unnecessary !important so there is only one authoritative
.personalizationTabButton definition that reflects the intended design.

height 40px !important
padding 0 !important
background-color var(--actionColorGhost) !important
border-radius 50
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Missing unit on border-radius.

border-radius 50 is missing a unit. For a circular button, this should be border-radius 50% or a specific pixel value like 50px.

🔎 Proposed fix
-  border-radius 50
+  border-radius 50%
📝 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
border-radius 50
border-radius 50%
🤖 Prompt for AI Agents
In src/components/Personalization/Personalization.styl around line 41, the
declaration "border-radius 50" is missing a unit; change it to an appropriate
unit such as "border-radius 50%" for a circular shape or "border-radius 50px"
(or another px/rem value) depending on the desired visual, and ensure the rest
of the stylesheet follows the same unit convention.

<div>
<List style={{ padding: 0 }}>
{AllWidgets.map((widget, index) => (
<ListItem dense button key={index} onClick={() => {}}>
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Empty onClick handler.

The ListItem has onClick={() => {}} which does nothing. Either remove it or implement the intended behavior (perhaps toggling the switch).

🔎 Proposed fix - remove empty handler
-         <ListItem dense button key={index} onClick={() => {}}>
+         <ListItem dense key={index}>
📝 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
<ListItem dense button key={index} onClick={() => {}}>
<ListItem dense key={index}>
🤖 Prompt for AI Agents
In src/components/Personalization/Widget.jsx around line 36, the ListItem uses
an empty onClick handler (onClick={() => {}}) which should be removed or
replaced with a real handler; if the intent is to toggle the associated Switch,
wire this to the component's toggle function (e.g., onClick={() =>
handleToggle(index)} or calling the same prop handler used by the Switch) and
ensure handleToggle updates the item state, or simply omit the onClick prop if
no click-on-row behavior is desired; also keep the ListItem as button if you
implement toggling so keyboard activation still works and add/update any
necessary aria attributes.

hour: '2-digit',
minute: '2-digit'
})
const seconds = currentTime.toLocaleTimeString([], { second: '2-digit' })
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Type mismatch: lz() expects a number but receives a string.

Line 22 obtains seconds as a string from toLocaleTimeString(), but Line 47 passes it to lz() which expects a number (checking num < 10). This comparison will not work as intended since string comparison differs from numeric comparison.

🔎 Proposed fix
-  const seconds = currentTime.toLocaleTimeString([], { second: '2-digit' })
+  const seconds = currentTime.getSeconds()

This ensures lz() receives a number for proper zero-padding logic.

Also applies to: 47-47

🤖 Prompt for AI Agents
In src/components/Widgets/Apps/ClockWidget.jsx around lines 22 and 47, seconds
is derived as a string via toLocaleTimeString() but later passed to lz() which
expects a number; change the value passed to lz() to a numeric type (e.g., use
Number(...) or parseInt(...) on the seconds string at line 22 or right before
calling lz at line 47) so lz() receives a number and numeric
comparison/zero-padding works correctly.

@@ -0,0 +1,71 @@
import CozyClient, { Q } from 'cozy-client'

const DEFAULT_CACHE_TIMEOUT_QUERIES = 9 * 60 * 1000 // 10 minutes
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Correct the comment to match the actual timeout value.

The comment states "10 minutes" but the calculation is 9 * 60 * 1000, which equals 9 minutes. This discrepancy could confuse future maintainers.

🔎 Proposed fix
-const DEFAULT_CACHE_TIMEOUT_QUERIES = 9 * 60 * 1000 // 10 minutes
+const DEFAULT_CACHE_TIMEOUT_QUERIES = 9 * 60 * 1000 // 9 minutes

Note: The same issue appears in src/components/Widgets/Apps/Queries/PapillonQueries.js and should be corrected there as well.

📝 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
const DEFAULT_CACHE_TIMEOUT_QUERIES = 9 * 60 * 1000 // 10 minutes
const DEFAULT_CACHE_TIMEOUT_QUERIES = 9 * 60 * 1000 // 9 minutes
🤖 Prompt for AI Agents
In src/components/Widgets/Apps/Queries/DriveQueries.js around line 3, the inline
comment incorrectly says "10 minutes" while the value is 9 * 60 * 1000 (9
minutes); update the comment to "9 minutes" to match the calculation (or change
the calculation to 10 * 60 * 1000 if the intended timeout is 10 minutes). Apply
the same correction to src/components/Widgets/Apps/Queries/PapillonQueries.js
where the identical mismatch exists.

Comment on lines +20 to +21
} catch (e) {
throw new Error('Error rendering Widget component', e)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Incorrect Error constructor usage.

The Error constructor only accepts a single message argument. The second parameter e will be ignored, losing the original error context.

🔎 Proposed fix

Use the cause option (available in modern JavaScript) or manually chain the error:

   } catch (e) {
-    throw new Error('Error rendering Widget component', e)
+    throw new Error('Error rendering Widget component', { cause: e })
   }

Alternatively, if cause option isn't supported in your target environment:

   } catch (e) {
-    throw new Error('Error rendering Widget component', e)
+    const error = new Error('Error rendering Widget component')
+    error.originalError = e
+    throw error
   }
📝 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
} catch (e) {
throw new Error('Error rendering Widget component', e)
} catch (e) {
throw new Error('Error rendering Widget component', { cause: e })
}
🤖 Prompt for AI Agents
In src/components/Widgets/Atoms/Widget.jsx around lines 20-21, the catch block
calls throw new Error('Error rendering Widget component', e) which misuses the
Error constructor and drops the original error; replace it so the original error
is preserved — either throw a new Error('Error rendering Widget component', {
cause: e }) if your runtime supports the cause option, or create a new Error,
assign newError.cause = e (and/or newError.stack += '\nCaused by: ' + e.stack)
and then throw that newError so the original error context is retained.

Comment on lines +214 to +218
<MenuItem
onClick={() => {
layoutControls('uninstall')
closeMenu()
}}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Missing index argument for uninstall action.

Same issue as above - the uninstall action also needs the index parameter.

🔎 Proposed fix
           <MenuItem
             onClick={() => {
-              layoutControls('uninstall')
+              layoutControls(i, 'uninstall')
               closeMenu()
             }}
           >
📝 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
<MenuItem
onClick={() => {
layoutControls('uninstall')
closeMenu()
}}
<MenuItem
onClick={() => {
layoutControls(i, 'uninstall')
closeMenu()
}}
🤖 Prompt for AI Agents
In src/components/Widgets/Atoms/WidgetTabs.jsx around lines 214 to 218, the
MenuItem onClick calls layoutControls('uninstall') without the required index
argument; update the handler to pass the correct index (e.g.,
layoutControls('uninstall', index)) and ensure the index variable is in scope
(use the index from the surrounding map/loop or capture it in the closure) then
call closeMenu() as before so the uninstall action receives the proper item
index.

Comment on lines +45 to +47
const reorderWidget = (fromIndex, displaceBy) => {
moveWidget(installedWidgets[fromIndex], fromIndex + displaceBy)
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Missing bounds check in reorderWidget.

reorderWidget doesn't validate the target index. Moving the first widget "up" results in index -1, and moving the last widget "down" exceeds the array length. This could cause undefined behavior in moveWidget.

🔎 Proposed fix
  const reorderWidget = (fromIndex, displaceBy) => {
+   const newIndex = fromIndex + displaceBy
+   if (newIndex < 0 || newIndex >= installedWidgets.length) {
+     return // Already at boundary
+   }
-   moveWidget(installedWidgets[fromIndex], fromIndex + displaceBy)
+   moveWidget(installedWidgets[fromIndex], newIndex)
  }
📝 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
const reorderWidget = (fromIndex, displaceBy) => {
moveWidget(installedWidgets[fromIndex], fromIndex + displaceBy)
}
const reorderWidget = (fromIndex, displaceBy) => {
const newIndex = fromIndex + displaceBy
if (newIndex < 0 || newIndex >= installedWidgets.length) {
return // Already at boundary
}
moveWidget(installedWidgets[fromIndex], newIndex)
}
🤖 Prompt for AI Agents
In src/components/Widgets/WidgetsWrapper.jsx around lines 45 to 47,
reorderWidget calls moveWidget with a computed target index but does not
validate bounds; calling it with fromIndex + displaceBy can produce -1 or an
index >= installedWidgets.length. Add a bounds check: compute targetIndex =
fromIndex + displaceBy, return early if targetIndex < 0 or targetIndex >=
installedWidgets.length, and only call moveWidget when targetIndex is valid
(optionally clamp or log a warning if desired).

Comment on lines +76 to +86
{installedWidgets.map((widgetIndex, i) => {
const WidgetComponent = AvailableWidgets[widgetIndex].component
return (
<WidgetComponent
key={widgetIndex}
i={[i, installedWidgets.length]}
layoutControls={command => {
layoutControls(i, command)
}}
/>
)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Missing null check for widget lookup.

If widgetIndex (the widget name) doesn't exist in AvailableWidgets, WidgetComponent will be undefined, causing a runtime crash when React tries to render it. This could happen if a user has a widget installed that was later removed from the registry.

🔎 Proposed fix
        {installedWidgets.map((widgetIndex, i) => {
-         const WidgetComponent = AvailableWidgets[widgetIndex].component
+         const widgetConfig = AvailableWidgets[widgetIndex]
+         if (!widgetConfig) {
+           return null // Widget no longer available
+         }
+         const WidgetComponent = widgetConfig.component
          return (
📝 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
{installedWidgets.map((widgetIndex, i) => {
const WidgetComponent = AvailableWidgets[widgetIndex].component
return (
<WidgetComponent
key={widgetIndex}
i={[i, installedWidgets.length]}
layoutControls={command => {
layoutControls(i, command)
}}
/>
)
{installedWidgets.map((widgetIndex, i) => {
const widgetConfig = AvailableWidgets[widgetIndex]
if (!widgetConfig) {
return null // Widget no longer available
}
const WidgetComponent = widgetConfig.component
return (
<WidgetComponent
key={widgetIndex}
i={[i, installedWidgets.length]}
layoutControls={command => {
layoutControls(i, command)
}}
/>
)
🤖 Prompt for AI Agents
In src/components/Widgets/WidgetsWrapper.jsx around lines 76 to 86, the code
looks up AvailableWidgets[widgetIndex] without validating it and then attempts
to render its component, which will throw if the widget key is missing; update
the mapping to check that AvailableWidgets[widgetIndex] and its .component exist
before rendering — if missing, either skip rendering that entry (return null) or
render a safe fallback placeholder and optionally log a warning or telemetry
message so the UI doesn't crash when a previously-registered widget is absent.

Comment on lines +20 to +36
React.useEffect(() => {
void localForage
.getItem<string[]>('installedWidgets')
.then(widgets => {
if (widgets && Array.isArray(widgets)) {
setInstalledWidgets(widgets)
}
return undefined
})
.catch(error => {
throw new Error(`Failed to load installed widgets: ${String(error)}`)
})
}, [])

React.useEffect(() => {
void localForage.setItem('installedWidgets', installedWidgets)
}, [installedWidgets])
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Race condition: persistence effect may overwrite stored data before load completes.

The second useEffect (line 34-36) runs on every installedWidgets change, including the initial empty array. This will persist an empty array to localForage before the first useEffect finishes loading the actual stored widgets, causing data loss on page refresh.

Additionally, throwing inside an async .catch() will result in an unhandled promise rejection rather than being caught by React error boundaries.

🔎 Proposed fix
 export const WidgetProvider = ({
   children
 }: {
   children: React.ReactNode
 }): JSX.Element => {
   const [installedWidgets, setInstalledWidgets] = useState<string[]>([])
+  const [isLoaded, setIsLoaded] = useState(false)

   React.useEffect(() => {
     void localForage
       .getItem<string[]>('installedWidgets')
       .then(widgets => {
         if (widgets && Array.isArray(widgets)) {
           setInstalledWidgets(widgets)
         }
+        setIsLoaded(true)
         return undefined
       })
       .catch(error => {
-        throw new Error(`Failed to load installed widgets: ${String(error)}`)
+        console.error(`Failed to load installed widgets: ${String(error)}`)
+        setIsLoaded(true)
       })
   }, [])

   React.useEffect(() => {
+    if (!isLoaded) return
     void localForage.setItem('installedWidgets', installedWidgets)
-  }, [installedWidgets])
+  }, [installedWidgets, isLoaded])

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In src/hooks/useWidgetContext.tsx around lines 20 to 36, the persistence effect
can overwrite stored widgets with the initial empty array before the load
completes and the catch handler throws (causing unhandled promise rejections);
change the state initialization and effects so the loader sets installedWidgets
from localForage and you track load completion (e.g., initialize
installedWidgets to null or add an isLoaded flag), only call localForage.setItem
when the load is finished and installedWidgets is non-null, and replace the
throw in the .catch with a safe error handling path (log the error or set an
error state) so rejected promises are not thrown unhandled.

Copy link
Copy Markdown
Contributor

@Crash-- Crash-- left a comment

Choose a reason for hiding this comment

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

Difficult to review, I didn't even try to understand the code:

  • Too much inline styling
  • Too much component within the same file

But I do appreciate the queries / views / widget stuff 👍

width 40px !important
min-height 40px !important
height 40px !important
padding 0 !important
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.

generally not a good sign to have to use !important

import IconButton from 'cozy-ui/transpiled/react/IconButton'
import cx from 'classnames'
import Widget from './Widget'
import { Transition } from 'react-transition-group'
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.

Do we really need this new lib? It seems to add a lot of kb to the bundle at the end.

style={{
width: '100%',
height: '100%',
position: 'relative'
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.

no inline style please. At least not this way because if the PersonalizationModal re-render, it'll create a new style object, and then it'll re-render the children here

{tabs
.filter(tab => tab.component)
.map((tab, index) => (
<Transition
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.

Don't we already have something for tabs in cozy-ui?

<div
key={tab.label}
className={`${styles[`personalization-tab`]} ${
styles[`personalization-tab--${state}`]
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.

Try to respect the grep test please https://jamie-wong.com/2013/07/12/grep-test/

primary={
<Filename
variant={'subtitle1'}
filename={file.name.split('.').slice(0, -1).join('.')}
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.

we have tools in cozy-client to do that.

WebkitMaskImage:
'linear-gradient(to right, rgba(0,0,0,1) 90%, rgba(0,0,0,0) 100%)',
color: 'inherit'
}}
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.

why not put this into the className?

textOverflow: 'ellipsis',
overflow: 'hidden',
whiteSpace: 'nowrap'
}}
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.

no inline please

}
/>
)
}
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.

split this file please

.app-widget-container
gap: 1rem
/* TODO : make this cleaner */
z-index: 9
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.

we have css variable for zIndex, please use them :)

@Crash--
Copy link
Copy Markdown
Contributor

Crash-- commented Dec 19, 2025

@shepilov This is why we need to have the shared drive from the io.cozy.files itself. Because if we don't, then all the widgets we'll want to add in several application will need to be aware of our implementations details. :p

@Crash-- Crash-- closed this Dec 19, 2025
@Crash-- Crash-- reopened this Dec 19, 2025
Copy link
Copy Markdown

@codescene-delta-analysis codescene-delta-analysis bot left a comment

Choose a reason for hiding this comment

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

Gates Failed
New code is healthy (3 new files with code health below 10.00)
Enforce advisory code health rules (3 files with Code Duplication, Overall Code Complexity, Complex Method)

Gates Passed
4 Quality Gates Passed

See analysis details in CodeScene

Reason for failure
New code is healthy Violations Code Health Impact
DriveWidgetView.jsx 2 rules 8.82 Suppress
PapillonWidgetView.jsx 1 rule 9.39 Suppress
WidgetTabs.jsx 1 rule 9.69 Suppress
Enforce advisory code health rules Violations Code Health Impact
DriveWidgetView.jsx 2 advisory rules 8.82 Suppress
PapillonWidgetView.jsx 1 advisory rule 9.39 Suppress
WidgetTabs.jsx 1 advisory rule 9.69 Suppress

Quality Gate Profile: Pay Down Tech Debt
Want more control? Customize Code Health rules or catch issues early with our IDE extension and CLI tool.

Comment on lines +88 to +125
export const DriveWidgetFileTab = ({ setOpenedFile }) => {
const client = useClient()
const { t } = useI18n()

const driveQuery = buildDriveRecentsQuery()

const { data: files, filesFetchStatus } = useQuery(
driveQuery.definition,
driveQuery.options
)

const filteredFiles = files?.filter(
file => !file.path || !file.path.startsWith(SETTINGS_DIR_PATH)
)

if (filesFetchStatus === 'loading') {
return <LoadingWidgetView />
}

if (!files || files.length === 0) {
return <UnimplementedWidgetView label={t('Widget.Drive.NoRecentFiles')} />
}

return (
<List dense style={{ padding: 0 }}>
{filteredFiles &&
filteredFiles.length > 0 &&
filteredFiles.map((file, i) => (
<WidgetDriveFileItem
key={file._id}
file={file}
client={client}
open={() => setOpenedFile({ files: filteredFiles, index: i })}
/>
))}
</List>
)
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

❌ New issue: Code Duplication
The module contains 4 functions with similar structure: DriveWidgetFavoritesTab,DriveWidgetFileTab,DriveWidgetFoldersTab,DriveWidgetSharingsTab

Suppress

@@ -0,0 +1,375 @@
import React, { useState } from 'react'
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

❌ New issue: Overall Code Complexity
This module has a mean cyclomatic complexity of 4.86 across 7 functions. The mean complexity threshold is 4

Suppress

@@ -0,0 +1,447 @@
import { useAppLinkWithStoreFallback, useClient, useQuery } from 'cozy-client'
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

❌ New issue: Overall Code Complexity
This module has a mean cyclomatic complexity of 5.29 across 7 functions. The mean complexity threshold is 4

Suppress

Comment on lines +21 to +120
export const WidgetTabs = ({
tabs,
app,
layoutControls,
defaultTab = 0,
onTabChange,
index
}) => {
const { t } = useI18n()
const client = useClient()
const [selectedTab, setSelectedTab] = useState(defaultTab)

const i = index && index[0] ? index[0] : 0
const j = index && index[1] ? index[1] - 1 : 0

const appLink = useAppLinkWithStoreFallback(app, client, '/')

const [menuOpened, setMenuOpened] = React.useState(false)
const ref = React.useRef(null)

const toggleMenu = () => {
setMenuOpened(!menuOpened)
}

const closeMenu = () => {
setMenuOpened(false)
}

const changeTab = index => {
setSelectedTab(index)
if (onTabChange) {
onTabChange(index)
}
}

if (!tabs || tabs.length === 0) {
return <UnimplementedWidgetView label="No tabs available" />
}

const tabButtonStyle = {
width: '32px',
maxWidth: '32px',
minWidth: '32px',
height: '32px',
borderRadius: 50,
padding: 0
}

return (
<div
style={{
display: 'flex',
flexDirection: 'row',
height: '100%'
}}
>
<div
style={{
flex: 1,
overflowY: 'hidden',
position: 'relative'
}}
>
<div style={{ position: 'relative', minHeight: '100%' }}>
{tabs.map((tab, index) => {
const isActive = index === selectedTab
const content =
typeof tab.render === 'function' ? tab.render() : tab.render

return (
<Transition
in={isActive}
timeout={0}
key={tab.label}
mountOnEnter
>
{state => (
<div
key={tab.label}
style={{
display: 'block',
position: 'absolute',
top: 0,
left: 0,
width: '100%',
height: '100%',
inset: 0,
zIndex: isActive ? 3 : 2,
pointerEvents: isActive ? 'all' : 'none'
}}
className={`${styles[`app-tab`]} ${
styles[`app-tab--${state}`]
}`}
>
{content}
</div>
)}
</Transition>
)
})}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

❌ New issue: Complex Method
WidgetTabs has a cyclomatic complexity of 11, threshold = 10

Suppress

@ecnivtwelve ecnivtwelve marked this pull request as draft December 22, 2025 08:35
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants