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;
|
recordBoardId: string;
|
||||||
viewBarId: string;
|
viewBarId: string;
|
||||||
objectNameSingular: string;
|
objectNameSingular: string;
|
||||||
createRecord: () => Promise<void>;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const RecordIndexBoardContainer = ({
|
export const RecordIndexBoardContainer = ({
|
||||||
|
|||||||
@ -4,14 +4,12 @@ import { useRecoilCallback, useRecoilState, useSetRecoilState } from 'recoil';
|
|||||||
import { useColumnDefinitionsFromFieldMetadata } from '@/object-metadata/hooks/useColumnDefinitionsFromFieldMetadata';
|
import { useColumnDefinitionsFromFieldMetadata } from '@/object-metadata/hooks/useColumnDefinitionsFromFieldMetadata';
|
||||||
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
|
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
|
||||||
import { useObjectNameSingularFromPlural } from '@/object-metadata/hooks/useObjectNameSingularFromPlural';
|
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 { RecordIndexBoardContainer } from '@/object-record/record-index/components/RecordIndexBoardContainer';
|
||||||
import { RecordIndexBoardDataLoader } from '@/object-record/record-index/components/RecordIndexBoardDataLoader';
|
import { RecordIndexBoardDataLoader } from '@/object-record/record-index/components/RecordIndexBoardDataLoader';
|
||||||
import { RecordIndexBoardDataLoaderEffect } from '@/object-record/record-index/components/RecordIndexBoardDataLoaderEffect';
|
import { RecordIndexBoardDataLoaderEffect } from '@/object-record/record-index/components/RecordIndexBoardDataLoaderEffect';
|
||||||
import { RecordIndexTableContainer } from '@/object-record/record-index/components/RecordIndexTableContainer';
|
import { RecordIndexTableContainer } from '@/object-record/record-index/components/RecordIndexTableContainer';
|
||||||
import { RecordIndexTableContainerEffect } from '@/object-record/record-index/components/RecordIndexTableContainerEffect';
|
import { RecordIndexTableContainerEffect } from '@/object-record/record-index/components/RecordIndexTableContainerEffect';
|
||||||
import { RecordIndexViewBarEffect } from '@/object-record/record-index/components/RecordIndexViewBarEffect';
|
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 { RecordIndexOptionsDropdown } from '@/object-record/record-index/options/components/RecordIndexOptionsDropdown';
|
||||||
import { recordIndexFieldDefinitionsState } from '@/object-record/record-index/states/recordIndexFieldDefinitionsState';
|
import { recordIndexFieldDefinitionsState } from '@/object-record/record-index/states/recordIndexFieldDefinitionsState';
|
||||||
import { recordIndexFiltersState } from '@/object-record/record-index/states/recordIndexFiltersState';
|
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 { recordIndexViewTypeState } from '@/object-record/record-index/states/recordIndexViewTypeState';
|
||||||
|
|
||||||
import { InformationBannerWrapper } from '@/information-banner/components/InformationBannerWrapper';
|
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 { RecordFieldValueSelectorContextProvider } from '@/object-record/record-store/contexts/RecordFieldValueSelectorContext';
|
||||||
import { useRecordTable } from '@/object-record/record-table/hooks/useRecordTable';
|
import { useRecordTable } from '@/object-record/record-table/hooks/useRecordTable';
|
||||||
import { SpreadsheetImportProvider } from '@/spreadsheet-import/provider/components/SpreadsheetImportProvider';
|
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 { mapViewFieldsToColumnDefinitions } from '@/views/utils/mapViewFieldsToColumnDefinitions';
|
||||||
import { mapViewFiltersToFilters } from '@/views/utils/mapViewFiltersToFilters';
|
import { mapViewFiltersToFilters } from '@/views/utils/mapViewFiltersToFilters';
|
||||||
import { mapViewSortsToSorts } from '@/views/utils/mapViewSortsToSorts';
|
import { mapViewSortsToSorts } from '@/views/utils/mapViewSortsToSorts';
|
||||||
|
import { useContext } from 'react';
|
||||||
import { isDeeplyEqual } from '~/utils/isDeeplyEqual';
|
import { isDeeplyEqual } from '~/utils/isDeeplyEqual';
|
||||||
|
|
||||||
const StyledContainer = styled.div`
|
const StyledContainer = styled.div`
|
||||||
@ -46,20 +45,15 @@ const StyledContainerWithPadding = styled.div<{ fullHeight?: boolean }>`
|
|||||||
padding-left: ${({ theme }) => theme.table.horizontalCellPadding};
|
padding-left: ${({ theme }) => theme.table.horizontalCellPadding};
|
||||||
`;
|
`;
|
||||||
|
|
||||||
type RecordIndexContainerProps = {
|
export const RecordIndexContainer = () => {
|
||||||
recordIndexId: string;
|
|
||||||
objectNamePlural: string;
|
|
||||||
createRecord: () => Promise<void>;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const RecordIndexContainer = ({
|
|
||||||
createRecord,
|
|
||||||
recordIndexId,
|
|
||||||
objectNamePlural,
|
|
||||||
}: RecordIndexContainerProps) => {
|
|
||||||
const [recordIndexViewType, setRecordIndexViewType] = useRecoilState(
|
const [recordIndexViewType, setRecordIndexViewType] = useRecoilState(
|
||||||
recordIndexViewTypeState,
|
recordIndexViewTypeState,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const { objectNamePlural, recordIndexId } = useContext(
|
||||||
|
RecordIndexRootPropsContext,
|
||||||
|
);
|
||||||
|
|
||||||
const { objectNameSingular } = useObjectNameSingularFromPlural({
|
const { objectNameSingular } = useObjectNameSingularFromPlural({
|
||||||
objectNamePlural,
|
objectNamePlural,
|
||||||
});
|
});
|
||||||
@ -110,20 +104,6 @@ export const RecordIndexContainer = ({
|
|||||||
[columnDefinitions, setTableColumns],
|
[columnDefinitions, setTableColumns],
|
||||||
);
|
);
|
||||||
|
|
||||||
const { handleIndexIdentifierClick } = useHandleIndexIdentifierClick({
|
|
||||||
objectMetadataItem,
|
|
||||||
recordIndexId,
|
|
||||||
});
|
|
||||||
|
|
||||||
const handleIndexRecordsLoaded = useRecoilCallback(
|
|
||||||
({ set }) =>
|
|
||||||
() => {
|
|
||||||
// TODO: find a better way to reset this state ?
|
|
||||||
set(lastShowPageRecordIdState, null);
|
|
||||||
},
|
|
||||||
[],
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<StyledContainer>
|
<StyledContainer>
|
||||||
<InformationBannerWrapper />
|
<InformationBannerWrapper />
|
||||||
@ -170,46 +150,37 @@ export const RecordIndexContainer = ({
|
|||||||
/>
|
/>
|
||||||
</StyledContainerWithPadding>
|
</StyledContainerWithPadding>
|
||||||
</SpreadsheetImportProvider>
|
</SpreadsheetImportProvider>
|
||||||
<RecordIndexEventContext.Provider
|
|
||||||
value={{
|
{recordIndexViewType === ViewType.Table && (
|
||||||
onIndexIdentifierClick: handleIndexIdentifierClick,
|
<>
|
||||||
onIndexRecordsLoaded: handleIndexRecordsLoaded,
|
<RecordIndexTableContainer
|
||||||
}}
|
recordTableId={recordIndexId}
|
||||||
>
|
viewBarId={recordIndexId}
|
||||||
{recordIndexViewType === ViewType.Table && (
|
/>
|
||||||
<>
|
<RecordIndexTableContainerEffect
|
||||||
<RecordIndexTableContainer
|
objectNameSingular={objectNameSingular}
|
||||||
recordTableId={recordIndexId}
|
recordTableId={recordIndexId}
|
||||||
viewBarId={recordIndexId}
|
viewBarId={recordIndexId}
|
||||||
objectNameSingular={objectNameSingular}
|
/>
|
||||||
createRecord={createRecord}
|
</>
|
||||||
/>
|
)}
|
||||||
<RecordIndexTableContainerEffect
|
{recordIndexViewType === ViewType.Kanban && (
|
||||||
objectNameSingular={objectNameSingular}
|
<StyledContainerWithPadding fullHeight>
|
||||||
recordTableId={recordIndexId}
|
<RecordIndexBoardContainer
|
||||||
viewBarId={recordIndexId}
|
recordBoardId={recordIndexId}
|
||||||
/>
|
viewBarId={recordIndexId}
|
||||||
</>
|
objectNameSingular={objectNameSingular}
|
||||||
)}
|
/>
|
||||||
{recordIndexViewType === ViewType.Kanban && (
|
<RecordIndexBoardDataLoader
|
||||||
<StyledContainerWithPadding fullHeight>
|
objectNameSingular={objectNameSingular}
|
||||||
<RecordIndexBoardContainer
|
recordBoardId={recordIndexId}
|
||||||
recordBoardId={recordIndexId}
|
/>
|
||||||
viewBarId={recordIndexId}
|
<RecordIndexBoardDataLoaderEffect
|
||||||
objectNameSingular={objectNameSingular}
|
objectNameSingular={objectNameSingular}
|
||||||
createRecord={createRecord}
|
recordBoardId={recordIndexId}
|
||||||
/>
|
/>
|
||||||
<RecordIndexBoardDataLoader
|
</StyledContainerWithPadding>
|
||||||
objectNameSingular={objectNameSingular}
|
)}
|
||||||
recordBoardId={recordIndexId}
|
|
||||||
/>
|
|
||||||
<RecordIndexBoardDataLoaderEffect
|
|
||||||
objectNameSingular={objectNameSingular}
|
|
||||||
recordBoardId={recordIndexId}
|
|
||||||
/>
|
|
||||||
</StyledContainerWithPadding>
|
|
||||||
)}
|
|
||||||
</RecordIndexEventContext.Provider>
|
|
||||||
</RecordFieldValueSelectorContextProvider>
|
</RecordFieldValueSelectorContextProvider>
|
||||||
</StyledContainer>
|
</StyledContainer>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -3,27 +3,23 @@ import { useIcons } from 'twenty-ui';
|
|||||||
|
|
||||||
import { useFilteredObjectMetadataItems } from '@/object-metadata/hooks/useFilteredObjectMetadataItems';
|
import { useFilteredObjectMetadataItems } from '@/object-metadata/hooks/useFilteredObjectMetadataItems';
|
||||||
import { RecordIndexPageKanbanAddButton } from '@/object-record/record-index/components/RecordIndexPageKanbanAddButton';
|
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 { recordIndexViewTypeState } from '@/object-record/record-index/states/recordIndexViewTypeState';
|
||||||
import { PageAddButton } from '@/ui/layout/page/PageAddButton';
|
import { PageAddButton } from '@/ui/layout/page/PageAddButton';
|
||||||
import { PageHeader } from '@/ui/layout/page/PageHeader';
|
import { PageHeader } from '@/ui/layout/page/PageHeader';
|
||||||
import { PageHotkeysEffect } from '@/ui/layout/page/PageHotkeysEffect';
|
import { PageHotkeysEffect } from '@/ui/layout/page/PageHotkeysEffect';
|
||||||
import { ViewType } from '@/views/types/ViewType';
|
import { ViewType } from '@/views/types/ViewType';
|
||||||
|
import { useContext } from 'react';
|
||||||
import { capitalize } from '~/utils/string/capitalize';
|
import { capitalize } from '~/utils/string/capitalize';
|
||||||
|
|
||||||
type RecordIndexPageHeaderProps = {
|
export const RecordIndexPageHeader = () => {
|
||||||
createRecord: () => void;
|
|
||||||
recordIndexId: string;
|
|
||||||
objectNamePlural: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const RecordIndexPageHeader = ({
|
|
||||||
createRecord,
|
|
||||||
recordIndexId,
|
|
||||||
objectNamePlural,
|
|
||||||
}: RecordIndexPageHeaderProps) => {
|
|
||||||
const { findObjectMetadataItemByNamePlural } =
|
const { findObjectMetadataItemByNamePlural } =
|
||||||
useFilteredObjectMetadataItems();
|
useFilteredObjectMetadataItems();
|
||||||
|
|
||||||
|
const { objectNamePlural, onCreateRecord } = useContext(
|
||||||
|
RecordIndexRootPropsContext,
|
||||||
|
);
|
||||||
|
|
||||||
const objectMetadataItem =
|
const objectMetadataItem =
|
||||||
findObjectMetadataItemByNamePlural(objectNamePlural);
|
findObjectMetadataItemByNamePlural(objectNamePlural);
|
||||||
|
|
||||||
@ -40,16 +36,17 @@ export const RecordIndexPageHeader = ({
|
|||||||
const pageHeaderTitle =
|
const pageHeaderTitle =
|
||||||
objectMetadataItem?.labelPlural ?? capitalize(objectNamePlural);
|
objectMetadataItem?.labelPlural ?? capitalize(objectNamePlural);
|
||||||
|
|
||||||
|
const handleAddButtonClick = () => {
|
||||||
|
onCreateRecord();
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PageHeader title={pageHeaderTitle} Icon={Icon}>
|
<PageHeader title={pageHeaderTitle} Icon={Icon}>
|
||||||
<PageHotkeysEffect onAddButtonClick={createRecord} />
|
<PageHotkeysEffect onAddButtonClick={handleAddButtonClick} />
|
||||||
{isTable ? (
|
{isTable ? (
|
||||||
<PageAddButton onClick={createRecord} />
|
<PageAddButton onClick={handleAddButtonClick} />
|
||||||
) : (
|
) : (
|
||||||
<RecordIndexPageKanbanAddButton
|
<RecordIndexPageKanbanAddButton />
|
||||||
recordIndexId={recordIndexId}
|
|
||||||
objectNamePlural={objectNamePlural}
|
|
||||||
/>
|
|
||||||
)}
|
)}
|
||||||
</PageHeader>
|
</PageHeader>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -2,6 +2,7 @@ import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSi
|
|||||||
import { useRecordBoardStates } from '@/object-record/record-board/hooks/internal/useRecordBoardStates';
|
import { useRecordBoardStates } from '@/object-record/record-board/hooks/internal/useRecordBoardStates';
|
||||||
import { RecordBoardColumnDefinition } from '@/object-record/record-board/types/RecordBoardColumnDefinition';
|
import { RecordBoardColumnDefinition } from '@/object-record/record-board/types/RecordBoardColumnDefinition';
|
||||||
import { RecordIndexPageKanbanAddMenuItem } from '@/object-record/record-index/components/RecordIndexPageKanbanAddMenuItem';
|
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 { useRecordIndexPageKanbanAddButton } from '@/object-record/record-index/hooks/useRecordIndexPageKanbanAddButton';
|
||||||
import { SingleEntitySelect } from '@/object-record/relation-picker/components/SingleEntitySelect';
|
import { SingleEntitySelect } from '@/object-record/relation-picker/components/SingleEntitySelect';
|
||||||
import { useEntitySelectSearch } from '@/object-record/relation-picker/hooks/useEntitySelectSearch';
|
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 { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown';
|
||||||
import { usePreviousHotkeyScope } from '@/ui/utilities/hotkey/hooks/usePreviousHotkeyScope';
|
import { usePreviousHotkeyScope } from '@/ui/utilities/hotkey/hooks/usePreviousHotkeyScope';
|
||||||
import styled from '@emotion/styled';
|
import styled from '@emotion/styled';
|
||||||
import { useCallback, useState } from 'react';
|
import { useCallback, useContext, useState } from 'react';
|
||||||
import { useRecoilValue } from 'recoil';
|
import { useRecoilValue } from 'recoil';
|
||||||
import { IconPlus, isDefined } from 'twenty-ui';
|
import { IconPlus, isDefined } from 'twenty-ui';
|
||||||
|
|
||||||
@ -26,20 +27,16 @@ const StyledDropDownMenu = styled(DropdownMenu)`
|
|||||||
width: 200px;
|
width: 200px;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
type RecordIndexPageKanbanAddButtonProps = {
|
export const RecordIndexPageKanbanAddButton = () => {
|
||||||
recordIndexId: string;
|
|
||||||
objectNamePlural: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const RecordIndexPageKanbanAddButton = ({
|
|
||||||
recordIndexId,
|
|
||||||
objectNamePlural,
|
|
||||||
}: RecordIndexPageKanbanAddButtonProps) => {
|
|
||||||
const dropdownId = `record-index-page-add-button-dropdown`;
|
const dropdownId = `record-index-page-add-button-dropdown`;
|
||||||
const [isSelectingCompany, setIsSelectingCompany] = useState(false);
|
const [isSelectingCompany, setIsSelectingCompany] = useState(false);
|
||||||
const [selectedColumnDefinition, setSelectedColumnDefinition] =
|
const [selectedColumnDefinition, setSelectedColumnDefinition] =
|
||||||
useState<RecordBoardColumnDefinition>();
|
useState<RecordBoardColumnDefinition>();
|
||||||
|
|
||||||
|
const { recordIndexId, objectNamePlural } = useContext(
|
||||||
|
RecordIndexRootPropsContext,
|
||||||
|
);
|
||||||
|
|
||||||
const { columnIdsState } = useRecordBoardStates(recordIndexId);
|
const { columnIdsState } = useRecordBoardStates(recordIndexId);
|
||||||
const columnIds = useRecoilValue(columnIdsState);
|
const columnIds = useRecoilValue(columnIdsState);
|
||||||
|
|
||||||
|
|||||||
@ -1,23 +1,23 @@
|
|||||||
import { useUpdateOneRecord } from '@/object-record/hooks/useUpdateOneRecord';
|
import { useUpdateOneRecord } from '@/object-record/hooks/useUpdateOneRecord';
|
||||||
import { RecordUpdateHookParams } from '@/object-record/record-field/contexts/FieldContext';
|
import { RecordUpdateHookParams } from '@/object-record/record-field/contexts/FieldContext';
|
||||||
import { RecordIndexRemoveSortingModal } from '@/object-record/record-index/components/RecordIndexRemoveSortingModal';
|
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 { RecordTableActionBar } from '@/object-record/record-table/action-bar/components/RecordTableActionBar';
|
||||||
import { RecordTableWithWrappers } from '@/object-record/record-table/components/RecordTableWithWrappers';
|
import { RecordTableWithWrappers } from '@/object-record/record-table/components/RecordTableWithWrappers';
|
||||||
import { RecordTableContextMenu } from '@/object-record/record-table/context-menu/components/RecordTableContextMenu';
|
import { RecordTableContextMenu } from '@/object-record/record-table/context-menu/components/RecordTableContextMenu';
|
||||||
|
import { useContext } from 'react';
|
||||||
|
|
||||||
type RecordIndexTableContainerProps = {
|
type RecordIndexTableContainerProps = {
|
||||||
recordTableId: string;
|
recordTableId: string;
|
||||||
viewBarId: string;
|
viewBarId: string;
|
||||||
objectNameSingular: string;
|
|
||||||
createRecord: () => Promise<void>;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const RecordIndexTableContainer = ({
|
export const RecordIndexTableContainer = ({
|
||||||
recordTableId,
|
recordTableId,
|
||||||
viewBarId,
|
viewBarId,
|
||||||
objectNameSingular,
|
|
||||||
createRecord,
|
|
||||||
}: RecordIndexTableContainerProps) => {
|
}: RecordIndexTableContainerProps) => {
|
||||||
|
const { objectNameSingular } = useContext(RecordIndexRootPropsContext);
|
||||||
|
|
||||||
const { updateOneRecord } = useUpdateOneRecord({
|
const { updateOneRecord } = useUpdateOneRecord({
|
||||||
objectNameSingular,
|
objectNameSingular,
|
||||||
});
|
});
|
||||||
@ -36,7 +36,6 @@ export const RecordIndexTableContainer = ({
|
|||||||
objectNameSingular={objectNameSingular}
|
objectNameSingular={objectNameSingular}
|
||||||
viewBarId={viewBarId}
|
viewBarId={viewBarId}
|
||||||
updateRecordMutation={updateEntity}
|
updateRecordMutation={updateEntity}
|
||||||
createRecord={createRecord}
|
|
||||||
/>
|
/>
|
||||||
<RecordTableActionBar recordTableId={recordTableId} />
|
<RecordTableActionBar recordTableId={recordTableId} />
|
||||||
<RecordIndexRemoveSortingModal 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 { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
|
||||||
import { getFilterTypeFromFieldType } from '@/object-metadata/utils/formatFieldMetadataItemsAsFilterDefinitions';
|
import { getFilterTypeFromFieldType } from '@/object-metadata/utils/formatFieldMetadataItemsAsFilterDefinitions';
|
||||||
import { Filter } from '@/object-record/object-filter-dropdown/types/Filter';
|
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 { useCombinedViewFilters } from '@/views/hooks/useCombinedViewFilters';
|
||||||
import { ViewFilterOperand } from '@/views/types/ViewFilterOperand';
|
import { ViewFilterOperand } from '@/views/types/ViewFilterOperand';
|
||||||
|
import { useRecoilCallback } from 'recoil';
|
||||||
import { isDefined } from '~/utils/isDefined';
|
import { isDefined } from '~/utils/isDefined';
|
||||||
|
|
||||||
type UseHandleToggleTrashColumnFilterProps = {
|
type UseHandleToggleTrashColumnFilterProps = {
|
||||||
@ -26,6 +28,7 @@ export const useHandleToggleTrashColumnFilter = ({
|
|||||||
useColumnDefinitionsFromFieldMetadata(objectMetadataItem);
|
useColumnDefinitionsFromFieldMetadata(objectMetadataItem);
|
||||||
|
|
||||||
const { upsertCombinedViewFilter } = useCombinedViewFilters(viewBarId);
|
const { upsertCombinedViewFilter } = useCombinedViewFilters(viewBarId);
|
||||||
|
const { isSoftDeleteActiveState } = useRecordTableStates(viewBarId);
|
||||||
|
|
||||||
const handleToggleTrashColumnFilter = useCallback(() => {
|
const handleToggleTrashColumnFilter = useCallback(() => {
|
||||||
const trashFieldMetadata = objectMetadataItem.fields.find(
|
const trashFieldMetadata = objectMetadataItem.fields.find(
|
||||||
@ -63,5 +66,15 @@ export const useHandleToggleTrashColumnFilter = ({
|
|||||||
upsertCombinedViewFilter(newFilter);
|
upsertCombinedViewFilter(newFilter);
|
||||||
}, [columnDefinitions, objectMetadataItem, upsertCombinedViewFilter]);
|
}, [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,
|
hiddenTableColumns,
|
||||||
} = useRecordIndexOptionsForTable(recordIndexId);
|
} = useRecordIndexOptionsForTable(recordIndexId);
|
||||||
|
|
||||||
const handleToggleTrashColumnFilter = useHandleToggleTrashColumnFilter({
|
const { handleToggleTrashColumnFilter, toggleSoftDeleteFilterState } =
|
||||||
objectNameSingular,
|
useHandleToggleTrashColumnFilter({
|
||||||
viewBarId: recordIndexId,
|
objectNameSingular,
|
||||||
});
|
viewBarId: recordIndexId,
|
||||||
|
});
|
||||||
|
|
||||||
const {
|
const {
|
||||||
visibleBoardFields,
|
visibleBoardFields,
|
||||||
@ -163,6 +164,7 @@ export const RecordIndexOptionsDropdownContent = ({
|
|||||||
<MenuItem
|
<MenuItem
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
handleToggleTrashColumnFilter();
|
handleToggleTrashColumnFilter();
|
||||||
|
toggleSoftDeleteFilterState(true);
|
||||||
closeDropdown();
|
closeDropdown();
|
||||||
}}
|
}}
|
||||||
LeftIcon={IconRotate2}
|
LeftIcon={IconRotate2}
|
||||||
|
|||||||
@ -1,9 +1,8 @@
|
|||||||
import styled from '@emotion/styled';
|
import styled from '@emotion/styled';
|
||||||
import { isNonEmptyString, isNull } from '@sniptt/guards';
|
import { isNonEmptyString, isNull } from '@sniptt/guards';
|
||||||
|
|
||||||
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
|
|
||||||
import { RecordTableContextProvider } from '@/object-record/record-table/components/RecordTableContextProvider';
|
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 { useRecordTableStates } from '@/object-record/record-table/hooks/internal/useRecordTableStates';
|
||||||
import { RecordTableBody } from '@/object-record/record-table/record-table-body/components/RecordTableBody';
|
import { RecordTableBody } from '@/object-record/record-table/record-table-body/components/RecordTableBody';
|
||||||
import { RecordTableBodyEffect } from '@/object-record/record-table/record-table-body/components/RecordTableBodyEffect';
|
import { RecordTableBodyEffect } from '@/object-record/record-table/record-table-body/components/RecordTableBodyEffect';
|
||||||
@ -25,7 +24,6 @@ type RecordTableProps = {
|
|||||||
recordTableId: string;
|
recordTableId: string;
|
||||||
objectNameSingular: string;
|
objectNameSingular: string;
|
||||||
onColumnsChange: (columns: any) => void;
|
onColumnsChange: (columns: any) => void;
|
||||||
createRecord: () => void;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const RecordTable = ({
|
export const RecordTable = ({
|
||||||
@ -33,7 +31,6 @@ export const RecordTable = ({
|
|||||||
recordTableId,
|
recordTableId,
|
||||||
objectNameSingular,
|
objectNameSingular,
|
||||||
onColumnsChange,
|
onColumnsChange,
|
||||||
createRecord,
|
|
||||||
}: RecordTableProps) => {
|
}: RecordTableProps) => {
|
||||||
const { scopeId } = useRecordTableStates(recordTableId);
|
const { scopeId } = useRecordTableStates(recordTableId);
|
||||||
|
|
||||||
@ -51,12 +48,10 @@ export const RecordTable = ({
|
|||||||
|
|
||||||
const pendingRecordId = useRecoilValue(pendingRecordIdState);
|
const pendingRecordId = useRecoilValue(pendingRecordIdState);
|
||||||
|
|
||||||
const { objectMetadataItem: foundObjectMetadataItem } = useObjectMetadataItem(
|
const recordTableIsEmpty =
|
||||||
{ objectNameSingular },
|
!isRecordTableInitialLoading &&
|
||||||
);
|
tableRowIds.length === 0 &&
|
||||||
|
isNull(pendingRecordId);
|
||||||
const objectLabel = foundObjectMetadataItem?.labelSingular;
|
|
||||||
const isRemote = foundObjectMetadataItem?.isRemote ?? false;
|
|
||||||
|
|
||||||
if (!isNonEmptyString(objectNameSingular)) {
|
if (!isNonEmptyString(objectNameSingular)) {
|
||||||
return <></>;
|
return <></>;
|
||||||
@ -73,18 +68,11 @@ export const RecordTable = ({
|
|||||||
viewBarId={viewBarId}
|
viewBarId={viewBarId}
|
||||||
>
|
>
|
||||||
<RecordTableBodyEffect />
|
<RecordTableBodyEffect />
|
||||||
{!isRecordTableInitialLoading &&
|
{recordTableIsEmpty ? (
|
||||||
tableRowIds.length === 0 &&
|
<RecordTableEmptyState />
|
||||||
isNull(pendingRecordId) ? (
|
|
||||||
<RecordTableEmptyState
|
|
||||||
objectNameSingular={objectNameSingular}
|
|
||||||
objectLabel={objectLabel}
|
|
||||||
createRecord={createRecord}
|
|
||||||
isRemote={isRemote}
|
|
||||||
/>
|
|
||||||
) : (
|
) : (
|
||||||
<StyledTable className="entity-table-cell">
|
<StyledTable className="entity-table-cell">
|
||||||
<RecordTableHeader createRecord={createRecord} />
|
<RecordTableHeader />
|
||||||
<RecordTableBody />
|
<RecordTableBody />
|
||||||
</StyledTable>
|
</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;
|
recordTableId: string;
|
||||||
viewBarId: string;
|
viewBarId: string;
|
||||||
updateRecordMutation: (params: any) => void;
|
updateRecordMutation: (params: any) => void;
|
||||||
createRecord: () => Promise<void>;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const RecordTableWithWrappers = ({
|
export const RecordTableWithWrappers = ({
|
||||||
updateRecordMutation,
|
updateRecordMutation,
|
||||||
createRecord,
|
|
||||||
objectNameSingular,
|
objectNameSingular,
|
||||||
recordTableId,
|
recordTableId,
|
||||||
viewBarId,
|
viewBarId,
|
||||||
@ -80,7 +78,6 @@ export const RecordTableWithWrappers = ({
|
|||||||
recordTableId={recordTableId}
|
recordTableId={recordTableId}
|
||||||
objectNameSingular={objectNameSingular}
|
objectNameSingular={objectNameSingular}
|
||||||
onColumnsChange={handleColumnsChange}
|
onColumnsChange={handleColumnsChange}
|
||||||
createRecord={createRecord}
|
|
||||||
/>
|
/>
|
||||||
<DragSelect
|
<DragSelect
|
||||||
dragSelectable={tableBodyRef}
|
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 { 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 { RecordTableScope } from '@/object-record/record-table/scopes/RecordTableScope';
|
||||||
import { SnackBarProviderScope } from '@/ui/feedback/snack-bar-manager/scopes/SnackBarProviderScope';
|
import { SnackBarProviderScope } from '@/ui/feedback/snack-bar-manager/scopes/SnackBarProviderScope';
|
||||||
|
import { ComponentDecorator } from 'twenty-ui';
|
||||||
import { MemoryRouterDecorator } from '~/testing/decorators/MemoryRouterDecorator';
|
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 = {
|
const meta: Meta = {
|
||||||
title: 'Modules/ObjectRecord/RecordTable/RecordTableEmptyState',
|
title: 'Modules/ObjectRecord/RecordTable/RecordTableEmptyStateRemote',
|
||||||
component: RecordTableEmptyState,
|
component: RecordTableEmptyStateRemote,
|
||||||
decorators: [
|
decorators: [
|
||||||
|
ComponentDecorator,
|
||||||
MemoryRouterDecorator,
|
MemoryRouterDecorator,
|
||||||
|
ObjectMetadataItemsDecorator,
|
||||||
|
RecordTableDecorator,
|
||||||
(Story) => (
|
(Story) => (
|
||||||
<SnackBarProviderScope snackBarManagerScopeId="snack-bar-manager">
|
<SnackBarProviderScope snackBarManagerScopeId="snack-bar-manager">
|
||||||
<RecordTableScope
|
<RecordTableScope
|
||||||
@ -21,25 +28,12 @@ const meta: Meta = {
|
|||||||
</SnackBarProviderScope>
|
</SnackBarProviderScope>
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
parameters: {
|
||||||
|
msw: graphqlMocks,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export default meta;
|
export default meta;
|
||||||
type Story = StoryObj<typeof RecordTableEmptyState>;
|
type Story = StoryObj<typeof RecordTableEmptyStateRemote>;
|
||||||
|
|
||||||
export const Default: Story = {
|
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: () => {},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
@ -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 { availableTableColumnsComponentState } from '@/object-record/record-table/states/availableTableColumnsComponentState';
|
||||||
import { currentTableCellInEditModePositionComponentState } from '@/object-record/record-table/states/currentTableCellInEditModePositionComponentState';
|
import { currentTableCellInEditModePositionComponentState } from '@/object-record/record-table/states/currentTableCellInEditModePositionComponentState';
|
||||||
import { isRecordTableInitialLoadingComponentState } from '@/object-record/record-table/states/isRecordTableInitialLoadingComponentState';
|
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 { isSoftFocusActiveComponentState } from '@/object-record/record-table/states/isSoftFocusActiveComponentState';
|
||||||
import { isSoftFocusOnTableCellComponentFamilyState } from '@/object-record/record-table/states/isSoftFocusOnTableCellComponentFamilyState';
|
import { isSoftFocusOnTableCellComponentFamilyState } from '@/object-record/record-table/states/isSoftFocusOnTableCellComponentFamilyState';
|
||||||
import { isTableCellInEditModeComponentFamilyState } from '@/object-record/record-table/states/isTableCellInEditModeComponentFamilyState';
|
import { isTableCellInEditModeComponentFamilyState } from '@/object-record/record-table/states/isTableCellInEditModeComponentFamilyState';
|
||||||
@ -88,6 +89,10 @@ export const useRecordTableStates = (recordTableId?: string) => {
|
|||||||
isTableCellInEditModeComponentFamilyState,
|
isTableCellInEditModeComponentFamilyState,
|
||||||
scopeId,
|
scopeId,
|
||||||
),
|
),
|
||||||
|
isSoftDeleteActiveState: extractComponentState(
|
||||||
|
isSoftDeleteFilterActiveComponentState,
|
||||||
|
scopeId,
|
||||||
|
),
|
||||||
isSoftFocusActiveState: extractComponentState(
|
isSoftFocusActiveState: extractComponentState(
|
||||||
isSoftFocusActiveComponentState,
|
isSoftFocusActiveComponentState,
|
||||||
scopeId,
|
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 = ({
|
export const RecordTableHeader = () => {
|
||||||
createRecord,
|
|
||||||
}: {
|
|
||||||
createRecord: () => void;
|
|
||||||
}) => {
|
|
||||||
const { visibleTableColumnsSelector } = useRecordTableStates();
|
const { visibleTableColumnsSelector } = useRecordTableStates();
|
||||||
|
|
||||||
const visibleTableColumns = useRecoilValue(visibleTableColumnsSelector());
|
const visibleTableColumns = useRecoilValue(visibleTableColumnsSelector());
|
||||||
@ -88,11 +84,7 @@ export const RecordTableHeader = ({
|
|||||||
<RecordTableHeaderDragDropColumn />
|
<RecordTableHeaderDragDropColumn />
|
||||||
<RecordTableHeaderCheckboxColumn />
|
<RecordTableHeaderCheckboxColumn />
|
||||||
{visibleTableColumns.map((column) => (
|
{visibleTableColumns.map((column) => (
|
||||||
<RecordTableHeaderCell
|
<RecordTableHeaderCell key={column.fieldMetadataId} column={column} />
|
||||||
key={column.fieldMetadataId}
|
|
||||||
column={column}
|
|
||||||
createRecord={createRecord}
|
|
||||||
/>
|
|
||||||
))}
|
))}
|
||||||
<RecordTableHeaderLastColumn />
|
<RecordTableHeaderLastColumn />
|
||||||
</tr>
|
</tr>
|
||||||
|
|||||||
@ -5,6 +5,7 @@ import { IconPlus } from 'twenty-ui';
|
|||||||
|
|
||||||
import { FieldMetadata } from '@/object-record/record-field/types/FieldMetadata';
|
import { FieldMetadata } from '@/object-record/record-field/types/FieldMetadata';
|
||||||
import { useRecordTableStates } from '@/object-record/record-table/hooks/internal/useRecordTableStates';
|
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 { useTableColumns } from '@/object-record/record-table/hooks/useTableColumns';
|
||||||
import { RecordTableColumnHeadWithDropdown } from '@/object-record/record-table/record-table-header/components/RecordTableColumnHeadWithDropdown';
|
import { RecordTableColumnHeadWithDropdown } from '@/object-record/record-table/record-table-header/components/RecordTableColumnHeadWithDropdown';
|
||||||
import { isRecordTableScrolledLeftComponentState } from '@/object-record/record-table/states/isRecordTableScrolledLeftComponentState';
|
import { isRecordTableScrolledLeftComponentState } from '@/object-record/record-table/states/isRecordTableScrolledLeftComponentState';
|
||||||
@ -90,10 +91,8 @@ const StyledHeaderIcon = styled.div`
|
|||||||
|
|
||||||
export const RecordTableHeaderCell = ({
|
export const RecordTableHeaderCell = ({
|
||||||
column,
|
column,
|
||||||
createRecord,
|
|
||||||
}: {
|
}: {
|
||||||
column: ColumnDefinition<FieldMetadata>;
|
column: ColumnDefinition<FieldMetadata>;
|
||||||
createRecord: () => void;
|
|
||||||
}) => {
|
}) => {
|
||||||
const { resizeFieldOffsetState, tableColumnsState } = useRecordTableStates();
|
const { resizeFieldOffsetState, tableColumnsState } = useRecordTableStates();
|
||||||
|
|
||||||
@ -185,6 +184,12 @@ export const RecordTableHeaderCell = ({
|
|||||||
const disableColumnResize =
|
const disableColumnResize =
|
||||||
column.isLabelIdentifier && isMobile && !isRecordTableScrolledLeft;
|
column.isLabelIdentifier && isMobile && !isRecordTableScrolledLeft;
|
||||||
|
|
||||||
|
const { createNewTableRecord } = useCreateNewTableRecord();
|
||||||
|
|
||||||
|
const handlePlusButtonClick = () => {
|
||||||
|
createNewTableRecord();
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<StyledColumnHeaderCell
|
<StyledColumnHeaderCell
|
||||||
key={column.fieldMetadataId}
|
key={column.fieldMetadataId}
|
||||||
@ -206,7 +211,7 @@ export const RecordTableHeaderCell = ({
|
|||||||
Icon={IconPlus}
|
Icon={IconPlus}
|
||||||
size="small"
|
size="small"
|
||||||
accent="tertiary"
|
accent="tertiary"
|
||||||
onClick={createRecord}
|
onClick={handlePlusButtonClick}
|
||||||
/>
|
/>
|
||||||
</StyledHeaderIcon>
|
</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}
|
objectNameSingular={objectNameSingular}
|
||||||
recordTableId={recordIndexId}
|
recordTableId={recordIndexId}
|
||||||
viewBarId={viewBarId}
|
viewBarId={viewBarId}
|
||||||
createRecord={async () => {}}
|
|
||||||
updateRecordMutation={() => {}}
|
updateRecordMutation={() => {}}
|
||||||
/>
|
/>
|
||||||
</StyledContainer>
|
</StyledContainer>
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import { useEffect } from 'react';
|
|
||||||
import { useTheme } from '@emotion/react';
|
import { useTheme } from '@emotion/react';
|
||||||
import styled from '@emotion/styled';
|
import styled from '@emotion/styled';
|
||||||
import { animate, motion, useMotionValue, useTransform } from 'framer-motion';
|
import { animate, motion, useMotionValue, useTransform } from 'framer-motion';
|
||||||
|
import { useEffect } from 'react';
|
||||||
|
|
||||||
import { BACKGROUND } from '@/ui/layout/animated-placeholder/constants/Background';
|
import { BACKGROUND } from '@/ui/layout/animated-placeholder/constants/Background';
|
||||||
import { DARK_BACKGROUND } from '@/ui/layout/animated-placeholder/constants/DarkBackground';
|
import { DARK_BACKGROUND } from '@/ui/layout/animated-placeholder/constants/DarkBackground';
|
||||||
@ -35,8 +35,12 @@ const StyledMovingImage = styled(motion.img)<StyledImageProps>`
|
|||||||
z-index: 2;
|
z-index: 2;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
export type AnimatedPlaceholderType =
|
||||||
|
| keyof typeof BACKGROUND
|
||||||
|
| keyof typeof MOVING_IMAGE;
|
||||||
|
|
||||||
interface AnimatedPlaceholderProps {
|
interface AnimatedPlaceholderProps {
|
||||||
type: keyof typeof BACKGROUND | keyof typeof MOVING_IMAGE;
|
type: AnimatedPlaceholderType;
|
||||||
}
|
}
|
||||||
|
|
||||||
const AnimatedPlaceholder = ({ type }: AnimatedPlaceholderProps) => {
|
const AnimatedPlaceholder = ({ type }: AnimatedPlaceholderProps) => {
|
||||||
|
|||||||
@ -12,4 +12,5 @@ export const BACKGROUND: Record<string, string> = {
|
|||||||
emptyInbox: '/images/placeholders/background/empty_inbox_bg.png',
|
emptyInbox: '/images/placeholders/background/empty_inbox_bg.png',
|
||||||
error404: '/images/placeholders/background/404_bg.png',
|
error404: '/images/placeholders/background/404_bg.png',
|
||||||
error500: '/images/placeholders/background/500_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',
|
loadingMessages: '/images/placeholders/background/loading_messages_bg.png',
|
||||||
loadingAccounts: '/images/placeholders/background/loading_accounts_bg.png',
|
loadingAccounts: '/images/placeholders/background/loading_accounts_bg.png',
|
||||||
emptyFunctions: '/images/placeholders/dark-background/empty_functions_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',
|
loadingMessages: '/images/placeholders/moving-image/loading_messages.png',
|
||||||
loadingAccounts: '/images/placeholders/moving-image/loading_accounts.png',
|
loadingAccounts: '/images/placeholders/moving-image/loading_accounts.png',
|
||||||
emptyFunctions: '/images/placeholders/dark-moving-image/empty_functions.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',
|
emptyInbox: '/images/placeholders/moving-image/empty_inbox.png',
|
||||||
error404: '/images/placeholders/moving-image/404.png',
|
error404: '/images/placeholders/moving-image/404.png',
|
||||||
error500: '/images/placeholders/moving-image/500.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 { useIcons } from 'twenty-ui';
|
||||||
|
|
||||||
|
import { useObjectNameSingularFromPlural } from '@/object-metadata/hooks/useObjectNameSingularFromPlural';
|
||||||
import { Filter } from '@/object-record/object-filter-dropdown/types/Filter';
|
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 { SortOrFilterChip } from '@/views/components/SortOrFilterChip';
|
||||||
import { useCombinedViewFilters } from '@/views/hooks/useCombinedViewFilters';
|
import { useCombinedViewFilters } from '@/views/hooks/useCombinedViewFilters';
|
||||||
|
import { useParams } from 'react-router-dom';
|
||||||
|
|
||||||
type VariantFilterChipProps = {
|
type VariantFilterChipProps = {
|
||||||
viewFilter: Filter;
|
viewFilter: Filter;
|
||||||
|
viewBarId: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const VariantFilterChip = ({ viewFilter }: VariantFilterChipProps) => {
|
export const VariantFilterChip = ({
|
||||||
|
viewFilter,
|
||||||
|
viewBarId,
|
||||||
|
}: VariantFilterChipProps) => {
|
||||||
const { removeCombinedViewFilter } = useCombinedViewFilters();
|
const { removeCombinedViewFilter } = useCombinedViewFilters();
|
||||||
|
|
||||||
|
const { objectNamePlural } = useParams();
|
||||||
|
|
||||||
|
const { objectNameSingular } = useObjectNameSingularFromPlural({
|
||||||
|
objectNamePlural: objectNamePlural ?? '',
|
||||||
|
});
|
||||||
|
|
||||||
|
const { toggleSoftDeleteFilterState } = useHandleToggleTrashColumnFilter({
|
||||||
|
objectNameSingular,
|
||||||
|
viewBarId,
|
||||||
|
});
|
||||||
|
|
||||||
const { getIcon } = useIcons();
|
const { getIcon } = useIcons();
|
||||||
|
|
||||||
const handleRemoveClick = () => {
|
const handleRemoveClick = () => {
|
||||||
removeCombinedViewFilter(viewFilter.id);
|
removeCombinedViewFilter(viewFilter.id);
|
||||||
|
if (
|
||||||
|
viewFilter.definition.label === 'Deleted' &&
|
||||||
|
viewFilter.operand === 'isNotEmpty'
|
||||||
|
) {
|
||||||
|
toggleSoftDeleteFilterState(false);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@ -97,6 +97,7 @@ export const ViewBarDetails = ({
|
|||||||
hasFilterButton = false,
|
hasFilterButton = false,
|
||||||
rightComponent,
|
rightComponent,
|
||||||
filterDropdownId,
|
filterDropdownId,
|
||||||
|
viewBarId,
|
||||||
}: ViewBarDetailsProps) => {
|
}: ViewBarDetailsProps) => {
|
||||||
const {
|
const {
|
||||||
canPersistViewSelector,
|
canPersistViewSelector,
|
||||||
@ -169,6 +170,7 @@ export const ViewBarDetails = ({
|
|||||||
// Also as filter is spread into viewFilter, definition is present
|
// Also as filter is spread into viewFilter, definition is present
|
||||||
// FixMe: Ugly hack to make it work
|
// FixMe: Ugly hack to make it work
|
||||||
viewFilter={viewFilter as unknown as Filter}
|
viewFilter={viewFilter as unknown as Filter}
|
||||||
|
viewBarId={viewBarId}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
{!!otherViewFilters.length &&
|
{!!otherViewFilters.length &&
|
||||||
|
|||||||
@ -154,6 +154,7 @@ export const useCombinedViewFilters = (viewBarComponentId?: string) => {
|
|||||||
unsavedToUpsertViewFiltersState,
|
unsavedToUpsertViewFiltersState,
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
upsertCombinedViewFilter,
|
upsertCombinedViewFilter,
|
||||||
removeCombinedViewFilter,
|
removeCombinedViewFilter,
|
||||||
|
|||||||
@ -1,16 +1,18 @@
|
|||||||
import styled from '@emotion/styled';
|
import styled from '@emotion/styled';
|
||||||
import { useParams } from 'react-router-dom';
|
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 { RecordIndexContainer } from '@/object-record/record-index/components/RecordIndexContainer';
|
||||||
import { RecordIndexPageHeader } from '@/object-record/record-index/components/RecordIndexPageHeader';
|
import { RecordIndexPageHeader } from '@/object-record/record-index/components/RecordIndexPageHeader';
|
||||||
import { useRecordTable } from '@/object-record/record-table/hooks/useRecordTable';
|
import { RecordIndexRootPropsContext } from '@/object-record/record-index/contexts/RecordIndexRootPropsContext';
|
||||||
import { DEFAULT_CELL_SCOPE } from '@/object-record/record-table/record-table-cell/hooks/useOpenRecordTableCellV2';
|
import { useHandleIndexIdentifierClick } from '@/object-record/record-index/hooks/useHandleIndexIdentifierClick';
|
||||||
import { useSelectedTableCellEditMode } from '@/object-record/record-table/record-table-cell/hooks/useSelectedTableCellEditMode';
|
import { useCreateNewTableRecord } from '@/object-record/record-table/hooks/useCreateNewTableRecords';
|
||||||
import { PageBody } from '@/ui/layout/page/PageBody';
|
import { PageBody } from '@/ui/layout/page/PageBody';
|
||||||
import { PageContainer } from '@/ui/layout/page/PageContainer';
|
import { PageContainer } from '@/ui/layout/page/PageContainer';
|
||||||
import { useSetHotkeyScope } from '@/ui/utilities/hotkey/hooks/useSetHotkeyScope';
|
|
||||||
import { PageTitle } from '@/ui/utilities/page-title/PageTitle';
|
import { PageTitle } from '@/ui/utilities/page-title/PageTitle';
|
||||||
|
import { useRecoilCallback } from 'recoil';
|
||||||
import { capitalize } from '~/utils/string/capitalize';
|
import { capitalize } from '~/utils/string/capitalize';
|
||||||
|
|
||||||
const StyledIndexContainer = styled.div`
|
const StyledIndexContainer = styled.div`
|
||||||
@ -23,39 +25,55 @@ export const RecordIndexPage = () => {
|
|||||||
const objectNamePlural = useParams().objectNamePlural ?? '';
|
const objectNamePlural = useParams().objectNamePlural ?? '';
|
||||||
|
|
||||||
const recordIndexId = objectNamePlural ?? '';
|
const recordIndexId = objectNamePlural ?? '';
|
||||||
const setHotkeyScope = useSetHotkeyScope();
|
|
||||||
|
|
||||||
const { setSelectedTableCellEditMode } = useSelectedTableCellEditMode({
|
const { objectNameSingular } = useObjectNameSingularFromPlural({
|
||||||
scopeId: recordIndexId,
|
objectNamePlural,
|
||||||
});
|
});
|
||||||
|
|
||||||
const { setPendingRecordId } = useRecordTable({
|
const { objectMetadataItem } = useObjectMetadataItem({
|
||||||
recordTableId: recordIndexId,
|
objectNameSingular,
|
||||||
});
|
});
|
||||||
|
|
||||||
const handleAddButtonClick = async () => {
|
const { createNewTableRecord } = useCreateNewTableRecord(recordIndexId);
|
||||||
setPendingRecordId(v4());
|
|
||||||
setSelectedTableCellEditMode(-1, 0);
|
const handleCreateRecord = () => {
|
||||||
setHotkeyScope(DEFAULT_CELL_SCOPE.scope, DEFAULT_CELL_SCOPE.customScopes);
|
createNewTableRecord();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const { handleIndexIdentifierClick } = useHandleIndexIdentifierClick({
|
||||||
|
objectMetadataItem,
|
||||||
|
recordIndexId,
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleIndexRecordsLoaded = useRecoilCallback(
|
||||||
|
({ set }) =>
|
||||||
|
() => {
|
||||||
|
// TODO: find a better way to reset this state ?
|
||||||
|
set(lastShowPageRecordIdState, null);
|
||||||
|
},
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PageContainer>
|
<PageContainer>
|
||||||
<PageTitle title={`${capitalize(objectNamePlural)}`} />
|
<RecordIndexRootPropsContext.Provider
|
||||||
<RecordIndexPageHeader
|
value={{
|
||||||
createRecord={handleAddButtonClick}
|
recordIndexId,
|
||||||
recordIndexId={recordIndexId}
|
objectNamePlural,
|
||||||
objectNamePlural={objectNamePlural}
|
objectNameSingular,
|
||||||
/>
|
onIndexRecordsLoaded: handleIndexRecordsLoaded,
|
||||||
<PageBody>
|
onIndexIdentifierClick: handleIndexIdentifierClick,
|
||||||
<StyledIndexContainer>
|
onCreateRecord: handleCreateRecord,
|
||||||
<RecordIndexContainer
|
}}
|
||||||
recordIndexId={recordIndexId}
|
>
|
||||||
objectNamePlural={objectNamePlural}
|
<PageTitle title={`${capitalize(objectNamePlural)}`} />
|
||||||
createRecord={handleAddButtonClick}
|
<RecordIndexPageHeader />
|
||||||
/>
|
<PageBody>
|
||||||
</StyledIndexContainer>
|
<StyledIndexContainer>
|
||||||
</PageBody>
|
<RecordIndexContainer />
|
||||||
|
</StyledIndexContainer>
|
||||||
|
</PageBody>
|
||||||
|
</RecordIndexRootPropsContext.Provider>
|
||||||
</PageContainer>
|
</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