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:
committed by
GitHub
parent
3be30651b7
commit
7bab65b569
@ -38,6 +38,10 @@ const StyledYear = styled.span`
|
||||
color: ${({ theme }) => theme.font.color.light};
|
||||
`;
|
||||
|
||||
const StyledTitleContainer = styled.div`
|
||||
margin-bottom: ${({ theme }) => theme.spacing(4)};
|
||||
`;
|
||||
|
||||
export const Calendar = ({
|
||||
targetableObject,
|
||||
}: {
|
||||
@ -131,14 +135,16 @@ export const Calendar = ({
|
||||
|
||||
return (
|
||||
<Section key={monthTime}>
|
||||
<H3Title
|
||||
title={
|
||||
<>
|
||||
{monthLabel}
|
||||
{isLastMonthOfYear && <StyledYear> {year}</StyledYear>}
|
||||
</>
|
||||
}
|
||||
/>
|
||||
<StyledTitleContainer>
|
||||
<H3Title
|
||||
title={
|
||||
<>
|
||||
{monthLabel}
|
||||
{isLastMonthOfYear && <StyledYear> {year}</StyledYear>}
|
||||
</>
|
||||
}
|
||||
/>
|
||||
</StyledTitleContainer>
|
||||
<CalendarMonthCard dayTimes={monthDayTimes} />
|
||||
</Section>
|
||||
);
|
||||
|
||||
@ -143,12 +143,6 @@ const SettingsDevelopers = lazy(() =>
|
||||
})),
|
||||
);
|
||||
|
||||
const SettingsObjectEdit = lazy(() =>
|
||||
import('~/pages/settings/data-model/SettingsObjectEdit').then((module) => ({
|
||||
default: module.SettingsObjectEdit,
|
||||
})),
|
||||
);
|
||||
|
||||
const SettingsIntegrations = lazy(() =>
|
||||
import('~/pages/settings/integrations/SettingsIntegrations').then(
|
||||
(module) => ({
|
||||
@ -292,7 +286,6 @@ export const SettingsRoutes = ({
|
||||
path={SettingsPath.ObjectDetail}
|
||||
element={<SettingsObjectDetailPage />}
|
||||
/>
|
||||
<Route path={SettingsPath.ObjectEdit} element={<SettingsObjectEdit />} />
|
||||
<Route path={SettingsPath.NewObject} element={<SettingsNewObject />} />
|
||||
<Route path={SettingsPath.Developers} element={<SettingsDevelopers />} />
|
||||
{isCRMMigrationEnabled && (
|
||||
|
||||
@ -16,7 +16,7 @@ import { useApolloMetadataClient } from './useApolloMetadataClient';
|
||||
export const useUpdateOneObjectMetadataItem = () => {
|
||||
const apolloClientMetadata = useApolloMetadataClient();
|
||||
|
||||
const [mutate] = useMutation<
|
||||
const [mutate, { loading }] = useMutation<
|
||||
UpdateOneObjectMetadataItemMutation,
|
||||
UpdateOneObjectMetadataItemMutationVariables
|
||||
>(UPDATE_ONE_OBJECT_METADATA_ITEM, {
|
||||
@ -42,5 +42,6 @@ export const useUpdateOneObjectMetadataItem = () => {
|
||||
|
||||
return {
|
||||
updateOneObjectMetadataItem,
|
||||
loading,
|
||||
};
|
||||
};
|
||||
|
||||
@ -5,7 +5,9 @@ import styled from '@emotion/styled';
|
||||
import { ReactNode } from 'react';
|
||||
import { isDefined } from '~/utils/isDefined';
|
||||
|
||||
const StyledSettingsPageContainer = styled.div<{ width?: number }>`
|
||||
const StyledSettingsPageContainer = styled.div<{
|
||||
width?: number;
|
||||
}>`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: ${({ theme }) => theme.spacing(8)};
|
||||
|
||||
@ -0,0 +1,44 @@
|
||||
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||
import { SettingsObjectFieldTable } from '~/pages/settings/data-model/SettingsObjectFieldTable';
|
||||
|
||||
import styled from '@emotion/styled';
|
||||
import { Button, H2Title, IconPlus, Section, UndecoratedLink } from 'twenty-ui';
|
||||
|
||||
const StyledDiv = styled.div`
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
padding-top: ${({ theme }) => theme.spacing(2)};
|
||||
`;
|
||||
|
||||
type ObjectFieldsProps = {
|
||||
objectMetadataItem: ObjectMetadataItem;
|
||||
};
|
||||
|
||||
export const ObjectFields = ({ objectMetadataItem }: ObjectFieldsProps) => {
|
||||
const shouldDisplayAddFieldButton = !objectMetadataItem.isRemote;
|
||||
|
||||
return (
|
||||
<Section>
|
||||
<H2Title
|
||||
title="Fields"
|
||||
description={`Customise the fields available in the ${objectMetadataItem.labelSingular} views and their display order in the ${objectMetadataItem.labelSingular} detail view and menus.`}
|
||||
/>
|
||||
<SettingsObjectFieldTable
|
||||
objectMetadataItem={objectMetadataItem}
|
||||
mode="view"
|
||||
/>
|
||||
{shouldDisplayAddFieldButton && (
|
||||
<StyledDiv>
|
||||
<UndecoratedLink to={'./new-field/select'}>
|
||||
<Button
|
||||
Icon={IconPlus}
|
||||
title="Add Field"
|
||||
size="small"
|
||||
variant="secondary"
|
||||
/>
|
||||
</UndecoratedLink>
|
||||
</StyledDiv>
|
||||
)}
|
||||
</Section>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,20 @@
|
||||
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||
|
||||
import { H2Title, Section } from 'twenty-ui';
|
||||
import { SettingsObjectIndexTable } from '~/pages/settings/data-model/SettingsObjectIndexTable';
|
||||
|
||||
type ObjectIndexesProps = {
|
||||
objectMetadataItem: ObjectMetadataItem;
|
||||
};
|
||||
|
||||
export const ObjectIndexes = ({ objectMetadataItem }: ObjectIndexesProps) => {
|
||||
return (
|
||||
<Section>
|
||||
<H2Title
|
||||
title="Indexes"
|
||||
description={`Advanced feature to improve the performance of queries and to enforce unicity constraints.`}
|
||||
/>
|
||||
<SettingsObjectIndexTable objectMetadataItem={objectMetadataItem} />
|
||||
</Section>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,225 @@
|
||||
/* eslint-disable react/jsx-props-no-spreading */
|
||||
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { FormProvider, useForm } from 'react-hook-form';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { Button, H2Title, IconArchive, Section } from 'twenty-ui';
|
||||
import { z, ZodError } from 'zod';
|
||||
|
||||
import { useLastVisitedObjectMetadataItem } from '@/navigation/hooks/useLastVisitedObjectMetadataItem';
|
||||
import { useLastVisitedView } from '@/navigation/hooks/useLastVisitedView';
|
||||
import { useUpdateOneObjectMetadataItem } from '@/object-metadata/hooks/useUpdateOneObjectMetadataItem';
|
||||
import { getObjectSlug } from '@/object-metadata/utils/getObjectSlug';
|
||||
import { RecordFieldValueSelectorContextProvider } from '@/object-record/record-store/contexts/RecordFieldValueSelectorContext';
|
||||
import {
|
||||
IS_LABEL_SYNCED_WITH_NAME_LABEL,
|
||||
SettingsDataModelObjectAboutForm,
|
||||
settingsDataModelObjectAboutFormSchema,
|
||||
} from '@/settings/data-model/objects/forms/components/SettingsDataModelObjectAboutForm';
|
||||
import { settingsDataModelObjectIdentifiersFormSchema } from '@/settings/data-model/objects/forms/components/SettingsDataModelObjectIdentifiersForm';
|
||||
import { SettingsDataModelObjectSettingsFormCard } from '@/settings/data-model/objects/forms/components/SettingsDataModelObjectSettingsFormCard';
|
||||
import { settingsUpdateObjectInputSchema } from '@/settings/data-model/validation-schemas/settingsUpdateObjectInputSchema';
|
||||
import { getSettingsPagePath } from '@/settings/utils/getSettingsPagePath';
|
||||
import { SettingsPath } from '@/types/SettingsPath';
|
||||
import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar';
|
||||
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
|
||||
import { navigationMemorizedUrlState } from '@/ui/navigation/states/navigationMemorizedUrlState';
|
||||
import styled from '@emotion/styled';
|
||||
import isEmpty from 'lodash.isempty';
|
||||
import pick from 'lodash.pick';
|
||||
import { useSetRecoilState } from 'recoil';
|
||||
import { updatedObjectSlugState } from '~/pages/settings/data-model/states/updatedObjectSlugState';
|
||||
import { computeMetadataNameFromLabelOrThrow } from '~/pages/settings/data-model/utils/compute-metadata-name-from-label.utils';
|
||||
|
||||
const objectEditFormSchema = z
|
||||
.object({})
|
||||
.merge(settingsDataModelObjectAboutFormSchema)
|
||||
.merge(settingsDataModelObjectIdentifiersFormSchema);
|
||||
|
||||
type SettingsDataModelObjectEditFormValues = z.infer<
|
||||
typeof objectEditFormSchema
|
||||
>;
|
||||
|
||||
type ObjectSettingsProps = {
|
||||
objectMetadataItem: ObjectMetadataItem;
|
||||
};
|
||||
|
||||
const StyledContentContainer = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: ${({ theme }) => theme.spacing(8)};
|
||||
`;
|
||||
|
||||
const StyledFormSection = styled(Section)`
|
||||
padding-left: 0 !important;
|
||||
`;
|
||||
|
||||
export const ObjectSettings = ({ objectMetadataItem }: ObjectSettingsProps) => {
|
||||
const navigate = useNavigate();
|
||||
const { enqueueSnackBar } = useSnackBar();
|
||||
const setUpdatedObjectSlugState = useSetRecoilState(updatedObjectSlugState);
|
||||
|
||||
const { updateOneObjectMetadataItem } = useUpdateOneObjectMetadataItem();
|
||||
const { lastVisitedObjectMetadataItemId } =
|
||||
useLastVisitedObjectMetadataItem();
|
||||
const { getLastVisitedViewIdFromObjectMetadataItemId } = useLastVisitedView();
|
||||
|
||||
const settingsObjectsPagePath = getSettingsPagePath(SettingsPath.Objects);
|
||||
|
||||
const formConfig = useForm<SettingsDataModelObjectEditFormValues>({
|
||||
mode: 'onTouched',
|
||||
resolver: zodResolver(objectEditFormSchema),
|
||||
});
|
||||
|
||||
const setNavigationMemorizedUrl = useSetRecoilState(
|
||||
navigationMemorizedUrlState,
|
||||
);
|
||||
|
||||
const getUpdatePayload = (
|
||||
formValues: SettingsDataModelObjectEditFormValues,
|
||||
) => {
|
||||
let values = formValues;
|
||||
const dirtyFieldKeys = Object.keys(
|
||||
formConfig.formState.dirtyFields,
|
||||
) as (keyof SettingsDataModelObjectEditFormValues)[];
|
||||
const shouldComputeNamesFromLabels: boolean = dirtyFieldKeys.includes(
|
||||
IS_LABEL_SYNCED_WITH_NAME_LABEL,
|
||||
)
|
||||
? (formValues.isLabelSyncedWithName as boolean)
|
||||
: objectMetadataItem.isLabelSyncedWithName;
|
||||
|
||||
if (shouldComputeNamesFromLabels) {
|
||||
values = {
|
||||
...values,
|
||||
...(values.labelSingular && dirtyFieldKeys.includes('labelSingular')
|
||||
? {
|
||||
nameSingular: computeMetadataNameFromLabelOrThrow(
|
||||
formValues.labelSingular,
|
||||
),
|
||||
}
|
||||
: {}),
|
||||
...(values.labelPlural && dirtyFieldKeys.includes('labelPlural')
|
||||
? {
|
||||
namePlural: computeMetadataNameFromLabelOrThrow(
|
||||
formValues.labelPlural,
|
||||
),
|
||||
}
|
||||
: {}),
|
||||
};
|
||||
}
|
||||
|
||||
return settingsUpdateObjectInputSchema.parse(
|
||||
pick(values, [
|
||||
...dirtyFieldKeys,
|
||||
...(shouldComputeNamesFromLabels &&
|
||||
dirtyFieldKeys.includes('labelPlural')
|
||||
? ['namePlural']
|
||||
: []),
|
||||
...(shouldComputeNamesFromLabels &&
|
||||
dirtyFieldKeys.includes('labelSingular')
|
||||
? ['nameSingular']
|
||||
: []),
|
||||
]),
|
||||
);
|
||||
};
|
||||
|
||||
const handleSave = async (
|
||||
formValues: SettingsDataModelObjectEditFormValues,
|
||||
) => {
|
||||
if (isEmpty(formConfig.formState.dirtyFields) === true) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const updatePayload = getUpdatePayload(formValues);
|
||||
const objectNamePluralForRedirection =
|
||||
updatePayload.namePlural ?? objectMetadataItem.namePlural;
|
||||
const objectSlug = getObjectSlug({
|
||||
...updatePayload,
|
||||
namePlural: objectNamePluralForRedirection,
|
||||
});
|
||||
|
||||
setUpdatedObjectSlugState(objectSlug);
|
||||
|
||||
await updateOneObjectMetadataItem({
|
||||
idToUpdate: objectMetadataItem.id,
|
||||
updatePayload,
|
||||
});
|
||||
|
||||
formConfig.reset(undefined, { keepValues: true });
|
||||
|
||||
if (lastVisitedObjectMetadataItemId === objectMetadataItem.id) {
|
||||
const lastVisitedView = getLastVisitedViewIdFromObjectMetadataItemId(
|
||||
objectMetadataItem.id,
|
||||
);
|
||||
setNavigationMemorizedUrl(
|
||||
`/objects/${objectNamePluralForRedirection}?view=${lastVisitedView}`,
|
||||
);
|
||||
}
|
||||
|
||||
navigate(`${settingsObjectsPagePath}/${objectSlug}`);
|
||||
} catch (error) {
|
||||
if (error instanceof ZodError) {
|
||||
enqueueSnackBar(error.issues[0].message, {
|
||||
variant: SnackBarVariant.Error,
|
||||
});
|
||||
} else {
|
||||
enqueueSnackBar((error as Error).message, {
|
||||
variant: SnackBarVariant.Error,
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleDisable = async () => {
|
||||
await updateOneObjectMetadataItem({
|
||||
idToUpdate: objectMetadataItem.id,
|
||||
updatePayload: { isActive: false },
|
||||
});
|
||||
navigate(settingsObjectsPagePath);
|
||||
};
|
||||
|
||||
return (
|
||||
<RecordFieldValueSelectorContextProvider>
|
||||
<FormProvider {...formConfig}>
|
||||
<StyledContentContainer>
|
||||
<StyledFormSection>
|
||||
<H2Title
|
||||
title="About"
|
||||
description="Name in both singular (e.g., 'Invoice') and plural (e.g., 'Invoices') forms."
|
||||
/>
|
||||
<SettingsDataModelObjectAboutForm
|
||||
disabled={!objectMetadataItem.isCustom}
|
||||
disableNameEdit={!objectMetadataItem.isCustom}
|
||||
objectMetadataItem={objectMetadataItem}
|
||||
onBlur={() => {
|
||||
formConfig.handleSubmit(handleSave)();
|
||||
}}
|
||||
/>
|
||||
</StyledFormSection>
|
||||
<StyledFormSection>
|
||||
<Section>
|
||||
<H2Title
|
||||
title="Options"
|
||||
description="Choose the fields that will identify your records"
|
||||
/>
|
||||
<SettingsDataModelObjectSettingsFormCard
|
||||
objectMetadataItem={objectMetadataItem}
|
||||
/>
|
||||
</Section>
|
||||
</StyledFormSection>
|
||||
<StyledFormSection>
|
||||
<Section>
|
||||
<H2Title title="Danger zone" description="Deactivate object" />
|
||||
<Button
|
||||
Icon={IconArchive}
|
||||
title="Deactivate"
|
||||
size="small"
|
||||
onClick={handleDisable}
|
||||
/>
|
||||
</Section>
|
||||
</StyledFormSection>
|
||||
</StyledContentContainer>
|
||||
</FormProvider>
|
||||
</RecordFieldValueSelectorContextProvider>
|
||||
);
|
||||
};
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@ -11,7 +11,6 @@ export enum SettingsPath {
|
||||
Objects = 'objects',
|
||||
ObjectOverview = 'objects/overview',
|
||||
ObjectDetail = 'objects/:objectSlug',
|
||||
ObjectEdit = 'objects/:objectSlug/edit',
|
||||
ObjectNewFieldSelect = 'objects/:objectSlug/new-field/select',
|
||||
ObjectNewFieldConfigure = 'objects/:objectSlug/new-field/configure',
|
||||
ObjectFieldEdit = 'objects/:objectSlug/:fieldSlug',
|
||||
|
||||
@ -17,6 +17,7 @@ export type TextAreaProps = {
|
||||
placeholder?: string;
|
||||
value?: string;
|
||||
className?: string;
|
||||
onBlur?: () => void;
|
||||
};
|
||||
|
||||
const StyledContainer = styled.div`
|
||||
@ -70,6 +71,7 @@ export const TextArea = ({
|
||||
value = '',
|
||||
className,
|
||||
onChange,
|
||||
onBlur,
|
||||
}: TextAreaProps) => {
|
||||
const computedMinRows = Math.min(minRows, MAX_ROWS);
|
||||
|
||||
@ -86,6 +88,7 @@ export const TextArea = ({
|
||||
|
||||
const handleBlur: FocusEventHandler<HTMLTextAreaElement> = () => {
|
||||
goBackToPreviousHotkeyScope();
|
||||
onBlur?.();
|
||||
};
|
||||
|
||||
return (
|
||||
|
||||
@ -99,7 +99,7 @@ const StyledTrailingIconContainer = styled.div<
|
||||
align-items: center;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
padding-right: ${({ theme }) => theme.spacing(1)};
|
||||
padding-right: ${({ theme }) => theme.spacing(2)};
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
|
||||
@ -10,7 +10,7 @@ import { PageHeader } from './PageHeader';
|
||||
|
||||
type SubMenuTopBarContainerProps = {
|
||||
children: JSX.Element | JSX.Element[];
|
||||
title?: string;
|
||||
title?: string | JSX.Element;
|
||||
actionButton?: ReactNode;
|
||||
className?: string;
|
||||
links: BreadcrumbProps['links'];
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import { useTheme } from '@emotion/react';
|
||||
import styled from '@emotion/styled';
|
||||
import { ReactElement } from 'react';
|
||||
import { IconComponent, Pill } from 'twenty-ui';
|
||||
|
||||
type TabProps = {
|
||||
@ -10,7 +11,7 @@ type TabProps = {
|
||||
className?: string;
|
||||
onClick?: () => void;
|
||||
disabled?: boolean;
|
||||
pill?: string;
|
||||
pill?: string | ReactElement;
|
||||
};
|
||||
|
||||
const StyledTab = styled.div<{ active?: boolean; disabled?: boolean }>`
|
||||
@ -73,7 +74,7 @@ export const Tab = ({
|
||||
<StyledHover>
|
||||
{Icon && <Icon size={theme.icon.size.md} />}
|
||||
{title}
|
||||
{pill && <Pill label={pill} />}
|
||||
{pill && typeof pill === 'string' ? <Pill label={pill} /> : pill}
|
||||
</StyledHover>
|
||||
</StyledTab>
|
||||
);
|
||||
|
||||
@ -15,7 +15,7 @@ export type SingleTabProps = {
|
||||
id: string;
|
||||
hide?: boolean;
|
||||
disabled?: boolean;
|
||||
pill?: string;
|
||||
pill?: string | React.ReactElement;
|
||||
};
|
||||
|
||||
type TabListProps = {
|
||||
|
||||
Reference in New Issue
Block a user