diff --git a/front/src/modules/settings/data-model/components/SettingsObjectFieldPreview.tsx b/front/src/modules/settings/data-model/components/SettingsObjectFieldPreview.tsx
new file mode 100644
index 000000000..069275f5e
--- /dev/null
+++ b/front/src/modules/settings/data-model/components/SettingsObjectFieldPreview.tsx
@@ -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 (
+
+
+
+ {!!ObjectIcon && (
+
+ )}
+ {objectLabelPlural}
+
+ {isObjectCustom ? (
+
+ ) : (
+
+ )}
+
+
+
+ {!!FieldIcon && (
+
+ )}
+ {fieldLabel}:
+
+ {fieldValue}
+
+
+ );
+};
diff --git a/front/src/modules/settings/data-model/components/SettingsObjectFieldPreviewCard.tsx b/front/src/modules/settings/data-model/components/SettingsObjectFieldPreviewCard.tsx
new file mode 100644
index 000000000..48eab6686
--- /dev/null
+++ b/front/src/modules/settings/data-model/components/SettingsObjectFieldPreviewCard.tsx
@@ -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 (
+
+
+ Preview
+ {preview}
+
+ {!!form && {form}}
+
+ );
+};
diff --git a/front/src/modules/settings/data-model/components/__stories__/SettingsObjectFieldPreview.stories.tsx b/front/src/modules/settings/data-model/components/__stories__/SettingsObjectFieldPreview.stories.tsx
new file mode 100644
index 000000000..509ca9d9b
--- /dev/null
+++ b/front/src/modules/settings/data-model/components/__stories__/SettingsObjectFieldPreview.stories.tsx
@@ -0,0 +1,26 @@
+import { Meta, StoryObj } from '@storybook/react';
+
+import { ComponentDecorator } from '~/testing/decorators/ComponentDecorator';
+
+import { SettingsObjectFieldPreview } from '../SettingsObjectFieldPreview';
+
+const meta: Meta = {
+ 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;
+
+export const StandardObject: Story = { args: { isObjectCustom: false } };
+
+export const CustomObject: Story = { args: { isObjectCustom: true } };
diff --git a/front/src/modules/settings/data-model/components/__stories__/SettingsObjectFieldPreviewCard.stories.tsx b/front/src/modules/settings/data-model/components/__stories__/SettingsObjectFieldPreviewCard.stories.tsx
new file mode 100644
index 000000000..ed317d58c
--- /dev/null
+++ b/front/src/modules/settings/data-model/components/__stories__/SettingsObjectFieldPreviewCard.stories.tsx
@@ -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 = {
+ title: 'Modules/Settings/DataModel/SettingsObjectFieldPreviewCard',
+ component: SettingsObjectFieldPreviewCard,
+ decorators: [ComponentDecorator],
+ args: {
+ preview: (
+
+ ),
+ },
+};
+
+export default meta;
+type Story = StoryObj;
+
+export const Default: Story = {};
+
+export const WithForm: Story = {
+ args: { form: },
+};
diff --git a/front/src/modules/ui/input/hooks/useLazyLoadIcon.ts b/front/src/modules/ui/input/hooks/useLazyLoadIcon.ts
index 79de15fef..a6be3ab23 100644
--- a/front/src/modules/ui/input/hooks/useLazyLoadIcon.ts
+++ b/front/src/modules/ui/input/hooks/useLazyLoadIcon.ts
@@ -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();
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 };
};