Twnty-#6797 view/edit inactive feature (#6953)

This PR resolves the issue raised under #6797. Earlier no view/edit
option were present for inActive objects in data-model inside settings.
To resolve that following changes were done:

1. `SettingsObjectFieldItemTableRow` was not passing the onEdit with url
to `SettingsObjectFieldActiveActionDropdown` to redirect on clicking it.
So passed onEdit there.
2. `SettingsObjectFieldActiveActionDropdown` was not implementing the
onEdit functionality, so implemented that by creating `handleEdit()`
function.
3. `SettingsObjectFieldEdit` was assuming only `activeObjectMetadata
`will be coming to render on page. So, when inactive object was accessed
the path not found error message was thrown. Thus did changes to manage
both active and inactive objects by generalizing the
              `activeObjectMetadata ` ->  `objectMetadata` 
               `activeMetadataField `-> `metadataField`.
4. `findObjectMetadataItemBySlug `function was written inside
`useFilteredObjectMetadataItems` for fetching active/inactive object.
5. Updated `SettingsObjectFieldEdit` button to show and change the
active to inactive state and vice versa.
6. Test was written for `findObjectMetadataItemBySlug`.

---------

Co-authored-by: Lucas Bordeau <bordeau.lucas@gmail.com>
This commit is contained in:
HKS07 2024-09-18 15:31:21 +05:30 committed by GitHub
parent df8bb84b35
commit cfc00c7924
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 99 additions and 37 deletions

View File

@ -57,6 +57,26 @@ describe('useFilteredObjectMetadataItems', () => {
});
});
it('should findObjectMetadataItemBySlug', async () => {
const { result } = renderHook(
() => {
const setMetadataItems = useSetRecoilState(objectMetadataItemsState);
setMetadataItems(mockObjectMetadataItems);
return useFilteredObjectMetadataItems();
},
{
wrapper: Wrapper,
},
);
act(() => {
const res = result.current.findObjectMetadataItemBySlug('people');
expect(res).toBeDefined();
expect(res?.namePlural).toBe('people');
});
});
it('should findObjectMetadataItemById', async () => {
const { result } = renderHook(
() => {

View File

@ -27,6 +27,11 @@ export const useFilteredObjectMetadataItems = () => {
({ isActive, isSystem }) => !isActive && !isSystem,
);
const findObjectMetadataItemBySlug = (slug: string) =>
objectMetadataItems.find(
(objectMetadataItem) => getObjectSlug(objectMetadataItem) === slug,
);
const findActiveObjectMetadataItemBySlug = (slug: string) =>
activeObjectMetadataItems.find(
(activeObjectMetadataItem) =>
@ -50,6 +55,7 @@ export const useFilteredObjectMetadataItems = () => {
findObjectMetadataItemByNamePlural,
inactiveObjectMetadataItems,
objectMetadataItems,
findObjectMetadataItemBySlug,
alphaSortedActiveObjectMetadataItems,
};
};

View File

@ -1,4 +1,10 @@
import { IconArchiveOff, IconDotsVertical, IconTrash } from 'twenty-ui';
import {
IconArchiveOff,
IconDotsVertical,
IconEye,
IconPencil,
IconTrash,
} from 'twenty-ui';
import { LightIconButton } from '@/ui/input/button/components/LightIconButton';
import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown';
@ -12,6 +18,7 @@ type SettingsObjectFieldInactiveActionDropdownProps = {
isCustomField?: boolean;
fieldType?: FieldMetadataType;
onActivate: () => void;
onEdit: () => void;
onDelete: () => void;
scopeKey: string;
};
@ -20,6 +27,7 @@ export const SettingsObjectFieldInactiveActionDropdown = ({
onActivate,
scopeKey,
onDelete,
onEdit,
isCustomField,
}: SettingsObjectFieldInactiveActionDropdownProps) => {
const dropdownId = `${scopeKey}-settings-field-disabled-action-dropdown`;
@ -36,6 +44,11 @@ export const SettingsObjectFieldInactiveActionDropdown = ({
closeDropdown();
};
const handleEdit = () => {
onEdit();
closeDropdown();
};
const isDeletable = isCustomField;
return (
@ -47,6 +60,11 @@ export const SettingsObjectFieldInactiveActionDropdown = ({
dropdownComponents={
<DropdownMenu width="160px">
<DropdownMenuItemsContainer>
<MenuItem
text={isCustomField ? 'Edit' : 'View'}
LeftIcon={isCustomField ? IconPencil : IconEye}
onClick={handleEdit}
/>
<MenuItem
text="Activate"
LeftIcon={IconArchiveOff}

View File

@ -269,6 +269,7 @@ export const SettingsObjectFieldItemTableRow = ({
<SettingsObjectFieldInactiveActionDropdown
isCustomField={fieldMetadataItem.isCustom === true}
scopeKey={fieldMetadataItem.id}
onEdit={() => navigate(linkToNavigate)}
onActivate={() => activateMetadataField(fieldMetadataItem)}
onDelete={() => deleteMetadataField(fieldMetadataItem)}
/>

View File

@ -5,7 +5,12 @@ import pick from 'lodash.pick';
import { useEffect } from 'react';
import { FormProvider, useForm } from 'react-hook-form';
import { useNavigate, useParams } from 'react-router-dom';
import { H2Title, IconArchive, IconHierarchy2 } from 'twenty-ui';
import {
H2Title,
IconArchive,
IconArchiveOff,
IconHierarchy2,
} from 'twenty-ui';
import { z } from 'zod';
import { useFieldMetadataItem } from '@/object-metadata/hooks/useFieldMetadataItem';
@ -56,16 +61,15 @@ export const SettingsObjectFieldEdit = () => {
const { enqueueSnackBar } = useSnackBar();
const { objectSlug = '', fieldSlug = '' } = useParams();
const { findActiveObjectMetadataItemBySlug } =
useFilteredObjectMetadataItems();
const { findObjectMetadataItemBySlug } = useFilteredObjectMetadataItems();
const activeObjectMetadataItem =
findActiveObjectMetadataItemBySlug(objectSlug);
const objectMetadataItem = findObjectMetadataItemBySlug(objectSlug);
const { deactivateMetadataField } = useFieldMetadataItem();
const activeMetadataField = activeObjectMetadataItem?.fields.find(
(metadataField) =>
metadataField.isActive && getFieldSlug(metadataField) === fieldSlug,
const { deactivateMetadataField, activateMetadataField } =
useFieldMetadataItem();
const fieldMetadataItem = objectMetadataItem?.fields.find(
(fieldMetadataItem) => getFieldSlug(fieldMetadataItem) === fieldSlug,
);
const getRelationMetadata = useGetRelationMetadata();
@ -74,11 +78,11 @@ export const SettingsObjectFieldEdit = () => {
const apolloClient = useApolloClient();
const { findManyRecordsQuery } = useFindManyRecordsQuery({
objectNameSingular: activeObjectMetadataItem?.nameSingular || '',
objectNameSingular: objectMetadataItem?.nameSingular || '',
});
const refetchRecords = async () => {
if (!activeObjectMetadataItem) return;
if (!objectMetadataItem) return;
await apolloClient.query({
query: findManyRecordsQuery,
fetchPolicy: 'network-only',
@ -89,27 +93,29 @@ export const SettingsObjectFieldEdit = () => {
mode: 'onTouched',
resolver: zodResolver(settingsFieldFormSchema()),
values: {
icon: activeMetadataField?.icon ?? 'Icon123',
type: activeMetadataField?.type as SettingsSupportedFieldType,
label: activeMetadataField?.label ?? '',
description: activeMetadataField?.description,
icon: fieldMetadataItem?.icon ?? 'Icon',
type: fieldMetadataItem?.type as SettingsSupportedFieldType,
label: fieldMetadataItem?.label ?? '',
description: fieldMetadataItem?.description,
},
});
useEffect(() => {
if (!activeObjectMetadataItem || !activeMetadataField) {
if (!objectMetadataItem || !fieldMetadataItem) {
navigate(AppPath.NotFound);
}
}, [activeMetadataField, activeObjectMetadataItem, navigate]);
}, [fieldMetadataItem, objectMetadataItem, navigate]);
const { isDirty, isValid, isSubmitting } = formConfig.formState;
const canSave = isDirty && isValid && !isSubmitting;
if (!activeObjectMetadataItem || !activeMetadataField) return null;
if (!isDefined(objectMetadataItem) || !isDefined(fieldMetadataItem)) {
return null;
}
const isLabelIdentifier = isLabelIdentifierField({
fieldMetadataItem: activeMetadataField,
objectMetadataItem: activeObjectMetadataItem,
fieldMetadataItem: fieldMetadataItem,
objectMetadataItem: objectMetadataItem,
});
const handleSave = async (
@ -125,7 +131,7 @@ export const SettingsObjectFieldEdit = () => {
) {
const { relationFieldMetadataItem } =
getRelationMetadata({
fieldMetadataItem: activeMetadataField,
fieldMetadataItem: fieldMetadataItem,
}) ?? {};
if (isDefined(relationFieldMetadataItem)) {
@ -145,7 +151,7 @@ export const SettingsObjectFieldEdit = () => {
);
await updateOneFieldMetadataItem({
fieldMetadataIdToUpdate: activeMetadataField.id,
fieldMetadataIdToUpdate: fieldMetadataItem.id,
updatePayload: formattedInput,
});
}
@ -161,12 +167,17 @@ export const SettingsObjectFieldEdit = () => {
};
const handleDeactivate = async () => {
await deactivateMetadataField(activeMetadataField);
await deactivateMetadataField(fieldMetadataItem);
navigate(`/settings/objects/${objectSlug}`);
};
const handleActivate = async () => {
await activateMetadataField(fieldMetadataItem);
navigate(`/settings/objects/${objectSlug}`);
};
const shouldDisplaySaveAndCancel =
canPersistFieldMetadataItemUpdate(activeMetadataField);
canPersistFieldMetadataItemUpdate(fieldMetadataItem);
return (
<RecordFieldValueSelectorContextProvider>
@ -174,7 +185,7 @@ export const SettingsObjectFieldEdit = () => {
<FormProvider {...formConfig}>
<SubMenuTopBarContainer
Icon={IconHierarchy2}
title={activeMetadataField?.label}
title={fieldMetadataItem?.label}
links={[
{
children: 'Workspace',
@ -185,11 +196,11 @@ export const SettingsObjectFieldEdit = () => {
href: '/settings/objects',
},
{
children: activeObjectMetadataItem.labelPlural,
children: objectMetadataItem.labelPlural,
href: `/settings/objects/${objectSlug}`,
},
{
children: activeMetadataField.label,
children: fieldMetadataItem.label,
},
]}
actionButton={
@ -210,8 +221,8 @@ export const SettingsObjectFieldEdit = () => {
description="The name and icon of this field"
/>
<SettingsDataModelFieldIconLabelForm
disabled={!activeMetadataField.isCustom}
fieldMetadataItem={activeMetadataField}
disabled={!fieldMetadataItem.isCustom}
fieldMetadataItem={fieldMetadataItem}
maxLength={FIELD_NAME_MAXIMUM_LENGTH}
/>
</Section>
@ -219,8 +230,8 @@ export const SettingsObjectFieldEdit = () => {
<H2Title title="Values" description="The values of this field" />
<SettingsDataModelFieldSettingsFormCard
disableCurrencyForm
fieldMetadataItem={activeMetadataField}
objectMetadataItem={activeObjectMetadataItem}
fieldMetadataItem={fieldMetadataItem}
objectMetadataItem={objectMetadataItem}
/>
</Section>
<Section>
@ -229,8 +240,8 @@ export const SettingsObjectFieldEdit = () => {
description="The description of this field"
/>
<SettingsDataModelFieldDescriptionForm
disabled={!activeMetadataField.isCustom}
fieldMetadataItem={activeMetadataField}
disabled={!fieldMetadataItem.isCustom}
fieldMetadataItem={fieldMetadataItem}
/>
</Section>
{!isLabelIdentifier && (
@ -240,11 +251,17 @@ export const SettingsObjectFieldEdit = () => {
description="Deactivate this field"
/>
<Button
Icon={IconArchive}
Icon={
fieldMetadataItem.isActive ? IconArchive : IconArchiveOff
}
variant="secondary"
title="Deactivate"
title={fieldMetadataItem.isActive ? 'Deactivate' : 'Activate'}
size="small"
onClick={handleDeactivate}
onClick={
fieldMetadataItem.isActive
? handleDeactivate
: handleActivate
}
/>
</Section>
)}