fix: view group followup (#9162)

This PR fixes all followup that @Bonapara add on Discord.

- [x] When no group by is set, clicking on group by should open the
"field selection" menu
- [x] When closed, chevron should be "chevron-right" instead of
"chevron-up"
- [x] Sort : Add ability to switch from alphabetical to manual when
moving a option in sort alphabetical
- [x] Add subtext for group by and sort
- [x] Group by menu display bug
- [x] Changing the sort should not close the menu
- [x] Group by Activation -> shows empty state + is slow
- [x] Switching from Kanban view Settings to Table Options menu displays
an empty menu
- [x] Unnecessary spacing under groups
- [x] When no "select" are set on an object, redirect the user directly
to the new Select field page
- [x] Sort : Default should be manual
- [x] Hidding "no value" displays all options and remove the "hide empty
group" toggle
- [x] Hide Empty group option disappeared
- [x] Group by should not be persisted on "Locked/Main view" (**For now
we just disable the group by on main view**)
- [x] Hide Empty group should not be activated by default on
Opportunities Kanban view
- [ ] Animate the group opening/closing (**We'll be done later**)

Performance improvement:

https://github.com/user-attachments/assets/fd2acf66-0e56-45d0-8b2f-99c62e57d6f7

https://github.com/user-attachments/assets/80f1a2e1-9f77-4923-b85d-acb9cad96886

Also fix #9036

---------

Co-authored-by: Lucas Bordeau <bordeau.lucas@gmail.com>
This commit is contained in:
Jérémy M
2025-01-02 16:40:28 +01:00
committed by GitHub
parent 866c29e9ee
commit 0f1458cbe9
49 changed files with 676 additions and 320 deletions

View File

@ -330,7 +330,6 @@ export enum FeatureFlagKey {
IsSsoEnabled = 'IsSSOEnabled',
IsStripeIntegrationEnabled = 'IsStripeIntegrationEnabled',
IsUniqueIndexesEnabled = 'IsUniqueIndexesEnabled',
IsViewGroupsEnabled = 'IsViewGroupsEnabled',
IsWorkflowEnabled = 'IsWorkflowEnabled'
}

View File

@ -32,7 +32,7 @@ export const triggerAttachRelationOptimisticEffect = ({
id: targetRecordCacheId,
fields: {
[fieldNameOnTargetRecord]: (targetRecordFieldValue, { toReference }) => {
const fieldValueisObjectRecordConnectionWithRefs =
const fieldValueIsObjectRecordConnectionWithRefs =
isObjectRecordConnectionWithRefs(
sourceObjectNameSingular,
targetRecordFieldValue,
@ -47,7 +47,7 @@ export const triggerAttachRelationOptimisticEffect = ({
return targetRecordFieldValue;
}
if (fieldValueisObjectRecordConnectionWithRefs) {
if (fieldValueIsObjectRecordConnectionWithRefs) {
const nextEdges: RecordGqlRefEdge[] = [
...targetRecordFieldValue.edges,
{

View File

@ -34,6 +34,7 @@ export const ObjectOptionsDropdown = ({
clickableComponent={
<StyledHeaderDropdownButton>Options</StyledHeaderDropdownButton>
}
onClose={handleResetContent}
dropdownComponents={
<ObjectOptionsDropdownContext.Provider
value={{

View File

@ -25,6 +25,7 @@ import { useSetRecoilState } from 'recoil';
export const ObjectOptionsDropdownHiddenRecordGroupsContent = () => {
const {
viewType,
currentContentId,
recordIndexId,
objectMetadataItem,
@ -47,6 +48,7 @@ export const ObjectOptionsDropdownHiddenRecordGroupsContent = () => {
const { handleVisibilityChange: handleRecordGroupVisibilityChange } =
useRecordGroupVisibility({
viewBarId: recordIndexId,
viewType,
});
const viewGroupSettingsUrl = getSettingsPagePath(

View File

@ -30,8 +30,7 @@ import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
import { useGetCurrentView } from '@/views/hooks/useGetCurrentView';
import { ViewType } from '@/views/types/ViewType';
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
import { FeatureFlagKey } from '~/generated/graphql';
import { isDefined } from '~/utils/isDefined';
export const ObjectOptionsDropdownMenuContent = () => {
const {
@ -42,10 +41,6 @@ export const ObjectOptionsDropdownMenuContent = () => {
closeDropdown,
} = useOptionsDropdown();
const isViewGroupEnabled = useIsFeatureEnabled(
FeatureFlagKey.IsViewGroupsEnabled,
);
const { getIcon } = useIcons();
const { currentViewWithCombinedFiltersAndSorts: currentView } =
useGetCurrentView();
@ -120,9 +115,13 @@ export const ObjectOptionsDropdownMenuContent = () => {
contextualText={`${visibleBoardFields.length} shown`}
hasSubMenu
/>
{(viewType === ViewType.Kanban || isViewGroupEnabled) && (
{viewType === ViewType.Kanban && currentView?.key !== 'INDEX' && (
<MenuItem
onClick={() => onContentChange('recordGroups')}
onClick={() =>
isDefined(recordGroupFieldMetadata)
? onContentChange('recordGroups')
: onContentChange('recordGroupFields')
}
LeftIcon={IconLayoutList}
text="Group by"
contextualText={recordGroupFieldMetadata?.label}

View File

@ -26,6 +26,7 @@ import { navigationMemorizedUrlState } from '@/ui/navigation/states/navigationMe
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
import { useLocation } from 'react-router-dom';
import { useSetRecoilState } from 'recoil';
import { FieldMetadataType } from '~/generated-metadata/graphql';
import { isDefined } from '~/utils/isDefined';
export const ObjectOptionsDropdownRecordGroupFieldsContent = () => {
@ -36,6 +37,7 @@ export const ObjectOptionsDropdownRecordGroupFieldsContent = () => {
recordIndexId,
objectMetadataItem,
onContentChange,
resetContent,
closeDropdown,
} = useOptionsDropdown();
@ -47,7 +49,7 @@ export const ObjectOptionsDropdownRecordGroupFieldsContent = () => {
hiddenRecordGroupIdsComponentSelector,
);
const recordGroupFieldMetadataItem = useRecoilComponentValueV2(
const recordGroupFieldMetadata = useRecoilComponentValueV2(
recordGroupFieldMetadataComponentState,
);
@ -64,11 +66,14 @@ export const ObjectOptionsDropdownRecordGroupFieldsContent = () => {
viewBarComponentId: recordIndexId,
});
const newFieldSettingsUrl = getSettingsPagePath(
SettingsPath.ObjectNewFieldSelect,
const newSelectFieldSettingsUrl = getSettingsPagePath(
SettingsPath.ObjectNewFieldConfigure,
{
objectSlug: objectNamePlural,
},
{
fieldType: FieldMetadataType.Select,
},
);
const location = useLocation();
@ -101,7 +106,11 @@ export const ObjectOptionsDropdownRecordGroupFieldsContent = () => {
<>
<DropdownMenuHeader
StartIcon={IconChevronLeft}
onClick={() => onContentChange('recordGroups')}
onClick={() =>
isDefined(recordGroupFieldMetadata)
? onContentChange('recordGroups')
: resetContent()
}
>
Group by
</DropdownMenuHeader>
@ -114,13 +123,13 @@ export const ObjectOptionsDropdownRecordGroupFieldsContent = () => {
<DropdownMenuItemsContainer>
<MenuItemSelect
text="None"
selected={!isDefined(recordGroupFieldMetadataItem)}
selected={!isDefined(recordGroupFieldMetadata)}
onClick={handleResetRecordGroupField}
/>
{filteredRecordGroupFieldMetadataItems.map((fieldMetadataItem) => (
<MenuItemSelect
key={fieldMetadataItem.id}
selected={fieldMetadataItem.id === recordGroupFieldMetadataItem?.id}
selected={fieldMetadataItem.id === recordGroupFieldMetadata?.id}
onClick={() => handleRecordGroupFieldChange(fieldMetadataItem)}
LeftIcon={getIcon(fieldMetadataItem.icon)}
text={fieldMetadataItem.label}
@ -130,7 +139,7 @@ export const ObjectOptionsDropdownRecordGroupFieldsContent = () => {
<DropdownMenuSeparator />
<DropdownMenuItemsContainer>
<UndecoratedLink
to={newFieldSettingsUrl}
to={newSelectFieldSettingsUrl}
onClick={() => {
setNavigationMemorizedUrl(location.pathname + location.search);
closeDropdown();

View File

@ -17,8 +17,7 @@ import { useRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
export const ObjectOptionsDropdownRecordGroupSortContent = () => {
const { currentContentId, onContentChange, closeDropdown } =
useOptionsDropdown();
const { currentContentId, onContentChange } = useOptionsDropdown();
const hiddenRecordGroupIds = useRecoilComponentValueV2(
hiddenRecordGroupIdsComponentSelector,
@ -30,7 +29,6 @@ export const ObjectOptionsDropdownRecordGroupSortContent = () => {
const handleRecordGroupSortChange = (sort: RecordGroupSort) => {
setRecordGroupSort(sort);
closeDropdown();
};
useEffect(() => {

View File

@ -11,47 +11,54 @@ import {
} from 'twenty-ui';
import { useOptionsDropdown } from '@/object-record/object-options-dropdown/hooks/useOptionsDropdown';
import { RecordGroupReorderConfirmationModal } from '@/object-record/record-group/components/RecordGroupReorderConfirmationModal';
import { RecordGroupsVisibilityDropdownSection } from '@/object-record/record-group/components/RecordGroupsVisibilityDropdownSection';
import { useRecordGroupReorder } from '@/object-record/record-group/hooks/useRecordGroupReorder';
import { useRecordGroupReorderConfirmationModal } from '@/object-record/record-group/hooks/useRecordGroupReorderConfirmationModal';
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 { visibleRecordGroupIdsComponentSelector } from '@/object-record/record-group/states/selectors/visibleRecordGroupIdsComponentSelector';
import { recordIndexRecordGroupHideComponentState } from '@/object-record/record-index/states/recordIndexRecordGroupHideComponentState';
import { recordIndexRecordGroupIsDraggableSortComponentSelector } from '@/object-record/record-index/states/selectors/recordIndexRecordGroupIsDraggableSortComponentSelector';
import { visibleRecordGroupIdsComponentFamilySelector } from '@/object-record/record-group/states/selectors/visibleRecordGroupIdsComponentFamilySelector';
import { recordIndexRecordGroupHideComponentFamilyState } from '@/object-record/record-index/states/recordIndexRecordGroupHideComponentFamilyState';
import { recordIndexRecordGroupSortComponentState } from '@/object-record/record-index/states/recordIndexRecordGroupSortComponentState';
import { DropdownMenuHeader } from '@/ui/layout/dropdown/components/DropdownMenuHeader';
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownMenuSeparator';
import { useRecoilComponentFamilyValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentFamilyValueV2';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
import { FeatureFlagKey } from '~/generated/graphql';
import { useGetCurrentView } from '@/views/hooks/useGetCurrentView';
export const ObjectOptionsDropdownRecordGroupsContent = () => {
const isViewGroupEnabled = useIsFeatureEnabled(
FeatureFlagKey.IsViewGroupsEnabled,
);
const {
viewType,
currentContentId,
recordIndexId,
onContentChange,
resetContent,
} = useOptionsDropdown();
const { currentContentId, recordIndexId, onContentChange, resetContent } =
useOptionsDropdown();
const { currentViewWithCombinedFiltersAndSorts: currentView } =
useGetCurrentView();
const recordGroupFieldMetadata = useRecoilComponentValueV2(
recordGroupFieldMetadataComponentState,
);
const visibleRecordGroupIds = useRecoilComponentValueV2(
visibleRecordGroupIdsComponentSelector,
const visibleRecordGroupIds = useRecoilComponentFamilyValueV2(
visibleRecordGroupIdsComponentFamilySelector,
viewType,
);
const hiddenRecordGroupIds = useRecoilComponentValueV2(
hiddenRecordGroupIdsComponentSelector,
);
const isDragableSortRecordGroup = useRecoilComponentValueV2(
recordIndexRecordGroupIsDraggableSortComponentSelector,
const hideEmptyRecordGroup = useRecoilComponentFamilyValueV2(
recordIndexRecordGroupHideComponentFamilyState,
viewType,
);
const hideEmptyRecordGroup = useRecoilComponentValueV2(
recordIndexRecordGroupHideComponentState,
const recordGroupSort = useRecoilComponentValueV2(
recordIndexRecordGroupSortComponentState,
);
const {
@ -59,12 +66,16 @@ export const ObjectOptionsDropdownRecordGroupsContent = () => {
handleHideEmptyRecordGroupChange,
} = useRecordGroupVisibility({
viewBarId: recordIndexId,
viewType,
});
const { handleOrderChange: handleRecordGroupOrderChange } =
useRecordGroupReorder({
viewBarId: recordIndexId,
});
const {
handleRecordGroupOrderChangeWithModal,
handleRecordGroupReorderConfirmClick,
} = useRecordGroupReorderConfirmationModal({
recordIndexId,
viewType,
});
useEffect(() => {
if (
@ -81,22 +92,20 @@ export const ObjectOptionsDropdownRecordGroupsContent = () => {
Group by
</DropdownMenuHeader>
<DropdownMenuItemsContainer>
{isViewGroupEnabled && (
{currentView?.key !== 'INDEX' && (
<>
<MenuItem
onClick={() => onContentChange('recordGroupFields')}
LeftIcon={IconLayoutList}
text={
!recordGroupFieldMetadata
? 'Group by'
: `Group by "${recordGroupFieldMetadata.label}"`
}
text="Group by"
contextualText={recordGroupFieldMetadata?.label}
hasSubMenu
/>
<MenuItem
onClick={() => onContentChange('recordGroupSort')}
LeftIcon={IconSortDescending}
text="Sort"
contextualText={recordGroupSort}
hasSubMenu
/>
</>
@ -115,9 +124,9 @@ export const ObjectOptionsDropdownRecordGroupsContent = () => {
<RecordGroupsVisibilityDropdownSection
title="Visible groups"
recordGroupIds={visibleRecordGroupIds}
onDragEnd={handleRecordGroupOrderChange}
onDragEnd={handleRecordGroupOrderChangeWithModal}
onVisibilityChange={handleRecordGroupVisibilityChange}
isDraggable={isDragableSortRecordGroup}
isDraggable={true}
showDragGrip={true}
/>
</>
@ -134,6 +143,9 @@ export const ObjectOptionsDropdownRecordGroupsContent = () => {
</DropdownMenuItemsContainer>
</>
)}
<RecordGroupReorderConfirmationModal
onConfirmClick={handleRecordGroupReorderConfirmClick}
/>
</>
);
};

View File

@ -15,7 +15,7 @@ import { RecordBoardScope } from '@/object-record/record-board/scopes/RecordBoar
import { RecordBoardComponentInstanceContext } from '@/object-record/record-board/states/contexts/RecordBoardComponentInstanceContext';
import { getDraggedRecordPosition } from '@/object-record/record-board/utils/getDraggedRecordPosition';
import { recordGroupDefinitionFamilyState } from '@/object-record/record-group/states/recordGroupDefinitionFamilyState';
import { visibleRecordGroupIdsComponentSelector } from '@/object-record/record-group/states/selectors/visibleRecordGroupIdsComponentSelector';
import { visibleRecordGroupIdsComponentFamilySelector } from '@/object-record/record-group/states/selectors/visibleRecordGroupIdsComponentFamilySelector';
import { recordIndexRecordIdsByGroupComponentFamilyState } from '@/object-record/record-index/states/recordIndexRecordIdsByGroupComponentFamilyState';
import { recordIndexAllRecordIdsComponentSelector } from '@/object-record/record-index/states/selectors/recordIndexAllRecordIdsComponentSelector';
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
@ -26,8 +26,9 @@ import { useListenClickOutside } from '@/ui/utilities/pointer-event/hooks/useLis
import { getScopeIdFromComponentId } from '@/ui/utilities/recoil-scope/utils/getScopeIdFromComponentId';
import { ScrollWrapper } from '@/ui/utilities/scroll/components/ScrollWrapper';
import { useRecoilComponentCallbackStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackStateV2';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
import { useRecoilComponentFamilyValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentFamilyValueV2';
import { getSnapshotValue } from '@/ui/utilities/state/utils/getSnapshotValue';
import { ViewType } from '@/views/types/ViewType';
import { useScrollRestoration } from '~/hooks/useScrollRestoration';
const StyledContainer = styled.div`
@ -64,8 +65,9 @@ export const RecordBoard = () => {
useContext(RecordBoardContext);
const boardRef = useRef<HTMLDivElement>(null);
const visibleRecordGroupIds = useRecoilComponentValueV2(
visibleRecordGroupIdsComponentSelector,
const visibleRecordGroupIds = useRecoilComponentFamilyValueV2(
visibleRecordGroupIdsComponentFamilySelector,
ViewType.Kanban,
);
const recordIndexRecordIdsByGroupFamilyState =

View File

@ -1,6 +1,7 @@
import { RecordBoardColumnHeaderWrapper } from '@/object-record/record-board/record-board-column/components/RecordBoardColumnHeaderWrapper';
import { visibleRecordGroupIdsComponentSelector } from '@/object-record/record-group/states/selectors/visibleRecordGroupIdsComponentSelector';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
import { visibleRecordGroupIdsComponentFamilySelector } from '@/object-record/record-group/states/selectors/visibleRecordGroupIdsComponentFamilySelector';
import { useRecoilComponentFamilyValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentFamilyValueV2';
import { ViewType } from '@/views/types/ViewType';
import styled from '@emotion/styled';
const StyledHeaderContainer = styled.div`
@ -23,8 +24,9 @@ const StyledHeaderContainer = styled.div`
`;
export const RecordBoardHeader = () => {
const visibleRecordGroupIds = useRecoilComponentValueV2(
visibleRecordGroupIdsComponentSelector,
const visibleRecordGroupIds = useRecoilComponentFamilyValueV2(
visibleRecordGroupIdsComponentFamilySelector,
ViewType.Kanban,
);
return (

View File

@ -2,18 +2,19 @@ import { useRecoilCallback } from 'recoil';
import { recordGroupDefinitionFamilyState } from '@/object-record/record-group/states/recordGroupDefinitionFamilyState';
import { recordGroupFieldMetadataComponentState } from '@/object-record/record-group/states/recordGroupFieldMetadataComponentState';
import { visibleRecordGroupIdsComponentSelector } from '@/object-record/record-group/states/selectors/visibleRecordGroupIdsComponentSelector';
import { visibleRecordGroupIdsComponentFamilySelector } from '@/object-record/record-group/states/selectors/visibleRecordGroupIdsComponentFamilySelector';
import { recordIndexRecordIdsByGroupComponentFamilyState } from '@/object-record/record-index/states/recordIndexRecordIdsByGroupComponentFamilyState';
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
import { sortRecordsByPosition } from '@/object-record/utils/sortRecordsByPosition';
import { useRecoilComponentCallbackStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackStateV2';
import { getSnapshotValue } from '@/ui/utilities/state/utils/getSnapshotValue';
import { ViewType } from '@/views/types/ViewType';
import { isDeeplyEqual } from '~/utils/isDeeplyEqual';
import { isDefined } from '~/utils/isDefined';
export const useSetRecordBoardRecordIds = (recordBoardId?: string) => {
const visibleRecordGroupIdsSelector = useRecoilComponentCallbackStateV2(
visibleRecordGroupIdsComponentSelector,
const visibleRecordGroupIdsFamilySelector = useRecoilComponentCallbackStateV2(
visibleRecordGroupIdsComponentFamilySelector,
);
const recordGroupFieldMetadataState = useRecoilComponentCallbackStateV2(
@ -32,7 +33,7 @@ export const useSetRecordBoardRecordIds = (recordBoardId?: string) => {
(records: ObjectRecord[]) => {
const recordGroupIds = getSnapshotValue(
snapshot,
visibleRecordGroupIdsSelector,
visibleRecordGroupIdsFamilySelector(ViewType.Kanban),
);
for (const recordGroupId of recordGroupIds) {
@ -72,7 +73,7 @@ export const useSetRecordBoardRecordIds = (recordBoardId?: string) => {
}
},
[
visibleRecordGroupIdsSelector,
visibleRecordGroupIdsFamilySelector,
recordIndexRecordIdsByGroupFamilyState,
recordGroupFieldMetadataState,
],

View File

@ -4,9 +4,10 @@ import { useCallback, useRef } from 'react';
import { useRecordGroupActions } from '@/object-record/record-group/hooks/useRecordGroupActions';
import { DropdownMenu } from '@/ui/layout/dropdown/components/DropdownMenu';
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
import { useListenClickOutside } from '@/ui/utilities/pointer-event/hooks/useListenClickOutside';
import { MenuItem } from 'twenty-ui';
import { OverlayContainer } from '@/ui/layout/overlay/components/OverlayContainer';
import { useListenClickOutside } from '@/ui/utilities/pointer-event/hooks/useListenClickOutside';
import { ViewType } from '@/views/types/ViewType';
import { MenuItem } from 'twenty-ui';
const StyledMenuContainer = styled.div`
position: absolute;
@ -27,7 +28,9 @@ export const RecordBoardColumnDropdownMenu = ({
}: RecordBoardColumnDropdownMenuProps) => {
const boardColumnMenuRef = useRef<HTMLDivElement>(null);
const recordGroupActions = useRecordGroupActions();
const recordGroupActions = useRecordGroupActions({
viewType: ViewType.Kanban,
});
const closeMenu = useCallback(() => {
onClose();

View File

@ -0,0 +1,39 @@
import { isRecordGroupReorderConfirmationModalVisibleState } from '@/object-record/record-group/states/isRecordGroupReorderConfirmationModalVisibleState';
import { recordIndexRecordGroupSortComponentState } from '@/object-record/record-index/states/recordIndexRecordGroupSortComponentState';
import { ConfirmationModal } from '@/ui/layout/modal/components/ConfirmationModal';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
import { createPortal } from 'react-dom';
import { useRecoilState } from 'recoil';
type RecordGroupReorderConfirmationModalProps = {
onConfirmClick: () => void;
};
export const RecordGroupReorderConfirmationModal = ({
onConfirmClick,
}: RecordGroupReorderConfirmationModalProps) => {
const [
isRecordGroupReorderConfirmationModalVisible,
setIsRecordGroupReorderConfirmationModalVisible,
] = useRecoilState(isRecordGroupReorderConfirmationModalVisibleState);
const recordGroupSort = useRecoilComponentValueV2(
recordIndexRecordGroupSortComponentState,
);
if (!isRecordGroupReorderConfirmationModalVisible) {
return null;
}
return createPortal(
<ConfirmationModal
isOpen={isRecordGroupReorderConfirmationModalVisible}
setIsOpen={setIsRecordGroupReorderConfirmationModalVisible}
title="Group sorting"
subtitle={`Would you like to remove ${recordGroupSort} group sorting ?`}
onConfirmClick={onConfirmClick}
deleteButtonText="Remove"
/>,
document.body,
);
};

View File

@ -8,12 +8,19 @@ import { RecordGroupAction } from '@/object-record/record-group/types/RecordGrou
import { useRecordIndexContextOrThrow } from '@/object-record/record-index/contexts/RecordIndexContext';
import { navigationMemorizedUrlState } from '@/ui/navigation/states/navigationMemorizedUrlState';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
import { ViewType } from '@/views/types/ViewType';
import { useCallback, useContext, useMemo } from 'react';
import { useLocation, useNavigate } from 'react-router-dom';
import { useSetRecoilState } from 'recoil';
import { IconEyeOff, IconSettings, isDefined } from 'twenty-ui';
export const useRecordGroupActions = () => {
type UseRecordGroupActionsParams = {
viewType: ViewType;
};
export const useRecordGroupActions = ({
viewType,
}: UseRecordGroupActionsParams) => {
const navigate = useNavigate();
const location = useLocation();
@ -34,6 +41,7 @@ export const useRecordGroupActions = () => {
const { handleVisibilityChange: handleRecordGroupVisibilityChange } =
useRecordGroupVisibility({
viewBarId: recordIndexId,
viewType,
});
const setNavigationMemorizedUrl = useSetRecoilState(

View File

@ -2,11 +2,12 @@ import { OnDragEndResponder } from '@hello-pangea/dnd';
import { useSetRecordGroup } from '@/object-record/record-group/hooks/useSetRecordGroup';
import { recordGroupDefinitionFamilyState } from '@/object-record/record-group/states/recordGroupDefinitionFamilyState';
import { visibleRecordGroupIdsComponentSelector } from '@/object-record/record-group/states/selectors/visibleRecordGroupIdsComponentSelector';
import { visibleRecordGroupIdsComponentFamilySelector } from '@/object-record/record-group/states/selectors/visibleRecordGroupIdsComponentFamilySelector';
import { RecordGroupDefinition } from '@/object-record/record-group/types/RecordGroupDefinition';
import { useRecoilComponentCallbackStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackStateV2';
import { getSnapshotValue } from '@/ui/utilities/state/utils/getSnapshotValue';
import { useSaveCurrentViewGroups } from '@/views/hooks/useSaveCurrentViewGroups';
import { ViewType } from '@/views/types/ViewType';
import { mapRecordGroupDefinitionsToViewGroups } from '@/views/utils/mapRecordGroupDefinitionsToViewGroups';
import { useRecoilCallback } from 'recoil';
import { moveArrayItem } from '~/utils/array/moveArrayItem';
@ -15,15 +16,17 @@ import { isDefined } from '~/utils/isDefined';
type UseRecordGroupHandlersParams = {
viewBarId: string;
viewType: ViewType;
};
export const useRecordGroupReorder = ({
viewBarId,
viewType,
}: UseRecordGroupHandlersParams) => {
const setRecordGroup = useSetRecordGroup(viewBarId);
const visibleRecordGroupIdsSelector = useRecoilComponentCallbackStateV2(
visibleRecordGroupIdsComponentSelector,
const visibleRecordGroupIdsFamilySelector = useRecoilComponentCallbackStateV2(
visibleRecordGroupIdsComponentFamilySelector,
);
const { saveViewGroups } = useSaveCurrentViewGroups(viewBarId);
@ -37,7 +40,7 @@ export const useRecordGroupReorder = ({
const visibleRecordGroupIds = getSnapshotValue(
snapshot,
visibleRecordGroupIdsSelector,
visibleRecordGroupIdsFamilySelector(viewType),
);
const reorderedVisibleRecordGroupIds = moveArrayItem(
@ -80,7 +83,12 @@ export const useRecordGroupReorder = ({
mapRecordGroupDefinitionsToViewGroups(updatedRecordGroups),
);
},
[saveViewGroups, setRecordGroup, visibleRecordGroupIdsSelector],
[
saveViewGroups,
setRecordGroup,
viewType,
visibleRecordGroupIdsFamilySelector,
],
);
return {

View File

@ -0,0 +1,78 @@
import { useRecordGroupReorder } from '@/object-record/record-group/hooks/useRecordGroupReorder';
import { isRecordGroupReorderConfirmationModalVisibleState } from '@/object-record/record-group/states/isRecordGroupReorderConfirmationModalVisibleState';
import { RecordGroupSort } from '@/object-record/record-group/types/RecordGroupSort';
import { recordIndexRecordGroupSortComponentState } from '@/object-record/record-index/states/recordIndexRecordGroupSortComponentState';
import { recordIndexRecordGroupIsDraggableSortComponentSelector } from '@/object-record/record-index/states/selectors/recordIndexRecordGroupIsDraggableSortComponentSelector';
import { useGoBackToPreviousDropdownFocusId } from '@/ui/layout/dropdown/hooks/useGoBackToPreviousDropdownFocusId';
import { useSetActiveDropdownFocusIdAndMemorizePrevious } from '@/ui/layout/dropdown/hooks/useSetFocusedDropdownIdAndMemorizePrevious';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentStateV2';
import { ViewType } from '@/views/types/ViewType';
import { OnDragEndResponder } from '@hello-pangea/dnd';
import { useState } from 'react';
import { useSetRecoilState } from 'recoil';
type UseRecordGroupReorderConfirmationModalParams = {
recordIndexId: string;
viewType: ViewType;
};
export const useRecordGroupReorderConfirmationModal = ({
recordIndexId,
viewType,
}: UseRecordGroupReorderConfirmationModalParams) => {
const { setActiveDropdownFocusIdAndMemorizePrevious } =
useSetActiveDropdownFocusIdAndMemorizePrevious();
const { goBackToPreviousDropdownFocusId } =
useGoBackToPreviousDropdownFocusId();
const setIsRecordGroupReorderConfirmationModalVisible = useSetRecoilState(
isRecordGroupReorderConfirmationModalVisibleState,
);
const [pendingDragEndReorder, setPendingDragEndReorder] =
useState<Parameters<OnDragEndResponder> | null>(null);
const { handleOrderChange: handleRecordGroupOrderChange } =
useRecordGroupReorder({
viewBarId: recordIndexId,
viewType,
});
const isDragableSortRecordGroup = useRecoilComponentValueV2(
recordIndexRecordGroupIsDraggableSortComponentSelector,
);
const setRecordGroupSort = useSetRecoilComponentStateV2(
recordIndexRecordGroupSortComponentState,
);
const handleRecordGroupOrderChangeWithModal: OnDragEndResponder = (
result,
provided,
) => {
if (!isDragableSortRecordGroup) {
setIsRecordGroupReorderConfirmationModalVisible(true);
setActiveDropdownFocusIdAndMemorizePrevious(null);
setPendingDragEndReorder([result, provided]);
} else {
handleRecordGroupOrderChange(result, provided);
}
};
const handleConfirmClick = () => {
if (!pendingDragEndReorder) {
throw new Error('pendingDragEndReorder is not set');
}
setRecordGroupSort(RecordGroupSort.Manual);
setPendingDragEndReorder(null);
handleRecordGroupOrderChange(...pendingDragEndReorder);
goBackToPreviousDropdownFocusId();
};
return {
handleRecordGroupOrderChangeWithModal,
handleRecordGroupReorderConfirmClick: handleConfirmClick,
};
};

View File

@ -1,20 +1,25 @@
import { recordGroupDefinitionFamilyState } from '@/object-record/record-group/states/recordGroupDefinitionFamilyState';
import { RecordGroupDefinition } from '@/object-record/record-group/types/RecordGroupDefinition';
import { recordIndexRecordGroupHideComponentState } from '@/object-record/record-index/states/recordIndexRecordGroupHideComponentState';
import { recordIndexRecordGroupHideComponentFamilyState } from '@/object-record/record-index/states/recordIndexRecordGroupHideComponentFamilyState';
import { useRecoilComponentCallbackStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackStateV2';
import { useSaveCurrentViewGroups } from '@/views/hooks/useSaveCurrentViewGroups';
import { ViewType } from '@/views/types/ViewType';
import { recordGroupDefinitionToViewGroup } from '@/views/utils/recordGroupDefinitionToViewGroup';
import { useRecoilCallback } from 'recoil';
type UseRecordGroupVisibilityParams = {
viewBarId: string;
viewType: ViewType;
};
export const useRecordGroupVisibility = ({
viewBarId,
viewType,
}: UseRecordGroupVisibilityParams) => {
const objectOptionsDropdownRecordGroupHideState =
useRecoilComponentCallbackStateV2(recordIndexRecordGroupHideComponentState);
const objectOptionsDropdownRecordGroupHideFamilyState =
useRecoilComponentCallbackStateV2(
recordIndexRecordGroupHideComponentFamilyState,
);
const { saveViewGroup } = useSaveCurrentViewGroups(viewBarId);
@ -27,22 +32,19 @@ export const useRecordGroupVisibility = ({
);
saveViewGroup(recordGroupDefinitionToViewGroup(updatedRecordGroup));
// If visibility is manually toggled, we should reset the hideEmptyRecordGroup state
set(objectOptionsDropdownRecordGroupHideState, false);
},
[saveViewGroup, objectOptionsDropdownRecordGroupHideState],
[saveViewGroup],
);
const handleHideEmptyRecordGroupChange = useRecoilCallback(
({ set }) =>
async () => {
set(
objectOptionsDropdownRecordGroupHideState,
objectOptionsDropdownRecordGroupHideFamilyState(viewType),
(currentHideState) => !currentHideState,
);
},
[objectOptionsDropdownRecordGroupHideState],
[viewType, objectOptionsDropdownRecordGroupHideFamilyState],
);
return {

View File

@ -0,0 +1,6 @@
import { atom } from 'recoil';
export const isRecordGroupReorderConfirmationModalVisibleState = atom<boolean>({
key: 'isRecordGroupReorderConfirmationModalVisibleState',
default: false,
});

View File

@ -0,0 +1,8 @@
import { OnDragEndResponder } from '@hello-pangea/dnd';
import { atom } from 'recoil';
export const recordGroupPendingDragEndReorderState =
atom<Parameters<OnDragEndResponder> | null>({
key: 'recordGroupPendingDragEndReorderState',
default: null,
});

View File

@ -0,0 +1,50 @@
import { recordGroupDefinitionFamilyState } from '@/object-record/record-group/states/recordGroupDefinitionFamilyState';
import { recordGroupIdsComponentState } from '@/object-record/record-group/states/recordGroupIdsComponentState';
import {
RecordGroupDefinition,
RecordGroupDefinitionType,
} from '@/object-record/record-group/types/RecordGroupDefinition';
import { recordGroupSortedInsert } from '@/object-record/record-group/utils/recordGroupSortedInsert';
import { createComponentSelectorV2 } from '@/ui/utilities/state/component-state/utils/createComponentSelectorV2';
import { ViewComponentInstanceContext } from '@/views/states/contexts/ViewComponentInstanceContext';
import { isDefined } from '~/utils/isDefined';
export const availableRecordGroupIdsComponentSelector =
createComponentSelectorV2<RecordGroupDefinition['id'][]>({
key: 'availableRecordGroupIdsComponentSelector',
componentInstanceContext: ViewComponentInstanceContext,
get:
({ instanceId }) =>
({ get }) => {
const recordGroupIds = get(
recordGroupIdsComponentState.atomFamily({
instanceId,
}),
);
const result: RecordGroupDefinition[] = [];
for (const recordGroupId of recordGroupIds) {
const recordGroupDefinition = get(
recordGroupDefinitionFamilyState(recordGroupId),
);
if (!isDefined(recordGroupDefinition)) {
continue;
}
if (
recordGroupDefinition.type === RecordGroupDefinitionType.NoValue
) {
continue;
}
recordGroupSortedInsert(result, recordGroupDefinition, (a, b) =>
a.title.localeCompare(b.title),
);
}
return result.map(({ id }) => id);
},
});

View File

@ -0,0 +1,84 @@
import { recordGroupDefinitionFamilyState } from '@/object-record/record-group/states/recordGroupDefinitionFamilyState';
import { recordGroupIdsComponentState } from '@/object-record/record-group/states/recordGroupIdsComponentState';
import { RecordGroupDefinition } from '@/object-record/record-group/types/RecordGroupDefinition';
import { RecordGroupSort } from '@/object-record/record-group/types/RecordGroupSort';
import { recordGroupSortedInsert } from '@/object-record/record-group/utils/recordGroupSortedInsert';
import { recordIndexRecordGroupHideComponentFamilyState } from '@/object-record/record-index/states/recordIndexRecordGroupHideComponentFamilyState';
import { recordIndexRecordGroupSortComponentState } from '@/object-record/record-index/states/recordIndexRecordGroupSortComponentState';
import { recordIndexRecordIdsByGroupComponentFamilyState } from '@/object-record/record-index/states/recordIndexRecordIdsByGroupComponentFamilyState';
import { createComponentFamilySelectorV2 } from '@/ui/utilities/state/component-state/utils/createComponentFamilySelectorV2';
import { ViewComponentInstanceContext } from '@/views/states/contexts/ViewComponentInstanceContext';
import { ViewType } from '@/views/types/ViewType';
import { isDefined } from '~/utils/isDefined';
export const visibleRecordGroupIdsComponentFamilySelector =
createComponentFamilySelectorV2<RecordGroupDefinition['id'][], ViewType>({
key: 'visibleRecordGroupIdsComponentFamilySelector',
componentInstanceContext: ViewComponentInstanceContext,
get:
({ instanceId, familyKey }) =>
({ get }) => {
const recordGroupSort = get(
recordIndexRecordGroupSortComponentState.atomFamily({
instanceId,
}),
);
const recordGroupIds = get(
recordGroupIdsComponentState.atomFamily({
instanceId,
}),
);
const hideEmptyRecordGroup = get(
recordIndexRecordGroupHideComponentFamilyState.atomFamily({
instanceId,
familyKey,
}),
);
const result: RecordGroupDefinition[] = [];
const comparator = (
a: RecordGroupDefinition,
b: RecordGroupDefinition,
) => {
switch (recordGroupSort) {
case RecordGroupSort.Alphabetical:
return a.title.localeCompare(b.title);
case RecordGroupSort.ReverseAlphabetical:
return b.title.localeCompare(a.title);
case RecordGroupSort.Manual:
default:
return a.position - b.position;
}
};
for (const recordGroupId of recordGroupIds) {
const recordGroupDefinition = get(
recordGroupDefinitionFamilyState(recordGroupId),
);
const recordIds = get(
recordIndexRecordIdsByGroupComponentFamilyState.atomFamily({
instanceId,
familyKey: recordGroupId,
}),
);
if (!isDefined(recordGroupDefinition)) {
continue;
}
if (hideEmptyRecordGroup && recordIds.length === 0) {
continue;
}
if (!recordGroupDefinition.isVisible) {
continue;
}
recordGroupSortedInsert(result, recordGroupDefinition, comparator);
}
return result.map(({ id }) => id);
},
});

View File

@ -1,83 +0,0 @@
import { recordGroupDefinitionFamilyState } from '@/object-record/record-group/states/recordGroupDefinitionFamilyState';
import { recordGroupIdsComponentState } from '@/object-record/record-group/states/recordGroupIdsComponentState';
import { RecordGroupDefinition } from '@/object-record/record-group/types/RecordGroupDefinition';
import { RecordGroupSort } from '@/object-record/record-group/types/RecordGroupSort';
import { recordGroupSortedInsert } from '@/object-record/record-group/utils/recordGroupSortedInsert';
import { recordIndexRecordGroupHideComponentState } from '@/object-record/record-index/states/recordIndexRecordGroupHideComponentState';
import { recordIndexRecordGroupSortComponentState } from '@/object-record/record-index/states/recordIndexRecordGroupSortComponentState';
import { recordIndexRecordIdsByGroupComponentFamilyState } from '@/object-record/record-index/states/recordIndexRecordIdsByGroupComponentFamilyState';
import { createComponentSelectorV2 } from '@/ui/utilities/state/component-state/utils/createComponentSelectorV2';
import { ViewComponentInstanceContext } from '@/views/states/contexts/ViewComponentInstanceContext';
import { isDefined } from '~/utils/isDefined';
export const visibleRecordGroupIdsComponentSelector = createComponentSelectorV2<
RecordGroupDefinition['id'][]
>({
key: 'visibleRecordGroupIdsComponentSelector',
componentInstanceContext: ViewComponentInstanceContext,
get:
({ instanceId }) =>
({ get }) => {
const recordGroupSort = get(
recordIndexRecordGroupSortComponentState.atomFamily({
instanceId,
}),
);
const recordGroupIds = get(
recordGroupIdsComponentState.atomFamily({
instanceId,
}),
);
const hideEmptyRecordGroup = get(
recordIndexRecordGroupHideComponentState.atomFamily({
instanceId,
}),
);
const result: RecordGroupDefinition[] = [];
const comparator = (
a: RecordGroupDefinition,
b: RecordGroupDefinition,
) => {
switch (recordGroupSort) {
case RecordGroupSort.Alphabetical:
return a.title.localeCompare(b.title);
case RecordGroupSort.ReverseAlphabetical:
return b.title.localeCompare(a.title);
case RecordGroupSort.Manual:
default:
return a.position - b.position;
}
};
for (const recordGroupId of recordGroupIds) {
const recordGroupDefinition = get(
recordGroupDefinitionFamilyState(recordGroupId),
);
const recordIds = get(
recordIndexRecordIdsByGroupComponentFamilyState.atomFamily({
instanceId,
familyKey: recordGroupId,
}),
);
if (!isDefined(recordGroupDefinition)) {
continue;
}
if (hideEmptyRecordGroup && recordIds.length === 0) {
continue;
}
if (!recordGroupDefinition.isVisible) {
continue;
}
recordGroupSortedInsert(result, recordGroupDefinition, comparator);
}
return result.map(({ id }) => id);
},
});

View File

@ -1,5 +1,5 @@
import { recordGroupFieldMetadataComponentState } from '@/object-record/record-group/states/recordGroupFieldMetadataComponentState';
import { visibleRecordGroupIdsComponentSelector } from '@/object-record/record-group/states/selectors/visibleRecordGroupIdsComponentSelector';
import { availableRecordGroupIdsComponentSelector } from '@/object-record/record-group/states/selectors/availableRecordGroupIdsComponentSelector';
import { RecordGroupDefinition } from '@/object-record/record-group/types/RecordGroupDefinition';
import { RecordIndexPageKanbanAddMenuItem } from '@/object-record/record-index/components/RecordIndexPageKanbanAddMenuItem';
import { useRecordIndexContextOrThrow } from '@/object-record/record-index/contexts/RecordIndexContext';
@ -8,18 +8,27 @@ import { isRecordGroupTableSectionToggledComponentState } from '@/object-record/
import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown';
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown';
import { PageAddButton } from '@/ui/layout/page/components/PageAddButton';
import { useSetActiveDropdownFocusIdAndMemorizePrevious } from '@/ui/layout/dropdown/hooks/useSetFocusedDropdownIdAndMemorizePrevious';
import { useRecoilComponentCallbackStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackStateV2';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
import { useRecoilCallback } from 'recoil';
export const RecordIndexPageTableAddButtonInGroup = () => {
const dropdownId = `record-index-page-table-add-button-dropdown`;
type RecordIndexAddRecordInGroupDropdownProps = {
dropdownId: string;
clickableComponent: React.ReactNode;
};
export const RecordIndexAddRecordInGroupDropdown = ({
dropdownId,
clickableComponent,
}: RecordIndexAddRecordInGroupDropdownProps) => {
const { objectMetadataItem } = useRecordIndexContextOrThrow();
const visibleRecordGroupIds = useRecoilComponentValueV2(
visibleRecordGroupIdsComponentSelector,
const { setActiveDropdownFocusIdAndMemorizePrevious } =
useSetActiveDropdownFocusIdAndMemorizePrevious();
const recordGroupIds = useRecoilComponentValueV2(
availableRecordGroupIdsComponentSelector,
);
const recordGroupFieldMetadata = useRecoilComponentValueV2(
@ -44,11 +53,13 @@ export const RecordIndexPageTableAddButtonInGroup = () => {
(recordGroup: RecordGroupDefinition) => {
set(isRecordGroupTableSectionToggledState(recordGroup.id), true);
createNewTableRecordInGroup(recordGroup.id);
setActiveDropdownFocusIdAndMemorizePrevious(null);
closeDropdown();
},
[
closeDropdown,
createNewTableRecordInGroup,
setActiveDropdownFocusIdAndMemorizePrevious,
isRecordGroupTableSectionToggledState,
],
);
@ -61,11 +72,11 @@ export const RecordIndexPageTableAddButtonInGroup = () => {
<Dropdown
dropdownMenuWidth="200px"
dropdownPlacement="bottom-start"
clickableComponent={<PageAddButton />}
clickableComponent={clickableComponent}
dropdownId={dropdownId}
dropdownComponents={
<DropdownMenuItemsContainer>
{visibleRecordGroupIds.map((recordGroupId) => (
{recordGroupIds.map((recordGroupId) => (
<RecordIndexPageKanbanAddMenuItem
key={recordGroupId}
columnId={recordGroupId}

View File

@ -2,7 +2,7 @@ import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSi
import { useAddNewCard } from '@/object-record/record-board/record-board-column/hooks/useAddNewCard';
import { useIsOpportunitiesCompanyFieldDisabled } from '@/object-record/record-board/record-board-column/hooks/useIsOpportunitiesCompanyFieldDisabled';
import { recordBoardVisibleFieldDefinitionsComponentSelector } from '@/object-record/record-board/states/selectors/recordBoardVisibleFieldDefinitionsComponentSelector';
import { visibleRecordGroupIdsComponentSelector } from '@/object-record/record-group/states/selectors/visibleRecordGroupIdsComponentSelector';
import { visibleRecordGroupIdsComponentFamilySelector } from '@/object-record/record-group/states/selectors/visibleRecordGroupIdsComponentFamilySelector';
import { RecordGroupDefinition } from '@/object-record/record-group/types/RecordGroupDefinition';
import { RecordIndexPageKanbanAddMenuItem } from '@/object-record/record-index/components/RecordIndexPageKanbanAddMenuItem';
import { useRecordIndexContextOrThrow } from '@/object-record/record-index/contexts/RecordIndexContext';
@ -11,7 +11,9 @@ import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown';
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown';
import { PageAddButton } from '@/ui/layout/page/components/PageAddButton';
import { useRecoilComponentFamilyValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentFamilyValueV2';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
import { ViewType } from '@/views/types/ViewType';
import { useCallback } from 'react';
import { useRecoilValue } from 'recoil';
@ -20,8 +22,9 @@ export const RecordIndexPageKanbanAddButton = () => {
const { recordIndexId, objectMetadataItem } = useRecordIndexContextOrThrow();
const visibleRecordGroupIds = useRecoilComponentValueV2(
visibleRecordGroupIdsComponentSelector,
const visibleRecordGroupIds = useRecoilComponentFamilyValueV2(
visibleRecordGroupIdsComponentFamilySelector,
ViewType.Kanban,
);
const recordIndexKanbanFieldMetadataId = useRecoilValue(

View File

@ -1,6 +1,7 @@
import { hasRecordGroupsComponentSelector } from '@/object-record/record-group/states/selectors/hasRecordGroupsComponentSelector';
import { RecordIndexPageTableAddButtonInGroup } from '@/object-record/record-index/components/RecordIndexPageTableAddButtonInGroup';
import { RecordIndexAddRecordInGroupDropdown } from '@/object-record/record-index/components/RecordIndexAddRecordInGroupDropdown';
import { RecordIndexPageTableAddButtonNoGroup } from '@/object-record/record-index/components/RecordIndexPageTableAddButtonNoGroup';
import { PageAddButton } from '@/ui/layout/page/components/PageAddButton';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
export const RecordIndexPageTableAddButton = () => {
@ -12,5 +13,10 @@ export const RecordIndexPageTableAddButton = () => {
return <RecordIndexPageTableAddButtonNoGroup />;
}
return <RecordIndexPageTableAddButtonInGroup />;
return (
<RecordIndexAddRecordInGroupDropdown
dropdownId="record-index-page-table-add-button-dropdown"
clickableComponent={<PageAddButton />}
/>
);
};

View File

@ -61,6 +61,8 @@ export const useHandleRecordGroupField = ({
(option) =>
!existingGroupKeys.has(`${fieldMetadataItem.id}:${option.value}`),
)
// Alphabetically sort the options by default
.sort((a, b) => a.value.localeCompare(b.value))
.map(
(option, index) =>
({

View File

@ -0,0 +1,19 @@
import { createComponentFamilyStateV2 } from '@/ui/utilities/state/component-state/utils/createComponentFamilyStateV2';
import { ViewComponentInstanceContext } from '@/views/states/contexts/ViewComponentInstanceContext';
import { ViewType } from '@/views/types/ViewType';
export const recordIndexRecordGroupHideComponentFamilyState =
createComponentFamilyStateV2<boolean, ViewType>({
key: 'recordIndexRecordGroupHideComponentFamilyState',
defaultValue: ({ familyKey }) => {
switch (familyKey) {
case ViewType.Kanban:
return false;
case ViewType.Table:
return true;
default:
return false;
}
},
componentInstanceContext: ViewComponentInstanceContext,
});

View File

@ -1,9 +0,0 @@
import { createComponentStateV2 } from '@/ui/utilities/state/component-state/utils/createComponentStateV2';
import { ViewComponentInstanceContext } from '@/views/states/contexts/ViewComponentInstanceContext';
export const recordIndexRecordGroupHideComponentState =
createComponentStateV2<boolean>({
key: 'recordIndexRecordGroupHideComponentState',
defaultValue: false,
componentInstanceContext: ViewComponentInstanceContext,
});

View File

@ -1,5 +1,5 @@
import styled from '@emotion/styled';
import { isNonEmptyString, isNull } from '@sniptt/guards';
import { isNonEmptyString } from '@sniptt/guards';
import { hasRecordGroupsComponentSelector } from '@/object-record/record-group/states/selectors/hasRecordGroupsComponentSelector';
import { recordIndexAllRecordIdsComponentSelector } from '@/object-record/record-index/states/selectors/recordIndexAllRecordIdsComponentSelector';
@ -16,7 +16,7 @@ import { RecordTableRecordGroupsBody } from '@/object-record/record-table/record
import { RecordTableAggregateFooter } from '@/object-record/record-table/record-table-footer/components/RecordTableAggregateFooter';
import { RecordTableHeader } from '@/object-record/record-table/record-table-header/components/RecordTableHeader';
import { isRecordTableInitialLoadingComponentState } from '@/object-record/record-table/states/isRecordTableInitialLoadingComponentState';
import { recordTablePendingRecordIdComponentState } from '@/object-record/record-table/states/recordTablePendingRecordIdComponentState';
import { hasPendingRecordComponentSelector } from '@/object-record/record-table/states/selectors/hasPendingRecordComponentSelector';
import { DragSelect } from '@/ui/utilities/drag-select/components/DragSelect';
import { useClickOutsideListener } from '@/ui/utilities/pointer-event/hooks/useClickOutsideListener';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
@ -54,8 +54,8 @@ export const RecordTable = () => {
recordTableId,
);
const pendingRecordId = useRecoilComponentValueV2(
recordTablePendingRecordIdComponentState,
const hasPendingRecord = useRecoilComponentValueV2(
hasPendingRecordComponentSelector,
recordTableId,
);
@ -67,7 +67,7 @@ export const RecordTable = () => {
const recordTableIsEmpty =
!isRecordTableInitialLoading &&
allRecordIds.length === 0 &&
isNull(pendingRecordId);
!hasPendingRecord;
const { resetTableRowSelection, setRowSelected } = useRecordTable({
recordTableId,

View File

@ -1,6 +1,8 @@
import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords';
import { hasRecordGroupsComponentSelector } from '@/object-record/record-group/states/selectors/hasRecordGroupsComponentSelector';
import { useRecordTableContextOrThrow } from '@/object-record/record-table/contexts/RecordTableContext';
import { RecordTableEmptyStateNoRecordAtAll } from '@/object-record/record-table/empty-state/components/RecordTableEmptyStateNoRecordAtAll';
import { RecordTableEmptyStateByGroupNoRecordAtAll } from '@/object-record/record-table/empty-state/components/RecordTableEmptyStateByGroupNoRecordAtAll';
import { RecordTableEmptyStateNoGroupNoRecordAtAll } from '@/object-record/record-table/empty-state/components/RecordTableEmptyStateNoGroupNoRecordAtAll';
import { RecordTableEmptyStateNoRecordFoundForFilter } from '@/object-record/record-table/empty-state/components/RecordTableEmptyStateNoRecordFoundForFilter';
import { RecordTableEmptyStateRemote } from '@/object-record/record-table/empty-state/components/RecordTableEmptyStateRemote';
import { RecordTableEmptyStateSoftDelete } from '@/object-record/record-table/empty-state/components/RecordTableEmptyStateSoftDelete';
@ -11,6 +13,10 @@ export const RecordTableEmptyState = () => {
const { recordTableId, objectNameSingular, objectMetadataItem } =
useRecordTableContextOrThrow();
const hasRecordGroups = useRecoilComponentValueV2(
hasRecordGroupsComponentSelector,
);
const { totalCount } = useFindManyRecords({ objectNameSingular, limit: 1 });
const noRecordAtAll = totalCount === 0;
@ -26,7 +32,11 @@ export const RecordTableEmptyState = () => {
} else if (isSoftDeleteActive === true) {
return <RecordTableEmptyStateSoftDelete />;
} else if (noRecordAtAll) {
return <RecordTableEmptyStateNoRecordAtAll />;
if (hasRecordGroups) {
return <RecordTableEmptyStateByGroupNoRecordAtAll />;
}
return <RecordTableEmptyStateNoGroupNoRecordAtAll />;
} else {
return <RecordTableEmptyStateNoRecordFoundForFilter />;
}

View File

@ -0,0 +1,52 @@
import { Button, IconPlus } from 'twenty-ui';
import { useObjectLabel } from '@/object-metadata/hooks/useObjectLabel';
import { RecordIndexAddRecordInGroupDropdown } from '@/object-record/record-index/components/RecordIndexAddRecordInGroupDropdown';
import { recordIndexRecordGroupHideComponentFamilyState } from '@/object-record/record-index/states/recordIndexRecordGroupHideComponentFamilyState';
import { useRecordTableContextOrThrow } from '@/object-record/record-table/contexts/RecordTableContext';
import { RecordTableEmptyStateDisplay } from '@/object-record/record-table/empty-state/components/RecordTableEmptyStateDisplay';
import { useSetRecoilComponentFamilyStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentFamilyStateV2';
import { ViewType } from '@/views/types/ViewType';
export const RecordTableEmptyStateByGroupNoRecordAtAll = () => {
const { objectMetadataItem } = useRecordTableContextOrThrow();
const setHideEmptyRecordGroup = useSetRecoilComponentFamilyStateV2(
recordIndexRecordGroupHideComponentFamilyState,
ViewType.Table,
);
const objectLabel = useObjectLabel(objectMetadataItem);
const buttonTitle = `Add a ${objectLabel}`;
const title = `Add your first ${objectLabel}`;
const subTitle = `Use our API or add your first ${objectLabel} manually`;
const handleButtonClick = () => {
// When we have no records in the group, we want to show the empty state
setHideEmptyRecordGroup(false);
};
return (
<RecordTableEmptyStateDisplay
title={title}
subTitle={subTitle}
animatedPlaceholderType="noRecord"
buttonComponent={
<RecordIndexAddRecordInGroupDropdown
dropdownId="record-table-empty-state-add-button-dropdown"
clickableComponent={
<Button
Icon={IconPlus}
title={buttonTitle}
variant={'secondary'}
onClick={handleButtonClick}
/>
}
/>
}
/>
);
};

View File

@ -11,41 +11,49 @@ import {
IconComponent,
} from 'twenty-ui';
type RecordTableEmptyStateDisplayProps = {
animatedPlaceholderType: AnimatedPlaceholderType;
title: string;
subTitle: string;
Icon: IconComponent;
type RecordTableEmptyStateDisplayButtonComponentProps = {
buttonComponent?: React.ReactNode;
};
type RecordTableEmptyStateDisplayButtonProps = {
ButtonIcon: IconComponent;
buttonTitle: string;
onClick: () => void;
};
export const RecordTableEmptyStateDisplay = ({
Icon,
animatedPlaceholderType,
buttonTitle,
onClick,
subTitle,
title,
}: RecordTableEmptyStateDisplayProps) => {
type RecordTableEmptyStateDisplayProps = {
animatedPlaceholderType: AnimatedPlaceholderType;
title: string;
subTitle: string;
} & (
| RecordTableEmptyStateDisplayButtonComponentProps
| RecordTableEmptyStateDisplayButtonProps
);
export const RecordTableEmptyStateDisplay = (
props: RecordTableEmptyStateDisplayProps,
) => {
const { objectMetadataItem } = useRecordTableContextOrThrow();
const isReadOnly = isObjectMetadataReadOnly(objectMetadataItem);
return (
<AnimatedPlaceholderEmptyContainer>
<AnimatedPlaceholder type={animatedPlaceholderType} />
<AnimatedPlaceholder type={props.animatedPlaceholderType} />
<AnimatedPlaceholderEmptyTextContainer>
<AnimatedPlaceholderEmptyTitle>{title}</AnimatedPlaceholderEmptyTitle>
<AnimatedPlaceholderEmptyTitle>
{props.title}
</AnimatedPlaceholderEmptyTitle>
<AnimatedPlaceholderEmptySubTitle>
{subTitle}
{props.subTitle}
</AnimatedPlaceholderEmptySubTitle>
</AnimatedPlaceholderEmptyTextContainer>
{!isReadOnly && (
{'buttonComponent' in props && props.buttonComponent}
{'buttonTitle' in props && !isReadOnly && (
<Button
Icon={Icon}
title={buttonTitle}
Icon={props.ButtonIcon}
title={props.buttonTitle}
variant={'secondary'}
onClick={onClick}
onClick={props.onClick}
/>
)}
</AnimatedPlaceholderEmptyContainer>

View File

@ -5,7 +5,7 @@ import { useRecordTableContextOrThrow } from '@/object-record/record-table/conte
import { RecordTableEmptyStateDisplay } from '@/object-record/record-table/empty-state/components/RecordTableEmptyStateDisplay';
import { useCreateNewTableRecord } from '@/object-record/record-table/hooks/useCreateNewTableRecords';
export const RecordTableEmptyStateNoRecordAtAll = () => {
export const RecordTableEmptyStateNoGroupNoRecordAtAll = () => {
const { objectMetadataItem, recordTableId } = useRecordTableContextOrThrow();
const { createNewTableRecord } = useCreateNewTableRecord(recordTableId);
@ -27,7 +27,7 @@ export const RecordTableEmptyStateNoRecordAtAll = () => {
buttonTitle={buttonTitle}
subTitle={subTitle}
title={title}
Icon={IconPlus}
ButtonIcon={IconPlus}
animatedPlaceholderType="noRecord"
onClick={handleButtonClick}
/>

View File

@ -27,7 +27,7 @@ export const RecordTableEmptyStateNoRecordFoundForFilter = () => {
buttonTitle={buttonTitle}
subTitle={subTitle}
title={title}
Icon={IconPlus}
ButtonIcon={IconPlus}
animatedPlaceholderType="noMatchRecord"
onClick={handleButtonClick}
/>

View File

@ -16,7 +16,7 @@ export const RecordTableEmptyStateRemote = () => {
buttonTitle={'Go to Settings'}
subTitle={'If this is unexpected, please verify your settings.'}
title={'No Data Available for Remote Table'}
Icon={IconSettings}
ButtonIcon={IconSettings}
animatedPlaceholderType="noRecord"
onClick={handleButtonClick}
/>

View File

@ -43,7 +43,7 @@ export const RecordTableEmptyStateSoftDelete = () => {
buttonTitle={'Remove Deleted filter'}
subTitle={'No deleted records matching the filter criteria were found.'}
title={`No Deleted ${objectLabel} found`}
Icon={IconFilterOff}
ButtonIcon={IconFilterOff}
animatedPlaceholderType="noDeletedRecord"
onClick={handleButtonClick}
/>

View File

@ -1,7 +1,7 @@
import { Meta, StoryObj } from '@storybook/react';
import { RecordTableComponentInstance } from '@/object-record/record-table/components/RecordTableComponentInstance';
import { RecordTableEmptyStateNoRecordAtAll } from '@/object-record/record-table/empty-state/components/RecordTableEmptyStateNoRecordAtAll';
import { RecordTableEmptyStateNoGroupNoRecordAtAll } from '@/object-record/record-table/empty-state/components/RecordTableEmptyStateNoGroupNoRecordAtAll';
import { SnackBarProviderScope } from '@/ui/feedback/snack-bar-manager/scopes/SnackBarProviderScope';
import { ComponentDecorator } from 'twenty-ui';
import { MemoryRouterDecorator } from '~/testing/decorators/MemoryRouterDecorator';
@ -10,8 +10,9 @@ import { RecordTableDecorator } from '~/testing/decorators/RecordTableDecorator'
import { graphqlMocks } from '~/testing/graphqlMocks';
const meta: Meta = {
title: 'Modules/ObjectRecord/RecordTable/RecordTableEmptyStateNoRecordAtAll',
component: RecordTableEmptyStateNoRecordAtAll,
title:
'Modules/ObjectRecord/RecordTable/RecordTableEmptyStateNoGroupNoRecordAtAll',
component: RecordTableEmptyStateNoGroupNoRecordAtAll,
decorators: [
ComponentDecorator,
MemoryRouterDecorator,
@ -34,6 +35,6 @@ const meta: Meta = {
};
export default meta;
type Story = StoryObj<typeof RecordTableEmptyStateNoRecordAtAll>;
type Story = StoryObj<typeof RecordTableEmptyStateNoGroupNoRecordAtAll>;
export const Default: Story = {};

View File

@ -1,5 +1,5 @@
import { RecordGroupContext } from '@/object-record/record-group/states/context/RecordGroupContext';
import { visibleRecordGroupIdsComponentSelector } from '@/object-record/record-group/states/selectors/visibleRecordGroupIdsComponentSelector';
import { visibleRecordGroupIdsComponentFamilySelector } from '@/object-record/record-group/states/selectors/visibleRecordGroupIdsComponentFamilySelector';
import { recordIndexAllRecordIdsComponentSelector } from '@/object-record/record-index/states/selectors/recordIndexAllRecordIdsComponentSelector';
import { RecordTableRecordGroupBodyContextProvider } from '@/object-record/record-table/components/RecordTableRecordGroupBodyContextProvider';
import { RecordTableRecordGroupRows } from '@/object-record/record-table/components/RecordTableRecordGroupRows';
@ -7,10 +7,11 @@ import { RecordTableBodyDroppable } from '@/object-record/record-table/record-ta
import { RecordTableBodyLoading } from '@/object-record/record-table/record-table-body/components/RecordTableBodyLoading';
import { RecordTableBodyRecordGroupDragDropContextProvider } from '@/object-record/record-table/record-table-body/components/RecordTableBodyRecordGroupDragDropContextProvider';
import { RecordTableAggregateFooter } from '@/object-record/record-table/record-table-footer/components/RecordTableAggregateFooter';
import { RecordTableRecordGroupEmptyRow } from '@/object-record/record-table/record-table-section/components/RecordTableRecordGroupEmptyRow';
import { RecordTableRecordGroupSection } from '@/object-record/record-table/record-table-section/components/RecordTableRecordGroupSection';
import { isRecordTableInitialLoadingComponentState } from '@/object-record/record-table/states/isRecordTableInitialLoadingComponentState';
import { useRecoilComponentFamilyValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentFamilyValueV2';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
import { ViewType } from '@/views/types/ViewType';
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
import { FeatureFlagKey } from '~/generated/graphql';
@ -26,8 +27,9 @@ export const RecordTableRecordGroupsBody = () => {
isRecordTableInitialLoadingComponentState,
);
const visibleRecordGroupIds = useRecoilComponentValueV2(
visibleRecordGroupIdsComponentSelector,
const visibleRecordGroupIds = useRecoilComponentFamilyValueV2(
visibleRecordGroupIdsComponentFamilySelector,
ViewType.Table,
);
if (isRecordTableInitialLoading && allRecordIds.length === 0) {
@ -37,14 +39,13 @@ export const RecordTableRecordGroupsBody = () => {
return (
<>
<RecordTableBodyRecordGroupDragDropContextProvider>
{visibleRecordGroupIds.map((recordGroupId, index) => (
{visibleRecordGroupIds.map((recordGroupId) => (
<RecordTableRecordGroupBodyContextProvider
key={recordGroupId}
recordGroupId={recordGroupId}
>
<RecordGroupContext.Provider value={{ recordGroupId }}>
<RecordTableBodyDroppable recordGroupId={recordGroupId}>
{index > 0 && <RecordTableRecordGroupEmptyRow />}
<RecordTableRecordGroupSection />
<RecordTableRecordGroupRows />
</RecordTableBodyDroppable>

View File

@ -1,7 +0,0 @@
import styled from '@emotion/styled';
const StyledTrContainer = styled.tr`
height: 32px;
`;
export const RecordTableRecordGroupEmptyRow = StyledTrContainer;

View File

@ -2,7 +2,7 @@ import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import { motion } from 'framer-motion';
import { useCallback } from 'react';
import { IconChevronUp, isDefined, Tag } from 'twenty-ui';
import { IconChevronDown, isDefined, Tag } from 'twenty-ui';
import { useCurrentRecordGroupId } from '@/object-record/record-group/hooks/useCurrentRecordGroupId';
import { recordGroupDefinitionFamilyState } from '@/object-record/record-group/states/recordGroupDefinitionFamilyState';
@ -83,13 +83,13 @@ export const RecordTableRecordGroupSection = () => {
<td aria-hidden />
<StyledChevronContainer>
<motion.span
animate={{ rotate: isRecordGroupTableSectionToggled ? 180 : 0 }}
animate={{ rotate: !isRecordGroupTableSectionToggled ? -90 : 0 }}
transition={{ duration: theme.animation.duration.normal }}
style={{
display: 'inline-block',
}}
>
<IconChevronUp size={theme.icon.size.md} />
<IconChevronDown size={theme.icon.size.md} />
</motion.span>
</StyledChevronContainer>
<StyledRecordGroupSection>

View File

@ -0,0 +1,46 @@
import { recordGroupIdsComponentState } from '@/object-record/record-group/states/recordGroupIdsComponentState';
import { hasRecordGroupsComponentSelector } from '@/object-record/record-group/states/selectors/hasRecordGroupsComponentSelector';
import { RecordTableComponentInstanceContext } from '@/object-record/record-table/states/context/RecordTableComponentInstanceContext';
import { recordTablePendingRecordIdByGroupComponentFamilyState } from '@/object-record/record-table/states/recordTablePendingRecordIdByGroupComponentFamilyState';
import { recordTablePendingRecordIdComponentState } from '@/object-record/record-table/states/recordTablePendingRecordIdComponentState';
import { createComponentSelectorV2 } from '@/ui/utilities/state/component-state/utils/createComponentSelectorV2';
import { isDefined } from '~/utils/isDefined';
export const hasPendingRecordComponentSelector = createComponentSelectorV2({
key: 'hasPendingRecordComponentSelector',
componentInstanceContext: RecordTableComponentInstanceContext,
get:
({ instanceId }) =>
({ get }) => {
const hasRecordGroups = get(
hasRecordGroupsComponentSelector.selectorFamily({ instanceId }),
);
if (!hasRecordGroups) {
const pendingRecordId = get(
recordTablePendingRecordIdComponentState.atomFamily({ instanceId }),
);
return !isDefined(pendingRecordId);
}
const recordGroupIds = get(
recordGroupIdsComponentState.atomFamily({ instanceId }),
);
for (const recordGroupId of recordGroupIds) {
const pendingRecordId = get(
recordTablePendingRecordIdByGroupComponentFamilyState.atomFamily({
instanceId,
familyKey: recordGroupId,
}),
);
if (!isDefined(pendingRecordId)) {
return true;
}
}
return false;
},
});

View File

@ -11,6 +11,7 @@ type Params<V extends string> = {
export const getSettingsPagePath = <Path extends SettingsPath>(
path: Path,
params?: Params<Path>,
searchParams?: Record<string, string>,
) => {
let resultPath = `/settings/${path}`;
@ -26,5 +27,11 @@ export const getSettingsPagePath = <Path extends SettingsPath>(
resultPath = `${resultPath}/${params?.id}`;
}
if (isDefined(searchParams)) {
const searchParamsString = new URLSearchParams(searchParams).toString();
resultPath = `${resultPath}?${searchParamsString}`;
}
return resultPath;
};

View File

@ -71,6 +71,7 @@ export const DropdownMenuItemsContainer = ({
<ScrollWrapper
contextProviderName="dropdownMenuItemsContainer"
componentInstanceId={`scroll-wrapper-dropdown-menu-${id}`}
heightMode="fit-content"
>
<StyledDropdownMenuItemsExternalContainer
hasMaxHeight={hasMaxHeight}

View File

@ -6,7 +6,7 @@ import { previousDropdownFocusIdState } from '@/ui/layout/dropdown/states/previo
export const useSetActiveDropdownFocusIdAndMemorizePrevious = () => {
const setActiveDropdownFocusIdAndMemorizePrevious = useRecoilCallback(
({ snapshot, set }) =>
(dropdownId: string) => {
(dropdownId: string | null) => {
const focusedDropdownId = snapshot
.getLoadable(activeDropdownFocusIdState)
.getValue();

View File

@ -142,7 +142,9 @@ export const ConfirmationModal = ({
</Section>
)}
<StyledCenteredButton
onClick={() => setIsOpen(false)}
onClick={() => {
setIsOpen(false);
}}
variant="secondary"
title="Cancel"
fullWidth

View File

@ -15,9 +15,21 @@ import { scrollWrapperScrollTopComponentState } from '@/ui/utilities/scroll/stat
import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentStateV2';
import 'overlayscrollbars/overlayscrollbars.css';
const StyledScrollWrapper = styled.div<{ scrollHide?: boolean }>`
type HeightMode = 'full' | 'fit-content';
const StyledScrollWrapper = styled.div<{
scrollHide?: boolean;
heightMode: HeightMode;
}>`
display: flex;
height: 100%;
height: ${({ heightMode }) => {
switch (heightMode) {
case 'full':
return '100%';
case 'fit-content':
return 'fit-content';
}
}};
width: 100%;
.os-scrollbar-handle {
@ -33,6 +45,7 @@ const StyledInnerContainer = styled.div`
export type ScrollWrapperProps = {
children: React.ReactNode;
className?: string;
heightMode?: HeightMode;
defaultEnableXScroll?: boolean;
defaultEnableYScroll?: boolean;
contextProviderName: ContextProviderName;
@ -44,6 +57,7 @@ export const ScrollWrapper = ({
componentInstanceId,
children,
className,
heightMode = 'full',
defaultEnableXScroll = true,
defaultEnableYScroll = true,
contextProviderName,
@ -164,6 +178,7 @@ export const ScrollWrapper = ({
ref={scrollableRef}
className={className}
scrollHide={scrollHide}
heightMode={heightMode}
>
<StyledInnerContainer>{children}</StyledInnerContainer>
</StyledScrollWrapper>

View File

@ -2,7 +2,14 @@ import { ComponentFamilyStateKeyV2 } from '@/ui/utilities/state/component-state/
import { ComponentFamilyStateV2 } from '@/ui/utilities/state/component-state/types/ComponentFamilyStateV2';
import { ComponentInstanceStateContext } from '@/ui/utilities/state/component-state/types/ComponentInstanceStateContext';
import { globalComponentInstanceContextMap } from '@/ui/utilities/state/component-state/utils/globalComponentInstanceContextMap';
import { AtomEffect, atomFamily, SerializableParam } from 'recoil';
import {
AtomEffect,
atomFamily,
Loadable,
RecoilValue,
SerializableParam,
WrappedValue,
} from 'recoil';
import { isDefined } from 'twenty-ui';
@ -11,7 +18,16 @@ type CreateComponentFamilyStateArgs<
FamilyKey extends SerializableParam,
> = {
key: string;
defaultValue: ValueType;
defaultValue:
| ValueType
| ((
param: ComponentFamilyStateKeyV2<FamilyKey>,
) =>
| ValueType
| RecoilValue<ValueType>
| Promise<ValueType>
| Loadable<ValueType>
| WrappedValue<ValueType>);
componentInstanceContext: ComponentInstanceStateContext<any> | null;
effects?:
| AtomEffect<ValueType>[]

View File

@ -1,77 +1,43 @@
import { useApolloClient } from '@apollo/client';
import { useCallback } from 'react';
import { v4 } from 'uuid';
import { triggerCreateRecordsOptimisticEffect } from '@/apollo/optimistic-effect/utils/triggerCreateRecordsOptimisticEffect';
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { useCreateOneRecordMutation } from '@/object-record/hooks/useCreateOneRecordMutation';
import { useDeleteOneRecordMutation } from '@/object-record/hooks/useDeleteOneRecordMutation';
import { useCreateManyRecords } from '@/object-record/hooks/useCreateManyRecords';
import { useDestroyManyRecords } from '@/object-record/hooks/useDestroyManyRecords';
import { useUpdateOneRecordMutation } from '@/object-record/hooks/useUpdateOneRecordMutation';
import { GraphQLView } from '@/views/types/GraphQLView';
import { ViewGroup } from '@/views/types/ViewGroup';
import { useApolloClient } from '@apollo/client';
export const usePersistViewGroupRecords = () => {
const { objectMetadataItem } = useObjectMetadataItem({
objectNameSingular: CoreObjectNameSingular.ViewGroup,
});
const apolloClient = useApolloClient();
const { createOneRecordMutation } = useCreateOneRecordMutation({
const { createManyRecords } = useCreateManyRecords({
objectNameSingular: CoreObjectNameSingular.ViewGroup,
shouldMatchRootQueryFilter: true,
});
const { updateOneRecordMutation } = useUpdateOneRecordMutation({
objectNameSingular: CoreObjectNameSingular.ViewGroup,
});
const { deleteOneRecordMutation } = useDeleteOneRecordMutation({
const { destroyManyRecords } = useDestroyManyRecords({
objectNameSingular: CoreObjectNameSingular.ViewGroup,
});
const { objectMetadataItems } = useObjectMetadataItems();
const apolloClient = useApolloClient();
const createViewGroupRecords = useCallback(
(viewGroupsToCreate: ViewGroup[], view: GraphQLView) => {
if (!viewGroupsToCreate.length) return;
return Promise.all(
viewGroupsToCreate.map((viewGroup) =>
apolloClient.mutate({
mutation: createOneRecordMutation,
variables: {
input: {
fieldMetadataId: viewGroup.fieldMetadataId,
viewId: view.id,
isVisible: viewGroup.isVisible,
position: viewGroup.position,
id: v4(),
fieldValue: viewGroup.fieldValue,
},
},
update: (cache, { data }) => {
const record = data?.['createViewGroup'];
if (!record) return;
triggerCreateRecordsOptimisticEffect({
cache,
objectMetadataItem,
recordsToCreate: [record],
objectMetadataItems,
});
},
}),
),
return createManyRecords(
viewGroupsToCreate.map((viewGroup) => ({
...viewGroup,
view: {
id: view.id,
},
})),
);
},
[
apolloClient,
createOneRecordMutation,
objectMetadataItem,
objectMetadataItems,
],
[createManyRecords],
);
const updateViewGroupRecords = useCallback(
@ -95,7 +61,8 @@ export const usePersistViewGroupRecords = () => {
const mutationResults = await Promise.all(mutationPromises);
// FixMe: Using triggerCreateRecordsOptimisticEffect is actaully causing multiple records to be created
// FixMe: Using useUpdateOneRecord hook that call triggerUpdateRecordsOptimisticEffect is actaully causing multiple records to be created
// This is a temporary fix
mutationResults.forEach(({ data }) => {
const record = data?.['updateViewGroup'];
@ -120,33 +87,11 @@ export const usePersistViewGroupRecords = () => {
async (viewGroupsToDelete: ViewGroup[]) => {
if (!viewGroupsToDelete.length) return;
const mutationPromises = viewGroupsToDelete.map((viewGroup) =>
apolloClient.mutate<{ deleteViewGroup: ViewGroup }>({
mutation: deleteOneRecordMutation,
variables: {
idToDelete: viewGroup.id,
},
// Avoid cache being updated with stale data
fetchPolicy: 'no-cache',
}),
return destroyManyRecords(
viewGroupsToDelete.map((viewGroup) => viewGroup.id),
);
const mutationResults = await Promise.all(mutationPromises);
mutationResults.forEach(({ data }) => {
const record = data?.['deleteViewGroup'];
if (!record) return;
apolloClient.cache.evict({
id: apolloClient.cache.identify({
__typename: 'ViewGroup',
id: record.id,
}),
});
});
},
[apolloClient, deleteOneRecordMutation],
[destroyManyRecords],
);
return {

View File

@ -14,7 +14,6 @@ export enum FeatureFlagKey {
IsMicrosoftSyncEnabled = 'IS_MICROSOFT_SYNC_ENABLED',
IsAdvancedFiltersEnabled = 'IS_ADVANCED_FILTERS_ENABLED',
IsAggregateQueryEnabled = 'IS_AGGREGATE_QUERY_ENABLED',
IsViewGroupsEnabled = 'IS_VIEW_GROUPS_ENABLED',
IsPageHeaderV2Enabled = 'IS_PAGE_HEADER_V2_ENABLED',
IsCrmMigrationEnabled = 'IS_CRM_MIGRATION_ENABLED',
IsJsonFilterEnabled = 'IS_JSON_FILTER_ENABLED',