feat: set field as custom object label identifier in Object Detail (#3360)

* feat: set field as custom object label identifier in Object Detail

Closes #3302

* feat: prevent disabling Object label identitifer field in back-end

* refactor: review - extract isLabelIdentifier variable
This commit is contained in:
Thaïs 2024-01-17 08:19:41 -03:00 committed by GitHub
parent 8864528d55
commit 96d990e275
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 166 additions and 79 deletions

View File

@ -8,6 +8,7 @@ import { useMapToObjectRecordIdentifier } from '@/object-metadata/hooks/useMapTo
import { objectMetadataItemFamilySelector } from '@/object-metadata/states/objectMetadataItemFamilySelector';
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
import { getBasePathToShowPage } from '@/object-metadata/utils/getBasePathToShowPage';
import { getLabelIdentifierFieldMetadataItem } from '@/object-metadata/utils/getLabelIdentifierFieldMetadataItem';
import { getObjectMetadataItemsMock } from '@/object-metadata/utils/getObjectMetadataItemsMock';
import { useGenerateCreateManyRecordMutation } from '@/object-record/hooks/useGenerateCreateManyRecordMutation';
import { useGenerateCreateOneRecordMutation } from '@/object-record/hooks/useGenerateCreateOneRecordMutation';
@ -120,9 +121,8 @@ export const useObjectMetadataItem = (
objectMetadataItem,
});
const labelIdentifierFieldMetadata = objectMetadataItem.fields.find(
({ name }) => name === 'name',
);
const labelIdentifierFieldMetadata =
getLabelIdentifierFieldMetadataItem(objectMetadataItem);
const basePathToShowPage = getBasePathToShowPage({
objectMetadataItem,

View File

@ -51,7 +51,12 @@ export const useObjectMetadataItemForSettings = () => {
const editObjectMetadataItem = (
input: Pick<
ObjectMetadataItem,
'id' | 'labelPlural' | 'labelSingular' | 'icon' | 'description'
| 'description'
| 'icon'
| 'id'
| 'labelIdentifierFieldMetadataId'
| 'labelPlural'
| 'labelSingular'
>,
) =>
updateOneObjectMetadataItem({

View File

@ -5,11 +5,17 @@ import { ObjectMetadataItem } from '../types/ObjectMetadataItem';
export const formatObjectMetadataItemInput = (
input: Pick<
ObjectMetadataItem,
'labelPlural' | 'labelSingular' | 'icon' | 'description'
| 'description'
| 'icon'
| 'labelIdentifierFieldMetadataId'
| 'labelPlural'
| 'labelSingular'
>,
) => ({
description: input.description?.trim() ?? null,
icon: input.icon,
labelIdentifierFieldMetadataId:
input.labelIdentifierFieldMetadataId?.trim() ?? null,
labelPlural: input.labelPlural.trim(),
labelSingular: input.labelSingular.trim(),
namePlural: toCamelCase(input.labelPlural.trim()),

View File

@ -1,12 +1,13 @@
import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { isLabelIdentifierField } from '@/object-metadata/utils/isLabelIdentifierField';
export const getLabelIdentifierFieldMetadataItem = (
objectMetadataItem: ObjectMetadataItem,
): FieldMetadataItem | undefined => {
return objectMetadataItem.fields.find(
(field) =>
field.id === objectMetadataItem.labelIdentifierFieldMetadataId ||
field.name === 'name',
): FieldMetadataItem | undefined =>
objectMetadataItem.fields.find((fieldMetadataItem) =>
isLabelIdentifierField({
fieldMetadataItem,
objectMetadataItem,
}),
);
};

View File

@ -1,17 +1,15 @@
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { OrderBy } from '@/object-metadata/types/OrderBy';
import { OrderByField } from '@/object-metadata/types/OrderByField';
import { getLabelIdentifierFieldMetadataItem } from '@/object-metadata/utils/getLabelIdentifierFieldMetadataItem';
import { FieldMetadataType } from '~/generated-metadata/graphql';
export const getObjectOrderByField = (
objectMetadataItem: ObjectMetadataItem,
orderBy?: OrderBy | null,
): OrderByField => {
const labelIdentifierFieldMetadata = objectMetadataItem.fields.find(
(field) =>
field.id === objectMetadataItem.labelIdentifierFieldMetadataId ||
field.name === 'name',
);
const labelIdentifierFieldMetadata =
getLabelIdentifierFieldMetadataItem(objectMetadataItem);
if (labelIdentifierFieldMetadata) {
switch (labelIdentifierFieldMetadata.type) {

View File

@ -13,8 +13,7 @@ export const isLabelIdentifierField = ({
ObjectMetadataItem,
'labelIdentifierFieldMetadataId'
>;
}) => {
return isDefined(objectMetadataItem.labelIdentifierFieldMetadataId)
}) =>
isDefined(objectMetadataItem.labelIdentifierFieldMetadataId)
? fieldMetadataItem.id === objectMetadataItem.labelIdentifierFieldMetadataId
: fieldMetadataItem.name === DEFAULT_LABEL_IDENTIFIER_FIELD_NAME;
};

View File

@ -1,6 +1,7 @@
import { useContext } from 'react';
import { useRecoilValue, useSetRecoilState } from 'recoil';
import { isLabelIdentifierField } from '@/object-metadata/utils/isLabelIdentifierField';
import { useRecordTableStates } from '@/object-record/record-table/hooks/internal/useRecordTableStates';
import { RelationPickerHotkeyScope } from '@/object-record/relation-picker/types/RelationPickerHotkeyScope';
import { contextMenuIsOpenState } from '@/ui/navigation/context-menu/states/contextMenuIsOpenState';
@ -65,9 +66,16 @@ export const RecordTableCellContainer = ({
useUpdateRecord: () => [updateRecord, {}],
hotkeyScope: customHotkeyScope,
basePathToShowPage: objectMetadataConfig?.basePathToShowPage,
isLabelIdentifier:
columnDefinition.fieldMetadataId ===
objectMetadataConfig?.labelIdentifierFieldMetadataId,
isLabelIdentifier: isLabelIdentifierField({
fieldMetadataItem: {
id: columnDefinition.fieldMetadataId,
name: columnDefinition.metadata.fieldName,
},
objectMetadataItem: {
labelIdentifierFieldMetadataId:
objectMetadataConfig?.labelIdentifierFieldMetadataId,
},
}),
}}
>
<RecordTableCell customHotkeyScope={{ scope: customHotkeyScope }} />

View File

@ -3,6 +3,7 @@ import {
IconDotsVertical,
IconEye,
IconPencil,
IconTextSize,
} from '@/ui/display/icon';
import { LightIconButton } from '@/ui/input/button/components/LightIconButton';
import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown';
@ -15,6 +16,7 @@ type SettingsObjectFieldActiveActionDropdownProps = {
isCustomField?: boolean;
onDisable?: () => void;
onEdit: () => void;
onSetAsLabelIdentifier?: () => void;
scopeKey: string;
};
@ -22,6 +24,7 @@ export const SettingsObjectFieldActiveActionDropdown = ({
isCustomField,
onDisable,
onEdit,
onSetAsLabelIdentifier,
scopeKey,
}: SettingsObjectFieldActiveActionDropdownProps) => {
const dropdownId = `${scopeKey}-settings-field-active-action-dropdown`;
@ -38,6 +41,11 @@ export const SettingsObjectFieldActiveActionDropdown = ({
closeDropdown();
};
const handleSetAsLabelIdentifier = () => {
onSetAsLabelIdentifier?.();
closeDropdown();
};
return (
<Dropdown
dropdownId={dropdownId}
@ -52,6 +60,13 @@ export const SettingsObjectFieldActiveActionDropdown = ({
LeftIcon={isCustomField ? IconPencil : IconEye}
onClick={handleEdit}
/>
{!!onSetAsLabelIdentifier && (
<MenuItem
text="Set as record text"
LeftIcon={IconTextSize}
onClick={handleSetAsLabelIdentifier}
/>
)}
{!!onDisable && (
<MenuItem
text="Disable"

View File

@ -26,6 +26,7 @@ import { Table } from '@/ui/layout/table/components/Table';
import { TableHeader } from '@/ui/layout/table/components/TableHeader';
import { TableSection } from '@/ui/layout/table/components/TableSection';
import { Breadcrumb } from '@/ui/navigation/bread-crumb/components/Breadcrumb';
import { FieldMetadataType } from '~/generated-metadata/graphql';
const StyledDiv = styled.div`
display: flex;
@ -37,8 +38,11 @@ export const SettingsObjectDetail = () => {
const navigate = useNavigate();
const { objectSlug = '' } = useParams();
const { disableObjectMetadataItem, findActiveObjectMetadataItemBySlug } =
useObjectMetadataItemForSettings();
const {
disableObjectMetadataItem,
editObjectMetadataItem,
findActiveObjectMetadataItemBySlug,
} = useObjectMetadataItemForSettings();
const activeObjectMetadataItem =
findActiveObjectMetadataItemBySlug(objectSlug);
@ -64,10 +68,17 @@ export const SettingsObjectDetail = () => {
navigate('/settings/objects');
};
const handleDisableField = async (
const handleDisableField = (activeFieldMetadatItem: FieldMetadataItem) => {
disableMetadataField(activeFieldMetadatItem);
};
const handleSetLabelIdentifierField = (
activeFieldMetadatItem: FieldMetadataItem,
) => {
disableMetadataField(activeFieldMetadatItem);
editObjectMetadataItem({
...activeObjectMetadataItem,
labelIdentifierFieldMetadataId: activeFieldMetadatItem.id,
});
};
return (
@ -104,38 +115,56 @@ export const SettingsObjectDetail = () => {
</StyledObjectFieldTableRow>
{!!activeMetadataFields.length && (
<TableSection title="Active">
{activeMetadataFields.map((activeMetadataField) => (
<SettingsObjectFieldItemTableRow
key={activeMetadataField.id}
identifierType={getFieldIdentifierType(
activeMetadataField,
activeObjectMetadataItem,
)}
variant={
activeObjectMetadataItem.isCustom
? 'identifier'
: 'field-type'
}
fieldMetadataItem={activeMetadataField}
ActionIcon={
<SettingsObjectFieldActiveActionDropdown
isCustomField={!!activeMetadataField.isCustom}
scopeKey={activeMetadataField.id}
onEdit={() =>
navigate(`./${getFieldSlug(activeMetadataField)}`)
}
onDisable={
isLabelIdentifierField({
fieldMetadataItem: activeMetadataField,
objectMetadataItem: activeObjectMetadataItem,
})
? undefined
: () => handleDisableField(activeMetadataField)
}
/>
}
/>
))}
{activeMetadataFields.map((activeMetadataField) => {
const isLabelIdentifier = isLabelIdentifierField({
fieldMetadataItem: activeMetadataField,
objectMetadataItem: activeObjectMetadataItem,
});
const canBeSetAsLabelIdentifier =
activeObjectMetadataItem.isCustom &&
!isLabelIdentifier &&
[FieldMetadataType.Text, FieldMetadataType.Number].includes(
activeMetadataField.type,
);
return (
<SettingsObjectFieldItemTableRow
key={activeMetadataField.id}
identifierType={getFieldIdentifierType(
activeMetadataField,
activeObjectMetadataItem,
)}
variant={
activeObjectMetadataItem.isCustom
? 'identifier'
: 'field-type'
}
fieldMetadataItem={activeMetadataField}
ActionIcon={
<SettingsObjectFieldActiveActionDropdown
isCustomField={!!activeMetadataField.isCustom}
scopeKey={activeMetadataField.id}
onEdit={() =>
navigate(`./${getFieldSlug(activeMetadataField)}`)
}
onSetAsLabelIdentifier={
canBeSetAsLabelIdentifier
? () =>
handleSetLabelIdentifierField(
activeMetadataField,
)
: undefined
}
onDisable={
isLabelIdentifier
? undefined
: () => handleDisableField(activeMetadataField)
}
/>
}
/>
);
})}
</TableSection>
)}
{!!disabledMetadataFields.length && (

View File

@ -5,6 +5,7 @@ import { useFieldMetadataItem } from '@/object-metadata/hooks/useFieldMetadataIt
import { useObjectMetadataItemForSettings } from '@/object-metadata/hooks/useObjectMetadataItemForSettings';
import { useRelationMetadata } from '@/object-metadata/hooks/useRelationMetadata';
import { getFieldSlug } from '@/object-metadata/utils/getFieldSlug';
import { isLabelIdentifierField } from '@/object-metadata/utils/isLabelIdentifierField';
import { SaveAndCancelButtons } from '@/settings/components/SaveAndCancelButtons/SaveAndCancelButtons';
import { SettingsHeaderContainer } from '@/settings/components/SettingsHeaderContainer';
import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer';
@ -114,6 +115,11 @@ export const SettingsObjectFieldEdit = () => {
const canSave = isValid && hasFormChanged;
const isLabelIdentifier = isLabelIdentifierField({
fieldMetadataItem: activeMetadataField,
objectMetadataItem: activeObjectMetadataItem,
});
const handleSave = async () => {
if (!validatedFormValues) return;
@ -203,15 +209,17 @@ export const SettingsObjectFieldEdit = () => {
select: formValues.select,
}}
/>
<Section>
<H2Title title="Danger zone" description="Disable this field" />
<Button
Icon={IconArchive}
title="Disable"
size="small"
onClick={handleDisable}
/>
</Section>
{!isLabelIdentifier && (
<Section>
<H2Title title="Danger zone" description="Disable this field" />
<Button
Icon={IconArchive}
title="Disable"
size="small"
onClick={handleDisable}
/>
</Section>
)}
</SettingsPageContainer>
</SubMenuTopBarContainer>
);

View File

@ -4,6 +4,7 @@ import styled from '@emotion/styled';
import { useFieldMetadataItem } from '@/object-metadata/hooks/useFieldMetadataItem';
import { useObjectMetadataItemForSettings } from '@/object-metadata/hooks/useObjectMetadataItemForSettings';
import { isLabelIdentifierField } from '@/object-metadata/utils/isLabelIdentifierField';
import { SaveAndCancelButtons } from '@/settings/components/SaveAndCancelButtons/SaveAndCancelButtons';
import { SettingsHeaderContainer } from '@/settings/components/SettingsHeaderContainer';
import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer';
@ -133,16 +134,23 @@ export const SettingsObjectNewFieldStep1 = () => {
</StyledObjectFieldTableRow>
{!!activeMetadataFields.length && (
<TableSection isInitiallyExpanded={false} title="Active">
{activeMetadataFields.map((field) => (
{activeMetadataFields.map((activeMetadataField) => (
<SettingsObjectFieldItemTableRow
key={field.id}
fieldMetadataItem={field}
key={activeMetadataField.id}
fieldMetadataItem={activeMetadataField}
ActionIcon={
<LightIconButton
Icon={IconMinus}
accent="tertiary"
onClick={() => handleToggleField(field.id)}
/>
isLabelIdentifierField({
fieldMetadataItem: activeMetadataField,
objectMetadataItem: activeObjectMetadataItem,
}) ? undefined : (
<LightIconButton
Icon={IconMinus}
accent="tertiary"
onClick={() =>
handleToggleField(activeMetadataField.id)
}
/>
)
}
/>
))}
@ -150,15 +158,17 @@ export const SettingsObjectNewFieldStep1 = () => {
)}
{!!disabledMetadataFields.length && (
<TableSection title="Disabled">
{disabledMetadataFields.map((field) => (
{disabledMetadataFields.map((disabledMetadataField) => (
<SettingsObjectFieldItemTableRow
key={field.name}
fieldMetadataItem={field}
key={disabledMetadataField.name}
fieldMetadataItem={disabledMetadataField}
ActionIcon={
<LightIconButton
Icon={IconPlus}
accent="tertiary"
onClick={() => handleToggleField(field.id)}
onClick={() =>
handleToggleField(disabledMetadataField.id)
}
/>
}
/>

View File

@ -186,6 +186,14 @@ export class FieldMetadataService extends TypeOrmQueryService<FieldMetadataEntit
throw new NotFoundException('Object does not exist');
}
if (
objectMetadata.labelIdentifierFieldMetadataId ===
existingFieldMetadata.id &&
fieldMetadataInput.isActive === false
) {
throw new BadRequestException('Cannot deactivate label identifier field');
}
// Check if the id of the options has been provided
if (fieldMetadataInput.options) {
for (const option of fieldMetadataInput.options) {