Improve board experience 🖼️ (#16063)
This PR improves the general UX and DX of boards, by modifying the query effect to only use paged group by queries. In this PR we implement two more things in the backend for group by queries : - Fixed ORDER BY in the PARTITION BY sub-query (this wasn't working because it was applied in the main query, so it sorted randomly picked records, which was a correct sort on an incorrect dataset returned by the sub-query) - Added offset paging in PARTITION BY Miscellaneous, various bug fixes and improvements along the way : - Throttled loading of cards to avoid React freeze - Handling of drag & drop - Handling of create / delete / update - Reworked skeleton (the library slows down a lot with hundreds of skeleton for a spinning effect that is hardly noticed) - Fixed refetch of aggregate queries (I included the new group by aggregates query we use in the existing refetch mechanism) - Re-trigger queries on filters and sorts changes - Unselect all record ids when deleting / restoring / detroying - Fetch only groups that still have records to lighten the group by query. # What remains to be done This is still a naïve fetch more implementation that will work for a few fetch more rounds, but if you scroll and load say 200 cards per column on a board, React will re-render all 200 cards of each column each time. We would probably need to virtualize the board with paged queries as we did for the table, this could be done after this PR but seems less urgent. What's nice is that this new query pattern is well designed for virtualization also, drawing from our experience with table virtualization, and adapted to a multi-column request pattern, like a 2:2 matrix of records, for our boards. So the remaining work would be to design a UI solution for virtualizing this matrix of records, which could be quite different from our table virtualization mechanism.
This commit is contained in:
+5
@@ -10,6 +10,7 @@ import { contextStoreTargetedRecordsRuleComponentState } from '@/context-store/s
|
||||
import { computeContextStoreFilters } from '@/context-store/utils/computeContextStoreFilters';
|
||||
import { DEFAULT_QUERY_PAGE_SIZE } from '@/object-record/constants/DefaultQueryPageSize';
|
||||
import { useIncrementalDeleteManyRecords } from '@/object-record/hooks/useIncrementalDeleteManyRecords';
|
||||
import { useRemoveSelectedRecordsFromRecordBoard } from '@/object-record/record-board/hooks/useRemoveSelectedRecordsFromRecordBoard';
|
||||
import { useFilterValueDependencies } from '@/object-record/record-filter/hooks/useFilterValueDependencies';
|
||||
import { useRecordIndexIdFromCurrentContextStore } from '@/object-record/record-index/hooks/useRecordIndexIdFromCurrentContextStore';
|
||||
import { useResetTableRowSelection } from '@/object-record/record-table/hooks/internal/useResetTableRowSelection';
|
||||
@@ -47,6 +48,9 @@ export const DeleteMultipleRecordsAction = () => {
|
||||
contextStoreAnyFieldFilterValueComponentState,
|
||||
);
|
||||
|
||||
const { removeSelectedRecordsFromRecordBoard } =
|
||||
useRemoveSelectedRecordsFromRecordBoard(recordIndexId);
|
||||
|
||||
const { filterValueDependencies } = useFilterValueDependencies();
|
||||
|
||||
const graphqlFilter = computeContextStoreFilters({
|
||||
@@ -85,6 +89,7 @@ export const DeleteMultipleRecordsAction = () => {
|
||||
};
|
||||
|
||||
const handleDeleteClick = async () => {
|
||||
removeSelectedRecordsFromRecordBoard();
|
||||
resetTableRowSelection();
|
||||
await incrementalDeleteManyRecords();
|
||||
};
|
||||
|
||||
+4
@@ -10,6 +10,7 @@ import { contextStoreTargetedRecordsRuleComponentState } from '@/context-store/s
|
||||
import { computeContextStoreFilters } from '@/context-store/utils/computeContextStoreFilters';
|
||||
import { DEFAULT_QUERY_PAGE_SIZE } from '@/object-record/constants/DefaultQueryPageSize';
|
||||
import { useIncrementalDestroyManyRecords } from '@/object-record/hooks/useIncrementalDestroyManyRecords';
|
||||
import { useRemoveSelectedRecordsFromRecordBoard } from '@/object-record/record-board/hooks/useRemoveSelectedRecordsFromRecordBoard';
|
||||
import { useFilterValueDependencies } from '@/object-record/record-filter/hooks/useFilterValueDependencies';
|
||||
import { useRecordIndexIdFromCurrentContextStore } from '@/object-record/record-index/hooks/useRecordIndexIdFromCurrentContextStore';
|
||||
import { useResetTableRowSelection } from '@/object-record/record-table/hooks/internal/useResetTableRowSelection';
|
||||
@@ -32,6 +33,8 @@ export const DestroyMultipleRecordsAction = () => {
|
||||
}
|
||||
|
||||
const { resetTableRowSelection } = useResetTableRowSelection(recordIndexId);
|
||||
const { removeSelectedRecordsFromRecordBoard } =
|
||||
useRemoveSelectedRecordsFromRecordBoard(recordIndexId);
|
||||
|
||||
const contextStoreTargetedRecordsRule = useRecoilComponentValue(
|
||||
contextStoreTargetedRecordsRuleComponentState,
|
||||
@@ -93,6 +96,7 @@ export const DestroyMultipleRecordsAction = () => {
|
||||
};
|
||||
|
||||
const handleDestroyClick = async () => {
|
||||
removeSelectedRecordsFromRecordBoard();
|
||||
resetTableRowSelection();
|
||||
await incrementalDestroyManyRecords();
|
||||
};
|
||||
|
||||
+4
@@ -8,6 +8,7 @@ import { computeContextStoreFilters } from '@/context-store/utils/computeContext
|
||||
import { DEFAULT_QUERY_PAGE_SIZE } from '@/object-record/constants/DefaultQueryPageSize';
|
||||
import { useLazyFetchAllRecords } from '@/object-record/hooks/useLazyFetchAllRecords';
|
||||
import { useRestoreManyRecords } from '@/object-record/hooks/useRestoreManyRecords';
|
||||
import { useRemoveSelectedRecordsFromRecordBoard } from '@/object-record/record-board/hooks/useRemoveSelectedRecordsFromRecordBoard';
|
||||
import { useFilterValueDependencies } from '@/object-record/record-filter/hooks/useFilterValueDependencies';
|
||||
import { useRecordIndexIdFromCurrentContextStore } from '@/object-record/record-index/hooks/useRecordIndexIdFromCurrentContextStore';
|
||||
import { useResetTableRowSelection } from '@/object-record/record-table/hooks/internal/useResetTableRowSelection';
|
||||
@@ -28,6 +29,8 @@ export const RestoreMultipleRecordsAction = () => {
|
||||
}
|
||||
|
||||
const { resetTableRowSelection } = useResetTableRowSelection(recordIndexId);
|
||||
const { removeSelectedRecordsFromRecordBoard } =
|
||||
useRemoveSelectedRecordsFromRecordBoard(recordIndexId);
|
||||
|
||||
const { restoreManyRecords } = useRestoreManyRecords({
|
||||
objectNameSingular: objectMetadataItem.nameSingular,
|
||||
@@ -75,6 +78,7 @@ export const RestoreMultipleRecordsAction = () => {
|
||||
});
|
||||
|
||||
const handleRestoreClick = async () => {
|
||||
removeSelectedRecordsFromRecordBoard();
|
||||
const recordsToRestore = await fetchAllRecordIds();
|
||||
const recordIdsToRestore = recordsToRestore.map((record) => record.id);
|
||||
|
||||
|
||||
+6
@@ -3,6 +3,7 @@ import { useSelectedRecordIdOrThrow } from '@/action-menu/actions/record-actions
|
||||
import { useDeleteFavorite } from '@/favorites/hooks/useDeleteFavorite';
|
||||
import { useFavorites } from '@/favorites/hooks/useFavorites';
|
||||
import { useDeleteOneRecord } from '@/object-record/hooks/useDeleteOneRecord';
|
||||
import { useRemoveSelectedRecordsFromRecordBoard } from '@/object-record/record-board/hooks/useRemoveSelectedRecordsFromRecordBoard';
|
||||
import { useRecordIndexIdFromCurrentContextStore } from '@/object-record/record-index/hooks/useRecordIndexIdFromCurrentContextStore';
|
||||
import { useResetTableRowSelection } from '@/object-record/record-table/hooks/internal/useResetTableRowSelection';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
@@ -15,6 +16,9 @@ export const DeleteSingleRecordAction = () => {
|
||||
|
||||
const { resetTableRowSelection } = useResetTableRowSelection(recordIndexId);
|
||||
|
||||
const { removeSelectedRecordsFromRecordBoard } =
|
||||
useRemoveSelectedRecordsFromRecordBoard(recordIndexId);
|
||||
|
||||
const { deleteOneRecord } = useDeleteOneRecord({
|
||||
objectNameSingular: objectMetadataItem.nameSingular,
|
||||
});
|
||||
@@ -23,6 +27,8 @@ export const DeleteSingleRecordAction = () => {
|
||||
const { deleteFavorite } = useDeleteFavorite();
|
||||
|
||||
const handleDeleteClick = async () => {
|
||||
removeSelectedRecordsFromRecordBoard();
|
||||
|
||||
resetTableRowSelection();
|
||||
|
||||
const foundFavorite = favorites?.find(
|
||||
|
||||
+5
@@ -1,6 +1,7 @@
|
||||
import { ActionModal } from '@/action-menu/actions/components/ActionModal';
|
||||
import { useSelectedRecordIdOrThrow } from '@/action-menu/actions/record-actions/single-record/hooks/useSelectedRecordIdOrThrow';
|
||||
import { useDestroyOneRecord } from '@/object-record/hooks/useDestroyOneRecord';
|
||||
import { useRemoveSelectedRecordsFromRecordBoard } from '@/object-record/record-board/hooks/useRemoveSelectedRecordsFromRecordBoard';
|
||||
import { useRecordIndexIdFromCurrentContextStore } from '@/object-record/record-index/hooks/useRecordIndexIdFromCurrentContextStore';
|
||||
import { useResetTableRowSelection } from '@/object-record/record-table/hooks/internal/useResetTableRowSelection';
|
||||
import { t } from '@lingui/core/macro';
|
||||
@@ -17,11 +18,15 @@ export const DestroySingleRecordAction = () => {
|
||||
|
||||
const { resetTableRowSelection } = useResetTableRowSelection(recordIndexId);
|
||||
|
||||
const { removeSelectedRecordsFromRecordBoard } =
|
||||
useRemoveSelectedRecordsFromRecordBoard(recordIndexId);
|
||||
|
||||
const { destroyOneRecord } = useDestroyOneRecord({
|
||||
objectNameSingular: objectMetadataItem.nameSingular,
|
||||
});
|
||||
|
||||
const handleDeleteClick = async () => {
|
||||
removeSelectedRecordsFromRecordBoard();
|
||||
resetTableRowSelection();
|
||||
|
||||
await destroyOneRecord(recordId);
|
||||
|
||||
+4
@@ -1,6 +1,7 @@
|
||||
import { ActionModal } from '@/action-menu/actions/components/ActionModal';
|
||||
import { useSelectedRecordIdOrThrow } from '@/action-menu/actions/record-actions/single-record/hooks/useSelectedRecordIdOrThrow';
|
||||
import { useRestoreManyRecords } from '@/object-record/hooks/useRestoreManyRecords';
|
||||
import { useRemoveSelectedRecordsFromRecordBoard } from '@/object-record/record-board/hooks/useRemoveSelectedRecordsFromRecordBoard';
|
||||
import { useRecordIndexIdFromCurrentContextStore } from '@/object-record/record-index/hooks/useRecordIndexIdFromCurrentContextStore';
|
||||
import { useResetTableRowSelection } from '@/object-record/record-table/hooks/internal/useResetTableRowSelection';
|
||||
import { t } from '@lingui/core/macro';
|
||||
@@ -12,12 +13,15 @@ export const RestoreSingleRecordAction = () => {
|
||||
const recordId = useSelectedRecordIdOrThrow();
|
||||
|
||||
const { resetTableRowSelection } = useResetTableRowSelection(recordIndexId);
|
||||
const { removeSelectedRecordsFromRecordBoard } =
|
||||
useRemoveSelectedRecordsFromRecordBoard(recordIndexId);
|
||||
|
||||
const { restoreManyRecords } = useRestoreManyRecords({
|
||||
objectNameSingular: objectMetadataItem.nameSingular,
|
||||
});
|
||||
|
||||
const handleRestoreClick = async () => {
|
||||
removeSelectedRecordsFromRecordBoard();
|
||||
resetTableRowSelection();
|
||||
|
||||
await restoreManyRecords({
|
||||
|
||||
@@ -24,7 +24,7 @@ import { ContextStoreViewType } from '@/context-store/types/ContextStoreViewType
|
||||
import { CoreObjectNamePlural } from '@/object-metadata/types/CoreObjectNamePlural';
|
||||
import { useActiveRecordBoardCard } from '@/object-record/record-board/hooks/useActiveRecordBoardCard';
|
||||
import { useFocusedRecordBoardCard } from '@/object-record/record-board/hooks/useFocusedRecordBoardCard';
|
||||
import { useRecordBoardSelection } from '@/object-record/record-board/hooks/useRecordBoardSelection';
|
||||
import { useResetRecordBoardSelection } from '@/object-record/record-board/hooks/useResetRecordBoardSelection';
|
||||
import { useResetFocusStackToRecordIndex } from '@/object-record/record-index/hooks/useResetFocusStackToRecordIndex';
|
||||
import { useResetTableRowSelection } from '@/object-record/record-table/hooks/internal/useResetTableRowSelection';
|
||||
import { useActiveRecordTableRow } from '@/object-record/record-table/hooks/useActiveRecordTableRow';
|
||||
@@ -82,7 +82,8 @@ export const PageChangeEffect = () => {
|
||||
const { unfocusRecordTableRow } = useFocusedRecordTableRow(recordIndexId);
|
||||
const { deactivateRecordTableRow } = useActiveRecordTableRow(recordIndexId);
|
||||
|
||||
const { resetRecordSelection } = useRecordBoardSelection(recordIndexId);
|
||||
const { resetRecordBoardSelection } =
|
||||
useResetRecordBoardSelection(recordIndexId);
|
||||
const { deactivateBoardCard } = useActiveRecordBoardCard(recordIndexId);
|
||||
const { unfocusBoardCard } = useFocusedRecordBoardCard(recordIndexId);
|
||||
|
||||
@@ -141,7 +142,7 @@ export const PageChangeEffect = () => {
|
||||
deactivateRecordTableRow();
|
||||
}
|
||||
if (contextStoreCurrentViewType === ContextStoreViewType.Kanban) {
|
||||
resetRecordSelection();
|
||||
resetRecordBoardSelection();
|
||||
deactivateBoardCard();
|
||||
unfocusBoardCard();
|
||||
}
|
||||
@@ -304,7 +305,7 @@ export const PageChangeEffect = () => {
|
||||
resetTableRowSelection,
|
||||
unfocusRecordTableRow,
|
||||
deactivateRecordTableRow,
|
||||
resetRecordSelection,
|
||||
resetRecordBoardSelection,
|
||||
deactivateBoardCard,
|
||||
unfocusBoardCard,
|
||||
resetFocusStackToRecordIndex,
|
||||
|
||||
+3
-3
@@ -5,9 +5,9 @@ export type RecordGqlConnection = {
|
||||
__typename?: string;
|
||||
edges: RecordGqlEdge[];
|
||||
pageInfo: {
|
||||
__typename?: string;
|
||||
hasNextPage?: boolean;
|
||||
hasPreviousPage?: boolean;
|
||||
__typename?: Nullable<string>;
|
||||
hasNextPage?: Nullable<boolean>;
|
||||
hasPreviousPage?: Nullable<boolean>;
|
||||
startCursor?: Nullable<string>;
|
||||
endCursor?: Nullable<string>;
|
||||
totalCount?: number;
|
||||
|
||||
+5
-1
@@ -1,5 +1,6 @@
|
||||
import { useApolloCoreClient } from '@/object-metadata/hooks/useApolloCoreClient';
|
||||
import { useRefetchAggregateQueries } from '@/object-record/hooks/useRefetchAggregateQueries';
|
||||
import { getGroupByAggregateQueryName } from '@/object-record/record-aggregate/utils/getGroupByAggregateQueryName';
|
||||
import { getAggregateQueryName } from '@/object-record/utils/getAggregateQueryName';
|
||||
import { renderHook } from '@testing-library/react';
|
||||
|
||||
@@ -22,6 +23,9 @@ describe('useRefetchAggregateQueries', () => {
|
||||
// Arrange
|
||||
const objectMetadataNamePlural = 'opportunities';
|
||||
const expectedQueryName = getAggregateQueryName(objectMetadataNamePlural);
|
||||
const expectedQueryNameGroupBy = getGroupByAggregateQueryName({
|
||||
objectMetadataNamePlural,
|
||||
});
|
||||
|
||||
// Act
|
||||
const { result } = renderHook(() =>
|
||||
@@ -32,7 +36,7 @@ describe('useRefetchAggregateQueries', () => {
|
||||
// Assert
|
||||
expect(mockRefetchQueries).toHaveBeenCalledTimes(1);
|
||||
expect(mockRefetchQueries).toHaveBeenCalledWith({
|
||||
include: [expectedQueryName],
|
||||
include: [expectedQueryName, expectedQueryNameGroupBy],
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useApolloCoreClient } from '@/object-metadata/hooks/useApolloCoreClient';
|
||||
import { getGroupByAggregateQueryName } from '@/object-record/record-aggregate/utils/getGroupByAggregateQueryName';
|
||||
import { getAggregateQueryName } from '@/object-record/utils/getAggregateQueryName';
|
||||
|
||||
export const useRefetchAggregateQueries = ({
|
||||
@@ -11,8 +12,12 @@ export const useRefetchAggregateQueries = ({
|
||||
const refetchAggregateQueries = async () => {
|
||||
const queryName = getAggregateQueryName(objectMetadataNamePlural);
|
||||
|
||||
const groupByAggregateQueryName = getGroupByAggregateQueryName({
|
||||
objectMetadataNamePlural,
|
||||
});
|
||||
|
||||
await apolloCoreClient.refetchQueries({
|
||||
include: [queryName],
|
||||
include: [queryName, groupByAggregateQueryName],
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
+2
-2
@@ -2,8 +2,8 @@ import { ObjectOptionsDropdownMenuViewName } from '@/object-record/object-option
|
||||
import { OBJECT_OPTIONS_DROPDOWN_ID } from '@/object-record/object-options-dropdown/constants/ObjectOptionsDropdownId';
|
||||
import { useObjectOptionsDropdown } from '@/object-record/object-options-dropdown/hooks/useObjectOptionsDropdown';
|
||||
import { useObjectOptionsForBoard } from '@/object-record/object-options-dropdown/hooks/useObjectOptionsForBoard';
|
||||
import { recordGroupFieldMetadataComponentState } from '@/object-record/record-group/states/recordGroupFieldMetadataComponentState';
|
||||
import { recordIndexCalendarLayoutState } from '@/object-record/record-index/states/recordIndexCalendarLayoutState';
|
||||
import { recordIndexGroupFieldMetadataItemComponentState } from '@/object-record/record-index/states/recordIndexGroupFieldMetadataComponentState';
|
||||
import { DropdownContent } from '@/ui/layout/dropdown/components/DropdownContent';
|
||||
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
|
||||
import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownMenuSeparator';
|
||||
@@ -55,7 +55,7 @@ export const ObjectOptionsDropdownCustomView = ({
|
||||
: null;
|
||||
|
||||
const recordGroupFieldMetadata = useRecoilComponentValue(
|
||||
recordGroupFieldMetadataComponentState,
|
||||
recordIndexGroupFieldMetadataItemComponentState,
|
||||
);
|
||||
|
||||
const calendarFieldMetadata = currentView?.calendarFieldMetadataId
|
||||
|
||||
+2
-2
@@ -5,8 +5,8 @@ import { useObjectNamePluralFromSingular } from '@/object-metadata/hooks/useObje
|
||||
import { useObjectOptionsDropdown } from '@/object-record/object-options-dropdown/hooks/useObjectOptionsDropdown';
|
||||
import { RecordGroupsVisibilityDropdownSection } from '@/object-record/record-group/components/RecordGroupsVisibilityDropdownSection';
|
||||
import { useRecordGroupVisibility } from '@/object-record/record-group/hooks/useRecordGroupVisibility';
|
||||
import { recordGroupFieldMetadataComponentState } from '@/object-record/record-group/states/recordGroupFieldMetadataComponentState';
|
||||
import { hiddenRecordGroupIdsComponentSelector } from '@/object-record/record-group/states/selectors/hiddenRecordGroupIdsComponentSelector';
|
||||
import { recordIndexGroupFieldMetadataItemComponentState } from '@/object-record/record-index/states/recordIndexGroupFieldMetadataComponentState';
|
||||
import { DropdownContent } from '@/ui/layout/dropdown/components/DropdownContent';
|
||||
import { DropdownMenuHeader } from '@/ui/layout/dropdown/components/DropdownMenuHeader/DropdownMenuHeader';
|
||||
import { DropdownMenuHeaderLeftComponent } from '@/ui/layout/dropdown/components/DropdownMenuHeader/internal/DropdownMenuHeaderLeftComponent';
|
||||
@@ -32,7 +32,7 @@ export const ObjectOptionsDropdownHiddenRecordGroupsContent = () => {
|
||||
} = useObjectOptionsDropdown();
|
||||
|
||||
const recordGroupFieldMetadata = useRecoilComponentValue(
|
||||
recordGroupFieldMetadataComponentState,
|
||||
recordIndexGroupFieldMetadataItemComponentState,
|
||||
);
|
||||
|
||||
const hiddenRecordGroupIds = useRecoilComponentValue(
|
||||
|
||||
+2
-2
@@ -1,8 +1,8 @@
|
||||
import { OBJECT_OPTIONS_DROPDOWN_ID } from '@/object-record/object-options-dropdown/constants/ObjectOptionsDropdownId';
|
||||
import { useObjectOptionsDropdown } from '@/object-record/object-options-dropdown/hooks/useObjectOptionsDropdown';
|
||||
import { useSetViewTypeFromLayoutOptionsMenu } from '@/object-record/object-options-dropdown/hooks/useSetViewTypeFromLayoutOptionsMenu';
|
||||
import { recordGroupFieldMetadataComponentState } from '@/object-record/record-group/states/recordGroupFieldMetadataComponentState';
|
||||
import { recordIndexCalendarLayoutState } from '@/object-record/record-index/states/recordIndexCalendarLayoutState';
|
||||
import { recordIndexGroupFieldMetadataItemComponentState } from '@/object-record/record-index/states/recordIndexGroupFieldMetadataComponentState';
|
||||
import { recordIndexOpenRecordInState } from '@/object-record/record-index/states/recordIndexOpenRecordInState';
|
||||
import { DropdownContent } from '@/ui/layout/dropdown/components/DropdownContent';
|
||||
import { DropdownMenuHeader } from '@/ui/layout/dropdown/components/DropdownMenuHeader/DropdownMenuHeader';
|
||||
@@ -65,7 +65,7 @@ export const ObjectOptionsDropdownLayoutContent = () => {
|
||||
recordIndexCalendarLayoutState,
|
||||
);
|
||||
const recordGroupFieldMetadata = useRecoilComponentValue(
|
||||
recordGroupFieldMetadataComponentState,
|
||||
recordIndexGroupFieldMetadataItemComponentState,
|
||||
);
|
||||
|
||||
const calendarFieldMetadata = currentView?.calendarFieldMetadataId
|
||||
|
||||
+2
-2
@@ -5,9 +5,9 @@ import { useObjectNamePluralFromSingular } from '@/object-metadata/hooks/useObje
|
||||
import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
|
||||
import { useObjectOptionsDropdown } from '@/object-record/object-options-dropdown/hooks/useObjectOptionsDropdown';
|
||||
import { useSearchRecordGroupField } from '@/object-record/object-options-dropdown/hooks/useSearchRecordGroupField';
|
||||
import { recordGroupFieldMetadataComponentState } from '@/object-record/record-group/states/recordGroupFieldMetadataComponentState';
|
||||
import { hiddenRecordGroupIdsComponentSelector } from '@/object-record/record-group/states/selectors/hiddenRecordGroupIdsComponentSelector';
|
||||
import { useHandleRecordGroupField } from '@/object-record/record-index/hooks/useHandleRecordGroupField';
|
||||
import { recordIndexGroupFieldMetadataItemComponentState } from '@/object-record/record-index/states/recordIndexGroupFieldMetadataComponentState';
|
||||
import { DropdownContent } from '@/ui/layout/dropdown/components/DropdownContent';
|
||||
import { DropdownMenuHeader } from '@/ui/layout/dropdown/components/DropdownMenuHeader/DropdownMenuHeader';
|
||||
import { DropdownMenuHeaderLeftComponent } from '@/ui/layout/dropdown/components/DropdownMenuHeader/internal/DropdownMenuHeaderLeftComponent';
|
||||
@@ -52,7 +52,7 @@ export const ObjectOptionsDropdownRecordGroupFieldsContent = () => {
|
||||
);
|
||||
|
||||
const recordGroupFieldMetadata = useRecoilComponentValue(
|
||||
recordGroupFieldMetadataComponentState,
|
||||
recordIndexGroupFieldMetadataItemComponentState,
|
||||
);
|
||||
|
||||
const {
|
||||
|
||||
+2
-2
@@ -4,9 +4,9 @@ import { OBJECT_OPTIONS_DROPDOWN_ID } from '@/object-record/object-options-dropd
|
||||
import { useObjectOptionsDropdown } from '@/object-record/object-options-dropdown/hooks/useObjectOptionsDropdown';
|
||||
import { RecordGroupsVisibilityDropdownSection } from '@/object-record/record-group/components/RecordGroupsVisibilityDropdownSection';
|
||||
import { useRecordGroupVisibility } from '@/object-record/record-group/hooks/useRecordGroupVisibility';
|
||||
import { recordGroupFieldMetadataComponentState } from '@/object-record/record-group/states/recordGroupFieldMetadataComponentState';
|
||||
import { hiddenRecordGroupIdsComponentSelector } from '@/object-record/record-group/states/selectors/hiddenRecordGroupIdsComponentSelector';
|
||||
import { visibleRecordGroupIdsComponentFamilySelector } from '@/object-record/record-group/states/selectors/visibleRecordGroupIdsComponentFamilySelector';
|
||||
import { recordIndexGroupFieldMetadataItemComponentState } from '@/object-record/record-index/states/recordIndexGroupFieldMetadataComponentState';
|
||||
import { recordIndexRecordGroupSortComponentState } from '@/object-record/record-index/states/recordIndexRecordGroupSortComponentState';
|
||||
import { recordIndexShouldHideEmptyRecordGroupsComponentState } from '@/object-record/record-index/states/recordIndexShouldHideEmptyRecordGroupsComponentState';
|
||||
import { DropdownContent } from '@/ui/layout/dropdown/components/DropdownContent';
|
||||
@@ -47,7 +47,7 @@ export const ObjectOptionsDropdownRecordGroupsContent = () => {
|
||||
const { currentView } = useGetCurrentViewOnly();
|
||||
|
||||
const recordGroupFieldMetadata = useRecoilComponentValue(
|
||||
recordGroupFieldMetadataComponentState,
|
||||
recordIndexGroupFieldMetadataItemComponentState,
|
||||
);
|
||||
|
||||
const visibleRecordGroupIds = useRecoilComponentFamilyValue(
|
||||
|
||||
+5
-2
@@ -1,8 +1,9 @@
|
||||
import gql from 'graphql-tag';
|
||||
|
||||
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||
import { getGroupByAggregateQueryName } from '@/object-record/record-aggregate/utils/getGroupByAggregateQueryName';
|
||||
import { getGroupByQueryResultGqlFieldName } from '@/page-layout/utils/getGroupByQueryResultGqlFieldName';
|
||||
import { capitalize } from 'twenty-shared/utils';
|
||||
import { getGroupByQueryResultGqlFieldName } from '../../../page-layout/utils/getGroupByQueryResultGqlFieldName';
|
||||
|
||||
export const generateGroupByAggregateQuery = ({
|
||||
objectMetadataItem,
|
||||
@@ -12,7 +13,9 @@ export const generateGroupByAggregateQuery = ({
|
||||
aggregateOperationGqlFields: string[];
|
||||
}) => {
|
||||
const capitalizedSingular = capitalize(objectMetadataItem.nameSingular);
|
||||
const queryName = `${capitalize(objectMetadataItem.namePlural)}GroupBy`;
|
||||
const queryName = getGroupByAggregateQueryName({
|
||||
objectMetadataNamePlural: objectMetadataItem.namePlural,
|
||||
});
|
||||
const queryFieldName = getGroupByQueryResultGqlFieldName(objectMetadataItem);
|
||||
|
||||
return gql`
|
||||
|
||||
+62
@@ -0,0 +1,62 @@
|
||||
import gql from 'graphql-tag';
|
||||
|
||||
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||
import { mapObjectMetadataToGraphQLQuery } from '@/object-metadata/utils/mapObjectMetadataToGraphQLQuery';
|
||||
import { type RecordGqlOperationGqlRecordFields } from '@/object-record/graphql/types/RecordGqlOperationGqlRecordFields';
|
||||
import { getGroupByQueryResultGqlFieldName } from '@/page-layout/utils/getGroupByQueryResultGqlFieldName';
|
||||
import { type ObjectPermissions } from 'twenty-shared/types';
|
||||
import { capitalize } from 'twenty-shared/utils';
|
||||
|
||||
export const generateGroupsRecordsGroupByQuery = ({
|
||||
objectMetadataItems,
|
||||
objectMetadataItem,
|
||||
recordGqlFields,
|
||||
computeReferences,
|
||||
objectPermissionsByObjectMetadataId,
|
||||
}: {
|
||||
objectMetadataItems: ObjectMetadataItem[];
|
||||
objectMetadataItem: ObjectMetadataItem;
|
||||
recordGqlFields: RecordGqlOperationGqlRecordFields;
|
||||
computeReferences?: boolean;
|
||||
objectPermissionsByObjectMetadataId: Record<
|
||||
string,
|
||||
ObjectPermissions & { objectMetadataId: string }
|
||||
>;
|
||||
}) => {
|
||||
const capitalizedSingular = capitalize(objectMetadataItem.nameSingular);
|
||||
const queryName = `${capitalize(objectMetadataItem.namePlural)}GroupByRecords`;
|
||||
const queryFieldName = getGroupByQueryResultGqlFieldName(objectMetadataItem);
|
||||
|
||||
return gql`
|
||||
query ${queryName}(
|
||||
$groupBy: [${capitalizedSingular}GroupByInput!]!
|
||||
$filter: ${capitalizedSingular}FilterInput
|
||||
$orderByForRecords: [${capitalizedSingular}OrderByInput!]
|
||||
$limit: Int
|
||||
$offsetForRecords: Int
|
||||
) {
|
||||
${queryFieldName}(
|
||||
groupBy: $groupBy
|
||||
filter: $filter
|
||||
orderByForRecords: $orderByForRecords
|
||||
limit: $limit
|
||||
offsetForRecords: $offsetForRecords
|
||||
) {
|
||||
groupByDimensionValues
|
||||
edges {
|
||||
node ${mapObjectMetadataToGraphQLQuery({
|
||||
objectMetadataItems,
|
||||
objectMetadataItem,
|
||||
recordGqlFields,
|
||||
computeReferences,
|
||||
objectPermissionsByObjectMetadataId,
|
||||
})}
|
||||
}
|
||||
pageInfo {
|
||||
endCursor
|
||||
hasNextPage
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
};
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
import { capitalize } from 'twenty-shared/utils';
|
||||
|
||||
export const getGroupByAggregateQueryName = ({
|
||||
objectMetadataNamePlural,
|
||||
}: {
|
||||
objectMetadataNamePlural: string;
|
||||
}) => {
|
||||
return `${capitalize(objectMetadataNamePlural)}GroupByAggregates`;
|
||||
};
|
||||
+9
-26
@@ -1,35 +1,24 @@
|
||||
import styled from '@emotion/styled';
|
||||
// Atlassian dnd does not support StrictMode from RN 18, so we use a fork @hello-pangea/dnd https://github.com/atlassian/react-beautiful-dnd/issues/2350
|
||||
|
||||
import { useContext, useRef } from 'react';
|
||||
|
||||
import { RecordBoardClickOutsideEffect } from '@/object-record/record-board/components/RecordBoardClickOutsideEffect';
|
||||
import { RecordBoardColumns } from '@/object-record/record-board/components/RecordBoardColumns';
|
||||
import { RecordBoardDragDropContext } from '@/object-record/record-board/components/RecordBoardDragDropContext';
|
||||
import { RecordBoardDragSelect } from '@/object-record/record-board/components/RecordBoardDragSelect';
|
||||
import { RecordBoardEffects } from '@/object-record/record-board/components/RecordBoardEffects';
|
||||
import { RecordBoardFetchMoreInViewTriggerComponent } from '@/object-record/record-board/components/RecordBoardFetchMoreInViewTriggerComponent';
|
||||
import { RecordBoardHeader } from '@/object-record/record-board/components/RecordBoardHeader';
|
||||
import { RecordBoardScrollToFocusedCardEffect } from '@/object-record/record-board/components/RecordBoardScrollToFocusedCardEffect';
|
||||
import { RecordBoardStickyHeaderEffect } from '@/object-record/record-board/components/RecordBoardStickyHeaderEffect';
|
||||
import { RecordBoardContext } from '@/object-record/record-board/contexts/RecordBoardContext';
|
||||
import { RecordBoardDeactivateBoardCardEffect } from '@/object-record/record-board/record-board-card/components/RecordBoardDeactivateBoardCardEffect';
|
||||
import { RecordBoardComponentInstanceContext } from '@/object-record/record-board/states/contexts/RecordBoardComponentInstanceContext';
|
||||
import { ScrollWrapper } from '@/ui/utilities/scroll/components/ScrollWrapper';
|
||||
|
||||
const StyledContainer = styled.div`
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: row;
|
||||
flex-direction: column;
|
||||
min-height: 100%;
|
||||
position: relative;
|
||||
`;
|
||||
|
||||
const StyledColumnContainer = styled.div`
|
||||
display: flex;
|
||||
|
||||
& > *:not(:first-of-type) {
|
||||
border-left: 1px solid ${({ theme }) => theme.border.color.light};
|
||||
}
|
||||
`;
|
||||
|
||||
const StyledContainerContainer = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -48,30 +37,24 @@ export const RecordBoard = () => {
|
||||
const boardRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
return (
|
||||
<RecordBoardComponentInstanceContext.Provider
|
||||
value={{ instanceId: recordBoardId }}
|
||||
>
|
||||
<>
|
||||
<ScrollWrapper
|
||||
componentInstanceId={`scroll-wrapper-record-board-${recordBoardId}`}
|
||||
>
|
||||
<RecordBoardStickyHeaderEffect />
|
||||
<RecordBoardScrollToFocusedCardEffect />
|
||||
<RecordBoardDeactivateBoardCardEffect />
|
||||
<RecordBoardEffects />
|
||||
<StyledContainerContainer>
|
||||
<RecordBoardHeader />
|
||||
<StyledBoardContentContainer>
|
||||
<StyledContainer ref={boardRef}>
|
||||
<RecordBoardDragDropContext>
|
||||
<StyledColumnContainer>
|
||||
<RecordBoardColumns />
|
||||
</StyledColumnContainer>
|
||||
<RecordBoardColumns />
|
||||
</RecordBoardDragDropContext>
|
||||
<RecordBoardDragSelect boardRef={boardRef} />
|
||||
<RecordBoardFetchMoreInViewTriggerComponent />
|
||||
</StyledContainer>
|
||||
</StyledBoardContentContainer>
|
||||
</StyledContainerContainer>
|
||||
</ScrollWrapper>
|
||||
<RecordBoardClickOutsideEffect />
|
||||
</RecordBoardComponentInstanceContext.Provider>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
+6
-3
@@ -3,7 +3,7 @@ import { Key } from 'ts-key-enum';
|
||||
|
||||
import { RecordBoardContext } from '@/object-record/record-board/contexts/RecordBoardContext';
|
||||
import { useFocusedRecordBoardCard } from '@/object-record/record-board/hooks/useFocusedRecordBoardCard';
|
||||
import { useRecordBoardSelection } from '@/object-record/record-board/hooks/useRecordBoardSelection';
|
||||
import { useResetRecordBoardSelection } from '@/object-record/record-board/hooks/useResetRecordBoardSelection';
|
||||
import { recordBoardSelectedRecordIdsComponentSelector } from '@/object-record/record-board/states/selectors/recordBoardSelectedRecordIdsComponentSelector';
|
||||
import { useResetFocusStackToRecordIndex } from '@/object-record/record-index/hooks/useResetFocusStackToRecordIndex';
|
||||
import { PageFocusId } from '@/types/PageFocusId';
|
||||
@@ -13,7 +13,8 @@ import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/ho
|
||||
export const RecordBoardBodyEscapeHotkeyEffect = () => {
|
||||
const { recordBoardId } = useContext(RecordBoardContext);
|
||||
|
||||
const { resetRecordSelection } = useRecordBoardSelection(recordBoardId);
|
||||
const { resetRecordBoardSelection } =
|
||||
useResetRecordBoardSelection(recordBoardId);
|
||||
const { unfocusBoardCard } = useFocusedRecordBoardCard(recordBoardId);
|
||||
const { resetFocusStackToRecordIndex } = useResetFocusStackToRecordIndex();
|
||||
|
||||
@@ -26,9 +27,11 @@ export const RecordBoardBodyEscapeHotkeyEffect = () => {
|
||||
|
||||
const handleEscape = () => {
|
||||
unfocusBoardCard();
|
||||
|
||||
if (isAtLeastOneRecordSelected) {
|
||||
resetRecordSelection();
|
||||
resetRecordBoardSelection();
|
||||
}
|
||||
|
||||
resetFocusStackToRecordIndex();
|
||||
};
|
||||
|
||||
|
||||
+4
-3
@@ -4,7 +4,7 @@ import { RECORD_BOARD_CLICK_OUTSIDE_LISTENER_ID } from '@/object-record/record-b
|
||||
import { RecordBoardContext } from '@/object-record/record-board/contexts/RecordBoardContext';
|
||||
import { useActiveRecordBoardCard } from '@/object-record/record-board/hooks/useActiveRecordBoardCard';
|
||||
import { useFocusedRecordBoardCard } from '@/object-record/record-board/hooks/useFocusedRecordBoardCard';
|
||||
import { useRecordBoardSelection } from '@/object-record/record-board/hooks/useRecordBoardSelection';
|
||||
import { useResetRecordBoardSelection } from '@/object-record/record-board/hooks/useResetRecordBoardSelection';
|
||||
import { RECORD_BOARD_CARD_CLICK_OUTSIDE_ID } from '@/object-record/record-board/record-board-card/constants/RecordBoardCardClickOutsideId';
|
||||
import { isDraggingRecordComponentState } from '@/object-record/record-drag/states/isDraggingRecordComponentState';
|
||||
|
||||
@@ -25,7 +25,8 @@ export const RecordBoardClickOutsideEffect = () => {
|
||||
const { deactivateBoardCard } = useActiveRecordBoardCard(recordBoardId);
|
||||
const { unfocusBoardCard } = useFocusedRecordBoardCard(recordBoardId);
|
||||
|
||||
const { resetRecordSelection } = useRecordBoardSelection(recordBoardId);
|
||||
const { resetRecordBoardSelection } =
|
||||
useResetRecordBoardSelection(recordBoardId);
|
||||
|
||||
useListenClickOutside({
|
||||
excludedClickOutsideIds: [
|
||||
@@ -40,7 +41,7 @@ export const RecordBoardClickOutsideEffect = () => {
|
||||
refs: [],
|
||||
callback: () => {
|
||||
if (!isDraggingRecord) {
|
||||
resetRecordSelection();
|
||||
resetRecordBoardSelection();
|
||||
deactivateBoardCard();
|
||||
unfocusBoardCard();
|
||||
}
|
||||
|
||||
+22
-9
@@ -2,6 +2,15 @@ import { RecordBoardColumn } from '@/object-record/record-board/record-board-col
|
||||
import { visibleRecordGroupIdsComponentFamilySelector } from '@/object-record/record-group/states/selectors/visibleRecordGroupIdsComponentFamilySelector';
|
||||
import { useRecoilComponentFamilyValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentFamilyValue';
|
||||
import { ViewType } from '@/views/types/ViewType';
|
||||
import styled from '@emotion/styled';
|
||||
|
||||
const StyledColumnContainer = styled.div`
|
||||
display: flex;
|
||||
|
||||
& > *:not(:first-of-type) {
|
||||
border-left: 1px solid ${({ theme }) => theme.border.color.light};
|
||||
}
|
||||
`;
|
||||
|
||||
export const RecordBoardColumns = () => {
|
||||
const visibleRecordGroupIds = useRecoilComponentFamilyValue(
|
||||
@@ -9,13 +18,17 @@ export const RecordBoardColumns = () => {
|
||||
ViewType.Kanban,
|
||||
);
|
||||
|
||||
return visibleRecordGroupIds.map((recordGroupId, index) => {
|
||||
return (
|
||||
<RecordBoardColumn
|
||||
key={recordGroupId}
|
||||
recordBoardColumnId={recordGroupId}
|
||||
recordBoardColumnIndex={index}
|
||||
/>
|
||||
);
|
||||
});
|
||||
return (
|
||||
<StyledColumnContainer>
|
||||
{visibleRecordGroupIds.map((recordGroupId, index) => {
|
||||
return (
|
||||
<RecordBoardColumn
|
||||
key={recordGroupId}
|
||||
recordBoardColumnId={recordGroupId}
|
||||
recordBoardColumnIndex={index}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</StyledColumnContainer>
|
||||
);
|
||||
};
|
||||
|
||||
+17
-10
@@ -7,23 +7,26 @@ import { RecordBoard } from '@/object-record/record-board/components/RecordBoard
|
||||
import { RecordBoardBodyEscapeHotkeyEffect } from '@/object-record/record-board/components/RecordBoardBodyEscapeHotkeyEffect';
|
||||
import { RecordBoardHotkeyEffect } from '@/object-record/record-board/components/RecordBoardHotkeyEffect';
|
||||
import { RecordBoardContext } from '@/object-record/record-board/contexts/RecordBoardContext';
|
||||
import { RecordBoardComponentInstanceContext } from '@/object-record/record-board/states/contexts/RecordBoardComponentInstanceContext';
|
||||
import { recordIndexGroupFieldMetadataItemComponentState } from '@/object-record/record-index/states/recordIndexGroupFieldMetadataComponentState';
|
||||
|
||||
import { RecordIndexRemoveSortingModal } from '@/object-record/record-index/components/RecordIndexRemoveSortingModal';
|
||||
import { RECORD_INDEX_REMOVE_SORTING_MODAL_ID } from '@/object-record/record-index/constants/RecordIndexRemoveSortingModalId';
|
||||
import { recordIndexGroupFieldMetadataItemComponentState } from '@/object-record/record-index/states/recordIndexGroupFieldMetadataItemComponentState';
|
||||
|
||||
import { isModalOpenedComponentState } from '@/ui/layout/modal/states/isModalOpenedComponentState';
|
||||
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
type RecordIndexBoardContainerProps = {
|
||||
type RecordBoardContainerProps = {
|
||||
recordBoardId: string;
|
||||
viewBarId: string;
|
||||
objectNameSingular: string;
|
||||
};
|
||||
|
||||
export const RecordIndexBoardContainer = ({
|
||||
export const RecordBoardContainer = ({
|
||||
recordBoardId,
|
||||
objectNameSingular,
|
||||
}: RecordIndexBoardContainerProps) => {
|
||||
}: RecordBoardContainerProps) => {
|
||||
const { objectMetadataItem } = useObjectMetadataItem({
|
||||
objectNameSingular,
|
||||
});
|
||||
@@ -64,12 +67,16 @@ export const RecordIndexBoardContainer = ({
|
||||
objectPermissions,
|
||||
}}
|
||||
>
|
||||
<RecordBoard />
|
||||
{isRecordIndexRemoveSortingModalOpened && (
|
||||
<RecordIndexRemoveSortingModal />
|
||||
)}
|
||||
<RecordBoardHotkeyEffect />
|
||||
<RecordBoardBodyEscapeHotkeyEffect />
|
||||
<RecordBoardComponentInstanceContext.Provider
|
||||
value={{ instanceId: recordBoardId }}
|
||||
>
|
||||
<RecordBoard />
|
||||
{isRecordIndexRemoveSortingModalOpened && (
|
||||
<RecordIndexRemoveSortingModal />
|
||||
)}
|
||||
<RecordBoardHotkeyEffect />
|
||||
<RecordBoardBodyEscapeHotkeyEffect />
|
||||
</RecordBoardComponentInstanceContext.Provider>
|
||||
</RecordBoardContext.Provider>
|
||||
);
|
||||
};
|
||||
-1
@@ -38,7 +38,6 @@ export const RecordBoardDragDropContext = ({
|
||||
|
||||
const { startRecordDrag } = useStartRecordDrag();
|
||||
const { endRecordDrag } = useEndRecordDrag();
|
||||
// const multiDragState = useRecordDragState('board', recordBoardId);
|
||||
|
||||
const { processBoardCardDrop } = useProcessBoardCardDrop();
|
||||
|
||||
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
import { RecordBoardClickOutsideEffect } from '@/object-record/record-board/components/RecordBoardClickOutsideEffect';
|
||||
import { RecordBoardQueryEffect } from '@/object-record/record-board/components/RecordBoardQueryEffect';
|
||||
import { RecordBoardScrollToFocusedCardEffect } from '@/object-record/record-board/components/RecordBoardScrollToFocusedCardEffect';
|
||||
import { RecordBoardSelectRecordsEffect } from '@/object-record/record-board/components/RecordBoardSelectRecordsEffect';
|
||||
import { RecordBoardStickyHeaderEffect } from '@/object-record/record-board/components/RecordBoardStickyHeaderEffect';
|
||||
import { RecordBoardDeactivateBoardCardEffect } from '@/object-record/record-board/record-board-card/components/RecordBoardDeactivateBoardCardEffect';
|
||||
|
||||
export const RecordBoardEffects = () => {
|
||||
return (
|
||||
<>
|
||||
<RecordBoardStickyHeaderEffect />
|
||||
<RecordBoardScrollToFocusedCardEffect />
|
||||
<RecordBoardDeactivateBoardCardEffect />
|
||||
<RecordBoardQueryEffect />
|
||||
<RecordBoardSelectRecordsEffect />
|
||||
<RecordBoardClickOutsideEffect />
|
||||
</>
|
||||
);
|
||||
};
|
||||
+76
@@ -0,0 +1,76 @@
|
||||
import styled from '@emotion/styled';
|
||||
import { useEffect } from 'react';
|
||||
import { useInView } from 'react-intersection-observer';
|
||||
|
||||
import { RECORD_BOARD_COLUMN_PADDING_AND_BORDER_WIDTH } from '@/object-record/record-board/constants/RecordBoardColumnPaddingAndBorderWidth';
|
||||
|
||||
import { RECORD_BOARD_COLUMN_WIDTH } from '@/object-record/record-board/constants/RecordBoardColumnWidth';
|
||||
import { recordBoardIsFetchingMoreComponentState } from '@/object-record/record-board/states/recordBoardIsFetchingMoreComponentState';
|
||||
import { recordBoardShouldFetchMoreComponentState } from '@/object-record/record-board/states/recordBoardShouldFetchMoreComponentState';
|
||||
import { visibleRecordGroupIdsComponentFamilySelector } from '@/object-record/record-group/states/selectors/visibleRecordGroupIdsComponentFamilySelector';
|
||||
import { recordIndexRecordGroupsAreInInitialLoadingComponentState } from '@/object-record/record-index/states/recordIndexRecordGroupsAreInInitialLoadingComponentState';
|
||||
import { useScrollWrapperHTMLElement } from '@/ui/utilities/scroll/hooks/useScrollWrapperHTMLElement';
|
||||
import { useRecoilComponentFamilyValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentFamilyValue';
|
||||
import { useRecoilComponentState } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentState';
|
||||
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
|
||||
import { ViewType } from '@/views/types/ViewType';
|
||||
|
||||
const StyledFetchMoreTriggerDiv = styled.div<{ width: number }>`
|
||||
max-width: ${({ width }) => width}px;
|
||||
min-width: ${({ width }) => width}px;
|
||||
`;
|
||||
|
||||
export const RecordBoardFetchMoreInViewTriggerComponent = () => {
|
||||
const [shouldFetchMore, setShouldFetchMore] = useRecoilComponentState(
|
||||
recordBoardShouldFetchMoreComponentState,
|
||||
);
|
||||
|
||||
const isInitialLoading = useRecoilComponentValue(
|
||||
recordIndexRecordGroupsAreInInitialLoadingComponentState,
|
||||
);
|
||||
|
||||
const isFetchingMore = useRecoilComponentValue(
|
||||
recordBoardIsFetchingMoreComponentState,
|
||||
);
|
||||
|
||||
const { scrollWrapperHTMLElement } = useScrollWrapperHTMLElement();
|
||||
|
||||
const { ref, inView } = useInView({
|
||||
rootMargin: '1600px',
|
||||
root: scrollWrapperHTMLElement,
|
||||
});
|
||||
|
||||
const visibleRecordGroupIds = useRecoilComponentFamilyValue(
|
||||
visibleRecordGroupIdsComponentFamilySelector,
|
||||
ViewType.Kanban,
|
||||
);
|
||||
|
||||
const componentWidth =
|
||||
visibleRecordGroupIds.length * RECORD_BOARD_COLUMN_WIDTH +
|
||||
visibleRecordGroupIds.length *
|
||||
RECORD_BOARD_COLUMN_PADDING_AND_BORDER_WIDTH -
|
||||
1;
|
||||
|
||||
useEffect(() => {
|
||||
if (!isInitialLoading && !isFetchingMore) {
|
||||
const newShouldFetchMore = inView;
|
||||
|
||||
if (shouldFetchMore !== newShouldFetchMore) {
|
||||
setShouldFetchMore(newShouldFetchMore);
|
||||
}
|
||||
}
|
||||
}, [
|
||||
shouldFetchMore,
|
||||
setShouldFetchMore,
|
||||
inView,
|
||||
isInitialLoading,
|
||||
isFetchingMore,
|
||||
]);
|
||||
|
||||
return (
|
||||
<StyledFetchMoreTriggerDiv
|
||||
width={componentWidth}
|
||||
ref={ref}
|
||||
></StyledFetchMoreTriggerDiv>
|
||||
);
|
||||
};
|
||||
+88
@@ -0,0 +1,88 @@
|
||||
import { useTriggerRecordBoardFetchMore } from '@/object-record/record-board/hooks/useTriggerRecordBoardFetchMore';
|
||||
import { useTriggerRecordBoardInitialQuery } from '@/object-record/record-board/hooks/useTriggerRecordBoardInitialQuery';
|
||||
import { lastRecordBoardQueryIdentifierComponentState } from '@/object-record/record-board/states/lastRecordBoardQueryIdentifierComponentState';
|
||||
import { recordBoardCurrentGroupByQueryOffsetComponentState } from '@/object-record/record-board/states/recordBoardCurrentGroupByQueryOffsetComponentState';
|
||||
import { recordBoardIsFetchingMoreComponentState } from '@/object-record/record-board/states/recordBoardIsFetchingMoreComponentState';
|
||||
import { recordBoardShouldFetchMoreComponentState } from '@/object-record/record-board/states/recordBoardShouldFetchMoreComponentState';
|
||||
import { useRecordIndexContextOrThrow } from '@/object-record/record-index/contexts/RecordIndexContext';
|
||||
import { useRecordIndexGroupCommonQueryVariables } from '@/object-record/record-index/hooks/useRecordIndexGroupCommonQueryVariables';
|
||||
|
||||
import { recordIndexRecordGroupsAreInInitialLoadingComponentState } from '@/object-record/record-index/states/recordIndexRecordGroupsAreInInitialLoadingComponentState';
|
||||
import { getQueryIdentifier } from '@/object-record/utils/getQueryIdentifier';
|
||||
import { useScrollWrapperHTMLElement } from '@/ui/utilities/scroll/hooks/useScrollWrapperHTMLElement';
|
||||
import { useRecoilComponentState } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentState';
|
||||
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
|
||||
import { useSetRecoilComponentState } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentState';
|
||||
|
||||
import { useEffect } from 'react';
|
||||
|
||||
export const RecordBoardQueryEffect = () => {
|
||||
const { objectMetadataItem } = useRecordIndexContextOrThrow();
|
||||
|
||||
const [lastRecordBoardQueryIdentifier, setLastRecordBoardQueryIdentifier] =
|
||||
useRecoilComponentState(lastRecordBoardQueryIdentifierComponentState);
|
||||
|
||||
const [recordIndexRecordGroupsAreInInitialLoading] = useRecoilComponentState(
|
||||
recordIndexRecordGroupsAreInInitialLoadingComponentState,
|
||||
);
|
||||
|
||||
const setRecordBoardCurrentGroupByQueryOffset = useSetRecoilComponentState(
|
||||
recordBoardCurrentGroupByQueryOffsetComponentState,
|
||||
);
|
||||
|
||||
const { combinedFilters, orderBy } =
|
||||
useRecordIndexGroupCommonQueryVariables();
|
||||
|
||||
const queryIdentifier = getQueryIdentifier({
|
||||
objectNameSingular: objectMetadataItem.nameSingular,
|
||||
filter: combinedFilters,
|
||||
orderBy,
|
||||
});
|
||||
|
||||
const queryIdentifierHasChanged =
|
||||
queryIdentifier !== lastRecordBoardQueryIdentifier;
|
||||
|
||||
const { scrollWrapperHTMLElement } = useScrollWrapperHTMLElement();
|
||||
|
||||
const [shouldFetchMore] = useRecoilComponentState(
|
||||
recordBoardShouldFetchMoreComponentState,
|
||||
);
|
||||
|
||||
const recordBoardIsFetchingMore = useRecoilComponentValue(
|
||||
recordBoardIsFetchingMoreComponentState,
|
||||
);
|
||||
|
||||
const { triggerRecordBoardFetchMore } = useTriggerRecordBoardFetchMore();
|
||||
|
||||
const { triggerRecordBoardInitialQuery } =
|
||||
useTriggerRecordBoardInitialQuery();
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
!recordIndexRecordGroupsAreInInitialLoading &&
|
||||
queryIdentifierHasChanged
|
||||
) {
|
||||
triggerRecordBoardInitialQuery();
|
||||
} else if (
|
||||
!recordIndexRecordGroupsAreInInitialLoading &&
|
||||
shouldFetchMore &&
|
||||
!queryIdentifierHasChanged &&
|
||||
!recordBoardIsFetchingMore
|
||||
) {
|
||||
triggerRecordBoardFetchMore();
|
||||
}
|
||||
}, [
|
||||
triggerRecordBoardInitialQuery,
|
||||
queryIdentifierHasChanged,
|
||||
setLastRecordBoardQueryIdentifier,
|
||||
queryIdentifier,
|
||||
setRecordBoardCurrentGroupByQueryOffset,
|
||||
scrollWrapperHTMLElement,
|
||||
recordIndexRecordGroupsAreInInitialLoading,
|
||||
shouldFetchMore,
|
||||
recordBoardIsFetchingMore,
|
||||
triggerRecordBoardFetchMore,
|
||||
]);
|
||||
|
||||
return null;
|
||||
};
|
||||
+1
-8
@@ -5,16 +5,9 @@ import { recordBoardSelectedRecordIdsComponentSelector } from '@/object-record/r
|
||||
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
|
||||
import { useSetRecoilComponentState } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentState';
|
||||
|
||||
type RecordIndexBoardDataLoaderEffectProps = {
|
||||
recordBoardId: string;
|
||||
};
|
||||
|
||||
export const RecordIndexBoardDataLoaderEffect = ({
|
||||
recordBoardId,
|
||||
}: RecordIndexBoardDataLoaderEffectProps) => {
|
||||
export const RecordBoardSelectRecordsEffect = () => {
|
||||
const selectedRecordIds = useRecoilComponentValue(
|
||||
recordBoardSelectedRecordIdsComponentSelector,
|
||||
recordBoardId,
|
||||
);
|
||||
|
||||
const setContextStoreTargetedRecords = useSetRecoilComponentState(
|
||||
+1
@@ -0,0 +1 @@
|
||||
export const RECORD_BOARD_COLUMN_PADDING_AND_BORDER_WIDTH = 17;
|
||||
+1
@@ -0,0 +1 @@
|
||||
export const RECORD_BOARD_COLUMN_WIDTH = 200;
|
||||
+1
@@ -0,0 +1 @@
|
||||
export const RECORD_BOARD_FETCH_MORE_THROTTLING_WAIT_TIME_IN_MILLISECONDS_TO_AVOID_REACT_FREEZE = 50;
|
||||
+1
@@ -0,0 +1 @@
|
||||
export const RECORD_BOARD_QUERY_PAGE_SIZE = 10;
|
||||
+6
-3
@@ -4,6 +4,7 @@ import { useActiveRecordBoardCard } from '@/object-record/record-board/hooks/use
|
||||
import { useFocusedRecordBoardCard } from '@/object-record/record-board/hooks/useFocusedRecordBoardCard';
|
||||
import { useRecordBoardSelectAllHotkeys } from '@/object-record/record-board/hooks/useRecordBoardSelectAllHotkeys';
|
||||
import { useRecordBoardSelection } from '@/object-record/record-board/hooks/useRecordBoardSelection';
|
||||
import { useResetRecordBoardSelection } from '@/object-record/record-board/hooks/useResetRecordBoardSelection';
|
||||
import { RecordBoardCardContext } from '@/object-record/record-board/record-board-card/contexts/RecordBoardCardContext';
|
||||
import { isRecordBoardCardSelectedComponentFamilyState } from '@/object-record/record-board/states/isRecordBoardCardSelectedComponentFamilyState';
|
||||
import { recordBoardSelectedRecordIdsComponentSelector } from '@/object-record/record-board/states/selectors/recordBoardSelectedRecordIdsComponentSelector';
|
||||
@@ -21,8 +22,9 @@ export const useRecordBoardCardHotkeys = (focusId: string) => {
|
||||
|
||||
const { openRecordInCommandMenu } = useOpenRecordInCommandMenu();
|
||||
const { activateBoardCard } = useActiveRecordBoardCard();
|
||||
const { setRecordAsSelected, resetRecordSelection } =
|
||||
useRecordBoardSelection();
|
||||
const { setRecordAsSelected } = useRecordBoardSelection();
|
||||
|
||||
const { resetRecordBoardSelection } = useResetRecordBoardSelection();
|
||||
const { unfocusBoardCard } = useFocusedRecordBoardCard(recordBoardId);
|
||||
|
||||
const isRecordBoardCardSelected = useRecoilComponentFamilyValue(
|
||||
@@ -56,8 +58,9 @@ export const useRecordBoardCardHotkeys = (focusId: string) => {
|
||||
|
||||
const handleEscape = () => {
|
||||
unfocusBoardCard();
|
||||
|
||||
if (isAtLeastOneRecordSelected) {
|
||||
resetRecordSelection();
|
||||
resetRecordBoardSelection();
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
-23
@@ -32,28 +32,6 @@ export const useRecordBoardSelection = (recordBoardId?: string) => {
|
||||
getActionMenuIdFromRecordIndexId(instanceIdFromProps),
|
||||
);
|
||||
|
||||
const resetRecordSelection = useRecoilCallback(
|
||||
({ snapshot, set }) =>
|
||||
() => {
|
||||
closeDropdown(dropdownId);
|
||||
|
||||
const recordIds = getSnapshotValue(
|
||||
snapshot,
|
||||
recordBoardSelectedRecordIdsSelector,
|
||||
);
|
||||
|
||||
for (const recordId of recordIds) {
|
||||
set(isRecordBoardCardSelectedFamilyState(recordId), false);
|
||||
}
|
||||
},
|
||||
[
|
||||
closeDropdown,
|
||||
dropdownId,
|
||||
recordBoardSelectedRecordIdsSelector,
|
||||
isRecordBoardCardSelectedFamilyState,
|
||||
],
|
||||
);
|
||||
|
||||
const setRecordAsSelected = useRecoilCallback(
|
||||
({ snapshot, set }) =>
|
||||
(recordId: string, isSelected: boolean) => {
|
||||
@@ -85,7 +63,6 @@ export const useRecordBoardSelection = (recordBoardId?: string) => {
|
||||
);
|
||||
|
||||
return {
|
||||
resetRecordSelection,
|
||||
setRecordAsSelected,
|
||||
checkIfLastUnselectAndCloseDropdown,
|
||||
};
|
||||
|
||||
+101
@@ -0,0 +1,101 @@
|
||||
import { useResetRecordBoardSelection } from '@/object-record/record-board/hooks/useResetRecordBoardSelection';
|
||||
import { recordBoardSelectedRecordIdsComponentSelector } from '@/object-record/record-board/states/selectors/recordBoardSelectedRecordIdsComponentSelector';
|
||||
import { recordGroupDefinitionsComponentSelector } from '@/object-record/record-group/states/selectors/recordGroupDefinitionsComponentSelector';
|
||||
import { recordIndexGroupFieldMetadataItemComponentState } from '@/object-record/record-index/states/recordIndexGroupFieldMetadataComponentState';
|
||||
import { recordIndexRecordIdsByGroupComponentFamilyState } from '@/object-record/record-index/states/recordIndexRecordIdsByGroupComponentFamilyState';
|
||||
import { useRecoilComponentCallbackState } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackState';
|
||||
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
|
||||
import { getSnapshotValue } from '@/ui/utilities/state/utils/getSnapshotValue';
|
||||
import { useRecoilCallback } from 'recoil';
|
||||
import { isDefined, isNonEmptyArray } from 'twenty-shared/utils';
|
||||
|
||||
export const useRemoveSelectedRecordsFromRecordBoard = (
|
||||
recordBoardIndexId: string,
|
||||
) => {
|
||||
const recordGroupDefinitions = useRecoilComponentValue(
|
||||
recordGroupDefinitionsComponentSelector,
|
||||
recordBoardIndexId,
|
||||
);
|
||||
|
||||
const groupByFieldMetadataItem = useRecoilComponentValue(
|
||||
recordIndexGroupFieldMetadataItemComponentState,
|
||||
recordBoardIndexId,
|
||||
);
|
||||
|
||||
const recordIndexRecordIdsByGroupCallbackState =
|
||||
useRecoilComponentCallbackState(
|
||||
recordIndexRecordIdsByGroupComponentFamilyState,
|
||||
recordBoardIndexId,
|
||||
);
|
||||
|
||||
const recordBoardSelectedRecordIdsCallbackSelector =
|
||||
useRecoilComponentCallbackState(
|
||||
recordBoardSelectedRecordIdsComponentSelector,
|
||||
recordBoardIndexId,
|
||||
);
|
||||
|
||||
const { resetRecordBoardSelection } =
|
||||
useResetRecordBoardSelection(recordBoardIndexId);
|
||||
|
||||
const removeSelectedRecordsFromRecordBoard = useRecoilCallback(
|
||||
({ snapshot, set }) =>
|
||||
() => {
|
||||
const deletedRecordIds = getSnapshotValue(
|
||||
snapshot,
|
||||
recordBoardSelectedRecordIdsCallbackSelector,
|
||||
);
|
||||
|
||||
if (
|
||||
!isDefined(groupByFieldMetadataItem) ||
|
||||
!isNonEmptyArray(recordGroupDefinitions) ||
|
||||
!isNonEmptyArray(deletedRecordIds)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const recordGroup of recordGroupDefinitions) {
|
||||
const currentRecordIds = getSnapshotValue(
|
||||
snapshot,
|
||||
recordIndexRecordIdsByGroupCallbackState(recordGroup.id),
|
||||
);
|
||||
|
||||
let groupRecordIdsUpdated = [...currentRecordIds];
|
||||
|
||||
for (const deletedRecordId of deletedRecordIds) {
|
||||
const indexOfDeletedRecordIdInGroupRecordIds =
|
||||
groupRecordIdsUpdated.findIndex(
|
||||
(recordIdInRecordGroup) =>
|
||||
recordIdInRecordGroup === deletedRecordId,
|
||||
);
|
||||
|
||||
if (indexOfDeletedRecordIdInGroupRecordIds > -1) {
|
||||
groupRecordIdsUpdated = groupRecordIdsUpdated.toSpliced(
|
||||
indexOfDeletedRecordIdInGroupRecordIds,
|
||||
1,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (groupRecordIdsUpdated.length !== currentRecordIds.length) {
|
||||
set(
|
||||
recordIndexRecordIdsByGroupCallbackState(recordGroup.id),
|
||||
groupRecordIdsUpdated,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
resetRecordBoardSelection();
|
||||
},
|
||||
[
|
||||
groupByFieldMetadataItem,
|
||||
recordIndexRecordIdsByGroupCallbackState,
|
||||
recordGroupDefinitions,
|
||||
recordBoardSelectedRecordIdsCallbackSelector,
|
||||
resetRecordBoardSelection,
|
||||
],
|
||||
);
|
||||
|
||||
return {
|
||||
removeSelectedRecordsFromRecordBoard,
|
||||
};
|
||||
};
|
||||
+60
@@ -0,0 +1,60 @@
|
||||
import { useRecoilCallback } from 'recoil';
|
||||
|
||||
import { getActionMenuDropdownIdFromActionMenuId } from '@/action-menu/utils/getActionMenuDropdownIdFromActionMenuId';
|
||||
import { getActionMenuIdFromRecordIndexId } from '@/action-menu/utils/getActionMenuIdFromRecordIndexId';
|
||||
import { RecordBoardComponentInstanceContext } from '@/object-record/record-board/states/contexts/RecordBoardComponentInstanceContext';
|
||||
import { isRecordBoardCardSelectedComponentFamilyState } from '@/object-record/record-board/states/isRecordBoardCardSelectedComponentFamilyState';
|
||||
import { recordBoardSelectedRecordIdsComponentSelector } from '@/object-record/record-board/states/selectors/recordBoardSelectedRecordIdsComponentSelector';
|
||||
import { useCloseDropdown } from '@/ui/layout/dropdown/hooks/useCloseDropdown';
|
||||
import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
|
||||
import { useRecoilComponentCallbackState } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackState';
|
||||
import { getSnapshotValue } from '@/ui/utilities/state/utils/getSnapshotValue';
|
||||
|
||||
export const useResetRecordBoardSelection = (recordBoardId?: string) => {
|
||||
const instanceIdFromProps = useAvailableComponentInstanceIdOrThrow(
|
||||
RecordBoardComponentInstanceContext,
|
||||
recordBoardId,
|
||||
);
|
||||
|
||||
const isRecordBoardCardSelectedFamilyState = useRecoilComponentCallbackState(
|
||||
isRecordBoardCardSelectedComponentFamilyState,
|
||||
recordBoardId,
|
||||
);
|
||||
|
||||
const recordBoardSelectedRecordIdsSelector = useRecoilComponentCallbackState(
|
||||
recordBoardSelectedRecordIdsComponentSelector,
|
||||
recordBoardId,
|
||||
);
|
||||
|
||||
const { closeDropdown } = useCloseDropdown();
|
||||
|
||||
const dropdownId = getActionMenuDropdownIdFromActionMenuId(
|
||||
getActionMenuIdFromRecordIndexId(instanceIdFromProps),
|
||||
);
|
||||
|
||||
const resetRecordBoardSelection = useRecoilCallback(
|
||||
({ snapshot, set }) =>
|
||||
() => {
|
||||
closeDropdown(dropdownId);
|
||||
|
||||
const recordIds = getSnapshotValue(
|
||||
snapshot,
|
||||
recordBoardSelectedRecordIdsSelector,
|
||||
);
|
||||
|
||||
for (const recordId of recordIds) {
|
||||
set(isRecordBoardCardSelectedFamilyState(recordId), false);
|
||||
}
|
||||
},
|
||||
[
|
||||
closeDropdown,
|
||||
dropdownId,
|
||||
recordBoardSelectedRecordIdsSelector,
|
||||
isRecordBoardCardSelectedFamilyState,
|
||||
],
|
||||
);
|
||||
|
||||
return {
|
||||
resetRecordBoardSelection,
|
||||
};
|
||||
};
|
||||
+5
-4
@@ -1,6 +1,6 @@
|
||||
import { useRecoilCallback } from 'recoil';
|
||||
|
||||
import { useRecordBoardSelection } from '@/object-record/record-board/hooks/useRecordBoardSelection';
|
||||
import { useResetRecordBoardSelection } from '@/object-record/record-board/hooks/useResetRecordBoardSelection';
|
||||
import { isRecordBoardCardSelectedComponentFamilyState } from '@/object-record/record-board/states/isRecordBoardCardSelectedComponentFamilyState';
|
||||
import { allCardsSelectedStatusComponentSelector } from '@/object-record/record-board/states/selectors/allCardsSelectedStatusComponentSelector';
|
||||
import { allRecordIdsOfAllRecordGroupsComponentSelector } from '@/object-record/record-index/states/selectors/allRecordIdsOfAllRecordGroupsComponentSelector';
|
||||
@@ -22,7 +22,8 @@ export const useSelectAllCards = (recordBoardId?: string) => {
|
||||
recordBoardId,
|
||||
);
|
||||
|
||||
const { resetRecordSelection } = useRecordBoardSelection(recordBoardId);
|
||||
const { resetRecordBoardSelection } =
|
||||
useResetRecordBoardSelection(recordBoardId);
|
||||
|
||||
const selectAllCards = useRecoilCallback(
|
||||
({ set, snapshot }) =>
|
||||
@@ -38,7 +39,7 @@ export const useSelectAllCards = (recordBoardId?: string) => {
|
||||
);
|
||||
|
||||
if (allCardsSelectedStatus === 'all') {
|
||||
resetRecordSelection();
|
||||
resetRecordBoardSelection();
|
||||
}
|
||||
|
||||
for (const recordId of allRecordIds) {
|
||||
@@ -52,7 +53,7 @@ export const useSelectAllCards = (recordBoardId?: string) => {
|
||||
[
|
||||
allCardsSelectedStatusSelector,
|
||||
allRecordIdsOfAllRecordGroupsCallbackSelector,
|
||||
resetRecordSelection,
|
||||
resetRecordBoardSelection,
|
||||
isRecordBoardCardSelectedFamilyState,
|
||||
],
|
||||
);
|
||||
|
||||
+2
-2
@@ -2,7 +2,7 @@ import { useRecoilCallback } from 'recoil';
|
||||
|
||||
import { emptyRecordGroupByIdComponentFamilyState } from '@/object-record/record-group/states/emptyRecordGroupByIdComponentFamilyState';
|
||||
import { recordGroupDefinitionFamilyState } from '@/object-record/record-group/states/recordGroupDefinitionFamilyState';
|
||||
import { recordGroupFieldMetadataComponentState } from '@/object-record/record-group/states/recordGroupFieldMetadataComponentState';
|
||||
import { recordIndexGroupFieldMetadataItemComponentState } from '@/object-record/record-index/states/recordIndexGroupFieldMetadataComponentState';
|
||||
import { recordIndexRecordIdsByGroupComponentFamilyState } from '@/object-record/record-index/states/recordIndexRecordIdsByGroupComponentFamilyState';
|
||||
import { type ObjectRecord } from '@/object-record/types/ObjectRecord';
|
||||
import { useRecoilComponentCallbackState } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackState';
|
||||
@@ -12,7 +12,7 @@ import { isDeeplyEqual } from '~/utils/isDeeplyEqual';
|
||||
|
||||
export const useSetRecordIdsForColumn = (recordBoardId?: string) => {
|
||||
const recordGroupFieldMetadataState = useRecoilComponentCallbackState(
|
||||
recordGroupFieldMetadataComponentState,
|
||||
recordIndexGroupFieldMetadataItemComponentState,
|
||||
recordBoardId,
|
||||
);
|
||||
|
||||
|
||||
+231
@@ -0,0 +1,231 @@
|
||||
import { getRecordsFromRecordConnection } from '@/object-record/cache/utils/getRecordsFromRecordConnection';
|
||||
import { RECORD_BOARD_FETCH_MORE_THROTTLING_WAIT_TIME_IN_MILLISECONDS_TO_AVOID_REACT_FREEZE } from '@/object-record/record-board/constants/RecordBoardFetchMoreThrottlingWaitTimeInMillisecondsToAvoidReactFreeze';
|
||||
import { RECORD_BOARD_QUERY_PAGE_SIZE } from '@/object-record/record-board/constants/RecordBoardQueryPageSize';
|
||||
|
||||
import { recordBoardCurrentGroupByQueryOffsetComponentState } from '@/object-record/record-board/states/recordBoardCurrentGroupByQueryOffsetComponentState';
|
||||
import { recordBoardIsFetchingMoreComponentState } from '@/object-record/record-board/states/recordBoardIsFetchingMoreComponentState';
|
||||
import { recordBoardShouldFetchMoreInColumnComponentFamilyState } from '@/object-record/record-board/states/recordBoardShouldFetchMoreInColumnComponentFamilyState';
|
||||
import { recordGroupDefinitionsComponentSelector } from '@/object-record/record-group/states/selectors/recordGroupDefinitionsComponentSelector';
|
||||
import { useRecordIndexContextOrThrow } from '@/object-record/record-index/contexts/RecordIndexContext';
|
||||
import { useRecordIndexGroupCommonQueryVariables } from '@/object-record/record-index/hooks/useRecordIndexGroupCommonQueryVariables';
|
||||
import { useRecordIndexGroupsRecordsLazyGroupBy } from '@/object-record/record-index/hooks/useRecordIndexGroupsRecordsLazyGroupBy';
|
||||
import { recordIndexGroupFieldMetadataItemComponentState } from '@/object-record/record-index/states/recordIndexGroupFieldMetadataComponentState';
|
||||
import { recordIndexRecordIdsByGroupComponentFamilyState } from '@/object-record/record-index/states/recordIndexRecordIdsByGroupComponentFamilyState';
|
||||
import { useUpsertRecordsInStore } from '@/object-record/record-store/hooks/useUpsertRecordsInStore';
|
||||
import { getGroupByQueryResultGqlFieldName } from '@/page-layout/utils/getGroupByQueryResultGqlFieldName';
|
||||
import { useRecoilComponentCallbackState } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackState';
|
||||
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
|
||||
import { getSnapshotValue } from '@/ui/utilities/state/utils/getSnapshotValue';
|
||||
import { isNonEmptyArray } from '@sniptt/guards';
|
||||
import { useRecoilCallback } from 'recoil';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { sortByProperty } from '~/utils/array/sortByProperty';
|
||||
import { sleep } from '~/utils/sleep';
|
||||
|
||||
export const useTriggerRecordBoardFetchMore = () => {
|
||||
const recordGroupDefinitions = useRecoilComponentValue(
|
||||
recordGroupDefinitionsComponentSelector,
|
||||
);
|
||||
|
||||
const { objectMetadataItem } = useRecordIndexContextOrThrow();
|
||||
|
||||
const recordIndexGroupFieldMetadataItem = useRecoilComponentValue(
|
||||
recordIndexGroupFieldMetadataItemComponentState,
|
||||
);
|
||||
|
||||
const recordBoardShouldFetchMoreInColumnFamilyCallbackState =
|
||||
useRecoilComponentCallbackState(
|
||||
recordBoardShouldFetchMoreInColumnComponentFamilyState,
|
||||
);
|
||||
|
||||
const { combinedFilters } = useRecordIndexGroupCommonQueryVariables();
|
||||
|
||||
const { upsertRecordsInStore } = useUpsertRecordsInStore();
|
||||
|
||||
const recordBoardCurrentGroupByQueryOffsetCallbackState =
|
||||
useRecoilComponentCallbackState(
|
||||
recordBoardCurrentGroupByQueryOffsetComponentState,
|
||||
);
|
||||
|
||||
const { executeRecordIndexGroupsRecordsLazyGroupBy } =
|
||||
useRecordIndexGroupsRecordsLazyGroupBy({
|
||||
groupByFieldMetadataItem: recordIndexGroupFieldMetadataItem,
|
||||
objectMetadataItem,
|
||||
});
|
||||
|
||||
const recordIndexRecordIdsByGroupCallbackState =
|
||||
useRecoilComponentCallbackState(
|
||||
recordIndexRecordIdsByGroupComponentFamilyState,
|
||||
);
|
||||
|
||||
const recordBoardIsFetchingMoreCallbackState =
|
||||
useRecoilComponentCallbackState(recordBoardIsFetchingMoreComponentState);
|
||||
|
||||
const triggerRecordBoardFetchMore = useRecoilCallback(
|
||||
({ set, snapshot }) =>
|
||||
async () => {
|
||||
const isAlreadyFetchingMore = getSnapshotValue(
|
||||
snapshot,
|
||||
recordBoardIsFetchingMoreCallbackState,
|
||||
);
|
||||
|
||||
const cleanStateBeforeExit = () => {
|
||||
set(recordBoardIsFetchingMoreCallbackState, false);
|
||||
};
|
||||
|
||||
if (isAlreadyFetchingMore) {
|
||||
return;
|
||||
}
|
||||
|
||||
set(recordBoardIsFetchingMoreCallbackState, true);
|
||||
|
||||
const currentOffset = getSnapshotValue(
|
||||
snapshot,
|
||||
recordBoardCurrentGroupByQueryOffsetCallbackState,
|
||||
);
|
||||
|
||||
const newOffset = currentOffset + RECORD_BOARD_QUERY_PAGE_SIZE;
|
||||
|
||||
const recordGroupValuesThatShouldBeFetched = recordGroupDefinitions
|
||||
.filter((recordGroupDefinition) => {
|
||||
return getSnapshotValue(
|
||||
snapshot,
|
||||
recordBoardShouldFetchMoreInColumnFamilyCallbackState(
|
||||
recordGroupDefinition.id,
|
||||
),
|
||||
);
|
||||
})
|
||||
.map((recordGroupDefinition) => recordGroupDefinition.value)
|
||||
.filter(isDefined);
|
||||
|
||||
if (!isNonEmptyArray(recordGroupValuesThatShouldBeFetched)) {
|
||||
cleanStateBeforeExit();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const recordIndexGroupsRecordsGroupByLazyQueryResult =
|
||||
await executeRecordIndexGroupsRecordsLazyGroupBy({
|
||||
variables: {
|
||||
offsetForRecords: newOffset,
|
||||
filter: {
|
||||
...combinedFilters,
|
||||
[recordIndexGroupFieldMetadataItem?.name ?? '']: {
|
||||
in: [...recordGroupValuesThatShouldBeFetched],
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
set(recordBoardCurrentGroupByQueryOffsetCallbackState, newOffset);
|
||||
|
||||
if (!isDefined(recordIndexGroupsRecordsGroupByLazyQueryResult)) {
|
||||
cleanStateBeforeExit();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const queryFieldName =
|
||||
getGroupByQueryResultGqlFieldName(objectMetadataItem);
|
||||
|
||||
const groups =
|
||||
recordIndexGroupsRecordsGroupByLazyQueryResult.data?.[queryFieldName];
|
||||
|
||||
if (!isDefined(groups)) {
|
||||
cleanStateBeforeExit();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const sortedRecordGroupDefinitions = recordGroupDefinitions.toSorted(
|
||||
sortByProperty('position'),
|
||||
);
|
||||
|
||||
for (const recordGroupDefinition of sortedRecordGroupDefinitions) {
|
||||
const foundGroupInResult = groups.find(
|
||||
(recordGroup: any) =>
|
||||
(recordGroup.groupByDimensionValues[0] as string) ===
|
||||
recordGroupDefinition.value,
|
||||
);
|
||||
|
||||
if (!isDefined(foundGroupInResult)) {
|
||||
set(
|
||||
recordBoardShouldFetchMoreInColumnFamilyCallbackState(
|
||||
recordGroupDefinition.id,
|
||||
),
|
||||
false,
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
const newRecords = getRecordsFromRecordConnection({
|
||||
recordConnection: foundGroupInResult,
|
||||
});
|
||||
|
||||
if (!isNonEmptyArray(newRecords)) {
|
||||
set(
|
||||
recordBoardShouldFetchMoreInColumnFamilyCallbackState(
|
||||
recordGroupDefinition.id,
|
||||
),
|
||||
false,
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
const currentRecordIds = getSnapshotValue(
|
||||
snapshot,
|
||||
recordIndexRecordIdsByGroupCallbackState(recordGroupDefinition.id),
|
||||
);
|
||||
|
||||
const newRecordIds = currentRecordIds.concat(
|
||||
newRecords.map((record) => record.id),
|
||||
);
|
||||
|
||||
set(
|
||||
recordIndexRecordIdsByGroupCallbackState(recordGroupDefinition.id),
|
||||
newRecordIds,
|
||||
);
|
||||
|
||||
upsertRecordsInStore(newRecords);
|
||||
|
||||
if (newRecords.length < RECORD_BOARD_QUERY_PAGE_SIZE) {
|
||||
set(
|
||||
recordBoardShouldFetchMoreInColumnFamilyCallbackState(
|
||||
recordGroupDefinition.id,
|
||||
),
|
||||
false,
|
||||
);
|
||||
} else {
|
||||
set(
|
||||
recordBoardShouldFetchMoreInColumnFamilyCallbackState(
|
||||
recordGroupDefinition.id,
|
||||
),
|
||||
true,
|
||||
);
|
||||
}
|
||||
|
||||
await sleep(
|
||||
RECORD_BOARD_FETCH_MORE_THROTTLING_WAIT_TIME_IN_MILLISECONDS_TO_AVOID_REACT_FREEZE,
|
||||
);
|
||||
}
|
||||
|
||||
cleanStateBeforeExit();
|
||||
},
|
||||
[
|
||||
objectMetadataItem,
|
||||
recordGroupDefinitions,
|
||||
upsertRecordsInStore,
|
||||
executeRecordIndexGroupsRecordsLazyGroupBy,
|
||||
recordIndexRecordIdsByGroupCallbackState,
|
||||
recordBoardIsFetchingMoreCallbackState,
|
||||
recordBoardCurrentGroupByQueryOffsetCallbackState,
|
||||
recordBoardShouldFetchMoreInColumnFamilyCallbackState,
|
||||
combinedFilters,
|
||||
recordIndexGroupFieldMetadataItem,
|
||||
],
|
||||
);
|
||||
|
||||
return {
|
||||
triggerRecordBoardFetchMore,
|
||||
};
|
||||
};
|
||||
+183
@@ -0,0 +1,183 @@
|
||||
import { getRecordsFromRecordConnection } from '@/object-record/cache/utils/getRecordsFromRecordConnection';
|
||||
import { RECORD_BOARD_QUERY_PAGE_SIZE } from '@/object-record/record-board/constants/RecordBoardQueryPageSize';
|
||||
import { useSetRecordIdsForColumn } from '@/object-record/record-board/hooks/useSetRecordIdsForColumn';
|
||||
import { lastRecordBoardQueryIdentifierComponentState } from '@/object-record/record-board/states/lastRecordBoardQueryIdentifierComponentState';
|
||||
import { recordBoardCurrentGroupByQueryOffsetComponentState } from '@/object-record/record-board/states/recordBoardCurrentGroupByQueryOffsetComponentState';
|
||||
import { recordBoardShouldFetchMoreInColumnComponentFamilyState } from '@/object-record/record-board/states/recordBoardShouldFetchMoreInColumnComponentFamilyState';
|
||||
import { recordGroupDefinitionsComponentSelector } from '@/object-record/record-group/states/selectors/recordGroupDefinitionsComponentSelector';
|
||||
import { useRecordIndexContextOrThrow } from '@/object-record/record-index/contexts/RecordIndexContext';
|
||||
import { useRecordIndexGroupCommonQueryVariables } from '@/object-record/record-index/hooks/useRecordIndexGroupCommonQueryVariables';
|
||||
import { useRecordIndexGroupsRecordsLazyGroupBy } from '@/object-record/record-index/hooks/useRecordIndexGroupsRecordsLazyGroupBy';
|
||||
import { recordIndexGroupFieldMetadataItemComponentState } from '@/object-record/record-index/states/recordIndexGroupFieldMetadataComponentState';
|
||||
import { recordIndexRecordGroupsAreInInitialLoadingComponentState } from '@/object-record/record-index/states/recordIndexRecordGroupsAreInInitialLoadingComponentState';
|
||||
import { useUpsertRecordsInStore } from '@/object-record/record-store/hooks/useUpsertRecordsInStore';
|
||||
import { getQueryIdentifier } from '@/object-record/utils/getQueryIdentifier';
|
||||
import { getGroupByQueryResultGqlFieldName } from '@/page-layout/utils/getGroupByQueryResultGqlFieldName';
|
||||
import { useScrollWrapperHTMLElement } from '@/ui/utilities/scroll/hooks/useScrollWrapperHTMLElement';
|
||||
import { useRecoilComponentCallbackState } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackState';
|
||||
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
|
||||
import { useSetRecoilComponentState } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentState';
|
||||
import { isNonEmptyArray } from '@sniptt/guards';
|
||||
import { useRecoilCallback } from 'recoil';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
export const useTriggerRecordBoardInitialQuery = () => {
|
||||
const recordGroupDefinitions = useRecoilComponentValue(
|
||||
recordGroupDefinitionsComponentSelector,
|
||||
);
|
||||
|
||||
const { objectMetadataItem } = useRecordIndexContextOrThrow();
|
||||
|
||||
const recordIndexGroupFieldMetadataItem = useRecoilComponentValue(
|
||||
recordIndexGroupFieldMetadataItemComponentState,
|
||||
);
|
||||
|
||||
const setLastRecordBoardQueryIdentifier = useSetRecoilComponentState(
|
||||
lastRecordBoardQueryIdentifierComponentState,
|
||||
);
|
||||
|
||||
const recordBoardShouldFetchMoreInColumnFamilyCallbackState =
|
||||
useRecoilComponentCallbackState(
|
||||
recordBoardShouldFetchMoreInColumnComponentFamilyState,
|
||||
);
|
||||
|
||||
const recordIndexRecordGroupsAreInInitialLoadingCallbackState =
|
||||
useRecoilComponentCallbackState(
|
||||
recordIndexRecordGroupsAreInInitialLoadingComponentState,
|
||||
);
|
||||
|
||||
const setRecordBoardCurrentGroupByQueryOffset = useSetRecoilComponentState(
|
||||
recordBoardCurrentGroupByQueryOffsetComponentState,
|
||||
);
|
||||
|
||||
const { combinedFilters, orderBy } =
|
||||
useRecordIndexGroupCommonQueryVariables();
|
||||
|
||||
const queryIdentifier = getQueryIdentifier({
|
||||
objectNameSingular: objectMetadataItem.nameSingular,
|
||||
filter: combinedFilters,
|
||||
orderBy,
|
||||
});
|
||||
|
||||
const { setRecordIdsForColumn } = useSetRecordIdsForColumn();
|
||||
const { upsertRecordsInStore } = useUpsertRecordsInStore();
|
||||
|
||||
const { scrollWrapperHTMLElement } = useScrollWrapperHTMLElement();
|
||||
|
||||
const { executeRecordIndexGroupsRecordsLazyGroupBy } =
|
||||
useRecordIndexGroupsRecordsLazyGroupBy({
|
||||
groupByFieldMetadataItem: recordIndexGroupFieldMetadataItem,
|
||||
objectMetadataItem,
|
||||
});
|
||||
|
||||
const triggerRecordBoardInitialQuery = useRecoilCallback(
|
||||
({ set }) =>
|
||||
async () => {
|
||||
set(recordIndexRecordGroupsAreInInitialLoadingCallbackState, true);
|
||||
|
||||
const cleanStateBeforeExit = () => {
|
||||
set(recordIndexRecordGroupsAreInInitialLoadingCallbackState, false);
|
||||
|
||||
setLastRecordBoardQueryIdentifier(queryIdentifier);
|
||||
|
||||
setRecordBoardCurrentGroupByQueryOffset(0);
|
||||
|
||||
scrollWrapperHTMLElement?.scrollTo({ top: 0, left: 0 });
|
||||
};
|
||||
|
||||
const recordIndexGroupsRecordsGroupByLazyQueryResult =
|
||||
await executeRecordIndexGroupsRecordsLazyGroupBy();
|
||||
|
||||
if (!isDefined(recordIndexGroupsRecordsGroupByLazyQueryResult)) {
|
||||
cleanStateBeforeExit();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const queryFieldName =
|
||||
getGroupByQueryResultGqlFieldName(objectMetadataItem);
|
||||
|
||||
const groups =
|
||||
recordIndexGroupsRecordsGroupByLazyQueryResult.data?.[queryFieldName];
|
||||
|
||||
if (!isDefined(groups)) {
|
||||
cleanStateBeforeExit();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
for (const recordGroupDefinition of recordGroupDefinitions) {
|
||||
const foundGroupInResult = groups?.find(
|
||||
(recordGroup: any) =>
|
||||
(recordGroup.groupByDimensionValues[0] as string) ===
|
||||
recordGroupDefinition.value,
|
||||
);
|
||||
|
||||
if (!isDefined(foundGroupInResult)) {
|
||||
setRecordIdsForColumn(recordGroupDefinition.id, []);
|
||||
set(
|
||||
recordBoardShouldFetchMoreInColumnFamilyCallbackState(
|
||||
recordGroupDefinition.id,
|
||||
),
|
||||
false,
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
const records = getRecordsFromRecordConnection({
|
||||
recordConnection: foundGroupInResult,
|
||||
});
|
||||
|
||||
if (!isNonEmptyArray(records)) {
|
||||
setRecordIdsForColumn(recordGroupDefinition.id, []);
|
||||
set(
|
||||
recordBoardShouldFetchMoreInColumnFamilyCallbackState(
|
||||
recordGroupDefinition.id,
|
||||
),
|
||||
false,
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
upsertRecordsInStore(records);
|
||||
|
||||
setRecordIdsForColumn(recordGroupDefinition.id, records);
|
||||
|
||||
if (records.length < RECORD_BOARD_QUERY_PAGE_SIZE) {
|
||||
set(
|
||||
recordBoardShouldFetchMoreInColumnFamilyCallbackState(
|
||||
recordGroupDefinition.id,
|
||||
),
|
||||
false,
|
||||
);
|
||||
} else {
|
||||
set(
|
||||
recordBoardShouldFetchMoreInColumnFamilyCallbackState(
|
||||
recordGroupDefinition.id,
|
||||
),
|
||||
true,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
cleanStateBeforeExit();
|
||||
},
|
||||
[
|
||||
recordIndexRecordGroupsAreInInitialLoadingCallbackState,
|
||||
executeRecordIndexGroupsRecordsLazyGroupBy,
|
||||
objectMetadataItem,
|
||||
setLastRecordBoardQueryIdentifier,
|
||||
queryIdentifier,
|
||||
setRecordBoardCurrentGroupByQueryOffset,
|
||||
scrollWrapperHTMLElement,
|
||||
recordGroupDefinitions,
|
||||
upsertRecordsInStore,
|
||||
setRecordIdsForColumn,
|
||||
recordBoardShouldFetchMoreInColumnFamilyCallbackState,
|
||||
],
|
||||
);
|
||||
|
||||
return {
|
||||
triggerRecordBoardInitialQuery,
|
||||
};
|
||||
};
|
||||
+37
-26
@@ -1,6 +1,6 @@
|
||||
import { useTheme } from '@emotion/react';
|
||||
import styled from '@emotion/styled';
|
||||
import Skeleton, { SkeletonTheme } from 'react-loading-skeleton';
|
||||
import { SkeletonTheme } from 'react-loading-skeleton';
|
||||
|
||||
import { SKELETON_LOADER_HEIGHT_SIZES } from '@/activities/components/SkeletonLoader';
|
||||
import { RecordCardBodyContainer } from '@/object-record/record-card/components/RecordCardBodyContainer';
|
||||
@@ -8,17 +8,30 @@ import { RecordCardHeaderContainer } from '@/object-record/record-card/component
|
||||
import { visibleRecordFieldsComponentSelector } from '@/object-record/record-field/states/visibleRecordFieldsComponentSelector';
|
||||
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
|
||||
import { useGetCurrentViewOnly } from '@/views/hooks/useGetCurrentViewOnly';
|
||||
|
||||
const StyledSkeletonIconAndText = styled.div`
|
||||
display: flex;
|
||||
gap: ${({ theme }) => theme.spacing(1)};
|
||||
`;
|
||||
|
||||
const StyledSkeletonTitle = styled.div`
|
||||
padding-left: ${({ theme }) => theme.spacing(2)};
|
||||
padding-left: ${({ theme }) => theme.spacing(1)};
|
||||
`;
|
||||
|
||||
const StyledSeparator = styled.div`
|
||||
height: ${({ theme }) => theme.spacing(2)};
|
||||
const StyledBodyContainer = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: ${({ theme }) => theme.spacing(0.5)};
|
||||
padding-top: 4px;
|
||||
padding-bottom: 4px;
|
||||
`;
|
||||
|
||||
const StyledStaticCellSkeleton = styled.div<{ width: number; height: number }>`
|
||||
background-color: ${({ theme }) => theme.background.tertiary};
|
||||
border-radius: ${({ theme }) => theme.border.radius.sm};
|
||||
|
||||
width: ${({ width }) => width}px;
|
||||
height: ${({ height }) => height}px;
|
||||
`;
|
||||
|
||||
export const RecordBoardColumnCardContainerSkeletonLoader = () => {
|
||||
@@ -32,7 +45,7 @@ export const RecordBoardColumnCardContainerSkeletonLoader = () => {
|
||||
visibleRecordFieldsComponentSelector,
|
||||
);
|
||||
|
||||
const numberOfFields = visibleRecordFields.length;
|
||||
const numberOfFields = visibleRecordFields.length - 1;
|
||||
|
||||
const skeletonItems = Array.from({ length: numberOfFields }).map(
|
||||
(_, index) => ({
|
||||
@@ -46,32 +59,30 @@ export const RecordBoardColumnCardContainerSkeletonLoader = () => {
|
||||
<SkeletonTheme
|
||||
baseColor={theme.background.tertiary}
|
||||
highlightColor={theme.background.transparent.lighter}
|
||||
borderRadius={4}
|
||||
borderRadius={2}
|
||||
>
|
||||
<RecordCardHeaderContainer isCompact={isCompactModeActive}>
|
||||
<StyledSkeletonTitle>
|
||||
<Skeleton
|
||||
width={titleSkeletonWidth}
|
||||
height={SKELETON_LOADER_HEIGHT_SIZES.standard.s}
|
||||
/>
|
||||
<StyledStaticCellSkeleton width={titleSkeletonWidth} height={12} />
|
||||
</StyledSkeletonTitle>
|
||||
</RecordCardHeaderContainer>
|
||||
<StyledSeparator />
|
||||
{!isCompactModeActive &&
|
||||
skeletonItems.map(({ id }) => (
|
||||
<RecordCardBodyContainer key={id}>
|
||||
<StyledSkeletonIconAndText>
|
||||
<Skeleton
|
||||
width={16}
|
||||
height={SKELETON_LOADER_HEIGHT_SIZES.standard.s}
|
||||
/>
|
||||
<Skeleton
|
||||
width={151}
|
||||
height={SKELETON_LOADER_HEIGHT_SIZES.standard.s}
|
||||
/>
|
||||
</StyledSkeletonIconAndText>
|
||||
</RecordCardBodyContainer>
|
||||
))}
|
||||
<StyledBodyContainer>
|
||||
{!isCompactModeActive &&
|
||||
skeletonItems.map(({ id }) => (
|
||||
<RecordCardBodyContainer key={id}>
|
||||
<StyledSkeletonIconAndText>
|
||||
<StyledStaticCellSkeleton
|
||||
width={16}
|
||||
height={SKELETON_LOADER_HEIGHT_SIZES.standard.s}
|
||||
/>
|
||||
<StyledStaticCellSkeleton
|
||||
width={151}
|
||||
height={SKELETON_LOADER_HEIGHT_SIZES.standard.s}
|
||||
/>
|
||||
</StyledSkeletonIconAndText>
|
||||
</RecordCardBodyContainer>
|
||||
))}
|
||||
</StyledBodyContainer>
|
||||
</SkeletonTheme>
|
||||
);
|
||||
};
|
||||
|
||||
+27
-43
@@ -1,17 +1,18 @@
|
||||
import styled from '@emotion/styled';
|
||||
import { Draggable, type DroppableProvided } from '@hello-pangea/dnd';
|
||||
import { useContext } from 'react';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
|
||||
import { RecordBoardCardDraggableContainer } from '@/object-record/record-board/record-board-card/components/RecordBoardCardDraggableContainer';
|
||||
import { RecordBoardColumnCardContainerSkeletonLoader } from '@/object-record/record-board/record-board-column/components/RecordBoardColumnCardContainerSkeletonLoader';
|
||||
import { RecordBoardColumnFetchMoreLoader } from '@/object-record/record-board/record-board-column/components/RecordBoardColumnFetchMoreLoader';
|
||||
|
||||
import { RecordBoardColumnNewRecordButton } from '@/object-record/record-board/record-board-column/components/RecordBoardColumnNewRecordButton';
|
||||
import { RecordBoardColumnContext } from '@/object-record/record-board/record-board-column/contexts/RecordBoardColumnContext';
|
||||
import { getNumberOfCardsPerColumnForSkeletonLoading } from '@/object-record/record-board/record-board-column/utils/getNumberOfCardsPerColumnForSkeletonLoading';
|
||||
|
||||
import { RecordBoardColumnLoadingSkeletonCards } from '@/object-record/record-board/record-board-column/components/RecordBoardColumnLoadingSkeletonCards';
|
||||
import { recordBoardShouldFetchMoreInColumnComponentFamilyState } from '@/object-record/record-board/states/recordBoardShouldFetchMoreInColumnComponentFamilyState';
|
||||
import { recordIndexRecordGroupsAreInInitialLoadingComponentState } from '@/object-record/record-index/states/recordIndexRecordGroupsAreInInitialLoadingComponentState';
|
||||
import { recordIndexRecordIdsByGroupComponentFamilyState } from '@/object-record/record-index/states/recordIndexRecordIdsByGroupComponentFamilyState';
|
||||
import { isRecordIndexBoardColumnLoadingFamilyState } from '@/object-record/states/isRecordBoardColumnLoadingFamilyState';
|
||||
import { useRecoilComponentFamilyValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentFamilyValue';
|
||||
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
|
||||
|
||||
const StyledColumnCardsContainer = styled.div`
|
||||
display: flex;
|
||||
@@ -22,17 +23,6 @@ const StyledColumnCardsContainer = styled.div`
|
||||
const StyledNewButtonContainer = styled.div`
|
||||
padding-bottom: ${({ theme }) => theme.spacing(4)};
|
||||
`;
|
||||
// eslint-disable-next-line @nx/workspace-no-hardcoded-colors
|
||||
const StyledSkeletonCardContainer = styled.div`
|
||||
background-color: ${({ theme }) => theme.background.secondary};
|
||||
border: 1px solid ${({ theme }) => theme.background.quaternary};
|
||||
border-radius: ${({ theme }) => theme.border.radius.md};
|
||||
box-shadow:
|
||||
0px 4px 8px 0px rgba(0, 0, 0, 0.08),
|
||||
0px 0px 4px 0px rgba(0, 0, 0, 0.08);
|
||||
color: ${({ theme }) => theme.font.color.primary};
|
||||
margin-bottom: ${({ theme }) => theme.spacing(2)};
|
||||
`;
|
||||
|
||||
type RecordBoardColumnCardsContainerProps = {
|
||||
recordBoardColumnId: string;
|
||||
@@ -45,15 +35,18 @@ export const RecordBoardColumnCardsContainer = ({
|
||||
}: RecordBoardColumnCardsContainerProps) => {
|
||||
const { columnDefinition } = useContext(RecordBoardColumnContext);
|
||||
|
||||
const columnId = columnDefinition.id;
|
||||
|
||||
const recordIds = useRecoilComponentFamilyValue(
|
||||
recordIndexRecordIdsByGroupComponentFamilyState,
|
||||
recordBoardColumnId,
|
||||
);
|
||||
|
||||
const isRecordIndexBoardColumnLoading = useRecoilValue(
|
||||
isRecordIndexBoardColumnLoadingFamilyState(columnId),
|
||||
const recordIndexRecordGroupsAreInInitialLoading = useRecoilComponentValue(
|
||||
recordIndexRecordGroupsAreInInitialLoadingComponentState,
|
||||
);
|
||||
|
||||
const recordBoardShouldFetchMoreInColumn = useRecoilComponentFamilyValue(
|
||||
recordBoardShouldFetchMoreInColumnComponentFamilyState,
|
||||
recordBoardColumnId,
|
||||
);
|
||||
|
||||
return (
|
||||
@@ -62,29 +55,20 @@ export const RecordBoardColumnCardsContainer = ({
|
||||
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||
{...droppableProvided?.droppableProps}
|
||||
>
|
||||
{isRecordIndexBoardColumnLoading
|
||||
? Array.from(
|
||||
{
|
||||
length: getNumberOfCardsPerColumnForSkeletonLoading(
|
||||
columnDefinition.position,
|
||||
),
|
||||
},
|
||||
(_, index) => (
|
||||
<StyledSkeletonCardContainer
|
||||
key={`${columnDefinition.id}-${index}`}
|
||||
>
|
||||
<RecordBoardColumnCardContainerSkeletonLoader />
|
||||
</StyledSkeletonCardContainer>
|
||||
),
|
||||
)
|
||||
: recordIds.map((recordId, index) => (
|
||||
<RecordBoardCardDraggableContainer
|
||||
key={recordId}
|
||||
recordId={recordId}
|
||||
rowIndex={index}
|
||||
/>
|
||||
))}
|
||||
<RecordBoardColumnFetchMoreLoader />
|
||||
{recordIndexRecordGroupsAreInInitialLoading ? (
|
||||
<RecordBoardColumnLoadingSkeletonCards />
|
||||
) : (
|
||||
recordIds.map((recordId, index) => (
|
||||
<RecordBoardCardDraggableContainer
|
||||
key={recordId}
|
||||
recordId={recordId}
|
||||
rowIndex={index}
|
||||
/>
|
||||
))
|
||||
)}
|
||||
{recordBoardShouldFetchMoreInColumn ? (
|
||||
<RecordBoardColumnLoadingSkeletonCards />
|
||||
) : null}
|
||||
<Draggable
|
||||
draggableId={`new-${columnDefinition.id}-bottom`}
|
||||
index={recordIds.length}
|
||||
|
||||
-44
@@ -1,44 +0,0 @@
|
||||
import styled from '@emotion/styled';
|
||||
import { useContext, useEffect } from 'react';
|
||||
import { useInView } from 'react-intersection-observer';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
|
||||
import { RecordBoardColumnContext } from '@/object-record/record-board/record-board-column/contexts/RecordBoardColumnContext';
|
||||
import { isRecordBoardFetchingRecordsByColumnFamilyState } from '@/object-record/record-board/states/isRecordBoardFetchingRecordsByColumnFamilyState';
|
||||
import { recordBoardShouldFetchMoreInColumnComponentFamilyState } from '@/object-record/record-board/states/recordBoardShouldFetchMoreInColumnComponentFamilyState';
|
||||
import { useSetRecoilComponentFamilyState } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentFamilyState';
|
||||
|
||||
const StyledText = styled.div`
|
||||
align-items: center;
|
||||
box-shadow: none;
|
||||
color: ${({ theme }) => theme.grayScale.gray9};
|
||||
display: flex;
|
||||
height: 32px;
|
||||
margin-left: ${({ theme }) => theme.spacing(8)};
|
||||
padding-left: ${({ theme }) => theme.spacing(2)};
|
||||
`;
|
||||
|
||||
export const RecordBoardColumnFetchMoreLoader = () => {
|
||||
const { columnDefinition } = useContext(RecordBoardColumnContext);
|
||||
|
||||
const isFetchingRecord = useRecoilValue(
|
||||
isRecordBoardFetchingRecordsByColumnFamilyState(columnDefinition.id),
|
||||
);
|
||||
|
||||
const setShouldFetchMore = useSetRecoilComponentFamilyState(
|
||||
recordBoardShouldFetchMoreInColumnComponentFamilyState,
|
||||
columnDefinition.id,
|
||||
);
|
||||
|
||||
const { ref, inView } = useInView();
|
||||
|
||||
useEffect(() => {
|
||||
setShouldFetchMore(inView);
|
||||
}, [setShouldFetchMore, inView]);
|
||||
|
||||
return (
|
||||
<div ref={ref}>
|
||||
{isFetchingRecord && <StyledText>Loading more...</StyledText>}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
+60
-9
@@ -5,16 +5,25 @@ import { useObjectPermissionsForObject } from '@/object-record/hooks/useObjectPe
|
||||
import { RecordBoardContext } from '@/object-record/record-board/contexts/RecordBoardContext';
|
||||
import { RecordBoardColumnDropdownMenu } from '@/object-record/record-board/record-board-column/components/RecordBoardColumnDropdownMenu';
|
||||
import { RecordBoardColumnHeaderAggregateDropdown } from '@/object-record/record-board/record-board-column/components/RecordBoardColumnHeaderAggregateDropdown';
|
||||
|
||||
import { RECORD_BOARD_COLUMN_WIDTH } from '@/object-record/record-board/constants/RecordBoardColumnWidth';
|
||||
import { RecordBoardColumnContext } from '@/object-record/record-board/record-board-column/contexts/RecordBoardColumnContext';
|
||||
import { hasAnySoftDeleteFilterOnViewComponentSelector } from '@/object-record/record-filter/states/hasAnySoftDeleteFilterOnView';
|
||||
import { recordGroupDefinitionsComponentSelector } from '@/object-record/record-group/states/selectors/recordGroupDefinitionsComponentSelector';
|
||||
import { RecordGroupDefinitionType } from '@/object-record/record-group/types/RecordGroupDefinition';
|
||||
import { recordIndexAggregateDisplayLabelComponentState } from '@/object-record/record-index/states/recordIndexAggregateDisplayLabelComponentState';
|
||||
import { recordIndexAggregateDisplayValueForGroupValueComponentFamilyState } from '@/object-record/record-index/states/recordIndexAggregateDisplayValueForGroupValueComponentFamilyState';
|
||||
import { recordIndexRecordIdsByGroupComponentFamilyState } from '@/object-record/record-index/states/recordIndexRecordIdsByGroupComponentFamilyState';
|
||||
import { useUpsertRecordsInStore } from '@/object-record/record-store/hooks/useUpsertRecordsInStore';
|
||||
import { useCreateNewIndexRecord } from '@/object-record/record-table/hooks/useCreateNewIndexRecord';
|
||||
import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown';
|
||||
import { useToggleDropdown } from '@/ui/layout/dropdown/hooks/useToggleDropdown';
|
||||
import { useRecoilComponentCallbackState } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackState';
|
||||
import { useRecoilComponentFamilyValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentFamilyValue';
|
||||
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
|
||||
import { getSnapshotValue } from '@/ui/utilities/state/utils/getSnapshotValue';
|
||||
import { useRecoilCallback } from 'recoil';
|
||||
import { findByProperty, isDefined } from 'twenty-shared/utils';
|
||||
import { Tag } from 'twenty-ui/components';
|
||||
import { IconDotsVertical, IconPlus } from 'twenty-ui/display';
|
||||
import { LightIconButton } from 'twenty-ui/input';
|
||||
@@ -56,8 +65,8 @@ const StyledColumn = styled.div`
|
||||
background-color: ${({ theme }) => theme.background.primary};
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
max-width: 200px;
|
||||
min-width: 200px;
|
||||
max-width: ${RECORD_BOARD_COLUMN_WIDTH}px;
|
||||
min-width: ${RECORD_BOARD_COLUMN_WIDTH}px;
|
||||
|
||||
padding: ${({ theme }) => theme.spacing(2)};
|
||||
|
||||
@@ -95,15 +104,63 @@ export const RecordBoardColumnHeader = () => {
|
||||
recordIndexAggregateDisplayValueForGroupValueComponentFamilyState,
|
||||
{ groupValue: columnDefinition?.value ?? '' },
|
||||
);
|
||||
const { upsertRecordsInStore } = useUpsertRecordsInStore();
|
||||
|
||||
const recordIndexRecordIdsByGroupCallbackState =
|
||||
useRecoilComponentCallbackState(
|
||||
recordIndexRecordIdsByGroupComponentFamilyState,
|
||||
);
|
||||
|
||||
const recordIndexAggregateDisplayLabel = useRecoilComponentValue(
|
||||
recordIndexAggregateDisplayLabelComponentState,
|
||||
);
|
||||
|
||||
const recordGroupDefinitions = useRecoilComponentValue(
|
||||
recordGroupDefinitionsComponentSelector,
|
||||
);
|
||||
|
||||
const { toggleDropdown } = useToggleDropdown();
|
||||
|
||||
const dropdownId = `record-board-column-dropdown-${columnDefinition.id}`;
|
||||
|
||||
const handleCreateNewRecordClick = useRecoilCallback(
|
||||
({ set, snapshot }) =>
|
||||
async () => {
|
||||
const createdRecord = await createNewIndexRecord({
|
||||
position: 'first',
|
||||
[selectFieldMetadataItem.name]: columnDefinition.value,
|
||||
});
|
||||
|
||||
const recordGroup = recordGroupDefinitions.find(
|
||||
findByProperty('value', createdRecord[selectFieldMetadataItem.name]),
|
||||
);
|
||||
|
||||
if (isDefined(recordGroup)) {
|
||||
const currentRecordIds = getSnapshotValue(
|
||||
snapshot,
|
||||
recordIndexRecordIdsByGroupCallbackState(recordGroup.id),
|
||||
);
|
||||
|
||||
const newRecordIds = [createdRecord.id, ...currentRecordIds];
|
||||
|
||||
set(
|
||||
recordIndexRecordIdsByGroupCallbackState(recordGroup.id),
|
||||
newRecordIds,
|
||||
);
|
||||
}
|
||||
|
||||
upsertRecordsInStore([createdRecord]);
|
||||
},
|
||||
[
|
||||
createNewIndexRecord,
|
||||
columnDefinition,
|
||||
recordGroupDefinitions,
|
||||
recordIndexRecordIdsByGroupCallbackState,
|
||||
upsertRecordsInStore,
|
||||
selectFieldMetadataItem,
|
||||
],
|
||||
);
|
||||
|
||||
return (
|
||||
<StyledColumn>
|
||||
<StyledHeader
|
||||
@@ -166,13 +223,7 @@ export const RecordBoardColumnHeader = () => {
|
||||
<LightIconButton
|
||||
accent="tertiary"
|
||||
Icon={IconPlus}
|
||||
onClick={() => {
|
||||
createNewIndexRecord({
|
||||
position: 'first',
|
||||
[selectFieldMetadataItem.name]:
|
||||
columnDefinition.value,
|
||||
});
|
||||
}}
|
||||
onClick={handleCreateNewRecordClick}
|
||||
/>
|
||||
)}
|
||||
</StyledHeaderActions>
|
||||
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
import { RECORD_BOARD_QUERY_PAGE_SIZE } from '@/object-record/record-board/constants/RecordBoardQueryPageSize';
|
||||
import { RecordBoardColumnCardContainerSkeletonLoader } from '@/object-record/record-board/record-board-column/components/RecordBoardColumnCardContainerSkeletonLoader';
|
||||
import styled from '@emotion/styled';
|
||||
|
||||
const StyledSkeletonCardContainer = styled.div`
|
||||
background-color: ${({ theme }) => theme.background.secondary};
|
||||
border: 1px solid ${({ theme }) => theme.background.quaternary};
|
||||
border-radius: ${({ theme }) => theme.border.radius.sm};
|
||||
box-shadow:
|
||||
0px 4px 8px 0px ${({ theme }) => theme.color.gray2},
|
||||
0px 0px 4px 0px ${({ theme }) => theme.color.gray2};
|
||||
color: ${({ theme }) => theme.font.color.primary};
|
||||
margin-bottom: ${({ theme }) => theme.spacing(2)};
|
||||
`;
|
||||
|
||||
export const RecordBoardColumnLoadingSkeletonCards = () => {
|
||||
return Array.from(
|
||||
{
|
||||
length: RECORD_BOARD_QUERY_PAGE_SIZE,
|
||||
},
|
||||
(_, index) => (
|
||||
<StyledSkeletonCardContainer key={`${index}`}>
|
||||
<RecordBoardColumnCardContainerSkeletonLoader />
|
||||
</StyledSkeletonCardContainer>
|
||||
),
|
||||
);
|
||||
};
|
||||
-13
@@ -1,13 +0,0 @@
|
||||
export const getNumberOfCardsPerColumnForSkeletonLoading = (
|
||||
columnIndex: number,
|
||||
): number => {
|
||||
const skeletonCounts: Record<number, number> = {
|
||||
0: 2,
|
||||
1: 1,
|
||||
2: 3,
|
||||
3: 0,
|
||||
4: 1,
|
||||
};
|
||||
|
||||
return skeletonCounts[columnIndex] || 0;
|
||||
};
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
import { RecordBoardComponentInstanceContext } from '@/object-record/record-board/states/contexts/RecordBoardComponentInstanceContext';
|
||||
import { createComponentState } from '@/ui/utilities/state/component-state/utils/createComponentState';
|
||||
|
||||
export const lastRecordBoardQueryIdentifierComponentState =
|
||||
createComponentState<string>({
|
||||
key: 'lastRecordBoardQueryIdentifierComponentState',
|
||||
componentInstanceContext: RecordBoardComponentInstanceContext,
|
||||
defaultValue: '',
|
||||
});
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
import { RecordBoardComponentInstanceContext } from '@/object-record/record-board/states/contexts/RecordBoardComponentInstanceContext';
|
||||
import { createComponentState } from '@/ui/utilities/state/component-state/utils/createComponentState';
|
||||
|
||||
export const recordBoardCurrentGroupByQueryOffsetComponentState =
|
||||
createComponentState<number>({
|
||||
key: 'recordBoardCurrentGroupByQueryOffsetComponentState',
|
||||
defaultValue: 0,
|
||||
componentInstanceContext: RecordBoardComponentInstanceContext,
|
||||
});
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
import { RecordBoardComponentInstanceContext } from '@/object-record/record-board/states/contexts/RecordBoardComponentInstanceContext';
|
||||
import { createComponentState } from '@/ui/utilities/state/component-state/utils/createComponentState';
|
||||
|
||||
export const recordBoardIsFetchingMoreComponentState =
|
||||
createComponentState<boolean>({
|
||||
key: 'recordBoardIsFetchingMoreComponentState',
|
||||
defaultValue: false,
|
||||
componentInstanceContext: RecordBoardComponentInstanceContext,
|
||||
});
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
import { RecordBoardComponentInstanceContext } from '@/object-record/record-board/states/contexts/RecordBoardComponentInstanceContext';
|
||||
import { createComponentState } from '@/ui/utilities/state/component-state/utils/createComponentState';
|
||||
|
||||
export const recordBoardShouldFetchMoreComponentState =
|
||||
createComponentState<boolean>({
|
||||
key: 'recordBoardShouldFetchMoreComponentState',
|
||||
defaultValue: false,
|
||||
componentInstanceContext: RecordBoardComponentInstanceContext,
|
||||
});
|
||||
+16
-16
@@ -1,22 +1,24 @@
|
||||
import { type DropResult } from '@hello-pangea/dnd';
|
||||
import { useContext } from 'react';
|
||||
import { useRecoilCallback } from 'recoil';
|
||||
|
||||
import { RecordBoardContext } from '@/object-record/record-board/contexts/RecordBoardContext';
|
||||
|
||||
import { processGroupDrop } from '@/object-record/record-drag/utils/processGroupDrop';
|
||||
|
||||
import { RecordBoardContext } from '@/object-record/record-board/contexts/RecordBoardContext';
|
||||
import { useUpdateDroppedRecordOnBoard } from '@/object-record/record-drag/hooks/useUpdateDroppedRecordOnBoard';
|
||||
import { recordIndexRecordIdsByGroupComponentFamilyState } from '@/object-record/record-index/states/recordIndexRecordIdsByGroupComponentFamilyState';
|
||||
import { useRecoilComponentCallbackState } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackState';
|
||||
import { useContext } from 'react';
|
||||
|
||||
export const useProcessBoardCardDrop = () => {
|
||||
const { updateOneRecord, selectFieldMetadataItem } =
|
||||
useContext(RecordBoardContext);
|
||||
const { selectFieldMetadataItem } = useContext(RecordBoardContext);
|
||||
|
||||
const recordIndexRecordIdsByGroupFamilyState =
|
||||
const recordIndexRecordIdsByGroupCallbackFamilyState =
|
||||
useRecoilComponentCallbackState(
|
||||
recordIndexRecordIdsByGroupComponentFamilyState,
|
||||
);
|
||||
|
||||
const { updateDroppedRecordOnBoard } = useUpdateDroppedRecordOnBoard();
|
||||
|
||||
const processBoardCardDrop = useRecoilCallback(
|
||||
({ snapshot }) =>
|
||||
(boardCardDropResult: DropResult, selectedRecordIds: string[]) => {
|
||||
@@ -26,22 +28,20 @@ export const useProcessBoardCardDrop = () => {
|
||||
groupDropResult: boardCardDropResult,
|
||||
snapshot,
|
||||
selectedRecordIds,
|
||||
recordIdsByGroupFamilyState: recordIndexRecordIdsByGroupFamilyState,
|
||||
recordIdsByGroupFamilyState:
|
||||
recordIndexRecordIdsByGroupCallbackFamilyState,
|
||||
onUpdateRecord: ({ recordId, position }, targetRecordGroupValue) => {
|
||||
updateOneRecord({
|
||||
idToUpdate: recordId,
|
||||
updateOneRecordInput: {
|
||||
[selectFieldMetadataItem.name]: targetRecordGroupValue,
|
||||
position,
|
||||
},
|
||||
});
|
||||
updateDroppedRecordOnBoard(
|
||||
{ recordId, position },
|
||||
targetRecordGroupValue,
|
||||
);
|
||||
},
|
||||
});
|
||||
},
|
||||
[
|
||||
updateOneRecord,
|
||||
selectFieldMetadataItem,
|
||||
recordIndexRecordIdsByGroupFamilyState,
|
||||
recordIndexRecordIdsByGroupCallbackFamilyState,
|
||||
updateDroppedRecordOnBoard,
|
||||
],
|
||||
);
|
||||
|
||||
|
||||
+1
@@ -91,6 +91,7 @@ export const useProcessCalendarCardDrop = () => {
|
||||
arrayOfRecordsWithPosition: recordsWithPosition,
|
||||
idOfItemToMove: recordId,
|
||||
idOfTargetItem: targetRecord.id,
|
||||
isDroppedAfterList,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
+6
@@ -72,6 +72,10 @@ export const useProcessTableWithoutGroupRecordDrop = () => {
|
||||
selectedRowIdsSelector,
|
||||
);
|
||||
|
||||
const isDroppedAfterList =
|
||||
tableRecordDropResult.destination.index + 1 >=
|
||||
allSparseRecordIds.length;
|
||||
|
||||
const recordsWithPosition: RecordWithPosition[] = allSparseRecordIds
|
||||
.filter(isDefined)
|
||||
.map((recordId) => ({
|
||||
@@ -104,6 +108,7 @@ export const useProcessTableWithoutGroupRecordDrop = () => {
|
||||
sourceRecordId: draggedRecordId,
|
||||
targetRecordId: targetRecordId ?? '',
|
||||
recordsWithPosition: contiguousRecordsWithPosition,
|
||||
isDroppedAfterList,
|
||||
});
|
||||
|
||||
if (!isDefined(singleDragResult.position)) {
|
||||
@@ -137,6 +142,7 @@ export const useProcessTableWithoutGroupRecordDrop = () => {
|
||||
targetRecordId: targetRecordId ?? '',
|
||||
selectedRecordIds: originalDragSelection,
|
||||
recordsWithPosition: contiguousRecordsWithPosition,
|
||||
isDroppedAfterList,
|
||||
});
|
||||
|
||||
for (const update of multiDragResult.recordUpdates) {
|
||||
|
||||
+170
@@ -0,0 +1,170 @@
|
||||
import { RecordBoardContext } from '@/object-record/record-board/contexts/RecordBoardContext';
|
||||
import { extractRecordPositions } from '@/object-record/record-drag/utils/extractRecordPositions';
|
||||
import { recordGroupDefinitionsComponentSelector } from '@/object-record/record-group/states/selectors/recordGroupDefinitionsComponentSelector';
|
||||
import { type RecordGroupDefinition } from '@/object-record/record-group/types/RecordGroupDefinition';
|
||||
import { recordIndexRecordIdsByGroupComponentFamilyState } from '@/object-record/record-index/states/recordIndexRecordIdsByGroupComponentFamilyState';
|
||||
import { useUpsertRecordsInStore } from '@/object-record/record-store/hooks/useUpsertRecordsInStore';
|
||||
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
|
||||
import { useRecoilComponentCallbackState } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackState';
|
||||
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
|
||||
import { getSnapshotValue } from '@/ui/utilities/state/utils/getSnapshotValue';
|
||||
import { useContext } from 'react';
|
||||
import { useRecoilCallback } from 'recoil';
|
||||
import { findByProperty, isDefined } from 'twenty-shared/utils';
|
||||
import { sortByProperty } from '~/utils/array/sortByProperty';
|
||||
|
||||
export const useUpdateDroppedRecordOnBoard = () => {
|
||||
const { updateOneRecord, selectFieldMetadataItem } =
|
||||
useContext(RecordBoardContext);
|
||||
|
||||
const recordIndexRecordIdsByGroupCallbackFamilyState =
|
||||
useRecoilComponentCallbackState(
|
||||
recordIndexRecordIdsByGroupComponentFamilyState,
|
||||
);
|
||||
|
||||
const recordGroupDefinitions = useRecoilComponentValue(
|
||||
recordGroupDefinitionsComponentSelector,
|
||||
);
|
||||
|
||||
const { upsertRecordsInStore } = useUpsertRecordsInStore();
|
||||
|
||||
const updateDroppedRecordOnBoard = useRecoilCallback(
|
||||
({ snapshot, set }) =>
|
||||
(
|
||||
{
|
||||
recordId,
|
||||
position: newPosition,
|
||||
}: { recordId: string; position?: number },
|
||||
targetRecordGroupValue: RecordGroupDefinition['value'],
|
||||
) => {
|
||||
const initialRecord = getSnapshotValue(
|
||||
snapshot,
|
||||
recordStoreFamilyState(recordId),
|
||||
);
|
||||
|
||||
if (!isDefined(newPosition)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isDefined(initialRecord)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const initialRecordGroupValue =
|
||||
initialRecord[selectFieldMetadataItem.name];
|
||||
|
||||
const initialRecordGroup = recordGroupDefinitions.find(
|
||||
findByProperty('value', initialRecordGroupValue),
|
||||
);
|
||||
|
||||
if (!isDefined(initialRecordGroup)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const targetRecordGroup = recordGroupDefinitions.find(
|
||||
findByProperty('value', targetRecordGroupValue),
|
||||
);
|
||||
|
||||
if (!isDefined(targetRecordGroup)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const initialRecordGroupId = initialRecordGroup.id;
|
||||
const targetRecordGroupId = targetRecordGroup.id;
|
||||
|
||||
const movingInsideSameRecordGroup =
|
||||
initialRecordGroupId === targetRecordGroupId;
|
||||
|
||||
const isSamePosition = initialRecord.position === newPosition;
|
||||
|
||||
if (movingInsideSameRecordGroup && isSamePosition) {
|
||||
return;
|
||||
}
|
||||
|
||||
const currentRecordIdsInInitialRecordGroup = getSnapshotValue(
|
||||
snapshot,
|
||||
recordIndexRecordIdsByGroupCallbackFamilyState(initialRecordGroupId),
|
||||
);
|
||||
|
||||
const positionOfDroppedRecordInInitialRecordIds =
|
||||
currentRecordIdsInInitialRecordGroup.findIndex(
|
||||
(id) => id === recordId,
|
||||
);
|
||||
|
||||
let currentRecordIdsInTargetRecordGroup = getSnapshotValue(
|
||||
snapshot,
|
||||
recordIndexRecordIdsByGroupCallbackFamilyState(targetRecordGroupId),
|
||||
);
|
||||
|
||||
if (positionOfDroppedRecordInInitialRecordIds === -1) {
|
||||
throw new Error(
|
||||
`Cannot find record id in initial record group ids on drop, this should not happen`,
|
||||
);
|
||||
}
|
||||
|
||||
const newInitialGroupRecordIds =
|
||||
currentRecordIdsInInitialRecordGroup.toSpliced(
|
||||
positionOfDroppedRecordInInitialRecordIds,
|
||||
1,
|
||||
);
|
||||
|
||||
if (movingInsideSameRecordGroup) {
|
||||
currentRecordIdsInTargetRecordGroup = newInitialGroupRecordIds;
|
||||
} else {
|
||||
set(
|
||||
recordIndexRecordIdsByGroupCallbackFamilyState(
|
||||
initialRecordGroupId,
|
||||
),
|
||||
newInitialGroupRecordIds,
|
||||
);
|
||||
}
|
||||
|
||||
const targetGroupRecordsWithIds = extractRecordPositions(
|
||||
currentRecordIdsInTargetRecordGroup,
|
||||
snapshot,
|
||||
);
|
||||
|
||||
const newTargetRecordGroupWithIds = [
|
||||
...targetGroupRecordsWithIds,
|
||||
{
|
||||
id: recordId,
|
||||
position: newPosition,
|
||||
},
|
||||
];
|
||||
|
||||
newTargetRecordGroupWithIds.sort(sortByProperty('position', 'asc'));
|
||||
|
||||
set(
|
||||
recordIndexRecordIdsByGroupCallbackFamilyState(targetRecordGroupId),
|
||||
newTargetRecordGroupWithIds.map((record) => record.id),
|
||||
);
|
||||
|
||||
upsertRecordsInStore([
|
||||
{
|
||||
...initialRecord,
|
||||
[selectFieldMetadataItem.name]: targetRecordGroupValue,
|
||||
position: newPosition,
|
||||
},
|
||||
]);
|
||||
|
||||
updateOneRecord({
|
||||
idToUpdate: initialRecord.id,
|
||||
updateOneRecordInput: {
|
||||
[selectFieldMetadataItem.name]: targetRecordGroupValue,
|
||||
position: newPosition,
|
||||
},
|
||||
});
|
||||
},
|
||||
[
|
||||
recordGroupDefinitions,
|
||||
recordIndexRecordIdsByGroupCallbackFamilyState,
|
||||
selectFieldMetadataItem,
|
||||
upsertRecordsInStore,
|
||||
updateOneRecord,
|
||||
],
|
||||
);
|
||||
|
||||
return {
|
||||
updateDroppedRecordOnBoard,
|
||||
};
|
||||
};
|
||||
+4
@@ -17,6 +17,7 @@ describe('processMultiDrag', () => {
|
||||
targetRecordId: 'record-4',
|
||||
selectedRecordIds,
|
||||
recordsWithPosition,
|
||||
isDroppedAfterList: true,
|
||||
});
|
||||
|
||||
expect(dragResult.recordUpdates).toEqual([
|
||||
@@ -35,6 +36,7 @@ describe('processMultiDrag', () => {
|
||||
targetRecordId: '',
|
||||
selectedRecordIds,
|
||||
recordsWithPosition,
|
||||
isDroppedAfterList: false,
|
||||
});
|
||||
}).toThrow('Cannot find item to move for id : ');
|
||||
});
|
||||
@@ -47,6 +49,7 @@ describe('processMultiDrag', () => {
|
||||
targetRecordId: 'record-3',
|
||||
selectedRecordIds,
|
||||
recordsWithPosition,
|
||||
isDroppedAfterList: false,
|
||||
});
|
||||
|
||||
expect(dragResult.recordUpdates).toEqual([
|
||||
@@ -62,6 +65,7 @@ describe('processMultiDrag', () => {
|
||||
targetRecordId: 'record-3',
|
||||
selectedRecordIds,
|
||||
recordsWithPosition,
|
||||
isDroppedAfterList: false,
|
||||
});
|
||||
|
||||
expect(dragResult.recordUpdates).toEqual([]);
|
||||
|
||||
+6
@@ -13,6 +13,7 @@ describe('processSingleDrag', () => {
|
||||
sourceRecordId: 'record-1',
|
||||
targetRecordId: 'record-3',
|
||||
recordsWithPosition: mockRecordPositionData,
|
||||
isDroppedAfterList: true,
|
||||
});
|
||||
|
||||
expect(dragResult).toEqual({
|
||||
@@ -26,6 +27,7 @@ describe('processSingleDrag', () => {
|
||||
sourceRecordId: 'record-3',
|
||||
targetRecordId: 'record-1',
|
||||
recordsWithPosition: mockRecordPositionData,
|
||||
isDroppedAfterList: false,
|
||||
});
|
||||
|
||||
expect(dragResult).toEqual({
|
||||
@@ -39,6 +41,7 @@ describe('processSingleDrag', () => {
|
||||
sourceRecordId: 'record-1',
|
||||
targetRecordId: 'record-2',
|
||||
recordsWithPosition: mockRecordPositionData,
|
||||
isDroppedAfterList: false,
|
||||
});
|
||||
|
||||
expect(dragResult).toEqual({
|
||||
@@ -52,6 +55,7 @@ describe('processSingleDrag', () => {
|
||||
sourceRecordId: 'record-3',
|
||||
targetRecordId: 'record-2',
|
||||
recordsWithPosition: mockRecordPositionData,
|
||||
isDroppedAfterList: false,
|
||||
});
|
||||
|
||||
expect(dragResult).toEqual({
|
||||
@@ -65,6 +69,7 @@ describe('processSingleDrag', () => {
|
||||
sourceRecordId: 'record-4',
|
||||
targetRecordId: 'record-2',
|
||||
recordsWithPosition: mockRecordPositionData,
|
||||
isDroppedAfterList: false,
|
||||
});
|
||||
|
||||
expect(dragResult).toEqual({
|
||||
@@ -79,6 +84,7 @@ describe('processSingleDrag', () => {
|
||||
sourceRecordId: 'record-1',
|
||||
targetRecordId: 'record-1',
|
||||
recordsWithPosition: [],
|
||||
isDroppedAfterList: false,
|
||||
}),
|
||||
).toThrowError('Cannot find item to move for id : record-1');
|
||||
});
|
||||
|
||||
@@ -106,6 +106,7 @@ export const processGroupDrop = ({
|
||||
sourceRecordId: draggedRecordId,
|
||||
targetRecordId: targetRecord.id,
|
||||
recordsWithPosition: recordsWithPosition,
|
||||
isDroppedAfterList,
|
||||
});
|
||||
|
||||
if (!isDefined(singleDragResult.position)) {
|
||||
@@ -125,6 +126,7 @@ export const processGroupDrop = ({
|
||||
selectedRecordIds,
|
||||
recordsWithPosition,
|
||||
targetRecordId: targetRecord.id,
|
||||
isDroppedAfterList,
|
||||
});
|
||||
|
||||
for (const update of multiDragResult.recordUpdates) {
|
||||
|
||||
@@ -7,6 +7,7 @@ type MultiDragContext = {
|
||||
targetRecordId: string;
|
||||
selectedRecordIds: string[];
|
||||
recordsWithPosition: RecordWithPosition[];
|
||||
isDroppedAfterList: boolean;
|
||||
};
|
||||
|
||||
export const processMultiDrag = ({
|
||||
@@ -14,12 +15,14 @@ export const processMultiDrag = ({
|
||||
targetRecordId,
|
||||
selectedRecordIds,
|
||||
recordsWithPosition,
|
||||
isDroppedAfterList,
|
||||
}: MultiDragContext): MultiDragResult => {
|
||||
const newPositionOfDraggedRecords = computeNewPositionsOfDraggedRecords({
|
||||
arrayOfRecordsWithPosition: recordsWithPosition,
|
||||
draggedRecordId,
|
||||
targetRecordId,
|
||||
sourceRecordIds: selectedRecordIds,
|
||||
isDroppedAfterList,
|
||||
});
|
||||
|
||||
return {
|
||||
|
||||
@@ -7,17 +7,20 @@ type SingleDragContext = {
|
||||
targetRecordId: string;
|
||||
sourceRecordId: string;
|
||||
recordsWithPosition: RecordWithPosition[];
|
||||
isDroppedAfterList: boolean;
|
||||
};
|
||||
|
||||
export const processSingleDrag = ({
|
||||
targetRecordId,
|
||||
sourceRecordId,
|
||||
recordsWithPosition,
|
||||
isDroppedAfterList,
|
||||
}: SingleDragContext): RecordWithPosition => {
|
||||
const newPosition = computeNewPositionOfDraggedRecord({
|
||||
arrayOfRecordsWithPosition: recordsWithPosition,
|
||||
idOfItemToMove: sourceRecordId,
|
||||
idOfTargetItem: targetRecordId,
|
||||
isDroppedAfterList,
|
||||
});
|
||||
|
||||
return {
|
||||
|
||||
+3
@@ -35,10 +35,13 @@ export const useReorderVisibleRecordFields = (recordTableId: string) => {
|
||||
|
||||
const recordToMove = visibleRecordFields[fromIndex];
|
||||
|
||||
const isDroppedAfterList = toIndex >= visibleRecordFields.length;
|
||||
|
||||
const newPositionOfTargetRecord = computeNewPositionOfDraggedRecord({
|
||||
arrayOfRecordsWithPosition: currentRecordFields,
|
||||
idOfItemToMove: idOfRecordToMove,
|
||||
idOfTargetItem: idOfTargetRecord,
|
||||
isDroppedAfterList,
|
||||
});
|
||||
|
||||
updateRecordField(recordToMove.fieldMetadataItemId, {
|
||||
|
||||
+6
-6
@@ -2,11 +2,11 @@ import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadata
|
||||
import { RecordBoardColumnContext } from '@/object-record/record-board/record-board-column/contexts/RecordBoardColumnContext';
|
||||
import { useRecordGroupVisibility } from '@/object-record/record-group/hooks/useRecordGroupVisibility';
|
||||
import { useReorderRecordGroups } from '@/object-record/record-group/hooks/useReorderRecordGroups';
|
||||
import { recordGroupFieldMetadataComponentState } from '@/object-record/record-group/states/recordGroupFieldMetadataComponentState';
|
||||
import { visibleRecordGroupIdsComponentFamilySelector } from '@/object-record/record-group/states/selectors/visibleRecordGroupIdsComponentFamilySelector';
|
||||
import { type RecordGroupAction } from '@/object-record/record-group/types/RecordGroupActions';
|
||||
import { useRecordIndexContextOrThrow } from '@/object-record/record-index/contexts/RecordIndexContext';
|
||||
import { useRecordIndexIdFromCurrentContextStore } from '@/object-record/record-index/hooks/useRecordIndexIdFromCurrentContextStore';
|
||||
import { recordIndexGroupFieldMetadataItemComponentState } from '@/object-record/record-index/states/recordIndexGroupFieldMetadataComponentState';
|
||||
import { useHasPermissionFlag } from '@/settings/roles/hooks/useHasPermissionFlag';
|
||||
import { navigationMemorizedUrlState } from '@/ui/navigation/states/navigationMemorizedUrlState';
|
||||
import { useRecoilComponentFamilyValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentFamilyValue';
|
||||
@@ -49,8 +49,8 @@ export const useRecordGroupActions = ({
|
||||
objectNameSingular,
|
||||
});
|
||||
|
||||
const recordGroupFieldMetadata = useRecoilComponentValue(
|
||||
recordGroupFieldMetadataComponentState,
|
||||
const recordIndexGroupFieldMetadataItem = useRecoilComponentValue(
|
||||
recordIndexGroupFieldMetadataItemComponentState,
|
||||
);
|
||||
|
||||
const { handleVisibilityChange: handleRecordGroupVisibilityChange } =
|
||||
@@ -73,13 +73,13 @@ export const useRecordGroupActions = ({
|
||||
const navigateToSelectSettings = useCallback(() => {
|
||||
setNavigationMemorizedUrl(location.pathname + location.search);
|
||||
|
||||
if (!isDefined(recordGroupFieldMetadata)) {
|
||||
if (!isDefined(recordIndexGroupFieldMetadataItem)) {
|
||||
throw new Error('recordGroupFieldMetadata is not a non-empty string');
|
||||
}
|
||||
|
||||
navigate(SettingsPath.ObjectFieldEdit, {
|
||||
objectNamePlural: objectMetadataItem.namePlural,
|
||||
fieldName: recordGroupFieldMetadata.name,
|
||||
fieldName: recordIndexGroupFieldMetadataItem.name,
|
||||
});
|
||||
}, [
|
||||
setNavigationMemorizedUrl,
|
||||
@@ -87,7 +87,7 @@ export const useRecordGroupActions = ({
|
||||
location.search,
|
||||
navigate,
|
||||
objectMetadataItem,
|
||||
recordGroupFieldMetadata,
|
||||
recordIndexGroupFieldMetadataItem,
|
||||
]);
|
||||
|
||||
const hasAccessToDataModelSettings = useHasPermissionFlag(
|
||||
|
||||
+4
-4
@@ -1,17 +1,17 @@
|
||||
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
|
||||
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||
import { recordGroupDefinitionFamilyState } from '@/object-record/record-group/states/recordGroupDefinitionFamilyState';
|
||||
import { recordGroupFieldMetadataComponentState } from '@/object-record/record-group/states/recordGroupFieldMetadataComponentState';
|
||||
import { recordGroupIdsComponentState } from '@/object-record/record-group/states/recordGroupIdsComponentState';
|
||||
import { type RecordGroupDefinition } from '@/object-record/record-group/types/RecordGroupDefinition';
|
||||
import { recordIndexGroupFieldMetadataItemComponentState } from '@/object-record/record-index/states/recordIndexGroupFieldMetadataComponentState';
|
||||
import { getRecordIndexIdFromObjectNamePluralAndViewId } from '@/object-record/utils/getRecordIndexIdFromObjectNamePluralAndViewId';
|
||||
import { getSnapshotValue } from '@/ui/utilities/state/utils/getSnapshotValue';
|
||||
import { type ViewGroup } from '@/views/types/ViewGroup';
|
||||
import { mapViewGroupsToRecordGroupDefinitions } from '@/views/utils/mapViewGroupsToRecordGroupDefinitions';
|
||||
import { useCallback } from 'react';
|
||||
import { useRecoilCallback } from 'recoil';
|
||||
import { isDeeplyEqual } from '~/utils/isDeeplyEqual';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { isDeeplyEqual } from '~/utils/isDeeplyEqual';
|
||||
|
||||
export const useSetRecordGroups = () => {
|
||||
const setRecordGroups = useRecoilCallback(
|
||||
@@ -48,7 +48,7 @@ export const useSetRecordGroups = () => {
|
||||
: undefined;
|
||||
const currentFieldMetadata = getSnapshotValue(
|
||||
snapshot,
|
||||
recordGroupFieldMetadataComponentState.atomFamily({
|
||||
recordIndexGroupFieldMetadataItemComponentState.atomFamily({
|
||||
instanceId: recordIndexId,
|
||||
}),
|
||||
);
|
||||
@@ -56,7 +56,7 @@ export const useSetRecordGroups = () => {
|
||||
// Set the field metadata linked to the record groups
|
||||
if (!isDeeplyEqual(fieldMetadata, currentFieldMetadata)) {
|
||||
set(
|
||||
recordGroupFieldMetadataComponentState.atomFamily({
|
||||
recordIndexGroupFieldMetadataItemComponentState.atomFamily({
|
||||
instanceId: recordIndexId,
|
||||
}),
|
||||
fieldMetadata,
|
||||
|
||||
-11
@@ -1,11 +0,0 @@
|
||||
import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
|
||||
import { createComponentState } from '@/ui/utilities/state/component-state/utils/createComponentState';
|
||||
import { ViewComponentInstanceContext } from '@/views/states/contexts/ViewComponentInstanceContext';
|
||||
|
||||
export const recordGroupFieldMetadataComponentState = createComponentState<
|
||||
FieldMetadataItem | undefined
|
||||
>({
|
||||
key: 'recordGroupFieldMetadataComponentState',
|
||||
defaultValue: undefined,
|
||||
componentInstanceContext: ViewComponentInstanceContext,
|
||||
});
|
||||
-72
@@ -1,72 +0,0 @@
|
||||
import { useEffect } from 'react';
|
||||
import { useRecoilState, useSetRecoilState } from 'recoil';
|
||||
|
||||
import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
|
||||
import { isRecordBoardFetchingRecordsByColumnFamilyState } from '@/object-record/record-board/states/isRecordBoardFetchingRecordsByColumnFamilyState';
|
||||
import { recordBoardShouldFetchMoreInColumnComponentFamilyState } from '@/object-record/record-board/states/recordBoardShouldFetchMoreInColumnComponentFamilyState';
|
||||
import { useLoadRecordIndexBoardColumn } from '@/object-record/record-index/hooks/useLoadRecordIndexBoardColumn';
|
||||
import { isRecordIndexBoardColumnLoadingFamilyState } from '@/object-record/states/isRecordBoardColumnLoadingFamilyState';
|
||||
import { useRecoilComponentFamilyState } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentFamilyState';
|
||||
|
||||
export const RecordIndexBoardColumnLoaderEffect = ({
|
||||
objectNameSingular,
|
||||
recordBoardId,
|
||||
kanbanFieldMetadataItem,
|
||||
columnId,
|
||||
}: {
|
||||
recordBoardId: string;
|
||||
objectNameSingular: string;
|
||||
kanbanFieldMetadataItem: FieldMetadataItem;
|
||||
columnId: string;
|
||||
}) => {
|
||||
const [shouldFetchMore, setShouldFetchMore] = useRecoilComponentFamilyState(
|
||||
recordBoardShouldFetchMoreInColumnComponentFamilyState,
|
||||
columnId,
|
||||
recordBoardId,
|
||||
);
|
||||
|
||||
const [loadingRecordsForThisColumn, setLoadingRecordsForThisColumn] =
|
||||
useRecoilState(isRecordBoardFetchingRecordsByColumnFamilyState(columnId));
|
||||
|
||||
const { fetchMoreRecords, loading, records, hasNextPage } =
|
||||
useLoadRecordIndexBoardColumn({
|
||||
objectNameSingular,
|
||||
recordBoardId,
|
||||
kanbanFieldMetadataItem,
|
||||
columnId,
|
||||
});
|
||||
|
||||
const setIsRecordIndexLoading = useSetRecoilState(
|
||||
isRecordIndexBoardColumnLoadingFamilyState(columnId),
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
setIsRecordIndexLoading(loading && records.length === 0);
|
||||
}, [records, loading, setIsRecordIndexLoading]);
|
||||
|
||||
useEffect(() => {
|
||||
const run = async () => {
|
||||
if (!loading && shouldFetchMore && hasNextPage) {
|
||||
setLoadingRecordsForThisColumn(true);
|
||||
setShouldFetchMore(false);
|
||||
|
||||
await fetchMoreRecords?.();
|
||||
|
||||
setLoadingRecordsForThisColumn(false);
|
||||
}
|
||||
};
|
||||
|
||||
run();
|
||||
}, [
|
||||
setShouldFetchMore,
|
||||
fetchMoreRecords,
|
||||
loading,
|
||||
shouldFetchMore,
|
||||
setLoadingRecordsForThisColumn,
|
||||
loadingRecordsForThisColumn,
|
||||
|
||||
hasNextPage,
|
||||
]);
|
||||
|
||||
return <></>;
|
||||
};
|
||||
-47
@@ -1,47 +0,0 @@
|
||||
import { recordGroupIdsComponentState } from '@/object-record/record-group/states/recordGroupIdsComponentState';
|
||||
import { RecordIndexBoardColumnLoaderEffect } from '@/object-record/record-index/components/RecordIndexBoardColumnLoaderEffect';
|
||||
import { recordIndexGroupFieldMetadataItemComponentState } from '@/object-record/record-index/states/recordIndexGroupFieldMetadataItemComponentState';
|
||||
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
type RecordIndexBoardDataLoaderProps = {
|
||||
objectNameSingular: string;
|
||||
recordBoardId: string;
|
||||
};
|
||||
|
||||
export const RecordIndexBoardDataLoader = ({
|
||||
objectNameSingular,
|
||||
recordBoardId,
|
||||
}: RecordIndexBoardDataLoaderProps) => {
|
||||
const recordGroupIds = useRecoilComponentValue(recordGroupIdsComponentState);
|
||||
|
||||
const recordIndexGroupFieldMetadataItem = useRecoilComponentValue(
|
||||
recordIndexGroupFieldMetadataItemComponentState,
|
||||
);
|
||||
|
||||
if (!isDefined(recordIndexGroupFieldMetadataItem)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{recordGroupIds.map((recordGroupId) => (
|
||||
<RecordIndexBoardColumnLoaderEffect
|
||||
objectNameSingular={objectNameSingular}
|
||||
kanbanFieldMetadataItem={recordIndexGroupFieldMetadataItem}
|
||||
recordBoardId={recordBoardId}
|
||||
columnId={recordGroupId}
|
||||
key={recordGroupId}
|
||||
/>
|
||||
))}
|
||||
{recordIndexGroupFieldMetadataItem.isNullable === true && (
|
||||
<RecordIndexBoardColumnLoaderEffect
|
||||
objectNameSingular={objectNameSingular}
|
||||
kanbanFieldMetadataItem={recordIndexGroupFieldMetadataItem}
|
||||
recordBoardId={recordBoardId}
|
||||
columnId="no-value"
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
+2
-11
@@ -2,9 +2,7 @@ import styled from '@emotion/styled';
|
||||
import { useRecoilState } from 'recoil';
|
||||
|
||||
import { ObjectOptionsDropdown } from '@/object-record/object-options-dropdown/components/ObjectOptionsDropdown';
|
||||
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 { RecordBoardContainer } from '@/object-record/record-board/components/RecordBoardContainer';
|
||||
import { RecordIndexTableContainer } from '@/object-record/record-index/components/RecordIndexTableContainer';
|
||||
import { RecordIndexViewBarEffect } from '@/object-record/record-index/components/RecordIndexViewBarEffect';
|
||||
import { recordIndexViewTypeState } from '@/object-record/record-index/states/recordIndexViewTypeState';
|
||||
@@ -15,7 +13,6 @@ import { SpreadsheetImportProvider } from '@/spreadsheet-import/provider/compone
|
||||
|
||||
import { RecordIndexCalendarContainer } from '@/object-record/record-index/components/RecordIndexCalendarContainer';
|
||||
import { RecordIndexFiltersToContextStoreEffect } from '@/object-record/record-index/components/RecordIndexFiltersToContextStoreEffect';
|
||||
import { RecordIndexTableContainerEffect } from '@/object-record/record-index/components/RecordIndexTableContainerEffect';
|
||||
import { ViewBar } from '@/views/components/ViewBar';
|
||||
import { ViewType } from '@/views/types/ViewType';
|
||||
|
||||
@@ -68,21 +65,15 @@ export const RecordIndexContainer = () => {
|
||||
{recordIndexViewType === ViewType.Table && (
|
||||
<>
|
||||
<RecordIndexTableContainer recordTableId={recordIndexId} />
|
||||
<RecordIndexTableContainerEffect />
|
||||
</>
|
||||
)}
|
||||
{recordIndexViewType === ViewType.Kanban && (
|
||||
<StyledContainerWithPadding>
|
||||
<RecordIndexBoardContainer
|
||||
<RecordBoardContainer
|
||||
recordBoardId={recordIndexId}
|
||||
viewBarId={recordIndexId}
|
||||
objectNameSingular={objectNameSingular}
|
||||
/>
|
||||
<RecordIndexBoardDataLoader
|
||||
objectNameSingular={objectNameSingular}
|
||||
recordBoardId={recordIndexId}
|
||||
/>
|
||||
<RecordIndexBoardDataLoaderEffect recordBoardId={recordIndexId} />
|
||||
</StyledContainerWithPadding>
|
||||
)}
|
||||
{recordIndexViewType === ViewType.Calendar && (
|
||||
|
||||
+30
-7
@@ -1,4 +1,5 @@
|
||||
import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
|
||||
import { recordGroupDefinitionsComponentSelector } from '@/object-record/record-group/states/selectors/recordGroupDefinitionsComponentSelector';
|
||||
import { useRecordIndexContextOrThrow } from '@/object-record/record-index/contexts/RecordIndexContext';
|
||||
import { useAggregateGqlFieldsFromRecordIndexGroupAggregates } from '@/object-record/record-index/hooks/useAggregateGqlFieldsFromRecordIndexGroupAggregates';
|
||||
import { useRecordIndexGroupsAggregatesGroupBy } from '@/object-record/record-index/hooks/useRecordIndexGroupsAggregatesGroupBy';
|
||||
@@ -9,6 +10,7 @@ import { recordIndexAggregateDisplayLabelComponentState } from '@/object-record/
|
||||
import { turnRecordIndexGroupByAggregateQueryResultIntoRecordAggregateValueByGroupValue } from '@/object-record/record-index/utils/turnRecordIndexGroupByAggregateQueryResultIntoRecordAggregateValueByGroupValue';
|
||||
import { type ExtendedAggregateOperations } from '@/object-record/record-table/types/ExtendedAggregateOperations';
|
||||
import { useRecoilComponentCallbackState } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackState';
|
||||
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
|
||||
import { useEffect } from 'react';
|
||||
import { type Nullable } from 'twenty-shared/types';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
@@ -49,6 +51,10 @@ export const RecordIndexGroupAggregateQueryEffect = ({
|
||||
const { setRecordIndexAggregateDisplayValueForRecordGroupValue } =
|
||||
useSetRecordIndexAggregateDisplayValueForRecordGroupValue();
|
||||
|
||||
const recordGroupDefinitions = useRecoilComponentValue(
|
||||
recordGroupDefinitionsComponentSelector,
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
!loading &&
|
||||
@@ -71,13 +77,29 @@ export const RecordIndexGroupAggregateQueryEffect = ({
|
||||
recordIndexGroupAggregateFieldMetadataItem,
|
||||
);
|
||||
|
||||
for (const recordAggregateValueByGroupValue of recordAggregateValueByGroupValueArray) {
|
||||
setRecordIndexAggregateDisplayValueForRecordGroupValue(
|
||||
recordIndexGroupAggregateOperation,
|
||||
recordIndexGroupAggregateFieldMetadataItem,
|
||||
recordAggregateValueByGroupValue.recordGroupValue,
|
||||
recordAggregateValueByGroupValue.recordAggregateValue,
|
||||
);
|
||||
for (const recordGroupDefinition of recordGroupDefinitions) {
|
||||
const foundAggregateValueForGroup =
|
||||
recordAggregateValueByGroupValueArray.find(
|
||||
(recordAggregateValueByGroupValue) =>
|
||||
recordAggregateValueByGroupValue.recordGroupValue ===
|
||||
recordGroupDefinition.value,
|
||||
);
|
||||
|
||||
if (isDefined(foundAggregateValueForGroup)) {
|
||||
setRecordIndexAggregateDisplayValueForRecordGroupValue(
|
||||
recordIndexGroupAggregateOperation,
|
||||
recordIndexGroupAggregateFieldMetadataItem,
|
||||
foundAggregateValueForGroup.recordGroupValue,
|
||||
foundAggregateValueForGroup.recordAggregateValue,
|
||||
);
|
||||
} else {
|
||||
setRecordIndexAggregateDisplayValueForRecordGroupValue(
|
||||
recordIndexGroupAggregateOperation,
|
||||
recordIndexGroupAggregateFieldMetadataItem,
|
||||
recordGroupDefinition.value ?? '',
|
||||
0,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -92,6 +114,7 @@ export const RecordIndexGroupAggregateQueryEffect = ({
|
||||
recordAggregateGqlField,
|
||||
recordIndexAggregateDisplayLabelCallbackState,
|
||||
objectMetadataItem,
|
||||
recordGroupDefinitions,
|
||||
]);
|
||||
|
||||
return null;
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
import { RecordIndexGroupAggregateQueryEffect } from '@/object-record/record-index/components/RecordIndexGroupAggregateQueryEffect';
|
||||
import { recordIndexGroupAggregateFieldMetadataItemComponentState } from '@/object-record/record-index/states/recordIndexGroupAggregateFieldMetadataItemComponentState';
|
||||
import { recordIndexGroupAggregateOperationComponentState } from '@/object-record/record-index/states/recordIndexGroupAggregateOperationComponentState';
|
||||
import { recordIndexGroupFieldMetadataItemComponentState } from '@/object-record/record-index/states/recordIndexGroupFieldMetadataItemComponentState';
|
||||
import { recordIndexGroupFieldMetadataItemComponentState } from '@/object-record/record-index/states/recordIndexGroupFieldMetadataComponentState';
|
||||
|
||||
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
+2
-2
@@ -14,8 +14,8 @@ import { useObjectOptionsForBoard } from '@/object-record/object-options-dropdow
|
||||
import { visibleRecordFieldsComponentSelector } from '@/object-record/record-field/states/visibleRecordFieldsComponentSelector';
|
||||
import { type RecordField } from '@/object-record/record-field/types/RecordField';
|
||||
import { useFilterValueDependencies } from '@/object-record/record-filter/hooks/useFilterValueDependencies';
|
||||
import { recordGroupFieldMetadataComponentState } from '@/object-record/record-group/states/recordGroupFieldMetadataComponentState';
|
||||
import { useFindManyRecordIndexTableParams } from '@/object-record/record-index/hooks/useFindManyRecordIndexTableParams';
|
||||
import { recordIndexGroupFieldMetadataItemComponentState } from '@/object-record/record-index/states/recordIndexGroupFieldMetadataComponentState';
|
||||
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
|
||||
import { ViewType } from '@/views/types/ViewType';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
@@ -59,7 +59,7 @@ export const useRecordIndexLazyFetchRecords = ({
|
||||
});
|
||||
|
||||
const recordGroupFieldMetadata = useRecoilComponentValue(
|
||||
recordGroupFieldMetadataComponentState,
|
||||
recordIndexGroupFieldMetadataItemComponentState,
|
||||
recordIndexId,
|
||||
);
|
||||
|
||||
|
||||
-131
@@ -1,131 +0,0 @@
|
||||
import { useEffect } from 'react';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
|
||||
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
|
||||
import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords';
|
||||
import { turnSortsIntoOrderBy } from '@/object-record/object-sort-dropdown/utils/turnSortsIntoOrderBy';
|
||||
import { useSetRecordIdsForColumn } from '@/object-record/record-board/hooks/useSetRecordIdsForColumn';
|
||||
import { currentRecordFilterGroupsComponentState } from '@/object-record/record-filter-group/states/currentRecordFilterGroupsComponentState';
|
||||
import { useFilterValueDependencies } from '@/object-record/record-filter/hooks/useFilterValueDependencies';
|
||||
import { currentRecordFiltersComponentState } from '@/object-record/record-filter/states/currentRecordFiltersComponentState';
|
||||
import { recordGroupDefinitionFamilyState } from '@/object-record/record-group/states/recordGroupDefinitionFamilyState';
|
||||
|
||||
import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
|
||||
import { currentRecordSortsComponentState } from '@/object-record/record-sort/states/currentRecordSortsComponentState';
|
||||
|
||||
import { useRecordsFieldVisibleGqlFields } from '@/object-record/record-field/hooks/useRecordsFieldVisibleGqlFields';
|
||||
import { anyFieldFilterValueComponentState } from '@/object-record/record-filter/states/anyFieldFilterValueComponentState';
|
||||
import { recordGroupFieldMetadataComponentState } from '@/object-record/record-group/states/recordGroupFieldMetadataComponentState';
|
||||
import { useUpsertRecordsInStore } from '@/object-record/record-store/hooks/useUpsertRecordsInStore';
|
||||
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
|
||||
import {
|
||||
combineFilters,
|
||||
computeRecordGqlOperationFilter,
|
||||
isDefined,
|
||||
turnAnyFieldFilterIntoRecordGqlFilter,
|
||||
} from 'twenty-shared/utils';
|
||||
|
||||
type UseLoadRecordIndexBoardProps = {
|
||||
objectNameSingular: string;
|
||||
kanbanFieldMetadataItem: FieldMetadataItem;
|
||||
recordBoardId: string;
|
||||
columnId: string;
|
||||
};
|
||||
|
||||
export const useLoadRecordIndexBoardColumn = ({
|
||||
objectNameSingular,
|
||||
kanbanFieldMetadataItem,
|
||||
recordBoardId,
|
||||
columnId,
|
||||
}: UseLoadRecordIndexBoardProps) => {
|
||||
const { objectMetadataItem } = useObjectMetadataItem({
|
||||
objectNameSingular,
|
||||
});
|
||||
const { setRecordIdsForColumn } = useSetRecordIdsForColumn(recordBoardId);
|
||||
const { upsertRecordsInStore } = useUpsertRecordsInStore();
|
||||
|
||||
const recordGroupDefinition = useRecoilValue(
|
||||
recordGroupDefinitionFamilyState(columnId),
|
||||
);
|
||||
|
||||
const currentRecordFilterGroups = useRecoilComponentValue(
|
||||
currentRecordFilterGroupsComponentState,
|
||||
);
|
||||
|
||||
const currentRecordFilters = useRecoilComponentValue(
|
||||
currentRecordFiltersComponentState,
|
||||
);
|
||||
|
||||
const currentRecordSorts = useRecoilComponentValue(
|
||||
currentRecordSortsComponentState,
|
||||
);
|
||||
|
||||
const { filterValueDependencies } = useFilterValueDependencies();
|
||||
|
||||
const requestFilters = computeRecordGqlOperationFilter({
|
||||
filterValueDependencies,
|
||||
recordFilters: currentRecordFilters,
|
||||
recordFilterGroups: currentRecordFilterGroups,
|
||||
fields: objectMetadataItem.fields,
|
||||
});
|
||||
|
||||
const anyFieldFilterValue = useRecoilComponentValue(
|
||||
anyFieldFilterValueComponentState,
|
||||
);
|
||||
|
||||
const { recordGqlOperationFilter: anyFieldFilter } =
|
||||
turnAnyFieldFilterIntoRecordGqlFilter({
|
||||
fields: objectMetadataItem.fields,
|
||||
filterValue: anyFieldFilterValue,
|
||||
});
|
||||
|
||||
const orderBy = turnSortsIntoOrderBy(objectMetadataItem, currentRecordSorts);
|
||||
const recordGroupFieldMetadata = useRecoilComponentValue(
|
||||
recordGroupFieldMetadataComponentState,
|
||||
recordBoardId,
|
||||
);
|
||||
|
||||
const recordGqlFields = useRecordsFieldVisibleGqlFields({
|
||||
objectMetadataItem,
|
||||
additionalFieldMetadataId: recordGroupFieldMetadata?.id,
|
||||
});
|
||||
|
||||
const recordIndexKanbanFieldMetadataFilterValue = isDefined(
|
||||
recordGroupDefinition?.value,
|
||||
)
|
||||
? { in: [recordGroupDefinition?.value] }
|
||||
: { is: 'NULL' };
|
||||
|
||||
const combinedFilters = combineFilters([
|
||||
anyFieldFilter,
|
||||
requestFilters,
|
||||
{
|
||||
[kanbanFieldMetadataItem.name]: recordIndexKanbanFieldMetadataFilterValue,
|
||||
},
|
||||
]);
|
||||
|
||||
const { records, loading, fetchMoreRecords, queryIdentifier, hasNextPage } =
|
||||
useFindManyRecords({
|
||||
objectNameSingular,
|
||||
filter: combinedFilters,
|
||||
orderBy,
|
||||
recordGqlFields,
|
||||
limit: 10,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
setRecordIdsForColumn(columnId, records);
|
||||
}, [records, setRecordIdsForColumn, columnId]);
|
||||
|
||||
useEffect(() => {
|
||||
upsertRecordsInStore(records);
|
||||
}, [records, upsertRecordsInStore]);
|
||||
|
||||
return {
|
||||
records,
|
||||
loading,
|
||||
fetchMoreRecords,
|
||||
queryIdentifier,
|
||||
hasNextPage,
|
||||
};
|
||||
};
|
||||
+1
-1
@@ -6,12 +6,12 @@ import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataI
|
||||
import { formatFieldMetadataItemAsColumnDefinition } from '@/object-metadata/utils/formatFieldMetadataItemAsColumnDefinition';
|
||||
import { type FieldMetadata } from '@/object-record/record-field/ui/types/FieldMetadata';
|
||||
import { useSetRecordGroups } from '@/object-record/record-group/hooks/useSetRecordGroups';
|
||||
import { recordIndexGroupFieldMetadataItemComponentState } from '@/object-record/record-index/states/recordIndexGroupFieldMetadataComponentState';
|
||||
|
||||
import { recordIndexCalendarFieldMetadataIdState } from '@/object-record/record-index/states/recordIndexCalendarFieldMetadataIdState';
|
||||
import { recordIndexFieldDefinitionsState } from '@/object-record/record-index/states/recordIndexFieldDefinitionsState';
|
||||
import { recordIndexGroupAggregateFieldMetadataItemComponentState } from '@/object-record/record-index/states/recordIndexGroupAggregateFieldMetadataItemComponentState';
|
||||
import { recordIndexGroupAggregateOperationComponentState } from '@/object-record/record-index/states/recordIndexGroupAggregateOperationComponentState';
|
||||
import { recordIndexGroupFieldMetadataItemComponentState } from '@/object-record/record-index/states/recordIndexGroupFieldMetadataItemComponentState';
|
||||
import { recordIndexOpenRecordInState } from '@/object-record/record-index/states/recordIndexOpenRecordInState';
|
||||
import { recordIndexViewTypeState } from '@/object-record/record-index/states/recordIndexViewTypeState';
|
||||
import { viewFieldAggregateOperationState } from '@/object-record/record-table/record-table-footer/states/viewFieldAggregateOperationState';
|
||||
|
||||
+69
@@ -0,0 +1,69 @@
|
||||
import { turnSortsIntoOrderBy } from '@/object-record/object-sort-dropdown/utils/turnSortsIntoOrderBy';
|
||||
import { useRecordsFieldVisibleGqlFields } from '@/object-record/record-field/hooks/useRecordsFieldVisibleGqlFields';
|
||||
import { currentRecordFilterGroupsComponentState } from '@/object-record/record-filter-group/states/currentRecordFilterGroupsComponentState';
|
||||
import { useFilterValueDependencies } from '@/object-record/record-filter/hooks/useFilterValueDependencies';
|
||||
import { anyFieldFilterValueComponentState } from '@/object-record/record-filter/states/anyFieldFilterValueComponentState';
|
||||
import { currentRecordFiltersComponentState } from '@/object-record/record-filter/states/currentRecordFiltersComponentState';
|
||||
import { useRecordIndexContextOrThrow } from '@/object-record/record-index/contexts/RecordIndexContext';
|
||||
import { recordIndexGroupFieldMetadataItemComponentState } from '@/object-record/record-index/states/recordIndexGroupFieldMetadataComponentState';
|
||||
import { currentRecordSortsComponentState } from '@/object-record/record-sort/states/currentRecordSortsComponentState';
|
||||
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
|
||||
import {
|
||||
combineFilters,
|
||||
computeRecordGqlOperationFilter,
|
||||
turnAnyFieldFilterIntoRecordGqlFilter,
|
||||
} from 'twenty-shared/utils';
|
||||
|
||||
export const useRecordIndexGroupCommonQueryVariables = () => {
|
||||
const { objectMetadataItem } = useRecordIndexContextOrThrow();
|
||||
|
||||
const currentRecordFilterGroups = useRecoilComponentValue(
|
||||
currentRecordFilterGroupsComponentState,
|
||||
);
|
||||
|
||||
const currentRecordFilters = useRecoilComponentValue(
|
||||
currentRecordFiltersComponentState,
|
||||
);
|
||||
|
||||
const currentRecordSorts = useRecoilComponentValue(
|
||||
currentRecordSortsComponentState,
|
||||
);
|
||||
|
||||
const { filterValueDependencies } = useFilterValueDependencies();
|
||||
|
||||
const requestFilters = computeRecordGqlOperationFilter({
|
||||
filterValueDependencies,
|
||||
recordFilters: currentRecordFilters,
|
||||
recordFilterGroups: currentRecordFilterGroups,
|
||||
fields: objectMetadataItem.fields,
|
||||
});
|
||||
|
||||
const anyFieldFilterValue = useRecoilComponentValue(
|
||||
anyFieldFilterValueComponentState,
|
||||
);
|
||||
|
||||
const { recordGqlOperationFilter: anyFieldFilter } =
|
||||
turnAnyFieldFilterIntoRecordGqlFilter({
|
||||
fields: objectMetadataItem.fields,
|
||||
filterValue: anyFieldFilterValue,
|
||||
});
|
||||
|
||||
const orderBy = turnSortsIntoOrderBy(objectMetadataItem, currentRecordSorts);
|
||||
|
||||
const recordGroupFieldMetadata = useRecoilComponentValue(
|
||||
recordIndexGroupFieldMetadataItemComponentState,
|
||||
);
|
||||
|
||||
const recordGqlFields = useRecordsFieldVisibleGqlFields({
|
||||
objectMetadataItem,
|
||||
additionalFieldMetadataId: recordGroupFieldMetadata?.id,
|
||||
});
|
||||
|
||||
const combinedFilters = combineFilters([anyFieldFilter, requestFilters]);
|
||||
|
||||
return {
|
||||
combinedFilters,
|
||||
recordGqlFields,
|
||||
orderBy,
|
||||
};
|
||||
};
|
||||
+71
@@ -0,0 +1,71 @@
|
||||
import { useApolloCoreClient } from '@/object-metadata/hooks/useApolloCoreClient';
|
||||
import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems';
|
||||
import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
|
||||
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||
import { type RecordGqlEdge } from '@/object-record/graphql/types/RecordGqlEdge';
|
||||
import { useObjectPermissions } from '@/object-record/hooks/useObjectPermissions';
|
||||
import { generateGroupsRecordsGroupByQuery } from '@/object-record/record-aggregate/utils/generateGroupsRecordsGroupByQuery';
|
||||
import { useRecordIndexGroupCommonQueryVariables } from '@/object-record/record-index/hooks/useRecordIndexGroupCommonQueryVariables';
|
||||
import { buildGroupByFieldObject } from '@/page-layout/widgets/graph/utils/buildGroupByFieldObject';
|
||||
import { useLazyQuery } from '@apollo/client';
|
||||
import { type Nullable } from 'twenty-shared/types';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { type PageInfo } from '~/generated/graphql';
|
||||
|
||||
export type GroupsRecordsGroupByLazyResult = {
|
||||
[queryGqlFieldName: string]: Array<{
|
||||
groupByDimensionValues: string[];
|
||||
edges: RecordGqlEdge[];
|
||||
pageInfo: PageInfo;
|
||||
__typename: string;
|
||||
}>;
|
||||
};
|
||||
|
||||
export const useRecordIndexGroupsRecordsLazyGroupBy = ({
|
||||
objectMetadataItem,
|
||||
groupByFieldMetadataItem,
|
||||
}: {
|
||||
objectMetadataItem: ObjectMetadataItem;
|
||||
groupByFieldMetadataItem: Nullable<FieldMetadataItem>;
|
||||
}) => {
|
||||
const { objectMetadataItems } = useObjectMetadataItems();
|
||||
const apolloCoreClient = useApolloCoreClient();
|
||||
|
||||
const { combinedFilters, orderBy, recordGqlFields } =
|
||||
useRecordIndexGroupCommonQueryVariables();
|
||||
|
||||
const { objectPermissionsByObjectMetadataId } = useObjectPermissions();
|
||||
|
||||
const recordIndexGroupsRecordGroupsGroupByQuery =
|
||||
generateGroupsRecordsGroupByQuery({
|
||||
objectMetadataItem,
|
||||
objectMetadataItems,
|
||||
objectPermissionsByObjectMetadataId,
|
||||
recordGqlFields,
|
||||
});
|
||||
|
||||
const groupByGqlInput = isDefined(groupByFieldMetadataItem)
|
||||
? buildGroupByFieldObject({
|
||||
field: groupByFieldMetadataItem,
|
||||
})
|
||||
: {};
|
||||
|
||||
const [executeRecordIndexGroupsRecordsLazyGroupBy] =
|
||||
useLazyQuery<GroupsRecordsGroupByLazyResult>(
|
||||
recordIndexGroupsRecordGroupsGroupByQuery,
|
||||
{
|
||||
variables: {
|
||||
filter: { ...combinedFilters },
|
||||
groupBy: {
|
||||
...groupByGqlInput,
|
||||
},
|
||||
orderByForRecords: orderBy,
|
||||
},
|
||||
client: apolloCoreClient,
|
||||
},
|
||||
);
|
||||
|
||||
return {
|
||||
executeRecordIndexGroupsRecordsLazyGroupBy,
|
||||
};
|
||||
};
|
||||
+4
-4
@@ -1,10 +1,10 @@
|
||||
import { ContextStoreComponentInstanceContext } from '@/context-store/states/contexts/ContextStoreComponentInstanceContext';
|
||||
import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
|
||||
import { createComponentState } from '@/ui/utilities/state/component-state/utils/createComponentState';
|
||||
import { ViewComponentInstanceContext } from '@/views/states/contexts/ViewComponentInstanceContext';
|
||||
|
||||
export const recordIndexGroupFieldMetadataItemComponentState =
|
||||
createComponentState<FieldMetadataItem | null>({
|
||||
createComponentState<FieldMetadataItem | undefined>({
|
||||
key: 'recordIndexGroupFieldMetadataItemComponentState',
|
||||
defaultValue: null,
|
||||
componentInstanceContext: ContextStoreComponentInstanceContext,
|
||||
defaultValue: undefined,
|
||||
componentInstanceContext: ViewComponentInstanceContext,
|
||||
});
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
import { ContextStoreComponentInstanceContext } from '@/context-store/states/contexts/ContextStoreComponentInstanceContext';
|
||||
import { createComponentState } from '@/ui/utilities/state/component-state/utils/createComponentState';
|
||||
|
||||
export const recordIndexRecordGroupsAreInInitialLoadingComponentState =
|
||||
createComponentState<boolean>({
|
||||
key: 'recordIndexRecordGroupsAreInInitialLoadingComponentState',
|
||||
defaultValue: false,
|
||||
componentInstanceContext: ContextStoreComponentInstanceContext,
|
||||
});
|
||||
+2
@@ -1,4 +1,5 @@
|
||||
import { useDeleteOneRecord } from '@/object-record/hooks/useDeleteOneRecord';
|
||||
import { RecordIndexTableContainerEffect } from '@/object-record/record-index/components/RecordIndexTableContainerEffect';
|
||||
import { useOpenRecordFromIndexView } from '@/object-record/record-index/hooks/useOpenRecordFromIndexView';
|
||||
import { RecordTable } from '@/object-record/record-table/components/RecordTable';
|
||||
import { RecordTableComponentInstance } from '@/object-record/record-table/components/RecordTableComponentInstance';
|
||||
@@ -67,6 +68,7 @@ export const RecordTableWithWrappers = ({
|
||||
>
|
||||
<RecordUpdateContext.Provider value={updateRecordMutation}>
|
||||
<RecordTable />
|
||||
<RecordIndexTableContainerEffect />
|
||||
</RecordUpdateContext.Provider>
|
||||
</ScrollWrapper>
|
||||
</EntityDeleteContext.Provider>
|
||||
|
||||
+3
-3
@@ -2,7 +2,7 @@
|
||||
|
||||
exports[`generateGroupByQuery should generate valid GraphQL query for empty aggregate operations 1`] = `
|
||||
"
|
||||
query PeopleGroupBy(
|
||||
query PeopleGroupByAggregates(
|
||||
$groupBy: [PersonGroupByInput!]!
|
||||
$filter: PersonFilterInput
|
||||
$orderBy: [PersonOrderByWithGroupByInput!]
|
||||
@@ -24,7 +24,7 @@ exports[`generateGroupByQuery should generate valid GraphQL query for empty aggr
|
||||
|
||||
exports[`generateGroupByQuery should generate valid GraphQL query for multiple aggregate operations 1`] = `
|
||||
"
|
||||
query OpportunitiesGroupBy(
|
||||
query OpportunitiesGroupByAggregates(
|
||||
$groupBy: [OpportunityGroupByInput!]!
|
||||
$filter: OpportunityFilterInput
|
||||
$orderBy: [OpportunityOrderByWithGroupByInput!]
|
||||
@@ -49,7 +49,7 @@ exports[`generateGroupByQuery should generate valid GraphQL query for multiple a
|
||||
|
||||
exports[`generateGroupByQuery should generate valid GraphQL query for single aggregate operation 1`] = `
|
||||
"
|
||||
query OpportunitiesGroupBy(
|
||||
query OpportunitiesGroupByAggregates(
|
||||
$groupBy: [OpportunityGroupByInput!]!
|
||||
$filter: OpportunityFilterInput
|
||||
$orderBy: [OpportunityOrderByWithGroupByInput!]
|
||||
|
||||
+89
-26
@@ -23,43 +23,106 @@ const mockRecordsWithPosition: RecordWithPosition[] = [
|
||||
];
|
||||
|
||||
describe('computeNewPositionOfRecordWithPosition', () => {
|
||||
it('should compute first position', () => {
|
||||
const newPosition = computeNewPositionOfDraggedRecord({
|
||||
arrayOfRecordsWithPosition: mockRecordsWithPosition,
|
||||
idOfItemToMove: 'B',
|
||||
idOfTargetItem: 'A',
|
||||
describe('dragging inside same list', () => {
|
||||
it('should compute first position', () => {
|
||||
const newPosition = computeNewPositionOfDraggedRecord({
|
||||
arrayOfRecordsWithPosition: mockRecordsWithPosition,
|
||||
idOfItemToMove: 'B',
|
||||
idOfTargetItem: 'A',
|
||||
isDroppedAfterList: false,
|
||||
});
|
||||
|
||||
expect(newPosition).toEqual(-1);
|
||||
});
|
||||
|
||||
expect(newPosition).toEqual(-1);
|
||||
it('should compute last position', () => {
|
||||
const newPosition = computeNewPositionOfDraggedRecord({
|
||||
arrayOfRecordsWithPosition: mockRecordsWithPosition,
|
||||
idOfItemToMove: 'B',
|
||||
idOfTargetItem: 'D',
|
||||
isDroppedAfterList: true,
|
||||
});
|
||||
|
||||
expect(newPosition).toEqual(4);
|
||||
});
|
||||
|
||||
it('should compute intermediary position after target item', () => {
|
||||
const newPosition = computeNewPositionOfDraggedRecord({
|
||||
arrayOfRecordsWithPosition: mockRecordsWithPosition,
|
||||
idOfItemToMove: 'A',
|
||||
idOfTargetItem: 'B',
|
||||
isDroppedAfterList: false,
|
||||
});
|
||||
|
||||
expect(newPosition).toEqual(1.5);
|
||||
});
|
||||
|
||||
it('should compute intermediary position before target item', () => {
|
||||
const newPosition = computeNewPositionOfDraggedRecord({
|
||||
arrayOfRecordsWithPosition: mockRecordsWithPosition,
|
||||
idOfItemToMove: 'A',
|
||||
idOfTargetItem: 'C',
|
||||
isDroppedAfterList: false,
|
||||
});
|
||||
|
||||
expect(newPosition).toEqual(2.5);
|
||||
});
|
||||
});
|
||||
|
||||
it('should compute last position', () => {
|
||||
const newPosition = computeNewPositionOfDraggedRecord({
|
||||
arrayOfRecordsWithPosition: mockRecordsWithPosition,
|
||||
idOfItemToMove: 'B',
|
||||
idOfTargetItem: 'D',
|
||||
describe('dragging from another list', () => {
|
||||
it('should compute first position', () => {
|
||||
const newPosition = computeNewPositionOfDraggedRecord({
|
||||
arrayOfRecordsWithPosition: mockRecordsWithPosition,
|
||||
idOfItemToMove: 'Unknown',
|
||||
idOfTargetItem: 'A',
|
||||
isDroppedAfterList: false,
|
||||
});
|
||||
|
||||
expect(newPosition).toEqual(-1);
|
||||
});
|
||||
|
||||
expect(newPosition).toEqual(4);
|
||||
});
|
||||
it('should compute last position', () => {
|
||||
const newPosition = computeNewPositionOfDraggedRecord({
|
||||
arrayOfRecordsWithPosition: mockRecordsWithPosition,
|
||||
idOfItemToMove: 'Unknown',
|
||||
idOfTargetItem: 'D',
|
||||
isDroppedAfterList: true,
|
||||
});
|
||||
|
||||
it('should compute intermediary position after target item', () => {
|
||||
const newPosition = computeNewPositionOfDraggedRecord({
|
||||
arrayOfRecordsWithPosition: mockRecordsWithPosition,
|
||||
idOfItemToMove: 'A',
|
||||
idOfTargetItem: 'B',
|
||||
expect(newPosition).toEqual(4);
|
||||
});
|
||||
|
||||
expect(newPosition).toEqual(1.5);
|
||||
});
|
||||
it('should compute position before last item', () => {
|
||||
const newPosition = computeNewPositionOfDraggedRecord({
|
||||
arrayOfRecordsWithPosition: mockRecordsWithPosition,
|
||||
idOfItemToMove: 'Unknown',
|
||||
idOfTargetItem: 'D',
|
||||
isDroppedAfterList: false,
|
||||
});
|
||||
|
||||
it('should compute intermediary position before target item', () => {
|
||||
const newPosition = computeNewPositionOfDraggedRecord({
|
||||
arrayOfRecordsWithPosition: mockRecordsWithPosition,
|
||||
idOfItemToMove: 'A',
|
||||
idOfTargetItem: 'C',
|
||||
expect(newPosition).toEqual(2.5);
|
||||
});
|
||||
|
||||
expect(newPosition).toEqual(2.5);
|
||||
it('should compute intermediary position after target item', () => {
|
||||
const newPosition = computeNewPositionOfDraggedRecord({
|
||||
arrayOfRecordsWithPosition: mockRecordsWithPosition,
|
||||
idOfItemToMove: 'Unknown',
|
||||
idOfTargetItem: 'B',
|
||||
isDroppedAfterList: false,
|
||||
});
|
||||
|
||||
expect(newPosition).toEqual(0.5);
|
||||
});
|
||||
|
||||
it('should compute intermediary position before target item', () => {
|
||||
const newPosition = computeNewPositionOfDraggedRecord({
|
||||
arrayOfRecordsWithPosition: mockRecordsWithPosition,
|
||||
idOfItemToMove: 'Unknown',
|
||||
idOfTargetItem: 'C',
|
||||
isDroppedAfterList: false,
|
||||
});
|
||||
|
||||
expect(newPosition).toEqual(1.5);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
+6
-6
@@ -10,10 +10,12 @@ export const computeNewPositionOfDraggedRecord = ({
|
||||
arrayOfRecordsWithPosition,
|
||||
idOfItemToMove,
|
||||
idOfTargetItem,
|
||||
isDroppedAfterList,
|
||||
}: {
|
||||
arrayOfRecordsWithPosition: RecordWithPosition[];
|
||||
idOfItemToMove: string;
|
||||
idOfTargetItem: string;
|
||||
isDroppedAfterList: boolean;
|
||||
}) => {
|
||||
const targetItem = arrayOfRecordsWithPosition.find(
|
||||
(recordToFind) => recordToFind.id === idOfTargetItem,
|
||||
@@ -29,6 +31,10 @@ export const computeNewPositionOfDraggedRecord = ({
|
||||
|
||||
const targetPosition = targetItem.position;
|
||||
|
||||
if (isDroppedAfterList) {
|
||||
return targetPosition + 1;
|
||||
}
|
||||
|
||||
const sortedRecordsByAscendingPosition = arrayOfRecordsWithPosition.toSorted(
|
||||
sortByProperty('position'),
|
||||
);
|
||||
@@ -43,16 +49,10 @@ export const computeNewPositionOfDraggedRecord = ({
|
||||
(recordToFind) => recordToFind.id === idOfTargetItem,
|
||||
);
|
||||
|
||||
const lastIndex = sortedRecordsByAscendingPosition.length - 1;
|
||||
|
||||
const shouldGoToFirstPosition = indexOfTargetItem === 0;
|
||||
|
||||
const shouldGoToLastPosition = indexOfTargetItem === lastIndex;
|
||||
|
||||
if (shouldGoToFirstPosition) {
|
||||
return targetPosition - 1;
|
||||
} else if (shouldGoToLastPosition) {
|
||||
return targetPosition + 1;
|
||||
} else {
|
||||
if (itemToMoveIsNotInTable) {
|
||||
const itemBeforeTargetItem =
|
||||
|
||||
+3
-5
@@ -8,11 +8,13 @@ export const computeNewPositionsOfDraggedRecords = ({
|
||||
draggedRecordId,
|
||||
targetRecordId,
|
||||
sourceRecordIds,
|
||||
isDroppedAfterList,
|
||||
}: {
|
||||
arrayOfRecordsWithPosition: RecordWithPosition[];
|
||||
draggedRecordId: string;
|
||||
targetRecordId: string;
|
||||
sourceRecordIds: string[];
|
||||
isDroppedAfterList: boolean;
|
||||
}): RecordWithPosition[] | null => {
|
||||
const targetItem = arrayOfRecordsWithPosition.find(
|
||||
(recordToFind) => recordToFind.id === targetRecordId,
|
||||
@@ -38,12 +40,8 @@ export const computeNewPositionsOfDraggedRecords = ({
|
||||
(recordToFind) => recordToFind.id === targetRecordId,
|
||||
);
|
||||
|
||||
const lastIndex = arrayOfRecordsWithPosition.length - 1;
|
||||
|
||||
const shouldGoToFirstPosition = indexOfTargetItem === 0;
|
||||
|
||||
const shouldGoToLastPosition = indexOfTargetItem === lastIndex;
|
||||
|
||||
if (shouldGoToFirstPosition) {
|
||||
const newPositions = computeNewEvenlySpacedPositions({
|
||||
startingPosition: targetPosition - 1,
|
||||
@@ -58,7 +56,7 @@ export const computeNewPositionsOfDraggedRecords = ({
|
||||
}));
|
||||
|
||||
return newSourceRecordsWithPosition;
|
||||
} else if (shouldGoToLastPosition) {
|
||||
} else if (isDroppedAfterList) {
|
||||
const newPositions = computeNewEvenlySpacedPositions({
|
||||
startingPosition: targetPosition,
|
||||
endingPosition: targetPosition + sourceRecordIds.length + 1,
|
||||
|
||||
+1
@@ -158,6 +158,7 @@ export class CommonGroupByQueryRunnerService extends CommonBaseQueryRunnerServic
|
||||
queryRunnerContext,
|
||||
orderByForRecords: args.orderByForRecords ?? [],
|
||||
groupLimit: args.limit,
|
||||
offsetForRecords: args.offsetForRecords,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -71,6 +71,7 @@ export interface GroupByQueryArgs {
|
||||
includeRecords?: boolean;
|
||||
selectedFields: CommonSelectedFields;
|
||||
limit?: number;
|
||||
offsetForRecords?: number;
|
||||
}
|
||||
export interface DestroyOneQueryArgs {
|
||||
id: string;
|
||||
|
||||
+32
@@ -1,3 +1,5 @@
|
||||
import { isNonEmptyString } from '@sniptt/guards';
|
||||
import { isDefined } from 'class-validator';
|
||||
import { type OrderByWithGroupBy } from 'twenty-shared/types';
|
||||
import { type FindOptionsWhere, type ObjectLiteral } from 'typeorm';
|
||||
|
||||
@@ -115,6 +117,36 @@ export class GraphqlQueryParser {
|
||||
return queryBuilder.orderBy(parsedOrderBys);
|
||||
}
|
||||
|
||||
public getOrderByRawSQL(
|
||||
orderBy: ObjectRecordOrderBy | OrderByWithGroupBy,
|
||||
objectNameSingular: string,
|
||||
isForwardPagination = true,
|
||||
): string {
|
||||
const parsedOrderBys = this.orderFieldParser.parse(
|
||||
orderBy as ObjectRecordOrderBy,
|
||||
objectNameSingular,
|
||||
isForwardPagination,
|
||||
);
|
||||
|
||||
const orderByRawSQLClauseArray = Object.entries(parsedOrderBys).map(
|
||||
([orderByField, orderByCondition]) => {
|
||||
const nullsCondition = isDefined(orderByCondition.nulls)
|
||||
? ` ${orderByCondition.nulls}`
|
||||
: '';
|
||||
|
||||
return `${orderByField} ${orderByCondition.order}${nullsCondition}`;
|
||||
},
|
||||
);
|
||||
|
||||
const orderByRawSQLString = orderByRawSQLClauseArray.join(', ');
|
||||
|
||||
const orderByCompleteSQLClause = isNonEmptyString(orderByRawSQLString)
|
||||
? `ORDER BY ${orderByRawSQLString}`
|
||||
: '';
|
||||
|
||||
return orderByCompleteSQLClause;
|
||||
}
|
||||
|
||||
public applyGroupByOrderToBuilder(
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
queryBuilder: WorkspaceSelectQueryBuilder<any>,
|
||||
|
||||
+73
-11
@@ -1,5 +1,6 @@
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
|
||||
import { isNonEmptyString } from '@sniptt/guards';
|
||||
import isEmpty from 'lodash.isempty';
|
||||
import { ObjectRecord } from 'twenty-shared/types';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
@@ -44,6 +45,7 @@ export class GroupByWithRecordsService {
|
||||
queryRunnerContext,
|
||||
orderByForRecords,
|
||||
groupLimit,
|
||||
offsetForRecords,
|
||||
}: {
|
||||
queryBuilderWithGroupBy: WorkspaceSelectQueryBuilder<ObjectLiteral>;
|
||||
queryBuilderWithFiltersAndWithoutGroupBy: WorkspaceSelectQueryBuilder<ObjectLiteral>;
|
||||
@@ -52,6 +54,7 @@ export class GroupByWithRecordsService {
|
||||
queryRunnerContext: CommonExtendedQueryRunnerContext;
|
||||
orderByForRecords: ObjectRecordOrderBy;
|
||||
groupLimit?: number;
|
||||
offsetForRecords?: number;
|
||||
}): Promise<CommonGroupByOutputItem[]> {
|
||||
const effectiveGroupLimit = getGroupLimit(groupLimit);
|
||||
|
||||
@@ -91,16 +94,21 @@ export class GroupByWithRecordsService {
|
||||
flatObjectMetadata,
|
||||
flatObjectMetadataMaps,
|
||||
flatFieldMetadataMaps,
|
||||
offsetForRecords,
|
||||
});
|
||||
|
||||
const recordsResult = await queryBuilderWithPartitionBy.getRawMany();
|
||||
|
||||
const allRecords = recordsResult
|
||||
.flatMap((group) => group.records)
|
||||
.filter(isDefined);
|
||||
|
||||
if (!isEmpty(selectedFieldsResult.relations)) {
|
||||
await this.processNestedRelationsHelper.processNestedRelations({
|
||||
flatObjectMetadataMaps,
|
||||
flatFieldMetadataMaps,
|
||||
parentObjectMetadataItem: flatObjectMetadata,
|
||||
parentObjectRecords: recordsResult.flatMap((group) => group.records),
|
||||
parentObjectRecords: allRecords,
|
||||
parentObjectRecordsAggregatedValues: {},
|
||||
relations: selectedFieldsResult.relations,
|
||||
aggregate: selectedFieldsResult.aggregate,
|
||||
@@ -141,6 +149,7 @@ export class GroupByWithRecordsService {
|
||||
flatObjectMetadata,
|
||||
flatObjectMetadataMaps,
|
||||
flatFieldMetadataMaps,
|
||||
offsetForRecords = 0,
|
||||
}: {
|
||||
queryBuilderForSubQuery: WorkspaceSelectQueryBuilder<ObjectLiteral>;
|
||||
columnsToSelect: Record<string, boolean>;
|
||||
@@ -151,11 +160,8 @@ export class GroupByWithRecordsService {
|
||||
flatObjectMetadata: FlatObjectMetadata;
|
||||
flatObjectMetadataMaps: FlatEntityMaps<FlatObjectMetadata>;
|
||||
flatFieldMetadataMaps: FlatEntityMaps<FlatFieldMetadata>;
|
||||
offsetForRecords?: number;
|
||||
}): WorkspaceSelectQueryBuilder<ObjectLiteral> {
|
||||
const groupByExpressions = groupByDefinitions
|
||||
.map((def) => def.expression)
|
||||
.join(', ');
|
||||
|
||||
const groupByAliases = groupByDefinitions
|
||||
.map((def) => `"${def.alias}"`)
|
||||
.join(', ');
|
||||
@@ -179,9 +185,17 @@ export class GroupByWithRecordsService {
|
||||
const subQuery = queryBuilderForSubQuery
|
||||
.select(recordSelectWithAlias)
|
||||
.addSelect(groupBySelectWithAlias)
|
||||
.addSelect(`ROW_NUMBER() OVER (PARTITION BY ${groupByExpressions})`, 'rn')
|
||||
.andWhere(groupConditions);
|
||||
|
||||
this.applyPartitionByToBuilder({
|
||||
groupByDefinitions,
|
||||
flatObjectMetadata,
|
||||
flatObjectMetadataMaps,
|
||||
flatFieldMetadataMaps,
|
||||
orderByForRecords,
|
||||
queryBuilder: subQuery,
|
||||
});
|
||||
|
||||
if (!isEmpty(orderByForRecords)) {
|
||||
const graphqlQueryParser = new GraphqlQueryParser(
|
||||
flatObjectMetadata,
|
||||
@@ -198,16 +212,17 @@ export class GroupByWithRecordsService {
|
||||
|
||||
let mainQueryQueryBuilder = repository.createQueryBuilder();
|
||||
|
||||
const pageStart = offsetForRecords;
|
||||
const pageEnd = offsetForRecords + RECORDS_PER_GROUP_LIMIT;
|
||||
|
||||
const mainQuery = mainQueryQueryBuilder
|
||||
.from(`(${subQuery.getQuery()})`, 'ranked_records')
|
||||
.setParameters(queryBuilderForSubQuery.expressionMap.parameters)
|
||||
.where('rn <= :recordsPerGroupLimit', {
|
||||
recordsPerGroupLimit: RECORDS_PER_GROUP_LIMIT,
|
||||
})
|
||||
|
||||
.select(groupByAliases)
|
||||
.addSelect(
|
||||
`JSON_AGG(
|
||||
CASE WHEN rn <= ${RECORDS_PER_GROUP_LIMIT} THEN
|
||||
CASE WHEN record_row_number > ${pageStart} AND record_row_number <= ${pageEnd} THEN
|
||||
JSON_BUILD_OBJECT(
|
||||
${[
|
||||
...Object.keys(columnsToSelect).map(
|
||||
@@ -219,7 +234,7 @@ export class GroupByWithRecordsService {
|
||||
].join(',\n ')}
|
||||
)
|
||||
END
|
||||
) FILTER (WHERE rn <= ${RECORDS_PER_GROUP_LIMIT})`,
|
||||
) FILTER (WHERE record_row_number > ${pageStart} AND record_row_number <= ${pageEnd})`,
|
||||
'records',
|
||||
)
|
||||
.groupBy(groupByAliases);
|
||||
@@ -232,6 +247,53 @@ export class GroupByWithRecordsService {
|
||||
return mainQuery as WorkspaceSelectQueryBuilder<ObjectLiteral>;
|
||||
}
|
||||
|
||||
private applyPartitionByToBuilder({
|
||||
groupByDefinitions,
|
||||
flatObjectMetadata,
|
||||
flatObjectMetadataMaps,
|
||||
flatFieldMetadataMaps,
|
||||
orderByForRecords,
|
||||
queryBuilder,
|
||||
}: {
|
||||
queryBuilder: WorkspaceSelectQueryBuilder<ObjectLiteral>;
|
||||
groupByDefinitions: GroupByDefinition[];
|
||||
orderByForRecords: ObjectRecordOrderBy;
|
||||
flatObjectMetadata: FlatObjectMetadata;
|
||||
flatObjectMetadataMaps: FlatEntityMaps<FlatObjectMetadata>;
|
||||
flatFieldMetadataMaps: FlatEntityMaps<FlatFieldMetadata>;
|
||||
}) {
|
||||
const groupByExpressions = groupByDefinitions
|
||||
.map((def) => def.expression)
|
||||
.join(', ');
|
||||
|
||||
const hasOrderByForRecords = !isEmpty(orderByForRecords);
|
||||
|
||||
if (hasOrderByForRecords) {
|
||||
const graphqlQueryParser = new GraphqlQueryParser(
|
||||
flatObjectMetadata,
|
||||
flatObjectMetadataMaps,
|
||||
flatFieldMetadataMaps,
|
||||
);
|
||||
|
||||
const orderByRawSQL = graphqlQueryParser.getOrderByRawSQL(
|
||||
orderByForRecords,
|
||||
flatObjectMetadata.nameSingular,
|
||||
);
|
||||
|
||||
if (isNonEmptyString(orderByRawSQL)) {
|
||||
return queryBuilder.addSelect(
|
||||
`ROW_NUMBER() OVER (PARTITION BY ${groupByExpressions} ${orderByRawSQL})`,
|
||||
'record_row_number',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return queryBuilder.addSelect(
|
||||
`ROW_NUMBER() OVER (PARTITION BY ${groupByExpressions})`,
|
||||
'record_row_number',
|
||||
);
|
||||
}
|
||||
|
||||
private buildGroupConditions(
|
||||
groupsResult: Array<Record<string, unknown>>,
|
||||
groupByDefinitions: GroupByDefinition[],
|
||||
|
||||
+1
@@ -64,6 +64,7 @@ export interface GroupByResolverArgs<Filter = ObjectRecordFilter> {
|
||||
viewId?: string;
|
||||
orderBy?: OrderByWithGroupBy;
|
||||
orderByForRecords?: ObjectRecordOrderBy;
|
||||
offsetForRecords?: number;
|
||||
limit?: number;
|
||||
}
|
||||
|
||||
|
||||
+4
@@ -190,6 +190,10 @@ export const getResolverArgs = (
|
||||
type: GraphQLInt,
|
||||
isNullable: true,
|
||||
},
|
||||
offsetForRecords: {
|
||||
type: GraphQLInt,
|
||||
isNullable: true,
|
||||
},
|
||||
};
|
||||
default:
|
||||
throw new Error(`Unknown resolver type: ${type}`);
|
||||
|
||||
Reference in New Issue
Block a user