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 { 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 { AppPath } from '@/types/AppPath';
|
||||||
import { SettingsPath } from '@/types/SettingsPath';
|
import { SettingsPath } from '@/types/SettingsPath';
|
||||||
import { DefaultLayout } from '@/ui/layout/page/DefaultLayout';
|
import { DefaultLayout } from '@/ui/layout/page/DefaultLayout';
|
||||||
import { PageTitle } from '@/ui/utilities/page-title/PageTitle';
|
import { PageTitle } from '@/ui/utilities/page-title/PageTitle';
|
||||||
|
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
|
||||||
import { CommandMenuEffect } from '~/effect-components/CommandMenuEffect';
|
import { CommandMenuEffect } from '~/effect-components/CommandMenuEffect';
|
||||||
import { GotoHotkeysEffect } from '~/effect-components/GotoHotkeysEffect';
|
import { GotoHotkeysEffect } from '~/effect-components/GotoHotkeysEffect';
|
||||||
import { useDefaultHomePagePath } from '~/hooks/useDefaultHomePagePath';
|
import { useDefaultHomePagePath } from '~/hooks/useDefaultHomePagePath';
|
||||||
@ -16,6 +15,8 @@ import { SignInUp } from '~/pages/auth/SignInUp';
|
|||||||
import { VerifyEffect } from '~/pages/auth/VerifyEffect';
|
import { VerifyEffect } from '~/pages/auth/VerifyEffect';
|
||||||
import { ImpersonateEffect } from '~/pages/impersonate/ImpersonateEffect';
|
import { ImpersonateEffect } from '~/pages/impersonate/ImpersonateEffect';
|
||||||
import { NotFound } from '~/pages/not-found/NotFound';
|
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 { Opportunities } from '~/pages/opportunities/Opportunities';
|
||||||
import { SettingsAccounts } from '~/pages/settings/accounts/SettingsAccounts';
|
import { SettingsAccounts } from '~/pages/settings/accounts/SettingsAccounts';
|
||||||
import { SettingsAccountsEmails } from '~/pages/settings/accounts/SettingsAccountsEmails';
|
import { SettingsAccountsEmails } from '~/pages/settings/accounts/SettingsAccountsEmails';
|
||||||
@ -43,6 +44,9 @@ export const App = () => {
|
|||||||
const { defaultHomePagePath } = useDefaultHomePagePath();
|
const { defaultHomePagePath } = useDefaultHomePagePath();
|
||||||
|
|
||||||
const pageTitle = getPageTitleFromPath(pathname);
|
const pageTitle = getPageTitleFromPath(pathname);
|
||||||
|
const isNewRecordBoardEnabled = useIsFeatureEnabled(
|
||||||
|
'IS_NEW_RECORD_BOARD_ENABLED',
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@ -62,8 +66,13 @@ export const App = () => {
|
|||||||
<Route path={AppPath.TasksPage} element={<Tasks />} />
|
<Route path={AppPath.TasksPage} element={<Tasks />} />
|
||||||
<Route path={AppPath.Impersonate} element={<ImpersonateEffect />} />
|
<Route path={AppPath.Impersonate} element={<ImpersonateEffect />} />
|
||||||
|
|
||||||
<Route path={AppPath.OpportunitiesPage} element={<Opportunities />} />
|
{!isNewRecordBoardEnabled && (
|
||||||
<Route path={AppPath.RecordTablePage} element={<RecordTablePage />} />
|
<Route
|
||||||
|
path={AppPath.OpportunitiesPage}
|
||||||
|
element={<Opportunities />}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<Route path={AppPath.RecordIndexPage} element={<RecordIndexPage />} />
|
||||||
<Route path={AppPath.RecordShowPage} element={<RecordShowPage />} />
|
<Route path={AppPath.RecordShowPage} element={<RecordShowPage />} />
|
||||||
|
|
||||||
<Route
|
<Route
|
||||||
|
|||||||
@ -134,7 +134,7 @@ export const PageChangeEffect = () => {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
switch (true) {
|
switch (true) {
|
||||||
case isMatchingLocation(AppPath.RecordTablePage): {
|
case isMatchingLocation(AppPath.RecordIndexPage): {
|
||||||
setHotkeyScope(TableHotkeyScope.Table, {
|
setHotkeyScope(TableHotkeyScope.Table, {
|
||||||
goto: true,
|
goto: true,
|
||||||
keyboardShortcutMenu: 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 { useObjectNameSingularFromPlural } from '@/object-metadata/hooks/useObjectNameSingularFromPlural';
|
||||||
import { RecordUpdateHookParams } from '@/object-record/field/contexts/FieldContext';
|
import { RecordUpdateHookParams } from '@/object-record/field/contexts/FieldContext';
|
||||||
import { useUpdateOneRecord } from '@/object-record/hooks/useUpdateOneRecord';
|
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 { RecordTableWithWrappers } from '@/object-record/record-table/components/RecordTableWithWrappers';
|
||||||
import { TableOptionsDropdownId } from '@/object-record/record-table/constants/TableOptionsDropdownId';
|
import { TableOptionsDropdownId } from '@/object-record/record-table/constants/TableOptionsDropdownId';
|
||||||
import { useRecordTable } from '@/object-record/record-table/hooks/useRecordTable';
|
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 { mapViewFiltersToFilters } from '@/views/utils/mapViewFiltersToFilters';
|
||||||
import { mapViewSortsToSorts } from '@/views/utils/mapViewSortsToSorts';
|
import { mapViewSortsToSorts } from '@/views/utils/mapViewSortsToSorts';
|
||||||
|
|
||||||
import { RecordTableEffect } from './RecordTableEffect';
|
|
||||||
|
|
||||||
const StyledContainer = styled.div`
|
const StyledContainer = styled.div`
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
@ -27,7 +26,7 @@ const StyledContainer = styled.div`
|
|||||||
padding-left: ${({ theme }) => theme.table.horizontalCellPadding};
|
padding-left: ${({ theme }) => theme.table.horizontalCellPadding};
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const RecordTableContainer = ({
|
export const RecordIndexContainer = ({
|
||||||
recordTableId,
|
recordTableId,
|
||||||
objectNamePlural,
|
objectNamePlural,
|
||||||
createRecord,
|
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 = '/',
|
Index = '/',
|
||||||
TasksPage = '/tasks',
|
TasksPage = '/tasks',
|
||||||
OpportunitiesPage = '/objects/opportunities',
|
OpportunitiesPage = '/objects/opportunities',
|
||||||
RecordTablePage = '/objects/:objectNamePlural',
|
|
||||||
|
|
||||||
|
RecordIndexPage = '/objects/:objectNamePlural',
|
||||||
RecordShowPage = '/object/:objectNameSingular/:objectRecordId',
|
RecordShowPage = '/object/:objectNameSingular/:objectRecordId',
|
||||||
|
|
||||||
SettingsCatchAll = `/settings/*`,
|
SettingsCatchAll = `/settings/*`,
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
export type FeatureFlagKey =
|
export type FeatureFlagKey =
|
||||||
| 'IS_MESSAGING_ENABLED'
|
| 'IS_MESSAGING_ENABLED'
|
||||||
| 'IS_QUICK_ACTIONS_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 { useObjectMetadataItemForSettings } from '@/object-metadata/hooks/useObjectMetadataItemForSettings';
|
||||||
import { useObjectNameSingularFromPlural } from '@/object-metadata/hooks/useObjectNameSingularFromPlural';
|
import { useObjectNameSingularFromPlural } from '@/object-metadata/hooks/useObjectNameSingularFromPlural';
|
||||||
import { useCreateOneRecord } from '@/object-record/hooks/useCreateOneRecord';
|
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 { RecordTableActionBar } from '@/object-record/record-table/action-bar/components/RecordTableActionBar';
|
||||||
import { RecordTableContextMenu } from '@/object-record/record-table/context-menu/components/RecordTableContextMenu';
|
import { RecordTableContextMenu } from '@/object-record/record-table/context-menu/components/RecordTableContextMenu';
|
||||||
import { useSelectedTableCellEditMode } from '@/object-record/record-table/record-table-cell/hooks/useSelectedTableCellEditMode';
|
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 { PageHotkeysEffect } from '@/ui/layout/page/PageHotkeysEffect';
|
||||||
import { useSetHotkeyScope } from '@/ui/utilities/hotkey/hooks/useSetHotkeyScope';
|
import { useSetHotkeyScope } from '@/ui/utilities/hotkey/hooks/useSetHotkeyScope';
|
||||||
|
|
||||||
import { RecordTableContainer } from './RecordTableContainer';
|
|
||||||
|
|
||||||
const StyledTableContainer = styled.div`
|
const StyledTableContainer = styled.div`
|
||||||
display: flex;
|
display: flex;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const RecordTablePage = () => {
|
export const RecordIndexPage = () => {
|
||||||
const objectNamePlural = useParams().objectNamePlural ?? '';
|
const objectNamePlural = useParams().objectNamePlural ?? '';
|
||||||
|
|
||||||
const { objectNameSingular } = useObjectNameSingularFromPlural({
|
const { objectNameSingular } = useObjectNameSingularFromPlural({
|
||||||
@ -87,7 +86,7 @@ export const RecordTablePage = () => {
|
|||||||
</PageHeader>
|
</PageHeader>
|
||||||
<PageBody>
|
<PageBody>
|
||||||
<StyledTableContainer>
|
<StyledTableContainer>
|
||||||
<RecordTableContainer
|
<RecordIndexContainer
|
||||||
recordTableId={recordTableId}
|
recordTableId={recordTableId}
|
||||||
objectNamePlural={objectNamePlural}
|
objectNamePlural={objectNamePlural}
|
||||||
createRecord={handleAddButtonClick}
|
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',
|
IsMessagingEnabled = 'IS_MESSAGING_ENABLED',
|
||||||
IsRatingFieldTypeEnabled = 'IS_RATING_FIELD_TYPE_ENABLED',
|
IsRatingFieldTypeEnabled = 'IS_RATING_FIELD_TYPE_ENABLED',
|
||||||
IsWorkspaceCleanable = 'IS_WORKSPACE_CLEANABLE',
|
IsWorkspaceCleanable = 'IS_WORKSPACE_CLEANABLE',
|
||||||
|
IsNewRecordBoardEnabled = 'IS_NEW_RECORD_BOARD_ENABLED',
|
||||||
}
|
}
|
||||||
|
|
||||||
@Entity({ name: 'featureFlag', schema: 'core' })
|
@Entity({ name: 'featureFlag', schema: 'core' })
|
||||||
|
|||||||
Reference in New Issue
Block a user