Introduce new board feature flag (#3602)
This commit is contained in:
@ -1,11 +1,10 @@
|
||||
import { Navigate, Route, Routes, useLocation } from 'react-router-dom';
|
||||
|
||||
import { RecordShowPage } from '@/object-record/components/RecordShowPage';
|
||||
import { RecordTablePage } from '@/object-record/components/RecordTablePage';
|
||||
import { AppPath } from '@/types/AppPath';
|
||||
import { SettingsPath } from '@/types/SettingsPath';
|
||||
import { DefaultLayout } from '@/ui/layout/page/DefaultLayout';
|
||||
import { PageTitle } from '@/ui/utilities/page-title/PageTitle';
|
||||
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
|
||||
import { CommandMenuEffect } from '~/effect-components/CommandMenuEffect';
|
||||
import { GotoHotkeysEffect } from '~/effect-components/GotoHotkeysEffect';
|
||||
import { useDefaultHomePagePath } from '~/hooks/useDefaultHomePagePath';
|
||||
@ -16,6 +15,8 @@ import { SignInUp } from '~/pages/auth/SignInUp';
|
||||
import { VerifyEffect } from '~/pages/auth/VerifyEffect';
|
||||
import { ImpersonateEffect } from '~/pages/impersonate/ImpersonateEffect';
|
||||
import { NotFound } from '~/pages/not-found/NotFound';
|
||||
import { RecordIndexPage } from '~/pages/object-record/RecordIndexPage';
|
||||
import { RecordShowPage } from '~/pages/object-record/RecordShowPage';
|
||||
import { Opportunities } from '~/pages/opportunities/Opportunities';
|
||||
import { SettingsAccounts } from '~/pages/settings/accounts/SettingsAccounts';
|
||||
import { SettingsAccountsEmails } from '~/pages/settings/accounts/SettingsAccountsEmails';
|
||||
@ -43,6 +44,9 @@ export const App = () => {
|
||||
const { defaultHomePagePath } = useDefaultHomePagePath();
|
||||
|
||||
const pageTitle = getPageTitleFromPath(pathname);
|
||||
const isNewRecordBoardEnabled = useIsFeatureEnabled(
|
||||
'IS_NEW_RECORD_BOARD_ENABLED',
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
@ -62,8 +66,13 @@ export const App = () => {
|
||||
<Route path={AppPath.TasksPage} element={<Tasks />} />
|
||||
<Route path={AppPath.Impersonate} element={<ImpersonateEffect />} />
|
||||
|
||||
<Route path={AppPath.OpportunitiesPage} element={<Opportunities />} />
|
||||
<Route path={AppPath.RecordTablePage} element={<RecordTablePage />} />
|
||||
{!isNewRecordBoardEnabled && (
|
||||
<Route
|
||||
path={AppPath.OpportunitiesPage}
|
||||
element={<Opportunities />}
|
||||
/>
|
||||
)}
|
||||
<Route path={AppPath.RecordIndexPage} element={<RecordIndexPage />} />
|
||||
<Route path={AppPath.RecordShowPage} element={<RecordShowPage />} />
|
||||
|
||||
<Route
|
||||
|
||||
@ -134,7 +134,7 @@ export const PageChangeEffect = () => {
|
||||
|
||||
useEffect(() => {
|
||||
switch (true) {
|
||||
case isMatchingLocation(AppPath.RecordTablePage): {
|
||||
case isMatchingLocation(AppPath.RecordIndexPage): {
|
||||
setHotkeyScope(TableHotkeyScope.Table, {
|
||||
goto: true,
|
||||
keyboardShortcutMenu: true,
|
||||
|
||||
@ -1,327 +0,0 @@
|
||||
import { useEffect } from 'react';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { useSetRecoilState } from 'recoil';
|
||||
|
||||
import { useFavorites } from '@/favorites/hooks/useFavorites';
|
||||
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
|
||||
import { formatFieldMetadataItemAsColumnDefinition } from '@/object-metadata/utils/formatFieldMetadataItemAsColumnDefinition';
|
||||
import { isObjectMetadataAvailableForRelation } from '@/object-metadata/utils/isObjectMetadataAvailableForRelation';
|
||||
import { parseFieldType } from '@/object-metadata/utils/parseFieldType';
|
||||
import {
|
||||
FieldContext,
|
||||
RecordUpdateHook,
|
||||
RecordUpdateHookParams,
|
||||
} from '@/object-record/field/contexts/FieldContext';
|
||||
import { entityFieldsFamilyState } from '@/object-record/field/states/entityFieldsFamilyState';
|
||||
import { RecordInlineCell } from '@/object-record/record-inline-cell/components/RecordInlineCell';
|
||||
import { PropertyBox } from '@/object-record/record-inline-cell/property-box/components/PropertyBox';
|
||||
import { InlineCellHotkeyScope } from '@/object-record/record-inline-cell/types/InlineCellHotkeyScope';
|
||||
import { RecordRelationFieldCardSection } from '@/object-record/record-relation-card/components/RecordRelationFieldCardSection';
|
||||
import { isFieldMetadataItemAvailable } from '@/object-record/utils/isFieldMetadataItemAvailable';
|
||||
import { IconBuildingSkyscraper } from '@/ui/display/icon';
|
||||
import { PageBody } from '@/ui/layout/page/PageBody';
|
||||
import { PageContainer } from '@/ui/layout/page/PageContainer';
|
||||
import { PageFavoriteButton } from '@/ui/layout/page/PageFavoriteButton';
|
||||
import { PageHeader } from '@/ui/layout/page/PageHeader';
|
||||
import { ShowPageContainer } from '@/ui/layout/page/ShowPageContainer';
|
||||
import { ShowPageAddButton } from '@/ui/layout/show-page/components/ShowPageAddButton';
|
||||
import { ShowPageLeftContainer } from '@/ui/layout/show-page/components/ShowPageLeftContainer';
|
||||
import { ShowPageMoreButton } from '@/ui/layout/show-page/components/ShowPageMoreButton';
|
||||
import { ShowPageRightContainer } from '@/ui/layout/show-page/components/ShowPageRightContainer';
|
||||
import { ShowPageSummaryCard } from '@/ui/layout/show-page/components/ShowPageSummaryCard';
|
||||
import { ShowPageRecoilScopeContext } from '@/ui/layout/states/ShowPageRecoilScopeContext';
|
||||
import { PageTitle } from '@/ui/utilities/page-title/PageTitle';
|
||||
import { RecoilScope } from '@/ui/utilities/recoil-scope/components/RecoilScope';
|
||||
import {
|
||||
FieldMetadataType,
|
||||
FileFolder,
|
||||
useUploadImageMutation,
|
||||
} from '~/generated/graphql';
|
||||
import { isDefined } from '~/utils/isDefined';
|
||||
|
||||
import { useFindOneRecord } from '../hooks/useFindOneRecord';
|
||||
import { useUpdateOneRecord } from '../hooks/useUpdateOneRecord';
|
||||
|
||||
export const RecordShowPage = () => {
|
||||
const { objectNameSingular, objectRecordId } = useParams<{
|
||||
objectNameSingular: string;
|
||||
objectRecordId: string;
|
||||
}>();
|
||||
|
||||
if (!objectNameSingular) {
|
||||
throw new Error(`Object name is not defined`);
|
||||
}
|
||||
|
||||
const {
|
||||
objectMetadataItem,
|
||||
labelIdentifierFieldMetadata,
|
||||
mapToObjectRecordIdentifier,
|
||||
} = useObjectMetadataItem({
|
||||
objectNameSingular,
|
||||
});
|
||||
|
||||
const { favorites, createFavorite, deleteFavorite } = useFavorites();
|
||||
|
||||
const setEntityFields = useSetRecoilState(
|
||||
entityFieldsFamilyState(objectRecordId ?? ''),
|
||||
);
|
||||
|
||||
const { record, loading } = useFindOneRecord({
|
||||
objectRecordId,
|
||||
objectNameSingular,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (!record) return;
|
||||
setEntityFields(record);
|
||||
}, [record, setEntityFields]);
|
||||
|
||||
const [uploadImage] = useUploadImageMutation();
|
||||
const { updateOneRecord } = useUpdateOneRecord({ objectNameSingular });
|
||||
|
||||
const useUpdateOneObjectRecordMutation: RecordUpdateHook = () => {
|
||||
const updateEntity = ({ variables }: RecordUpdateHookParams) => {
|
||||
updateOneRecord?.({
|
||||
idToUpdate: variables.where.id as string,
|
||||
updateOneRecordInput: variables.updateOneRecordInput,
|
||||
});
|
||||
};
|
||||
|
||||
return [updateEntity, { loading: false }];
|
||||
};
|
||||
|
||||
const correspondingFavorite = favorites.find(
|
||||
(favorite) => favorite.recordId === objectRecordId,
|
||||
);
|
||||
|
||||
const isFavorite = isDefined(correspondingFavorite);
|
||||
|
||||
const handleFavoriteButtonClick = async () => {
|
||||
if (!objectNameSingular || !record) return;
|
||||
|
||||
if (isFavorite && record) {
|
||||
deleteFavorite(correspondingFavorite.id);
|
||||
} else {
|
||||
createFavorite(record, objectNameSingular);
|
||||
}
|
||||
};
|
||||
|
||||
const pageName =
|
||||
objectNameSingular === 'person'
|
||||
? record?.name.firstName + ' ' + record?.name.lastName
|
||||
: record?.name;
|
||||
|
||||
const onUploadPicture = async (file: File) => {
|
||||
if (objectNameSingular !== 'person') {
|
||||
return;
|
||||
}
|
||||
|
||||
const result = await uploadImage({
|
||||
variables: {
|
||||
file,
|
||||
fileFolder: FileFolder.PersonPicture,
|
||||
},
|
||||
});
|
||||
|
||||
const avatarUrl = result?.data?.uploadImage;
|
||||
|
||||
if (!avatarUrl) {
|
||||
return;
|
||||
}
|
||||
if (!updateOneRecord) {
|
||||
return;
|
||||
}
|
||||
if (!record) {
|
||||
return;
|
||||
}
|
||||
|
||||
await updateOneRecord({
|
||||
idToUpdate: record.id,
|
||||
updateOneRecordInput: {
|
||||
avatarUrl,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const availableFieldMetadataItems = objectMetadataItem.fields
|
||||
.filter(
|
||||
(fieldMetadataItem) =>
|
||||
isFieldMetadataItemAvailable(fieldMetadataItem) &&
|
||||
fieldMetadataItem.id !== labelIdentifierFieldMetadata?.id,
|
||||
)
|
||||
.sort((fieldMetadataItemA, fieldMetadataItemB) =>
|
||||
fieldMetadataItemA.name.localeCompare(fieldMetadataItemB.name),
|
||||
);
|
||||
|
||||
const inlineFieldMetadataItems = availableFieldMetadataItems.filter(
|
||||
(fieldMetadataItem) =>
|
||||
fieldMetadataItem.type !== FieldMetadataType.Relation,
|
||||
);
|
||||
|
||||
const relationFieldMetadataItems = availableFieldMetadataItems.filter(
|
||||
(fieldMetadataItem) =>
|
||||
fieldMetadataItem.type === FieldMetadataType.Relation,
|
||||
);
|
||||
|
||||
return (
|
||||
<PageContainer>
|
||||
<PageTitle title={pageName} />
|
||||
<PageHeader
|
||||
title={pageName ?? ''}
|
||||
hasBackButton
|
||||
Icon={IconBuildingSkyscraper}
|
||||
>
|
||||
{record && (
|
||||
<>
|
||||
<PageFavoriteButton
|
||||
isFavorite={isFavorite}
|
||||
onClick={handleFavoriteButtonClick}
|
||||
/>
|
||||
<ShowPageAddButton
|
||||
key="add"
|
||||
entity={{
|
||||
id: record.id,
|
||||
targetObjectNameSingular: objectMetadataItem?.nameSingular,
|
||||
}}
|
||||
/>
|
||||
<ShowPageMoreButton
|
||||
key="more"
|
||||
recordId={record.id}
|
||||
objectNameSingular={objectNameSingular}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</PageHeader>
|
||||
<PageBody>
|
||||
<RecoilScope CustomRecoilScopeContext={ShowPageRecoilScopeContext}>
|
||||
<ShowPageContainer>
|
||||
<ShowPageLeftContainer>
|
||||
{!loading && !!record && (
|
||||
<>
|
||||
<ShowPageSummaryCard
|
||||
id={record.id}
|
||||
logoOrAvatar={
|
||||
mapToObjectRecordIdentifier(record).avatarUrl ?? ''
|
||||
}
|
||||
avatarPlaceholder={
|
||||
mapToObjectRecordIdentifier(record).name ?? ''
|
||||
}
|
||||
date={record.createdAt ?? ''}
|
||||
title={
|
||||
<FieldContext.Provider
|
||||
value={{
|
||||
entityId: record.id,
|
||||
recoilScopeId:
|
||||
record.id + labelIdentifierFieldMetadata?.id,
|
||||
isLabelIdentifier: false,
|
||||
fieldDefinition: {
|
||||
type: parseFieldType(
|
||||
labelIdentifierFieldMetadata?.type ||
|
||||
FieldMetadataType.Text,
|
||||
),
|
||||
iconName: '',
|
||||
fieldMetadataId:
|
||||
labelIdentifierFieldMetadata?.id ?? '',
|
||||
label: labelIdentifierFieldMetadata?.label || '',
|
||||
metadata: {
|
||||
fieldName:
|
||||
labelIdentifierFieldMetadata?.name || '',
|
||||
},
|
||||
},
|
||||
useUpdateRecord: useUpdateOneObjectRecordMutation,
|
||||
hotkeyScope: InlineCellHotkeyScope.InlineCell,
|
||||
}}
|
||||
>
|
||||
<RecordInlineCell />
|
||||
</FieldContext.Provider>
|
||||
}
|
||||
avatarType={
|
||||
mapToObjectRecordIdentifier(record).avatarType ??
|
||||
'rounded'
|
||||
}
|
||||
onUploadPicture={
|
||||
objectNameSingular === 'person'
|
||||
? onUploadPicture
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
<PropertyBox extraPadding={true}>
|
||||
{inlineFieldMetadataItems.map(
|
||||
(fieldMetadataItem, index) => (
|
||||
<FieldContext.Provider
|
||||
key={record.id + fieldMetadataItem.id}
|
||||
value={{
|
||||
entityId: record.id,
|
||||
maxWidth: 200,
|
||||
recoilScopeId: record.id + fieldMetadataItem.id,
|
||||
isLabelIdentifier: false,
|
||||
fieldDefinition:
|
||||
formatFieldMetadataItemAsColumnDefinition({
|
||||
field: fieldMetadataItem,
|
||||
position: index,
|
||||
objectMetadataItem,
|
||||
showLabel: true,
|
||||
labelWidth: 90,
|
||||
}),
|
||||
useUpdateRecord: useUpdateOneObjectRecordMutation,
|
||||
hotkeyScope: InlineCellHotkeyScope.InlineCell,
|
||||
}}
|
||||
>
|
||||
<RecordInlineCell />
|
||||
</FieldContext.Provider>
|
||||
),
|
||||
)}
|
||||
</PropertyBox>
|
||||
{relationFieldMetadataItems
|
||||
.filter((item) => {
|
||||
const relationObjectMetadataItem = item.toRelationMetadata
|
||||
? item.toRelationMetadata.fromObjectMetadata
|
||||
: item.fromRelationMetadata?.toObjectMetadata;
|
||||
|
||||
if (!relationObjectMetadataItem) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return isObjectMetadataAvailableForRelation(
|
||||
relationObjectMetadataItem,
|
||||
);
|
||||
})
|
||||
.map((fieldMetadataItem, index) => (
|
||||
<FieldContext.Provider
|
||||
key={record.id + fieldMetadataItem.id}
|
||||
value={{
|
||||
entityId: record.id,
|
||||
recoilScopeId: record.id + fieldMetadataItem.id,
|
||||
isLabelIdentifier: false,
|
||||
fieldDefinition:
|
||||
formatFieldMetadataItemAsColumnDefinition({
|
||||
field: fieldMetadataItem,
|
||||
position: index,
|
||||
objectMetadataItem,
|
||||
}),
|
||||
useUpdateRecord: useUpdateOneObjectRecordMutation,
|
||||
hotkeyScope: InlineCellHotkeyScope.InlineCell,
|
||||
}}
|
||||
>
|
||||
<RecordRelationFieldCardSection />
|
||||
</FieldContext.Provider>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
</ShowPageLeftContainer>
|
||||
<ShowPageRightContainer
|
||||
targetableObject={{
|
||||
id: record?.id ?? '',
|
||||
targetObjectNameSingular: objectMetadataItem?.nameSingular,
|
||||
}}
|
||||
timeline
|
||||
tasks
|
||||
notes
|
||||
emails
|
||||
/>
|
||||
</ShowPageContainer>
|
||||
</RecoilScope>
|
||||
</PageBody>
|
||||
</PageContainer>
|
||||
);
|
||||
};
|
||||
@ -6,6 +6,7 @@ import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadata
|
||||
import { useObjectNameSingularFromPlural } from '@/object-metadata/hooks/useObjectNameSingularFromPlural';
|
||||
import { RecordUpdateHookParams } from '@/object-record/field/contexts/FieldContext';
|
||||
import { useUpdateOneRecord } from '@/object-record/hooks/useUpdateOneRecord';
|
||||
import { RecordTableEffect } from '@/object-record/record-index/components/RecordTableEffect';
|
||||
import { RecordTableWithWrappers } from '@/object-record/record-table/components/RecordTableWithWrappers';
|
||||
import { TableOptionsDropdownId } from '@/object-record/record-table/constants/TableOptionsDropdownId';
|
||||
import { useRecordTable } from '@/object-record/record-table/hooks/useRecordTable';
|
||||
@ -17,8 +18,6 @@ import { mapViewFieldsToColumnDefinitions } from '@/views/utils/mapViewFieldsToC
|
||||
import { mapViewFiltersToFilters } from '@/views/utils/mapViewFiltersToFilters';
|
||||
import { mapViewSortsToSorts } from '@/views/utils/mapViewSortsToSorts';
|
||||
|
||||
import { RecordTableEffect } from './RecordTableEffect';
|
||||
|
||||
const StyledContainer = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@ -27,7 +26,7 @@ const StyledContainer = styled.div`
|
||||
padding-left: ${({ theme }) => theme.table.horizontalCellPadding};
|
||||
`;
|
||||
|
||||
export const RecordTableContainer = ({
|
||||
export const RecordIndexContainer = ({
|
||||
recordTableId,
|
||||
objectNamePlural,
|
||||
createRecord,
|
||||
@ -0,0 +1,253 @@
|
||||
import { useEffect } from 'react';
|
||||
import { useSetRecoilState } from 'recoil';
|
||||
|
||||
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
|
||||
import { formatFieldMetadataItemAsColumnDefinition } from '@/object-metadata/utils/formatFieldMetadataItemAsColumnDefinition';
|
||||
import { isObjectMetadataAvailableForRelation } from '@/object-metadata/utils/isObjectMetadataAvailableForRelation';
|
||||
import { parseFieldType } from '@/object-metadata/utils/parseFieldType';
|
||||
import {
|
||||
FieldContext,
|
||||
RecordUpdateHook,
|
||||
RecordUpdateHookParams,
|
||||
} from '@/object-record/field/contexts/FieldContext';
|
||||
import { entityFieldsFamilyState } from '@/object-record/field/states/entityFieldsFamilyState';
|
||||
import { useFindOneRecord } from '@/object-record/hooks/useFindOneRecord';
|
||||
import { useUpdateOneRecord } from '@/object-record/hooks/useUpdateOneRecord';
|
||||
import { RecordInlineCell } from '@/object-record/record-inline-cell/components/RecordInlineCell';
|
||||
import { PropertyBox } from '@/object-record/record-inline-cell/property-box/components/PropertyBox';
|
||||
import { InlineCellHotkeyScope } from '@/object-record/record-inline-cell/types/InlineCellHotkeyScope';
|
||||
import { RecordRelationFieldCardSection } from '@/object-record/record-relation-card/components/RecordRelationFieldCardSection';
|
||||
import { isFieldMetadataItemAvailable } from '@/object-record/utils/isFieldMetadataItemAvailable';
|
||||
import { ShowPageContainer } from '@/ui/layout/page/ShowPageContainer';
|
||||
import { ShowPageLeftContainer } from '@/ui/layout/show-page/components/ShowPageLeftContainer';
|
||||
import { ShowPageRightContainer } from '@/ui/layout/show-page/components/ShowPageRightContainer';
|
||||
import { ShowPageSummaryCard } from '@/ui/layout/show-page/components/ShowPageSummaryCard';
|
||||
import { ShowPageRecoilScopeContext } from '@/ui/layout/states/ShowPageRecoilScopeContext';
|
||||
import { RecoilScope } from '@/ui/utilities/recoil-scope/components/RecoilScope';
|
||||
import {
|
||||
FieldMetadataType,
|
||||
FileFolder,
|
||||
useUploadImageMutation,
|
||||
} from '~/generated/graphql';
|
||||
|
||||
type RecordShowContainerProps = {
|
||||
objectNameSingular: string;
|
||||
objectRecordId: string;
|
||||
};
|
||||
|
||||
export const RecordShowContainer = ({
|
||||
objectNameSingular,
|
||||
objectRecordId,
|
||||
}: RecordShowContainerProps) => {
|
||||
const {
|
||||
objectMetadataItem,
|
||||
labelIdentifierFieldMetadata,
|
||||
mapToObjectRecordIdentifier,
|
||||
} = useObjectMetadataItem({
|
||||
objectNameSingular,
|
||||
});
|
||||
|
||||
const setEntityFields = useSetRecoilState(
|
||||
entityFieldsFamilyState(objectRecordId ?? ''),
|
||||
);
|
||||
|
||||
const { record, loading } = useFindOneRecord({
|
||||
objectRecordId,
|
||||
objectNameSingular,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (!record) return;
|
||||
setEntityFields(record);
|
||||
}, [record, setEntityFields]);
|
||||
|
||||
const [uploadImage] = useUploadImageMutation();
|
||||
const { updateOneRecord } = useUpdateOneRecord({ objectNameSingular });
|
||||
|
||||
const useUpdateOneObjectRecordMutation: RecordUpdateHook = () => {
|
||||
const updateEntity = ({ variables }: RecordUpdateHookParams) => {
|
||||
updateOneRecord?.({
|
||||
idToUpdate: variables.where.id as string,
|
||||
updateOneRecordInput: variables.updateOneRecordInput,
|
||||
});
|
||||
};
|
||||
|
||||
return [updateEntity, { loading: false }];
|
||||
};
|
||||
|
||||
const onUploadPicture = async (file: File) => {
|
||||
if (objectNameSingular !== 'person') {
|
||||
return;
|
||||
}
|
||||
|
||||
const result = await uploadImage({
|
||||
variables: {
|
||||
file,
|
||||
fileFolder: FileFolder.PersonPicture,
|
||||
},
|
||||
});
|
||||
|
||||
const avatarUrl = result?.data?.uploadImage;
|
||||
|
||||
if (!avatarUrl) {
|
||||
return;
|
||||
}
|
||||
if (!updateOneRecord) {
|
||||
return;
|
||||
}
|
||||
if (!record) {
|
||||
return;
|
||||
}
|
||||
|
||||
await updateOneRecord({
|
||||
idToUpdate: record.id,
|
||||
updateOneRecordInput: {
|
||||
avatarUrl,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const availableFieldMetadataItems = objectMetadataItem.fields
|
||||
.filter(
|
||||
(fieldMetadataItem) =>
|
||||
isFieldMetadataItemAvailable(fieldMetadataItem) &&
|
||||
fieldMetadataItem.id !== labelIdentifierFieldMetadata?.id,
|
||||
)
|
||||
.sort((fieldMetadataItemA, fieldMetadataItemB) =>
|
||||
fieldMetadataItemA.name.localeCompare(fieldMetadataItemB.name),
|
||||
);
|
||||
|
||||
const inlineFieldMetadataItems = availableFieldMetadataItems.filter(
|
||||
(fieldMetadataItem) =>
|
||||
fieldMetadataItem.type !== FieldMetadataType.Relation,
|
||||
);
|
||||
|
||||
const relationFieldMetadataItems = availableFieldMetadataItems.filter(
|
||||
(fieldMetadataItem) =>
|
||||
fieldMetadataItem.type === FieldMetadataType.Relation,
|
||||
);
|
||||
|
||||
return (
|
||||
<RecoilScope CustomRecoilScopeContext={ShowPageRecoilScopeContext}>
|
||||
<ShowPageContainer>
|
||||
<ShowPageLeftContainer>
|
||||
{!loading && !!record && (
|
||||
<>
|
||||
<ShowPageSummaryCard
|
||||
id={record.id}
|
||||
logoOrAvatar={
|
||||
mapToObjectRecordIdentifier(record).avatarUrl ?? ''
|
||||
}
|
||||
avatarPlaceholder={
|
||||
mapToObjectRecordIdentifier(record).name ?? ''
|
||||
}
|
||||
date={record.createdAt ?? ''}
|
||||
title={
|
||||
<FieldContext.Provider
|
||||
value={{
|
||||
entityId: record.id,
|
||||
recoilScopeId:
|
||||
record.id + labelIdentifierFieldMetadata?.id,
|
||||
isLabelIdentifier: false,
|
||||
fieldDefinition: {
|
||||
type: parseFieldType(
|
||||
labelIdentifierFieldMetadata?.type ||
|
||||
FieldMetadataType.Text,
|
||||
),
|
||||
iconName: '',
|
||||
fieldMetadataId: labelIdentifierFieldMetadata?.id ?? '',
|
||||
label: labelIdentifierFieldMetadata?.label || '',
|
||||
metadata: {
|
||||
fieldName: labelIdentifierFieldMetadata?.name || '',
|
||||
},
|
||||
},
|
||||
useUpdateRecord: useUpdateOneObjectRecordMutation,
|
||||
hotkeyScope: InlineCellHotkeyScope.InlineCell,
|
||||
}}
|
||||
>
|
||||
<RecordInlineCell />
|
||||
</FieldContext.Provider>
|
||||
}
|
||||
avatarType={
|
||||
mapToObjectRecordIdentifier(record).avatarType ?? 'rounded'
|
||||
}
|
||||
onUploadPicture={
|
||||
objectNameSingular === 'person' ? onUploadPicture : undefined
|
||||
}
|
||||
/>
|
||||
<PropertyBox extraPadding={true}>
|
||||
{inlineFieldMetadataItems.map((fieldMetadataItem, index) => (
|
||||
<FieldContext.Provider
|
||||
key={record.id + fieldMetadataItem.id}
|
||||
value={{
|
||||
entityId: record.id,
|
||||
maxWidth: 200,
|
||||
recoilScopeId: record.id + fieldMetadataItem.id,
|
||||
isLabelIdentifier: false,
|
||||
fieldDefinition:
|
||||
formatFieldMetadataItemAsColumnDefinition({
|
||||
field: fieldMetadataItem,
|
||||
position: index,
|
||||
objectMetadataItem,
|
||||
showLabel: true,
|
||||
labelWidth: 90,
|
||||
}),
|
||||
useUpdateRecord: useUpdateOneObjectRecordMutation,
|
||||
hotkeyScope: InlineCellHotkeyScope.InlineCell,
|
||||
}}
|
||||
>
|
||||
<RecordInlineCell />
|
||||
</FieldContext.Provider>
|
||||
))}
|
||||
</PropertyBox>
|
||||
{relationFieldMetadataItems
|
||||
.filter((item) => {
|
||||
const relationObjectMetadataItem = item.toRelationMetadata
|
||||
? item.toRelationMetadata.fromObjectMetadata
|
||||
: item.fromRelationMetadata?.toObjectMetadata;
|
||||
|
||||
if (!relationObjectMetadataItem) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return isObjectMetadataAvailableForRelation(
|
||||
relationObjectMetadataItem,
|
||||
);
|
||||
})
|
||||
.map((fieldMetadataItem, index) => (
|
||||
<FieldContext.Provider
|
||||
key={record.id + fieldMetadataItem.id}
|
||||
value={{
|
||||
entityId: record.id,
|
||||
recoilScopeId: record.id + fieldMetadataItem.id,
|
||||
isLabelIdentifier: false,
|
||||
fieldDefinition:
|
||||
formatFieldMetadataItemAsColumnDefinition({
|
||||
field: fieldMetadataItem,
|
||||
position: index,
|
||||
objectMetadataItem,
|
||||
}),
|
||||
useUpdateRecord: useUpdateOneObjectRecordMutation,
|
||||
hotkeyScope: InlineCellHotkeyScope.InlineCell,
|
||||
}}
|
||||
>
|
||||
<RecordRelationFieldCardSection />
|
||||
</FieldContext.Provider>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
</ShowPageLeftContainer>
|
||||
<ShowPageRightContainer
|
||||
targetableObject={{
|
||||
id: record?.id ?? '',
|
||||
targetObjectNameSingular: objectMetadataItem?.nameSingular,
|
||||
}}
|
||||
timeline
|
||||
tasks
|
||||
notes
|
||||
emails
|
||||
/>
|
||||
</ShowPageContainer>
|
||||
</RecoilScope>
|
||||
);
|
||||
};
|
||||
@ -14,8 +14,8 @@ export enum AppPath {
|
||||
Index = '/',
|
||||
TasksPage = '/tasks',
|
||||
OpportunitiesPage = '/objects/opportunities',
|
||||
RecordTablePage = '/objects/:objectNamePlural',
|
||||
|
||||
RecordIndexPage = '/objects/:objectNamePlural',
|
||||
RecordShowPage = '/object/:objectNameSingular/:objectRecordId',
|
||||
|
||||
SettingsCatchAll = `/settings/*`,
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
export type FeatureFlagKey =
|
||||
| 'IS_MESSAGING_ENABLED'
|
||||
| 'IS_QUICK_ACTIONS_ENABLED'
|
||||
| 'IS_RATING_FIELD_TYPE_ENABLED';
|
||||
| 'IS_RATING_FIELD_TYPE_ENABLED'
|
||||
| 'IS_NEW_RECORD_BOARD_ENABLED';
|
||||
|
||||
@ -8,6 +8,7 @@ import { OnboardingStatus } from '@/auth/utils/getOnboardingStatus';
|
||||
import { useObjectMetadataItemForSettings } from '@/object-metadata/hooks/useObjectMetadataItemForSettings';
|
||||
import { useObjectNameSingularFromPlural } from '@/object-metadata/hooks/useObjectNameSingularFromPlural';
|
||||
import { useCreateOneRecord } from '@/object-record/hooks/useCreateOneRecord';
|
||||
import { RecordIndexContainer } from '@/object-record/record-index/components/RecordIndexContainer';
|
||||
import { RecordTableActionBar } from '@/object-record/record-table/action-bar/components/RecordTableActionBar';
|
||||
import { RecordTableContextMenu } from '@/object-record/record-table/context-menu/components/RecordTableContextMenu';
|
||||
import { useSelectedTableCellEditMode } from '@/object-record/record-table/record-table-cell/hooks/useSelectedTableCellEditMode';
|
||||
@ -20,15 +21,13 @@ import { PageHeader } from '@/ui/layout/page/PageHeader';
|
||||
import { PageHotkeysEffect } from '@/ui/layout/page/PageHotkeysEffect';
|
||||
import { useSetHotkeyScope } from '@/ui/utilities/hotkey/hooks/useSetHotkeyScope';
|
||||
|
||||
import { RecordTableContainer } from './RecordTableContainer';
|
||||
|
||||
const StyledTableContainer = styled.div`
|
||||
display: flex;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
export const RecordTablePage = () => {
|
||||
export const RecordIndexPage = () => {
|
||||
const objectNamePlural = useParams().objectNamePlural ?? '';
|
||||
|
||||
const { objectNameSingular } = useObjectNameSingularFromPlural({
|
||||
@ -87,7 +86,7 @@ export const RecordTablePage = () => {
|
||||
</PageHeader>
|
||||
<PageBody>
|
||||
<StyledTableContainer>
|
||||
<RecordTableContainer
|
||||
<RecordIndexContainer
|
||||
recordTableId={recordTableId}
|
||||
objectNamePlural={objectNamePlural}
|
||||
createRecord={handleAddButtonClick}
|
||||
113
packages/twenty-front/src/pages/object-record/RecordShowPage.tsx
Normal file
113
packages/twenty-front/src/pages/object-record/RecordShowPage.tsx
Normal file
@ -0,0 +1,113 @@
|
||||
import { useEffect } from 'react';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { useSetRecoilState } from 'recoil';
|
||||
|
||||
import { useFavorites } from '@/favorites/hooks/useFavorites';
|
||||
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
|
||||
import { entityFieldsFamilyState } from '@/object-record/field/states/entityFieldsFamilyState';
|
||||
import { RecordShowContainer } from '@/object-record/record-show/components/RecordShowContainer';
|
||||
import { IconBuildingSkyscraper } from '@/ui/display/icon';
|
||||
import { PageBody } from '@/ui/layout/page/PageBody';
|
||||
import { PageContainer } from '@/ui/layout/page/PageContainer';
|
||||
import { PageFavoriteButton } from '@/ui/layout/page/PageFavoriteButton';
|
||||
import { PageHeader } from '@/ui/layout/page/PageHeader';
|
||||
import { ShowPageAddButton } from '@/ui/layout/show-page/components/ShowPageAddButton';
|
||||
import { ShowPageMoreButton } from '@/ui/layout/show-page/components/ShowPageMoreButton';
|
||||
import { PageTitle } from '@/ui/utilities/page-title/PageTitle';
|
||||
import { isDefined } from '~/utils/isDefined';
|
||||
|
||||
import { useFindOneRecord } from '../../modules/object-record/hooks/useFindOneRecord';
|
||||
|
||||
export const RecordShowPage = () => {
|
||||
const { objectNameSingular, objectRecordId } = useParams<{
|
||||
objectNameSingular: string;
|
||||
objectRecordId: string;
|
||||
}>();
|
||||
|
||||
if (!objectNameSingular) {
|
||||
throw new Error(`Object name is not defined`);
|
||||
}
|
||||
|
||||
if (!objectRecordId) {
|
||||
throw new Error(`Record id is not defined`);
|
||||
}
|
||||
|
||||
const { objectMetadataItem } = useObjectMetadataItem({
|
||||
objectNameSingular,
|
||||
});
|
||||
|
||||
const { favorites, createFavorite, deleteFavorite } = useFavorites();
|
||||
|
||||
const setEntityFields = useSetRecoilState(
|
||||
entityFieldsFamilyState(objectRecordId ?? ''),
|
||||
);
|
||||
|
||||
const { record } = useFindOneRecord({
|
||||
objectRecordId,
|
||||
objectNameSingular,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (!record) return;
|
||||
setEntityFields(record);
|
||||
}, [record, setEntityFields]);
|
||||
|
||||
const correspondingFavorite = favorites.find(
|
||||
(favorite) => favorite.recordId === objectRecordId,
|
||||
);
|
||||
|
||||
const isFavorite = isDefined(correspondingFavorite);
|
||||
|
||||
const handleFavoriteButtonClick = async () => {
|
||||
if (!objectNameSingular || !record) return;
|
||||
|
||||
if (isFavorite && record) {
|
||||
deleteFavorite(correspondingFavorite.id);
|
||||
} else {
|
||||
createFavorite(record, objectNameSingular);
|
||||
}
|
||||
};
|
||||
|
||||
const pageName =
|
||||
objectNameSingular === 'person'
|
||||
? record?.name.firstName + ' ' + record?.name.lastName
|
||||
: record?.name;
|
||||
|
||||
return (
|
||||
<PageContainer>
|
||||
<PageTitle title={pageName} />
|
||||
<PageHeader
|
||||
title={pageName ?? ''}
|
||||
hasBackButton
|
||||
Icon={IconBuildingSkyscraper}
|
||||
>
|
||||
{record && (
|
||||
<>
|
||||
<PageFavoriteButton
|
||||
isFavorite={isFavorite}
|
||||
onClick={handleFavoriteButtonClick}
|
||||
/>
|
||||
<ShowPageAddButton
|
||||
key="add"
|
||||
entity={{
|
||||
id: record.id,
|
||||
targetObjectNameSingular: objectMetadataItem?.nameSingular,
|
||||
}}
|
||||
/>
|
||||
<ShowPageMoreButton
|
||||
key="more"
|
||||
recordId={record.id}
|
||||
objectNameSingular={objectNameSingular}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</PageHeader>
|
||||
<PageBody>
|
||||
<RecordShowContainer
|
||||
objectNameSingular={objectNameSingular}
|
||||
objectRecordId={objectRecordId}
|
||||
/>
|
||||
</PageBody>
|
||||
</PageContainer>
|
||||
);
|
||||
};
|
||||
@ -17,6 +17,7 @@ export enum FeatureFlagKeys {
|
||||
IsMessagingEnabled = 'IS_MESSAGING_ENABLED',
|
||||
IsRatingFieldTypeEnabled = 'IS_RATING_FIELD_TYPE_ENABLED',
|
||||
IsWorkspaceCleanable = 'IS_WORKSPACE_CLEANABLE',
|
||||
IsNewRecordBoardEnabled = 'IS_NEW_RECORD_BOARD_ENABLED',
|
||||
}
|
||||
|
||||
@Entity({ name: 'featureFlag', schema: 'core' })
|
||||
|
||||
Reference in New Issue
Block a user