feat: add SettingsObjectFieldPreview and SettingsObjectFieldPreviewCard (#2376)

* feat: add SettingsObjectFieldPreview

Closes #2343

* feat: add SettingsObjectFieldPreviewCard

Closes #2349

* Fix ci

* Fix tests

* Fix tests

---------

Co-authored-by: Charles Bochet <charles@twenty.com>
This commit is contained in:
Thaïs
2023-11-06 23:14:47 +01:00
committed by GitHub
parent b3d460eb75
commit 377f95c9db
5 changed files with 225 additions and 7 deletions

View File

@@ -0,0 +1,105 @@
import { ReactNode } from 'react';
import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import { Tag } from '@/ui/display/tag/components/Tag';
import { useLazyLoadIcon } from '@/ui/input/hooks/useLazyLoadIcon';
type SettingsObjectFieldPreviewProps = {
objectIconKey: string;
objectLabelPlural: string;
isObjectCustom: boolean;
fieldIconKey: string;
fieldLabel: string;
fieldValue: ReactNode;
};
const StyledContainer = styled.div`
background-color: ${({ theme }) => theme.background.secondary};
border: 1px solid ${({ theme }) => theme.border.color.medium};
border-radius: ${({ theme }) => theme.border.radius.md};
box-sizing: border-box;
color: ${({ theme }) => theme.font.color.primary};
max-width: 480px;
padding: ${({ theme }) => theme.spacing(2)};
`;
const StyledObjectSummary = styled.div`
align-items: center;
display: flex;
justify-content: space-between;
margin-bottom: ${({ theme }) => theme.spacing(2)};
`;
const StyledObjectName = styled.div`
align-items: center;
display: flex;
font-weight: ${({ theme }) => theme.font.weight.medium};
gap: ${({ theme }) => theme.spacing(1)};
`;
const StyledFieldPreview = styled.div`
align-items: center;
background-color: ${({ theme }) => theme.background.primary};
border: 1px solid ${({ theme }) => theme.border.color.medium};
border-radius: ${({ theme }) => theme.border.radius.sm};
display: flex;
gap: ${({ theme }) => theme.spacing(2)};
height: ${({ theme }) => theme.spacing(8)};
overflow: hidden;
padding-left: ${({ theme }) => theme.spacing(2)};
white-space: nowrap;
`;
const StyledFieldLabel = styled.div`
align-items: center;
color: ${({ theme }) => theme.font.color.tertiary};
display: flex;
gap: ${({ theme }) => theme.spacing(1)};
`;
export const SettingsObjectFieldPreview = ({
objectIconKey,
objectLabelPlural,
isObjectCustom,
fieldIconKey,
fieldLabel,
fieldValue,
}: SettingsObjectFieldPreviewProps) => {
const theme = useTheme();
const { Icon: ObjectIcon } = useLazyLoadIcon(objectIconKey);
const { Icon: FieldIcon } = useLazyLoadIcon(fieldIconKey);
return (
<StyledContainer>
<StyledObjectSummary>
<StyledObjectName>
{!!ObjectIcon && (
<ObjectIcon
size={theme.icon.size.sm}
stroke={theme.icon.stroke.sm}
/>
)}
{objectLabelPlural}
</StyledObjectName>
{isObjectCustom ? (
<Tag color="orange" text="Custom" />
) : (
<Tag color="blue" text="Standard" />
)}
</StyledObjectSummary>
<StyledFieldPreview>
<StyledFieldLabel>
{!!FieldIcon && (
<FieldIcon
size={theme.icon.size.md}
stroke={theme.icon.stroke.sm}
/>
)}
{fieldLabel}:
</StyledFieldLabel>
{fieldValue}
</StyledFieldPreview>
</StyledContainer>
);
};

View File

@@ -0,0 +1,52 @@
import { ReactNode } from 'react';
import styled from '@emotion/styled';
type SettingsObjectFieldPreviewCardProps = {
preview: ReactNode;
form?: ReactNode;
};
const StyledPreviewContainer = styled.div`
background-color: ${({ theme }) => theme.background.transparent.lighter};
border: 1px solid ${({ theme }) => theme.border.color.medium};
border-radius: ${({ theme }) => theme.border.radius.sm};
padding: ${({ theme }) => theme.spacing(4)};
&:not(:last-child) {
border-bottom-left-radius: 0;
border-bottom-right-radius: 0;
}
`;
const StyledTitle = styled.h3`
color: ${({ theme }) => theme.font.color.extraLight};
font-size: ${({ theme }) => theme.font.size.sm};
font-weight: ${({ theme }) => theme.font.weight.medium};
margin: 0;
margin-bottom: ${({ theme }) => theme.spacing(4)};
`;
const StyledFormContainer = styled.div`
background-color: ${({ theme }) => theme.background.secondary};
border: 1px solid ${({ theme }) => theme.border.color.medium};
border-radius: ${({ theme }) => theme.border.radius.sm};
border-top: 0;
border-top-left-radius: 0;
border-top-right-radius: 0;
padding: ${({ theme }) => theme.spacing(4)};
`;
export const SettingsObjectFieldPreviewCard = ({
preview,
form,
}: SettingsObjectFieldPreviewCardProps) => {
return (
<div>
<StyledPreviewContainer>
<StyledTitle>Preview</StyledTitle>
{preview}
</StyledPreviewContainer>
{!!form && <StyledFormContainer>{form}</StyledFormContainer>}
</div>
);
};

View File

@@ -0,0 +1,26 @@
import { Meta, StoryObj } from '@storybook/react';
import { ComponentDecorator } from '~/testing/decorators/ComponentDecorator';
import { SettingsObjectFieldPreview } from '../SettingsObjectFieldPreview';
const meta: Meta<typeof SettingsObjectFieldPreview> = {
title: 'Modules/Settings/DataModel/SettingsObjectFieldPreview',
component: SettingsObjectFieldPreview,
decorators: [ComponentDecorator],
args: {
objectIconKey: 'IconUser',
objectLabelPlural: 'People',
fieldIconKey: 'IconNotes',
fieldLabel: 'Description',
fieldValue:
'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vestibulum magna enim, dapibus non enim in, lacinia faucibus nunc. Sed interdum ante sed felis facilisis, eget ultricies neque molestie. Mauris auctor, justo eu volutpat cursus, libero erat tempus nulla, non sodales lorem lacus a est.',
},
};
export default meta;
type Story = StoryObj<typeof SettingsObjectFieldPreview>;
export const StandardObject: Story = { args: { isObjectCustom: false } };
export const CustomObject: Story = { args: { isObjectCustom: true } };

View File

@@ -0,0 +1,34 @@
import { Meta, StoryObj } from '@storybook/react';
import { TextInput } from '@/ui/input/components/TextInput';
import { ComponentDecorator } from '~/testing/decorators/ComponentDecorator';
import { SettingsObjectFieldPreview } from '../SettingsObjectFieldPreview';
import { SettingsObjectFieldPreviewCard } from '../SettingsObjectFieldPreviewCard';
const meta: Meta<typeof SettingsObjectFieldPreviewCard> = {
title: 'Modules/Settings/DataModel/SettingsObjectFieldPreviewCard',
component: SettingsObjectFieldPreviewCard,
decorators: [ComponentDecorator],
args: {
preview: (
<SettingsObjectFieldPreview
objectIconKey="IconUser"
objectLabelPlural="People"
isObjectCustom={false}
fieldIconKey="IconNotes"
fieldLabel="Description"
fieldValue="Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vestibulum magna enim, dapibus non enim in, lacinia faucibus nunc. Sed interdum ante sed felis facilisis, eget ultricies neque molestie. Mauris auctor, justo eu volutpat cursus, libero erat tempus nulla, non sodales lorem lacus a est."
/>
),
},
};
export default meta;
type Story = StoryObj<typeof SettingsObjectFieldPreviewCard>;
export const Default: Story = {};
export const WithForm: Story = {
args: { form: <TextInput label="Lorem ipsum" placeholder="Lorem ipsum" /> },
};

View File

@@ -2,20 +2,21 @@ import { useEffect, useState } from 'react';
import { IconComponent } from '@/ui/display/icon/types/IconComponent';
import { useLazyLoadIcons } from './useLazyLoadIcons';
export const useLazyLoadIcon = (iconKey: string) => {
const { isLoadingIcons, icons } = useLazyLoadIcons();
const [Icon, setIcon] = useState<IconComponent | undefined>();
const [isLoadingIcon, setIsLoadingIcon] = useState(true);
useEffect(() => {
if (!iconKey) return;
import(`@tabler/icons-react/dist/esm/icons/${iconKey}.js`).then(
(lazyLoadedIcon) => {
setIcon(lazyLoadedIcon.default);
setIsLoadingIcon(false);
},
);
}, [iconKey]);
if (!isLoadingIcons) {
setIcon(icons[iconKey]);
setIsLoadingIcon(false);
}
}, [iconKey, icons, isLoadingIcons]);
return { Icon, isLoadingIcon };
};