mirror of
https://github.com/twentyhq/twenty.git
synced 2024-12-24 20:42:05 +03:00
Update new/edit object according to figma (#3093)
* made changes according to figma * remove click custom in test
This commit is contained in:
parent
4918865132
commit
b416b0f98f
@ -2,17 +2,20 @@ import styled from '@emotion/styled';
|
|||||||
|
|
||||||
import { validateMetadataLabel } from '@/object-metadata/utils/validateMetadataLabel';
|
import { validateMetadataLabel } from '@/object-metadata/utils/validateMetadataLabel';
|
||||||
import { H2Title } from '@/ui/display/typography/components/H2Title';
|
import { H2Title } from '@/ui/display/typography/components/H2Title';
|
||||||
|
import { IconPicker } from '@/ui/input/components/IconPicker';
|
||||||
import { TextArea } from '@/ui/input/components/TextArea';
|
import { TextArea } from '@/ui/input/components/TextArea';
|
||||||
import { TextInput } from '@/ui/input/components/TextInput';
|
import { TextInput } from '@/ui/input/components/TextInput';
|
||||||
import { Section } from '@/ui/layout/section/components/Section';
|
import { Section } from '@/ui/layout/section/components/Section';
|
||||||
|
|
||||||
type SettingsObjectFormSectionProps = {
|
type SettingsObjectFormSectionProps = {
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
|
icon?: string;
|
||||||
singularName?: string;
|
singularName?: string;
|
||||||
pluralName?: string;
|
pluralName?: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
onChange?: (
|
onChange?: (
|
||||||
formValues: Partial<{
|
formValues: Partial<{
|
||||||
|
icon: string;
|
||||||
labelSingular: string;
|
labelSingular: string;
|
||||||
labelPlural: string;
|
labelPlural: string;
|
||||||
description: string;
|
description: string;
|
||||||
@ -27,8 +30,22 @@ const StyledInputsContainer = styled.div`
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
const StyledLabel = styled.span`
|
||||||
|
color: ${({ theme }) => theme.font.color.light};
|
||||||
|
font-size: ${({ theme }) => theme.font.size.xs};
|
||||||
|
font-weight: ${({ theme }) => theme.font.weight.semiBold};
|
||||||
|
margin-bottom: ${({ theme }) => theme.spacing(1)};
|
||||||
|
text-transform: uppercase;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const StyledInputContainer = styled.div`
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
`;
|
||||||
|
|
||||||
export const SettingsObjectFormSection = ({
|
export const SettingsObjectFormSection = ({
|
||||||
disabled,
|
disabled,
|
||||||
|
icon = 'IconPigMoney',
|
||||||
singularName = '',
|
singularName = '',
|
||||||
pluralName = '',
|
pluralName = '',
|
||||||
description = '',
|
description = '',
|
||||||
@ -36,10 +53,20 @@ export const SettingsObjectFormSection = ({
|
|||||||
}: SettingsObjectFormSectionProps) => (
|
}: SettingsObjectFormSectionProps) => (
|
||||||
<Section>
|
<Section>
|
||||||
<H2Title
|
<H2Title
|
||||||
title="Name and description"
|
title="About"
|
||||||
description="Name in both singular (e.g., 'Invoice') and plural (e.g., 'Invoices') forms."
|
description="Name in both singular (e.g., 'Invoice') and plural (e.g., 'Invoices') forms."
|
||||||
/>
|
/>
|
||||||
<StyledInputsContainer>
|
<StyledInputsContainer>
|
||||||
|
<StyledInputContainer>
|
||||||
|
<StyledLabel>Icon</StyledLabel>
|
||||||
|
<IconPicker
|
||||||
|
disabled={disabled}
|
||||||
|
selectedIconKey={icon}
|
||||||
|
onChange={(icon) => {
|
||||||
|
onChange?.({ icon: icon.iconKey });
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</StyledInputContainer>
|
||||||
<TextInput
|
<TextInput
|
||||||
label="Singular"
|
label="Singular"
|
||||||
placeholder="Investor"
|
placeholder="Investor"
|
||||||
|
@ -1,70 +0,0 @@
|
|||||||
import { useTheme } from '@emotion/react';
|
|
||||||
import styled from '@emotion/styled';
|
|
||||||
|
|
||||||
import { IconBox, IconDatabase, IconFileCheck } from '@/ui/display/icon';
|
|
||||||
|
|
||||||
import { SettingsObjectTypeCard } from './SettingsObjectTypeCard';
|
|
||||||
|
|
||||||
export type NewObjectType = 'Standard' | 'Custom' | 'Remote';
|
|
||||||
|
|
||||||
type SettingsNewObjectTypeProps = {
|
|
||||||
selectedType?: NewObjectType;
|
|
||||||
onTypeSelect?: (type: NewObjectType) => void;
|
|
||||||
};
|
|
||||||
|
|
||||||
const StyledContainer = styled.div`
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
gap: ${({ theme }) => theme.spacing(2)};
|
|
||||||
`;
|
|
||||||
|
|
||||||
export const SettingsNewObjectType = ({
|
|
||||||
selectedType,
|
|
||||||
onTypeSelect,
|
|
||||||
}: SettingsNewObjectTypeProps) => {
|
|
||||||
const theme = useTheme();
|
|
||||||
return (
|
|
||||||
<StyledContainer>
|
|
||||||
<SettingsObjectTypeCard
|
|
||||||
title={'Standard'}
|
|
||||||
color="blue"
|
|
||||||
selected={selectedType === 'Standard'}
|
|
||||||
prefixIcon={
|
|
||||||
<IconFileCheck
|
|
||||||
size={theme.icon.size.lg}
|
|
||||||
stroke={theme.icon.stroke.sm}
|
|
||||||
color={theme.font.color.tertiary}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
onClick={() => onTypeSelect?.('Standard')}
|
|
||||||
/>
|
|
||||||
<SettingsObjectTypeCard
|
|
||||||
title="Custom"
|
|
||||||
color="orange"
|
|
||||||
selected={selectedType === 'Custom'}
|
|
||||||
prefixIcon={
|
|
||||||
<IconBox
|
|
||||||
size={theme.icon.size.lg}
|
|
||||||
stroke={theme.icon.stroke.sm}
|
|
||||||
color={theme.font.color.tertiary}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
onClick={() => onTypeSelect?.('Custom')}
|
|
||||||
/>
|
|
||||||
<SettingsObjectTypeCard
|
|
||||||
title="Remote"
|
|
||||||
soon
|
|
||||||
disabled
|
|
||||||
color="green"
|
|
||||||
selected={selectedType === 'Remote'}
|
|
||||||
prefixIcon={
|
|
||||||
<IconDatabase
|
|
||||||
size={theme.icon.size.lg}
|
|
||||||
stroke={theme.icon.stroke.sm}
|
|
||||||
color={theme.font.color.tertiary}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</StyledContainer>
|
|
||||||
);
|
|
||||||
};
|
|
@ -1,81 +0,0 @@
|
|||||||
import { useTheme } from '@emotion/react';
|
|
||||||
import styled from '@emotion/styled';
|
|
||||||
|
|
||||||
import { IconCheck } from '@/ui/display/icon';
|
|
||||||
import { SoonPill } from '@/ui/display/pill/components/SoonPill';
|
|
||||||
import { Tag } from '@/ui/display/tag/components/Tag';
|
|
||||||
import { ThemeColor } from '@/ui/theme/constants/colors';
|
|
||||||
|
|
||||||
const StyledObjectTypeCard = styled.div<SettingsObjectTypeCardProps>`
|
|
||||||
${({ theme, disabled, selected }) => `
|
|
||||||
background: ${theme.background.transparent.primary};
|
|
||||||
cursor: ${disabled ? 'not-allowed' : 'pointer'};
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
font-family: ${theme.font.family};
|
|
||||||
font-weight: 500;
|
|
||||||
border-style: solid;
|
|
||||||
border-width: '1px';
|
|
||||||
padding: ${theme.spacing(3)};
|
|
||||||
border-radius: ${theme.border.radius.sm};
|
|
||||||
gap: ${theme.spacing(2)};
|
|
||||||
border-color: ${
|
|
||||||
selected ? theme.border.color.inverted : theme.border.color.medium
|
|
||||||
};
|
|
||||||
color: ${theme.font.color.primary};
|
|
||||||
align-items: center;
|
|
||||||
width: 140px;
|
|
||||||
`}
|
|
||||||
`;
|
|
||||||
|
|
||||||
const StyledTag = styled(Tag)`
|
|
||||||
box-sizing: border-box;
|
|
||||||
height: ${({ theme }) => theme.spacing(5)};
|
|
||||||
`;
|
|
||||||
|
|
||||||
const StyledIconCheck = styled(IconCheck)`
|
|
||||||
margin-left: auto;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const StyledSoonPill = styled(SoonPill)`
|
|
||||||
margin-left: auto;
|
|
||||||
`;
|
|
||||||
|
|
||||||
type SettingsObjectTypeCardProps = {
|
|
||||||
prefixIcon?: React.ReactNode;
|
|
||||||
title: string;
|
|
||||||
soon?: boolean;
|
|
||||||
disabled?: boolean;
|
|
||||||
color: ThemeColor;
|
|
||||||
selected: boolean;
|
|
||||||
onClick?: () => void;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const SettingsObjectTypeCard = ({
|
|
||||||
prefixIcon,
|
|
||||||
title,
|
|
||||||
soon = false,
|
|
||||||
selected,
|
|
||||||
disabled = false,
|
|
||||||
color,
|
|
||||||
onClick,
|
|
||||||
}: SettingsObjectTypeCardProps) => {
|
|
||||||
const theme = useTheme();
|
|
||||||
return (
|
|
||||||
<StyledObjectTypeCard
|
|
||||||
title={title}
|
|
||||||
soon={soon}
|
|
||||||
disabled={disabled}
|
|
||||||
color={color}
|
|
||||||
selected={selected}
|
|
||||||
onClick={onClick}
|
|
||||||
>
|
|
||||||
{prefixIcon}
|
|
||||||
<StyledTag color={color} text={title} />
|
|
||||||
{soon && <StyledSoonPill />}
|
|
||||||
{!disabled && selected && <StyledIconCheck size={theme.icon.size.md} />}
|
|
||||||
</StyledObjectTypeCard>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export {};
|
|
@ -7,35 +7,17 @@ import { SaveAndCancelButtons } from '@/settings/components/SaveAndCancelButtons
|
|||||||
import { SettingsHeaderContainer } from '@/settings/components/SettingsHeaderContainer';
|
import { SettingsHeaderContainer } from '@/settings/components/SettingsHeaderContainer';
|
||||||
import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer';
|
import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer';
|
||||||
import { SettingsObjectFormSection } from '@/settings/data-model/components/SettingsObjectFormSection';
|
import { SettingsObjectFormSection } from '@/settings/data-model/components/SettingsObjectFormSection';
|
||||||
import { SettingsAvailableStandardObjectsSection } from '@/settings/data-model/new-object/components/SettingsAvailableStandardObjectsSection';
|
|
||||||
import {
|
|
||||||
NewObjectType,
|
|
||||||
SettingsNewObjectType,
|
|
||||||
} from '@/settings/data-model/new-object/components/SettingsNewObjectType';
|
|
||||||
import { SettingsObjectIconSection } from '@/settings/data-model/object-edit/SettingsObjectIconSection';
|
|
||||||
import { IconSettings } from '@/ui/display/icon';
|
import { IconSettings } from '@/ui/display/icon';
|
||||||
import { H2Title } from '@/ui/display/typography/components/H2Title';
|
|
||||||
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
|
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
|
||||||
import { SubMenuTopBarContainer } from '@/ui/layout/page/SubMenuTopBarContainer';
|
import { SubMenuTopBarContainer } from '@/ui/layout/page/SubMenuTopBarContainer';
|
||||||
import { Section } from '@/ui/layout/section/components/Section';
|
|
||||||
import { Breadcrumb } from '@/ui/navigation/bread-crumb/components/Breadcrumb';
|
import { Breadcrumb } from '@/ui/navigation/bread-crumb/components/Breadcrumb';
|
||||||
|
|
||||||
export const SettingsNewObject = () => {
|
export const SettingsNewObject = () => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const [selectedObjectType, setSelectedObjectType] =
|
|
||||||
useState<NewObjectType>('Standard');
|
|
||||||
const { enqueueSnackBar } = useSnackBar();
|
const { enqueueSnackBar } = useSnackBar();
|
||||||
|
|
||||||
const {
|
const { createObjectMetadataItem: createObject } =
|
||||||
activateObjectMetadataItem: activateObject,
|
useObjectMetadataItemForSettings();
|
||||||
createObjectMetadataItem: createObject,
|
|
||||||
disabledObjectMetadataItems: disabledObjects,
|
|
||||||
} = useObjectMetadataItemForSettings();
|
|
||||||
|
|
||||||
const [
|
|
||||||
selectedStandardObjectMetadataIds,
|
|
||||||
setSelectedStandardObjectMetadataIds,
|
|
||||||
] = useState<Record<string, boolean>>({});
|
|
||||||
|
|
||||||
const [customFormValues, setCustomFormValues] = useState<{
|
const [customFormValues, setCustomFormValues] = useState<{
|
||||||
description?: string;
|
description?: string;
|
||||||
@ -45,49 +27,28 @@ export const SettingsNewObject = () => {
|
|||||||
}>({ icon: 'IconPigMoney', labelPlural: '', labelSingular: '' });
|
}>({ icon: 'IconPigMoney', labelPlural: '', labelSingular: '' });
|
||||||
|
|
||||||
const canSave =
|
const canSave =
|
||||||
(selectedObjectType === 'Standard' &&
|
!!customFormValues.labelPlural && !!customFormValues.labelSingular;
|
||||||
Object.values(selectedStandardObjectMetadataIds).some(
|
|
||||||
(isSelected) => isSelected,
|
|
||||||
)) ||
|
|
||||||
(selectedObjectType === 'Custom' &&
|
|
||||||
!!customFormValues.labelPlural &&
|
|
||||||
!!customFormValues.labelSingular);
|
|
||||||
|
|
||||||
const handleSave = async () => {
|
const handleSave = async () => {
|
||||||
if (selectedObjectType === 'Standard') {
|
try {
|
||||||
await Promise.all(
|
const createdObject = await createObject({
|
||||||
Object.entries(selectedStandardObjectMetadataIds).map(
|
labelPlural: customFormValues.labelPlural,
|
||||||
([standardObjectMetadataId, isSelected]) =>
|
labelSingular: customFormValues.labelSingular,
|
||||||
isSelected
|
description: customFormValues.description,
|
||||||
? activateObject({ id: standardObjectMetadataId })
|
icon: customFormValues.icon,
|
||||||
: undefined,
|
});
|
||||||
),
|
|
||||||
|
navigate(
|
||||||
|
createdObject.data?.createOneObject.isActive
|
||||||
|
? `/settings/objects/${getObjectSlug(
|
||||||
|
createdObject.data.createOneObject,
|
||||||
|
)}`
|
||||||
|
: '/settings/objects',
|
||||||
);
|
);
|
||||||
|
} catch (error) {
|
||||||
navigate('/settings/objects');
|
enqueueSnackBar((error as Error).message, {
|
||||||
}
|
variant: 'error',
|
||||||
|
});
|
||||||
if (selectedObjectType === 'Custom') {
|
|
||||||
try {
|
|
||||||
const createdObject = await createObject({
|
|
||||||
labelPlural: customFormValues.labelPlural,
|
|
||||||
labelSingular: customFormValues.labelSingular,
|
|
||||||
description: customFormValues.description,
|
|
||||||
icon: customFormValues.icon,
|
|
||||||
});
|
|
||||||
|
|
||||||
navigate(
|
|
||||||
createdObject.data?.createOneObject.isActive
|
|
||||||
? `/settings/objects/${getObjectSlug(
|
|
||||||
createdObject.data.createOneObject,
|
|
||||||
)}`
|
|
||||||
: '/settings/objects',
|
|
||||||
);
|
|
||||||
} catch (error) {
|
|
||||||
enqueueSnackBar((error as Error).message, {
|
|
||||||
variant: 'error',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -109,53 +70,18 @@ export const SettingsNewObject = () => {
|
|||||||
onSave={handleSave}
|
onSave={handleSave}
|
||||||
/>
|
/>
|
||||||
</SettingsHeaderContainer>
|
</SettingsHeaderContainer>
|
||||||
<Section>
|
<SettingsObjectFormSection
|
||||||
<H2Title
|
icon={customFormValues.icon}
|
||||||
title="Object type"
|
singularName={customFormValues.labelSingular}
|
||||||
description="The type of object you want to add"
|
pluralName={customFormValues.labelPlural}
|
||||||
/>
|
description={customFormValues.description}
|
||||||
<SettingsNewObjectType
|
onChange={(formValues) => {
|
||||||
selectedType={selectedObjectType}
|
setCustomFormValues((previousValues) => ({
|
||||||
onTypeSelect={setSelectedObjectType}
|
...previousValues,
|
||||||
/>
|
...formValues,
|
||||||
</Section>
|
}));
|
||||||
{selectedObjectType === 'Standard' && (
|
}}
|
||||||
<SettingsAvailableStandardObjectsSection
|
/>
|
||||||
objectItems={disabledObjects.filter(({ isCustom }) => !isCustom)}
|
|
||||||
onChange={(selectedIds) =>
|
|
||||||
setSelectedStandardObjectMetadataIds((previousSelectedIds) => ({
|
|
||||||
...previousSelectedIds,
|
|
||||||
...selectedIds,
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
selectedIds={selectedStandardObjectMetadataIds}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{selectedObjectType === 'Custom' && (
|
|
||||||
<>
|
|
||||||
<SettingsObjectIconSection
|
|
||||||
label={customFormValues.labelPlural}
|
|
||||||
iconKey={customFormValues.icon}
|
|
||||||
onChange={({ iconKey }) => {
|
|
||||||
setCustomFormValues((previousValues) => ({
|
|
||||||
...previousValues,
|
|
||||||
icon: iconKey,
|
|
||||||
}));
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<SettingsObjectFormSection
|
|
||||||
singularName={customFormValues.labelSingular}
|
|
||||||
pluralName={customFormValues.labelPlural}
|
|
||||||
description={customFormValues.description}
|
|
||||||
onChange={(formValues) => {
|
|
||||||
setCustomFormValues((previousValues) => ({
|
|
||||||
...previousValues,
|
|
||||||
...formValues,
|
|
||||||
}));
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</SettingsPageContainer>
|
</SettingsPageContainer>
|
||||||
</SubMenuTopBarContainer>
|
</SubMenuTopBarContainer>
|
||||||
);
|
);
|
||||||
|
@ -7,7 +7,6 @@ import { SaveAndCancelButtons } from '@/settings/components/SaveAndCancelButtons
|
|||||||
import { SettingsHeaderContainer } from '@/settings/components/SettingsHeaderContainer';
|
import { SettingsHeaderContainer } from '@/settings/components/SettingsHeaderContainer';
|
||||||
import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer';
|
import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer';
|
||||||
import { SettingsObjectFormSection } from '@/settings/data-model/components/SettingsObjectFormSection';
|
import { SettingsObjectFormSection } from '@/settings/data-model/components/SettingsObjectFormSection';
|
||||||
import { SettingsObjectIconSection } from '@/settings/data-model/object-edit/SettingsObjectIconSection';
|
|
||||||
import { AppPath } from '@/types/AppPath';
|
import { AppPath } from '@/types/AppPath';
|
||||||
import { IconArchive, IconSettings } from '@/ui/display/icon';
|
import { IconArchive, IconSettings } from '@/ui/display/icon';
|
||||||
import { H2Title } from '@/ui/display/typography/components/H2Title';
|
import { H2Title } from '@/ui/display/typography/components/H2Title';
|
||||||
@ -113,19 +112,9 @@ export const SettingsObjectEdit = () => {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</SettingsHeaderContainer>
|
</SettingsHeaderContainer>
|
||||||
<SettingsObjectIconSection
|
|
||||||
disabled={!activeObjectMetadataItem.isCustom}
|
|
||||||
iconKey={formValues.icon}
|
|
||||||
label={formValues.labelPlural}
|
|
||||||
onChange={({ iconKey }) =>
|
|
||||||
setFormValues((previousFormValues) => ({
|
|
||||||
...previousFormValues,
|
|
||||||
icon: iconKey,
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<SettingsObjectFormSection
|
<SettingsObjectFormSection
|
||||||
disabled={!activeObjectMetadataItem.isCustom}
|
disabled={!activeObjectMetadataItem.isCustom}
|
||||||
|
icon={formValues.icon}
|
||||||
singularName={formValues.labelSingular}
|
singularName={formValues.labelSingular}
|
||||||
pluralName={formValues.labelPlural}
|
pluralName={formValues.labelPlural}
|
||||||
description={formValues.description}
|
description={formValues.description}
|
||||||
|
@ -48,7 +48,7 @@ export const SettingsObjects = () => {
|
|||||||
<StyledH1Title title="Objects" />
|
<StyledH1Title title="Objects" />
|
||||||
<Button
|
<Button
|
||||||
Icon={IconPlus}
|
Icon={IconPlus}
|
||||||
title="New object"
|
title="Add object"
|
||||||
accent="blue"
|
accent="blue"
|
||||||
size="small"
|
size="small"
|
||||||
onClick={() => navigate('/settings/objects/new')}
|
onClick={() => navigate('/settings/objects/new')}
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
import { Meta, StoryObj } from '@storybook/react';
|
import { Meta, StoryObj } from '@storybook/react';
|
||||||
import { userEvent, within } from '@storybook/test';
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
PageDecorator,
|
PageDecorator,
|
||||||
@ -31,15 +30,3 @@ export const WithStandardSelected: Story = {
|
|||||||
await sleep(100);
|
await sleep(100);
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export const WithCustomSelected: Story = {
|
|
||||||
play: async ({ canvasElement }) => {
|
|
||||||
const canvas = within(canvasElement);
|
|
||||||
|
|
||||||
await sleep(1000);
|
|
||||||
|
|
||||||
const customButtonElement = canvas.getByText('Custom');
|
|
||||||
|
|
||||||
await userEvent.click(customButtonElement);
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
Loading…
Reference in New Issue
Block a user