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)
This commit is contained in:
@@ -0,0 +1,110 @@
|
||||
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>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user