diff --git a/packages/twenty-front/src/modules/settings/components/SettingsDiscoveryHeroCard.tsx b/packages/twenty-front/src/modules/settings/components/SettingsDiscoveryHeroCard.tsx index 7b1eba49dd..4a6dc9511b 100644 --- a/packages/twenty-front/src/modules/settings/components/SettingsDiscoveryHeroCard.tsx +++ b/packages/twenty-front/src/modules/settings/components/SettingsDiscoveryHeroCard.tsx @@ -12,12 +12,12 @@ import { Card } from 'twenty-ui/surfaces'; import { ThemeContext, themeCssVariables } from 'twenty-ui/theme-constants'; import { FeatureFlagKey } from '~/generated-metadata/graphql'; -const COVER_HEIGHT = 150; +const DEFAULT_COVER_HEIGHT = 150; -const StyledCoverContainer = styled.div` +const StyledCoverContainer = styled.div<{ coverHeight: number }>` background: ${themeCssVariables.background.secondary}; box-sizing: border-box; - height: ${COVER_HEIGHT}px; + height: ${({ coverHeight }) => coverHeight}px; overflow: hidden; position: relative; `; @@ -50,6 +50,7 @@ type SettingsDiscoveryHeroCardProps = { darkSrc: string; instanceIdPrefix: string; tabs: SettingsCustomizeVideoModalTab[]; + coverHeight?: number; footer?: ReactNode; playButtonAriaLabel?: string; }; @@ -59,6 +60,7 @@ export const SettingsDiscoveryHeroCard = ({ darkSrc, instanceIdPrefix, tabs, + coverHeight = DEFAULT_COVER_HEIGHT, footer, playButtonAriaLabel, }: SettingsDiscoveryHeroCardProps) => { @@ -78,7 +80,7 @@ export const SettingsDiscoveryHeroCard = ({ return ( <> - + {shouldDisplayVideo && ( diff --git a/packages/twenty-front/src/modules/settings/components/SettingsDiscoveryHeroCardFooter.tsx b/packages/twenty-front/src/modules/settings/components/SettingsDiscoveryHeroCardFooter.tsx new file mode 100644 index 0000000000..6c648b9eae --- /dev/null +++ b/packages/twenty-front/src/modules/settings/components/SettingsDiscoveryHeroCardFooter.tsx @@ -0,0 +1,107 @@ +import { styled } from '@linaria/react'; +import { type ReactNode, useContext } from 'react'; +import { isDefined } from 'twenty-shared/utils'; +import { type IconComponent } from 'twenty-ui/icon'; +import { OverflowingTextWithTooltip } from 'twenty-ui/surfaces'; +import { ThemeContext, themeCssVariables } from 'twenty-ui/theme-constants'; + +const StyledContent = styled.div` + align-items: center; + background-color: ${themeCssVariables.background.secondary}; + display: flex; + gap: ${themeCssVariables.spacing[3]}; + padding: ${themeCssVariables.spacing[4]}; +`; + +const StyledIconContainer = styled.div` + align-items: center; + background-color: ${themeCssVariables.background.primary}; + border: 2px solid ${themeCssVariables.border.color.light}; + border-radius: ${themeCssVariables.border.radius.sm}; + display: flex; + height: ${themeCssVariables.spacing[8]}; + justify-content: center; + min-width: ${themeCssVariables.spacing[8]}; + width: ${themeCssVariables.spacing[8]}; +`; + +const StyledIcon = styled.div` + align-items: center; + display: inline-flex; + justify-content: center; + transform: rotate(-4deg); +`; + +const StyledTextContainer = styled.div` + align-items: flex-start; + display: flex; + flex: 1; + flex-direction: column; + gap: ${themeCssVariables.spacing[2]}; + justify-content: center; + min-width: 0; +`; + +const StyledTitle = styled.div` + align-items: center; + color: ${themeCssVariables.font.color.primary}; + display: flex; + font-weight: ${themeCssVariables.font.weight.medium}; + min-height: ${themeCssVariables.spacing[4]}; + width: 100%; +`; + +const StyledDescription = styled.div` + color: ${themeCssVariables.font.color.secondary}; + font-size: ${themeCssVariables.font.size.sm}; + line-height: ${themeCssVariables.text.lineHeight.lg}; + overflow: hidden; + text-box-edge: cap alphabetic; + text-box-trim: trim-both; +`; + +const StyledActionContainer = styled.div` + flex-shrink: 0; + margin-left: auto; +`; + +type SettingsDiscoveryHeroCardFooterProps = { + Icon: IconComponent; + title: ReactNode; + description?: string; + action?: ReactNode; +}; + +export const SettingsDiscoveryHeroCardFooter = ({ + Icon, + title, + description, + action, +}: SettingsDiscoveryHeroCardFooterProps) => { + const { theme } = useContext(ThemeContext); + + return ( + + + + + + + + {title} + {description && ( + + + + )} + + {isDefined(action) && ( + {action} + )} + + ); +}; diff --git a/packages/twenty-front/src/modules/settings/data-model/object-details/assets/record-page-layout-cover-dark.png b/packages/twenty-front/src/modules/settings/data-model/object-details/assets/record-page-layout-cover-dark.png new file mode 100644 index 0000000000..0f5b3316ba Binary files /dev/null and b/packages/twenty-front/src/modules/settings/data-model/object-details/assets/record-page-layout-cover-dark.png differ diff --git a/packages/twenty-front/src/modules/settings/data-model/object-details/assets/record-page-layout-cover-light.png b/packages/twenty-front/src/modules/settings/data-model/object-details/assets/record-page-layout-cover-light.png new file mode 100644 index 0000000000..d927bfa540 Binary files /dev/null and b/packages/twenty-front/src/modules/settings/data-model/object-details/assets/record-page-layout-cover-light.png differ diff --git a/packages/twenty-front/src/modules/settings/data-model/object-details/components/tabs/ObjectLayout.tsx b/packages/twenty-front/src/modules/settings/data-model/object-details/components/tabs/ObjectLayout.tsx index 73800ff5b9..880b980682 100644 --- a/packages/twenty-front/src/modules/settings/data-model/object-details/components/tabs/ObjectLayout.tsx +++ b/packages/twenty-front/src/modules/settings/data-model/object-details/components/tabs/ObjectLayout.tsx @@ -1,13 +1,11 @@ -import { useContext } from 'react'; - import { styled } from '@linaria/react'; import { useLingui } from '@lingui/react/macro'; import { AppPath } from 'twenty-shared/types'; import { isDefined } from 'twenty-shared/utils'; -import { IconAddressBook, IconReload } from 'twenty-ui/icon'; +import { IconAddressBook, IconPencil, IconReload } from 'twenty-ui/icon'; import { Button } from 'twenty-ui/input'; import { Section } from 'twenty-ui/layout'; -import { ThemeContext, themeCssVariables } from 'twenty-ui/theme-constants'; +import { themeCssVariables } from 'twenty-ui/theme-constants'; import { H2Title } from 'twenty-ui/typography'; import { useEnterLayoutCustomizationMode } from '@/layout-customization/hooks/useEnterLayoutCustomizationMode'; @@ -15,7 +13,10 @@ import { type EnrichedObjectMetadataItem } from '@/object-metadata/types/Enriche import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords'; import { useResetPageLayoutToDefault } from '@/page-layout/hooks/useResetPageLayoutToDefault'; import { recordPageLayoutByObjectMetadataIdFamilySelector } from '@/page-layout/states/selectors/recordPageLayoutByObjectMetadataIdFamilySelector'; -import { SettingsCard } from '@/settings/components/SettingsCard'; +import { SettingsDiscoveryHeroCard } from '@/settings/components/SettingsDiscoveryHeroCard'; +import { SettingsDiscoveryHeroCardFooter } from '@/settings/components/SettingsDiscoveryHeroCardFooter'; +import recordPageLayoutCoverDark from '@/settings/data-model/object-details/assets/record-page-layout-cover-dark.png'; +import recordPageLayoutCoverLight from '@/settings/data-model/object-details/assets/record-page-layout-cover-light.png'; import { ObjectOpenRecordInPicker } from '@/settings/data-model/object-details/components/tabs/ObjectOpenRecordInPicker'; import { useHasPermissionFlag } from '@/settings/roles/hooks/useHasPermissionFlag'; import { ConfirmationModal } from '@/ui/layout/modal/components/ConfirmationModal'; @@ -32,13 +33,13 @@ const StyledContentContainer = styled.div` `; const RESET_PAGE_LAYOUT_MODAL_ID = 'reset-page-layout-to-default-modal'; +const RECORD_PAGE_LAYOUT_HERO_INSTANCE_ID_PREFIX = 'record-page-layout-hero'; type ObjectLayoutProps = { objectMetadataItem: EnrichedObjectMetadataItem; }; export const ObjectLayout = ({ objectMetadataItem }: ObjectLayoutProps) => { - const { theme } = useContext(ThemeContext); const { t } = useLingui(); const navigateApp = useNavigateApp(); const { enterLayoutCustomizationMode } = useEnterLayoutCustomizationMode(); @@ -95,12 +96,30 @@ export const ObjectLayout = ({ objectMetadataItem }: ObjectLayoutProps) => { title={t`Record page`} description={t`Customize the workspace record page`} /> - } - onClick={handleCustomizeRecordPage} - disabled={!hasLayoutsPermission || !isDefined(firstRecord)} + + } + /> + } />
diff --git a/packages/twenty-front/src/modules/settings/data-model/object-details/components/tabs/__stories__/ObjectLayoutHeroCard.stories.tsx b/packages/twenty-front/src/modules/settings/data-model/object-details/components/tabs/__stories__/ObjectLayoutHeroCard.stories.tsx new file mode 100644 index 0000000000..c0825b0d9a --- /dev/null +++ b/packages/twenty-front/src/modules/settings/data-model/object-details/components/tabs/__stories__/ObjectLayoutHeroCard.stories.tsx @@ -0,0 +1,81 @@ +import { SettingsDiscoveryHeroCard } from '@/settings/components/SettingsDiscoveryHeroCard'; +import { SettingsDiscoveryHeroCardFooter } from '@/settings/components/SettingsDiscoveryHeroCardFooter'; +import recordPageLayoutCoverDark from '@/settings/data-model/object-details/assets/record-page-layout-cover-dark.png'; +import recordPageLayoutCoverLight from '@/settings/data-model/object-details/assets/record-page-layout-cover-light.png'; +import { styled } from '@linaria/react'; +import { type Meta, type StoryObj } from '@storybook/react-vite'; +import { expect, within } from 'storybook/test'; +import { IconAddressBook, IconPencil } from 'twenty-ui/icon'; +import { Button } from 'twenty-ui/input'; +import { ThemeProvider, themeCssVariables } from 'twenty-ui/theme-constants'; + +const StyledCanvas = styled.div` + background: ${themeCssVariables.background.primary}; + box-sizing: border-box; + padding: 32px; + width: 758px; +`; + +const meta: Meta = { + title: 'Modules/Settings/Data Model/Object Layout Hero Card', +}; + +export default meta; + +type Story = StoryObj; + +const renderCard = (colorScheme: 'light' | 'dark') => ( + + + + } + /> + } + /> + + +); + +export const Light: Story = { + render: () => renderCard('light'), + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + + expect(canvas.getByText('Customize record page')).toBeVisible(); + expect( + canvas.getByText('Customize how your record page looks.'), + ).toBeVisible(); + expect(canvas.getByRole('button', { name: 'Customize ...' })).toBeEnabled(); + }, +}; + +export const Dark: Story = { + render: () => renderCard('dark'), + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + + expect(canvas.getByText('Customize record page')).toBeVisible(); + expect( + canvas.getByText('Customize how your record page looks.'), + ).toBeVisible(); + expect(canvas.getByRole('button', { name: 'Customize ...' })).toBeEnabled(); + }, +}; diff --git a/packages/twenty-front/src/pages/settings/layout/SettingsLayout.tsx b/packages/twenty-front/src/pages/settings/layout/SettingsLayout.tsx index d0b917d0dd..2344999bdf 100644 --- a/packages/twenty-front/src/pages/settings/layout/SettingsLayout.tsx +++ b/packages/twenty-front/src/pages/settings/layout/SettingsLayout.tsx @@ -1,6 +1,6 @@ import { useEnterLayoutCustomizationMode } from '@/layout-customization/hooks/useEnterLayoutCustomizationMode'; import { SettingsDiscoveryHeroCard } from '@/settings/components/SettingsDiscoveryHeroCard'; -import { SettingsOptionCardContentButton } from '@/settings/components/SettingsOptions/SettingsOptionCardContentButton'; +import { SettingsDiscoveryHeroCardFooter } from '@/settings/components/SettingsDiscoveryHeroCardFooter'; import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer'; import { SettingsLayoutItemsStats } from '@/settings/layout/components/SettingsLayoutItemsStats'; import { SettingsPageLayout } from '@/settings/components/layout/SettingsPageLayout'; @@ -88,11 +88,11 @@ export const SettingsLayout = () => { tabs={heroTabs} playButtonAriaLabel={t`Watch customization demo`} footer={ -