Implement object fields and settings new layout (#7979)

### Description

- This PR has as the base branch the TWNTY-5491 branch, but we also had
to include updates from the main branch, and currently, there are
conflicts in the TWNTY-5491, that cause errors on typescript in this PR,
so, we can update once the conflicts are resolved on the base branch,
but the functionality can be reviewed anyway
- We Implemented a new layout of object details settings and new, the
data is auto-saved in `Settings `tab of object detail
- There is no indication to the user that data are saved automatically
in the design, currently we are disabling the form

### Demo\

<https://www.loom.com/share/4198c0aa54b5450780a570ceee574838?sid=b4ef0a42-2d41-435f-9f5f-1b16816939f7>

### Refs

#TWNTY-5491

---------

Co-authored-by: gitstart-twenty <gitstart-twenty@users.noreply.github.com>
Co-authored-by: gitstart-twenty <140154534+gitstart-twenty@users.noreply.github.com>
Co-authored-by: Marie Stoppa <marie.stoppa@essec.edu>
Co-authored-by: Weiko <corentin@twenty.com>
This commit is contained in:
gitstart-app[bot]
2024-11-07 14:50:53 +01:00
committed by GitHub
parent 3be30651b7
commit 7bab65b569
25 changed files with 496 additions and 391 deletions

View File

@ -49,6 +49,7 @@ type SettingsDataModelObjectAboutFormProps = {
disabled?: boolean;
disableNameEdit?: boolean;
objectMetadataItem?: ObjectMetadataItem;
onBlur?: () => void;
};
const StyledInputsContainer = styled.div`
@ -68,12 +69,16 @@ const StyledAdvancedSettingsSectionInputWrapper = styled.div`
flex-direction: column;
gap: ${({ theme }) => theme.spacing(4)};
width: 100%;
flex: 1;
`;
const StyledAdvancedSettingsOuterContainer = styled.div`
padding-top: ${({ theme }) => theme.spacing(4)};
`;
const StyledAdvancedSettingsContainer = styled.div`
display: flex;
gap: ${({ theme }) => theme.spacing(2)};
padding-top: ${({ theme }) => theme.spacing(4)};
position: relative;
width: 100%;
`;
@ -81,7 +86,7 @@ const StyledAdvancedSettingsContainer = styled.div`
const StyledIconToolContainer = styled.div`
border-right: 1px solid ${MAIN_COLORS.yellow};
display: flex;
left: ${({ theme }) => theme.spacing(-5)};
left: ${({ theme }) => theme.spacing(-6)};
position: absolute;
height: 100%;
`;
@ -105,6 +110,7 @@ export const SettingsDataModelObjectAboutForm = ({
disabled,
disableNameEdit,
objectMetadataItem,
onBlur,
}: SettingsDataModelObjectAboutFormProps) => {
const { control, watch, setValue } =
useFormContext<SettingsDataModelObjectAboutFormValues>();
@ -117,6 +123,9 @@ export const SettingsDataModelObjectAboutForm = ({
const isLabelSyncedWithName = watch(IS_LABEL_SYNCED_WITH_NAME_LABEL);
const labelSingular = watch('labelSingular');
const labelPlural = watch('labelPlural');
watch('nameSingular');
watch('namePlural');
watch('description');
const apiNameTooltipText = isLabelSyncedWithName
? 'Deactivate "Synchronize Objects Labels and API Names" to set a custom API name'
: 'Input must be in camel case and cannot start with a number';
@ -138,14 +147,14 @@ export const SettingsDataModelObjectAboutForm = ({
setValue(
'nameSingular',
computeMetadataNameFromLabelOrThrow(labelSingular),
{ shouldDirty: false },
{ shouldDirty: true },
);
};
const fillNamePluralFromLabelPlural = (labelPlural: string) => {
isDefined(labelPlural) &&
setValue('namePlural', computeMetadataNameFromLabelOrThrow(labelPlural), {
shouldDirty: false,
shouldDirty: true,
});
};
@ -184,6 +193,7 @@ export const SettingsDataModelObjectAboutForm = ({
fillNameSingularFromLabelSingular(value);
}
}}
onBlur={onBlur}
disabled={disabled || disableNameEdit}
fullWidth
maxLength={OBJECT_NAME_MAXIMUM_LENGTH}
@ -236,105 +246,110 @@ export const SettingsDataModelObjectAboutForm = ({
exit="exit"
variants={motionAnimationVariants}
>
<StyledAdvancedSettingsContainer>
<StyledIconToolContainer>
<StyledIconTool size={12} color={MAIN_COLORS.yellow} />
</StyledIconToolContainer>
<StyledAdvancedSettingsSectionInputWrapper>
{[
{
label: 'API Name (Singular)',
fieldName: 'nameSingular' as const,
placeholder: 'listing',
defaultValue: objectMetadataItem?.nameSingular,
disabled:
disabled || disableNameEdit || isLabelSyncedWithName,
tooltip: apiNameTooltipText,
},
{
label: 'API Name (Plural)',
fieldName: 'namePlural' as const,
placeholder: 'listings',
defaultValue: objectMetadataItem?.namePlural,
disabled:
disabled || disableNameEdit || isLabelSyncedWithName,
tooltip: apiNameTooltipText,
},
].map(
({
defaultValue,
fieldName,
label,
placeholder,
disabled,
tooltip,
}) => (
<StyledInputContainer
key={`object-${fieldName}-text-input`}
>
<Controller
name={fieldName}
control={control}
defaultValue={defaultValue}
render={({ field: { onChange, value } }) => (
<>
<TextInput
label={label}
placeholder={placeholder}
value={value}
onChange={onChange}
disabled={disabled}
fullWidth
maxLength={OBJECT_NAME_MAXIMUM_LENGTH}
RightIcon={() =>
tooltip && (
<>
<IconInfoCircle
id={infoCircleElementId + fieldName}
size={theme.icon.size.md}
color={theme.font.color.tertiary}
/>
<AppTooltip
anchorSelect={`#${infoCircleElementId}${fieldName}`}
content={tooltip}
offset={5}
noArrow
place="bottom"
positionStrategy="absolute"
delay={TooltipDelay.shortDelay}
/>
</>
)
}
/>
</>
)}
/>
</StyledInputContainer>
),
)}
<Controller
name={IS_LABEL_SYNCED_WITH_NAME_LABEL}
control={control}
defaultValue={
objectMetadataItem?.isLabelSyncedWithName ?? true
}
render={({ field: { onChange, value } }) => (
<SyncObjectLabelAndNameToggle
value={value ?? true}
onChange={(value) => {
onChange(value);
if (value === true) {
fillNamePluralFromLabelPlural(labelPlural);
fillNameSingularFromLabelSingular(labelSingular);
}
}}
/>
<StyledAdvancedSettingsOuterContainer>
<StyledAdvancedSettingsContainer>
<StyledIconToolContainer>
<StyledIconTool size={12} color={MAIN_COLORS.yellow} />
</StyledIconToolContainer>
<StyledAdvancedSettingsSectionInputWrapper>
{[
{
label: 'API Name (Singular)',
fieldName: 'nameSingular' as const,
placeholder: 'listing',
defaultValue: objectMetadataItem?.nameSingular,
disabled:
disabled || disableNameEdit || isLabelSyncedWithName,
tooltip: apiNameTooltipText,
},
{
label: 'API Name (Plural)',
fieldName: 'namePlural' as const,
placeholder: 'listings',
defaultValue: objectMetadataItem?.namePlural,
disabled:
disabled || disableNameEdit || isLabelSyncedWithName,
tooltip: apiNameTooltipText,
},
].map(
({
defaultValue,
fieldName,
label,
placeholder,
disabled,
tooltip,
}) => (
<StyledInputContainer
key={`object-${fieldName}-text-input`}
>
<Controller
name={fieldName}
control={control}
defaultValue={defaultValue}
render={({ field: { onChange, value } }) => (
<>
<TextInput
label={label}
placeholder={placeholder}
value={value}
onChange={onChange}
disabled={disabled}
fullWidth
maxLength={OBJECT_NAME_MAXIMUM_LENGTH}
onBlur={onBlur}
RightIcon={() =>
tooltip && (
<>
<IconInfoCircle
id={infoCircleElementId + fieldName}
size={theme.icon.size.md}
color={theme.font.color.tertiary}
style={{ outline: 'none' }}
/>
<AppTooltip
anchorSelect={`#${infoCircleElementId}${fieldName}`}
content={tooltip}
offset={5}
noArrow
place="bottom"
positionStrategy="fixed"
delay={TooltipDelay.shortDelay}
/>
</>
)
}
/>
</>
)}
/>
</StyledInputContainer>
),
)}
/>
</StyledAdvancedSettingsSectionInputWrapper>
</StyledAdvancedSettingsContainer>
<Controller
name={IS_LABEL_SYNCED_WITH_NAME_LABEL}
control={control}
defaultValue={
objectMetadataItem?.isLabelSyncedWithName ?? true
}
render={({ field: { onChange, value } }) => (
<SyncObjectLabelAndNameToggle
value={value ?? true}
disabled={!objectMetadataItem?.isCustom}
onChange={(value) => {
onChange(value);
if (value === true) {
fillNamePluralFromLabelPlural(labelPlural);
fillNameSingularFromLabelSingular(labelSingular);
}
onBlur?.();
}}
/>
)}
/>
</StyledAdvancedSettingsSectionInputWrapper>
</StyledAdvancedSettingsContainer>
</StyledAdvancedSettingsOuterContainer>
</motion.div>
)}
</AnimatePresence>

View File

@ -5,10 +5,11 @@ import { IconRefresh, MAIN_COLORS, Toggle } from 'twenty-ui';
const StyledToggleContainer = styled.div`
align-items: center;
border: 1px solid ${({ theme }) => theme.border.color.medium};
border-radius: ${({ theme }) => theme.border.radius.sm};
border-radius: ${({ theme }) => theme.border.radius.md};
display: flex;
justify-content: space-between;
padding: ${({ theme }) => theme.spacing(4)};
background: ${({ theme }) => theme.background.secondary};
`;
const StyledIconRefreshContainer = styled.div`
@ -40,17 +41,19 @@ const StyledDescription = styled.h3`
font-size: ${({ theme }) => theme.font.size.md};
font-weight: ${({ theme }) => theme.font.weight.regular};
margin: 0;
margin-top: ${({ theme }) => theme.spacing(2)};
margin-top: ${({ theme }) => theme.spacing(1)};
`;
type SyncObjectLabelAndNameToggleProps = {
value: boolean;
onChange: (value: boolean) => void;
disabled?: boolean;
};
export const SyncObjectLabelAndNameToggle = ({
value,
onChange,
disabled,
}: SyncObjectLabelAndNameToggleProps) => {
const theme = useTheme();
return (
@ -66,7 +69,12 @@ export const SyncObjectLabelAndNameToggle = ({
</StyledDescription>
</div>
</StyledTitleContainer>
<Toggle onChange={onChange} color={MAIN_COLORS.yellow} value={value} />
<Toggle
onChange={onChange}
color={MAIN_COLORS.yellow}
value={value}
disabled={disabled}
/>
</StyledToggleContainer>
);
};