Skip to content

Conversation

@harshit078
Copy link
Contributor

@harshit078 harshit078 commented Nov 15, 2025

Description

@harshit078 harshit078 changed the title feat: introduced mergePreview state and updated feat: resolve preview merge record uses a fake one Nov 15, 2025
@github-actions
Copy link
Contributor

github-actions bot commented Nov 15, 2025

🚀 Preview Environment Ready!

Your preview environment is available at: http://bore.pub:60847

This environment will automatically shut down when the PR is closed or after 5 hours.

@FelixMalfait FelixMalfait requested a review from neo773 November 15, 2025 19:27
@harshit078 harshit078 marked this pull request as ready for review November 17, 2025 07:31
@greptile-apps
Copy link
Contributor

greptile-apps bot commented Nov 17, 2025

Greptile Summary

  • Introduces mergePreviewRecordFamilyState to store merge preview records separately from main record store, preventing pollution of recordStoreFamilyState
  • Updates record store selectors to check preview state first before falling back to main store for seamless preview display
  • Implements race condition handling with runId tracking, though the implementation has a critical stale closure bug

Confidence Score: 2/5

  • This PR has a critical race condition bug that will cause the protection logic to fail
  • The race condition handling logic in useMergePreview.ts has a stale closure bug where currentRunId references the old value from the effect closure, making all race condition checks ineffective. This defeats the purpose of the race condition protection.
  • Pay close attention to packages/twenty-front/src/modules/object-record/record-merge/hooks/useMergePreview.ts - the race condition logic needs to be fixed using useRef

Important Files Changed

Filename Overview
packages/twenty-front/src/modules/object-record/record-merge/hooks/useMergePreview.ts Added race condition handling with runId tracking, but implementation has stale closure bug that breaks the race condition checks

Sequence Diagram

sequenceDiagram
    participant User
    participant useMergePreview
    participant mergePreviewRecordFamilyState
    participant useMergeManyRecords
    participant Backend
    participant recordStoreSelectors

    User->>useMergePreview: "selects records to merge"
    useMergePreview->>useMergePreview: "computes previewSignature"
    useMergePreview->>useMergeManyRecords: "mergeManyRecords(preview: true)"
    useMergeManyRecords->>Backend: "mutation with dryRun: true, no-cache"
    Backend-->>useMergeManyRecords: "returns preview record"
    useMergeManyRecords-->>useMergePreview: "returns preview record"
    useMergePreview->>mergePreviewRecordFamilyState: "stores preview record"
    User->>recordStoreSelectors: "views merged preview"
    recordStoreSelectors->>mergePreviewRecordFamilyState: "checks for preview record first"
    mergePreviewRecordFamilyState-->>recordStoreSelectors: "returns preview data"
    recordStoreSelectors-->>User: "displays preview"
    User->>useMergeManyRecords: "confirms merge"
    useMergeManyRecords->>Backend: "mutation with dryRun: false"
    Backend-->>useMergeManyRecords: "returns merged record"
    useMergeManyRecords->>useMergeManyRecords: "triggers refetchQueries"
Loading

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.

7 files reviewed, 2 comments

Edit Code Review Agent Settings | Greptile
React with 👍 or 👎 to share your feedback on this new summary format

@lucasbordeau lucasbordeau self-requested a review November 17, 2025 09:18
@charlesBochet charlesBochet self-assigned this Nov 18, 2025
@lucasbordeau lucasbordeau removed their request for review November 19, 2025 08:50
const [lastPreviewId, setLastPreviewId] = useState<string>('');
const [lastPreviewSignature, setLastPreviewSignature] = useState<string>('');
// eslint-disable-next-line @nx/workspace-no-state-useref
const currentRunIdRef = useRef(0);
Copy link
Member

Choose a reason for hiding this comment

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

@harshit078 we don't use useRef in the project

commandMenuNavigationMorphItemsByPage
.get(CommandMenuPages.MergeRecords)
?.map((morphItem) => morphItem.recordId) ?? [];
const selectedRecordIds = useMemo(
Copy link
Member

Choose a reason for hiding this comment

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

useMemo does not seems necessary here?

({ set }) =>
(record: ObjectRecord | null, previousId: string) => {
if (previousId !== '') {
set(mergePreviewRecordFamilyState(previousId), null);
Copy link
Member

Choose a reason for hiding this comment

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

I'm not sure about this, this does not seem robust, why do we need to clean the state?

if (previousId !== '') {
set(mergePreviewRecordFamilyState(previousId), null);
}
if (record !== null) {
Copy link
Member

Choose a reason for hiding this comment

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

use isDefined, isNonEmptyString helpers

useEffect(() => {
const fetchPreview = async () => {
if (selectedRecords.length < 2 || isMergeInProgress || isInitialized)
if (selectedRecordIds.length < 2 || isMergeInProgress) {
Copy link
Member

Choose a reason for hiding this comment

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

this code is not robust IMO, isInitialized seemed right. What issue are we trying to solve?

Copy link
Member

@charlesBochet charlesBochet left a comment

Choose a reason for hiding this comment

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

@harshit078 ty

Feedbacks:

  • let's not use useRef. What we should aim for: hooks that are called synchronously. Here, I think we should add records to the mergePreviewFamilyState in useOpenMergeRecordsPageInCommandMenu for instance. this would remove the need for useRef, useEffect, useMemo and all other hacks.

I think we should not try to clean the mergePreviewFamilyState, it does not worth it.

Also, I'm not convinced by the fact that recordStore is now loading data from two different sources but why not, it does not harm too much

@harshit078
Copy link
Contributor Author

@charlesBochet Hi charles, thanks for the comments. I am super aware that Twenty ooesnt use useRef but I couldnt find another approach round it which would pass lint test and was ideal. I'll address the comments and refactor the approach thanks !

Copilot AI review requested due to automatic review settings November 20, 2025 12:19
Copilot finished reviewing on behalf of harshit078 November 20, 2025 12:21
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull Request Overview

This PR introduces a dedicated Recoil atom mergePreviewRecordFamilyState to store merge preview records separately from the main record store, preventing pollution of recordStoreFamilyState during merge operations.

Key Changes:

  • Created mergePreviewRecordFamilyState to store preview records independently
  • Updated record store selectors to check preview state before falling back to main store
  • Refactored useMergePreview hook to manage preview records in the new state and handle race conditions with run IDs

Reviewed Changes

Copilot reviewed 8 out of 8 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
mergePreviewRecordFamilyState.ts New Recoil family state atom for storing merge preview records
recordStoreFamilySelector.ts Updated to read from preview state first, then fall back to main store
recordStoreIdentifierSelector.ts Added preview record check before reading from main store
recordStoreIdentifiersSelector.ts Added preview record check in bulk identifier retrieval
recordStoreFieldValueSelector.ts Enhanced to prioritize preview records for both regular and morph relation fields
useMergePreview.ts Refactored to use new preview state with race condition handling via run IDs
useMergeManyRecords.ts Improved conditional logic for preview vs. non-preview query options
RecordTitleCellTextFieldDisplay.tsx Changed to use selector-based field value retrieval instead of direct record access
Comments suppressed due to low confidence (1)

packages/twenty-front/src/modules/object-record/record-store/states/selectors/recordStoreFamilySelector.ts:23

  • The set method of this selector doesn't handle preview records. When a field value is set, it always writes to recordStoreFamilyState even if the current value is being read from a preview record in mergePreviewRecordFamilyState. This could cause inconsistent behavior where reads come from the preview but writes go to the main store.

Consider adding logic to the set method to handle preview records appropriately, or document that this selector is read-only for preview records.

  set:
    <T>({ fieldName, recordId }: { fieldName: string; recordId: string }) =>
    ({ set }, newValue: T) =>
      set(recordStoreFamilyState(recordId), (prevState) =>

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

mergeSettings,
setPreviewRecordById,
lastPreviewId,
lastPreviewSignature,
Copy link

Copilot AI Nov 20, 2025

Choose a reason for hiding this comment

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

[nitpick] The dependency array includes lastPreviewSignature which is compared inside the effect on line 80. This creates a circular dependency: the effect updates lastPreviewSignature, which triggers the effect again. However, the early return on line 80-82 prevents infinite loops. While this works, it's an unconventional pattern. Consider restructuring to avoid having state that's both read and written in the same effect appear in the dependency array.

Copilot uses AI. Check for mistakes.

const isEmpty =
recordValue?.[fieldDefinition.metadata.fieldName]?.trim() === '';
const isEmpty = fieldValue?.toString().trim() === '';
Copy link

Copilot AI Nov 20, 2025

Choose a reason for hiding this comment

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

Using .toString() on fieldValue could produce unexpected results for complex field types (objects, arrays, null). For example, objects would display as [object Object]. Consider type checking and handling different field types appropriately, or use the original field value access pattern with proper typing.

Copilot uses AI. Check for mistakes.
recordValue?.[fieldDefinition.metadata.fieldName] ||
fieldDefinition.label
}
text={fieldValue?.toString() || fieldDefinition.label}
Copy link

Copilot AI Nov 20, 2025

Choose a reason for hiding this comment

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

Similar to line 48, using .toString() on fieldValue can produce unexpected results for non-string field types. Objects would display as [object Object]. Consider ensuring the field value is properly typed as a string or handle different types appropriately.

Copilot uses AI. Check for mistakes.
}, [
selectedRecords,
mergeSettings,
previewSignature,
Copy link

Copilot AI Nov 20, 2025

Choose a reason for hiding this comment

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

The previewSignature is included in the dependency array but uses values that are already in the array (selectedRecordIds and mergeSettings.conflictPriorityIndex). Since previewSignature is derived from these values using useMemo, including it creates redundant dependencies. Remove previewSignature from the dependency array and keep only the primitive dependencies.

Suggested change
previewSignature,

Copilot uses AI. Check for mistakes.
Comment on lines +25 to +26
// eslint-disable-next-line @nx/workspace-no-state-useref
const currentRunIdRef = useRef(0);
Copy link

Copilot AI Nov 20, 2025

Choose a reason for hiding this comment

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

[nitpick] The comment eslint-disable-next-line @nx/workspace-no-state-useref suggests that using useRef here might violate a workspace rule. While the use of useRef for tracking run IDs is appropriate to prevent race conditions, consider if there's an alternative pattern that complies with your workspace rules or document why this exception is necessary.

Copilot uses AI. Check for mistakes.
@charlesBochet
Copy link
Member

@harshit078 I'm going to close it for now, let's re-open when ready

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.

2 participants