Feat(frontend): improve the soft delete empty state (#6877)

# This PR

- Fix #6834 

## Demo


https://www.loom.com/share/235c4425f3264f429e2064a9d1604a90?sid=02a815c9-3b1a-45e6-b5ce-d5eb3b40e10e

## Notes

- There is a missing icon in Figma corresponding to the
`noDeletedRecordFound` in the dark mode, thus I used the same icon
(different background because we have the correct background image) for
both dark / light modes
<img width="625" alt="Screenshot 2024-09-03 at 15 04 57"
src="https://github.com/user-attachments/assets/cbc0c3dd-a1ee-49a5-be9a-36450e78a992">
cc: @Bonapara

---------

Co-authored-by: Lucas Bordeau <bordeau.lucas@gmail.com>
This commit is contained in:
Pacifique LINJANJA
2024-09-18 09:39:39 +02:00
committed by GitHub
parent 9c885861a3
commit 601e15f028
45 changed files with 667 additions and 264 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.0 KiB

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.4 KiB

View File

@@ -0,0 +1,5 @@
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
export const useObjectIsRemote = (objectMetadataItem: ObjectMetadataItem) => {
return objectMetadataItem.isRemote ?? false;
};

View File

@@ -0,0 +1,5 @@
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
export const useObjectLabel = (objectMetadataItem: ObjectMetadataItem) => {
return objectMetadataItem?.labelSingular ?? '';
};

View File

@@ -14,7 +14,6 @@ type RecordIndexBoardContainerProps = {
recordBoardId: string;
viewBarId: string;
objectNameSingular: string;
createRecord: () => Promise<void>;
};
export const RecordIndexBoardContainer = ({

View File

@@ -4,14 +4,12 @@ import { useRecoilCallback, useRecoilState, useSetRecoilState } from 'recoil';
import { useColumnDefinitionsFromFieldMetadata } from '@/object-metadata/hooks/useColumnDefinitionsFromFieldMetadata';
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
import { useObjectNameSingularFromPlural } from '@/object-metadata/hooks/useObjectNameSingularFromPlural';
import { lastShowPageRecordIdState } from '@/object-record/record-field/states/lastShowPageRecordId';
import { RecordIndexBoardContainer } from '@/object-record/record-index/components/RecordIndexBoardContainer';
import { RecordIndexBoardDataLoader } from '@/object-record/record-index/components/RecordIndexBoardDataLoader';
import { RecordIndexBoardDataLoaderEffect } from '@/object-record/record-index/components/RecordIndexBoardDataLoaderEffect';
import { RecordIndexTableContainer } from '@/object-record/record-index/components/RecordIndexTableContainer';
import { RecordIndexTableContainerEffect } from '@/object-record/record-index/components/RecordIndexTableContainerEffect';
import { RecordIndexViewBarEffect } from '@/object-record/record-index/components/RecordIndexViewBarEffect';
import { RecordIndexEventContext } from '@/object-record/record-index/contexts/RecordIndexEventContext';
import { RecordIndexOptionsDropdown } from '@/object-record/record-index/options/components/RecordIndexOptionsDropdown';
import { recordIndexFieldDefinitionsState } from '@/object-record/record-index/states/recordIndexFieldDefinitionsState';
import { recordIndexFiltersState } from '@/object-record/record-index/states/recordIndexFiltersState';
@@ -21,7 +19,7 @@ import { recordIndexSortsState } from '@/object-record/record-index/states/recor
import { recordIndexViewTypeState } from '@/object-record/record-index/states/recordIndexViewTypeState';
import { InformationBannerWrapper } from '@/information-banner/components/InformationBannerWrapper';
import { useHandleIndexIdentifierClick } from '@/object-record/record-index/hooks/useHandleIndexIdentifierClick';
import { RecordIndexRootPropsContext } from '@/object-record/record-index/contexts/RecordIndexRootPropsContext';
import { RecordFieldValueSelectorContextProvider } from '@/object-record/record-store/contexts/RecordFieldValueSelectorContext';
import { useRecordTable } from '@/object-record/record-table/hooks/useRecordTable';
import { SpreadsheetImportProvider } from '@/spreadsheet-import/provider/components/SpreadsheetImportProvider';
@@ -31,6 +29,7 @@ import { ViewType } from '@/views/types/ViewType';
import { mapViewFieldsToColumnDefinitions } from '@/views/utils/mapViewFieldsToColumnDefinitions';
import { mapViewFiltersToFilters } from '@/views/utils/mapViewFiltersToFilters';
import { mapViewSortsToSorts } from '@/views/utils/mapViewSortsToSorts';
import { useContext } from 'react';
import { isDeeplyEqual } from '~/utils/isDeeplyEqual';
const StyledContainer = styled.div`
@@ -46,20 +45,15 @@ const StyledContainerWithPadding = styled.div<{ fullHeight?: boolean }>`
padding-left: ${({ theme }) => theme.table.horizontalCellPadding};
`;
type RecordIndexContainerProps = {
recordIndexId: string;
objectNamePlural: string;
createRecord: () => Promise<void>;
};
export const RecordIndexContainer = ({
createRecord,
recordIndexId,
objectNamePlural,
}: RecordIndexContainerProps) => {
export const RecordIndexContainer = () => {
const [recordIndexViewType, setRecordIndexViewType] = useRecoilState(
recordIndexViewTypeState,
);
const { objectNamePlural, recordIndexId } = useContext(
RecordIndexRootPropsContext,
);
const { objectNameSingular } = useObjectNameSingularFromPlural({
objectNamePlural,
});
@@ -110,20 +104,6 @@ export const RecordIndexContainer = ({
[columnDefinitions, setTableColumns],
);
const { handleIndexIdentifierClick } = useHandleIndexIdentifierClick({
objectMetadataItem,
recordIndexId,
});
const handleIndexRecordsLoaded = useRecoilCallback(
({ set }) =>
() => {
// TODO: find a better way to reset this state ?
set(lastShowPageRecordIdState, null);
},
[],
);
return (
<StyledContainer>
<InformationBannerWrapper />
@@ -170,46 +150,37 @@ export const RecordIndexContainer = ({
/>
</StyledContainerWithPadding>
</SpreadsheetImportProvider>
<RecordIndexEventContext.Provider
value={{
onIndexIdentifierClick: handleIndexIdentifierClick,
onIndexRecordsLoaded: handleIndexRecordsLoaded,
}}
>
{recordIndexViewType === ViewType.Table && (
<>
<RecordIndexTableContainer
recordTableId={recordIndexId}
viewBarId={recordIndexId}
objectNameSingular={objectNameSingular}
createRecord={createRecord}
/>
<RecordIndexTableContainerEffect
objectNameSingular={objectNameSingular}
recordTableId={recordIndexId}
viewBarId={recordIndexId}
/>
</>
)}
{recordIndexViewType === ViewType.Kanban && (
<StyledContainerWithPadding fullHeight>
<RecordIndexBoardContainer
recordBoardId={recordIndexId}
viewBarId={recordIndexId}
objectNameSingular={objectNameSingular}
createRecord={createRecord}
/>
<RecordIndexBoardDataLoader
objectNameSingular={objectNameSingular}
recordBoardId={recordIndexId}
/>
<RecordIndexBoardDataLoaderEffect
objectNameSingular={objectNameSingular}
recordBoardId={recordIndexId}
/>
</StyledContainerWithPadding>
)}
</RecordIndexEventContext.Provider>
{recordIndexViewType === ViewType.Table && (
<>
<RecordIndexTableContainer
recordTableId={recordIndexId}
viewBarId={recordIndexId}
/>
<RecordIndexTableContainerEffect
objectNameSingular={objectNameSingular}
recordTableId={recordIndexId}
viewBarId={recordIndexId}
/>
</>
)}
{recordIndexViewType === ViewType.Kanban && (
<StyledContainerWithPadding fullHeight>
<RecordIndexBoardContainer
recordBoardId={recordIndexId}
viewBarId={recordIndexId}
objectNameSingular={objectNameSingular}
/>
<RecordIndexBoardDataLoader
objectNameSingular={objectNameSingular}
recordBoardId={recordIndexId}
/>
<RecordIndexBoardDataLoaderEffect
objectNameSingular={objectNameSingular}
recordBoardId={recordIndexId}
/>
</StyledContainerWithPadding>
)}
</RecordFieldValueSelectorContextProvider>
</StyledContainer>
);

View File

@@ -3,27 +3,23 @@ import { useIcons } from 'twenty-ui';
import { useFilteredObjectMetadataItems } from '@/object-metadata/hooks/useFilteredObjectMetadataItems';
import { RecordIndexPageKanbanAddButton } from '@/object-record/record-index/components/RecordIndexPageKanbanAddButton';
import { RecordIndexRootPropsContext } from '@/object-record/record-index/contexts/RecordIndexRootPropsContext';
import { recordIndexViewTypeState } from '@/object-record/record-index/states/recordIndexViewTypeState';
import { PageAddButton } from '@/ui/layout/page/PageAddButton';
import { PageHeader } from '@/ui/layout/page/PageHeader';
import { PageHotkeysEffect } from '@/ui/layout/page/PageHotkeysEffect';
import { ViewType } from '@/views/types/ViewType';
import { useContext } from 'react';
import { capitalize } from '~/utils/string/capitalize';
type RecordIndexPageHeaderProps = {
createRecord: () => void;
recordIndexId: string;
objectNamePlural: string;
};
export const RecordIndexPageHeader = ({
createRecord,
recordIndexId,
objectNamePlural,
}: RecordIndexPageHeaderProps) => {
export const RecordIndexPageHeader = () => {
const { findObjectMetadataItemByNamePlural } =
useFilteredObjectMetadataItems();
const { objectNamePlural, onCreateRecord } = useContext(
RecordIndexRootPropsContext,
);
const objectMetadataItem =
findObjectMetadataItemByNamePlural(objectNamePlural);
@@ -40,16 +36,17 @@ export const RecordIndexPageHeader = ({
const pageHeaderTitle =
objectMetadataItem?.labelPlural ?? capitalize(objectNamePlural);
const handleAddButtonClick = () => {
onCreateRecord();
};
return (
<PageHeader title={pageHeaderTitle} Icon={Icon}>
<PageHotkeysEffect onAddButtonClick={createRecord} />
<PageHotkeysEffect onAddButtonClick={handleAddButtonClick} />
{isTable ? (
<PageAddButton onClick={createRecord} />
<PageAddButton onClick={handleAddButtonClick} />
) : (
<RecordIndexPageKanbanAddButton
recordIndexId={recordIndexId}
objectNamePlural={objectNamePlural}
/>
<RecordIndexPageKanbanAddButton />
)}
</PageHeader>
);

View File

@@ -2,6 +2,7 @@ import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSi
import { useRecordBoardStates } from '@/object-record/record-board/hooks/internal/useRecordBoardStates';
import { RecordBoardColumnDefinition } from '@/object-record/record-board/types/RecordBoardColumnDefinition';
import { RecordIndexPageKanbanAddMenuItem } from '@/object-record/record-index/components/RecordIndexPageKanbanAddMenuItem';
import { RecordIndexRootPropsContext } from '@/object-record/record-index/contexts/RecordIndexRootPropsContext';
import { useRecordIndexPageKanbanAddButton } from '@/object-record/record-index/hooks/useRecordIndexPageKanbanAddButton';
import { SingleEntitySelect } from '@/object-record/relation-picker/components/SingleEntitySelect';
import { useEntitySelectSearch } from '@/object-record/relation-picker/hooks/useEntitySelectSearch';
@@ -14,7 +15,7 @@ import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/Drop
import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown';
import { usePreviousHotkeyScope } from '@/ui/utilities/hotkey/hooks/usePreviousHotkeyScope';
import styled from '@emotion/styled';
import { useCallback, useState } from 'react';
import { useCallback, useContext, useState } from 'react';
import { useRecoilValue } from 'recoil';
import { IconPlus, isDefined } from 'twenty-ui';
@@ -26,20 +27,16 @@ const StyledDropDownMenu = styled(DropdownMenu)`
width: 200px;
`;
type RecordIndexPageKanbanAddButtonProps = {
recordIndexId: string;
objectNamePlural: string;
};
export const RecordIndexPageKanbanAddButton = ({
recordIndexId,
objectNamePlural,
}: RecordIndexPageKanbanAddButtonProps) => {
export const RecordIndexPageKanbanAddButton = () => {
const dropdownId = `record-index-page-add-button-dropdown`;
const [isSelectingCompany, setIsSelectingCompany] = useState(false);
const [selectedColumnDefinition, setSelectedColumnDefinition] =
useState<RecordBoardColumnDefinition>();
const { recordIndexId, objectNamePlural } = useContext(
RecordIndexRootPropsContext,
);
const { columnIdsState } = useRecordBoardStates(recordIndexId);
const columnIds = useRecoilValue(columnIdsState);

View File

@@ -1,23 +1,23 @@
import { useUpdateOneRecord } from '@/object-record/hooks/useUpdateOneRecord';
import { RecordUpdateHookParams } from '@/object-record/record-field/contexts/FieldContext';
import { RecordIndexRemoveSortingModal } from '@/object-record/record-index/components/RecordIndexRemoveSortingModal';
import { RecordIndexRootPropsContext } from '@/object-record/record-index/contexts/RecordIndexRootPropsContext';
import { RecordTableActionBar } from '@/object-record/record-table/action-bar/components/RecordTableActionBar';
import { RecordTableWithWrappers } from '@/object-record/record-table/components/RecordTableWithWrappers';
import { RecordTableContextMenu } from '@/object-record/record-table/context-menu/components/RecordTableContextMenu';
import { useContext } from 'react';
type RecordIndexTableContainerProps = {
recordTableId: string;
viewBarId: string;
objectNameSingular: string;
createRecord: () => Promise<void>;
};
export const RecordIndexTableContainer = ({
recordTableId,
viewBarId,
objectNameSingular,
createRecord,
}: RecordIndexTableContainerProps) => {
const { objectNameSingular } = useContext(RecordIndexRootPropsContext);
const { updateOneRecord } = useUpdateOneRecord({
objectNameSingular,
});
@@ -36,7 +36,6 @@ export const RecordIndexTableContainer = ({
objectNameSingular={objectNameSingular}
viewBarId={viewBarId}
updateRecordMutation={updateEntity}
createRecord={createRecord}
/>
<RecordTableActionBar recordTableId={recordTableId} />
<RecordIndexRemoveSortingModal recordTableId={recordTableId} />

View File

@@ -0,0 +1,13 @@
import { createRootPropsContext } from '~/utils/createRootPropsContext';
export type RecordIndexRootPropsContextProps = {
onIndexIdentifierClick: (recordId: string) => void;
onIndexRecordsLoaded: () => void;
onCreateRecord: () => void;
objectNamePlural: string;
objectNameSingular: string;
recordIndexId: string;
};
export const RecordIndexRootPropsContext =
createRootPropsContext<RecordIndexRootPropsContextProps>();

View File

@@ -5,8 +5,10 @@ import { useColumnDefinitionsFromFieldMetadata } from '@/object-metadata/hooks/u
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
import { getFilterTypeFromFieldType } from '@/object-metadata/utils/formatFieldMetadataItemsAsFilterDefinitions';
import { Filter } from '@/object-record/object-filter-dropdown/types/Filter';
import { useRecordTableStates } from '@/object-record/record-table/hooks/internal/useRecordTableStates';
import { useCombinedViewFilters } from '@/views/hooks/useCombinedViewFilters';
import { ViewFilterOperand } from '@/views/types/ViewFilterOperand';
import { useRecoilCallback } from 'recoil';
import { isDefined } from '~/utils/isDefined';
type UseHandleToggleTrashColumnFilterProps = {
@@ -26,6 +28,7 @@ export const useHandleToggleTrashColumnFilter = ({
useColumnDefinitionsFromFieldMetadata(objectMetadataItem);
const { upsertCombinedViewFilter } = useCombinedViewFilters(viewBarId);
const { isSoftDeleteActiveState } = useRecordTableStates(viewBarId);
const handleToggleTrashColumnFilter = useCallback(() => {
const trashFieldMetadata = objectMetadataItem.fields.find(
@@ -63,5 +66,15 @@ export const useHandleToggleTrashColumnFilter = ({
upsertCombinedViewFilter(newFilter);
}, [columnDefinitions, objectMetadataItem, upsertCombinedViewFilter]);
return handleToggleTrashColumnFilter;
const toggleSoftDeleteFilterState = useRecoilCallback(
({ set }) =>
(currentState: boolean) => {
set(isSoftDeleteActiveState, currentState);
},
[isSoftDeleteActiveState],
);
return {
handleToggleTrashColumnFilter,
toggleSoftDeleteFilterState,
};
};

View File

@@ -90,10 +90,11 @@ export const RecordIndexOptionsDropdownContent = ({
hiddenTableColumns,
} = useRecordIndexOptionsForTable(recordIndexId);
const handleToggleTrashColumnFilter = useHandleToggleTrashColumnFilter({
objectNameSingular,
viewBarId: recordIndexId,
});
const { handleToggleTrashColumnFilter, toggleSoftDeleteFilterState } =
useHandleToggleTrashColumnFilter({
objectNameSingular,
viewBarId: recordIndexId,
});
const {
visibleBoardFields,
@@ -163,6 +164,7 @@ export const RecordIndexOptionsDropdownContent = ({
<MenuItem
onClick={() => {
handleToggleTrashColumnFilter();
toggleSoftDeleteFilterState(true);
closeDropdown();
}}
LeftIcon={IconRotate2}

View File

@@ -1,9 +1,8 @@
import styled from '@emotion/styled';
import { isNonEmptyString, isNull } from '@sniptt/guards';
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
import { RecordTableContextProvider } from '@/object-record/record-table/components/RecordTableContextProvider';
import { RecordTableEmptyState } from '@/object-record/record-table/components/RecordTableEmptyState';
import { RecordTableEmptyState } from '@/object-record/record-table/empty-state/components/RecordTableEmptyState';
import { useRecordTableStates } from '@/object-record/record-table/hooks/internal/useRecordTableStates';
import { RecordTableBody } from '@/object-record/record-table/record-table-body/components/RecordTableBody';
import { RecordTableBodyEffect } from '@/object-record/record-table/record-table-body/components/RecordTableBodyEffect';
@@ -25,7 +24,6 @@ type RecordTableProps = {
recordTableId: string;
objectNameSingular: string;
onColumnsChange: (columns: any) => void;
createRecord: () => void;
};
export const RecordTable = ({
@@ -33,7 +31,6 @@ export const RecordTable = ({
recordTableId,
objectNameSingular,
onColumnsChange,
createRecord,
}: RecordTableProps) => {
const { scopeId } = useRecordTableStates(recordTableId);
@@ -51,12 +48,10 @@ export const RecordTable = ({
const pendingRecordId = useRecoilValue(pendingRecordIdState);
const { objectMetadataItem: foundObjectMetadataItem } = useObjectMetadataItem(
{ objectNameSingular },
);
const objectLabel = foundObjectMetadataItem?.labelSingular;
const isRemote = foundObjectMetadataItem?.isRemote ?? false;
const recordTableIsEmpty =
!isRecordTableInitialLoading &&
tableRowIds.length === 0 &&
isNull(pendingRecordId);
if (!isNonEmptyString(objectNameSingular)) {
return <></>;
@@ -73,18 +68,11 @@ export const RecordTable = ({
viewBarId={viewBarId}
>
<RecordTableBodyEffect />
{!isRecordTableInitialLoading &&
tableRowIds.length === 0 &&
isNull(pendingRecordId) ? (
<RecordTableEmptyState
objectNameSingular={objectNameSingular}
objectLabel={objectLabel}
createRecord={createRecord}
isRemote={isRemote}
/>
{recordTableIsEmpty ? (
<RecordTableEmptyState />
) : (
<StyledTable className="entity-table-cell">
<RecordTableHeader createRecord={createRecord} />
<RecordTableHeader />
<RecordTableBody />
</StyledTable>
)}

View File

@@ -1,68 +0,0 @@
import { useNavigate } from 'react-router-dom';
import { IconPlus, IconSettings } from 'twenty-ui';
import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords';
import { Button } from '@/ui/input/button/components/Button';
import AnimatedPlaceholder from '@/ui/layout/animated-placeholder/components/AnimatedPlaceholder';
import {
AnimatedPlaceholderEmptyContainer,
AnimatedPlaceholderEmptySubTitle,
AnimatedPlaceholderEmptyTextContainer,
AnimatedPlaceholderEmptyTitle,
} from '@/ui/layout/animated-placeholder/components/EmptyPlaceholderStyled';
type RecordTableEmptyStateProps = {
objectNameSingular: string;
objectLabel: string;
createRecord: () => void;
isRemote: boolean;
};
export const RecordTableEmptyState = ({
objectNameSingular,
objectLabel,
createRecord,
isRemote,
}: RecordTableEmptyStateProps) => {
const navigate = useNavigate();
const { totalCount } = useFindManyRecords({ objectNameSingular, limit: 1 });
const noExistingRecords = totalCount === 0;
const [title, subTitle, Icon, onClick, buttonTitle] = isRemote
? [
'No Data Available for Remote Table',
'If this is unexpected, please verify your settings.',
IconSettings,
() => navigate('/settings/integrations'),
'Go to Settings',
]
: [
noExistingRecords
? `Add your first ${objectLabel}`
: `No ${objectLabel} found`,
noExistingRecords
? `Use our API or add your first ${objectLabel} manually`
: 'No records matching the filter criteria were found.',
IconPlus,
createRecord,
`Add a ${objectLabel}`,
];
return (
<AnimatedPlaceholderEmptyContainer>
<AnimatedPlaceholder type="noRecord" />
<AnimatedPlaceholderEmptyTextContainer>
<AnimatedPlaceholderEmptyTitle>{title}</AnimatedPlaceholderEmptyTitle>
<AnimatedPlaceholderEmptySubTitle>
{subTitle}
</AnimatedPlaceholderEmptySubTitle>
</AnimatedPlaceholderEmptyTextContainer>
<Button
Icon={Icon}
title={buttonTitle}
variant={'secondary'}
onClick={onClick}
/>
</AnimatedPlaceholderEmptyContainer>
);
};

View File

@@ -37,12 +37,10 @@ type RecordTableWithWrappersProps = {
recordTableId: string;
viewBarId: string;
updateRecordMutation: (params: any) => void;
createRecord: () => Promise<void>;
};
export const RecordTableWithWrappers = ({
updateRecordMutation,
createRecord,
objectNameSingular,
recordTableId,
viewBarId,
@@ -80,7 +78,6 @@ export const RecordTableWithWrappers = ({
recordTableId={recordTableId}
objectNameSingular={objectNameSingular}
onColumnsChange={handleColumnsChange}
createRecord={createRecord}
/>
<DragSelect
dragSelectable={tableBodyRef}

View File

@@ -0,0 +1,34 @@
import { useObjectIsRemote } from '@/object-metadata/hooks/useObjectIsRemote';
import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords';
import { RecordTableContext } from '@/object-record/record-table/contexts/RecordTableContext';
import { RecordTableEmptyStateNoRecordAtAll } from '@/object-record/record-table/empty-state/components/RecordTableEmptyStateNoRecordAtAll';
import { RecordTableEmptyStateNoRecordFoundForFilter } from '@/object-record/record-table/empty-state/components/RecordTableEmptyStateNoRecordFoundForFilter';
import { RecordTableEmptyStateRemote } from '@/object-record/record-table/empty-state/components/RecordTableEmptyStateRemote';
import { RecordTableEmptyStateSoftDelete } from '@/object-record/record-table/empty-state/components/RecordTableEmptyStateSoftDelete';
import { useRecordTableStates } from '@/object-record/record-table/hooks/internal/useRecordTableStates';
import { useContext } from 'react';
import { useRecoilValue } from 'recoil';
export const RecordTableEmptyState = () => {
const { objectNameSingular, recordTableId, objectMetadataItem } =
useContext(RecordTableContext);
const { isSoftDeleteActiveState } = useRecordTableStates(recordTableId);
const { totalCount } = useFindManyRecords({ objectNameSingular, limit: 1 });
const noRecordAtAll = totalCount === 0;
const isRemote = useObjectIsRemote(objectMetadataItem);
const isSoftDeleteActive = useRecoilValue(isSoftDeleteActiveState);
if (isRemote) {
return <RecordTableEmptyStateRemote />;
} else if (isSoftDeleteActive === true) {
return <RecordTableEmptyStateSoftDelete />;
} else if (noRecordAtAll) {
return <RecordTableEmptyStateNoRecordAtAll />;
} else {
return <RecordTableEmptyStateNoRecordFoundForFilter />;
}
};

View File

@@ -0,0 +1,48 @@
import AnimatedPlaceholder, {
AnimatedPlaceholderType,
} from '@/ui/layout/animated-placeholder/components/AnimatedPlaceholder';
import {
AnimatedPlaceholderEmptyContainer,
AnimatedPlaceholderEmptySubTitle,
AnimatedPlaceholderEmptyTextContainer,
AnimatedPlaceholderEmptyTitle,
} from '@/ui/layout/animated-placeholder/components/EmptyPlaceholderStyled';
import { Button } from '@/ui/input/button/components/Button';
import { IconComponent } from 'twenty-ui';
type RecordTableEmptyStateDisplayProps = {
animatedPlaceholderType: AnimatedPlaceholderType;
title: string;
subTitle: string;
Icon: IconComponent;
buttonTitle: string;
onClick: () => void;
};
export const RecordTableEmptyStateDisplay = ({
Icon,
animatedPlaceholderType,
buttonTitle,
onClick,
subTitle,
title,
}: RecordTableEmptyStateDisplayProps) => {
return (
<AnimatedPlaceholderEmptyContainer>
<AnimatedPlaceholder type={animatedPlaceholderType} />
<AnimatedPlaceholderEmptyTextContainer>
<AnimatedPlaceholderEmptyTitle>{title}</AnimatedPlaceholderEmptyTitle>
<AnimatedPlaceholderEmptySubTitle>
{subTitle}
</AnimatedPlaceholderEmptySubTitle>
</AnimatedPlaceholderEmptyTextContainer>
<Button
Icon={Icon}
title={buttonTitle}
variant={'secondary'}
onClick={onClick}
/>
</AnimatedPlaceholderEmptyContainer>
);
};

View File

@@ -0,0 +1,36 @@
import { IconPlus } from 'twenty-ui';
import { useObjectLabel } from '@/object-metadata/hooks/useObjectLabel';
import { RecordTableContext } from '@/object-record/record-table/contexts/RecordTableContext';
import { RecordTableEmptyStateDisplay } from '@/object-record/record-table/empty-state/components/RecordTableEmptyStateDisplay';
import { useCreateNewTableRecord } from '@/object-record/record-table/hooks/useCreateNewTableRecords';
import { useContext } from 'react';
export const RecordTableEmptyStateNoRecordAtAll = () => {
const { createNewTableRecord } = useCreateNewTableRecord();
const { objectMetadataItem } = useContext(RecordTableContext);
const handleButtonClick = () => {
createNewTableRecord();
};
const objectLabel = useObjectLabel(objectMetadataItem);
const buttonTitle = `Add a ${objectLabel}`;
const title = `Add your first ${objectLabel}`;
const subTitle = `Use our API or add your first ${objectLabel} manually`;
return (
<RecordTableEmptyStateDisplay
buttonTitle={buttonTitle}
subTitle={subTitle}
title={title}
Icon={IconPlus}
animatedPlaceholderType="noRecord"
onClick={handleButtonClick}
/>
);
};

View File

@@ -0,0 +1,36 @@
import { IconPlus } from 'twenty-ui';
import { useObjectLabel } from '@/object-metadata/hooks/useObjectLabel';
import { RecordTableContext } from '@/object-record/record-table/contexts/RecordTableContext';
import { RecordTableEmptyStateDisplay } from '@/object-record/record-table/empty-state/components/RecordTableEmptyStateDisplay';
import { useCreateNewTableRecord } from '@/object-record/record-table/hooks/useCreateNewTableRecords';
import { useContext } from 'react';
export const RecordTableEmptyStateNoRecordFoundForFilter = () => {
const { createNewTableRecord } = useCreateNewTableRecord();
const { objectMetadataItem } = useContext(RecordTableContext);
const handleButtonClick = () => {
createNewTableRecord();
};
const objectLabel = useObjectLabel(objectMetadataItem);
const buttonTitle = `Add a ${objectLabel}`;
const title = `No ${objectLabel} found`;
const subTitle = 'No records matching the filter criteria were found.';
return (
<RecordTableEmptyStateDisplay
buttonTitle={buttonTitle}
subTitle={subTitle}
title={title}
Icon={IconPlus}
animatedPlaceholderType="noMatchRecord"
onClick={handleButtonClick}
/>
);
};

View File

@@ -0,0 +1,24 @@
/* eslint-disable @nx/workspace-no-navigate-prefer-link */
import { IconSettings } from 'twenty-ui';
import { RecordTableEmptyStateDisplay } from '@/object-record/record-table/empty-state/components/RecordTableEmptyStateDisplay';
import { useNavigate } from 'react-router-dom';
export const RecordTableEmptyStateRemote = () => {
const navigate = useNavigate();
const handleButtonClick = () => {
navigate('/settings/integrations');
};
return (
<RecordTableEmptyStateDisplay
buttonTitle={'Go to Settings'}
subTitle={'If this is unexpected, please verify your settings.'}
title={'No Data Available for Remote Table'}
Icon={IconSettings}
animatedPlaceholderType="noRecord"
onClick={handleButtonClick}
/>
);
};

View File

@@ -0,0 +1,49 @@
import { IconFilterOff } from 'twenty-ui';
import { useObjectLabel } from '@/object-metadata/hooks/useObjectLabel';
import { useHandleToggleTrashColumnFilter } from '@/object-record/record-index/hooks/useHandleToggleTrashColumnFilter';
import { RecordTableContext } from '@/object-record/record-table/contexts/RecordTableContext';
import { RecordTableEmptyStateDisplay } from '@/object-record/record-table/empty-state/components/RecordTableEmptyStateDisplay';
import { useRecordTableStates } from '@/object-record/record-table/hooks/internal/useRecordTableStates';
import { useCombinedViewFilters } from '@/views/hooks/useCombinedViewFilters';
import { useContext } from 'react';
import { useRecoilValue } from 'recoil';
export const RecordTableEmptyStateSoftDelete = () => {
const { objectMetadataItem, objectNameSingular, recordTableId } =
useContext(RecordTableContext);
const { removeCombinedViewFilter } = useCombinedViewFilters(recordTableId);
const { tableFiltersState } = useRecordTableStates(recordTableId);
const tableFilters = useRecoilValue(tableFiltersState);
const { toggleSoftDeleteFilterState } = useHandleToggleTrashColumnFilter({
objectNameSingular,
viewBarId: recordTableId,
});
const handleButtonClick = async () => {
removeCombinedViewFilter(
tableFilters.find(
(filter) =>
filter.definition.label === 'Deleted at' &&
filter.operand === 'isNotEmpty',
)?.id ?? '',
);
toggleSoftDeleteFilterState(false);
};
const objectLabel = useObjectLabel(objectMetadataItem);
return (
<RecordTableEmptyStateDisplay
buttonTitle={'Remove Deleted filter'}
subTitle={'No deleted records matching the filter criteria were found.'}
title={`No Deleted ${objectLabel} found`}
Icon={IconFilterOff}
animatedPlaceholderType="noDeletedRecord"
onClick={handleButtonClick}
/>
);
};

View File

@@ -0,0 +1,39 @@
import { Meta, StoryObj } from '@storybook/react';
import { RecordTableEmptyStateNoRecordAtAll } from '@/object-record/record-table/empty-state/components/RecordTableEmptyStateNoRecordAtAll';
import { RecordTableScope } from '@/object-record/record-table/scopes/RecordTableScope';
import { SnackBarProviderScope } from '@/ui/feedback/snack-bar-manager/scopes/SnackBarProviderScope';
import { ComponentDecorator } from 'twenty-ui';
import { MemoryRouterDecorator } from '~/testing/decorators/MemoryRouterDecorator';
import { ObjectMetadataItemsDecorator } from '~/testing/decorators/ObjectMetadataItemsDecorator';
import { RecordTableDecorator } from '~/testing/decorators/RecordTableDecorator';
import { graphqlMocks } from '~/testing/graphqlMocks';
const meta: Meta = {
title: 'Modules/ObjectRecord/RecordTable/RecordTableEmptyStateNoRecordAtAll',
component: RecordTableEmptyStateNoRecordAtAll,
decorators: [
ComponentDecorator,
MemoryRouterDecorator,
ObjectMetadataItemsDecorator,
RecordTableDecorator,
(Story) => (
<SnackBarProviderScope snackBarManagerScopeId="snack-bar-manager">
<RecordTableScope
recordTableScopeId="persons"
onColumnsChange={() => {}}
>
<Story />
</RecordTableScope>
</SnackBarProviderScope>
),
],
parameters: {
msw: graphqlMocks,
},
};
export default meta;
type Story = StoryObj<typeof RecordTableEmptyStateNoRecordAtAll>;
export const Default: Story = {};

View File

@@ -0,0 +1,40 @@
import { Meta, StoryObj } from '@storybook/react';
import { RecordTableEmptyStateNoRecordFoundForFilter } from '@/object-record/record-table/empty-state/components/RecordTableEmptyStateNoRecordFoundForFilter';
import { RecordTableScope } from '@/object-record/record-table/scopes/RecordTableScope';
import { SnackBarProviderScope } from '@/ui/feedback/snack-bar-manager/scopes/SnackBarProviderScope';
import { ComponentDecorator } from 'twenty-ui';
import { MemoryRouterDecorator } from '~/testing/decorators/MemoryRouterDecorator';
import { ObjectMetadataItemsDecorator } from '~/testing/decorators/ObjectMetadataItemsDecorator';
import { RecordTableDecorator } from '~/testing/decorators/RecordTableDecorator';
import { graphqlMocks } from '~/testing/graphqlMocks';
const meta: Meta = {
title:
'Modules/ObjectRecord/RecordTable/RecordTableEmptyStateNoRecordFoundForFilter',
component: RecordTableEmptyStateNoRecordFoundForFilter,
decorators: [
ComponentDecorator,
MemoryRouterDecorator,
ObjectMetadataItemsDecorator,
RecordTableDecorator,
(Story) => (
<SnackBarProviderScope snackBarManagerScopeId="snack-bar-manager">
<RecordTableScope
recordTableScopeId="persons"
onColumnsChange={() => {}}
>
<Story />
</RecordTableScope>
</SnackBarProviderScope>
),
],
parameters: {
msw: graphqlMocks,
},
};
export default meta;
type Story = StoryObj<typeof RecordTableEmptyStateNoRecordFoundForFilter>;
export const Default: Story = {};

View File

@@ -1,15 +1,22 @@
import { Meta, StoryObj } from '@storybook/react';
import { RecordTableEmptyState } from '@/object-record/record-table/components/RecordTableEmptyState';
import { RecordTableEmptyStateRemote } from '@/object-record/record-table/empty-state/components/RecordTableEmptyStateRemote';
import { RecordTableScope } from '@/object-record/record-table/scopes/RecordTableScope';
import { SnackBarProviderScope } from '@/ui/feedback/snack-bar-manager/scopes/SnackBarProviderScope';
import { ComponentDecorator } from 'twenty-ui';
import { MemoryRouterDecorator } from '~/testing/decorators/MemoryRouterDecorator';
import { ObjectMetadataItemsDecorator } from '~/testing/decorators/ObjectMetadataItemsDecorator';
import { RecordTableDecorator } from '~/testing/decorators/RecordTableDecorator';
import { graphqlMocks } from '~/testing/graphqlMocks';
const meta: Meta = {
title: 'Modules/ObjectRecord/RecordTable/RecordTableEmptyState',
component: RecordTableEmptyState,
title: 'Modules/ObjectRecord/RecordTable/RecordTableEmptyStateRemote',
component: RecordTableEmptyStateRemote,
decorators: [
ComponentDecorator,
MemoryRouterDecorator,
ObjectMetadataItemsDecorator,
RecordTableDecorator,
(Story) => (
<SnackBarProviderScope snackBarManagerScopeId="snack-bar-manager">
<RecordTableScope
@@ -21,25 +28,12 @@ const meta: Meta = {
</SnackBarProviderScope>
),
],
parameters: {
msw: graphqlMocks,
},
};
export default meta;
type Story = StoryObj<typeof RecordTableEmptyState>;
type Story = StoryObj<typeof RecordTableEmptyStateRemote>;
export const Default: Story = {
args: {
objectNameSingular: 'person',
objectLabel: 'person',
isRemote: false,
createRecord: () => {},
},
};
export const Remote: Story = {
args: {
objectNameSingular: 'person',
objectLabel: 'remote person',
isRemote: true,
createRecord: () => {},
},
};
export const Default: Story = {};

View File

@@ -0,0 +1,39 @@
import { Meta, StoryObj } from '@storybook/react';
import { RecordTableEmptyStateSoftDelete } from '@/object-record/record-table/empty-state/components/RecordTableEmptyStateSoftDelete';
import { RecordTableScope } from '@/object-record/record-table/scopes/RecordTableScope';
import { SnackBarProviderScope } from '@/ui/feedback/snack-bar-manager/scopes/SnackBarProviderScope';
import { ComponentDecorator } from 'twenty-ui';
import { MemoryRouterDecorator } from '~/testing/decorators/MemoryRouterDecorator';
import { ObjectMetadataItemsDecorator } from '~/testing/decorators/ObjectMetadataItemsDecorator';
import { RecordTableDecorator } from '~/testing/decorators/RecordTableDecorator';
import { graphqlMocks } from '~/testing/graphqlMocks';
const meta: Meta = {
title: 'Modules/ObjectRecord/RecordTable/RecordTableEmptyStateSoftDelete',
component: RecordTableEmptyStateSoftDelete,
decorators: [
ComponentDecorator,
MemoryRouterDecorator,
ObjectMetadataItemsDecorator,
RecordTableDecorator,
(Story) => (
<SnackBarProviderScope snackBarManagerScopeId="snack-bar-manager">
<RecordTableScope
recordTableScopeId="persons"
onColumnsChange={() => {}}
>
<Story />
</RecordTableScope>
</SnackBarProviderScope>
),
],
parameters: {
msw: graphqlMocks,
},
};
export default meta;
type Story = StoryObj<typeof RecordTableEmptyStateSoftDelete>;
export const Default: Story = {};

View File

@@ -4,6 +4,7 @@ import { RecordTableScopeInternalContext } from '@/object-record/record-table/sc
import { availableTableColumnsComponentState } from '@/object-record/record-table/states/availableTableColumnsComponentState';
import { currentTableCellInEditModePositionComponentState } from '@/object-record/record-table/states/currentTableCellInEditModePositionComponentState';
import { isRecordTableInitialLoadingComponentState } from '@/object-record/record-table/states/isRecordTableInitialLoadingComponentState';
import { isSoftDeleteFilterActiveComponentState } from '@/object-record/record-table/states/isSoftDeleteFilterActiveComponentState';
import { isSoftFocusActiveComponentState } from '@/object-record/record-table/states/isSoftFocusActiveComponentState';
import { isSoftFocusOnTableCellComponentFamilyState } from '@/object-record/record-table/states/isSoftFocusOnTableCellComponentFamilyState';
import { isTableCellInEditModeComponentFamilyState } from '@/object-record/record-table/states/isTableCellInEditModeComponentFamilyState';
@@ -88,6 +89,10 @@ export const useRecordTableStates = (recordTableId?: string) => {
isTableCellInEditModeComponentFamilyState,
scopeId,
),
isSoftDeleteActiveState: extractComponentState(
isSoftDeleteFilterActiveComponentState,
scopeId,
),
isSoftFocusActiveState: extractComponentState(
isSoftFocusActiveComponentState,
scopeId,

View File

@@ -0,0 +1,33 @@
import { RecordTableContext } from '@/object-record/record-table/contexts/RecordTableContext';
import { useRecordTable } from '@/object-record/record-table/hooks/useRecordTable';
import { DEFAULT_CELL_SCOPE } from '@/object-record/record-table/record-table-cell/hooks/useOpenRecordTableCellV2';
import { useSelectedTableCellEditMode } from '@/object-record/record-table/record-table-cell/hooks/useSelectedTableCellEditMode';
import { useSetHotkeyScope } from '@/ui/utilities/hotkey/hooks/useSetHotkeyScope';
import { useContext } from 'react';
import { v4 } from 'uuid';
export const useCreateNewTableRecord = (recordTableIdFromProps?: string) => {
const { recordTableId } = useContext(RecordTableContext);
const recordTableIdToUse = recordTableIdFromProps ?? recordTableId;
const { setSelectedTableCellEditMode } = useSelectedTableCellEditMode({
scopeId: recordTableIdToUse,
});
const setHotkeyScope = useSetHotkeyScope();
const { setPendingRecordId } = useRecordTable({
recordTableId: recordTableIdToUse,
});
const createNewTableRecord = () => {
setPendingRecordId(v4());
setSelectedTableCellEditMode(-1, 0);
setHotkeyScope(DEFAULT_CELL_SCOPE.scope, DEFAULT_CELL_SCOPE.customScopes);
};
return {
createNewTableRecord,
};
};

View File

@@ -73,11 +73,7 @@ const StyledTableHead = styled.thead<{
}
`;
export const RecordTableHeader = ({
createRecord,
}: {
createRecord: () => void;
}) => {
export const RecordTableHeader = () => {
const { visibleTableColumnsSelector } = useRecordTableStates();
const visibleTableColumns = useRecoilValue(visibleTableColumnsSelector());
@@ -88,11 +84,7 @@ export const RecordTableHeader = ({
<RecordTableHeaderDragDropColumn />
<RecordTableHeaderCheckboxColumn />
{visibleTableColumns.map((column) => (
<RecordTableHeaderCell
key={column.fieldMetadataId}
column={column}
createRecord={createRecord}
/>
<RecordTableHeaderCell key={column.fieldMetadataId} column={column} />
))}
<RecordTableHeaderLastColumn />
</tr>

View File

@@ -5,6 +5,7 @@ import { IconPlus } from 'twenty-ui';
import { FieldMetadata } from '@/object-record/record-field/types/FieldMetadata';
import { useRecordTableStates } from '@/object-record/record-table/hooks/internal/useRecordTableStates';
import { useCreateNewTableRecord } from '@/object-record/record-table/hooks/useCreateNewTableRecords';
import { useTableColumns } from '@/object-record/record-table/hooks/useTableColumns';
import { RecordTableColumnHeadWithDropdown } from '@/object-record/record-table/record-table-header/components/RecordTableColumnHeadWithDropdown';
import { isRecordTableScrolledLeftComponentState } from '@/object-record/record-table/states/isRecordTableScrolledLeftComponentState';
@@ -90,10 +91,8 @@ const StyledHeaderIcon = styled.div`
export const RecordTableHeaderCell = ({
column,
createRecord,
}: {
column: ColumnDefinition<FieldMetadata>;
createRecord: () => void;
}) => {
const { resizeFieldOffsetState, tableColumnsState } = useRecordTableStates();
@@ -185,6 +184,12 @@ export const RecordTableHeaderCell = ({
const disableColumnResize =
column.isLabelIdentifier && isMobile && !isRecordTableScrolledLeft;
const { createNewTableRecord } = useCreateNewTableRecord();
const handlePlusButtonClick = () => {
createNewTableRecord();
};
return (
<StyledColumnHeaderCell
key={column.fieldMetadataId}
@@ -206,7 +211,7 @@ export const RecordTableHeaderCell = ({
Icon={IconPlus}
size="small"
accent="tertiary"
onClick={createRecord}
onClick={handlePlusButtonClick}
/>
</StyledHeaderIcon>
)}

View File

@@ -0,0 +1,7 @@
import { createComponentState } from '@/ui/utilities/state/component-state/utils/createComponentState';
export const isSoftDeleteFilterActiveComponentState =
createComponentState<boolean>({
key: 'isSoftDeleteFilterActiveComponentState',
defaultValue: false,
});

View File

@@ -41,7 +41,6 @@ export const SignInBackgroundMockContainer = () => {
objectNameSingular={objectNameSingular}
recordTableId={recordIndexId}
viewBarId={viewBarId}
createRecord={async () => {}}
updateRecordMutation={() => {}}
/>
</StyledContainer>

View File

@@ -1,7 +1,7 @@
import { useEffect } from 'react';
import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import { animate, motion, useMotionValue, useTransform } from 'framer-motion';
import { useEffect } from 'react';
import { BACKGROUND } from '@/ui/layout/animated-placeholder/constants/Background';
import { DARK_BACKGROUND } from '@/ui/layout/animated-placeholder/constants/DarkBackground';
@@ -35,8 +35,12 @@ const StyledMovingImage = styled(motion.img)<StyledImageProps>`
z-index: 2;
`;
export type AnimatedPlaceholderType =
| keyof typeof BACKGROUND
| keyof typeof MOVING_IMAGE;
interface AnimatedPlaceholderProps {
type: keyof typeof BACKGROUND | keyof typeof MOVING_IMAGE;
type: AnimatedPlaceholderType;
}
const AnimatedPlaceholder = ({ type }: AnimatedPlaceholderProps) => {

View File

@@ -12,4 +12,5 @@ export const BACKGROUND: Record<string, string> = {
emptyInbox: '/images/placeholders/background/empty_inbox_bg.png',
error404: '/images/placeholders/background/404_bg.png',
error500: '/images/placeholders/background/500_bg.png',
noDeletedRecord: '/images/placeholders/background/no_deleted_record_bg.png',
};

View File

@@ -12,4 +12,6 @@ export const DARK_BACKGROUND: Record<string, string> = {
loadingMessages: '/images/placeholders/background/loading_messages_bg.png',
loadingAccounts: '/images/placeholders/background/loading_accounts_bg.png',
emptyFunctions: '/images/placeholders/dark-background/empty_functions_bg.png',
noDeletedRecord:
'/images/placeholders/dark-background/no_deleted_record_bg.png',
};

View File

@@ -12,4 +12,6 @@ export const DARK_MOVING_IMAGE: Record<string, string> = {
loadingMessages: '/images/placeholders/moving-image/loading_messages.png',
loadingAccounts: '/images/placeholders/moving-image/loading_accounts.png',
emptyFunctions: '/images/placeholders/dark-moving-image/empty_functions.png',
noDeletedRecord:
'/images/placeholders/dark-moving-image/no_deleted_record.png',
};

View File

@@ -12,4 +12,5 @@ export const MOVING_IMAGE: Record<string, string> = {
emptyInbox: '/images/placeholders/moving-image/empty_inbox.png',
error404: '/images/placeholders/moving-image/404.png',
error500: '/images/placeholders/moving-image/500.png',
noDeletedRecord: '/images/placeholders/moving-image/no_deleted_record.png',
};

View File

@@ -1,20 +1,44 @@
import { useIcons } from 'twenty-ui';
import { useObjectNameSingularFromPlural } from '@/object-metadata/hooks/useObjectNameSingularFromPlural';
import { Filter } from '@/object-record/object-filter-dropdown/types/Filter';
import { useHandleToggleTrashColumnFilter } from '@/object-record/record-index/hooks/useHandleToggleTrashColumnFilter';
import { SortOrFilterChip } from '@/views/components/SortOrFilterChip';
import { useCombinedViewFilters } from '@/views/hooks/useCombinedViewFilters';
import { useParams } from 'react-router-dom';
type VariantFilterChipProps = {
viewFilter: Filter;
viewBarId: string;
};
export const VariantFilterChip = ({ viewFilter }: VariantFilterChipProps) => {
export const VariantFilterChip = ({
viewFilter,
viewBarId,
}: VariantFilterChipProps) => {
const { removeCombinedViewFilter } = useCombinedViewFilters();
const { objectNamePlural } = useParams();
const { objectNameSingular } = useObjectNameSingularFromPlural({
objectNamePlural: objectNamePlural ?? '',
});
const { toggleSoftDeleteFilterState } = useHandleToggleTrashColumnFilter({
objectNameSingular,
viewBarId,
});
const { getIcon } = useIcons();
const handleRemoveClick = () => {
removeCombinedViewFilter(viewFilter.id);
if (
viewFilter.definition.label === 'Deleted' &&
viewFilter.operand === 'isNotEmpty'
) {
toggleSoftDeleteFilterState(false);
}
};
return (

View File

@@ -97,6 +97,7 @@ export const ViewBarDetails = ({
hasFilterButton = false,
rightComponent,
filterDropdownId,
viewBarId,
}: ViewBarDetailsProps) => {
const {
canPersistViewSelector,
@@ -169,6 +170,7 @@ export const ViewBarDetails = ({
// Also as filter is spread into viewFilter, definition is present
// FixMe: Ugly hack to make it work
viewFilter={viewFilter as unknown as Filter}
viewBarId={viewBarId}
/>
))}
{!!otherViewFilters.length &&

View File

@@ -154,6 +154,7 @@ export const useCombinedViewFilters = (viewBarComponentId?: string) => {
unsavedToUpsertViewFiltersState,
],
);
return {
upsertCombinedViewFilter,
removeCombinedViewFilter,

View File

@@ -1,16 +1,18 @@
import styled from '@emotion/styled';
import { useParams } from 'react-router-dom';
import { v4 } from 'uuid';
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
import { useObjectNameSingularFromPlural } from '@/object-metadata/hooks/useObjectNameSingularFromPlural';
import { lastShowPageRecordIdState } from '@/object-record/record-field/states/lastShowPageRecordId';
import { RecordIndexContainer } from '@/object-record/record-index/components/RecordIndexContainer';
import { RecordIndexPageHeader } from '@/object-record/record-index/components/RecordIndexPageHeader';
import { useRecordTable } from '@/object-record/record-table/hooks/useRecordTable';
import { DEFAULT_CELL_SCOPE } from '@/object-record/record-table/record-table-cell/hooks/useOpenRecordTableCellV2';
import { useSelectedTableCellEditMode } from '@/object-record/record-table/record-table-cell/hooks/useSelectedTableCellEditMode';
import { RecordIndexRootPropsContext } from '@/object-record/record-index/contexts/RecordIndexRootPropsContext';
import { useHandleIndexIdentifierClick } from '@/object-record/record-index/hooks/useHandleIndexIdentifierClick';
import { useCreateNewTableRecord } from '@/object-record/record-table/hooks/useCreateNewTableRecords';
import { PageBody } from '@/ui/layout/page/PageBody';
import { PageContainer } from '@/ui/layout/page/PageContainer';
import { useSetHotkeyScope } from '@/ui/utilities/hotkey/hooks/useSetHotkeyScope';
import { PageTitle } from '@/ui/utilities/page-title/PageTitle';
import { useRecoilCallback } from 'recoil';
import { capitalize } from '~/utils/string/capitalize';
const StyledIndexContainer = styled.div`
@@ -23,39 +25,55 @@ export const RecordIndexPage = () => {
const objectNamePlural = useParams().objectNamePlural ?? '';
const recordIndexId = objectNamePlural ?? '';
const setHotkeyScope = useSetHotkeyScope();
const { setSelectedTableCellEditMode } = useSelectedTableCellEditMode({
scopeId: recordIndexId,
const { objectNameSingular } = useObjectNameSingularFromPlural({
objectNamePlural,
});
const { setPendingRecordId } = useRecordTable({
recordTableId: recordIndexId,
const { objectMetadataItem } = useObjectMetadataItem({
objectNameSingular,
});
const handleAddButtonClick = async () => {
setPendingRecordId(v4());
setSelectedTableCellEditMode(-1, 0);
setHotkeyScope(DEFAULT_CELL_SCOPE.scope, DEFAULT_CELL_SCOPE.customScopes);
const { createNewTableRecord } = useCreateNewTableRecord(recordIndexId);
const handleCreateRecord = () => {
createNewTableRecord();
};
const { handleIndexIdentifierClick } = useHandleIndexIdentifierClick({
objectMetadataItem,
recordIndexId,
});
const handleIndexRecordsLoaded = useRecoilCallback(
({ set }) =>
() => {
// TODO: find a better way to reset this state ?
set(lastShowPageRecordIdState, null);
},
[],
);
return (
<PageContainer>
<PageTitle title={`${capitalize(objectNamePlural)}`} />
<RecordIndexPageHeader
createRecord={handleAddButtonClick}
recordIndexId={recordIndexId}
objectNamePlural={objectNamePlural}
/>
<PageBody>
<StyledIndexContainer>
<RecordIndexContainer
recordIndexId={recordIndexId}
objectNamePlural={objectNamePlural}
createRecord={handleAddButtonClick}
/>
</StyledIndexContainer>
</PageBody>
<RecordIndexRootPropsContext.Provider
value={{
recordIndexId,
objectNamePlural,
objectNameSingular,
onIndexRecordsLoaded: handleIndexRecordsLoaded,
onIndexIdentifierClick: handleIndexIdentifierClick,
onCreateRecord: handleCreateRecord,
}}
>
<PageTitle title={`${capitalize(objectNamePlural)}`} />
<RecordIndexPageHeader />
<PageBody>
<StyledIndexContainer>
<RecordIndexContainer />
</StyledIndexContainer>
</PageBody>
</RecordIndexRootPropsContext.Provider>
</PageContainer>
);
};

View File

@@ -0,0 +1,39 @@
import { Decorator } from '@storybook/react';
import { useRecoilValue } from 'recoil';
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
import { RecordTableContext } from '@/object-record/record-table/contexts/RecordTableContext';
import { isDefined } from 'twenty-ui';
export const RecordTableDecorator: Decorator = (Story) => {
const objectMetadataItems = useRecoilValue(objectMetadataItemsState);
const personObjectMetadataItem = objectMetadataItems.find(
(objectMetadataItem) => objectMetadataItem.nameSingular === 'person',
);
if (!isDefined(personObjectMetadataItem)) {
return <Story />;
}
return (
<RecordTableContext.Provider
value={{
objectNameSingular: personObjectMetadataItem?.nameSingular,
objectMetadataItem: personObjectMetadataItem,
onCellMouseEnter: () => {},
onCloseTableCell: () => {},
onOpenTableCell: () => {},
onContextMenu: () => {},
onMoveFocus: () => {},
onMoveSoftFocusToCell: () => {},
onUpsertRecord: () => {},
recordTableId: 'persons',
viewBarId: 'view-bar',
visibleTableColumns: [],
}}
>
<Story />
</RecordTableContext.Provider>
);
};

View File

@@ -0,0 +1,11 @@
import { Context, createContext } from 'react';
type RootProps = Record<string, any>;
export type RootPropsContext<T extends RootProps> = T extends RootProps
? T
: never;
export const createRootPropsContext = <T extends RootProps>(): Context<
RootPropsContext<T>
> => createContext<RootPropsContext<T>>({} as RootPropsContext<T>);