mirror of
https://github.com/lingble/twenty.git
synced 2025-10-29 20:02:29 +00:00
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:
committed by
GitHub
parent
9c885861a3
commit
601e15f028
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 |
@@ -0,0 +1,5 @@
|
||||
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||
|
||||
export const useObjectIsRemote = (objectMetadataItem: ObjectMetadataItem) => {
|
||||
return objectMetadataItem.isRemote ?? false;
|
||||
};
|
||||
@@ -0,0 +1,5 @@
|
||||
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||
|
||||
export const useObjectLabel = (objectMetadataItem: ObjectMetadataItem) => {
|
||||
return objectMetadataItem?.labelSingular ?? '';
|
||||
};
|
||||
@@ -14,7 +14,6 @@ type RecordIndexBoardContainerProps = {
|
||||
recordBoardId: string;
|
||||
viewBarId: string;
|
||||
objectNameSingular: string;
|
||||
createRecord: () => Promise<void>;
|
||||
};
|
||||
|
||||
export const RecordIndexBoardContainer = ({
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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} />
|
||||
|
||||
@@ -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>();
|
||||
@@ -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,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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}
|
||||
|
||||
@@ -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 />;
|
||||
}
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -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}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -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}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -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}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -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 = {};
|
||||
@@ -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 = {};
|
||||
@@ -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 = {};
|
||||
@@ -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 = {};
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
};
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
import { createComponentState } from '@/ui/utilities/state/component-state/utils/createComponentState';
|
||||
|
||||
export const isSoftDeleteFilterActiveComponentState =
|
||||
createComponentState<boolean>({
|
||||
key: 'isSoftDeleteFilterActiveComponentState',
|
||||
defaultValue: false,
|
||||
});
|
||||
@@ -41,7 +41,6 @@ export const SignInBackgroundMockContainer = () => {
|
||||
objectNameSingular={objectNameSingular}
|
||||
recordTableId={recordIndexId}
|
||||
viewBarId={viewBarId}
|
||||
createRecord={async () => {}}
|
||||
updateRecordMutation={() => {}}
|
||||
/>
|
||||
</StyledContainer>
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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',
|
||||
};
|
||||
|
||||
@@ -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',
|
||||
};
|
||||
|
||||
@@ -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',
|
||||
};
|
||||
|
||||
@@ -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',
|
||||
};
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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 &&
|
||||
|
||||
@@ -154,6 +154,7 @@ export const useCombinedViewFilters = (viewBarComponentId?: string) => {
|
||||
unsavedToUpsertViewFiltersState,
|
||||
],
|
||||
);
|
||||
|
||||
return {
|
||||
upsertCombinedViewFilter,
|
||||
removeCombinedViewFilter,
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
11
packages/twenty-front/src/utils/createRootPropsContext.ts
Normal file
11
packages/twenty-front/src/utils/createRootPropsContext.ts
Normal 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>);
|
||||
Reference in New Issue
Block a user