[Issue-5772] Add sort feature on settings tables (#5787)

## Proposed Changes
-  Introduce  a new custom hook - useTableSort to sort table content
-  Add test cases for the new custom hook
- Integrate useTableSort hook on to the table in settings object and
settings object field pages

## Related Issue

https://github.com/twentyhq/twenty/issues/5772

## Evidence


https://github.com/twentyhq/twenty/assets/87609792/8be456ce-2fa5-44ec-8bbd-70fb6c8fdb30

## Evidence after addressing review comments


https://github.com/twentyhq/twenty/assets/87609792/c267e3da-72f9-4c0e-8c94-a38122d6395e

## Further comments

Apologies for the large PR. Looking forward for the review

---------

Co-authored-by: Félix Malfait <felix.malfait@gmail.com>
Co-authored-by: Lucas Bordeau <bordeau.lucas@gmail.com>
This commit is contained in:
Anand Krishnan M J
2024-08-14 20:41:17 +05:30
committed by GitHub
parent 0f75e14ab2
commit 59e14fabb4
40 changed files with 1229 additions and 445 deletions

View File

@ -1,28 +1,36 @@
import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import { ReactNode, useMemo } from 'react';
import { Nullable, useIcons } from 'twenty-ui';
import { useMemo } from 'react';
import { IconMinus, IconPlus, isDefined, useIcons } from 'twenty-ui';
import { useGetRelationMetadata } from '@/object-metadata/hooks/useGetRelationMetadata';
import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
import { getObjectSlug } from '@/object-metadata/utils/getObjectSlug';
import { FieldIdentifierType } from '@/settings/data-model/types/FieldIdentifierType';
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 { RELATION_TYPES } from '../../constants/RelationTypes';
import { LABEL_IDENTIFIER_FIELD_METADATA_TYPES } from '@/object-metadata/constants/LabelIdentifierFieldMetadataTypes';
import { useFieldMetadataItem } from '@/object-metadata/hooks/useFieldMetadataItem';
import { useUpdateOneObjectMetadataItem } from '@/object-metadata/hooks/useUpdateOneObjectMetadataItem';
import { getFieldSlug } from '@/object-metadata/utils/getFieldSlug';
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 { LightIconButton } from '@/ui/input/button/components/LightIconButton';
import { useNavigate } from 'react-router-dom';
import { useRecoilState } from 'recoil';
import { RelationMetadataType } from '~/generated-metadata/graphql';
import { SettingsObjectDetailTableItem } from '~/pages/settings/data-model/types/SettingsObjectDetailTableItem';
import { SettingsObjectFieldDataType } from './SettingsObjectFieldDataType';
type SettingsObjectFieldItemTableRowProps = {
ActionIcon: ReactNode;
fieldMetadataItem: FieldMetadataItem;
identifierType?: Nullable<FieldIdentifierType>;
variant?: 'field-type' | 'identifier';
isRemoteObjectField?: boolean;
to?: string;
settingsObjectDetailTableItem: SettingsObjectDetailTableItem;
status: 'active' | 'disabled';
mode: 'view' | 'new-field';
};
export const StyledObjectFieldTableRow = styled(TableRow)`
@ -40,13 +48,19 @@ const StyledIconTableCell = styled(TableCell)`
`;
export const SettingsObjectFieldItemTableRow = ({
ActionIcon,
fieldMetadataItem,
identifierType,
variant = 'field-type',
isRemoteObjectField,
to,
settingsObjectDetailTableItem,
mode,
status,
}: SettingsObjectFieldItemTableRowProps) => {
const { fieldMetadataItem, identifierType, objectMetadataItem } =
settingsObjectDetailTableItem;
const isRemoteObjectField = objectMetadataItem.isRemote;
const variant = objectMetadataItem.isCustom ? 'identifier' : 'field-type';
const navigate = useNavigate();
const theme = useTheme();
const { getIcon } = useIcons();
const Icon = getIcon(fieldMetadataItem.icon);
@ -62,31 +76,94 @@ export const SettingsObjectFieldItemTableRow = ({
const fieldType = fieldMetadataItem.type;
const isFieldTypeSupported = isFieldTypeSupportedInSettings(fieldType);
if (!isFieldTypeSupported) return null;
const RelationIcon = relationType
? RELATION_TYPES[relationType].Icon
: undefined;
const isLabelIdentifier = isLabelIdentifierField({
fieldMetadataItem,
objectMetadataItem,
});
const canToggleField = !isLabelIdentifier;
const canBeSetAsLabelIdentifier =
objectMetadataItem.isCustom &&
!isLabelIdentifier &&
LABEL_IDENTIFIER_FIELD_METADATA_TYPES.includes(fieldMetadataItem.type);
const linkToNavigate = `./${getFieldSlug(fieldMetadataItem)}`;
const {
activateMetadataField,
deactivateMetadataField,
deleteMetadataField,
} = useFieldMetadataItem();
const handleDisableField = (activeFieldMetadatItem: FieldMetadataItem) => {
deactivateMetadataField(activeFieldMetadatItem);
};
const { updateOneObjectMetadataItem } = useUpdateOneObjectMetadataItem();
const handleSetLabelIdentifierField = (
activeFieldMetadatItem: FieldMetadataItem,
) =>
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;
return (
<StyledObjectFieldTableRow to={to}>
<StyledObjectFieldTableRow
to={mode === 'view' ? linkToNavigate : undefined}
>
<StyledNameTableCell>
{!!Icon && (
<Icon size={theme.icon.size.md} stroke={theme.icon.stroke.sm} />
)}
{fieldMetadataItem.label}
</StyledNameTableCell>
<TableCell>
{variant === 'field-type' &&
(isRemoteObjectField
? 'Remote'
: fieldMetadataItem.isCustom
? 'Custom'
: 'Standard')}
{variant === 'identifier' &&
!!identifierType &&
(identifierType === 'label' ? 'Record text' : 'Record image')}
</TableCell>
<TableCell>{typeLabel}</TableCell>
<TableCell>
<SettingsObjectFieldDataType
Icon={RelationIcon}
@ -105,7 +182,48 @@ export const SettingsObjectFieldItemTableRow = ({
value={fieldType}
/>
</TableCell>
<StyledIconTableCell>{ActionIcon}</StyledIconTableCell>
<StyledIconTableCell>
{status === 'active' ? (
mode === 'view' ? (
<SettingsObjectFieldActiveActionDropdown
isCustomField={fieldMetadataItem.isCustom === true}
scopeKey={fieldMetadataItem.id}
onEdit={() => navigate(linkToNavigate)}
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}
scopeKey={fieldMetadataItem.id}
onActivate={() => activateMetadataField(fieldMetadataItem)}
onDelete={() => deleteMetadataField(fieldMetadataItem)}
/>
) : (
<LightIconButton
Icon={IconPlus}
accent="tertiary"
onClick={handleToggleField}
/>
)}
</StyledIconTableCell>
</StyledObjectFieldTableRow>
);
};

View File

@ -1,19 +1,19 @@
import { ReactNode } from 'react';
import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import { ReactNode } from 'react';
import { useIcons } from 'twenty-ui';
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords';
import { SettingsDataModelObjectTypeTag } from '@/settings/data-model/objects/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';
type SettingsObjectItemTableRowProps = {
export type SettingsObjectMetadataItemTableRowProps = {
action: ReactNode;
objectItem: ObjectMetadataItem;
to?: string;
objectMetadataItem: ObjectMetadataItem;
link?: string;
totalObjectCount: number;
};
export const StyledObjectTableRow = styled(TableRow)`
@ -30,35 +30,33 @@ const StyledActionTableCell = styled(TableCell)`
padding-right: ${({ theme }) => theme.spacing(1)};
`;
export const SettingsObjectItemTableRow = ({
export const SettingsObjectMetadataItemTableRow = ({
action,
objectItem,
to,
}: SettingsObjectItemTableRowProps) => {
objectMetadataItem,
link,
totalObjectCount,
}: SettingsObjectMetadataItemTableRowProps) => {
const theme = useTheme();
const { totalCount } = useFindManyRecords({
objectNameSingular: objectItem.nameSingular,
});
const { getIcon } = useIcons();
const Icon = getIcon(objectItem.icon);
const objectTypeLabel = getObjectTypeLabel(objectItem);
const Icon = getIcon(objectMetadataItem.icon);
const objectTypeLabel = getObjectTypeLabel(objectMetadataItem);
return (
<StyledObjectTableRow key={objectItem.namePlural} to={to}>
<StyledObjectTableRow key={objectMetadataItem.namePlural} to={link}>
<StyledNameTableCell>
{!!Icon && (
<Icon size={theme.icon.size.md} stroke={theme.icon.stroke.sm} />
)}
{objectItem.labelPlural}
{objectMetadataItem.labelPlural}
</StyledNameTableCell>
<TableCell>
<SettingsDataModelObjectTypeTag objectTypeLabel={objectTypeLabel} />
</TableCell>
<TableCell align="right">
{objectItem.fields.filter((field) => !field.isSystem).length}
{objectMetadataItem.fields.filter((field) => !field.isSystem).length}
</TableCell>
<TableCell align="right">{totalCount}</TableCell>
<TableCell align="right">{totalObjectCount}</TableCell>
<StyledActionTableCell>{action}</StyledActionTableCell>
</StyledObjectTableRow>
);

View File

@ -0,0 +1,14 @@
import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
import { createFamilyState } from '@/ui/utilities/state/utils/createFamilyState';
export type SortedFieldByTableFamilyStateKey = {
objectMetadataItemId: string;
};
export const settingsObjectFieldsFamilyState = createFamilyState<
FieldMetadataItem[] | null,
SortedFieldByTableFamilyStateKey
>({
key: 'settingsObjectFieldsFamilyState',
defaultValue: null,
});