From a3a44c83158926f764f8dfbadde77723dc51e8ea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A9lix=20Malfait?= Date: Thu, 4 Jun 2026 09:32:36 +0200 Subject: [PATCH] fix(front): settings skeleton, app-detail header & empty favorites (#21209) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three small post-redesign UI fixes. Each is an independent commit, so they can be split into separate PRs if preferred. ## 1. Settings loading skeleton — match the rounded-card layout The redesign (#21131) moved settings chrome into a rounded card (`SettingsPageLayout`: bordered header with breadcrumb + centered title, optional secondary bar, 760px body), but `SettingsSkeletonLoader` still rendered the old flat `PageHeader` + `PageBody` — so pages painted as a full-width flat bar then snapped into the card. - `SettingsSkeletonLoader` now reproduces the card and **reuses the real `SettingsPageHeader` + `SettingsPageContainer`**, so the frame aligns by construction; the card CSS is replicated (not `SettingsPageLayout`) to avoid the layout's side effects (hotkeys, side panel, info banner). - It's **composed with `SettingsSectionSkeletonLoader`** so the loading body is identical whether or not chrome is present. Rule: no chrome on screen yet → full-page skeleton; chrome already on screen → body-only `SettingsSectionSkeletonLoader` (the admin Enterprise tab now uses it, matching its sibling tabs). A short comment on each component documents this. ## 2. Application detail header — pass a plain title `SettingsApplicationDetails` / `SettingsAvailableApplicationDetails` passed a custom `SettingsApplicationDetailTitle` (avatar + name + multi-line description, fixed width) into `SettingsPageLayout`'s **centered single-line title slot**, which broke the header. They now pass the app's display name like every other page. The available-app "unlisted" notice moves into the body as a reusable `InlineBanner`; the now-unused `SettingsApplicationDetailTitle` is removed. ## 3. Navigation — hide Favorites when empty Always rendering the Favorites section (#21087) left a stray "Favorites" title above Workspace for users with no favorites. It now renders only when at least one favorite exists (redundant per-child guards dropped). Note: the "+ add favorite" entry point therefore appears once you have ≥1 favorite; the first favorite is created from a record/view as before. ## Verification - `nx typecheck twenty-front` ✅ · `oxlint` + `oxfmt --check` on changed files ✅ - i18n catalogs intentionally untouched — handled by the repo's separate i18n pipeline. --- .../favorites/components/FavoritesSection.tsx | 112 ++++++++--------- .../components/SettingsAdminTabContent.tsx | 4 +- .../components/SettingsSkeletonLoader.tsx | 92 ++++++++------ .../SettingsApplicationDetails.tsx | 9 +- .../SettingsAvailableApplicationDetails.tsx | 18 +-- .../SettingsApplicationDetailTitle.tsx | 114 ------------------ 6 files changed, 122 insertions(+), 227 deletions(-) delete mode 100644 packages/twenty-front/src/pages/settings/applications/components/SettingsApplicationDetailTitle.tsx diff --git a/packages/twenty-front/src/modules/navigation-menu-item/display/sections/favorites/components/FavoritesSection.tsx b/packages/twenty-front/src/modules/navigation-menu-item/display/sections/favorites/components/FavoritesSection.tsx index 268a102bb0..fbc0b3a4cc 100644 --- a/packages/twenty-front/src/modules/navigation-menu-item/display/sections/favorites/components/FavoritesSection.tsx +++ b/packages/twenty-front/src/modules/navigation-menu-item/display/sections/favorites/components/FavoritesSection.tsx @@ -91,8 +91,6 @@ export const FavoritesSection = () => { const handleAddFavorite = (event?: React.MouseEvent) => { event?.stopPropagation(); - // Expansion is gated on items existing, so this stays collapsed if the user - // cancels and reveals the favorite as soon as it is added. openNavigationSection(); setNavigationMenuItemEditSection('favorite'); setPendingInsertionNavigationMenuItem(null); @@ -119,10 +117,14 @@ export const FavoritesSection = () => { [deleteManyNavigationMenuItems], ); + if (topLevelItems.length === 0) { + return null; + } + return ( 0 && isNavigationSectionOpen} + isOpen={isNavigationSectionOpen} onToggle={toggleNavigationSection} rightIcon={ { /> } > - {topLevelItems.length > 0 && ( - - {topLevelItems.map((item, index) => ( - - {index === 0 ? ( - - - - ) : ( + + {topLevelItems.map((item, index) => ( + + {index === 0 ? ( + - )} - + ) : ( + - - - - ))} - + )} + + + + + ))} + + - - - - )} + compact + sectionId={NavigationSections.FAVORITES} + droppableId={ORPHAN_DROPPABLE_ID} + /> + + ); }; diff --git a/packages/twenty-front/src/modules/settings/admin-panel/components/SettingsAdminTabContent.tsx b/packages/twenty-front/src/modules/settings/admin-panel/components/SettingsAdminTabContent.tsx index 884d479c3f..70853518fd 100644 --- a/packages/twenty-front/src/modules/settings/admin-panel/components/SettingsAdminTabContent.tsx +++ b/packages/twenty-front/src/modules/settings/admin-panel/components/SettingsAdminTabContent.tsx @@ -5,7 +5,7 @@ import { SettingsAdminConfigVariables } from '@/settings/admin-panel/config-vari import { SETTINGS_ADMIN_TABS } from '@/settings/admin-panel/constants/SettingsAdminTabs'; import { SETTINGS_ADMIN_TABS_ID } from '@/settings/admin-panel/constants/SettingsAdminTabsId'; import { SettingsAdminHealthStatus } from '@/settings/admin-panel/health-status/components/SettingsAdminHealthStatus'; -import { SettingsSkeletonLoader } from '@/settings/components/SettingsSkeletonLoader'; +import { SettingsSectionSkeletonLoader } from '@/settings/components/SettingsSectionSkeletonLoader'; import { activeTabIdComponentState } from '@/ui/layout/tab-list/states/activeTabIdComponentState'; import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue'; import { lazy, Suspense } from 'react'; @@ -34,7 +34,7 @@ export const SettingsAdminTabContent = () => { return ; case SETTINGS_ADMIN_TABS.ENTERPRISE: return ( - }> + }> ); diff --git a/packages/twenty-front/src/modules/settings/components/SettingsSkeletonLoader.tsx b/packages/twenty-front/src/modules/settings/components/SettingsSkeletonLoader.tsx index b98a6c935b..1e082df590 100644 --- a/packages/twenty-front/src/modules/settings/components/SettingsSkeletonLoader.tsx +++ b/packages/twenty-front/src/modules/settings/components/SettingsSkeletonLoader.tsx @@ -1,54 +1,70 @@ import { SKELETON_LOADER_HEIGHT_SIZES } from '@/activities/components/SkeletonLoader'; -import { PageBody } from '@/ui/layout/page/components/PageBody'; -import { PageHeader } from '@/ui/layout/page/components/PageHeader'; +import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer'; +import { SettingsSectionSkeletonLoader } from '@/settings/components/SettingsSectionSkeletonLoader'; +import { SettingsPageHeader } from '@/settings/components/layout/SettingsPageHeader'; +import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile'; import { styled } from '@linaria/react'; import { useContext } from 'react'; import Skeleton, { SkeletonTheme } from 'react-loading-skeleton'; import { ThemeContext, themeCssVariables } from 'twenty-ui/theme-constants'; -const StyledContainer = styled.div` +const StyledRoot = styled.div<{ isMobile: boolean }>` display: flex; + flex: 1; + min-height: 0; + min-width: 0; + padding: ${({ isMobile }) => + isMobile ? themeCssVariables.spacing[1] : themeCssVariables.spacing[2]}; +`; + +const StyledCard = styled.div` + background: ${themeCssVariables.background.primary}; + border: 1px solid ${themeCssVariables.border.color.medium}; + border-radius: ${themeCssVariables.border.radius.md}; + box-sizing: border-box; + display: flex; + flex: 1; flex-direction: column; + min-height: 0; + overflow: hidden; width: 100%; `; -const StyledTitleLoaderContainer = styled.div` - margin: ${themeCssVariables.spacing[8]} ${themeCssVariables.spacing[8]} - ${themeCssVariables.spacing[2]}; -`; - export const SettingsSkeletonLoader = () => { + const isMobile = useIsMobile(); const { theme } = useContext(ThemeContext); + return ( - - - {' '} - - } - /> - - - - - - - - + + + + + ), + }, + ]} + title={ + + } + /> + + + + + + ); }; diff --git a/packages/twenty-front/src/pages/settings/applications/SettingsApplicationDetails.tsx b/packages/twenty-front/src/pages/settings/applications/SettingsApplicationDetails.tsx index 770df562cb..8e78e2353c 100644 --- a/packages/twenty-front/src/pages/settings/applications/SettingsApplicationDetails.tsx +++ b/packages/twenty-front/src/pages/settings/applications/SettingsApplicationDetails.tsx @@ -41,7 +41,6 @@ import { } from '~/generated-metadata/graphql'; import { useNavigateSettings } from '~/hooks/useNavigateSettings'; import { SettingsSectionSkeletonLoader } from '@/settings/components/SettingsSectionSkeletonLoader'; -import { SettingsApplicationDetailTitle } from '~/pages/settings/applications/components/SettingsApplicationDetailTitle'; import { CUSTOM_APPLICATION_ILLUSTRATIONS } from '~/pages/settings/applications/constants/CustomApplicationIllustrations'; import { STANDARD_APPLICATION_ILLUSTRATIONS } from '~/pages/settings/applications/constants/StandardApplicationIllustrations'; import { useFindApplicationConnectionProviders } from '~/pages/settings/applications/hooks/useFindApplicationConnectionProviders'; @@ -326,13 +325,7 @@ export const SettingsApplicationDetails = () => { return ( - } + title={displayName} links={[ { children: t`Workspace`, diff --git a/packages/twenty-front/src/pages/settings/applications/SettingsAvailableApplicationDetails.tsx b/packages/twenty-front/src/pages/settings/applications/SettingsAvailableApplicationDetails.tsx index 43c284d1f5..9915ba27ea 100644 --- a/packages/twenty-front/src/pages/settings/applications/SettingsAvailableApplicationDetails.tsx +++ b/packages/twenty-front/src/pages/settings/applications/SettingsAvailableApplicationDetails.tsx @@ -19,12 +19,14 @@ import { IconBook, IconBox, IconCommand, + IconEyeOff, IconGraph, IconInfoCircle, IconLego, IconListDetails, IconLock, IconShield, + InlineBanner, } from 'twenty-ui/display'; import { ApplicationRegistrationSourceType, @@ -32,7 +34,6 @@ import { FindOneApplicationByUniversalIdentifierDocument, PermissionFlagType, } from '~/generated-metadata/graphql'; -import { SettingsApplicationDetailTitle } from '~/pages/settings/applications/components/SettingsApplicationDetailTitle'; import { SettingsApplicationDetailAboutTab } from '~/pages/settings/applications/tabs/SettingsApplicationDetailAboutTab'; import { SettingsApplicationDetailContentTab } from '~/pages/settings/applications/tabs/SettingsApplicationDetailContentTab'; import { SettingsApplicationPermissionsTab } from '~/pages/settings/applications/tabs/SettingsApplicationPermissionsTab'; @@ -283,16 +284,15 @@ export const SettingsAvailableApplicationDetails = () => { }, { children: displayName }, ]} - title={ - - } + title={displayName} > + {isUnlisted && ( + + )} { - return OBJECT_SETTINGS_WIDTH + 'px'; - }}; -`; - -const StyledHeader = styled.div` - align-items: center; - display: flex; - gap: ${themeCssVariables.spacing[4]}; - justify-content: space-between; -`; - -const StyledHeaderLeft = styled.div` - display: flex; - flex-direction: column; - gap: ${themeCssVariables.spacing[2]}; -`; - -const StyledHeaderTop = styled.div` - align-items: center; - display: flex; - gap: ${themeCssVariables.spacing[1]}; -`; - -const StyledAppName = styled.div` - color: ${themeCssVariables.font.color.primary}; - font-size: ${themeCssVariables.font.size.lg}; - font-weight: ${themeCssVariables.font.weight.semiBold}; -`; - -const StyledAppDescription = styled.div` - color: ${themeCssVariables.font.color.tertiary}; - font-size: ${themeCssVariables.font.size.md}; - font-weight: ${themeCssVariables.font.weight.regular}; - line-height: ${themeCssVariables.text.lineHeight.lg}; -`; - -const StyledUnlistedBanner = styled.div` - align-items: center; - background-color: ${themeCssVariables.background.transparent.lighter}; - border: 1px solid ${themeCssVariables.border.color.medium}; - border-radius: ${themeCssVariables.border.radius.sm}; - color: ${themeCssVariables.font.color.secondary}; - display: flex; - font-size: ${themeCssVariables.font.size.md}; - font-weight: ${themeCssVariables.font.weight.regular}; - gap: ${themeCssVariables.spacing[2]}; - justify-content: center; - margin-bottom: ${themeCssVariables.spacing[8]}; - padding: ${themeCssVariables.spacing[3]}; -`; - -export const SettingsApplicationDetailTitle = ({ - displayName, - description, - applicationId, - isUnlisted = false, -}: SettingsApplicationDetailTitleProps) => { - const descriptionSummary = getApplicationDescriptionSummary(description); - - const { applicationChipData } = useApplicationChipData({ - applicationId, - }); - - return ( - - {isUnlisted && ( - - - {t`Application not listed on the marketplace. It was shared via a direct link`} - - )} - - - - - {displayName} - - {descriptionSummary && ( - {descriptionSummary} - )} - - - - ); -};