Files
twenty/packages/twenty-front/src/modules/activities/notes/components/NoteTile.tsx
T
Félix Malfait 938debd83e Refactor record layouts for backend-driven configuration (#15021)
## Refactor: Prepare frontend record layouts for backend-driven
configuration

This PR simplifies and prepares the frontend record layout system for
eventual migration to backend-driven layouts, aligning the architecture
with the existing PageLayout system used for dashboards.

### Key Changes

**Architecture Improvements:**
- Created unified LayoutRenderingContext that works for both record
pages (with targetRecord) and dashboards (standalone)
- Replaced prop drilling with context-based data flow - cards access
targetRecord and isInRightDrawer via context hooks
- Introduced useTargetRecord() helper hook that provides type-safe
access to the current record
- Created generic CardRenderer component that handles configuration
injection and context guards uniformly

**Configuration System:**
- Converted all tab icons from React components to JSON-serializable
strings (e.g., Icon: IconCheckbox → icon: 'IconCheckbox')
- Added configuration field to cards, matching the widget configuration
pattern on the backend
- Created CardConfiguration types system similar to WidgetConfiguration
on the backend
- Moved widget-specific props (like showDuplicatesSection) into
configuration objects

**Code Organization:**
- Extracted visibility evaluation logic into reusable
evaluateTabVisibility() utility
- Organized layouts into dedicated files (one per object:
base-record-layout.ts, company-record-layout.ts, etc.)
- Renamed components to match their purpose: Notes → NotesCard,
Attachments → FilesCard, etc.
- Consolidated card rendering from registry object to direct
getCardComponent() function

**API Alignment:**
- Made ifNoReadPermissionObject an explicit part of TabVisibilityConfig
(follows if* naming convention)
- Removed redundant targetObjectNameSingular from tab-level (now derived
from visibility config)
- Card API now mirrors Widget API (both use type, configuration,
accessed via context)
2025-10-10 16:26:17 +02:00

111 lines
3.6 KiB
TypeScript

import styled from '@emotion/styled';
import { ActivityTargetsInlineCell } from '@/activities/inline-cell/components/ActivityTargetsInlineCell';
import { useActivityTargetsComponentInstanceId } from '@/activities/inline-cell/hooks/useActivityTargetsComponentInstanceId';
import { type Note } from '@/activities/types/Note';
import { getActivityPreview } from '@/activities/utils/getActivityPreview';
import { useOpenRecordInCommandMenu } from '@/command-menu/hooks/useOpenRecordInCommandMenu';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { FieldContextProvider } from '@/object-record/record-field/ui/components/FieldContextProvider';
const StyledCard = styled.div<{ isSingleNote: boolean }>`
align-items: flex-start;
background: ${({ theme }) => theme.background.secondary};
border: 1px solid ${({ theme }) => theme.border.color.medium};
border-radius: ${({ theme }) => theme.border.radius.md};
display: flex;
flex-direction: column;
height: 300px;
justify-content: space-between;
max-width: ${({ isSingleNote }) => (isSingleNote ? '300px' : 'unset')};
`;
const StyledCardDetailsContainer = styled.div`
align-items: flex-start;
align-self: stretch;
cursor: pointer;
display: flex;
flex-direction: column;
gap: ${({ theme }) => theme.spacing(2)};
height: calc(100% - 45px);
justify-content: start;
padding: ${({ theme }) => theme.spacing(4)};
width: calc(100% - ${({ theme }) => theme.spacing(8)});
box-sizing: border-box;
`;
const StyledNoteTitle = styled.div`
color: ${({ theme }) => theme.font.color.primary};
font-weight: ${({ theme }) => theme.font.weight.medium};
`;
const StyledCardContent = styled.div`
align-self: stretch;
color: ${({ theme }) => theme.font.color.secondary};
line-break: anywhere;
overflow: hidden;
text-overflow: ellipsis;
white-space: pre-line;
width: 100%;
`;
const StyledFooter = styled.div`
align-items: center;
align-self: stretch;
border-top: 1px solid ${({ theme }) => theme.border.color.light};
color: ${({ theme }) => theme.font.color.primary};
display: flex;
flex-direction: row;
gap: ${({ theme }) => theme.spacing(1)};
justify-content: center;
padding: ${({ theme }) => theme.spacing(2)};
width: calc(100% - ${({ theme }) => theme.spacing(4)});
`;
export const NoteTile = ({
note,
isSingleNote,
}: {
note: Note;
isSingleNote: boolean;
}) => {
const { openRecordInCommandMenu } = useOpenRecordInCommandMenu();
const body = getActivityPreview(note?.bodyV2?.blocknote ?? null);
const baseComponentInstanceId = `note-card-${note.id}-targets`;
const componentInstanceId = useActivityTargetsComponentInstanceId(
baseComponentInstanceId,
);
return (
<StyledCard isSingleNote={isSingleNote}>
<StyledCardDetailsContainer
onClick={() =>
openRecordInCommandMenu({
recordId: note.id,
objectNameSingular: CoreObjectNameSingular.Note,
})
}
>
<StyledNoteTitle>{note.title ?? 'Task Title'}</StyledNoteTitle>
<StyledCardContent>{body}</StyledCardContent>
</StyledCardDetailsContainer>
<StyledFooter>
<FieldContextProvider
objectNameSingular={CoreObjectNameSingular.Note}
objectRecordId={note.id}
fieldMetadataName="noteTargets"
fieldPosition={0}
>
<ActivityTargetsInlineCell
componentInstanceId={componentInstanceId}
activityRecordId={note.id}
activityObjectNameSingular={CoreObjectNameSingular.Note}
/>
</FieldContextProvider>
</StyledFooter>
</StyledCard>
);
};