diff --git a/packages/twenty-front/src/App.tsx b/packages/twenty-front/src/App.tsx index 827e1751b..0d9506317 100644 --- a/packages/twenty-front/src/App.tsx +++ b/packages/twenty-front/src/App.tsx @@ -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 = () => { } /> } /> - } /> - } /> + {!isNewRecordBoardEnabled && ( + } + /> + )} + } /> } /> { useEffect(() => { switch (true) { - case isMatchingLocation(AppPath.RecordTablePage): { + case isMatchingLocation(AppPath.RecordIndexPage): { setHotkeyScope(TableHotkeyScope.Table, { goto: true, keyboardShortcutMenu: true, diff --git a/packages/twenty-front/src/modules/object-record/components/RecordShowPage.tsx b/packages/twenty-front/src/modules/object-record/components/RecordShowPage.tsx deleted file mode 100644 index 9ae11f363..000000000 --- a/packages/twenty-front/src/modules/object-record/components/RecordShowPage.tsx +++ /dev/null @@ -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 ( - - - - {record && ( - <> - - - - - )} - - - - - - {!loading && !!record && ( - <> - - - - } - avatarType={ - mapToObjectRecordIdentifier(record).avatarType ?? - 'rounded' - } - onUploadPicture={ - objectNameSingular === 'person' - ? onUploadPicture - : undefined - } - /> - - {inlineFieldMetadataItems.map( - (fieldMetadataItem, index) => ( - - - - ), - )} - - {relationFieldMetadataItems - .filter((item) => { - const relationObjectMetadataItem = item.toRelationMetadata - ? item.toRelationMetadata.fromObjectMetadata - : item.fromRelationMetadata?.toObjectMetadata; - - if (!relationObjectMetadataItem) { - return false; - } - - return isObjectMetadataAvailableForRelation( - relationObjectMetadataItem, - ); - }) - .map((fieldMetadataItem, index) => ( - - - - ))} - - )} - - - - - - - ); -}; diff --git a/packages/twenty-front/src/modules/object-record/components/RecordTableContainer.tsx b/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexContainer.tsx similarity index 96% rename from packages/twenty-front/src/modules/object-record/components/RecordTableContainer.tsx rename to packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexContainer.tsx index bf3428d8c..5b7cd7868 100644 --- a/packages/twenty-front/src/modules/object-record/components/RecordTableContainer.tsx +++ b/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexContainer.tsx @@ -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, diff --git a/packages/twenty-front/src/modules/object-record/components/RecordTableEffect.tsx b/packages/twenty-front/src/modules/object-record/record-index/components/RecordTableEffect.tsx similarity index 100% rename from packages/twenty-front/src/modules/object-record/components/RecordTableEffect.tsx rename to packages/twenty-front/src/modules/object-record/record-index/components/RecordTableEffect.tsx diff --git a/packages/twenty-front/src/modules/object-record/record-show/components/RecordShowContainer.tsx b/packages/twenty-front/src/modules/object-record/record-show/components/RecordShowContainer.tsx new file mode 100644 index 000000000..ce5d0f6b7 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-show/components/RecordShowContainer.tsx @@ -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 ( + + + + {!loading && !!record && ( + <> + + + + } + avatarType={ + mapToObjectRecordIdentifier(record).avatarType ?? 'rounded' + } + onUploadPicture={ + objectNameSingular === 'person' ? onUploadPicture : undefined + } + /> + + {inlineFieldMetadataItems.map((fieldMetadataItem, index) => ( + + + + ))} + + {relationFieldMetadataItems + .filter((item) => { + const relationObjectMetadataItem = item.toRelationMetadata + ? item.toRelationMetadata.fromObjectMetadata + : item.fromRelationMetadata?.toObjectMetadata; + + if (!relationObjectMetadataItem) { + return false; + } + + return isObjectMetadataAvailableForRelation( + relationObjectMetadataItem, + ); + }) + .map((fieldMetadataItem, index) => ( + + + + ))} + + )} + + + + + ); +}; diff --git a/packages/twenty-front/src/modules/types/AppPath.ts b/packages/twenty-front/src/modules/types/AppPath.ts index f3e646a9f..b01080c59 100644 --- a/packages/twenty-front/src/modules/types/AppPath.ts +++ b/packages/twenty-front/src/modules/types/AppPath.ts @@ -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/*`, diff --git a/packages/twenty-front/src/modules/workspace/types/FeatureFlagKey.ts b/packages/twenty-front/src/modules/workspace/types/FeatureFlagKey.ts index 194df33de..ecf85f0f4 100644 --- a/packages/twenty-front/src/modules/workspace/types/FeatureFlagKey.ts +++ b/packages/twenty-front/src/modules/workspace/types/FeatureFlagKey.ts @@ -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'; diff --git a/packages/twenty-front/src/modules/object-record/components/RecordTablePage.tsx b/packages/twenty-front/src/pages/object-record/RecordIndexPage.tsx similarity index 95% rename from packages/twenty-front/src/modules/object-record/components/RecordTablePage.tsx rename to packages/twenty-front/src/pages/object-record/RecordIndexPage.tsx index b41b08deb..8bdac648a 100644 --- a/packages/twenty-front/src/modules/object-record/components/RecordTablePage.tsx +++ b/packages/twenty-front/src/pages/object-record/RecordIndexPage.tsx @@ -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 = () => { - { + 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 ( + + + + {record && ( + <> + + + + + )} + + + + + + ); +}; diff --git a/packages/twenty-server/src/core/feature-flag/feature-flag.entity.ts b/packages/twenty-server/src/core/feature-flag/feature-flag.entity.ts index b77ee1516..91664dacf 100644 --- a/packages/twenty-server/src/core/feature-flag/feature-flag.entity.ts +++ b/packages/twenty-server/src/core/feature-flag/feature-flag.entity.ts @@ -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' })