Conversation
3fd537a to
cd97491
Compare
BundleMonFiles updated (5)
Unchanged files (11)
Total files change +139.88KB +3.3% Groups updated (2)
Unchanged groups (1)
Final result: ✅ View report in BundleMon website ➡️ |
|
@coderabbitai review |
✅ Actions performedReview triggered.
|
WalkthroughThis 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)
✅ Passed checks (2 passed)
✨ Finishing touches
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
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
WidgetPropsbefore 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
clientviauseClient()and passes it toDriveWidgetView. However, based on the relevant code snippets,DriveWidgetViewand its child components already calluseClient()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:
- React's error boundaries already catch rendering errors
- The rethrown error doesn't add meaningful context
- 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
clientprop is only used to pass toAppIcon. Other widget components in this PR useuseClient()directly (see relevant snippets from DriveWidget.jsx line 6, WidgetTabs.jsx line 29, etc.). For consistency, consider callinguseClient()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'duplicatedConsider removing these duplicates for cleaner data.
src/components/Personalization/PersonalizationModal.jsx (2)
34-57: Unusedenabledproperty and inline component creation.Two issues with the tabs configuration:
The
enabled: falseproperty (lines 41, 52) is never used anywhere in the component.The
componentproperty 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 addingunmountOnExitto 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. AddingunmountOnExitwill 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.logon 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.
widgetIndexis actually the widget name/key (e.g.,'drive'), not a numeric index. This is confusing given the context. Consider renaming towidgetNameorwidgetKey.src/components/Personalization/Widget.jsx (1)
17-28: Consider handling loading and error states for apps query.The
useQueryfor apps doesn't destructurefetchStatusorisLoading. If apps data hasn't loaded yet,getAppBySlugreturnsundefinedand falls back towidget.name, which is acceptable. However, explicitly handling loading state could improve UX.src/components/Widgets/Atoms/WidgetTabs.jsx (2)
90-119: Remove duplicatekeyprop on nested element.The
key={tab.label}is already set on theTransitioncomponent (line 94), so the duplicatekeyon the innerdiv(line 99) is redundant.🔎 Proposed fix
{state => ( <div - key={tab.label} style={{ display: 'block',
202-212: Consider adding a null check forappLink.url.If
useAppLinkWithStoreFallbackreturns 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
useI18nhook 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 forcozyMetadata.updatedAtaccess.If
folder.cozyMetadataorupdatedAtis 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 liket('Widget.Papillon.Tabs.Courses').
67-67: Missing date navigation functionality.
baseDateis 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 > 0is 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
10grades 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:
- Extract inline styles: 50+ inline style objects could be moved to a stylesheet or styled components for better performance and maintainability.
- Add PropTypes or TypeScript: Public exported components lack type definitions, making the API unclear to consumers.
- Extract navigation utility: The repeated
window.location.hrefpattern (lines 124, 306, 419) could be a custom hook likeuseNavigateToApp.- 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
⛔ Files ignored due to path filters (1)
yarn.lockis 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-4width 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
!importantdeclarations 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-hoverclass 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: Missingappprop in component signature.The component uses
appprop on line 8 but doesn't declare it in the function parameters. This will causeappto beundefinedwhen passed toPapillonWidgetView.🔎 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-eventshandling 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
ListItemIconare wrapped and themed within cozy-ui. As of version 88.0.0,MuiCozyTheme/ListItemIconwas moved to/reactfolder, 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, usecozy-ui/transpiled/react/MuiCozyTheme/ListItemIconinstead.Proposed fix
-import { ListItemIcon } from '@material-ui/core' +import ListItemIcon from 'cozy-ui/transpiled/react/ListItemIcon'(Or
cozy-ui/transpiled/react/MuiCozyTheme/ListItemIconfor 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
installWidgetanduninstallWidgetcallbacks correctly use functional updates to avoid stale closure issues, and the duplicate check ininstallWidgetprevents redundant entries.
49-66: LGTM!The
moveWidgetimplementation correctly handles reordering with proper boundary checks usingMath.maxandMath.minto ensure the target position is always valid.
80-86: LGTM!The
useWidgetContexthook 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
UnimplementedWidgetViewandLoadingWidgetViewhelper components are clean and reusable, providing consistent empty and loading states across widget tabs.
176-198: No changes needed. The code correctly passes theindexparameter through a callback closure pattern. WhenlayoutControls('up')is called in WidgetTabs, it invokes the callback created in WidgetsWrapper that captures the current widget indexiand properly forwards it to the originallayoutControls(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:DriveWidgetFoldersTabis exported but not used.This component is defined and exported but not included in the
DriveWidgetViewtabs 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
sourceAccountIdentifierdescending first, then bydueDatedescending. For a homework list, sorting primarily bydueDatewould typically be more user-friendly. Is this order intentional for index optimization?
50-83: LGTM!The
buildGradesQueryandbuildAccountsQueryfollow consistent patterns with proper index fields and fetch policies. The flexible period filtering usingperiod || { $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.hreftriggers 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.
| .personalizationTabButton | ||
| background-color: var(--actionColorDisabled) !important | ||
| background-color var(--actionColorDisabled) !important |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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.
| 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={() => {}}> |
There was a problem hiding this comment.
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.
| <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' }) |
There was a problem hiding this comment.
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 | |||
There was a problem hiding this comment.
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 minutesNote: 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.
| 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.
| } catch (e) { | ||
| throw new Error('Error rendering Widget component', e) |
There was a problem hiding this comment.
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.
| } 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.
| <MenuItem | ||
| onClick={() => { | ||
| layoutControls('uninstall') | ||
| closeMenu() | ||
| }} |
There was a problem hiding this comment.
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.
| <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.
| const reorderWidget = (fromIndex, displaceBy) => { | ||
| moveWidget(installedWidgets[fromIndex], fromIndex + displaceBy) | ||
| } |
There was a problem hiding this comment.
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.
| 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).
| {installedWidgets.map((widgetIndex, i) => { | ||
| const WidgetComponent = AvailableWidgets[widgetIndex].component | ||
| return ( | ||
| <WidgetComponent | ||
| key={widgetIndex} | ||
| i={[i, installedWidgets.length]} | ||
| layoutControls={command => { | ||
| layoutControls(i, command) | ||
| }} | ||
| /> | ||
| ) |
There was a problem hiding this comment.
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.
| {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.
| 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]) |
There was a problem hiding this comment.
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.
Crash--
left a comment
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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' |
There was a problem hiding this comment.
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' |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
Don't we already have something for tabs in cozy-ui?
| <div | ||
| key={tab.label} | ||
| className={`${styles[`personalization-tab`]} ${ | ||
| styles[`personalization-tab--${state}`] |
There was a problem hiding this comment.
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('.')} |
There was a problem hiding this comment.
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' | ||
| }} |
There was a problem hiding this comment.
why not put this into the className?
| textOverflow: 'ellipsis', | ||
| overflow: 'hidden', | ||
| whiteSpace: 'nowrap' | ||
| }} |
| } | ||
| /> | ||
| ) | ||
| } |
| .app-widget-container | ||
| gap: 1rem | ||
| /* TODO : make this cleaner */ | ||
| z-index: 9 |
There was a problem hiding this comment.
we have css variable for zIndex, please use them :)
|
@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 |
There was a problem hiding this comment.
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.
| 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> | ||
| ) | ||
| } |
There was a problem hiding this comment.
❌ New issue: Code Duplication
The module contains 4 functions with similar structure: DriveWidgetFavoritesTab,DriveWidgetFileTab,DriveWidgetFoldersTab,DriveWidgetSharingsTab
| @@ -0,0 +1,375 @@ | |||
| import React, { useState } from 'react' | |||
There was a problem hiding this comment.
❌ New issue: Overall Code Complexity
This module has a mean cyclomatic complexity of 4.86 across 7 functions. The mean complexity threshold is 4
| @@ -0,0 +1,447 @@ | |||
| import { useAppLinkWithStoreFallback, useClient, useQuery } from 'cozy-client' | |||
There was a problem hiding this comment.
❌ New issue: Overall Code Complexity
This module has a mean cyclomatic complexity of 5.29 across 7 functions. The mean complexity threshold is 4
| 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> | ||
| ) | ||
| })} |
There was a problem hiding this comment.
❌ New issue: Complex Method
WidgetTabs has a cyclomatic complexity of 11, threshold = 10
Changes
Widget
Personalization
Flags
home.wallpaper-personalization.enabledtruehome.widgets.enabledtrueScreenshots
Summary by CodeRabbit
Release Notes
New Features
Style
Chores
✏️ Tip: You can customize this high-level summary in your review settings.