Introduce new board feature flag (#3602)

This commit is contained in:
Charles Bochet
2024-01-24 14:24:02 +01:00
committed by GitHub
parent b991790f62
commit afc36c7329
11 changed files with 389 additions and 341 deletions

View File

@ -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>
);
};

View File

@ -1,121 +0,0 @@
import styled from '@emotion/styled';
import { useSpreadsheetCompanyImport } from '@/companies/hooks/useSpreadsheetCompanyImport';
import { useColumnDefinitionsFromFieldMetadata } from '@/object-metadata/hooks/useColumnDefinitionsFromFieldMetadata';
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
import { useObjectNameSingularFromPlural } from '@/object-metadata/hooks/useObjectNameSingularFromPlural';
import { RecordUpdateHookParams } from '@/object-record/field/contexts/FieldContext';
import { useUpdateOneRecord } from '@/object-record/hooks/useUpdateOneRecord';
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';
import { TableOptionsDropdown } from '@/object-record/record-table/options/components/TableOptionsDropdown';
import { useSpreadsheetPersonImport } from '@/people/hooks/useSpreadsheetPersonImport';
import { SpreadsheetImportProvider } from '@/spreadsheet-import/provider/components/SpreadsheetImportProvider';
import { ViewBar } from '@/views/components/ViewBar';
import { mapViewFieldsToColumnDefinitions } from '@/views/utils/mapViewFieldsToColumnDefinitions';
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;
height: 100%;
overflow: auto;
padding-left: ${({ theme }) => theme.table.horizontalCellPadding};
`;
export const RecordTableContainer = ({
recordTableId,
objectNamePlural,
createRecord,
}: {
recordTableId: string;
objectNamePlural: string;
createRecord: () => void;
}) => {
const { objectNameSingular } = useObjectNameSingularFromPlural({
objectNamePlural,
});
const { objectMetadataItem } = useObjectMetadataItem({
objectNameSingular,
});
const { columnDefinitions } =
useColumnDefinitionsFromFieldMetadata(objectMetadataItem);
const { updateOneRecord } = useUpdateOneRecord({
objectNameSingular,
});
const { openPersonSpreadsheetImport } = useSpreadsheetPersonImport();
const { openCompanySpreadsheetImport } = useSpreadsheetCompanyImport();
const viewBarId = objectNamePlural ?? '';
const { setTableFilters, setTableSorts, setTableColumns } = useRecordTable({
recordTableId,
});
const updateEntity = ({ variables }: RecordUpdateHookParams) => {
updateOneRecord?.({
idToUpdate: variables.where.id as string,
updateOneRecordInput: variables.updateOneRecordInput,
});
};
const handleImport = () => {
const openImport =
objectNamePlural === 'companies'
? openCompanySpreadsheetImport
: openPersonSpreadsheetImport;
openImport();
};
return (
<StyledContainer>
<SpreadsheetImportProvider>
<ViewBar
viewBarId={viewBarId}
optionsDropdownButton={
<TableOptionsDropdown
recordTableId={recordTableId}
onImport={
['companies', 'people'].includes(recordTableId)
? handleImport
: undefined
}
/>
}
optionsDropdownScopeId={TableOptionsDropdownId}
onViewFieldsChange={(viewFields) => {
setTableColumns(
mapViewFieldsToColumnDefinitions(viewFields, columnDefinitions),
);
}}
onViewFiltersChange={(viewFilters) => {
setTableFilters(mapViewFiltersToFilters(viewFilters));
}}
onViewSortsChange={(viewSorts) => {
setTableSorts(mapViewSortsToSorts(viewSorts));
}}
/>
</SpreadsheetImportProvider>
<RecordTableEffect
objectNamePlural={objectNamePlural}
recordTableId={recordTableId}
viewBarId={viewBarId}
/>
<RecordTableWithWrappers
recordTableId={recordTableId}
objectNamePlural={objectNamePlural}
viewBarId={viewBarId}
updateRecordMutation={updateEntity}
createRecord={createRecord}
/>
</StyledContainer>
);
};

View File

@ -1,112 +0,0 @@
import { useEffect } from 'react';
import { useColumnDefinitionsFromFieldMetadata } from '@/object-metadata/hooks/useColumnDefinitionsFromFieldMetadata';
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
import { useObjectNameSingularFromPlural } from '@/object-metadata/hooks/useObjectNameSingularFromPlural';
import { useRecordTableContextMenuEntries } from '@/object-record/hooks/useRecordTableContextMenuEntries';
import { useRecordTable } from '@/object-record/record-table/hooks/useRecordTable';
import { filterAvailableTableColumns } from '@/object-record/utils/filterAvailableTableColumns';
import { useViewBar } from '@/views/hooks/useViewBar';
import { ViewType } from '@/views/types/ViewType';
export const RecordTableEffect = ({
objectNamePlural,
recordTableId,
viewBarId,
}: {
objectNamePlural: string;
recordTableId: string;
viewBarId: string;
}) => {
const {
setAvailableTableColumns,
setOnEntityCountChange,
setObjectMetadataConfig,
} = useRecordTable({ recordTableId });
const { objectNameSingular } = useObjectNameSingularFromPlural({
objectNamePlural,
});
const {
objectMetadataItem,
basePathToShowPage,
labelIdentifierFieldMetadata,
} = useObjectMetadataItem({
objectNameSingular,
});
const { columnDefinitions, filterDefinitions, sortDefinitions } =
useColumnDefinitionsFromFieldMetadata(objectMetadataItem);
const {
setAvailableSortDefinitions,
setAvailableFilterDefinitions,
setAvailableFieldDefinitions,
setViewType,
setViewObjectMetadataId,
setEntityCountInCurrentView,
} = useViewBar({ viewBarId });
useEffect(() => {
if (basePathToShowPage && labelIdentifierFieldMetadata) {
setObjectMetadataConfig?.({
basePathToShowPage,
labelIdentifierFieldMetadataId: labelIdentifierFieldMetadata.id,
});
}
}, [
basePathToShowPage,
objectMetadataItem,
labelIdentifierFieldMetadata,
setObjectMetadataConfig,
]);
useEffect(() => {
if (!objectMetadataItem) {
return;
}
setViewObjectMetadataId?.(objectMetadataItem.id);
setViewType?.(ViewType.Table);
setAvailableSortDefinitions?.(sortDefinitions);
setAvailableFilterDefinitions?.(filterDefinitions);
setAvailableFieldDefinitions?.(columnDefinitions);
const availableTableColumns = columnDefinitions.filter(
filterAvailableTableColumns,
);
setAvailableTableColumns(availableTableColumns);
}, [
setViewObjectMetadataId,
setViewType,
columnDefinitions,
setAvailableSortDefinitions,
setAvailableFilterDefinitions,
setAvailableFieldDefinitions,
objectMetadataItem,
sortDefinitions,
filterDefinitions,
setAvailableTableColumns,
]);
const { setActionBarEntries, setContextMenuEntries } =
useRecordTableContextMenuEntries({
objectNamePlural,
recordTableId,
});
useEffect(() => {
setActionBarEntries?.();
setContextMenuEntries?.();
}, [setActionBarEntries, setContextMenuEntries]);
useEffect(() => {
setOnEntityCountChange(
() => (entityCount: number) => setEntityCountInCurrentView(entityCount),
);
}, [setEntityCountInCurrentView, setOnEntityCountChange]);
return <></>;
};

View File

@ -1,101 +0,0 @@
import { useEffect } from 'react';
import { useNavigate, useParams } from 'react-router-dom';
import styled from '@emotion/styled';
import { isNonEmptyString } from '@sniptt/guards';
import { useOnboardingStatus } from '@/auth/hooks/useOnboardingStatus';
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 { 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';
import { DEFAULT_CELL_SCOPE } from '@/object-record/record-table/record-table-cell/hooks/useTableCell';
import { useIcons } from '@/ui/display/icon/hooks/useIcons';
import { PageAddButton } from '@/ui/layout/page/PageAddButton';
import { PageBody } from '@/ui/layout/page/PageBody';
import { PageContainer } from '@/ui/layout/page/PageContainer';
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 = () => {
const objectNamePlural = useParams().objectNamePlural ?? '';
const { objectNameSingular } = useObjectNameSingularFromPlural({
objectNamePlural,
});
const onboardingStatus = useOnboardingStatus();
const navigate = useNavigate();
const { findObjectMetadataItemByNamePlural } =
useObjectMetadataItemForSettings();
const { getIcon } = useIcons();
const Icon = getIcon(
findObjectMetadataItemByNamePlural(objectNamePlural)?.icon,
);
useEffect(() => {
if (
!isNonEmptyString(objectNamePlural) &&
onboardingStatus === OnboardingStatus.Completed
) {
navigate('/');
}
}, [objectNamePlural, navigate, onboardingStatus]);
const { createOneRecord: createOneObject } = useCreateOneRecord({
objectNameSingular,
});
const recordTableId = objectNamePlural ?? '';
const { setSelectedTableCellEditMode } = useSelectedTableCellEditMode({
scopeId: recordTableId,
});
const setHotkeyScope = useSetHotkeyScope();
const handleAddButtonClick = async () => {
await createOneObject?.({});
setSelectedTableCellEditMode(0, 0);
setHotkeyScope(DEFAULT_CELL_SCOPE.scope, DEFAULT_CELL_SCOPE.customScopes);
};
return (
<PageContainer>
<PageHeader
title={
objectNamePlural.charAt(0).toUpperCase() + objectNamePlural.slice(1)
}
Icon={Icon}
>
<PageHotkeysEffect onAddButtonClick={handleAddButtonClick} />
<PageAddButton onClick={handleAddButtonClick} />
</PageHeader>
<PageBody>
<StyledTableContainer>
<RecordTableContainer
recordTableId={recordTableId}
objectNamePlural={objectNamePlural}
createRecord={handleAddButtonClick}
/>
</StyledTableContainer>
<RecordTableActionBar recordTableId={recordTableId} />
<RecordTableContextMenu recordTableId={recordTableId} />
</PageBody>
</PageContainer>
);
};