mirror of
https://github.com/lingble/twenty.git
synced 2025-11-02 05:37:56 +00:00
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:
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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 } };
|
||||
@@ -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" /> },
|
||||
};
|
||||
@@ -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 };
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user