b16ab1b7c9
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" />
330 lines
11 KiB
TypeScript
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>
|
|
);
|
|
};
|