947a4d5253
Fixes: #21243 # Issue When a user is on a specific record's page (for example, looking at a Person) and opens a related record (like a Note) in the right-side panel, clicking "Permanently Delete" on that Note would abruptly redirect the user to the main "Notes" list. This breaks the user's workflow, as they typically want to remain on the parent Person page after deleting a sub-record. # Root Cause Inside the `DestroyRecordsCommand` and `DeleteRecordsCommand` components, the application was programmed to unconditionally trigger a `navigateApp` redirect to the deleted object's index page upon successful deletion. The code did not account for whether the deletion was triggered from the main index page or from inside a contextual side panel. # How we fixed it I introduced a new `isInSidePanel` flag into the `HeadlessCommandContextApi`. The command menu now detects if the delete action originated from inside a side panel and passes this flag down the execution chain. If `true`, the `DestroyRecordsCommand` simply closes the panel (`closeSidePanelMenu()`) and keeps the user exactly where they were. ## After that fix, I encountered another issue (UI not updating automatically) Because the app now correctly kept the user on the Person page, a new bug surfaced: the "Note" chip inside the relation table did not disappear immediately. The user had to manually refresh the page to see the deletion. This happened because: 1. The Apollo optimistic cache occasionally failed to trace deeply nested morph-relationships back to the parent. 2. A standard local React `useState` fallback was insufficient. The table component aggressively unmounts and remounts relation cells whenever a user hovers over them to display interactive controls, which would wipe the local React state clean and cause the "ghost chip" to reappear. ##How I fixed that issue (and optimized it) I built an event-driven fallback using global Jotai state to permanently hide the chips: 1. **Precision ID Broadcasting**: I updated the `useDestroyManyRecords` and `useIncrementalDestroyManyRecords` hooks to extract and broadcast only the *confirmed* destroyed IDs directly from the Apollo mutation response, preventing false-positive UI removals if the backend performed a partial delete. 2. **Batch Optimizations**: During bulk deletions, events are now broadcasted per-batch rather than accumulating thousands of IDs in memory until the end, keeping the UI instantly responsive and memory bounded. 3. **Per-Cell State Scoping (`atomFamily`)**: Instead of a leaky global array, we used Jotai's `atomFamily` to dynamically generate a unique Noticeboard for every specific table cell (`${recordId}-${fieldName}`). This ensures the hidden-state survives mouse-hover unmounts while remaining cleanly isolated. 4. **Defensive Filtering**: The `RelationFromManyFieldDisplay` component was updated to defensively guard against `undefined` array entries and strictly match deleted IDs only against valid foreign keys (ending in `'Id'`), preventing false-positive removals if a UUID happened to be pasted into a description field. 5. **SSE Resilience**: We hardened the Server-Sent Event (SSE) listeners with optional chaining and null-filtering to ensure malformed backend payloads do not crash the real-time event pipeline. # Screen Recording https://github.com/user-attachments/assets/19fc8a3f-ba28-43c2-b1f3-a91127cdad97 --------- Co-authored-by: bosiraphael <raphael.bosi@gmail.com> Co-authored-by: Raphaël Bosi <71827178+bosiraphael@users.noreply.github.com>
86 lines
2.7 KiB
TypeScript
86 lines
2.7 KiB
TypeScript
import { useCallback } from 'react';
|
|
|
|
import { useEnrichHeadlessCommandContextApiWithWorkflowVersionTriggerInformation } from '@/command-menu-item/engine-command/hooks/useEnrichHeadlessCommandContextApiWithWorkflowVersionTriggerInformation';
|
|
import { headlessCommandContextApisState } from '@/command-menu-item/engine-command/states/headlessCommandContextApisState';
|
|
import { buildHeadlessCommandContextApi } from '@/command-menu-item/engine-command/utils/buildHeadlessCommandContextApi';
|
|
import { useStore } from 'jotai';
|
|
import { isDefined } from 'twenty-shared/utils';
|
|
import {
|
|
type CommandMenuItemAvailabilityType,
|
|
type EngineComponentKey,
|
|
type CommandMenuItemPayload,
|
|
} from '~/generated-metadata/graphql';
|
|
|
|
type MountCommandParams = {
|
|
engineCommandId: string;
|
|
contextStoreInstanceId: string;
|
|
engineComponentKey: EngineComponentKey;
|
|
frontComponentId?: string;
|
|
workflowVersionId?: string;
|
|
availabilityType?: CommandMenuItemAvailabilityType;
|
|
availabilityObjectMetadataId?: string | null;
|
|
payload?: CommandMenuItemPayload | null;
|
|
isInSidePanel?: boolean;
|
|
};
|
|
|
|
export const useMountCommand = () => {
|
|
const store = useStore();
|
|
|
|
const {
|
|
enrichHeadlessCommandContextApiWithWorkflowVersionTriggerInformation,
|
|
} = useEnrichHeadlessCommandContextApiWithWorkflowVersionTriggerInformation();
|
|
|
|
const mountCommand = useCallback(
|
|
async ({
|
|
engineCommandId,
|
|
contextStoreInstanceId,
|
|
engineComponentKey,
|
|
frontComponentId,
|
|
workflowVersionId,
|
|
availabilityType,
|
|
availabilityObjectMetadataId,
|
|
payload,
|
|
isInSidePanel,
|
|
}: MountCommandParams) => {
|
|
const headlessEngineCommandContextApi = buildHeadlessCommandContextApi({
|
|
store,
|
|
contextStoreInstanceId,
|
|
engineComponentKey,
|
|
payload,
|
|
isInSidePanel,
|
|
});
|
|
|
|
const commandState = isDefined(frontComponentId)
|
|
? { ...headlessEngineCommandContextApi, frontComponentId }
|
|
: isDefined(workflowVersionId) && isDefined(availabilityType)
|
|
? await enrichHeadlessCommandContextApiWithWorkflowVersionTriggerInformation(
|
|
{
|
|
headlessEngineCommandContextApi,
|
|
workflowVersionId,
|
|
availabilityType,
|
|
availabilityObjectMetadataId,
|
|
},
|
|
)
|
|
: headlessEngineCommandContextApi;
|
|
|
|
if (!isDefined(commandState)) {
|
|
return;
|
|
}
|
|
|
|
store.set(headlessCommandContextApisState.atom, (previousMap) => {
|
|
const newMap = new Map(previousMap);
|
|
|
|
newMap.set(engineCommandId, commandState);
|
|
|
|
return newMap;
|
|
});
|
|
},
|
|
[
|
|
store,
|
|
enrichHeadlessCommandContextApiWithWorkflowVersionTriggerInformation,
|
|
],
|
|
);
|
|
|
|
return mountCommand;
|
|
};
|