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"
/>
This commit is contained in:
martmull
2025-10-15 17:13:29 +02:00
committed by GitHub
parent c0ed246a03
commit b16ab1b7c9
109 changed files with 2464 additions and 1357 deletions
@@ -9,6 +9,7 @@ type SettingsDataModelPreviewFormCardProps = {
className?: string;
preview: ReactNode;
form?: ReactNode;
disabled?: boolean;
};
const StyledPreviewContainer = styled(CardContent)`
@@ -18,9 +18,11 @@ export type SettingsDataModelFieldBooleanFormValues = z.infer<
type SettingsDataModelFieldBooleanFormProps = {
existingFieldMetadataId: string;
disabled?: boolean;
};
export const SettingsDataModelFieldBooleanForm = ({
disabled,
existingFieldMetadataId,
}: SettingsDataModelFieldBooleanFormProps) => {
const { t } = useLingui();
@@ -46,6 +48,7 @@ export const SettingsDataModelFieldBooleanForm = ({
onChange={onChange}
dropdownId="object-field-default-value-select-boolean"
dropdownWidth={120}
disabled={disabled}
needIconCheck={false}
options={BOOLEAN_DATA_MODEL_SELECT_OPTIONS.map((option) => ({
...option,
@@ -12,11 +12,13 @@ import { type SettingsDataModelFieldEditFormValues } from '~/pages/settings/data
type SettingsDataModelFieldBooleanSettingsFormCardProps = {
existingFieldMetadataId: string;
objectNameSingular: string;
disabled?: boolean;
};
export const SettingsDataModelFieldBooleanSettingsFormCard = ({
existingFieldMetadataId,
objectNameSingular,
disabled = false,
}: SettingsDataModelFieldBooleanSettingsFormCardProps) => {
const { watch } = useFormContext<
SettingsDataModelFieldBooleanFormValues &
@@ -38,6 +40,7 @@ export const SettingsDataModelFieldBooleanSettingsFormCard = ({
}
form={
<SettingsDataModelFieldBooleanForm
disabled={disabled}
existingFieldMetadataId={existingFieldMetadataId}
/>
}
@@ -76,12 +76,14 @@ type SettingsDataModelFieldIconLabelFormProps = {
fieldMetadataItem?: FieldMetadataItem;
maxLength?: number;
isCreationMode?: boolean;
readonly?: boolean;
};
export const SettingsDataModelFieldIconLabelForm = ({
isCreationMode = false,
fieldMetadataItem,
maxLength,
readonly = false,
}: SettingsDataModelFieldIconLabelFormProps) => {
const {
control,
@@ -150,6 +152,7 @@ export const SettingsDataModelFieldIconLabelForm = ({
selectedIconKey={value ?? 'IconUsers'}
onChange={({ iconKey }) => onChange(iconKey)}
variant="primary"
disabled={readonly}
/>
)}
/>
@@ -162,7 +165,7 @@ export const SettingsDataModelFieldIconLabelForm = ({
instanceId={labelTextInputId}
placeholder={t`Employees`}
value={value}
disabled={!isLabelEditEnabled}
disabled={!isLabelEditEnabled || readonly}
onChange={(value) => {
onChange(value);
trigger('label');
@@ -199,6 +202,7 @@ export const SettingsDataModelFieldIconLabelForm = ({
placeholder={t`employees`}
value={value}
onChange={onChange}
readOnly={readonly}
disabled={!isNameEditEnabled}
fullWidth
maxLength={DATABASE_IDENTIFIER_MAXIMUM_LENGTH}
@@ -245,6 +249,7 @@ export const SettingsDataModelFieldIconLabelForm = ({
title={t`Synchronize Field Label and API Name`}
description={t`Should changing a field's label also change the API name?`}
checked={value ?? true}
disabled={readonly}
advancedMode
onChange={(value) => {
onChange(value);
@@ -18,12 +18,14 @@ type SettingsDataModelFieldIsUniqueFormProps = {
objectNameSingular: string;
fieldType: FieldMetadataType;
existingFieldMetadataId: string;
disabled?: boolean;
};
export const SettingsDataModelFieldIsUniqueForm = ({
fieldType,
existingFieldMetadataId,
objectNameSingular,
disabled = false,
}: SettingsDataModelFieldIsUniqueFormProps) => {
const { control } =
useFormContext<SettingsDataModelFieldIsUniqueFormValues>();
@@ -74,7 +76,7 @@ export const SettingsDataModelFieldIsUniqueForm = ({
toggleSize="small"
value={isUnique}
onChange={(value) => onChange(value)}
disabled={hasStandardUniqueIndex}
disabled={disabled || hasStandardUniqueIndex}
/>
</SettingsOptionCardContentSelect>
</>
@@ -160,6 +160,7 @@ type SettingsDataModelFieldSettingsFormCardProps = {
existingFieldMetadataId: string;
fieldType: FieldMetadataType;
objectNameSingular: string;
disabled?: boolean;
};
const previewableTypes = [
@@ -188,6 +189,7 @@ export const SettingsDataModelFieldSettingsFormCard = ({
existingFieldMetadataId,
fieldType,
objectNameSingular,
disabled = false,
}: SettingsDataModelFieldSettingsFormCardProps) => {
const { watch } = useFormContext<SettingsDataModelFieldEditFormValues>();
@@ -200,6 +202,7 @@ export const SettingsDataModelFieldSettingsFormCard = ({
<SettingsDataModelFieldBooleanSettingsFormCard
existingFieldMetadataId={existingFieldMetadataId}
objectNameSingular={objectNameSingular}
disabled={disabled}
/>
);
}
@@ -209,6 +212,7 @@ export const SettingsDataModelFieldSettingsFormCard = ({
<SettingsDataModelFieldCurrencySettingsFormCard
existingFieldMetadataId={existingFieldMetadataId}
objectNameSingular={objectNameSingular}
disabled={disabled}
/>
);
}
@@ -222,6 +226,7 @@ export const SettingsDataModelFieldSettingsFormCard = ({
existingFieldMetadataId={existingFieldMetadataId}
fieldType={fieldType}
objectNameSingular={objectNameSingular}
disabled={disabled}
/>
);
}
@@ -231,6 +236,7 @@ export const SettingsDataModelFieldSettingsFormCard = ({
<SettingsDataModelFieldRelationSettingsFormCard
existingFieldMetadataId={existingFieldMetadataId}
objectNameSingular={objectNameSingular}
disabled={disabled}
/>
);
}
@@ -240,6 +246,7 @@ export const SettingsDataModelFieldSettingsFormCard = ({
<SettingsDataModelFieldMorphRelationFormCard
existingFieldMetadataId={existingFieldMetadataId}
objectNameSingular={objectNameSingular}
disabled={disabled}
/>
);
}
@@ -249,6 +256,7 @@ export const SettingsDataModelFieldSettingsFormCard = ({
<SettingsDataModelFieldNumberSettingsFormCard
existingFieldMetadataId={existingFieldMetadataId}
objectNameSingular={objectNameSingular}
disabled={disabled}
/>
);
}
@@ -258,6 +266,7 @@ export const SettingsDataModelFieldSettingsFormCard = ({
<SettingsDataModelFieldTextSettingsFormCard
existingFieldMetadataId={existingFieldMetadataId}
objectNameSingular={objectNameSingular}
disabled={disabled}
/>
);
}
@@ -267,6 +276,7 @@ export const SettingsDataModelFieldSettingsFormCard = ({
<SettingsDataModelFieldAddressSettingsFormCard
existingFieldMetadataId={existingFieldMetadataId}
objectNameSingular={objectNameSingular}
disabled={disabled}
/>
);
}
@@ -276,6 +286,7 @@ export const SettingsDataModelFieldSettingsFormCard = ({
<SettingsDataModelFieldPhonesSettingsFormCard
existingFieldMetadataId={existingFieldMetadataId}
objectNameSingular={objectNameSingular}
disabled={disabled}
/>
);
}
@@ -289,6 +300,7 @@ export const SettingsDataModelFieldSettingsFormCard = ({
existingFieldMetadataId={existingFieldMetadataId}
fieldType={fieldType}
objectNameSingular={objectNameSingular}
disabled={disabled}
/>
);
}
@@ -318,6 +330,7 @@ export const SettingsDataModelFieldSettingsFormCard = ({
<SettingsDataModelFieldMaxValuesForm
existingFieldMetadataId={existingFieldMetadataId}
fieldType={fieldType}
disabled={disabled}
/>
<Separator />
</>
@@ -326,6 +339,7 @@ export const SettingsDataModelFieldSettingsFormCard = ({
fieldType={fieldType}
existingFieldMetadataId={existingFieldMetadataId}
objectNameSingular={objectNameSingular}
disabled={disabled}
/>
</>
}
@@ -42,6 +42,7 @@ export const SettingsDataModelFieldTextSettingsFormCard = ({
fieldType={FieldMetadataType.TEXT}
existingFieldMetadataId={existingFieldMetadataId}
objectNameSingular={objectNameSingular}
disabled={disabled}
/>
</>
}
@@ -55,6 +55,7 @@ export const SettingsDataModelFieldDateSettingsFormCard = ({
fieldType={fieldType}
existingFieldMetadataId={existingFieldMetadataId}
objectNameSingular={objectNameSingular}
disabled={disabled}
/>
</>
}
@@ -39,10 +39,12 @@ export type SettingsDataModelFieldMorphRelationFormValues = z.infer<
type SettingsDataModelFieldMorphRelationFormProps = {
existingFieldMetadataId: string;
disabled?: boolean;
};
export const SettingsDataModelFieldMorphRelationForm = ({
existingFieldMetadataId,
disabled = false,
}: SettingsDataModelFieldMorphRelationFormProps) => {
const { t } = useLingui();
const { control } =
@@ -90,7 +92,7 @@ export const SettingsDataModelFieldMorphRelationForm = ({
label={t`Relation type`}
dropdownId="relation-type-select"
fullWidth
disabled={disableRelationEdition}
disabled={disabled || disableRelationEdition}
value={value}
options={RELATION_TYPE_OPTIONS}
onChange={onChange}
@@ -18,11 +18,13 @@ import { type SettingsDataModelFieldEditFormValues } from '~/pages/settings/data
type SettingsDataModelFieldMorphRelationFormCardProps = {
existingFieldMetadataId: string;
objectNameSingular: string;
disabled?: boolean;
};
export const SettingsDataModelFieldMorphRelationFormCard = ({
existingFieldMetadataId,
objectNameSingular,
disabled = false,
}: SettingsDataModelFieldMorphRelationFormCardProps) => {
const { watch } = useFormContext<
SettingsDataModelFieldMorphRelationFormValues &
@@ -107,6 +109,7 @@ export const SettingsDataModelFieldMorphRelationFormCard = ({
form={
<SettingsDataModelFieldMorphRelationForm
existingFieldMetadataId={existingFieldMetadataId}
disabled={disabled}
/>
}
/>
@@ -42,6 +42,7 @@ export const SettingsDataModelFieldNumberSettingsFormCard = ({
fieldType={FieldMetadataType.NUMBER}
existingFieldMetadataId={existingFieldMetadataId}
objectNameSingular={objectNameSingular}
disabled={disabled}
/>
</>
}
@@ -58,6 +58,7 @@ export const SettingsDataModelFieldPhonesSettingsFormCard = ({
fieldType={FieldMetadataType.PHONES}
existingFieldMetadataId={existingFieldMetadataId}
objectNameSingular={objectNameSingular}
disabled={disabled}
/>
</>
}
@@ -47,6 +47,7 @@ export type SettingsDataModelFieldRelationFormValues = z.infer<
type SettingsDataModelFieldRelationFormProps = {
existingFieldMetadataId: string;
objectMetadataItem?: ObjectMetadataItem;
disabled?: boolean;
};
export const StyledContainer = styled.div`
@@ -84,6 +85,7 @@ export const RELATION_TYPE_OPTIONS = Object.entries(RELATION_TYPES).map(
export const SettingsDataModelFieldRelationForm = ({
existingFieldMetadataId,
objectMetadataItem,
disabled,
}: SettingsDataModelFieldRelationFormProps) => {
const { t } = useLingui();
const { control, watch: watchFormValue } =
@@ -129,7 +131,7 @@ export const SettingsDataModelFieldRelationForm = ({
label={t`Relation type`}
dropdownId="relation-type-select"
fullWidth
disabled={disableRelationEdition}
disabled={disabled || disableRelationEdition}
value={value}
options={RELATION_TYPE_OPTIONS}
onChange={onChange}
@@ -145,7 +147,7 @@ export const SettingsDataModelFieldRelationForm = ({
label={t`Object destination`}
dropdownId="object-destination-select"
fullWidth
disabled={disableRelationEdition}
disabled={disabled || disableRelationEdition}
value={value}
options={activeObjectMetadataItems
.filter(isObjectMetadataAvailableForRelation)
@@ -175,7 +177,7 @@ export const SettingsDataModelFieldRelationForm = ({
defaultValue={initialRelationFieldMetadataItem.icon}
render={({ field: { onChange, value } }) => (
<IconPicker
disabled={disableFieldEdition}
disabled={disabled || disableFieldEdition}
dropdownId="field-destination-icon-picker"
selectedIconKey={value ?? undefined}
onChange={({ iconKey }) => onChange(iconKey)}
@@ -190,7 +192,7 @@ export const SettingsDataModelFieldRelationForm = ({
render={({ field: { onChange, value } }) => (
<SettingsTextInput
instanceId="relation-field-label"
disabled={disableFieldEdition}
disabled={disabled || disableFieldEdition}
placeholder={t`Field name`}
value={value}
onChange={onChange}
@@ -18,11 +18,13 @@ import { type SettingsDataModelFieldEditFormValues } from '~/pages/settings/data
type SettingsDataModelFieldRelationSettingsFormCardProps = {
existingFieldMetadataId: string;
objectNameSingular: string;
disabled?: boolean;
};
export const SettingsDataModelFieldRelationSettingsFormCard = ({
existingFieldMetadataId,
objectNameSingular,
disabled = false,
}: SettingsDataModelFieldRelationSettingsFormCardProps) => {
const { watch } = useFormContext<
SettingsDataModelFieldRelationFormValues &
@@ -95,6 +97,7 @@ export const SettingsDataModelFieldRelationSettingsFormCard = ({
<SettingsDataModelFieldRelationForm
existingFieldMetadataId={existingFieldMetadataId}
objectMetadataItem={relationObjectMetadataItem}
disabled={disabled}
/>
}
/>
@@ -48,6 +48,7 @@ export type SettingsDataModelFieldSelectFormValues = z.infer<
type SettingsDataModelFieldSelectFormProps = {
fieldType: FieldMetadataType.SELECT | FieldMetadataType.MULTI_SELECT;
existingFieldMetadataId: string;
disabled?: boolean;
};
const StyledContainer = styled(CardContent)`
@@ -112,6 +113,7 @@ const StyledButton = styled(LightButton)`
export const SettingsDataModelFieldSelectForm = ({
existingFieldMetadataId,
fieldType,
disabled = false,
}: SettingsDataModelFieldSelectFormProps) => {
const { initialDefaultValue, initialOptions } =
useSelectSettingsFormInitialValues({
@@ -281,7 +283,11 @@ export const SettingsDataModelFieldSelectForm = ({
</StyledOptionsLabel>
</StyledLabelContainer>
<DraggableList
onDragEnd={(result) => handleDragEnd(options, result, onChange)}
onDragEnd={(result) =>
!disabled
? handleDragEnd(options, result, onChange)
: undefined
}
draggableItems={
<>
{options.map((option, index) => (
@@ -297,6 +303,9 @@ export const SettingsDataModelFieldSelectForm = ({
option={option}
isNewRow={index === options.length - 1}
onChange={(nextOption) => {
if (disabled) {
return;
}
const nextOptions = toSpliced(
options,
index,
@@ -315,6 +324,9 @@ export const SettingsDataModelFieldSelectForm = ({
}
}}
onRemove={() => {
if (disabled) {
return;
}
const nextOptions = toSpliced(
options,
index,
@@ -326,13 +338,24 @@ export const SettingsDataModelFieldSelectForm = ({
onChange(nextOptions);
}}
isDefault={isOptionDefaultValue(option.value)}
onSetAsDefault={() =>
handleSetOptionAsDefault(option.value)
}
onRemoveAsDefault={() =>
handleRemoveOptionAsDefault(option.value)
}
onInputEnter={handleInputEnter}
onSetAsDefault={() => {
if (disabled) {
return;
}
handleSetOptionAsDefault(option.value);
}}
onRemoveAsDefault={() => {
if (disabled) {
return;
}
handleRemoveOptionAsDefault(option.value);
}}
onInputEnter={() => {
if (disabled) {
return;
}
handleInputEnter();
}}
/>
}
/>
@@ -341,13 +364,15 @@ export const SettingsDataModelFieldSelectForm = ({
}
/>
</StyledContainer>
<StyledFooter>
<StyledButton
title={t`Add option`}
Icon={IconPlus}
onClick={handleAddOption}
/>
</StyledFooter>
{!disabled && (
<StyledFooter>
<StyledButton
title={t`Add option`}
Icon={IconPlus}
onClick={handleAddOption}
/>
</StyledFooter>
)}
</>
)}
/>
@@ -20,12 +20,14 @@ type SettingsDataModelFieldSelectSettingsFormCardProps = {
objectNameSingular: string;
fieldType: FieldMetadataType.SELECT | FieldMetadataType.MULTI_SELECT;
existingFieldMetadataId: string;
disabled?: boolean;
};
export const SettingsDataModelFieldSelectSettingsFormCard = ({
objectNameSingular,
fieldType,
existingFieldMetadataId,
disabled = false,
}: SettingsDataModelFieldSelectSettingsFormCardProps) => {
const { watch: watchFormValue } = useFormContext<
SettingsDataModelFieldSelectOrMultiSelectFormValues &
@@ -50,6 +52,7 @@ export const SettingsDataModelFieldSelectSettingsFormCard = ({
<SettingsDataModelFieldSelectForm
fieldType={fieldType}
existingFieldMetadataId={existingFieldMetadataId}
disabled={disabled}
/>
}
/>
@@ -6,8 +6,6 @@ import { Link } from 'react-router-dom';
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords';
import { ObjectFieldRow } from '@/settings/data-model/graph-overview/components/SettingsDataModelOverviewField';
import { SettingsDataModelObjectTypeTag } from '@/settings/data-model/objects/components/SettingsDataModelObjectTypeTag';
import { getObjectTypeLabel } from '@/settings/data-model/utils/getObjectTypeLabel';
import { FieldMetadataType } from '~/generated-metadata/graphql';
import { ObjectFieldRowWithoutRelation } from '@/settings/data-model/graph-overview/components/SettingsDataModelOverviewFieldWithoutRelation';
@@ -16,6 +14,7 @@ import { useState } from 'react';
import { SettingsPath } from 'twenty-shared/types';
import { getSettingsPath } from 'twenty-shared/utils';
import { IconChevronDown, IconChevronUp, useIcons } from 'twenty-ui/display';
import { SettingsItemTypeTag } from '@/settings/components/SettingsItemTypeTag';
type SettingsDataModelOverviewObjectNode = Node<ObjectMetadataItem, 'object'>;
type SettingsDataModelOverviewObjectProps =
@@ -134,9 +133,7 @@ export const SettingsDataModelOverviewObject = ({
</StyledObjectLink>
<StyledObjectInstanceCount> · {totalCount}</StyledObjectInstanceCount>
</StyledObjectName>
<SettingsDataModelObjectTypeTag
objectTypeLabel={getObjectTypeLabel(objectMetadataItem)}
></SettingsDataModelObjectTypeTag>
<SettingsItemTypeTag item={objectMetadataItem} />
</StyledHeader>
<StyledInnerCard>
@@ -20,10 +20,12 @@ type SettingsObjectFieldActiveActionDropdownProps = {
onEdit: () => void;
onSetAsLabelIdentifier?: () => void;
fieldMetadataItemId: string;
readonly?: boolean;
};
export const SettingsObjectFieldActiveActionDropdown = ({
isCustomField,
readonly = false,
onDeactivate,
onEdit,
onSetAsLabelIdentifier,
@@ -62,18 +64,18 @@ export const SettingsObjectFieldActiveActionDropdown = ({
<DropdownContent widthInPixels={GenericDropdownContentWidth.Narrow}>
<DropdownMenuItemsContainer>
<MenuItem
text={isCustomField ? 'Edit' : 'View'}
text={isCustomField && !readonly ? 'Edit' : 'View'}
LeftIcon={isCustomField ? IconPencil : IconEye}
onClick={handleEdit}
/>
{isDefined(onSetAsLabelIdentifier) && (
{isDefined(onSetAsLabelIdentifier) && !readonly && (
<MenuItem
text="Set as record text"
LeftIcon={IconTextSize}
onClick={handleSetAsLabelIdentifier}
/>
)}
{isDefined(onDeactivate) && (
{isDefined(onDeactivate) && !readonly && (
<MenuItem
text="Deactivate"
LeftIcon={IconArchive}
@@ -22,10 +22,12 @@ type SettingsObjectFieldInactiveActionDropdownProps = {
onEdit: () => void;
onDelete: () => void;
fieldMetadataItemId: string;
readonly?: boolean;
};
export const SettingsObjectFieldInactiveActionDropdown = ({
onActivate,
readonly = false,
fieldMetadataItemId,
onDelete,
onEdit,
@@ -66,16 +68,18 @@ export const SettingsObjectFieldInactiveActionDropdown = ({
<DropdownContent widthInPixels={GenericDropdownContentWidth.Narrow}>
<DropdownMenuItemsContainer>
<MenuItem
text={isCustomField ? t`Edit` : t`View`}
text={isCustomField && !readonly ? t`Edit` : t`View`}
LeftIcon={isCustomField ? IconPencil : IconEye}
onClick={handleEdit}
/>
<MenuItem
text={t`Activate`}
LeftIcon={IconArchiveOff}
onClick={handleActivate}
/>
{isDeletable && (
{!readonly && (
<MenuItem
text={t`Activate`}
LeftIcon={IconArchiveOff}
onClick={handleActivate}
/>
)}
{isDeletable && !readonly && (
<MenuItem
text={t`Delete`}
accent="danger"
@@ -30,6 +30,7 @@ import { type SettingsObjectDetailTableItem } from '~/pages/settings/data-model/
import { RELATION_TYPES } from '../../constants/RelationTypes';
import { SettingsObjectFieldDataType } from './SettingsObjectFieldDataType';
import { isObjectMetadataReadOnly } from '@/object-record/read-only/utils/isObjectMetadataReadOnly';
type SettingsObjectFieldItemTableRowProps = {
settingsObjectDetailTableItem: SettingsObjectDetailTableItem;
@@ -65,6 +66,10 @@ export const SettingsObjectFieldItemTableRow = ({
const { fieldMetadataItem, identifierType, objectMetadataItem } =
settingsObjectDetailTableItem;
const readonly = isObjectMetadataReadOnly({
objectMetadataItem,
});
const isRemoteObjectField = objectMetadataItem.isRemote;
const variant = objectMetadataItem.isCustom ? 'identifier' : 'field-type';
@@ -118,6 +123,10 @@ export const SettingsObjectFieldItemTableRow = ({
const handleDisableField = async (
activeFieldMetadatItem: FieldMetadataItem,
) => {
if (readonly) {
return;
}
await deactivateMetadataField(
activeFieldMetadatItem.id,
objectMetadataItem.id,
@@ -146,13 +155,17 @@ export const SettingsObjectFieldItemTableRow = ({
const handleSetLabelIdentifierField = (
activeFieldMetadatItem: FieldMetadataItem,
) =>
) => {
if (readonly) {
return;
}
updateOneObjectMetadataItem({
idToUpdate: objectMetadataItem.id,
updatePayload: {
labelIdentifierFieldMetadataId: activeFieldMetadatItem.id,
},
});
};
const [, setActiveSettingsObjectFields] = useRecoilState(
settingsObjectFieldsFamilyState({
@@ -254,6 +267,7 @@ export const SettingsObjectFieldItemTableRow = ({
mode === 'view' ? (
<SettingsObjectFieldActiveActionDropdown
isCustomField={fieldMetadataItem.isCustom === true}
readonly={readonly}
fieldMetadataItemId={fieldMetadataItem.id}
onEdit={() =>
navigate(SettingsPath.ObjectFieldEdit, {
@@ -284,6 +298,7 @@ export const SettingsObjectFieldItemTableRow = ({
) : mode === 'view' ? (
<SettingsObjectFieldInactiveActionDropdown
isCustomField={fieldMetadataItem.isCustom === true}
readonly={readonly}
fieldMetadataItemId={fieldMetadataItem.id}
onEdit={() =>
navigate(SettingsPath.ObjectFieldEdit, {
@@ -3,11 +3,10 @@ import styled from '@emotion/styled';
import { type ReactNode } from 'react';
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { SettingsDataModelObjectTypeTag } from '@/settings/data-model/objects/components/SettingsDataModelObjectTypeTag';
import { getObjectTypeLabel } from '@/settings/data-model/utils/getObjectTypeLabel';
import { TableCell } from '@/ui/layout/table/components/TableCell';
import { TableRow } from '@/ui/layout/table/components/TableRow';
import { useIcons } from 'twenty-ui/display';
import { SettingsItemTypeTag } from '@/settings/components/SettingsItemTypeTag';
export type SettingsObjectMetadataItemTableRowProps = {
action: ReactNode;
@@ -46,7 +45,6 @@ export const SettingsObjectMetadataItemTableRow = ({
const { getIcon } = useIcons();
const Icon = getIcon(objectMetadataItem.icon);
const objectTypeLabel = getObjectTypeLabel(objectMetadataItem);
return (
<StyledObjectTableRow key={objectMetadataItem.namePlural} to={link}>
@@ -63,7 +61,7 @@ export const SettingsObjectMetadataItemTableRow = ({
</StyledNameLabel>
</StyledNameTableCell>
<TableCell>
<SettingsDataModelObjectTypeTag objectTypeLabel={objectTypeLabel} />
<SettingsItemTypeTag item={objectMetadataItem} />
</TableCell>
<TableCell align="right">
{objectMetadataItem.fields.filter((field) => !field.isSystem).length}
@@ -14,6 +14,7 @@ import { SettingsPath } from 'twenty-shared/types';
import { ZodError } from 'zod';
import { useNavigateSettings } from '~/hooks/useNavigateSettings';
import { updatedObjectNamePluralState } from '~/pages/settings/data-model/states/updatedObjectNamePluralState';
import { isObjectMetadataReadOnly } from '@/object-record/read-only/utils/isObjectMetadataReadOnly';
type SettingsUpdateDataModelObjectAboutFormProps = {
objectMetadataItem: ObjectMetadataItem;
@@ -22,6 +23,7 @@ type SettingsUpdateDataModelObjectAboutFormProps = {
export const SettingsUpdateDataModelObjectAboutForm = ({
objectMetadataItem,
}: SettingsUpdateDataModelObjectAboutFormProps) => {
const readonly = isObjectMetadataReadOnly({ objectMetadataItem });
const navigate = useNavigateSettings();
const { enqueueErrorSnackBar } = useSnackBar();
const setUpdatedObjectNamePlural = useSetRecoilState(
@@ -54,6 +56,10 @@ export const SettingsUpdateDataModelObjectAboutForm = ({
const handleSave = async (
formValues: SettingsDataModelObjectAboutFormValues,
) => {
if (readonly) {
return;
}
if (!(Object.keys(formConfig.formState.dirtyFields).length > 0)) {
return;
}
@@ -71,10 +77,10 @@ export const SettingsUpdateDataModelObjectAboutForm = ({
description,
icon: icon ?? undefined,
isLabelSyncedWithName: formValues.isLabelSyncedWithName,
labelPlural: updatedObject.data?.updateOneObject.labelPlural,
labelSingular: updatedObject.data?.updateOneObject.labelSingular,
namePlural: updatedObject.data?.updateOneObject.namePlural,
nameSingular: updatedObject.data?.updateOneObject.nameSingular,
labelPlural: updatedObject?.data?.updateOneObject.labelPlural,
labelSingular: updatedObject?.data?.updateOneObject.labelSingular,
namePlural: updatedObject?.data?.updateOneObject.namePlural,
nameSingular: updatedObject?.data?.updateOneObject.nameSingular,
});
} else {
formConfig.reset(undefined, { keepValues: true });
@@ -91,6 +97,10 @@ export const SettingsUpdateDataModelObjectAboutForm = ({
const updateObjectMetadata = async (
formValues: SettingsDataModelObjectAboutFormValues,
) => {
if (readonly) {
return;
}
const updatePayload = { ...formValues };
if (!objectMetadataItem.isCustom) {
@@ -139,7 +149,7 @@ export const SettingsUpdateDataModelObjectAboutForm = ({
<FormProvider {...formConfig}>
<SettingsDataModelObjectAboutForm
onNewDirtyField={() => formConfig.handleSubmit(handleSave)()}
disableEdition={!objectMetadataItem.isCustom}
disableEdition={!objectMetadataItem.isCustom || readonly}
objectMetadataItem={objectMetadataItem}
/>
</FormProvider>
@@ -9,6 +9,7 @@ import { H2Title, IconPlus } from 'twenty-ui/display';
import { Button } from 'twenty-ui/input';
import { Section } from 'twenty-ui/layout';
import { UndecoratedLink } from 'twenty-ui/navigation';
import { isObjectMetadataReadOnly } from '@/object-record/read-only/utils/isObjectMetadataReadOnly';
const StyledDiv = styled.div`
display: flex;
@@ -21,7 +22,9 @@ type ObjectFieldsProps = {
};
export const ObjectFields = ({ objectMetadataItem }: ObjectFieldsProps) => {
const shouldDisplayAddFieldButton = !objectMetadataItem.isRemote;
const readonly = isObjectMetadataReadOnly({
objectMetadataItem,
});
const { t } = useLingui();
const objectLabelSingular = objectMetadataItem.labelSingular;
@@ -36,7 +39,7 @@ export const ObjectFields = ({ objectMetadataItem }: ObjectFieldsProps) => {
objectMetadataItem={objectMetadataItem}
mode="view"
/>
{shouldDisplayAddFieldButton && (
{!readonly && (
<StyledDiv>
<UndecoratedLink
to={getSettingsPath(SettingsPath.ObjectNewFieldSelect, {
@@ -10,6 +10,7 @@ import { H2Title, IconArchive } from 'twenty-ui/display';
import { Button } from 'twenty-ui/input';
import { Section } from 'twenty-ui/layout';
import { useNavigateSettings } from '~/hooks/useNavigateSettings';
import { isObjectMetadataReadOnly } from '@/object-record/read-only/utils/isObjectMetadataReadOnly';
type ObjectSettingsProps = {
objectMetadataItem: ObjectMetadataItem;
@@ -27,6 +28,7 @@ const StyledFormSection = styled(Section)`
export const ObjectSettings = ({ objectMetadataItem }: ObjectSettingsProps) => {
const { t } = useLingui();
const readonly = isObjectMetadataReadOnly({ objectMetadataItem });
const navigate = useNavigateSettings();
const { updateOneObjectMetadataItem } = useUpdateOneObjectMetadataItem();
const handleDisable = async () => {
@@ -59,17 +61,22 @@ export const ObjectSettings = ({ objectMetadataItem }: ObjectSettingsProps) => {
/>
</Section>
</StyledFormSection>
<StyledFormSection>
<Section>
<H2Title title={t`Danger zone`} description={t`Deactivate object`} />
<Button
Icon={IconArchive}
title={t`Deactivate`}
size="small"
onClick={handleDisable}
/>
</Section>
</StyledFormSection>
{!readonly && (
<StyledFormSection>
<Section>
<H2Title
title={t`Danger zone`}
description={t`Deactivate object`}
/>
<Button
Icon={IconArchive}
title={t`Deactivate`}
size="small"
onClick={handleDisable}
/>
</Section>
</StyledFormSection>
)}
</StyledContentContainer>
);
};
@@ -2,13 +2,12 @@ import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { SettingsDataModelObjectTypeTag } from '@/settings/data-model/objects/components/SettingsDataModelObjectTypeTag';
import { getObjectTypeLabel } from '@/settings/data-model/utils/getObjectTypeLabel';
import {
IconBox,
OverflowingTextWithTooltip,
useIcons,
} from 'twenty-ui/display';
import { SettingsItemTypeTag } from '@/settings/components/SettingsItemTypeTag';
export type SettingsDataModelObjectPreviewProps = {
className?: string;
@@ -69,7 +68,6 @@ const SettingsDataModelObjectPreviewItem = ({
const theme = useTheme();
const { getIcon } = useIcons();
const ObjectIcon = getIcon(objectMetadataItem.icon);
const objectTypeLabel = getObjectTypeLabel(objectMetadataItem);
return (
<>
@@ -90,7 +88,7 @@ const SettingsDataModelObjectPreviewItem = ({
}
/>
</StyledObjectName>
<SettingsDataModelObjectTypeTag objectTypeLabel={objectTypeLabel} />
<SettingsItemTypeTag item={objectMetadataItem} />
</StyledObjectPreview>
</>
);
@@ -1,21 +0,0 @@
import { type ObjectTypeLabel } from '@/settings/data-model/utils/getObjectTypeLabel';
import { Tag } from 'twenty-ui/components';
type SettingsDataModelObjectTypeTagProps = {
objectTypeLabel: ObjectTypeLabel;
className?: string;
};
export const SettingsDataModelObjectTypeTag = ({
className,
objectTypeLabel,
}: SettingsDataModelObjectTypeTagProps) => {
return (
<Tag
className={className}
color={objectTypeLabel.labelColor}
text={objectTypeLabel.labelText}
weight="medium"
/>
);
};
@@ -139,7 +139,11 @@ export const SettingsDataModelObjectAboutForm = ({
render={({ field: { onChange, value } }) => (
<IconPicker
selectedIconKey={value}
disabled={disableEdition}
onChange={({ iconKey }) => {
if (disableEdition) {
return;
}
onChange(iconKey);
onNewDirtyField?.();
}}
@@ -170,9 +174,10 @@ export const SettingsDataModelObjectAboutForm = ({
}}
onBlur={() => onNewDirtyField?.()}
disabled={
objectMetadataItem &&
!objectMetadataItem?.isCustom &&
isLabelSyncedWithName
disableEdition ||
(objectMetadataItem &&
!objectMetadataItem?.isCustom &&
isLabelSyncedWithName)
}
fullWidth
maxLength={OBJECT_NAME_MAXIMUM_LENGTH}
@@ -201,9 +206,10 @@ export const SettingsDataModelObjectAboutForm = ({
}}
onBlur={() => onNewDirtyField?.()}
disabled={
objectMetadataItem &&
!objectMetadataItem?.isCustom &&
isLabelSyncedWithName
disableEdition ||
(objectMetadataItem &&
!objectMetadataItem?.isCustom &&
isLabelSyncedWithName)
}
fullWidth
maxLength={OBJECT_NAME_MAXIMUM_LENGTH}
@@ -222,6 +228,7 @@ export const SettingsDataModelObjectAboutForm = ({
value={value ?? undefined}
onChange={(nextValue) => onChange(nextValue ?? null)}
onBlur={() => onNewDirtyField?.()}
disabled={disableEdition}
/>
)}
/>
@@ -326,6 +333,7 @@ export const SettingsDataModelObjectAboutForm = ({
description={t`Should changing an object's label also change the API?`}
checked={value ?? true}
advancedMode
disabled={disableEdition}
onChange={(value) => {
onChange(value);
const isCustomObject =
@@ -16,6 +16,7 @@ import { useNavigate } from 'react-router-dom';
import { isLabelIdentifierFieldMetadataTypes } from 'twenty-shared/utils';
import { IconCircleOff, IconPlus, useIcons } from 'twenty-ui/display';
import { type SelectOption } from 'twenty-ui/input';
import { isObjectMetadataReadOnly } from '@/object-record/read-only/utils/isObjectMetadataReadOnly';
export const settingsDataModelObjectIdentifiersFormSchema =
objectMetadataItemSchema.pick({
@@ -44,6 +45,7 @@ const StyledContainer = styled.div`
export const SettingsDataModelObjectIdentifiersForm = ({
objectMetadataItem,
}: SettingsDataModelObjectIdentifiersFormProps) => {
const readonly = isObjectMetadataReadOnly({ objectMetadataItem });
const formConfig = useForm<SettingsDataModelObjectIdentifiersFormValues>({
mode: 'onTouched',
resolver: zodResolver(settingsDataModelObjectIdentifiersFormSchema),
@@ -134,7 +136,7 @@ export const SettingsDataModelObjectIdentifiersForm = ({
options={options}
value={value}
withSearchInput={label === t`Record label`}
disabled={!objectMetadataItem.isCustom}
disabled={!objectMetadataItem.isCustom || readonly}
callToActionButton={
label === t`Record label`
? {
@@ -0,0 +1,51 @@
import { isDefined } from 'twenty-shared/utils';
export type ItemTagInfo =
| StandardItemTagInfo
| CustomItemTagInfo
| RemoteItemTagInfo
| ManagedItemTagInfo;
type StandardItemTagInfo = {
labelText: 'Standard';
labelColor: 'blue';
};
type CustomItemTagInfo = {
labelText: 'Custom';
labelColor: 'orange';
};
type RemoteItemTagInfo = {
labelText: 'Remote';
labelColor: 'green';
};
type ManagedItemTagInfo = {
labelText: 'Managed';
labelColor: 'sky';
};
export const getItemTagInfo = ({
isCustom,
isRemote,
applicationId,
}: {
isCustom?: boolean;
isRemote?: boolean;
applicationId?: string | null;
}): ItemTagInfo => {
if (isDefined(applicationId)) {
return { labelText: 'Managed', labelColor: 'sky' };
}
if (isCustom!!) {
return { labelText: 'Custom', labelColor: 'orange' };
}
if (isRemote!!) {
return { labelText: 'Remote', labelColor: 'green' };
}
return { labelText: 'Standard', labelColor: 'blue' };
};
@@ -1,39 +0,0 @@
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
export type ObjectTypeLabel =
| StandardObjectTypeLabel
| CustomObjectTypeLabel
| RemoteObjectTypeLabel;
type StandardObjectTypeLabel = {
labelText: 'Standard';
labelColor: 'blue';
};
type CustomObjectTypeLabel = {
labelText: 'Custom';
labelColor: 'orange';
};
type RemoteObjectTypeLabel = {
labelText: 'Remote';
labelColor: 'green';
};
export const getObjectTypeLabel = (
objectMetadataItem: Pick<ObjectMetadataItem, 'isCustom' | 'isRemote'>,
): ObjectTypeLabel =>
objectMetadataItem.isCustom
? {
labelText: 'Custom',
labelColor: 'orange',
}
: objectMetadataItem.isRemote
? {
labelText: 'Remote',
labelColor: 'green',
}
: {
labelText: 'Standard',
labelColor: 'blue',
};