-
Notifications
You must be signed in to change notification settings - Fork 2.2k
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
feat: default record group table #8397
base: main
Are you sure you want to change the base?
Conversation
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.
PR Summary
This PR introduces record group functionality to the table view, enabling better organization and management of records. The changes focus on state management and data structure improvements.
- Added
RecordGroupContextProvider
in/packages/twenty-front/src/modules/object-record/record-group/components/RecordGroupContextProvider.tsx
to manage record groups and provide group context to child components - Refactored row identification from numeric indices to record IDs in
/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/hooks/useSelectedTableCellEditMode.ts
for more stable cell referencing - Introduced dual state management for row IDs in
/packages/twenty-front/src/modules/object-record/record-table/hooks/internal/useSetRecordTableData.ts
- one for group-specific IDs and another for all row IDs - Added group-based filtering in
/packages/twenty-front/src/modules/object-record/record-index/hooks/useLoadRecordIndexTable.ts
to fetch records based on current record group definition - Implemented record group reordering functionality in
/packages/twenty-front/src/modules/object-record/record-group/hooks/useRecordGroupReorder.ts
with drag-and-drop support
30 file(s) reviewed, 22 comment(s)
Edit PR Review Bot Settings | Greptile
{visibleRecordGroups.map((recordGroup) => ( | ||
<RecordGroupContext.Provider | ||
key={recordGroup.id} | ||
value={{ recordGroupId: recordGroup.id }} | ||
> | ||
{children} | ||
</RecordGroupContext.Provider> | ||
))} |
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.
logic: Rendering children multiple times (once per group) could cause performance issues with large child trees. Consider restructuring to avoid duplicating the entire child component tree for each group.
|
||
if (visibleRecordGroups.length === 0) { | ||
return ( | ||
<RecordGroupContext.Provider value={{ recordGroupId: 'default' }}> |
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.
style: Consider using recordGroupDefaultId constant from RecordGroupDefinition.ts instead of hardcoding 'default'
const { visibleRecordGroups } = useRecordGroups({ | ||
objectMetadataNameSingular, | ||
}); |
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.
style: Destructuring only visibleRecordGroups but not handling loading states could cause flash of incorrect content
...twenty-front/src/modules/object-record/record-group/hooks/useCurrentRecordGroupDefinition.ts
Show resolved
Hide resolved
packages/twenty-front/src/modules/object-record/record-group/hooks/useCurrentRecordGroupId.ts
Show resolved
Hide resolved
const softFocusPosition = getSnapshotValue( | ||
snapshot, | ||
softFocusPositionState, | ||
); | ||
|
||
let newRowNumber = softFocusPosition.row - 1; | ||
const currentRowIndex = allRowIds.indexOf(softFocusPosition.recordId); |
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.
logic: indexOf could return -1 if recordId not found in allRowIds, causing focus issues
const allRowIds = getSnapshotValue(snapshot, tableAllRowIdsState); | ||
const softFocusPosition = getSnapshotValue( | ||
snapshot, | ||
softFocusPositionState, | ||
); |
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.
style: check if allRowIds is empty array before proceeding to avoid undefined access
setSoftFocusPosition, | ||
], | ||
); | ||
|
||
const moveLeft = useRecoilCallback( | ||
({ snapshot }) => | ||
() => { | ||
const rowIds = getSnapshotValue(snapshot, tableAllRowIdsState); |
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.
style: variable name 'rowIds' differs from 'allRowIds' used elsewhere - should be consistent
@@ -22,6 +23,8 @@ export const RecordTableBodyEffect = () => { | |||
|
|||
const [hasInitializedScroll, setHasInitiazedScroll] = useState(false); |
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.
syntax: 'setHasInitiazedScroll' contains a typo in 'Initiazed'
describe('useSelectedTableCellEditMode', () => { | ||
it('should have property setSelectedTableCellEditMode', async () => { |
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.
style: test name 'should have property setSelectedTableCellEditMode' does not match what is actually being tested - the test verifies the behavior of the function, not just its existence
a906d45
to
a79f9ab
Compare
@@ -30,7 +30,7 @@ export const useRecordGroupActions = () => { | |||
}); | |||
|
|||
const { viewGroupFieldMetadataItem } = useRecordGroups({ | |||
objectNameSingular, | |||
objectMetadataNameSingular: objectNameSingular, |
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.
We use objectNameSingular everywhere as a convention
|
||
export type RecordGroupDefinitionId = | ||
| RecordGroupDefinition['id'] | ||
| typeof recordGroupDefaultId; |
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.
I think we can find a better way to encode the "no record group" case, the most indicated way would be to use a different part of the tree when there is no record group, while re-using common components and logic.
Instead of checking if id === "default", we could have a recoil state / selector hasRecordGroups, and use this state higher up in the hierarchy to decide between for example : RecordTableBodyWithoutGroupEffect and recordGroup.map( => RecordTableBodyGroupEffect.
So general rule of thumb : let's try to create two parts of the tree, where it's needed, for the no group case and the multiple groups case, and have it very visible and high in the tree with a clear state that indicates what we mean, because those part of the codebase are very sensitive to technical debt and we should double the amount of effort to make it crystal clear.
Rule of thumb : it's easy to create components and states to do explicit conditional rendering and then re-use common logic inside them, so let's abuse it.
I'm pointing to an article about that, which @Devessier also shared recently and that is very on point here : https://kentcdodds.com/blog/aha-programming
It's better to have some duplicated code that we don't know how to factorize yet, but that is very clear, simple and explicit, instead of the opposite.
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.
Thanks for mentioning @lucasbordeau! Yeah, knowing when to create abstractions is a tricky topic.
@@ -82,7 +83,11 @@ export const RecordTable = ({ | |||
recordTableId={recordTableId} | |||
viewBarId={viewBarId} | |||
> | |||
<RecordTableBodyEffect /> | |||
<RecordGroupContextProvider |
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.
This pattern is never used in the codebase (and shouldn't if that's the case), we shouldn't abstract a .map in a component that will loop on its children. The preferred way is to be explicit about the fact that a component is displaying multiple items. Children shouldn't be transcluded like a parameter. Instead we should use a component like RecordTableGroupBodyEffects
, that will loop through what need to be displayed as children.
Component in the codebase can and should also be used to indicate meaning more than pure functionality, this way our JSX tree is very explicit about what is done.
For exemple here it could be :
RecordTableGroupBodyEffects
RecordTableBodyEffect
And we could use conditional rendering between the two at this level.
Another problem here is that the ContextProvider
suffix should be used only to abstract some logic that will be put into the Context + the Context.Provider wrapping.
Here it hides a conditional rendering + a .map which should be made explicit at that level of the tree.
Also I'm not sure that we should put a RecordGroupContext if there's no actual record group, that would not be explicit and meaningful, and could confuse further feature development.
Thus having the tree splitting in two here, or even higher RecordTableWithoutGroup
and RecordTableWithGroups
(maybe too high but worth considering) ?
Also it's obvious after reading this PR that we should use a recoil state to know explicitly if we're in the case of having record groups or not, instead of encoding it in different places.
Trying to mix the no group case into the multiple group case with magic ids and ifs will create technical debt in the long run IMHO.
I'm not sure about how it will look like exactly though, but you get the idea here.
if (visibleRecordGroupDefinitions.length !== 0) { | ||
// TODO: Hack to store all ids in the same order as the record group definitions | ||
// Should be replaced by something more efficient | ||
const allRowIds: string[] = []; |
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.
Works for me
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.
That works for me, using the record id instead of numbers is a good way to go here.
This PR is preparing states to we'll be able to handle view groups correctly with table data.
RowIds are now stores in 2 component states, one storing ids by view group and another storing all the rowIds.
We're doing that because some other state like focus, or select must not be scoped by view group id.