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', IsSsoEnabled = 'IsSSOEnabled',
IsStripeIntegrationEnabled = 'IsStripeIntegrationEnabled', IsStripeIntegrationEnabled = 'IsStripeIntegrationEnabled',
IsUniqueIndexesEnabled = 'IsUniqueIndexesEnabled', IsUniqueIndexesEnabled = 'IsUniqueIndexesEnabled',
IsViewGroupsEnabled = 'IsViewGroupsEnabled',
IsWorkflowEnabled = 'IsWorkflowEnabled' IsWorkflowEnabled = 'IsWorkflowEnabled'
} }

View File

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

View File

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

View File

@ -25,6 +25,7 @@ import { useSetRecoilState } from 'recoil';
export const ObjectOptionsDropdownHiddenRecordGroupsContent = () => { export const ObjectOptionsDropdownHiddenRecordGroupsContent = () => {
const { const {
viewType,
currentContentId, currentContentId,
recordIndexId, recordIndexId,
objectMetadataItem, objectMetadataItem,
@ -47,6 +48,7 @@ export const ObjectOptionsDropdownHiddenRecordGroupsContent = () => {
const { handleVisibilityChange: handleRecordGroupVisibilityChange } = const { handleVisibilityChange: handleRecordGroupVisibilityChange } =
useRecordGroupVisibility({ useRecordGroupVisibility({
viewBarId: recordIndexId, viewBarId: recordIndexId,
viewType,
}); });
const viewGroupSettingsUrl = getSettingsPagePath( 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 { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
import { useGetCurrentView } from '@/views/hooks/useGetCurrentView'; import { useGetCurrentView } from '@/views/hooks/useGetCurrentView';
import { ViewType } from '@/views/types/ViewType'; import { ViewType } from '@/views/types/ViewType';
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled'; import { isDefined } from '~/utils/isDefined';
import { FeatureFlagKey } from '~/generated/graphql';
export const ObjectOptionsDropdownMenuContent = () => { export const ObjectOptionsDropdownMenuContent = () => {
const { const {
@ -42,10 +41,6 @@ export const ObjectOptionsDropdownMenuContent = () => {
closeDropdown, closeDropdown,
} = useOptionsDropdown(); } = useOptionsDropdown();
const isViewGroupEnabled = useIsFeatureEnabled(
FeatureFlagKey.IsViewGroupsEnabled,
);
const { getIcon } = useIcons(); const { getIcon } = useIcons();
const { currentViewWithCombinedFiltersAndSorts: currentView } = const { currentViewWithCombinedFiltersAndSorts: currentView } =
useGetCurrentView(); useGetCurrentView();
@ -120,9 +115,13 @@ export const ObjectOptionsDropdownMenuContent = () => {
contextualText={`${visibleBoardFields.length} shown`} contextualText={`${visibleBoardFields.length} shown`}
hasSubMenu hasSubMenu
/> />
{(viewType === ViewType.Kanban || isViewGroupEnabled) && ( {viewType === ViewType.Kanban && currentView?.key !== 'INDEX' && (
<MenuItem <MenuItem
onClick={() => onContentChange('recordGroups')} onClick={() =>
isDefined(recordGroupFieldMetadata)
? onContentChange('recordGroups')
: onContentChange('recordGroupFields')
}
LeftIcon={IconLayoutList} LeftIcon={IconLayoutList}
text="Group by" text="Group by"
contextualText={recordGroupFieldMetadata?.label} 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 { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
import { useLocation } from 'react-router-dom'; import { useLocation } from 'react-router-dom';
import { useSetRecoilState } from 'recoil'; import { useSetRecoilState } from 'recoil';
import { FieldMetadataType } from '~/generated-metadata/graphql';
import { isDefined } from '~/utils/isDefined'; import { isDefined } from '~/utils/isDefined';
export const ObjectOptionsDropdownRecordGroupFieldsContent = () => { export const ObjectOptionsDropdownRecordGroupFieldsContent = () => {
@ -36,6 +37,7 @@ export const ObjectOptionsDropdownRecordGroupFieldsContent = () => {
recordIndexId, recordIndexId,
objectMetadataItem, objectMetadataItem,
onContentChange, onContentChange,
resetContent,
closeDropdown, closeDropdown,
} = useOptionsDropdown(); } = useOptionsDropdown();
@ -47,7 +49,7 @@ export const ObjectOptionsDropdownRecordGroupFieldsContent = () => {
hiddenRecordGroupIdsComponentSelector, hiddenRecordGroupIdsComponentSelector,
); );
const recordGroupFieldMetadataItem = useRecoilComponentValueV2( const recordGroupFieldMetadata = useRecoilComponentValueV2(
recordGroupFieldMetadataComponentState, recordGroupFieldMetadataComponentState,
); );
@ -64,11 +66,14 @@ export const ObjectOptionsDropdownRecordGroupFieldsContent = () => {
viewBarComponentId: recordIndexId, viewBarComponentId: recordIndexId,
}); });
const newFieldSettingsUrl = getSettingsPagePath( const newSelectFieldSettingsUrl = getSettingsPagePath(
SettingsPath.ObjectNewFieldSelect, SettingsPath.ObjectNewFieldConfigure,
{ {
objectSlug: objectNamePlural, objectSlug: objectNamePlural,
}, },
{
fieldType: FieldMetadataType.Select,
},
); );
const location = useLocation(); const location = useLocation();
@ -101,7 +106,11 @@ export const ObjectOptionsDropdownRecordGroupFieldsContent = () => {
<> <>
<DropdownMenuHeader <DropdownMenuHeader
StartIcon={IconChevronLeft} StartIcon={IconChevronLeft}
onClick={() => onContentChange('recordGroups')} onClick={() =>
isDefined(recordGroupFieldMetadata)
? onContentChange('recordGroups')
: resetContent()
}
> >
Group by Group by
</DropdownMenuHeader> </DropdownMenuHeader>
@ -114,13 +123,13 @@ export const ObjectOptionsDropdownRecordGroupFieldsContent = () => {
<DropdownMenuItemsContainer> <DropdownMenuItemsContainer>
<MenuItemSelect <MenuItemSelect
text="None" text="None"
selected={!isDefined(recordGroupFieldMetadataItem)} selected={!isDefined(recordGroupFieldMetadata)}
onClick={handleResetRecordGroupField} onClick={handleResetRecordGroupField}
/> />
{filteredRecordGroupFieldMetadataItems.map((fieldMetadataItem) => ( {filteredRecordGroupFieldMetadataItems.map((fieldMetadataItem) => (
<MenuItemSelect <MenuItemSelect
key={fieldMetadataItem.id} key={fieldMetadataItem.id}
selected={fieldMetadataItem.id === recordGroupFieldMetadataItem?.id} selected={fieldMetadataItem.id === recordGroupFieldMetadata?.id}
onClick={() => handleRecordGroupFieldChange(fieldMetadataItem)} onClick={() => handleRecordGroupFieldChange(fieldMetadataItem)}
LeftIcon={getIcon(fieldMetadataItem.icon)} LeftIcon={getIcon(fieldMetadataItem.icon)}
text={fieldMetadataItem.label} text={fieldMetadataItem.label}
@ -130,7 +139,7 @@ export const ObjectOptionsDropdownRecordGroupFieldsContent = () => {
<DropdownMenuSeparator /> <DropdownMenuSeparator />
<DropdownMenuItemsContainer> <DropdownMenuItemsContainer>
<UndecoratedLink <UndecoratedLink
to={newFieldSettingsUrl} to={newSelectFieldSettingsUrl}
onClick={() => { onClick={() => {
setNavigationMemorizedUrl(location.pathname + location.search); setNavigationMemorizedUrl(location.pathname + location.search);
closeDropdown(); 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'; import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
export const ObjectOptionsDropdownRecordGroupSortContent = () => { export const ObjectOptionsDropdownRecordGroupSortContent = () => {
const { currentContentId, onContentChange, closeDropdown } = const { currentContentId, onContentChange } = useOptionsDropdown();
useOptionsDropdown();
const hiddenRecordGroupIds = useRecoilComponentValueV2( const hiddenRecordGroupIds = useRecoilComponentValueV2(
hiddenRecordGroupIdsComponentSelector, hiddenRecordGroupIdsComponentSelector,
@ -30,7 +29,6 @@ export const ObjectOptionsDropdownRecordGroupSortContent = () => {
const handleRecordGroupSortChange = (sort: RecordGroupSort) => { const handleRecordGroupSortChange = (sort: RecordGroupSort) => {
setRecordGroupSort(sort); setRecordGroupSort(sort);
closeDropdown();
}; };
useEffect(() => { useEffect(() => {

View File

@ -11,47 +11,54 @@ import {
} from 'twenty-ui'; } from 'twenty-ui';
import { useOptionsDropdown } from '@/object-record/object-options-dropdown/hooks/useOptionsDropdown'; 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 { 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 { useRecordGroupVisibility } from '@/object-record/record-group/hooks/useRecordGroupVisibility';
import { recordGroupFieldMetadataComponentState } from '@/object-record/record-group/states/recordGroupFieldMetadataComponentState'; import { recordGroupFieldMetadataComponentState } from '@/object-record/record-group/states/recordGroupFieldMetadataComponentState';
import { hiddenRecordGroupIdsComponentSelector } from '@/object-record/record-group/states/selectors/hiddenRecordGroupIdsComponentSelector'; import { hiddenRecordGroupIdsComponentSelector } from '@/object-record/record-group/states/selectors/hiddenRecordGroupIdsComponentSelector';
import { visibleRecordGroupIdsComponentSelector } from '@/object-record/record-group/states/selectors/visibleRecordGroupIdsComponentSelector'; import { visibleRecordGroupIdsComponentFamilySelector } from '@/object-record/record-group/states/selectors/visibleRecordGroupIdsComponentFamilySelector';
import { recordIndexRecordGroupHideComponentState } from '@/object-record/record-index/states/recordIndexRecordGroupHideComponentState'; import { recordIndexRecordGroupHideComponentFamilyState } from '@/object-record/record-index/states/recordIndexRecordGroupHideComponentFamilyState';
import { recordIndexRecordGroupIsDraggableSortComponentSelector } from '@/object-record/record-index/states/selectors/recordIndexRecordGroupIsDraggableSortComponentSelector'; import { recordIndexRecordGroupSortComponentState } from '@/object-record/record-index/states/recordIndexRecordGroupSortComponentState';
import { DropdownMenuHeader } from '@/ui/layout/dropdown/components/DropdownMenuHeader'; import { DropdownMenuHeader } from '@/ui/layout/dropdown/components/DropdownMenuHeader';
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer'; import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownMenuSeparator'; 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 { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled'; import { useGetCurrentView } from '@/views/hooks/useGetCurrentView';
import { FeatureFlagKey } from '~/generated/graphql';
export const ObjectOptionsDropdownRecordGroupsContent = () => { export const ObjectOptionsDropdownRecordGroupsContent = () => {
const isViewGroupEnabled = useIsFeatureEnabled( const {
FeatureFlagKey.IsViewGroupsEnabled, viewType,
); currentContentId,
recordIndexId,
onContentChange,
resetContent,
} = useOptionsDropdown();
const { currentContentId, recordIndexId, onContentChange, resetContent } = const { currentViewWithCombinedFiltersAndSorts: currentView } =
useOptionsDropdown(); useGetCurrentView();
const recordGroupFieldMetadata = useRecoilComponentValueV2( const recordGroupFieldMetadata = useRecoilComponentValueV2(
recordGroupFieldMetadataComponentState, recordGroupFieldMetadataComponentState,
); );
const visibleRecordGroupIds = useRecoilComponentValueV2( const visibleRecordGroupIds = useRecoilComponentFamilyValueV2(
visibleRecordGroupIdsComponentSelector, visibleRecordGroupIdsComponentFamilySelector,
viewType,
); );
const hiddenRecordGroupIds = useRecoilComponentValueV2( const hiddenRecordGroupIds = useRecoilComponentValueV2(
hiddenRecordGroupIdsComponentSelector, hiddenRecordGroupIdsComponentSelector,
); );
const isDragableSortRecordGroup = useRecoilComponentValueV2( const hideEmptyRecordGroup = useRecoilComponentFamilyValueV2(
recordIndexRecordGroupIsDraggableSortComponentSelector, recordIndexRecordGroupHideComponentFamilyState,
viewType,
); );
const hideEmptyRecordGroup = useRecoilComponentValueV2( const recordGroupSort = useRecoilComponentValueV2(
recordIndexRecordGroupHideComponentState, recordIndexRecordGroupSortComponentState,
); );
const { const {
@ -59,12 +66,16 @@ export const ObjectOptionsDropdownRecordGroupsContent = () => {
handleHideEmptyRecordGroupChange, handleHideEmptyRecordGroupChange,
} = useRecordGroupVisibility({ } = useRecordGroupVisibility({
viewBarId: recordIndexId, viewBarId: recordIndexId,
viewType,
}); });
const { handleOrderChange: handleRecordGroupOrderChange } = const {
useRecordGroupReorder({ handleRecordGroupOrderChangeWithModal,
viewBarId: recordIndexId, handleRecordGroupReorderConfirmClick,
}); } = useRecordGroupReorderConfirmationModal({
recordIndexId,
viewType,
});
useEffect(() => { useEffect(() => {
if ( if (
@ -81,22 +92,20 @@ export const ObjectOptionsDropdownRecordGroupsContent = () => {
Group by Group by
</DropdownMenuHeader> </DropdownMenuHeader>
<DropdownMenuItemsContainer> <DropdownMenuItemsContainer>
{isViewGroupEnabled && ( {currentView?.key !== 'INDEX' && (
<> <>
<MenuItem <MenuItem
onClick={() => onContentChange('recordGroupFields')} onClick={() => onContentChange('recordGroupFields')}
LeftIcon={IconLayoutList} LeftIcon={IconLayoutList}
text={ text="Group by"
!recordGroupFieldMetadata contextualText={recordGroupFieldMetadata?.label}
? 'Group by'
: `Group by "${recordGroupFieldMetadata.label}"`
}
hasSubMenu hasSubMenu
/> />
<MenuItem <MenuItem
onClick={() => onContentChange('recordGroupSort')} onClick={() => onContentChange('recordGroupSort')}
LeftIcon={IconSortDescending} LeftIcon={IconSortDescending}
text="Sort" text="Sort"
contextualText={recordGroupSort}
hasSubMenu hasSubMenu
/> />
</> </>
@ -115,9 +124,9 @@ export const ObjectOptionsDropdownRecordGroupsContent = () => {
<RecordGroupsVisibilityDropdownSection <RecordGroupsVisibilityDropdownSection
title="Visible groups" title="Visible groups"
recordGroupIds={visibleRecordGroupIds} recordGroupIds={visibleRecordGroupIds}
onDragEnd={handleRecordGroupOrderChange} onDragEnd={handleRecordGroupOrderChangeWithModal}
onVisibilityChange={handleRecordGroupVisibilityChange} onVisibilityChange={handleRecordGroupVisibilityChange}
isDraggable={isDragableSortRecordGroup} isDraggable={true}
showDragGrip={true} showDragGrip={true}
/> />
</> </>
@ -134,6 +143,9 @@ export const ObjectOptionsDropdownRecordGroupsContent = () => {
</DropdownMenuItemsContainer> </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 { RecordBoardComponentInstanceContext } from '@/object-record/record-board/states/contexts/RecordBoardComponentInstanceContext';
import { getDraggedRecordPosition } from '@/object-record/record-board/utils/getDraggedRecordPosition'; import { getDraggedRecordPosition } from '@/object-record/record-board/utils/getDraggedRecordPosition';
import { recordGroupDefinitionFamilyState } from '@/object-record/record-group/states/recordGroupDefinitionFamilyState'; 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 { recordIndexRecordIdsByGroupComponentFamilyState } from '@/object-record/record-index/states/recordIndexRecordIdsByGroupComponentFamilyState';
import { recordIndexAllRecordIdsComponentSelector } from '@/object-record/record-index/states/selectors/recordIndexAllRecordIdsComponentSelector'; import { recordIndexAllRecordIdsComponentSelector } from '@/object-record/record-index/states/selectors/recordIndexAllRecordIdsComponentSelector';
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState'; 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 { getScopeIdFromComponentId } from '@/ui/utilities/recoil-scope/utils/getScopeIdFromComponentId';
import { ScrollWrapper } from '@/ui/utilities/scroll/components/ScrollWrapper'; import { ScrollWrapper } from '@/ui/utilities/scroll/components/ScrollWrapper';
import { useRecoilComponentCallbackStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackStateV2'; 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 { getSnapshotValue } from '@/ui/utilities/state/utils/getSnapshotValue';
import { ViewType } from '@/views/types/ViewType';
import { useScrollRestoration } from '~/hooks/useScrollRestoration'; import { useScrollRestoration } from '~/hooks/useScrollRestoration';
const StyledContainer = styled.div` const StyledContainer = styled.div`
@ -64,8 +65,9 @@ export const RecordBoard = () => {
useContext(RecordBoardContext); useContext(RecordBoardContext);
const boardRef = useRef<HTMLDivElement>(null); const boardRef = useRef<HTMLDivElement>(null);
const visibleRecordGroupIds = useRecoilComponentValueV2( const visibleRecordGroupIds = useRecoilComponentFamilyValueV2(
visibleRecordGroupIdsComponentSelector, visibleRecordGroupIdsComponentFamilySelector,
ViewType.Kanban,
); );
const recordIndexRecordIdsByGroupFamilyState = const recordIndexRecordIdsByGroupFamilyState =

View File

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

View File

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

View File

@ -4,9 +4,10 @@ import { useCallback, useRef } from 'react';
import { useRecordGroupActions } from '@/object-record/record-group/hooks/useRecordGroupActions'; import { useRecordGroupActions } from '@/object-record/record-group/hooks/useRecordGroupActions';
import { DropdownMenu } from '@/ui/layout/dropdown/components/DropdownMenu'; import { DropdownMenu } from '@/ui/layout/dropdown/components/DropdownMenu';
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer'; 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 { 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` const StyledMenuContainer = styled.div`
position: absolute; position: absolute;
@ -27,7 +28,9 @@ export const RecordBoardColumnDropdownMenu = ({
}: RecordBoardColumnDropdownMenuProps) => { }: RecordBoardColumnDropdownMenuProps) => {
const boardColumnMenuRef = useRef<HTMLDivElement>(null); const boardColumnMenuRef = useRef<HTMLDivElement>(null);
const recordGroupActions = useRecordGroupActions(); const recordGroupActions = useRecordGroupActions({
viewType: ViewType.Kanban,
});
const closeMenu = useCallback(() => { const closeMenu = useCallback(() => {
onClose(); 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 { useRecordIndexContextOrThrow } from '@/object-record/record-index/contexts/RecordIndexContext';
import { navigationMemorizedUrlState } from '@/ui/navigation/states/navigationMemorizedUrlState'; import { navigationMemorizedUrlState } from '@/ui/navigation/states/navigationMemorizedUrlState';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
import { ViewType } from '@/views/types/ViewType';
import { useCallback, useContext, useMemo } from 'react'; import { useCallback, useContext, useMemo } from 'react';
import { useLocation, useNavigate } from 'react-router-dom'; import { useLocation, useNavigate } from 'react-router-dom';
import { useSetRecoilState } from 'recoil'; import { useSetRecoilState } from 'recoil';
import { IconEyeOff, IconSettings, isDefined } from 'twenty-ui'; import { IconEyeOff, IconSettings, isDefined } from 'twenty-ui';
export const useRecordGroupActions = () => { type UseRecordGroupActionsParams = {
viewType: ViewType;
};
export const useRecordGroupActions = ({
viewType,
}: UseRecordGroupActionsParams) => {
const navigate = useNavigate(); const navigate = useNavigate();
const location = useLocation(); const location = useLocation();
@ -34,6 +41,7 @@ export const useRecordGroupActions = () => {
const { handleVisibilityChange: handleRecordGroupVisibilityChange } = const { handleVisibilityChange: handleRecordGroupVisibilityChange } =
useRecordGroupVisibility({ useRecordGroupVisibility({
viewBarId: recordIndexId, viewBarId: recordIndexId,
viewType,
}); });
const setNavigationMemorizedUrl = useSetRecoilState( 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 { useSetRecordGroup } from '@/object-record/record-group/hooks/useSetRecordGroup';
import { recordGroupDefinitionFamilyState } from '@/object-record/record-group/states/recordGroupDefinitionFamilyState'; 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 { RecordGroupDefinition } from '@/object-record/record-group/types/RecordGroupDefinition';
import { useRecoilComponentCallbackStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackStateV2'; import { useRecoilComponentCallbackStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackStateV2';
import { getSnapshotValue } from '@/ui/utilities/state/utils/getSnapshotValue'; import { getSnapshotValue } from '@/ui/utilities/state/utils/getSnapshotValue';
import { useSaveCurrentViewGroups } from '@/views/hooks/useSaveCurrentViewGroups'; import { useSaveCurrentViewGroups } from '@/views/hooks/useSaveCurrentViewGroups';
import { ViewType } from '@/views/types/ViewType';
import { mapRecordGroupDefinitionsToViewGroups } from '@/views/utils/mapRecordGroupDefinitionsToViewGroups'; import { mapRecordGroupDefinitionsToViewGroups } from '@/views/utils/mapRecordGroupDefinitionsToViewGroups';
import { useRecoilCallback } from 'recoil'; import { useRecoilCallback } from 'recoil';
import { moveArrayItem } from '~/utils/array/moveArrayItem'; import { moveArrayItem } from '~/utils/array/moveArrayItem';
@ -15,15 +16,17 @@ import { isDefined } from '~/utils/isDefined';
type UseRecordGroupHandlersParams = { type UseRecordGroupHandlersParams = {
viewBarId: string; viewBarId: string;
viewType: ViewType;
}; };
export const useRecordGroupReorder = ({ export const useRecordGroupReorder = ({
viewBarId, viewBarId,
viewType,
}: UseRecordGroupHandlersParams) => { }: UseRecordGroupHandlersParams) => {
const setRecordGroup = useSetRecordGroup(viewBarId); const setRecordGroup = useSetRecordGroup(viewBarId);
const visibleRecordGroupIdsSelector = useRecoilComponentCallbackStateV2( const visibleRecordGroupIdsFamilySelector = useRecoilComponentCallbackStateV2(
visibleRecordGroupIdsComponentSelector, visibleRecordGroupIdsComponentFamilySelector,
); );
const { saveViewGroups } = useSaveCurrentViewGroups(viewBarId); const { saveViewGroups } = useSaveCurrentViewGroups(viewBarId);
@ -37,7 +40,7 @@ export const useRecordGroupReorder = ({
const visibleRecordGroupIds = getSnapshotValue( const visibleRecordGroupIds = getSnapshotValue(
snapshot, snapshot,
visibleRecordGroupIdsSelector, visibleRecordGroupIdsFamilySelector(viewType),
); );
const reorderedVisibleRecordGroupIds = moveArrayItem( const reorderedVisibleRecordGroupIds = moveArrayItem(
@ -80,7 +83,12 @@ export const useRecordGroupReorder = ({
mapRecordGroupDefinitionsToViewGroups(updatedRecordGroups), mapRecordGroupDefinitionsToViewGroups(updatedRecordGroups),
); );
}, },
[saveViewGroups, setRecordGroup, visibleRecordGroupIdsSelector], [
saveViewGroups,
setRecordGroup,
viewType,
visibleRecordGroupIdsFamilySelector,
],
); );
return { 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 { recordGroupDefinitionFamilyState } from '@/object-record/record-group/states/recordGroupDefinitionFamilyState';
import { RecordGroupDefinition } from '@/object-record/record-group/types/RecordGroupDefinition'; 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 { useRecoilComponentCallbackStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackStateV2';
import { useSaveCurrentViewGroups } from '@/views/hooks/useSaveCurrentViewGroups'; import { useSaveCurrentViewGroups } from '@/views/hooks/useSaveCurrentViewGroups';
import { ViewType } from '@/views/types/ViewType';
import { recordGroupDefinitionToViewGroup } from '@/views/utils/recordGroupDefinitionToViewGroup'; import { recordGroupDefinitionToViewGroup } from '@/views/utils/recordGroupDefinitionToViewGroup';
import { useRecoilCallback } from 'recoil'; import { useRecoilCallback } from 'recoil';
type UseRecordGroupVisibilityParams = { type UseRecordGroupVisibilityParams = {
viewBarId: string; viewBarId: string;
viewType: ViewType;
}; };
export const useRecordGroupVisibility = ({ export const useRecordGroupVisibility = ({
viewBarId, viewBarId,
viewType,
}: UseRecordGroupVisibilityParams) => { }: UseRecordGroupVisibilityParams) => {
const objectOptionsDropdownRecordGroupHideState = const objectOptionsDropdownRecordGroupHideFamilyState =
useRecoilComponentCallbackStateV2(recordIndexRecordGroupHideComponentState); useRecoilComponentCallbackStateV2(
recordIndexRecordGroupHideComponentFamilyState,
);
const { saveViewGroup } = useSaveCurrentViewGroups(viewBarId); const { saveViewGroup } = useSaveCurrentViewGroups(viewBarId);
@ -27,22 +32,19 @@ export const useRecordGroupVisibility = ({
); );
saveViewGroup(recordGroupDefinitionToViewGroup(updatedRecordGroup)); saveViewGroup(recordGroupDefinitionToViewGroup(updatedRecordGroup));
// If visibility is manually toggled, we should reset the hideEmptyRecordGroup state
set(objectOptionsDropdownRecordGroupHideState, false);
}, },
[saveViewGroup, objectOptionsDropdownRecordGroupHideState], [saveViewGroup],
); );
const handleHideEmptyRecordGroupChange = useRecoilCallback( const handleHideEmptyRecordGroupChange = useRecoilCallback(
({ set }) => ({ set }) =>
async () => { async () => {
set( set(
objectOptionsDropdownRecordGroupHideState, objectOptionsDropdownRecordGroupHideFamilyState(viewType),
(currentHideState) => !currentHideState, (currentHideState) => !currentHideState,
); );
}, },
[objectOptionsDropdownRecordGroupHideState], [viewType, objectOptionsDropdownRecordGroupHideFamilyState],
); );
return { 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 { 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 { RecordGroupDefinition } from '@/object-record/record-group/types/RecordGroupDefinition';
import { RecordIndexPageKanbanAddMenuItem } from '@/object-record/record-index/components/RecordIndexPageKanbanAddMenuItem'; import { RecordIndexPageKanbanAddMenuItem } from '@/object-record/record-index/components/RecordIndexPageKanbanAddMenuItem';
import { useRecordIndexContextOrThrow } from '@/object-record/record-index/contexts/RecordIndexContext'; 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 { Dropdown } from '@/ui/layout/dropdown/components/Dropdown';
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer'; import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown'; 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 { useRecoilComponentCallbackStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackStateV2';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
import { useRecoilCallback } from 'recoil'; import { useRecoilCallback } from 'recoil';
export const RecordIndexPageTableAddButtonInGroup = () => { type RecordIndexAddRecordInGroupDropdownProps = {
const dropdownId = `record-index-page-table-add-button-dropdown`; dropdownId: string;
clickableComponent: React.ReactNode;
};
export const RecordIndexAddRecordInGroupDropdown = ({
dropdownId,
clickableComponent,
}: RecordIndexAddRecordInGroupDropdownProps) => {
const { objectMetadataItem } = useRecordIndexContextOrThrow(); const { objectMetadataItem } = useRecordIndexContextOrThrow();
const visibleRecordGroupIds = useRecoilComponentValueV2( const { setActiveDropdownFocusIdAndMemorizePrevious } =
visibleRecordGroupIdsComponentSelector, useSetActiveDropdownFocusIdAndMemorizePrevious();
const recordGroupIds = useRecoilComponentValueV2(
availableRecordGroupIdsComponentSelector,
); );
const recordGroupFieldMetadata = useRecoilComponentValueV2( const recordGroupFieldMetadata = useRecoilComponentValueV2(
@ -44,11 +53,13 @@ export const RecordIndexPageTableAddButtonInGroup = () => {
(recordGroup: RecordGroupDefinition) => { (recordGroup: RecordGroupDefinition) => {
set(isRecordGroupTableSectionToggledState(recordGroup.id), true); set(isRecordGroupTableSectionToggledState(recordGroup.id), true);
createNewTableRecordInGroup(recordGroup.id); createNewTableRecordInGroup(recordGroup.id);
setActiveDropdownFocusIdAndMemorizePrevious(null);
closeDropdown(); closeDropdown();
}, },
[ [
closeDropdown, closeDropdown,
createNewTableRecordInGroup, createNewTableRecordInGroup,
setActiveDropdownFocusIdAndMemorizePrevious,
isRecordGroupTableSectionToggledState, isRecordGroupTableSectionToggledState,
], ],
); );
@ -61,11 +72,11 @@ export const RecordIndexPageTableAddButtonInGroup = () => {
<Dropdown <Dropdown
dropdownMenuWidth="200px" dropdownMenuWidth="200px"
dropdownPlacement="bottom-start" dropdownPlacement="bottom-start"
clickableComponent={<PageAddButton />} clickableComponent={clickableComponent}
dropdownId={dropdownId} dropdownId={dropdownId}
dropdownComponents={ dropdownComponents={
<DropdownMenuItemsContainer> <DropdownMenuItemsContainer>
{visibleRecordGroupIds.map((recordGroupId) => ( {recordGroupIds.map((recordGroupId) => (
<RecordIndexPageKanbanAddMenuItem <RecordIndexPageKanbanAddMenuItem
key={recordGroupId} key={recordGroupId}
columnId={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 { useAddNewCard } from '@/object-record/record-board/record-board-column/hooks/useAddNewCard';
import { useIsOpportunitiesCompanyFieldDisabled } from '@/object-record/record-board/record-board-column/hooks/useIsOpportunitiesCompanyFieldDisabled'; import { useIsOpportunitiesCompanyFieldDisabled } from '@/object-record/record-board/record-board-column/hooks/useIsOpportunitiesCompanyFieldDisabled';
import { recordBoardVisibleFieldDefinitionsComponentSelector } from '@/object-record/record-board/states/selectors/recordBoardVisibleFieldDefinitionsComponentSelector'; 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 { RecordGroupDefinition } from '@/object-record/record-group/types/RecordGroupDefinition';
import { RecordIndexPageKanbanAddMenuItem } from '@/object-record/record-index/components/RecordIndexPageKanbanAddMenuItem'; import { RecordIndexPageKanbanAddMenuItem } from '@/object-record/record-index/components/RecordIndexPageKanbanAddMenuItem';
import { useRecordIndexContextOrThrow } from '@/object-record/record-index/contexts/RecordIndexContext'; 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 { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown'; import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown';
import { PageAddButton } from '@/ui/layout/page/components/PageAddButton'; 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 { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
import { ViewType } from '@/views/types/ViewType';
import { useCallback } from 'react'; import { useCallback } from 'react';
import { useRecoilValue } from 'recoil'; import { useRecoilValue } from 'recoil';
@ -20,8 +22,9 @@ export const RecordIndexPageKanbanAddButton = () => {
const { recordIndexId, objectMetadataItem } = useRecordIndexContextOrThrow(); const { recordIndexId, objectMetadataItem } = useRecordIndexContextOrThrow();
const visibleRecordGroupIds = useRecoilComponentValueV2( const visibleRecordGroupIds = useRecoilComponentFamilyValueV2(
visibleRecordGroupIdsComponentSelector, visibleRecordGroupIdsComponentFamilySelector,
ViewType.Kanban,
); );
const recordIndexKanbanFieldMetadataId = useRecoilValue( const recordIndexKanbanFieldMetadataId = useRecoilValue(

View File

@ -1,6 +1,7 @@
import { hasRecordGroupsComponentSelector } from '@/object-record/record-group/states/selectors/hasRecordGroupsComponentSelector'; 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 { 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'; import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
export const RecordIndexPageTableAddButton = () => { export const RecordIndexPageTableAddButton = () => {
@ -12,5 +13,10 @@ export const RecordIndexPageTableAddButton = () => {
return <RecordIndexPageTableAddButtonNoGroup />; 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) => (option) =>
!existingGroupKeys.has(`${fieldMetadataItem.id}:${option.value}`), !existingGroupKeys.has(`${fieldMetadataItem.id}:${option.value}`),
) )
// Alphabetically sort the options by default
.sort((a, b) => a.value.localeCompare(b.value))
.map( .map(
(option, index) => (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 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 { hasRecordGroupsComponentSelector } from '@/object-record/record-group/states/selectors/hasRecordGroupsComponentSelector';
import { recordIndexAllRecordIdsComponentSelector } from '@/object-record/record-index/states/selectors/recordIndexAllRecordIdsComponentSelector'; 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 { RecordTableAggregateFooter } from '@/object-record/record-table/record-table-footer/components/RecordTableAggregateFooter';
import { RecordTableHeader } from '@/object-record/record-table/record-table-header/components/RecordTableHeader'; import { RecordTableHeader } from '@/object-record/record-table/record-table-header/components/RecordTableHeader';
import { isRecordTableInitialLoadingComponentState } from '@/object-record/record-table/states/isRecordTableInitialLoadingComponentState'; 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 { DragSelect } from '@/ui/utilities/drag-select/components/DragSelect';
import { useClickOutsideListener } from '@/ui/utilities/pointer-event/hooks/useClickOutsideListener'; import { useClickOutsideListener } from '@/ui/utilities/pointer-event/hooks/useClickOutsideListener';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
@ -54,8 +54,8 @@ export const RecordTable = () => {
recordTableId, recordTableId,
); );
const pendingRecordId = useRecoilComponentValueV2( const hasPendingRecord = useRecoilComponentValueV2(
recordTablePendingRecordIdComponentState, hasPendingRecordComponentSelector,
recordTableId, recordTableId,
); );
@ -67,7 +67,7 @@ export const RecordTable = () => {
const recordTableIsEmpty = const recordTableIsEmpty =
!isRecordTableInitialLoading && !isRecordTableInitialLoading &&
allRecordIds.length === 0 && allRecordIds.length === 0 &&
isNull(pendingRecordId); !hasPendingRecord;
const { resetTableRowSelection, setRowSelected } = useRecordTable({ const { resetTableRowSelection, setRowSelected } = useRecordTable({
recordTableId, recordTableId,

View File

@ -1,6 +1,8 @@
import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords'; 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 { 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 { RecordTableEmptyStateNoRecordFoundForFilter } from '@/object-record/record-table/empty-state/components/RecordTableEmptyStateNoRecordFoundForFilter';
import { RecordTableEmptyStateRemote } from '@/object-record/record-table/empty-state/components/RecordTableEmptyStateRemote'; import { RecordTableEmptyStateRemote } from '@/object-record/record-table/empty-state/components/RecordTableEmptyStateRemote';
import { RecordTableEmptyStateSoftDelete } from '@/object-record/record-table/empty-state/components/RecordTableEmptyStateSoftDelete'; import { RecordTableEmptyStateSoftDelete } from '@/object-record/record-table/empty-state/components/RecordTableEmptyStateSoftDelete';
@ -11,6 +13,10 @@ export const RecordTableEmptyState = () => {
const { recordTableId, objectNameSingular, objectMetadataItem } = const { recordTableId, objectNameSingular, objectMetadataItem } =
useRecordTableContextOrThrow(); useRecordTableContextOrThrow();
const hasRecordGroups = useRecoilComponentValueV2(
hasRecordGroupsComponentSelector,
);
const { totalCount } = useFindManyRecords({ objectNameSingular, limit: 1 }); const { totalCount } = useFindManyRecords({ objectNameSingular, limit: 1 });
const noRecordAtAll = totalCount === 0; const noRecordAtAll = totalCount === 0;
@ -26,7 +32,11 @@ export const RecordTableEmptyState = () => {
} else if (isSoftDeleteActive === true) { } else if (isSoftDeleteActive === true) {
return <RecordTableEmptyStateSoftDelete />; return <RecordTableEmptyStateSoftDelete />;
} else if (noRecordAtAll) { } else if (noRecordAtAll) {
return <RecordTableEmptyStateNoRecordAtAll />; if (hasRecordGroups) {
return <RecordTableEmptyStateByGroupNoRecordAtAll />;
}
return <RecordTableEmptyStateNoGroupNoRecordAtAll />;
} else { } else {
return <RecordTableEmptyStateNoRecordFoundForFilter />; 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, IconComponent,
} from 'twenty-ui'; } from 'twenty-ui';
type RecordTableEmptyStateDisplayProps = { type RecordTableEmptyStateDisplayButtonComponentProps = {
animatedPlaceholderType: AnimatedPlaceholderType; buttonComponent?: React.ReactNode;
title: string; };
subTitle: string;
Icon: IconComponent; type RecordTableEmptyStateDisplayButtonProps = {
ButtonIcon: IconComponent;
buttonTitle: string; buttonTitle: string;
onClick: () => void; onClick: () => void;
}; };
export const RecordTableEmptyStateDisplay = ({ type RecordTableEmptyStateDisplayProps = {
Icon, animatedPlaceholderType: AnimatedPlaceholderType;
animatedPlaceholderType, title: string;
buttonTitle, subTitle: string;
onClick, } & (
subTitle, | RecordTableEmptyStateDisplayButtonComponentProps
title, | RecordTableEmptyStateDisplayButtonProps
}: RecordTableEmptyStateDisplayProps) => { );
export const RecordTableEmptyStateDisplay = (
props: RecordTableEmptyStateDisplayProps,
) => {
const { objectMetadataItem } = useRecordTableContextOrThrow(); const { objectMetadataItem } = useRecordTableContextOrThrow();
const isReadOnly = isObjectMetadataReadOnly(objectMetadataItem); const isReadOnly = isObjectMetadataReadOnly(objectMetadataItem);
return ( return (
<AnimatedPlaceholderEmptyContainer> <AnimatedPlaceholderEmptyContainer>
<AnimatedPlaceholder type={animatedPlaceholderType} /> <AnimatedPlaceholder type={props.animatedPlaceholderType} />
<AnimatedPlaceholderEmptyTextContainer> <AnimatedPlaceholderEmptyTextContainer>
<AnimatedPlaceholderEmptyTitle>{title}</AnimatedPlaceholderEmptyTitle> <AnimatedPlaceholderEmptyTitle>
{props.title}
</AnimatedPlaceholderEmptyTitle>
<AnimatedPlaceholderEmptySubTitle> <AnimatedPlaceholderEmptySubTitle>
{subTitle} {props.subTitle}
</AnimatedPlaceholderEmptySubTitle> </AnimatedPlaceholderEmptySubTitle>
</AnimatedPlaceholderEmptyTextContainer> </AnimatedPlaceholderEmptyTextContainer>
{!isReadOnly && ( {'buttonComponent' in props && props.buttonComponent}
{'buttonTitle' in props && !isReadOnly && (
<Button <Button
Icon={Icon} Icon={props.ButtonIcon}
title={buttonTitle} title={props.buttonTitle}
variant={'secondary'} variant={'secondary'}
onClick={onClick} onClick={props.onClick}
/> />
)} )}
</AnimatedPlaceholderEmptyContainer> </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 { RecordTableEmptyStateDisplay } from '@/object-record/record-table/empty-state/components/RecordTableEmptyStateDisplay';
import { useCreateNewTableRecord } from '@/object-record/record-table/hooks/useCreateNewTableRecords'; import { useCreateNewTableRecord } from '@/object-record/record-table/hooks/useCreateNewTableRecords';
export const RecordTableEmptyStateNoRecordAtAll = () => { export const RecordTableEmptyStateNoGroupNoRecordAtAll = () => {
const { objectMetadataItem, recordTableId } = useRecordTableContextOrThrow(); const { objectMetadataItem, recordTableId } = useRecordTableContextOrThrow();
const { createNewTableRecord } = useCreateNewTableRecord(recordTableId); const { createNewTableRecord } = useCreateNewTableRecord(recordTableId);
@ -27,7 +27,7 @@ export const RecordTableEmptyStateNoRecordAtAll = () => {
buttonTitle={buttonTitle} buttonTitle={buttonTitle}
subTitle={subTitle} subTitle={subTitle}
title={title} title={title}
Icon={IconPlus} ButtonIcon={IconPlus}
animatedPlaceholderType="noRecord" animatedPlaceholderType="noRecord"
onClick={handleButtonClick} onClick={handleButtonClick}
/> />

View File

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

View File

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

View File

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

View File

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

View File

@ -1,5 +1,5 @@
import { RecordGroupContext } from '@/object-record/record-group/states/context/RecordGroupContext'; 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 { recordIndexAllRecordIdsComponentSelector } from '@/object-record/record-index/states/selectors/recordIndexAllRecordIdsComponentSelector';
import { RecordTableRecordGroupBodyContextProvider } from '@/object-record/record-table/components/RecordTableRecordGroupBodyContextProvider'; import { RecordTableRecordGroupBodyContextProvider } from '@/object-record/record-table/components/RecordTableRecordGroupBodyContextProvider';
import { RecordTableRecordGroupRows } from '@/object-record/record-table/components/RecordTableRecordGroupRows'; 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 { RecordTableBodyLoading } from '@/object-record/record-table/record-table-body/components/RecordTableBodyLoading';
import { RecordTableBodyRecordGroupDragDropContextProvider } from '@/object-record/record-table/record-table-body/components/RecordTableBodyRecordGroupDragDropContextProvider'; 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 { 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 { RecordTableRecordGroupSection } from '@/object-record/record-table/record-table-section/components/RecordTableRecordGroupSection';
import { isRecordTableInitialLoadingComponentState } from '@/object-record/record-table/states/isRecordTableInitialLoadingComponentState'; 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 { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
import { ViewType } from '@/views/types/ViewType';
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled'; import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
import { FeatureFlagKey } from '~/generated/graphql'; import { FeatureFlagKey } from '~/generated/graphql';
@ -26,8 +27,9 @@ export const RecordTableRecordGroupsBody = () => {
isRecordTableInitialLoadingComponentState, isRecordTableInitialLoadingComponentState,
); );
const visibleRecordGroupIds = useRecoilComponentValueV2( const visibleRecordGroupIds = useRecoilComponentFamilyValueV2(
visibleRecordGroupIdsComponentSelector, visibleRecordGroupIdsComponentFamilySelector,
ViewType.Table,
); );
if (isRecordTableInitialLoading && allRecordIds.length === 0) { if (isRecordTableInitialLoading && allRecordIds.length === 0) {
@ -37,14 +39,13 @@ export const RecordTableRecordGroupsBody = () => {
return ( return (
<> <>
<RecordTableBodyRecordGroupDragDropContextProvider> <RecordTableBodyRecordGroupDragDropContextProvider>
{visibleRecordGroupIds.map((recordGroupId, index) => ( {visibleRecordGroupIds.map((recordGroupId) => (
<RecordTableRecordGroupBodyContextProvider <RecordTableRecordGroupBodyContextProvider
key={recordGroupId} key={recordGroupId}
recordGroupId={recordGroupId} recordGroupId={recordGroupId}
> >
<RecordGroupContext.Provider value={{ recordGroupId }}> <RecordGroupContext.Provider value={{ recordGroupId }}>
<RecordTableBodyDroppable recordGroupId={recordGroupId}> <RecordTableBodyDroppable recordGroupId={recordGroupId}>
{index > 0 && <RecordTableRecordGroupEmptyRow />}
<RecordTableRecordGroupSection /> <RecordTableRecordGroupSection />
<RecordTableRecordGroupRows /> <RecordTableRecordGroupRows />
</RecordTableBodyDroppable> </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 styled from '@emotion/styled';
import { motion } from 'framer-motion'; import { motion } from 'framer-motion';
import { useCallback } from 'react'; 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 { useCurrentRecordGroupId } from '@/object-record/record-group/hooks/useCurrentRecordGroupId';
import { recordGroupDefinitionFamilyState } from '@/object-record/record-group/states/recordGroupDefinitionFamilyState'; import { recordGroupDefinitionFamilyState } from '@/object-record/record-group/states/recordGroupDefinitionFamilyState';
@ -83,13 +83,13 @@ export const RecordTableRecordGroupSection = () => {
<td aria-hidden /> <td aria-hidden />
<StyledChevronContainer> <StyledChevronContainer>
<motion.span <motion.span
animate={{ rotate: isRecordGroupTableSectionToggled ? 180 : 0 }} animate={{ rotate: !isRecordGroupTableSectionToggled ? -90 : 0 }}
transition={{ duration: theme.animation.duration.normal }} transition={{ duration: theme.animation.duration.normal }}
style={{ style={{
display: 'inline-block', display: 'inline-block',
}} }}
> >
<IconChevronUp size={theme.icon.size.md} /> <IconChevronDown size={theme.icon.size.md} />
</motion.span> </motion.span>
</StyledChevronContainer> </StyledChevronContainer>
<StyledRecordGroupSection> <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>( export const getSettingsPagePath = <Path extends SettingsPath>(
path: Path, path: Path,
params?: Params<Path>, params?: Params<Path>,
searchParams?: Record<string, string>,
) => { ) => {
let resultPath = `/settings/${path}`; let resultPath = `/settings/${path}`;
@ -26,5 +27,11 @@ export const getSettingsPagePath = <Path extends SettingsPath>(
resultPath = `${resultPath}/${params?.id}`; resultPath = `${resultPath}/${params?.id}`;
} }
if (isDefined(searchParams)) {
const searchParamsString = new URLSearchParams(searchParams).toString();
resultPath = `${resultPath}?${searchParamsString}`;
}
return resultPath; return resultPath;
}; };

View File

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

View File

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

View File

@ -142,7 +142,9 @@ export const ConfirmationModal = ({
</Section> </Section>
)} )}
<StyledCenteredButton <StyledCenteredButton
onClick={() => setIsOpen(false)} onClick={() => {
setIsOpen(false);
}}
variant="secondary" variant="secondary"
title="Cancel" title="Cancel"
fullWidth 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 { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentStateV2';
import 'overlayscrollbars/overlayscrollbars.css'; 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; display: flex;
height: 100%; height: ${({ heightMode }) => {
switch (heightMode) {
case 'full':
return '100%';
case 'fit-content':
return 'fit-content';
}
}};
width: 100%; width: 100%;
.os-scrollbar-handle { .os-scrollbar-handle {
@ -33,6 +45,7 @@ const StyledInnerContainer = styled.div`
export type ScrollWrapperProps = { export type ScrollWrapperProps = {
children: React.ReactNode; children: React.ReactNode;
className?: string; className?: string;
heightMode?: HeightMode;
defaultEnableXScroll?: boolean; defaultEnableXScroll?: boolean;
defaultEnableYScroll?: boolean; defaultEnableYScroll?: boolean;
contextProviderName: ContextProviderName; contextProviderName: ContextProviderName;
@ -44,6 +57,7 @@ export const ScrollWrapper = ({
componentInstanceId, componentInstanceId,
children, children,
className, className,
heightMode = 'full',
defaultEnableXScroll = true, defaultEnableXScroll = true,
defaultEnableYScroll = true, defaultEnableYScroll = true,
contextProviderName, contextProviderName,
@ -164,6 +178,7 @@ export const ScrollWrapper = ({
ref={scrollableRef} ref={scrollableRef}
className={className} className={className}
scrollHide={scrollHide} scrollHide={scrollHide}
heightMode={heightMode}
> >
<StyledInnerContainer>{children}</StyledInnerContainer> <StyledInnerContainer>{children}</StyledInnerContainer>
</StyledScrollWrapper> </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 { ComponentFamilyStateV2 } from '@/ui/utilities/state/component-state/types/ComponentFamilyStateV2';
import { ComponentInstanceStateContext } from '@/ui/utilities/state/component-state/types/ComponentInstanceStateContext'; import { ComponentInstanceStateContext } from '@/ui/utilities/state/component-state/types/ComponentInstanceStateContext';
import { globalComponentInstanceContextMap } from '@/ui/utilities/state/component-state/utils/globalComponentInstanceContextMap'; 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'; import { isDefined } from 'twenty-ui';
@ -11,7 +18,16 @@ type CreateComponentFamilyStateArgs<
FamilyKey extends SerializableParam, FamilyKey extends SerializableParam,
> = { > = {
key: string; key: string;
defaultValue: ValueType; defaultValue:
| ValueType
| ((
param: ComponentFamilyStateKeyV2<FamilyKey>,
) =>
| ValueType
| RecoilValue<ValueType>
| Promise<ValueType>
| Loadable<ValueType>
| WrappedValue<ValueType>);
componentInstanceContext: ComponentInstanceStateContext<any> | null; componentInstanceContext: ComponentInstanceStateContext<any> | null;
effects?: effects?:
| AtomEffect<ValueType>[] | AtomEffect<ValueType>[]

View File

@ -1,77 +1,43 @@
import { useApolloClient } from '@apollo/client';
import { useCallback } from 'react'; 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 { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { useCreateOneRecordMutation } from '@/object-record/hooks/useCreateOneRecordMutation'; import { useCreateManyRecords } from '@/object-record/hooks/useCreateManyRecords';
import { useDeleteOneRecordMutation } from '@/object-record/hooks/useDeleteOneRecordMutation'; import { useDestroyManyRecords } from '@/object-record/hooks/useDestroyManyRecords';
import { useUpdateOneRecordMutation } from '@/object-record/hooks/useUpdateOneRecordMutation'; import { useUpdateOneRecordMutation } from '@/object-record/hooks/useUpdateOneRecordMutation';
import { GraphQLView } from '@/views/types/GraphQLView'; import { GraphQLView } from '@/views/types/GraphQLView';
import { ViewGroup } from '@/views/types/ViewGroup'; import { ViewGroup } from '@/views/types/ViewGroup';
import { useApolloClient } from '@apollo/client';
export const usePersistViewGroupRecords = () => { export const usePersistViewGroupRecords = () => {
const { objectMetadataItem } = useObjectMetadataItem({ const apolloClient = useApolloClient();
objectNameSingular: CoreObjectNameSingular.ViewGroup,
});
const { createOneRecordMutation } = useCreateOneRecordMutation({ const { createManyRecords } = useCreateManyRecords({
objectNameSingular: CoreObjectNameSingular.ViewGroup, objectNameSingular: CoreObjectNameSingular.ViewGroup,
shouldMatchRootQueryFilter: true,
}); });
const { updateOneRecordMutation } = useUpdateOneRecordMutation({ const { updateOneRecordMutation } = useUpdateOneRecordMutation({
objectNameSingular: CoreObjectNameSingular.ViewGroup, objectNameSingular: CoreObjectNameSingular.ViewGroup,
}); });
const { deleteOneRecordMutation } = useDeleteOneRecordMutation({ const { destroyManyRecords } = useDestroyManyRecords({
objectNameSingular: CoreObjectNameSingular.ViewGroup, objectNameSingular: CoreObjectNameSingular.ViewGroup,
}); });
const { objectMetadataItems } = useObjectMetadataItems();
const apolloClient = useApolloClient();
const createViewGroupRecords = useCallback( const createViewGroupRecords = useCallback(
(viewGroupsToCreate: ViewGroup[], view: GraphQLView) => { (viewGroupsToCreate: ViewGroup[], view: GraphQLView) => {
if (!viewGroupsToCreate.length) return; if (!viewGroupsToCreate.length) return;
return Promise.all( return createManyRecords(
viewGroupsToCreate.map((viewGroup) => viewGroupsToCreate.map((viewGroup) => ({
apolloClient.mutate({ ...viewGroup,
mutation: createOneRecordMutation, view: {
variables: { id: view.id,
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,
});
},
}),
),
); );
}, },
[ [createManyRecords],
apolloClient,
createOneRecordMutation,
objectMetadataItem,
objectMetadataItems,
],
); );
const updateViewGroupRecords = useCallback( const updateViewGroupRecords = useCallback(
@ -95,7 +61,8 @@ export const usePersistViewGroupRecords = () => {
const mutationResults = await Promise.all(mutationPromises); 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 }) => { mutationResults.forEach(({ data }) => {
const record = data?.['updateViewGroup']; const record = data?.['updateViewGroup'];
@ -120,33 +87,11 @@ export const usePersistViewGroupRecords = () => {
async (viewGroupsToDelete: ViewGroup[]) => { async (viewGroupsToDelete: ViewGroup[]) => {
if (!viewGroupsToDelete.length) return; if (!viewGroupsToDelete.length) return;
const mutationPromises = viewGroupsToDelete.map((viewGroup) => return destroyManyRecords(
apolloClient.mutate<{ deleteViewGroup: ViewGroup }>({ viewGroupsToDelete.map((viewGroup) => viewGroup.id),
mutation: deleteOneRecordMutation,
variables: {
idToDelete: viewGroup.id,
},
// Avoid cache being updated with stale data
fetchPolicy: 'no-cache',
}),
); );
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 { return {

View File

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