938debd83e
## 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)
111 lines
3.6 KiB
TypeScript
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>
|
|
);
|
|
};
|