Add email thread widget and message thread record page layout (#19351)
## Summary - Move email thread display from side panel to a dedicated record page with a new `EMAIL_THREAD` widget type - Add message thread as a standard object with page layout, subject field, and backfill command - Add reply-to-email command menu item for message thread records - Remove old side panel message thread components in favor of the new widget-based approach ## Type fixes - Add `EMAIL_THREAD` to `WidgetConfigurationType`, `WidgetType`, and all configuration/validator maps - Create `EmailThreadConfigurationDTO` and shared `EmailThreadConfiguration` type - Register EMAIL_THREAD in widget type validators, configuration resolvers, and standard widget mappings ## Test plan - [ ] Verify message thread record pages render with the email thread widget - [ ] Verify email thread preview navigates to the record page instead of opening side panel - [ ] Verify reply-to-email command appears for message thread records - [ ] Verify typecheck passes for both twenty-front and twenty-server - [ ] Run existing test suites to check for regressions 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
-89
@@ -1,89 +0,0 @@
|
||||
import { renderHook } from '@testing-library/react';
|
||||
import { act } from 'react';
|
||||
|
||||
import { SIDE_PANEL_COMPONENT_INSTANCE_ID } from '@/side-panel/constants/SidePanelComponentInstanceId';
|
||||
import { useOpenEmailThreadInSidePanel } from '@/side-panel/hooks/useOpenEmailThreadInSidePanel';
|
||||
import { viewableRecordIdComponentState } from '@/side-panel/pages/record-page/states/viewableRecordIdComponentState';
|
||||
import { ContextStoreViewType } from '@/context-store/types/ContextStoreViewType';
|
||||
import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue';
|
||||
import { SidePanelPages } from 'twenty-shared/types';
|
||||
import { IconMail } from 'twenty-ui/display';
|
||||
import { getJestMetadataAndApolloMocksAndCommandMenuWrapper } from '~/testing/jest/getJestMetadataAndApolloMocksAndCommandMenuWrapper';
|
||||
import { getTestEnrichedObjectMetadataItemsMock } from '~/testing/utils/getTestEnrichedObjectMetadataItemsMock';
|
||||
|
||||
jest.mock('uuid', () => ({
|
||||
v4: jest.fn().mockReturnValue('mocked-uuid'),
|
||||
}));
|
||||
|
||||
const mockNavigateSidePanel = jest.fn();
|
||||
jest.mock('@/side-panel/hooks/useNavigateSidePanel', () => ({
|
||||
useNavigateSidePanel: () => ({
|
||||
navigateSidePanel: mockNavigateSidePanel,
|
||||
}),
|
||||
}));
|
||||
|
||||
const personMockObjectMetadataItem =
|
||||
getTestEnrichedObjectMetadataItemsMock().find(
|
||||
(item) => item.nameSingular === 'person',
|
||||
)!;
|
||||
|
||||
const wrapper = getJestMetadataAndApolloMocksAndCommandMenuWrapper({
|
||||
apolloMocks: [],
|
||||
componentInstanceId: SIDE_PANEL_COMPONENT_INSTANCE_ID,
|
||||
contextStoreCurrentObjectMetadataNameSingular:
|
||||
personMockObjectMetadataItem.nameSingular,
|
||||
contextStoreCurrentViewId: 'my-view-id',
|
||||
contextStoreTargetedRecordsRule: {
|
||||
mode: 'selection',
|
||||
selectedRecordIds: [],
|
||||
},
|
||||
contextStoreNumberOfSelectedRecords: 0,
|
||||
contextStoreCurrentViewType: ContextStoreViewType.Table,
|
||||
});
|
||||
|
||||
const renderHooks = () => {
|
||||
const { result } = renderHook(
|
||||
() => {
|
||||
const { openEmailThreadInSidePanel } = useOpenEmailThreadInSidePanel();
|
||||
|
||||
const viewableRecordId = useAtomComponentStateValue(
|
||||
viewableRecordIdComponentState,
|
||||
'mocked-uuid',
|
||||
);
|
||||
|
||||
return {
|
||||
openEmailThreadInSidePanel,
|
||||
viewableRecordId,
|
||||
};
|
||||
},
|
||||
{
|
||||
wrapper,
|
||||
},
|
||||
);
|
||||
return { result };
|
||||
};
|
||||
|
||||
describe('useOpenEmailThreadInSidePanel', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should set the correct states and navigate to the email thread page', () => {
|
||||
const { result } = renderHooks();
|
||||
|
||||
const emailThreadId = 'email-thread-123';
|
||||
|
||||
act(() => {
|
||||
result.current.openEmailThreadInSidePanel(emailThreadId);
|
||||
});
|
||||
|
||||
expect(result.current.viewableRecordId).toBe(emailThreadId);
|
||||
|
||||
expect(mockNavigateSidePanel).toHaveBeenCalledWith({
|
||||
page: SidePanelPages.ViewEmailThread,
|
||||
pageTitle: 'Email Thread',
|
||||
pageIcon: IconMail,
|
||||
pageId: 'mocked-uuid',
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,63 +0,0 @@
|
||||
import { useNavigateSidePanel } from '@/side-panel/hooks/useNavigateSidePanel';
|
||||
import { viewableRecordIdComponentState } from '@/side-panel/pages/record-page/states/viewableRecordIdComponentState';
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { useCallback } from 'react';
|
||||
import { SidePanelPages } from 'twenty-shared/types';
|
||||
import { IconMail } from 'twenty-ui/display';
|
||||
import { v4 } from 'uuid';
|
||||
import { useStore } from 'jotai';
|
||||
|
||||
export const useOpenEmailThreadInSidePanel = () => {
|
||||
const store = useStore();
|
||||
const { navigateSidePanel } = useNavigateSidePanel();
|
||||
|
||||
const openEmailThreadInSidePanel = useCallback(
|
||||
(emailThreadId: string) => {
|
||||
const pageComponentInstanceId = v4();
|
||||
|
||||
store.set(
|
||||
viewableRecordIdComponentState.atomFamily({
|
||||
instanceId: pageComponentInstanceId,
|
||||
}),
|
||||
emailThreadId,
|
||||
);
|
||||
|
||||
// TODO: Uncomment this once we need to show the thread title in the navigation
|
||||
// const objectMetadataItem = snapshot
|
||||
// .getLoadable(objectMetadataItemsSelector)
|
||||
// .getValue()
|
||||
// .find(
|
||||
// ({ nameSingular }) =>
|
||||
// nameSingular === CoreObjectNameSingular.MessageThread,
|
||||
// );
|
||||
|
||||
// set(
|
||||
// commandMenuNavigationMorphItemsState,
|
||||
// new Map([
|
||||
// ...snapshot
|
||||
// .getLoadable(commandMenuNavigationMorphItemsState)
|
||||
// .getValue(),
|
||||
// [
|
||||
// pageComponentInstanceId,
|
||||
// {
|
||||
// objectMetadataId: objectMetadataItem?.id,
|
||||
// recordId: emailThreadId,
|
||||
// },
|
||||
// ],
|
||||
// ]),
|
||||
// );
|
||||
|
||||
navigateSidePanel({
|
||||
page: SidePanelPages.ViewEmailThread,
|
||||
pageTitle: t`Email Thread`,
|
||||
pageIcon: IconMail,
|
||||
pageId: pageComponentInstanceId,
|
||||
});
|
||||
},
|
||||
[navigateSidePanel, store],
|
||||
);
|
||||
|
||||
return {
|
||||
openEmailThreadInSidePanel,
|
||||
};
|
||||
};
|
||||
Reference in New Issue
Block a user