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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -3,6 +3,7 @@ import {
IconDotsVertical, IconDotsVertical,
IconEye, IconEye,
IconPencil, IconPencil,
IconTextSize,
} from '@/ui/display/icon'; } from '@/ui/display/icon';
import { LightIconButton } from '@/ui/input/button/components/LightIconButton'; import { LightIconButton } from '@/ui/input/button/components/LightIconButton';
import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown'; import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown';
@ -15,6 +16,7 @@ type SettingsObjectFieldActiveActionDropdownProps = {
isCustomField?: boolean; isCustomField?: boolean;
onDisable?: () => void; onDisable?: () => void;
onEdit: () => void; onEdit: () => void;
onSetAsLabelIdentifier?: () => void;
scopeKey: string; scopeKey: string;
}; };
@ -22,6 +24,7 @@ export const SettingsObjectFieldActiveActionDropdown = ({
isCustomField, isCustomField,
onDisable, onDisable,
onEdit, onEdit,
onSetAsLabelIdentifier,
scopeKey, scopeKey,
}: SettingsObjectFieldActiveActionDropdownProps) => { }: SettingsObjectFieldActiveActionDropdownProps) => {
const dropdownId = `${scopeKey}-settings-field-active-action-dropdown`; const dropdownId = `${scopeKey}-settings-field-active-action-dropdown`;
@ -38,6 +41,11 @@ export const SettingsObjectFieldActiveActionDropdown = ({
closeDropdown(); closeDropdown();
}; };
const handleSetAsLabelIdentifier = () => {
onSetAsLabelIdentifier?.();
closeDropdown();
};
return ( return (
<Dropdown <Dropdown
dropdownId={dropdownId} dropdownId={dropdownId}
@ -52,6 +60,13 @@ export const SettingsObjectFieldActiveActionDropdown = ({
LeftIcon={isCustomField ? IconPencil : IconEye} LeftIcon={isCustomField ? IconPencil : IconEye}
onClick={handleEdit} onClick={handleEdit}
/> />
{!!onSetAsLabelIdentifier && (
<MenuItem
text="Set as record text"
LeftIcon={IconTextSize}
onClick={handleSetAsLabelIdentifier}
/>
)}
{!!onDisable && ( {!!onDisable && (
<MenuItem <MenuItem
text="Disable" 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 { TableHeader } from '@/ui/layout/table/components/TableHeader';
import { TableSection } from '@/ui/layout/table/components/TableSection'; import { TableSection } from '@/ui/layout/table/components/TableSection';
import { Breadcrumb } from '@/ui/navigation/bread-crumb/components/Breadcrumb'; import { Breadcrumb } from '@/ui/navigation/bread-crumb/components/Breadcrumb';
import { FieldMetadataType } from '~/generated-metadata/graphql';
const StyledDiv = styled.div` const StyledDiv = styled.div`
display: flex; display: flex;
@ -37,8 +38,11 @@ export const SettingsObjectDetail = () => {
const navigate = useNavigate(); const navigate = useNavigate();
const { objectSlug = '' } = useParams(); const { objectSlug = '' } = useParams();
const { disableObjectMetadataItem, findActiveObjectMetadataItemBySlug } = const {
useObjectMetadataItemForSettings(); disableObjectMetadataItem,
editObjectMetadataItem,
findActiveObjectMetadataItemBySlug,
} = useObjectMetadataItemForSettings();
const activeObjectMetadataItem = const activeObjectMetadataItem =
findActiveObjectMetadataItemBySlug(objectSlug); findActiveObjectMetadataItemBySlug(objectSlug);
@ -64,10 +68,17 @@ export const SettingsObjectDetail = () => {
navigate('/settings/objects'); navigate('/settings/objects');
}; };
const handleDisableField = async ( const handleDisableField = (activeFieldMetadatItem: FieldMetadataItem) => {
disableMetadataField(activeFieldMetadatItem);
};
const handleSetLabelIdentifierField = (
activeFieldMetadatItem: FieldMetadataItem, activeFieldMetadatItem: FieldMetadataItem,
) => { ) => {
disableMetadataField(activeFieldMetadatItem); editObjectMetadataItem({
...activeObjectMetadataItem,
labelIdentifierFieldMetadataId: activeFieldMetadatItem.id,
});
}; };
return ( return (
@ -104,38 +115,56 @@ export const SettingsObjectDetail = () => {
</StyledObjectFieldTableRow> </StyledObjectFieldTableRow>
{!!activeMetadataFields.length && ( {!!activeMetadataFields.length && (
<TableSection title="Active"> <TableSection title="Active">
{activeMetadataFields.map((activeMetadataField) => ( {activeMetadataFields.map((activeMetadataField) => {
<SettingsObjectFieldItemTableRow const isLabelIdentifier = isLabelIdentifierField({
key={activeMetadataField.id} fieldMetadataItem: activeMetadataField,
identifierType={getFieldIdentifierType( objectMetadataItem: activeObjectMetadataItem,
activeMetadataField, });
activeObjectMetadataItem, const canBeSetAsLabelIdentifier =
)} activeObjectMetadataItem.isCustom &&
variant={ !isLabelIdentifier &&
activeObjectMetadataItem.isCustom [FieldMetadataType.Text, FieldMetadataType.Number].includes(
? 'identifier' activeMetadataField.type,
: 'field-type' );
}
fieldMetadataItem={activeMetadataField} return (
ActionIcon={ <SettingsObjectFieldItemTableRow
<SettingsObjectFieldActiveActionDropdown key={activeMetadataField.id}
isCustomField={!!activeMetadataField.isCustom} identifierType={getFieldIdentifierType(
scopeKey={activeMetadataField.id} activeMetadataField,
onEdit={() => activeObjectMetadataItem,
navigate(`./${getFieldSlug(activeMetadataField)}`) )}
} variant={
onDisable={ activeObjectMetadataItem.isCustom
isLabelIdentifierField({ ? 'identifier'
fieldMetadataItem: activeMetadataField, : 'field-type'
objectMetadataItem: activeObjectMetadataItem, }
}) fieldMetadataItem={activeMetadataField}
? undefined ActionIcon={
: () => handleDisableField(activeMetadataField) <SettingsObjectFieldActiveActionDropdown
} isCustomField={!!activeMetadataField.isCustom}
/> scopeKey={activeMetadataField.id}
} onEdit={() =>
/> navigate(`./${getFieldSlug(activeMetadataField)}`)
))} }
onSetAsLabelIdentifier={
canBeSetAsLabelIdentifier
? () =>
handleSetLabelIdentifierField(
activeMetadataField,
)
: undefined
}
onDisable={
isLabelIdentifier
? undefined
: () => handleDisableField(activeMetadataField)
}
/>
}
/>
);
})}
</TableSection> </TableSection>
)} )}
{!!disabledMetadataFields.length && ( {!!disabledMetadataFields.length && (

View File

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

View File

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