Files
twenty/packages/twenty-front/src/modules/settings/data-model/object-details/components/SettingsObjectFieldItemTableRow.tsx
T
martmull b16ab1b7c9 1518 extensibility front add an application section in settings (#15056)
Protected by IS_APPLICATION_ENABLED featureFlag

Add `Application` section in settings

<img width="301" height="137" alt="image"
src="https://github.com/user-attachments/assets/ee53bdd2-36f6-45c6-8646-17b1e08abf00"
/>


A `settings/applications` route listing all installed applications

<img width="661" height="428" alt="image"
src="https://github.com/user-attachments/assets/69d534c4-4e9e-452a-a3d9-ded0223bb457"
/>

Introduce a new Tag for application managed items

<img width="885" height="759" alt="image"
src="https://github.com/user-attachments/assets/19767be5-61e5-4bd2-a51d-54ed9bfb1923"
/>



A `settings/applications/<application_id>` details setting page listing
all objects, serverlessFunctions and agents created by the application:

<img width="917" height="778" alt="image"
src="https://github.com/user-attachments/assets/7fc056a6-1d73-4242-b2eb-6f8955d8597d"
/>

A `settings/applications/<application_id>/<serverless_function_id>`

<img width="905" height="652" alt="image"
src="https://github.com/user-attachments/assets/56ca0021-26bf-42cb-9abf-34879f16050a"
/>

Add trigger tab in serverless function details (readonly for now)

<img width="899" height="724" alt="image"
src="https://github.com/user-attachments/assets/5eeefa35-f2a4-4fd8-a640-7b5c5891f226"
/>

Set object, serverless and agent setting detail pages readonly for
managed items
<img width="1075" height="859" alt="image"
src="https://github.com/user-attachments/assets/57c73d69-4980-47a2-b752-8dc5ab494530"
/>
<img width="648" height="582" alt="image"
src="https://github.com/user-attachments/assets/5ad5f3f7-3bc3-4e40-870a-4981c6492524"
/>
<img width="982" height="692" alt="image"
src="https://github.com/user-attachments/assets/7ad756c4-5d33-4a0a-9eb8-416c040362b9"
/>
<img width="1077" height="647" alt="image"
src="https://github.com/user-attachments/assets/e086b9f5-4062-4d10-82a9-4023de3cad3f"
/>
2025-10-15 17:13:29 +02:00

330 lines
11 KiB
TypeScript

import { useDeleteOneFieldMetadataItem } from '@/object-metadata/hooks/useDeleteOneFieldMetadataItem';
import { useFieldMetadataItem } from '@/object-metadata/hooks/useFieldMetadataItem';
import { useGetRelationMetadata } from '@/object-metadata/hooks/useGetRelationMetadata';
import { useUpdateOneObjectMetadataItem } from '@/object-metadata/hooks/useUpdateOneObjectMetadataItem';
import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
import { isLabelIdentifierField } from '@/object-metadata/utils/isLabelIdentifierField';
import { SettingsObjectFieldActiveActionDropdown } from '@/settings/data-model/object-details/components/SettingsObjectFieldActiveActionDropdown';
import { SettingsObjectFieldInactiveActionDropdown } from '@/settings/data-model/object-details/components/SettingsObjectFieldDisabledActionDropdown';
import { settingsObjectFieldsFamilyState } from '@/settings/data-model/object-details/states/settingsObjectFieldsFamilyState';
import { isFieldTypeSupportedInSettings } from '@/settings/data-model/utils/isFieldTypeSupportedInSettings';
import { TableCell } from '@/ui/layout/table/components/TableCell';
import { TableRow } from '@/ui/layout/table/components/TableRow';
import { navigationMemorizedUrlState } from '@/ui/navigation/states/navigationMemorizedUrlState';
import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import { useMemo } from 'react';
import { useRecoilState } from 'recoil';
import { SettingsPath } from 'twenty-shared/types';
import {
getSettingsPath,
isDefined,
isLabelIdentifierFieldMetadataTypes,
} from 'twenty-shared/utils';
import { IconMinus, IconPlus, useIcons } from 'twenty-ui/display';
import { LightIconButton } from 'twenty-ui/input';
import { UndecoratedLink } from 'twenty-ui/navigation';
import { RelationType } from '~/generated-metadata/graphql';
import { useNavigateSettings } from '~/hooks/useNavigateSettings';
import { type SettingsObjectDetailTableItem } from '~/pages/settings/data-model/types/SettingsObjectDetailTableItem';
import { RELATION_TYPES } from '../../constants/RelationTypes';
import { SettingsObjectFieldDataType } from './SettingsObjectFieldDataType';
import { isObjectMetadataReadOnly } from '@/object-record/read-only/utils/isObjectMetadataReadOnly';
type SettingsObjectFieldItemTableRowProps = {
settingsObjectDetailTableItem: SettingsObjectDetailTableItem;
status: 'active' | 'disabled';
mode: 'view' | 'new-field';
};
export const StyledObjectFieldTableRow = styled(TableRow)`
grid-auto-columns: 180px 148px 148px 36px;
`;
const StyledNameTableCell = styled(TableCell)`
color: ${({ theme }) => theme.font.color.primary};
gap: ${({ theme }) => theme.spacing(2)};
`;
const StyledNameLabel = styled.div`
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
`;
const StyledIconTableCell = styled(TableCell)`
justify-content: center;
padding-right: ${({ theme }) => theme.spacing(1)};
`;
export const SettingsObjectFieldItemTableRow = ({
settingsObjectDetailTableItem,
mode,
status,
}: SettingsObjectFieldItemTableRowProps) => {
const { fieldMetadataItem, identifierType, objectMetadataItem } =
settingsObjectDetailTableItem;
const readonly = isObjectMetadataReadOnly({
objectMetadataItem,
});
const isRemoteObjectField = objectMetadataItem.isRemote;
const variant = objectMetadataItem.isCustom ? 'identifier' : 'field-type';
const navigate = useNavigateSettings();
const [navigationMemorizedUrl, setNavigationMemorizedUrl] = useRecoilState(
navigationMemorizedUrlState,
);
const theme = useTheme();
const { getIcon } = useIcons();
const Icon = getIcon(fieldMetadataItem.icon);
const getRelationMetadata = useGetRelationMetadata();
const { relationObjectMetadataItem, relationType } =
useMemo(
() => getRelationMetadata({ fieldMetadataItem }),
[fieldMetadataItem, getRelationMetadata],
) ?? {};
const fieldType = fieldMetadataItem.type;
const isFieldTypeSupported = isFieldTypeSupportedInSettings(fieldType);
const RelationIcon = relationType
? RELATION_TYPES[relationType].Icon
: undefined;
const isLabelIdentifier = isLabelIdentifierField({
fieldMetadataItem,
objectMetadataItem,
});
const canToggleField = !isLabelIdentifier;
const canBeSetAsLabelIdentifier =
objectMetadataItem.isCustom &&
!isLabelIdentifier &&
isLabelIdentifierFieldMetadataTypes(fieldMetadataItem.type);
const linkToNavigate = getSettingsPath(SettingsPath.ObjectFieldEdit, {
objectNamePlural: objectMetadataItem.namePlural,
fieldName: fieldMetadataItem.name,
});
const { activateMetadataField, deactivateMetadataField } =
useFieldMetadataItem();
const { deleteOneFieldMetadataItem } = useDeleteOneFieldMetadataItem();
const handleDisableField = async (
activeFieldMetadatItem: FieldMetadataItem,
) => {
if (readonly) {
return;
}
await deactivateMetadataField(
activeFieldMetadatItem.id,
objectMetadataItem.id,
);
// TODO: Add optimistic rendering for core views
const deletedViewIds: string[] = [];
const [baseUrl, queryParams] = navigationMemorizedUrl.includes('?')
? navigationMemorizedUrl.split('?')
: [navigationMemorizedUrl, ''];
const params = new URLSearchParams(queryParams);
const currentViewId = params.get('view');
if (isDefined(currentViewId) && deletedViewIds.includes(currentViewId)) {
params.delete('view');
const updatedUrl = params.toString()
? `${baseUrl}?${params.toString()}`
: baseUrl;
setNavigationMemorizedUrl(updatedUrl);
}
};
const { updateOneObjectMetadataItem } = useUpdateOneObjectMetadataItem();
const handleSetLabelIdentifierField = (
activeFieldMetadatItem: FieldMetadataItem,
) => {
if (readonly) {
return;
}
updateOneObjectMetadataItem({
idToUpdate: objectMetadataItem.id,
updatePayload: {
labelIdentifierFieldMetadataId: activeFieldMetadatItem.id,
},
});
};
const [, setActiveSettingsObjectFields] = useRecoilState(
settingsObjectFieldsFamilyState({
objectMetadataItemId: objectMetadataItem.id,
}),
);
const handleToggleField = () => {
setActiveSettingsObjectFields((previousFields) => {
const newFields = isDefined(previousFields)
? previousFields?.map((field) =>
field.id === fieldMetadataItem.id
? { ...field, isActive: !field.isActive }
: field,
)
: null;
return newFields;
});
};
const typeLabel =
variant === 'field-type'
? isRemoteObjectField
? 'Remote'
: fieldMetadataItem.isCustom
? 'Custom'
: 'Standard'
: variant === 'identifier'
? isDefined(identifierType)
? identifierType === 'label'
? 'Record text'
: 'Record image'
: ''
: '';
if (!isFieldTypeSupported) return null;
const isRelatedObjectLinkable =
isDefined(relationObjectMetadataItem?.namePlural) &&
!relationObjectMetadataItem.isSystem;
return (
<StyledObjectFieldTableRow
onClick={
mode === 'view'
? () =>
navigate(SettingsPath.ObjectFieldEdit, {
objectNamePlural: objectMetadataItem.namePlural,
fieldName: fieldMetadataItem.name,
})
: undefined
}
>
<UndecoratedLink to={linkToNavigate}>
<StyledNameTableCell>
{!!Icon && (
<Icon
style={{ minWidth: theme.icon.size.md }}
size={theme.icon.size.md}
stroke={theme.icon.stroke.sm}
/>
)}
<StyledNameLabel title={fieldMetadataItem.label}>
{fieldMetadataItem.label}
</StyledNameLabel>
</StyledNameTableCell>
</UndecoratedLink>
<TableCell>{typeLabel}</TableCell>
<TableCell>
<SettingsObjectFieldDataType
Icon={RelationIcon}
label={
relationType === RelationType.MANY_TO_ONE
? relationObjectMetadataItem?.labelSingular
: relationObjectMetadataItem?.labelPlural
}
labelDetail={
fieldMetadataItem.settings?.type === 'percentage' ? '%' : undefined
}
to={
isRelatedObjectLinkable
? getSettingsPath(SettingsPath.Objects, {
objectNamePlural: relationObjectMetadataItem.namePlural,
})
: undefined
}
value={fieldType}
onClick={(e) => {
if (isRelatedObjectLinkable) {
e.stopPropagation();
}
}}
/>
</TableCell>
<StyledIconTableCell>
{status === 'active' ? (
mode === 'view' ? (
<SettingsObjectFieldActiveActionDropdown
isCustomField={fieldMetadataItem.isCustom === true}
readonly={readonly}
fieldMetadataItemId={fieldMetadataItem.id}
onEdit={() =>
navigate(SettingsPath.ObjectFieldEdit, {
objectNamePlural: objectMetadataItem.namePlural,
fieldName: fieldMetadataItem.name,
})
}
onSetAsLabelIdentifier={
canBeSetAsLabelIdentifier
? () => handleSetLabelIdentifierField(fieldMetadataItem)
: undefined
}
onDeactivate={
isLabelIdentifier
? undefined
: () => handleDisableField(fieldMetadataItem)
}
/>
) : (
canToggleField && (
<LightIconButton
Icon={IconMinus}
accent="tertiary"
onClick={handleToggleField}
/>
)
)
) : mode === 'view' ? (
<SettingsObjectFieldInactiveActionDropdown
isCustomField={fieldMetadataItem.isCustom === true}
readonly={readonly}
fieldMetadataItemId={fieldMetadataItem.id}
onEdit={() =>
navigate(SettingsPath.ObjectFieldEdit, {
objectNamePlural: objectMetadataItem.namePlural,
fieldName: fieldMetadataItem.name,
})
}
onActivate={() =>
activateMetadataField(fieldMetadataItem.id, objectMetadataItem.id)
}
onDelete={() =>
deleteOneFieldMetadataItem({
idToDelete: fieldMetadataItem.id,
objectMetadataId: objectMetadataItem.id,
})
}
/>
) : (
<LightIconButton
Icon={IconPlus}
accent="tertiary"
onClick={handleToggleField}
/>
)}
</StyledIconTableCell>
</StyledObjectFieldTableRow>
);
};