Skip to content
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

Open
wants to merge 7 commits into
base: main
Choose a base branch
from
Open

Conversation

magrinj
Copy link
Member

@magrinj magrinj commented Nov 8, 2024

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.

Copy link

github-actions bot commented Nov 8, 2024

TODOs/FIXMEs:

  • // TODO: Hack to store all ids in the same order as the record group definitions: packages/twenty-front/src/modules/object-record/record-table/hooks/internal/useSetRecordTableData.ts

Generated by 🚫 dangerJS against a79f9ab

@magrinj magrinj marked this pull request as ready for review November 8, 2024 09:43
Copy link
Contributor

@greptile-apps greptile-apps bot left a 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

Comment on lines +27 to +34
{visibleRecordGroups.map((recordGroup) => (
<RecordGroupContext.Provider
key={recordGroup.id}
value={{ recordGroupId: recordGroup.id }}
>
{children}
</RecordGroupContext.Provider>
))}
Copy link
Contributor

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' }}>
Copy link
Contributor

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'

Comment on lines +13 to +15
const { visibleRecordGroups } = useRecordGroups({
objectMetadataNameSingular,
});
Copy link
Contributor

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

const softFocusPosition = getSnapshotValue(
snapshot,
softFocusPositionState,
);

let newRowNumber = softFocusPosition.row - 1;
const currentRowIndex = allRowIds.indexOf(softFocusPosition.recordId);
Copy link
Contributor

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

Comment on lines +28 to 32
const allRowIds = getSnapshotValue(snapshot, tableAllRowIdsState);
const softFocusPosition = getSnapshotValue(
snapshot,
softFocusPositionState,
);
Copy link
Contributor

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);
Copy link
Contributor

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);
Copy link
Contributor

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'

Comment on lines 28 to 29
describe('useSelectedTableCellEditMode', () => {
it('should have property setSelectedTableCellEditMode', async () => {
Copy link
Contributor

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

@@ -30,7 +30,7 @@ export const useRecordGroupActions = () => {
});

const { viewGroupFieldMetadataItem } = useRecordGroups({
objectNameSingular,
objectMetadataNameSingular: objectNameSingular,
Copy link
Contributor

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;
Copy link
Contributor

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.

Copy link
Contributor

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
Copy link
Contributor

@lucasbordeau lucasbordeau Nov 12, 2024

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[] = [];
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Works for me

Copy link
Contributor

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants