[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:
committed by
GitHub
parent
0f75e14ab2
commit
59e14fabb4
@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@ -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>
|
||||
);
|
||||
|
||||
@ -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,
|
||||
});
|
||||
Reference in New Issue
Block a user