-
Notifications
You must be signed in to change notification settings - Fork 1.5k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[Issue-5772] Add sort feature on settings tables #5787
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,105 @@ | ||
import { act, renderHook } from '@testing-library/react'; | ||
|
||
import { | ||
mockedTableData as tableData, | ||
tableDataSortedByFieldsCountInAscendingOrder, | ||
tableDataSortedByFieldsCountInDescendingOrder, | ||
tableDataSortedBylabelInAscendingOrder, | ||
tableDataSortedBylabelInDescendingOrder, | ||
} from '~/testing/mock-data/tableData'; | ||
|
||
import { useTableSort } from '../useTableSort'; | ||
|
||
describe('useTableSort hook', () => { | ||
test('initial sorting behavior for string fields', () => { | ||
const { result } = renderHook(() => useTableSort('labelPlural', tableData)); | ||
const [sortedData, , sortConfig] = result.current; | ||
|
||
expect(JSON.stringify(sortedData)).toBe( | ||
JSON.stringify(tableDataSortedBylabelInAscendingOrder), | ||
); | ||
expect(sortConfig).toEqual({ | ||
sortByColumnKey: 'labelPlural', | ||
sortOrder: 'ascending', | ||
}); | ||
}); | ||
|
||
test('initial sorting behavior for number fields', () => { | ||
const { result } = renderHook(() => useTableSort('fieldsCount', tableData)); | ||
const [sortedData, , sortConfig] = result.current; | ||
|
||
expect(JSON.stringify(sortedData)).toBe( | ||
JSON.stringify(tableDataSortedByFieldsCountInAscendingOrder), | ||
); | ||
expect(sortConfig).toEqual({ | ||
sortByColumnKey: 'fieldsCount', | ||
sortOrder: 'ascending', | ||
}); | ||
}); | ||
|
||
test('sorting behavior when clicking on string column header', () => { | ||
const { result } = renderHook(() => useTableSort('fieldsCount', tableData)); | ||
const [, handleSortClick] = result.current; | ||
|
||
act(() => { | ||
handleSortClick('labelPlural'); | ||
}); | ||
|
||
let [sortedData, , sortConfig] = result.current; | ||
|
||
expect(JSON.stringify(sortedData)).toBe( | ||
JSON.stringify(tableDataSortedBylabelInAscendingOrder), | ||
); | ||
expect(sortConfig).toEqual({ | ||
sortByColumnKey: 'labelPlural', | ||
sortOrder: 'ascending', | ||
}); | ||
|
||
act(() => { | ||
handleSortClick('labelPlural'); | ||
}); | ||
|
||
[sortedData, , sortConfig] = result.current; | ||
|
||
expect(JSON.stringify(sortedData)).toBe( | ||
JSON.stringify(tableDataSortedBylabelInDescendingOrder), | ||
); | ||
expect(sortConfig).toEqual({ | ||
sortByColumnKey: 'labelPlural', | ||
sortOrder: 'descending', | ||
}); | ||
}); | ||
|
||
test('sorting behavior when clicking on number column header', () => { | ||
const { result } = renderHook(() => useTableSort('labelPlural', tableData)); | ||
const [, handleSortClick] = result.current; | ||
|
||
act(() => { | ||
handleSortClick('fieldsCount'); | ||
}); | ||
|
||
let [sortedData, , sortConfig] = result.current; | ||
|
||
expect(JSON.stringify(sortedData)).toBe( | ||
JSON.stringify(tableDataSortedByFieldsCountInAscendingOrder), | ||
); | ||
expect(sortConfig).toEqual({ | ||
sortByColumnKey: 'fieldsCount', | ||
sortOrder: 'ascending', | ||
}); | ||
|
||
act(() => { | ||
handleSortClick('fieldsCount'); | ||
}); | ||
|
||
[sortedData, , sortConfig] = result.current; | ||
|
||
expect(JSON.stringify(sortedData)).toBe( | ||
JSON.stringify(tableDataSortedByFieldsCountInDescendingOrder), | ||
); | ||
expect(sortConfig).toEqual({ | ||
sortByColumnKey: 'fieldsCount', | ||
sortOrder: 'descending', | ||
}); | ||
}); | ||
}); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,62 @@ | ||
import { useState } from 'react'; | ||
|
||
enum sortOrders { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We already have |
||
ascending = 'ascending', | ||
descending = 'descending', | ||
} | ||
type SortConfig<T> = { | ||
sortByColumnKey: keyof T; | ||
sortOrder: sortOrders; | ||
}; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Consider renaming |
||
|
||
export const useTableSort = <T>( | ||
initialColumnKey: keyof T, | ||
tableData: T[], | ||
): [T[], (sortKey: keyof T) => void, SortConfig<T>] => { | ||
const [sortConfig, setSortConfig] = useState<SortConfig<T>>({ | ||
sortByColumnKey: initialColumnKey, | ||
sortOrder: sortOrders.ascending, | ||
}); | ||
|
||
const sortTableData = ( | ||
data: T[], | ||
columnKey: keyof T, | ||
order: sortOrders, | ||
): T[] => { | ||
return data.sort((a: T, b: T) => { | ||
if (typeof a[columnKey] === 'string') { | ||
return order === sortOrders.ascending | ||
? (a[columnKey] as string).localeCompare(b[columnKey] as string) | ||
: (b[columnKey] as string).localeCompare(a[columnKey] as string); | ||
} else if (typeof a[columnKey] === 'number') { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Handle cases where |
||
return order === sortOrders.ascending | ||
? (a[columnKey] as number) - (b[columnKey] as number) | ||
: (b[columnKey] as number) - (a[columnKey] as number); | ||
} else return 0; | ||
}); | ||
}; | ||
|
||
const sortedTableData = sortTableData( | ||
[...tableData], | ||
sortConfig.sortByColumnKey, | ||
sortConfig.sortOrder, | ||
); | ||
|
||
const toggleSortOrder = (sortOrder: sortOrders) => { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This function isn't toggling, it's just returning the opposite of the passed sortOrder, so it should either be renamed or either really toggling the state |
||
return sortOrder === sortOrders.ascending | ||
? sortOrders.descending | ||
: sortOrders.ascending; | ||
}; | ||
|
||
const handleSortClick = (columnKey: keyof T) => { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Handlers should be the scope of a component, not of a hook |
||
setSortConfig((state) => ({ | ||
sortByColumnKey: columnKey, | ||
sortOrder: | ||
state.sortByColumnKey === columnKey | ||
? toggleSortOrder(state.sortOrder) | ||
: sortOrders.ascending, | ||
})); | ||
}; | ||
|
||
return [sortedTableData, handleSortClick, sortConfig]; | ||
}; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,4 +1,4 @@ | ||
import { ReactNode, useMemo } from 'react'; | ||
import { ReactNode, useEffect, useMemo } from 'react'; | ||
import { useTheme } from '@emotion/react'; | ||
import styled from '@emotion/styled'; | ||
import { Nullable, useIcons } from 'twenty-ui'; | ||
|
@@ -7,6 +7,7 @@ import { useGetRelationMetadata } from '@/object-metadata/hooks/useGetRelationMe | |
import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem'; | ||
import { getObjectSlug } from '@/object-metadata/utils/getObjectSlug'; | ||
import { FieldIdentifierType } from '@/settings/data-model/types/FieldIdentifierType'; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ensure |
||
import { getSettingsFieldTypeConfig } from '@/settings/data-model/utils/getSettingsFieldTypeConfig'; | ||
import { isFieldTypeSupportedInSettings } from '@/settings/data-model/utils/isFieldTypeSupportedInSettings'; | ||
import { TableCell } from '@/ui/layout/table/components/TableCell'; | ||
import { TableRow } from '@/ui/layout/table/components/TableRow'; | ||
|
@@ -17,11 +18,12 @@ import { SettingsObjectFieldDataType } from './SettingsObjectFieldDataType'; | |
|
||
type SettingsObjectFieldItemTableRowProps = { | ||
ActionIcon: ReactNode; | ||
fieldMetadataItem: FieldMetadataItem; | ||
fieldMetadataItem: FieldMetadataItem & { fieldType: string | boolean }; | ||
identifierType?: Nullable<FieldIdentifierType>; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. fieldType is redundant, we already have a type property on fieldMetadataItem which should be used. |
||
variant?: 'field-type' | 'identifier'; | ||
isRemoteObjectField?: boolean; | ||
to?: string; | ||
updateDataTypes: (id: string, label?: string) => void; | ||
}; | ||
|
||
export const StyledObjectFieldTableRow = styled(TableRow)` | ||
|
@@ -41,10 +43,8 @@ const StyledIconTableCell = styled(TableCell)` | |
export const SettingsObjectFieldItemTableRow = ({ | ||
ActionIcon, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Consider adding a default value or error handling for |
||
fieldMetadataItem, | ||
identifierType, | ||
variant = 'field-type', | ||
isRemoteObjectField, | ||
to, | ||
updateDataTypes, | ||
}: SettingsObjectFieldItemTableRowProps) => { | ||
const theme = useTheme(); | ||
const { getIcon } = useIcons(); | ||
|
@@ -60,6 +60,18 @@ export const SettingsObjectFieldItemTableRow = ({ | |
|
||
const fieldType = fieldMetadataItem.type; | ||
const isFieldTypeSupported = isFieldTypeSupportedInSettings(fieldType); | ||
useEffect(() => { | ||
const fieldTypeConfig = getSettingsFieldTypeConfig(fieldMetadataItem.type); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This logic is not necessary because we already have a fieldMetadataItem array higher up in the tree, the row shouldn't be related to this feature. |
||
const label = | ||
relationObjectMetadataItem?.labelPlural ?? fieldTypeConfig?.label; | ||
//Hoist data types to parent component state to determine sort order | ||
updateDataTypes(fieldMetadataItem.id, label); | ||
}, [ | ||
fieldMetadataItem.id, | ||
fieldMetadataItem.type, | ||
relationObjectMetadataItem, | ||
updateDataTypes, | ||
]); | ||
|
||
if (!isFieldTypeSupported) return null; | ||
|
||
|
@@ -75,17 +87,7 @@ export const SettingsObjectFieldItemTableRow = ({ | |
)} | ||
{fieldMetadataItem.label} | ||
</StyledNameTableCell> | ||
<TableCell> | ||
{variant === 'field-type' && | ||
(isRemoteObjectField | ||
? 'Remote' | ||
: fieldMetadataItem.isCustom | ||
? 'Custom' | ||
: 'Standard')} | ||
{variant === 'identifier' && | ||
!!identifierType && | ||
(identifierType === 'label' ? 'Record text' : 'Record image')} | ||
</TableCell> | ||
<TableCell>{fieldMetadataItem.fieldType}</TableCell> | ||
<TableCell> | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We should be keep the derived state here, not adding a new field on fieldMetadataItem, if you need to share it with another place just create a util like getFieldMetadataItemTypeLabel(fieldMetadataItem) |
||
<SettingsObjectFieldDataType | ||
Icon={RelationIcon} | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,19 +1,27 @@ | ||
import { ReactNode } from 'react'; | ||
import { ReactNode, useEffect } from 'react'; | ||
import { useTheme } from '@emotion/react'; | ||
import styled from '@emotion/styled'; | ||
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 { | ||
getObjectTypeLabel, | ||
ObjectTypeLabel, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ensure |
||
} 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 SettingsObjectItemTableRowProps = { | ||
action: ReactNode; | ||
objectItem: ObjectMetadataItem; | ||
objectItem: ObjectMetadataItem & { | ||
objectTypeLabelText: ObjectTypeLabel['labelText']; | ||
fieldsCount: number; | ||
instancesCount: number; | ||
}; | ||
to?: string; | ||
updateInstanceCount: (id: string, instanceCount: number) => void; | ||
}; | ||
|
||
export const StyledObjectTableRow = styled(TableRow)` | ||
|
@@ -34,12 +42,18 @@ export const SettingsObjectItemTableRow = ({ | |
action, | ||
objectItem, | ||
to, | ||
updateInstanceCount, | ||
}: SettingsObjectItemTableRowProps) => { | ||
const theme = useTheme(); | ||
|
||
const { totalCount } = useFindManyRecords({ | ||
objectNameSingular: objectItem.nameSingular, | ||
}); | ||
useEffect(() => { | ||
//Hoist instance count to parent component state to determine sort order | ||
updateInstanceCount(objectItem.id, totalCount || 0); | ||
}, [objectItem.id, totalCount, updateInstanceCount]); | ||
|
||
const { getIcon } = useIcons(); | ||
const Icon = getIcon(objectItem.icon); | ||
const objectTypeLabel = getObjectTypeLabel(objectItem); | ||
|
@@ -55,9 +69,7 @@ export const SettingsObjectItemTableRow = ({ | |
<TableCell> | ||
<SettingsDataModelObjectTypeTag objectTypeLabel={objectTypeLabel} /> | ||
</TableCell> | ||
<TableCell align="right"> | ||
{objectItem.fields.filter((field) => !field.isSystem).length} | ||
</TableCell> | ||
<TableCell align="right">{objectItem.fieldsCount}</TableCell> | ||
<TableCell align="right">{totalCount}</TableCell> | ||
<StyledActionTableCell>{action}</StyledActionTableCell> | ||
</StyledObjectTableRow> | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Use
toEqual
instead oftoBe
for comparing objects or arrays to avoid potential issues with reference equality.