Merge branch 'main' into c--refactor-graphql-query-runner--move-api-event-emit-before-gql-processing

This commit is contained in:
Weiko
2024-11-20 20:33:31 +01:00
152 changed files with 3210 additions and 1452 deletions

View File

@@ -1,4 +1,5 @@
#!/bin/sh
set -e
# Check if the initialization has already been done and that we enabled automatic migration
if [ "${ENABLE_DB_MIGRATIONS}" = "true" ] && [ ! -f /app/docker-data/db_status ]; then

View File

@@ -30,6 +30,7 @@ export const WorkflowRunActionEffect = () => {
addActionMenuEntry({
type: 'workflow-run',
key: `workflow-run-${activeWorkflowVersion.id}`,
scope: 'global',
label: capitalize(activeWorkflowVersion.workflow.name),
position: index,
Icon: IconSettingsAutomation,

View File

@@ -37,8 +37,8 @@ export const DeleteRecordsActionEffect = ({
objectNameSingular: objectMetadataItem.nameSingular,
});
const favorites = useFavorites();
const deleteFavorite = useDeleteFavorite();
const { sortedFavorites: favorites } = useFavorites();
const { deleteFavorite } = useDeleteFavorite();
const contextStoreNumberOfSelectedRecords = useRecoilComponentValueV2(
contextStoreNumberOfSelectedRecordsComponentState,
@@ -106,6 +106,7 @@ export const DeleteRecordsActionEffect = ({
if (canDelete) {
addActionMenuEntry({
type: 'standard',
scope: 'record-selection',
key: 'delete',
label: 'Delete',
position,

View File

@@ -32,12 +32,10 @@ export const ExportRecordsActionEffect = ({
useEffect(() => {
addActionMenuEntry({
type: 'standard',
scope: 'record-selection',
key: 'export',
position,
label: displayedExportProgress(
contextStoreNumberOfSelectedRecords > 0 ? 'selection' : 'all',
progress,
),
label: displayedExportProgress(progress),
Icon: IconDatabaseExport,
accent: 'default',
onClick: () => download(),

View File

@@ -23,11 +23,11 @@ export const ManageFavoritesActionEffect = ({
contextStoreTargetedRecordsRuleComponentState,
);
const favorites = useFavorites();
const { sortedFavorites: favorites } = useFavorites();
const createFavorite = useCreateFavorite();
const { createFavorite } = useCreateFavorite();
const deleteFavorite = useDeleteFavorite();
const { deleteFavorite } = useDeleteFavorite();
const selectedRecordId =
contextStoreTargetedRecordsRule.mode === 'selection'
@@ -51,6 +51,7 @@ export const ManageFavoritesActionEffect = ({
addActionMenuEntry({
type: 'standard',
scope: 'record-selection',
key: 'manage-favorites',
label: isFavorite ? 'Remove from favorites' : 'Add to favorites',
position,

View File

@@ -57,6 +57,7 @@ export const WorkflowRunRecordActionEffect = ({
addActionMenuEntry({
type: 'workflow-run',
key: `workflow-run-${activeWorkflowVersion.id}`,
scope: 'record-selection',
label: capitalize(activeWorkflowVersion.workflow.name),
position: index,
Icon: IconSettingsAutomation,

View File

@@ -65,21 +65,25 @@ export const RightDrawerActionMenuDropdown = () => {
}}
dropdownComponents={
<DropdownMenuItemsContainer>
{actionMenuEntries.map((item, index) => (
<MenuItem
key={index}
LeftIcon={item.Icon}
onClick={() => {
closeDropdown(
getRightDrawerActionMenuDropdownIdFromActionMenuId(
actionMenuId,
),
);
item.onClick?.();
}}
text={item.label}
/>
))}
{actionMenuEntries
.filter(
(actionMenuEntry) => actionMenuEntry.scope === 'record-selection',
)
.map((actionMenuEntry, index) => (
<MenuItem
key={index}
LeftIcon={actionMenuEntry.Icon}
onClick={() => {
closeDropdown(
getRightDrawerActionMenuDropdownIdFromActionMenuId(
actionMenuId,
),
);
actionMenuEntry.onClick?.();
}}
text={actionMenuEntry.label}
/>
))}
</DropdownMenuItemsContainer>
}
/>

View File

@@ -48,6 +48,7 @@ const meta: Meta<typeof RecordIndexActionMenuBar> = {
map.set('delete', {
isPinned: true,
scope: 'record-selection',
type: 'standard',
key: 'delete',
label: 'Delete',

View File

@@ -22,6 +22,7 @@ export const Default: Story = {
args: {
entry: {
type: 'standard',
scope: 'record-selection',
key: 'delete',
label: 'Delete',
position: 0,
@@ -35,6 +36,7 @@ export const WithDangerAccent: Story = {
args: {
entry: {
type: 'standard',
scope: 'record-selection',
key: 'delete',
label: 'Delete',
position: 0,
@@ -49,6 +51,7 @@ export const WithInteraction: Story = {
args: {
entry: {
type: 'standard',
scope: 'record-selection',
key: 'markAsDone',
label: 'Mark as done',
position: 0,

View File

@@ -42,6 +42,7 @@ const meta: Meta<typeof RecordIndexActionMenuDropdown> = {
map.set('delete', {
type: 'standard',
scope: 'record-selection',
key: 'delete',
label: 'Delete',
position: 0,
@@ -51,6 +52,7 @@ const meta: Meta<typeof RecordIndexActionMenuDropdown> = {
map.set('markAsDone', {
type: 'standard',
scope: 'record-selection',
key: 'markAsDone',
label: 'Mark as done',
position: 1,
@@ -60,6 +62,7 @@ const meta: Meta<typeof RecordIndexActionMenuDropdown> = {
map.set('addToFavorites', {
type: 'standard',
scope: 'record-selection',
key: 'addToFavorites',
label: 'Add to favorites',
position: 2,

View File

@@ -55,6 +55,7 @@ const meta: Meta<typeof RightDrawerActionMenuDropdown> = {
map.set('addToFavorites', {
type: 'standard',
scope: 'record-selection',
key: 'addToFavorites',
label: 'Add to favorites',
position: 0,
@@ -64,6 +65,7 @@ const meta: Meta<typeof RightDrawerActionMenuDropdown> = {
map.set('export', {
type: 'standard',
scope: 'record-selection',
key: 'export',
label: 'Export',
position: 1,
@@ -73,6 +75,7 @@ const meta: Meta<typeof RightDrawerActionMenuDropdown> = {
map.set('delete', {
type: 'standard',
scope: 'record-selection',
key: 'delete',
label: 'Delete',
position: 2,

View File

@@ -3,6 +3,7 @@ import { IconComponent, MenuItemAccent } from 'twenty-ui';
export type ActionMenuEntry = {
type: 'standard' | 'workflow-run';
scope: 'global' | 'record-selection';
key: string;
label: string;
position: number;

View File

@@ -61,9 +61,9 @@ export const CurrentWorkspaceMemberFavorites = ({
const selectedFavoriteIndex = folder.favorites.findIndex((favorite) =>
isLocationMatchingFavorite(currentPath, currentViewPath, favorite),
);
const handleReorderFavorite = useReorderFavorite();
const { handleReorderFavorite } = useReorderFavorite();
const deleteFavorite = useDeleteFavorite();
const { deleteFavorite } = useDeleteFavorite();
const favoriteFolderContentLength = folder.favorites.length;
@@ -154,6 +154,7 @@ export const CurrentWorkspaceMemberFavorites = ({
key={favorite.id}
draggableId={favorite.id}
index={index}
isInsideScrollableContainer
itemComponent={
<NavigationDrawerSubItem
key={favorite.id}

View File

@@ -4,8 +4,8 @@ import { useRecoilState, useRecoilValue } from 'recoil';
import {
IconFolderPlus,
IconHeartOff,
isDefined,
LightIconButton,
isDefined,
} from 'twenty-ui';
import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState';
@@ -32,9 +32,9 @@ export const CurrentWorkspaceMemberFavoritesFolders = () => {
const currentViewPath = useLocation().pathname + useLocation().search;
const theme = useTheme();
const currentWorkspaceMember = useRecoilValue(currentWorkspaceMemberState);
const favorites = useFavorites();
const deleteFavorite = useDeleteFavorite();
const handleReorderFavorite = useReorderFavorite();
const { sortedFavorites: favorites } = useFavorites();
const { deleteFavorite } = useDeleteFavorite();
const { handleReorderFavorite } = useReorderFavorite();
const [isFavoriteFolderCreating, setIsFavoriteFolderCreating] =
useRecoilState(isFavoriteFolderCreatingState);
@@ -50,24 +50,25 @@ export const CurrentWorkspaceMemberFavoritesFolders = () => {
const toggleNewFolder = () => {
setIsFavoriteFolderCreating((current) => !current);
};
const shouldDisplayFavoritesWithFeatureFlagEnabled = true;
//todo: remove this logic once feature flag gating is removed
const shouldDisplayFavoritesWithoutFeatureFlagEnabled =
favorites.length > 0 || isFavoriteFolderCreating;
const shouldDisplayFavorites = isFavoriteFolderEnabled
? shouldDisplayFavoritesWithFeatureFlagEnabled
: shouldDisplayFavoritesWithoutFeatureFlagEnabled;
if (loading && isDefined(currentWorkspaceMember)) {
return <FavoritesSkeletonLoader />;
}
const currentWorkspaceMemberFavorites = favorites.filter(
(favorite) => favorite.workspaceMemberId === currentWorkspaceMember?.id,
);
const orphanFavorites = currentWorkspaceMemberFavorites.filter(
const orphanFavorites = favorites.filter(
(favorite) => !favorite.favoriteFolderId,
);
if (
(!currentWorkspaceMemberFavorites ||
currentWorkspaceMemberFavorites.length === 0) &&
!isFavoriteFolderCreating
) {
if (!shouldDisplayFavorites) {
return null;
}
@@ -104,6 +105,7 @@ export const CurrentWorkspaceMemberFavoritesFolders = () => {
key={favorite.id}
draggableId={favorite.id}
index={index}
isInsideScrollableContainer={true}
itemComponent={
<NavigationDrawerItem
key={favorite.id}

View File

@@ -38,6 +38,7 @@ export const FavoriteFolderNavigationDrawerItemDropdown = ({
dropdownHotkeyScope={{
scope: FavoriteFolderHotkeyScope.FavoriteFolderRightIconDropdown,
}}
usePortal
data-select-disable
clickableComponent={
<LightIconButton Icon={IconDotsVertical} accent="tertiary" />

View File

@@ -18,8 +18,8 @@ export const FavoriteFolders = ({
}: FavoriteFoldersProps) => {
const [newFolderName, setNewFolderName] = useState('');
const favoritesByFolder = useFavoritesByFolder();
const createFavoriteFolder = useCreateFavoriteFolder();
const { favoritesByFolder } = useFavoritesByFolder();
const { createNewFavoriteFolder } = useCreateFavoriteFolder();
const [isFavoriteFolderCreating, setIsFavoriteFolderCreating] =
useRecoilState(isFavoriteFolderCreatingState);
@@ -33,12 +33,12 @@ export const FavoriteFolders = ({
setIsFavoriteFolderCreating(false);
setNewFolderName('');
await createFavoriteFolder(value);
await createNewFavoriteFolder(value);
return true;
};
const handleClickOutside = async (
event: MouseEvent | TouchEvent,
_event: MouseEvent | TouchEvent,
value: string,
) => {
if (!value) {
@@ -48,7 +48,7 @@ export const FavoriteFolders = ({
setIsFavoriteFolderCreating(false);
setNewFolderName('');
await createFavoriteFolder(value);
await createNewFavoriteFolder(value);
};
const handleCancelFavoriteFolderCreation = () => {

View File

@@ -1,11 +1,11 @@
import { PageFavoriteButton } from '@/favorites/components/PageFavoriteButton';
import { FavoriteFolderPicker } from '@/favorites/favorite-folder-picker/components/FavoriteFolderPicker';
import { FavoriteFolderPickerEffect } from '@/favorites/favorite-folder-picker/components/FavoriteFolderPickerEffect';
import { FavoriteFolderPickerScope } from '@/favorites/favorite-folder-picker/scopes/FavoriteFolderPickerScope';
import { FavoriteFolderPickerComponentInstanceContext } from '@/favorites/favorite-folder-picker/scopes/FavoriteFolderPickerScope';
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown';
import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown';
import { DropdownScope } from '@/ui/layout/dropdown/scopes/DropdownScope';
import { PageFavoriteButton } from '@/ui/layout/page/components/PageFavoriteButton';
type PageFavoriteFoldersDropdownProps = {
dropdownId: string;
@@ -23,7 +23,9 @@ export const PageFavoriteFoldersDropdown = ({
const { closeDropdown } = useDropdown(dropdownId);
return (
<FavoriteFolderPickerScope favoriteFoldersScopeId={dropdownId}>
<FavoriteFolderPickerComponentInstanceContext
favoriteFoldersScopeId={dropdownId}
>
<DropdownScope dropdownScopeId={dropdownId}>
<Dropdown
dropdownId={dropdownId}
@@ -44,6 +46,6 @@ export const PageFavoriteFoldersDropdown = ({
}}
/>
</DropdownScope>
</FavoriteFolderPickerScope>
</FavoriteFolderPickerComponentInstanceContext>
);
};

View File

@@ -1,10 +0,0 @@
import styled from '@emotion/styled';
import { MenuItemMultiSelect } from '@ui/navigation/menu-item/components/MenuItemMultiSelect';
const StyledNoGapMenuItem = styled(MenuItemMultiSelect)`
& > div {
gap: 0;
}
`;
export const FavoriteFolderMenuItemMultiSelect = StyledNoGapMenuItem;

View File

@@ -36,7 +36,7 @@ export const FavoriteFolderPicker = ({
FavoriteFolderPickerInstanceContext,
);
const { getFoldersByIds, toggleFolderSelection } = useFavoriteFolderPicker({
const { favoriteFolders, toggleFolderSelection } = useFavoriteFolderPicker({
record,
objectNameSingular,
});
@@ -45,8 +45,7 @@ export const FavoriteFolderPicker = ({
favoriteFolderSearchFilterComponentState,
);
const folders = getFoldersByIds();
const filteredFolders = folders.filter((folder) =>
const filteredFolders = favoriteFolders.filter((folder) =>
folder.name
.toLowerCase()
.includes(favoriteFoldersSearchFilter.toLowerCase()),
@@ -94,7 +93,7 @@ export const FavoriteFolderPicker = ({
<DropdownMenuSeparator />
<DropdownMenuItemsContainer hasMaxHeight>
<FavoriteFolderPickerList
folders={folders}
folders={favoriteFolders}
toggleFolderSelection={toggleFolderSelection}
/>
</DropdownMenuItemsContainer>

View File

@@ -30,7 +30,7 @@ export const FavoriteFolderPickerEffect = ({
const { favoriteFolders } = usePrefetchedFavoritesFoldersData();
const favorites = useFavorites();
const { sortedFavorites: favorites } = useFavorites();
const setCheckedState = useSetRecoilComponentStateV2(
favoriteFolderPickerCheckedComponentState,
);

View File

@@ -14,10 +14,6 @@ const StyledFooter = styled.div`
border-top: 1px solid ${({ theme }) => theme.border.color.light};
`;
const StyledIconPlus = styled(IconPlus)`
padding-left: ${({ theme }) => theme.spacing(1)};
`;
export const FavoriteFolderPickerFooter = () => {
const [, setIsFavoriteFolderCreating] = useRecoilState(
isFavoriteFolderCreatingState,
@@ -35,7 +31,7 @@ export const FavoriteFolderPickerFooter = () => {
closeDropdown();
}}
text="Add folder"
LeftIcon={() => <StyledIconPlus size={theme.icon.size.md} />}
LeftIcon={() => <IconPlus size={theme.icon.size.md} />}
/>
</DropdownMenuItemsContainer>
</StyledFooter>

View File

@@ -4,8 +4,7 @@ import { FavoriteFolder } from '@/favorites/types/FavoriteFolder';
import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownMenuSeparator';
import { useRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentStateV2';
import styled from '@emotion/styled';
import { MenuItem } from 'twenty-ui';
import { FavoriteFolderMenuItemMultiSelect } from './FavoriteFolderMenuItemMultiSelect';
import { MenuItem, MenuItemMultiSelect } from 'twenty-ui';
const StyledItemsContainer = styled.div`
width: 100%;
@@ -30,6 +29,7 @@ export const FavoriteFolderPickerList = ({
const [favoriteFoldersSearchFilter] = useRecoilComponentStateV2(
favoriteFolderSearchFilterComponentState,
);
const [favoriteFolderPickerChecked] = useRecoilComponentStateV2(
favoriteFolderPickerCheckedComponentState,
);
@@ -47,7 +47,7 @@ export const FavoriteFolderPickerList = ({
return (
<StyledItemsContainer>
{showNoFolderOption && (
<FavoriteFolderMenuItemMultiSelect
<MenuItemMultiSelect
key={`menu-${NO_FOLDER_ID}`}
onSelectChange={() => toggleFolderSelection(NO_FOLDER_ID)}
selected={favoriteFolderPickerChecked.includes(NO_FOLDER_ID)}
@@ -60,7 +60,7 @@ export const FavoriteFolderPickerList = ({
)}
{filteredFolders.length > 0
? filteredFolders.map((folder) => (
<FavoriteFolderMenuItemMultiSelect
<MenuItemMultiSelect
key={`menu-${folder.id}`}
onSelectChange={() => toggleFolderSelection(folder.id)}
selected={favoriteFolderPickerChecked.includes(folder.id)}

View File

@@ -1,6 +1,5 @@
import { favoriteFolderIdsPickerComponentState } from '@/favorites/favorite-folder-picker/states/favoriteFolderIdPickerComponentState';
import { favoriteFolderPickerCheckedComponentState } from '@/favorites/favorite-folder-picker/states/favoriteFolderPickerCheckedComponentState';
import { favoriteFolderPickerComponentFamilyState } from '@/favorites/favorite-folder-picker/states/favoriteFolderPickerComponentFamilyState';
import { favoriteFoldersComponentSelector } from '@/favorites/favorite-folder-picker/states/selectors/favoriteFoldersComponentSelector';
import { useCreateFavorite } from '@/favorites/hooks/useCreateFavorite';
import { useDeleteFavorite } from '@/favorites/hooks/useDeleteFavorite';
import { useFavorites } from '@/favorites/hooks/useFavorites';
@@ -8,7 +7,7 @@ import { useFavorites } from '@/favorites/hooks/useFavorites';
import { FavoriteFolder } from '@/favorites/types/FavoriteFolder';
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
import { useRecoilComponentCallbackStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackStateV2';
import { useRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentStateV2';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
import { useRecoilCallback } from 'recoil';
import { isDefined } from 'twenty-ui';
@@ -17,46 +16,26 @@ type useFavoriteFolderPickerProps = {
objectNameSingular: string;
};
type FolderOperations = {
getFoldersByIds: () => FavoriteFolder[];
type useFavoriteFolderPickerReturnType = {
favoriteFolders: FavoriteFolder[];
toggleFolderSelection: (folderId: string) => Promise<void>;
};
export const useFavoriteFolderPicker = ({
record,
objectNameSingular,
}: useFavoriteFolderPickerProps): FolderOperations => {
const [favoriteFolderIdsPicker] = useRecoilComponentStateV2(
favoriteFolderIdsPickerComponentState,
);
}: useFavoriteFolderPickerProps): useFavoriteFolderPickerReturnType => {
const favoriteFoldersMultiSelectCheckedState =
useRecoilComponentCallbackStateV2(
favoriteFolderPickerCheckedComponentState,
);
const favoriteFolderPickerFamilyState = useRecoilComponentCallbackStateV2(
favoriteFolderPickerComponentFamilyState,
);
const { sortedFavorites: favorites } = useFavorites();
const { createFavorite } = useCreateFavorite();
const { deleteFavorite } = useDeleteFavorite();
const favorites = useFavorites();
const createFavorite = useCreateFavorite();
const deleteFavorite = useDeleteFavorite();
const getFoldersByIds = useRecoilCallback(
({ snapshot }) =>
(): FavoriteFolder[] => {
return favoriteFolderIdsPicker
.map((folderId) => {
const folderValue = snapshot
.getLoadable(favoriteFolderPickerFamilyState(folderId))
.getValue();
return folderValue;
})
.filter((folder): folder is FavoriteFolder => isDefined(folder));
},
[favoriteFolderIdsPicker, favoriteFolderPickerFamilyState],
const favoriteFolders = useRecoilComponentValueV2(
favoriteFoldersComponentSelector,
);
const toggleFolderSelection = useRecoilCallback(
@@ -123,7 +102,7 @@ export const useFavoriteFolderPicker = ({
);
return {
getFoldersByIds,
favoriteFolders,
toggleFolderSelection,
};
};

View File

@@ -1,15 +1,15 @@
import { FavoriteFolderPickerInstanceContext } from '@/favorites/favorite-folder-picker/states/context/FavoriteFolderPickerInstanceContext';
import { ReactNode } from 'react';
type FavoriteFolderPickerScopeProps = {
type FavoriteFolderPickerComponentInstanceContextProps = {
children: ReactNode;
favoriteFoldersScopeId: string;
};
export const FavoriteFolderPickerScope = ({
export const FavoriteFolderPickerComponentInstanceContext = ({
children,
favoriteFoldersScopeId,
}: FavoriteFolderPickerScopeProps) => {
}: FavoriteFolderPickerComponentInstanceContextProps) => {
return (
<FavoriteFolderPickerInstanceContext.Provider
value={{ instanceId: favoriteFoldersScopeId }}

View File

@@ -1,9 +0,0 @@
import { FavoriteFolderPickerInstanceContext } from '@/favorites/favorite-folder-picker/states/context/FavoriteFolderPickerInstanceContext';
import { createComponentStateV2 } from '@/ui/utilities/state/component-state/utils/createComponentStateV2';
export const favoriteFolderLoadingComponentState =
createComponentStateV2<boolean>({
key: 'favoriteFoldersLoadingComponentState',
defaultValue: false,
componentInstanceContext: FavoriteFolderPickerInstanceContext,
});

View File

@@ -0,0 +1,31 @@
import { FavoriteFolderPickerInstanceContext } from '@/favorites/favorite-folder-picker/states/context/FavoriteFolderPickerInstanceContext';
import { favoriteFolderIdsPickerComponentState } from '@/favorites/favorite-folder-picker/states/favoriteFolderIdPickerComponentState';
import { favoriteFolderPickerComponentFamilyState } from '@/favorites/favorite-folder-picker/states/favoriteFolderPickerComponentFamilyState';
import { FavoriteFolder } from '@/favorites/types/FavoriteFolder';
import { createComponentSelectorV2 } from '@/ui/utilities/state/component-state/utils/createComponentSelectorV2';
import { isDefined } from 'twenty-ui';
export const favoriteFoldersComponentSelector = createComponentSelectorV2<
FavoriteFolder[]
>({
key: 'favoriteFoldersComponentSelector',
componentInstanceContext: FavoriteFolderPickerInstanceContext,
get:
({ instanceId }) =>
({ get }) => {
const folderIds = get(
favoriteFolderIdsPickerComponentState.atomFamily({ instanceId }),
);
return folderIds
.map((folderId: string) =>
get(
favoriteFolderPickerComponentFamilyState.atomFamily({
instanceId,
familyKey: folderId,
}),
),
)
.filter((folder): folder is FavoriteFolder => isDefined(folder));
},
});

View File

@@ -44,7 +44,10 @@ describe('useCreateFavorite', () => {
{ wrapper: Wrapper },
);
result.current(favoriteTargetObjectRecord, CoreObjectNameSingular.Person);
result.current.createFavorite(
favoriteTargetObjectRecord,
CoreObjectNameSingular.Person,
);
await waitFor(() => {
expect(mocks[0].result).toHaveBeenCalled();

View File

@@ -38,7 +38,7 @@ describe('useDeleteFavorite', () => {
{ wrapper: Wrapper },
);
result.current(favoriteId);
result.current.deleteFavorite(favoriteId);
await waitFor(() => {
expect(mocks[1].result).toHaveBeenCalled();

View File

@@ -38,6 +38,6 @@ describe('useFavorites', () => {
{ wrapper: Wrapper },
);
expect(result.current).toEqual(sortedFavorites);
expect(result.current.sortedFavorites).toEqual(sortedFavorites);
});
});

View File

@@ -54,7 +54,10 @@ describe('useReorderFavorite', () => {
announce: () => {},
};
result.current(dragAndDropResult, responderProvided);
result.current.handleReorderFavorite(
dragAndDropResult,
responderProvided,
);
});
await waitFor(() => {

View File

@@ -34,5 +34,5 @@ export const useCreateFavorite = () => {
});
};
return createFavorite;
return { createFavorite };
};

View File

@@ -28,5 +28,5 @@ export const useCreateFavoriteFolder = () => {
});
};
return createNewFavoriteFolder;
return { createNewFavoriteFolder };
};

View File

@@ -10,5 +10,5 @@ export const useDeleteFavorite = () => {
deleteOneRecord(favoriteId);
};
return deleteFavorite;
return { deleteFavorite };
};

View File

@@ -1,23 +1,46 @@
import { Favorite } from '@/favorites/types/Favorite';
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { useReadFindManyRecordsQueryInCache } from '@/object-record/cache/hooks/useReadFindManyRecordsQueryInCache';
import { useDeleteOneRecord } from '@/object-record/hooks/useDeleteOneRecord';
import { usePrefetchedFavoritesData } from './usePrefetchedFavoritesData';
import { PREFETCH_CONFIG } from '@/prefetch/constants/PrefetchConfig';
import { usePrefetchRunQuery } from '@/prefetch/hooks/internal/usePrefetchRunQuery';
import { PrefetchKey } from '@/prefetch/types/PrefetchKey';
export const useDeleteFavoriteFolder = () => {
const { deleteOneRecord } = useDeleteOneRecord({
objectNameSingular: CoreObjectNameSingular.FavoriteFolder,
});
const { upsertFavorites, favorites, workspaceFavorites } =
usePrefetchedFavoritesData();
const { upsertRecordsInCache } = usePrefetchRunQuery<Favorite>({
prefetchKey: PrefetchKey.AllFavorites,
});
const { objectMetadataItem } = useObjectMetadataItem({
objectNameSingular:
PREFETCH_CONFIG[PrefetchKey.AllFavorites].objectNameSingular,
});
const { readFindManyRecordsQueryInCache } =
useReadFindManyRecordsQueryInCache({
objectMetadataItem,
});
const deleteFavoriteFolder = async (folderId: string): Promise<void> => {
await deleteOneRecord(folderId);
const updatedFavorites = [
...favorites.filter((favorite) => favorite.favoriteFolderId !== folderId),
...workspaceFavorites,
];
const allFavorites = readFindManyRecordsQueryInCache<Favorite>({
queryVariables: {},
recordGqlFields: PREFETCH_CONFIG[
PrefetchKey.AllFavorites
].operationSignatureFactory({ objectMetadataItem }).fields,
});
upsertFavorites(updatedFavorites);
const updatedFavorites = allFavorites.filter(
(favorite) => favorite.favoriteFolderId !== folderId,
);
upsertRecordsInCache(updatedFavorites);
};
return {

View File

@@ -52,5 +52,5 @@ export const useFavorites = () => {
],
);
return sortedFavorites;
return { sortedFavorites };
};

View File

@@ -1,62 +1,30 @@
import { sortFavorites } from '@/favorites/utils/sortFavorites';
import { useGetObjectRecordIdentifierByNameSingular } from '@/object-metadata/hooks/useGetObjectRecordIdentifierByNameSingular';
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { usePrefetchedData } from '@/prefetch/hooks/usePrefetchedData';
import { PrefetchKey } from '@/prefetch/types/PrefetchKey';
import { View } from '@/views/types/View';
import { useMemo } from 'react';
import { useRecoilValue } from 'recoil';
import { FieldMetadataType } from '~/generated-metadata/graphql';
import { useFavoritesMetadata } from './useFavoritesMetadata';
import { usePrefetchedFavoritesData } from './usePrefetchedFavoritesData';
import { usePrefetchedFavoritesFoldersData } from './usePrefetchedFavoritesFoldersData';
export const useFavoritesByFolder = () => {
const { favorites } = usePrefetchedFavoritesData();
const { favoriteFolders } = usePrefetchedFavoritesFoldersData();
const { records: views } = usePrefetchedData<View>(PrefetchKey.AllViews);
const objectMetadataItems = useRecoilValue(objectMetadataItemsState);
const getObjectRecordIdentifierByNameSingular =
useGetObjectRecordIdentifierByNameSingular();
const { objectMetadataItem: favoriteObjectMetadataItem } =
useObjectMetadataItem({
objectNameSingular: CoreObjectNameSingular.Favorite,
});
const favoriteRelationFields = useMemo(
() =>
favoriteObjectMetadataItem.fields.filter(
(fieldMetadataItem) =>
fieldMetadataItem.type === FieldMetadataType.Relation &&
fieldMetadataItem.name !== 'workspaceMember' &&
fieldMetadataItem.name !== 'favoriteFolder',
),
[favoriteObjectMetadataItem.fields],
);
const favoritesByFolder = useMemo(() => {
return favoriteFolders.map((folder) => ({
folderId: folder.id,
folderName: folder.name,
favorites: sortFavorites(
favorites.filter((favorite) => favorite.favoriteFolderId === folder.id),
favoriteRelationFields,
getObjectRecordIdentifierByNameSingular,
true,
views,
objectMetadataItems,
),
}));
}, [
favoriteFolders,
favorites,
favoriteRelationFields,
getObjectRecordIdentifierByNameSingular,
const {
views,
objectMetadataItems,
]);
getObjectRecordIdentifierByNameSingular,
favoriteRelationFields,
} = useFavoritesMetadata();
return favoritesByFolder;
const favoritesByFolder = favoriteFolders.map((folder) => ({
folderId: folder.id,
folderName: folder.name,
favorites: sortFavorites(
favorites.filter((favorite) => favorite.favoriteFolderId === folder.id),
favoriteRelationFields,
getObjectRecordIdentifierByNameSingular,
true,
views,
objectMetadataItems,
),
}));
return { favoritesByFolder };
};

View File

@@ -0,0 +1,35 @@
import { useGetObjectRecordIdentifierByNameSingular } from '@/object-metadata/hooks/useGetObjectRecordIdentifierByNameSingular';
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { usePrefetchedData } from '@/prefetch/hooks/usePrefetchedData';
import { PrefetchKey } from '@/prefetch/types/PrefetchKey';
import { View } from '@/views/types/View';
import { useRecoilValue } from 'recoil';
import { FieldMetadataType } from '~/generated-metadata/graphql';
export const useFavoritesMetadata = () => {
const { records: views } = usePrefetchedData<View>(PrefetchKey.AllViews);
const objectMetadataItems = useRecoilValue(objectMetadataItemsState);
const getObjectRecordIdentifierByNameSingular =
useGetObjectRecordIdentifierByNameSingular();
const { objectMetadataItem: favoriteObjectMetadataItem } =
useObjectMetadataItem({
objectNameSingular: CoreObjectNameSingular.Favorite,
});
const favoriteRelationFields = favoriteObjectMetadataItem.fields.filter(
(fieldMetadataItem) =>
fieldMetadataItem.type === FieldMetadataType.Relation &&
fieldMetadataItem.name !== 'workspaceMember' &&
fieldMetadataItem.name !== 'favoriteFolder',
);
return {
views,
objectMetadataItems,
getObjectRecordIdentifierByNameSingular,
favoriteRelationFields,
};
};

View File

@@ -12,7 +12,7 @@ export const useReorderFavorite = () => {
objectNameSingular: CoreObjectNameSingular.Favorite,
});
const reorderFavorite: OnDragEndResponder = (result) => {
const handleReorderFavorite: OnDragEndResponder = (result) => {
if (!result.destination) return;
const draggedFavoriteId = result.draggableId;
@@ -37,5 +37,5 @@ export const useReorderFavorite = () => {
});
};
return reorderFavorite;
return { handleReorderFavorite };
};

View File

@@ -1,50 +1,28 @@
import { sortFavorites } from '@/favorites/utils/sortFavorites';
import { useGetObjectRecordIdentifierByNameSingular } from '@/object-metadata/hooks/useGetObjectRecordIdentifierByNameSingular';
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { usePrefetchedData } from '@/prefetch/hooks/usePrefetchedData';
import { PrefetchKey } from '@/prefetch/types/PrefetchKey';
import { View } from '@/views/types/View';
import { useMemo } from 'react';
import { useRecoilValue } from 'recoil';
import { FieldMetadataType } from '~/generated-metadata/graphql';
import { useFavoritesMetadata } from './useFavoritesMetadata';
import { usePrefetchedFavoritesData } from './usePrefetchedFavoritesData';
export const useSortedFavorites = () => {
const { favorites, workspaceFavorites } = usePrefetchedFavoritesData();
const { records: views } = usePrefetchedData<View>(PrefetchKey.AllViews);
const objectMetadataItems = useRecoilValue(objectMetadataItemsState);
const { objectMetadataItem: favoriteObjectMetadataItem } =
useObjectMetadataItem({
objectNameSingular: CoreObjectNameSingular.Favorite,
});
const getObjectRecordIdentifierByNameSingular =
useGetObjectRecordIdentifierByNameSingular();
const favoriteRelationFieldMetadataItems = useMemo(
() =>
favoriteObjectMetadataItem.fields.filter(
(fieldMetadataItem) =>
fieldMetadataItem.type === FieldMetadataType.Relation &&
fieldMetadataItem.name !== 'workspaceMember' &&
fieldMetadataItem.name !== 'favoriteFolder',
),
[favoriteObjectMetadataItem.fields],
);
const {
views,
objectMetadataItems,
getObjectRecordIdentifierByNameSingular,
favoriteRelationFields,
} = useFavoritesMetadata();
const favoritesSorted = useMemo(() => {
return sortFavorites(
favorites,
favoriteRelationFieldMetadataItems,
favoriteRelationFields,
getObjectRecordIdentifierByNameSingular,
true,
views,
objectMetadataItems,
);
}, [
favoriteRelationFieldMetadataItems,
favoriteRelationFields,
favorites,
getObjectRecordIdentifierByNameSingular,
views,
@@ -54,14 +32,14 @@ export const useSortedFavorites = () => {
const workspaceFavoritesSorted = useMemo(() => {
return sortFavorites(
workspaceFavorites.filter((favorite) => favorite.viewId),
favoriteRelationFieldMetadataItems,
favoriteRelationFields,
getObjectRecordIdentifierByNameSingular,
false,
views,
objectMetadataItems,
);
}, [
favoriteRelationFieldMetadataItems,
favoriteRelationFields,
getObjectRecordIdentifierByNameSingular,
workspaceFavorites,
views,

View File

@@ -52,5 +52,5 @@ export const useWorkspaceFavorites = () => {
],
);
return sortedWorkspaceFavorites;
return { sortedWorkspaceFavorites };
};

View File

@@ -6,23 +6,20 @@ import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu';
import { CurrentWorkspaceMemberFavoritesFolders } from '@/favorites/components/CurrentWorkspaceMemberFavoritesFolders';
import { WorkspaceFavorites } from '@/favorites/components/WorkspaceFavorites';
import { NavigationDrawerOpenedSection } from '@/object-metadata/components/NavigationDrawerOpenedSection';
import { NavigationDrawerSectionForObjectMetadataItemsWrapper } from '@/object-metadata/components/NavigationDrawerSectionForObjectMetadataItemsWrapper';
import { RemoteNavigationDrawerSection } from '@/object-metadata/components/RemoteNavigationDrawerSection';
import { NavigationDrawerItem } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerItem';
import { NavigationDrawerSection } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerSection';
import { isNavigationDrawerExpandedState } from '@/ui/navigation/states/isNavigationDrawerExpanded';
import { navigationDrawerExpandedMemorizedState } from '@/ui/navigation/states/navigationDrawerExpandedMemorizedState';
import { navigationMemorizedUrlState } from '@/ui/navigation/states/navigationMemorizedUrlState';
import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile';
import { ScrollWrapper } from '@/ui/utilities/scroll/components/ScrollWrapper';
import styled from '@emotion/styled';
const StyledMainSection = styled(NavigationDrawerSection)`
min-height: fit-content;
`;
const StyledContainer = styled.div`
overflow-x: hidden;
overflow-y: auto;
`;
export const MainNavigationDrawerItems = () => {
const isMobile = useIsMobile();
const { toggleCommandMenu } = useCommandMenu();
@@ -59,15 +56,16 @@ export const MainNavigationDrawerItems = () => {
/>
</StyledMainSection>
)}
<StyledContainer>
<ScrollWrapper
contextProviderName="navigationDrawer"
enableXScroll={false}
scrollHide={true}
>
<NavigationDrawerOpenedSection />
<CurrentWorkspaceMemberFavoritesFolders />
<WorkspaceFavorites />
<NavigationDrawerSectionForObjectMetadataItemsWrapper isRemote={true} />
</StyledContainer>
<RemoteNavigationDrawerSection />
</ScrollWrapper>
</>
);
};

View File

@@ -7,7 +7,8 @@ import { View } from '@/views/types/View';
export const useFilteredObjectMetadataItemsForWorkspaceFavorites = () => {
const { records: views } = usePrefetchedData<View>(PrefetchKey.AllViews);
const workspaceFavorites = useWorkspaceFavorites();
const { sortedWorkspaceFavorites: workspaceFavorites } =
useWorkspaceFavorites();
const workspaceFavoriteIds = new Set(
workspaceFavorites.map((favorite) => favorite.recordId),

View File

@@ -4,7 +4,6 @@ import { NavigationDrawerAnimatedCollapseWrapper } from '@/ui/navigation/navigat
import { NavigationDrawerSection } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerSection';
import { NavigationDrawerSectionTitle } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerSectionTitle';
import { useNavigationSection } from '@/ui/navigation/navigation-drawer/hooks/useNavigationSection';
import { ScrollWrapper } from '@/ui/utilities/scroll/components/ScrollWrapper';
import styled from '@emotion/styled';
import { useRecoilValue } from 'recoil';
@@ -78,19 +77,15 @@ export const NavigationDrawerSectionForObjectMetadataItems = ({
onClick={() => toggleNavigationSection()}
/>
</NavigationDrawerAnimatedCollapseWrapper>
<ScrollWrapper contextProviderName="navigationDrawer">
<StyledObjectsMetaDataItemsWrapper>
{isNavigationSectionOpen &&
objectMetadataItemsForNavigationItems.map(
(objectMetadataItem) => (
<NavigationDrawerItemForObjectMetadataItem
key={`navigation-drawer-item-${objectMetadataItem.id}`}
objectMetadataItem={objectMetadataItem}
/>
),
)}
</StyledObjectsMetaDataItemsWrapper>
</ScrollWrapper>
<StyledObjectsMetaDataItemsWrapper>
{isNavigationSectionOpen &&
objectMetadataItemsForNavigationItems.map((objectMetadataItem) => (
<NavigationDrawerItemForObjectMetadataItem
key={`navigation-drawer-item-${objectMetadataItem.id}`}
objectMetadataItem={objectMetadataItem}
/>
))}
</StyledObjectsMetaDataItemsWrapper>
</NavigationDrawerSection>
)
);

View File

@@ -7,16 +7,12 @@ import { NavigationDrawerSectionForObjectMetadataItemsSkeletonLoader } from '@/o
import { useFilteredObjectMetadataItems } from '@/object-metadata/hooks/useFilteredObjectMetadataItems';
import { useIsPrefetchLoading } from '@/prefetch/hooks/useIsPrefetchLoading';
export const NavigationDrawerSectionForObjectMetadataItemsWrapper = ({
isRemote,
}: {
isRemote: boolean;
}) => {
export const RemoteNavigationDrawerSection = () => {
const currentUser = useRecoilValue(currentUserState);
const { activeObjectMetadataItems } = useFilteredObjectMetadataItems();
const filteredActiveObjectMetadataItems = activeObjectMetadataItems.filter(
(item) => (isRemote ? item.isRemote : !item.isRemote),
(item) => item.isRemote,
);
const loading = useIsPrefetchLoading();
@@ -26,9 +22,9 @@ export const NavigationDrawerSectionForObjectMetadataItemsWrapper = ({
return (
<NavigationDrawerSectionForObjectMetadataItems
sectionTitle={isRemote ? 'Remote' : 'Workspace'}
sectionTitle={'Remote'}
objectMetadataItems={filteredActiveObjectMetadataItems}
isRemote={isRemote}
isRemote={true}
/>
);
};

View File

@@ -1,44 +0,0 @@
import { Meta, StoryObj } from '@storybook/react';
import { ComponentWithRecoilScopeDecorator } from '~/testing/decorators/ComponentWithRecoilScopeDecorator';
import { ComponentWithRouterDecorator } from '~/testing/decorators/ComponentWithRouterDecorator';
import { IconsProviderDecorator } from '~/testing/decorators/IconsProviderDecorator';
import { ObjectMetadataItemsDecorator } from '~/testing/decorators/ObjectMetadataItemsDecorator';
import { SnackBarDecorator } from '~/testing/decorators/SnackBarDecorator';
import { graphqlMocks } from '~/testing/graphqlMocks';
import { NavigationDrawerSectionForObjectMetadataItemsWrapper } from '@/object-metadata/components/NavigationDrawerSectionForObjectMetadataItemsWrapper';
import { within } from '@storybook/test';
import { PrefetchLoadedDecorator } from '~/testing/decorators/PrefetchLoadedDecorator';
const meta: Meta<typeof NavigationDrawerSectionForObjectMetadataItemsWrapper> =
{
title:
'Modules/ObjectMetadata/NavigationDrawerSectionForObjectMetadataItemsWrapper',
component: NavigationDrawerSectionForObjectMetadataItemsWrapper,
decorators: [
IconsProviderDecorator,
ObjectMetadataItemsDecorator,
ComponentWithRouterDecorator,
ComponentWithRecoilScopeDecorator,
SnackBarDecorator,
PrefetchLoadedDecorator,
],
parameters: {
msw: graphqlMocks,
},
};
export default meta;
type Story = StoryObj<
typeof NavigationDrawerSectionForObjectMetadataItemsWrapper
>;
export const Default: Story = {
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
await canvas.findByText('People', undefined, { timeout: 10000 });
await canvas.findByText('Companies');
await canvas.findByText('Opportunities');
},
};

View File

@@ -109,6 +109,7 @@ export type PhonesFilter = {
export type SelectFilter = {
is?: IsFilter;
in?: string[];
eq?: string;
};
export type MultiSelectFilter = {

View File

@@ -5,7 +5,6 @@ import { useRecoilValue } from 'recoil';
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
import { mapObjectMetadataToGraphQLQuery } from '@/object-metadata/utils/mapObjectMetadataToGraphQLQuery';
import { RecordGqlOperationSignature } from '@/object-record/graphql/types/RecordGqlOperationSignature';
import { generateDepthOneRecordGqlFields } from '@/object-record/graphql/utils/generateDepthOneRecordGqlFields';
import { getSearchRecordsQueryResponseField } from '@/object-record/utils/getSearchRecordsQueryResponseField';
import { isObjectMetadataItemSearchable } from '@/object-record/utils/isObjectMetadataItemSearchable';
import { isNonEmptyArray } from '~/utils/isNonEmptyArray';
@@ -68,7 +67,7 @@ export const useGenerateCombinedSearchRecordsQuery = ({
) {
${filteredQueryKeyWithObjectMetadataItemArray
.map(
({ objectMetadataItem, fields }) =>
({ objectMetadataItem }) =>
`${getSearchRecordsQueryResponseField(objectMetadataItem.namePlural)}(filter: $filter${capitalize(
objectMetadataItem.nameSingular,
)},
@@ -79,11 +78,6 @@ export const useGenerateCombinedSearchRecordsQuery = ({
node ${mapObjectMetadataToGraphQLQuery({
objectMetadataItems: objectMetadataItems,
objectMetadataItem,
recordGqlFields:
fields ??
generateDepthOneRecordGqlFields({
objectMetadataItem,
}),
})}
cursor
}

View File

@@ -0,0 +1,57 @@
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { ObjectOptionsDropdownButton } from '@/object-record/object-options-dropdown/components/ObjectOptionsDropdownButton';
import { ObjectOptionsDropdownContent } from '@/object-record/object-options-dropdown/components/ObjectOptionsDropdownContent';
import { OBJECT_OPTIONS_DROPDOWN_ID } from '@/object-record/object-options-dropdown/constants/ObjectOptionsDropdownId';
import { ObjectOptionsDropdownContext } from '@/object-record/object-options-dropdown/states/contexts/ObjectOptionsDropdownContext';
import { ObjectOptionsContentId } from '@/object-record/object-options-dropdown/types/ObjectOptionsContentId';
import { TableOptionsHotkeyScope } from '@/object-record/record-table/types/TableOptionsHotkeyScope';
import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown';
import { ViewType } from '@/views/types/ViewType';
import { useCallback, useState } from 'react';
type ObjectOptionsDropdownProps = {
viewType: ViewType;
objectMetadataItem: ObjectMetadataItem;
recordIndexId: string;
};
export const ObjectOptionsDropdown = ({
recordIndexId,
objectMetadataItem,
viewType,
}: ObjectOptionsDropdownProps) => {
const [currentContentId, setCurrentContentId] =
useState<ObjectOptionsContentId | null>(null);
const handleContentChange = useCallback((key: ObjectOptionsContentId) => {
setCurrentContentId(key);
}, []);
const handleResetContent = useCallback(() => {
setCurrentContentId(null);
}, []);
return (
<Dropdown
dropdownId={OBJECT_OPTIONS_DROPDOWN_ID}
clickableComponent={<ObjectOptionsDropdownButton />}
dropdownMenuWidth={'200px'}
dropdownHotkeyScope={{ scope: TableOptionsHotkeyScope.Dropdown }}
dropdownOffset={{ y: 8 }}
dropdownComponents={
<ObjectOptionsDropdownContext.Provider
value={{
viewType,
objectMetadataItem,
recordIndexId,
currentContentId,
onContentChange: handleContentChange,
resetContent: handleResetContent,
}}
>
<ObjectOptionsDropdownContent />
</ObjectOptionsDropdownContext.Provider>
}
/>
);
};

View File

@@ -1,10 +1,10 @@
import { RECORD_INDEX_OPTIONS_DROPDOWN_ID } from '@/object-record/record-index/options/constants/RecordIndexOptionsDropdownId';
import { OBJECT_OPTIONS_DROPDOWN_ID } from '@/object-record/object-options-dropdown/constants/ObjectOptionsDropdownId';
import { StyledHeaderDropdownButton } from '@/ui/layout/dropdown/components/StyledHeaderDropdownButton';
import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown';
export const RecordIndexOptionsDropdownButton = () => {
export const ObjectOptionsDropdownButton = () => {
const { isDropdownOpen, toggleDropdown } = useDropdown(
RECORD_INDEX_OPTIONS_DROPDOWN_ID,
OBJECT_OPTIONS_DROPDOWN_ID,
);
return (

View File

@@ -0,0 +1,32 @@
import { ObjectOptionsDropdownFieldsContent } from '@/object-record/object-options-dropdown/components/ObjectOptionsDropdownFieldsContent';
import { ObjectOptionsDropdownHiddenFieldsContent } from '@/object-record/object-options-dropdown/components/ObjectOptionsDropdownHiddenFieldsContent';
import { ObjectOptionsDropdownHiddenRecordGroupsContent } from '@/object-record/object-options-dropdown/components/ObjectOptionsDropdownHiddenRecordGroupsContent';
import { ObjectOptionsDropdownMenuContent } from '@/object-record/object-options-dropdown/components/ObjectOptionsDropdownMenuContent';
import { ObjectOptionsDropdownRecordGroupFieldsContent } from '@/object-record/object-options-dropdown/components/ObjectOptionsDropdownRecordGroupFieldsContent';
import { ObjectOptionsDropdownRecordGroupsContent } from '@/object-record/object-options-dropdown/components/ObjectOptionsDropdownRecordGroupsContent';
import { ObjectOptionsDropdownRecordGroupSortContent } from '@/object-record/object-options-dropdown/components/ObjectOptionsDropdownRecordGroupSortContent';
import { ObjectOptionsDropdownViewSettingsContent } from '@/object-record/object-options-dropdown/components/ObjectOptionsDropdownViewSettingsContent';
import { useOptionsDropdown } from '@/object-record/object-options-dropdown/hooks/useOptionsDropdown';
export const ObjectOptionsDropdownContent = () => {
const { currentContentId } = useOptionsDropdown();
switch (currentContentId) {
case 'viewSettings':
return <ObjectOptionsDropdownViewSettingsContent />;
case 'fields':
return <ObjectOptionsDropdownFieldsContent />;
case 'hiddenFields':
return <ObjectOptionsDropdownHiddenFieldsContent />;
case 'recordGroups':
return <ObjectOptionsDropdownRecordGroupsContent />;
case 'recordGroupFields':
return <ObjectOptionsDropdownRecordGroupFieldsContent />;
case 'recordGroupSort':
return <ObjectOptionsDropdownRecordGroupSortContent />;
case 'hiddenRecordGroups':
return <ObjectOptionsDropdownHiddenRecordGroupsContent />;
default:
return <ObjectOptionsDropdownMenuContent />;
}
};

View File

@@ -0,0 +1,77 @@
import { IconChevronLeft, IconEyeOff, MenuItemNavigate } from 'twenty-ui';
import { useObjectOptionsForBoard } from '@/object-record/object-options-dropdown/hooks/useObjectOptionsForBoard';
import { useObjectOptionsForTable } from '@/object-record/object-options-dropdown/hooks/useObjectOptionsForTable';
import { useOptionsDropdown } from '@/object-record/object-options-dropdown/hooks/useOptionsDropdown';
import { DropdownMenuHeader } from '@/ui/layout/dropdown/components/DropdownMenuHeader';
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownMenuSeparator';
import { ScrollWrapper } from '@/ui/utilities/scroll/components/ScrollWrapper';
import { ViewFieldsVisibilityDropdownSection } from '@/views/components/ViewFieldsVisibilityDropdownSection';
import { ViewType } from '@/views/types/ViewType';
export const ObjectOptionsDropdownFieldsContent = () => {
const {
viewType,
recordIndexId,
objectMetadataItem,
onContentChange,
resetContent,
} = useOptionsDropdown();
const {
handleColumnVisibilityChange,
handleReorderColumns,
visibleTableColumns,
} = useObjectOptionsForTable(recordIndexId);
const {
visibleBoardFields,
handleReorderBoardFields,
handleBoardFieldVisibilityChange,
} = useObjectOptionsForBoard({
objectNameSingular: objectMetadataItem.nameSingular,
recordBoardId: recordIndexId,
viewBarId: recordIndexId,
});
const visibleRecordFields =
viewType === ViewType.Kanban ? visibleBoardFields : visibleTableColumns;
const handleReorderFields =
viewType === ViewType.Kanban
? handleReorderBoardFields
: handleReorderColumns;
const handleChangeFieldVisibility =
viewType === ViewType.Kanban
? handleBoardFieldVisibilityChange
: handleColumnVisibilityChange;
return (
<>
<DropdownMenuHeader StartIcon={IconChevronLeft} onClick={resetContent}>
Fields
</DropdownMenuHeader>
<ScrollWrapper contextProviderName="dropdownMenuItemsContainer">
<ViewFieldsVisibilityDropdownSection
title="Visible"
fields={visibleRecordFields}
isDraggable
onDragEnd={handleReorderFields}
onVisibilityChange={handleChangeFieldVisibility}
showSubheader={false}
showDragGrip={true}
/>
</ScrollWrapper>
<DropdownMenuSeparator />
<DropdownMenuItemsContainer>
<MenuItemNavigate
onClick={() => onContentChange('hiddenFields')}
LeftIcon={IconEyeOff}
text="Hidden Fields"
/>
</DropdownMenuItemsContainer>
</>
);
};

View File

@@ -0,0 +1,100 @@
import { useLocation } from 'react-router-dom';
import { useSetRecoilState } from 'recoil';
import {
IconChevronLeft,
IconSettings,
MenuItem,
UndecoratedLink,
} from 'twenty-ui';
import { useObjectNamePluralFromSingular } from '@/object-metadata/hooks/useObjectNamePluralFromSingular';
import { useObjectOptionsForBoard } from '@/object-record/object-options-dropdown/hooks/useObjectOptionsForBoard';
import { useObjectOptionsForTable } from '@/object-record/object-options-dropdown/hooks/useObjectOptionsForTable';
import { useOptionsDropdown } from '@/object-record/object-options-dropdown/hooks/useOptionsDropdown';
import { getSettingsPagePath } from '@/settings/utils/getSettingsPagePath';
import { SettingsPath } from '@/types/SettingsPath';
import { DropdownMenuHeader } from '@/ui/layout/dropdown/components/DropdownMenuHeader';
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownMenuSeparator';
import { navigationMemorizedUrlState } from '@/ui/navigation/states/navigationMemorizedUrlState';
import { ScrollWrapper } from '@/ui/utilities/scroll/components/ScrollWrapper';
import { ViewFieldsVisibilityDropdownSection } from '@/views/components/ViewFieldsVisibilityDropdownSection';
import { ViewType } from '@/views/types/ViewType';
export const ObjectOptionsDropdownHiddenFieldsContent = () => {
const {
viewType,
recordIndexId,
objectMetadataItem,
onContentChange,
closeDropdown,
} = useOptionsDropdown();
const { objectNamePlural } = useObjectNamePluralFromSingular({
objectNameSingular: objectMetadataItem.nameSingular,
});
const settingsUrl = getSettingsPagePath(SettingsPath.ObjectDetail, {
objectSlug: objectNamePlural,
});
const { handleColumnVisibilityChange, hiddenTableColumns } =
useObjectOptionsForTable(recordIndexId);
const { hiddenBoardFields, handleBoardFieldVisibilityChange } =
useObjectOptionsForBoard({
objectNameSingular: objectMetadataItem.nameSingular,
recordBoardId: recordIndexId,
viewBarId: recordIndexId,
});
const hiddenRecordFields =
viewType === ViewType.Kanban ? hiddenBoardFields : hiddenTableColumns;
const handleChangeFieldVisibility =
viewType === ViewType.Kanban
? handleBoardFieldVisibilityChange
: handleColumnVisibilityChange;
const location = useLocation();
const setNavigationMemorizedUrl = useSetRecoilState(
navigationMemorizedUrlState,
);
return (
<>
<DropdownMenuHeader
StartIcon={IconChevronLeft}
onClick={() => onContentChange('fields')}
>
Hidden Fields
</DropdownMenuHeader>
{hiddenRecordFields.length > 0 && (
<ScrollWrapper contextProviderName="dropdownMenuItemsContainer">
<ViewFieldsVisibilityDropdownSection
title="Hidden"
fields={hiddenRecordFields}
isDraggable={false}
onVisibilityChange={handleChangeFieldVisibility}
showSubheader={false}
showDragGrip={false}
/>
</ScrollWrapper>
)}
<DropdownMenuSeparator />
<UndecoratedLink
to={settingsUrl}
onClick={() => {
setNavigationMemorizedUrl(location.pathname + location.search);
closeDropdown();
}}
>
<DropdownMenuItemsContainer>
<MenuItem LeftIcon={IconSettings} text="Edit Fields" />
</DropdownMenuItemsContainer>
</UndecoratedLink>
</>
);
};

View File

@@ -0,0 +1,103 @@
import { useEffect } from 'react';
import {
IconChevronLeft,
IconSettings,
MenuItem,
UndecoratedLink,
} from 'twenty-ui';
import { useObjectNamePluralFromSingular } from '@/object-metadata/hooks/useObjectNamePluralFromSingular';
import { useOptionsDropdown } from '@/object-record/object-options-dropdown/hooks/useOptionsDropdown';
import { RecordGroupsVisibilityDropdownSection } from '@/object-record/record-group/components/RecordGroupsVisibilityDropdownSection';
import { useRecordGroups } from '@/object-record/record-group/hooks/useRecordGroups';
import { useRecordGroupVisibility } from '@/object-record/record-group/hooks/useRecordGroupVisibility';
import { getSettingsPagePath } from '@/settings/utils/getSettingsPagePath';
import { SettingsPath } from '@/types/SettingsPath';
import { DropdownMenuHeader } from '@/ui/layout/dropdown/components/DropdownMenuHeader';
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownMenuSeparator';
import { navigationMemorizedUrlState } from '@/ui/navigation/states/navigationMemorizedUrlState';
import { useLocation } from 'react-router-dom';
import { useSetRecoilState } from 'recoil';
export const ObjectOptionsDropdownHiddenRecordGroupsContent = () => {
const {
currentContentId,
viewType,
recordIndexId,
objectMetadataItem,
onContentChange,
closeDropdown,
} = useOptionsDropdown();
const { objectNamePlural } = useObjectNamePluralFromSingular({
objectNameSingular: objectMetadataItem.nameSingular,
});
const { hiddenRecordGroups, viewGroupFieldMetadataItem } = useRecordGroups({
objectNameSingular: objectMetadataItem.nameSingular,
});
const { handleVisibilityChange: handleRecordGroupVisibilityChange } =
useRecordGroupVisibility({
viewBarId: recordIndexId,
viewType,
});
const viewGroupSettingsUrl = getSettingsPagePath(
SettingsPath.ObjectFieldEdit,
{
objectSlug: objectNamePlural,
fieldSlug: viewGroupFieldMetadataItem?.name ?? '',
},
);
const location = useLocation();
const setNavigationMemorizedUrl = useSetRecoilState(
navigationMemorizedUrlState,
);
useEffect(() => {
if (
currentContentId === 'hiddenRecordGroups' &&
hiddenRecordGroups.length === 0
) {
onContentChange('recordGroups');
}
}, [hiddenRecordGroups, currentContentId, onContentChange]);
return (
<>
<DropdownMenuItemsContainer>
<DropdownMenuHeader
StartIcon={IconChevronLeft}
onClick={() => onContentChange('recordGroups')}
>
Hidden {viewGroupFieldMetadataItem?.label}
</DropdownMenuHeader>
</DropdownMenuItemsContainer>
<RecordGroupsVisibilityDropdownSection
title={`Hidden ${viewGroupFieldMetadataItem?.label}`}
recordGroups={hiddenRecordGroups}
onVisibilityChange={handleRecordGroupVisibilityChange}
isDraggable={false}
showSubheader={false}
showDragGrip={false}
/>
<DropdownMenuSeparator />
<UndecoratedLink
to={viewGroupSettingsUrl}
onClick={() => {
setNavigationMemorizedUrl(location.pathname + location.search);
closeDropdown();
}}
>
<DropdownMenuItemsContainer>
<MenuItem LeftIcon={IconSettings} text="Edit field values" />
</DropdownMenuItemsContainer>
</UndecoratedLink>
</>
);
};

View File

@@ -0,0 +1,145 @@
import { Key } from 'ts-key-enum';
import {
IconFileExport,
IconFileImport,
IconLayout,
IconLayoutList,
IconList,
IconRotate2,
IconTag,
MenuItem,
} from 'twenty-ui';
import { useObjectNamePluralFromSingular } from '@/object-metadata/hooks/useObjectNamePluralFromSingular';
import { useHandleToggleTrashColumnFilter } from '@/object-record/record-index/hooks/useHandleToggleTrashColumnFilter';
import { useObjectOptionsForBoard } from '@/object-record/object-options-dropdown/hooks/useObjectOptionsForBoard';
import { useOptionsDropdown } from '@/object-record/object-options-dropdown/hooks/useOptionsDropdown';
import { useRecordGroups } from '@/object-record/record-group/hooks/useRecordGroups';
import {
displayedExportProgress,
useExportRecords,
} from '@/object-record/record-index/export/hooks/useExportRecords';
import { TableOptionsHotkeyScope } from '@/object-record/record-table/types/TableOptionsHotkeyScope';
import { useOpenObjectRecordsSpreadsheetImportDialog } from '@/object-record/spreadsheet-import/hooks/useOpenObjectRecordsSpreadsheetImportDialog';
import { DropdownMenuHeader } from '@/ui/layout/dropdown/components/DropdownMenuHeader';
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownMenuSeparator';
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
import { ViewType } from '@/views/types/ViewType';
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
export const ObjectOptionsDropdownMenuContent = () => {
const {
recordIndexId,
objectMetadataItem,
viewType,
onContentChange,
closeDropdown,
} = useOptionsDropdown();
const isViewGroupEnabled = useIsFeatureEnabled('IS_VIEW_GROUPS_ENABLED');
const { objectNamePlural } = useObjectNamePluralFromSingular({
objectNameSingular: objectMetadataItem.nameSingular,
});
useScopedHotkeys(
[Key.Escape],
() => {
closeDropdown();
},
TableOptionsHotkeyScope.Dropdown,
);
const { handleToggleTrashColumnFilter, toggleSoftDeleteFilterState } =
useHandleToggleTrashColumnFilter({
objectNameSingular: objectMetadataItem.nameSingular,
viewBarId: recordIndexId,
});
const { visibleBoardFields } = useObjectOptionsForBoard({
objectNameSingular: objectMetadataItem.nameSingular,
recordBoardId: recordIndexId,
viewBarId: recordIndexId,
});
const { viewGroupFieldMetadataItem } = useRecordGroups({
objectNameSingular: objectMetadataItem.nameSingular,
});
const { openObjectRecordsSpreasheetImportDialog } =
useOpenObjectRecordsSpreadsheetImportDialog(
objectMetadataItem.nameSingular,
);
const { progress, download } = useExportRecords({
delayMs: 100,
filename: `${objectMetadataItem.nameSingular}.csv`,
objectMetadataItem,
recordIndexId,
viewType,
});
return (
<>
<DropdownMenuHeader StartIcon={IconList}>
{objectMetadataItem.labelPlural}
</DropdownMenuHeader>
{/** TODO: Should be removed when view settings contains more options */}
{viewType === ViewType.Kanban && (
<>
<DropdownMenuItemsContainer>
<MenuItem
onClick={() => onContentChange('viewSettings')}
LeftIcon={IconLayout}
text="View settings"
hasSubMenu
/>
</DropdownMenuItemsContainer>
<DropdownMenuSeparator />
</>
)}
<DropdownMenuItemsContainer>
<MenuItem
onClick={() => onContentChange('fields')}
LeftIcon={IconTag}
text="Fields"
contextualText={`${visibleBoardFields.length} shown`}
hasSubMenu
/>
{(viewType === ViewType.Kanban || isViewGroupEnabled) && (
<MenuItem
onClick={() => onContentChange('recordGroups')}
LeftIcon={IconLayoutList}
text="Group by"
contextualText={viewGroupFieldMetadataItem?.label}
hasSubMenu
/>
)}
</DropdownMenuItemsContainer>
<DropdownMenuSeparator />
<DropdownMenuItemsContainer>
<MenuItem
onClick={download}
LeftIcon={IconFileExport}
text={displayedExportProgress(progress)}
/>
<MenuItem
onClick={() => openObjectRecordsSpreasheetImportDialog()}
LeftIcon={IconFileImport}
text="Import"
/>
<MenuItem
onClick={() => {
handleToggleTrashColumnFilter();
toggleSoftDeleteFilterState(true);
closeDropdown();
}}
LeftIcon={IconRotate2}
text={`Deleted ${objectNamePlural}`}
/>
</DropdownMenuItemsContainer>
</>
);
};

View File

@@ -0,0 +1,118 @@
import { useEffect } from 'react';
import {
IconChevronLeft,
IconSettings,
MenuItem,
UndecoratedLink,
useIcons,
} from 'twenty-ui';
import { useObjectNamePluralFromSingular } from '@/object-metadata/hooks/useObjectNamePluralFromSingular';
import { StyledInput } from '@/object-record/object-filter-dropdown/components/ObjectFilterDropdownFilterSelect';
import { useOptionsDropdown } from '@/object-record/object-options-dropdown/hooks/useOptionsDropdown';
import { useSearchRecordGroupField } from '@/object-record/object-options-dropdown/hooks/useSearchRecordGroupField';
import { useRecordGroups } from '@/object-record/record-group/hooks/useRecordGroups';
import { useHandleRecordGroupField } from '@/object-record/record-index/hooks/useHandleRecordGroupField';
import { getSettingsPagePath } from '@/settings/utils/getSettingsPagePath';
import { SettingsPath } from '@/types/SettingsPath';
import { DropdownMenuHeader } from '@/ui/layout/dropdown/components/DropdownMenuHeader';
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownMenuSeparator';
import { navigationMemorizedUrlState } from '@/ui/navigation/states/navigationMemorizedUrlState';
import { useLocation } from 'react-router-dom';
import { useSetRecoilState } from 'recoil';
export const ObjectOptionsDropdownRecordGroupFieldsContent = () => {
const { getIcon } = useIcons();
const {
currentContentId,
recordIndexId,
objectMetadataItem,
onContentChange,
closeDropdown,
} = useOptionsDropdown();
const { objectNamePlural } = useObjectNamePluralFromSingular({
objectNameSingular: objectMetadataItem.nameSingular,
});
const { hiddenRecordGroups } = useRecordGroups({
objectNameSingular: objectMetadataItem.nameSingular,
});
const {
recordGroupFieldSearchInput,
setRecordGroupFieldSearchInput,
filteredRecordGroupFieldMetadataItems,
} = useSearchRecordGroupField();
const { handleRecordGroupFieldChange, resetRecordGroupField } =
useHandleRecordGroupField({
viewBarComponentId: recordIndexId,
});
const newFieldSettingsUrl = getSettingsPagePath(
SettingsPath.ObjectNewFieldSelect,
{
objectSlug: objectNamePlural,
},
);
const location = useLocation();
const setNavigationMemorizedUrl = useSetRecoilState(
navigationMemorizedUrlState,
);
useEffect(() => {
if (
currentContentId === 'hiddenRecordGroups' &&
hiddenRecordGroups.length === 0
) {
onContentChange('recordGroups');
}
}, [hiddenRecordGroups, currentContentId, onContentChange]);
return (
<>
<DropdownMenuHeader
StartIcon={IconChevronLeft}
onClick={() => onContentChange('recordGroups')}
>
Group by
</DropdownMenuHeader>
<StyledInput
autoFocus
value={recordGroupFieldSearchInput}
placeholder="Search fields"
onChange={(event) => setRecordGroupFieldSearchInput(event.target.value)}
/>
<DropdownMenuItemsContainer>
<MenuItem text="None" onClick={resetRecordGroupField} />
{filteredRecordGroupFieldMetadataItems.map((fieldMetadataItem) => (
<MenuItem
key={fieldMetadataItem.id}
onClick={() => {
handleRecordGroupFieldChange(fieldMetadataItem);
}}
LeftIcon={getIcon(fieldMetadataItem.icon)}
text={fieldMetadataItem.label}
/>
))}
</DropdownMenuItemsContainer>
<DropdownMenuSeparator />
<DropdownMenuItemsContainer>
<UndecoratedLink
to={newFieldSettingsUrl}
onClick={() => {
setNavigationMemorizedUrl(location.pathname + location.search);
closeDropdown();
}}
>
<MenuItem LeftIcon={IconSettings} text="Create select field" />
</UndecoratedLink>
</DropdownMenuItemsContainer>
</>
);
};

View File

@@ -0,0 +1,79 @@
import { useEffect } from 'react';
import {
IconChevronLeft,
IconHandMove,
IconSortAZ,
IconSortZA,
MenuItem,
} from 'twenty-ui';
import { useOptionsDropdown } from '@/object-record/object-options-dropdown/hooks/useOptionsDropdown';
import { useRecordGroups } from '@/object-record/record-group/hooks/useRecordGroups';
import { RecordGroupSort } from '@/object-record/record-group/types/RecordGroupSort';
import { recordIndexRecordGroupSortComponentState } from '@/object-record/record-index/states/recordIndexRecordGroupSortComponentState';
import { DropdownMenuHeader } from '@/ui/layout/dropdown/components/DropdownMenuHeader';
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentStateV2';
export const ObjectOptionsDropdownRecordGroupSortContent = () => {
const {
currentContentId,
objectMetadataItem,
onContentChange,
closeDropdown,
} = useOptionsDropdown();
const { hiddenRecordGroups } = useRecordGroups({
objectNameSingular: objectMetadataItem.nameSingular,
});
const setRecordGroupSort = useSetRecoilComponentStateV2(
recordIndexRecordGroupSortComponentState,
);
const handleRecordGroupSortChange = (sort: RecordGroupSort) => {
setRecordGroupSort(sort);
closeDropdown();
};
useEffect(() => {
if (
currentContentId === 'hiddenRecordGroups' &&
hiddenRecordGroups.length === 0
) {
onContentChange('recordGroups');
}
}, [hiddenRecordGroups, currentContentId, onContentChange]);
return (
<>
<DropdownMenuHeader
StartIcon={IconChevronLeft}
onClick={() => onContentChange('recordGroups')}
>
Sort
</DropdownMenuHeader>
<DropdownMenuItemsContainer>
<MenuItem
onClick={() => handleRecordGroupSortChange(RecordGroupSort.Manual)}
LeftIcon={IconHandMove}
text={RecordGroupSort.Manual}
/>
<MenuItem
onClick={() =>
handleRecordGroupSortChange(RecordGroupSort.Alphabetical)
}
LeftIcon={IconSortAZ}
text={RecordGroupSort.Alphabetical}
/>
<MenuItem
onClick={() =>
handleRecordGroupSortChange(RecordGroupSort.ReverseAlphabetical)
}
LeftIcon={IconSortZA}
text={RecordGroupSort.ReverseAlphabetical}
/>
</DropdownMenuItemsContainer>
</>
);
};

View File

@@ -0,0 +1,138 @@
import { useEffect } from 'react';
import {
IconChevronLeft,
IconCircleOff,
IconEyeOff,
IconLayoutList,
IconSortDescending,
MenuItem,
MenuItemNavigate,
MenuItemToggle,
} from 'twenty-ui';
import { useOptionsDropdown } from '@/object-record/object-options-dropdown/hooks/useOptionsDropdown';
import { RecordGroupsVisibilityDropdownSection } from '@/object-record/record-group/components/RecordGroupsVisibilityDropdownSection';
import { useRecordGroupReorder } from '@/object-record/record-group/hooks/useRecordGroupReorder';
import { useRecordGroups } from '@/object-record/record-group/hooks/useRecordGroups';
import { useRecordGroupVisibility } from '@/object-record/record-group/hooks/useRecordGroupVisibility';
import { recordIndexRecordGroupHideComponentState } from '@/object-record/record-index/states/recordIndexRecordGroupHideComponentState';
import { recordIndexRecordGroupIsDraggableSortComponentSelector } from '@/object-record/record-index/states/selectors/recordIndexRecordGroupIsDraggableSortComponentSelector';
import { DropdownMenuHeader } from '@/ui/layout/dropdown/components/DropdownMenuHeader';
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownMenuSeparator';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
export const ObjectOptionsDropdownRecordGroupsContent = () => {
const isViewGroupEnabled = useIsFeatureEnabled('IS_VIEW_GROUPS_ENABLED');
const {
currentContentId,
viewType,
recordIndexId,
objectMetadataItem,
onContentChange,
resetContent,
} = useOptionsDropdown();
const {
hiddenRecordGroups,
visibleRecordGroups,
viewGroupFieldMetadataItem,
} = useRecordGroups({
objectNameSingular: objectMetadataItem.nameSingular,
});
const isDragableSortRecordGroup = useRecoilComponentValueV2(
recordIndexRecordGroupIsDraggableSortComponentSelector,
);
const hideEmptyRecordGroup = useRecoilComponentValueV2(
recordIndexRecordGroupHideComponentState,
);
const {
handleVisibilityChange: handleRecordGroupVisibilityChange,
handleHideEmptyRecordGroupChange,
} = useRecordGroupVisibility({
viewBarId: recordIndexId,
viewType,
});
const { handleOrderChange: handleRecordGroupOrderChange } =
useRecordGroupReorder({
objectNameSingular: objectMetadataItem.nameSingular,
viewBarId: recordIndexId,
});
useEffect(() => {
if (
currentContentId === 'hiddenRecordGroups' &&
hiddenRecordGroups.length === 0
) {
onContentChange('recordGroups');
}
}, [hiddenRecordGroups, currentContentId, onContentChange]);
return (
<>
<DropdownMenuHeader StartIcon={IconChevronLeft} onClick={resetContent}>
Group by
</DropdownMenuHeader>
<DropdownMenuItemsContainer>
{isViewGroupEnabled && (
<>
<MenuItem
onClick={() => onContentChange('recordGroupFields')}
LeftIcon={IconLayoutList}
text={
!viewGroupFieldMetadataItem
? 'Group by'
: `Group by "${viewGroupFieldMetadataItem.label}"`
}
hasSubMenu
/>
<MenuItem
onClick={() => onContentChange('recordGroupSort')}
LeftIcon={IconSortDescending}
text="Sort"
hasSubMenu
/>
</>
)}
<MenuItemToggle
LeftIcon={IconCircleOff}
onToggleChange={handleHideEmptyRecordGroupChange}
toggled={hideEmptyRecordGroup}
text="Hide empty groups"
toggleSize="small"
/>
</DropdownMenuItemsContainer>
{visibleRecordGroups.length > 0 && (
<>
<DropdownMenuSeparator />
<RecordGroupsVisibilityDropdownSection
title="Visible groups"
recordGroups={visibleRecordGroups}
onDragEnd={handleRecordGroupOrderChange}
onVisibilityChange={handleRecordGroupVisibilityChange}
isDraggable={isDragableSortRecordGroup}
showDragGrip={true}
/>
</>
)}
{hiddenRecordGroups.length > 0 && (
<>
<DropdownMenuSeparator />
<DropdownMenuItemsContainer>
<MenuItemNavigate
onClick={() => onContentChange('hiddenRecordGroups')}
LeftIcon={IconEyeOff}
text={`Hidden ${viewGroupFieldMetadataItem?.label ?? ''}`}
/>
</DropdownMenuItemsContainer>
</>
)}
</>
);
};

View File

@@ -0,0 +1,50 @@
import {
IconBaselineDensitySmall,
IconChevronLeft,
MenuItemToggle,
} from 'twenty-ui';
import { useObjectOptionsForBoard } from '@/object-record/object-options-dropdown/hooks/useObjectOptionsForBoard';
import { useOptionsDropdown } from '@/object-record/object-options-dropdown/hooks/useOptionsDropdown';
import { DropdownMenuHeader } from '@/ui/layout/dropdown/components/DropdownMenuHeader';
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
import { useGetCurrentView } from '@/views/hooks/useGetCurrentView';
import { ViewType } from '@/views/types/ViewType';
export const ObjectOptionsDropdownViewSettingsContent = () => {
const { currentViewWithCombinedFiltersAndSorts } = useGetCurrentView();
const { recordIndexId, objectMetadataItem, viewType, resetContent } =
useOptionsDropdown();
const { isCompactModeActive, setAndPersistIsCompactModeActive } =
useObjectOptionsForBoard({
objectNameSingular: objectMetadataItem.nameSingular,
recordBoardId: recordIndexId,
viewBarId: recordIndexId,
});
return (
<>
<DropdownMenuHeader StartIcon={IconChevronLeft} onClick={resetContent}>
View settings
</DropdownMenuHeader>
<DropdownMenuItemsContainer>
{viewType === ViewType.Kanban && (
<MenuItemToggle
LeftIcon={IconBaselineDensitySmall}
onToggleChange={() =>
setAndPersistIsCompactModeActive(
!isCompactModeActive,
currentViewWithCombinedFiltersAndSorts,
)
}
toggled={isCompactModeActive}
text="Compact view"
toggleSize="small"
/>
)}
</DropdownMenuItemsContainer>
</>
);
};

View File

@@ -0,0 +1,123 @@
import { Meta, StoryObj } from '@storybook/react';
import { ComponentDecorator } from 'twenty-ui';
import { ContextStoreComponentInstanceContext } from '@/context-store/states/contexts/ContextStoreComponentInstanceContext';
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
import { ObjectOptionsDropdownContent } from '@/object-record/object-options-dropdown/components/ObjectOptionsDropdownContent';
import { ObjectOptionsDropdownContext } from '@/object-record/object-options-dropdown/states/contexts/ObjectOptionsDropdownContext';
import { ObjectOptionsContentId } from '@/object-record/object-options-dropdown/types/ObjectOptionsContentId';
import { RecordIndexRootPropsContext } from '@/object-record/record-index/contexts/RecordIndexRootPropsContext';
import { RecordTableComponentInstanceContext } from '@/object-record/record-table/states/context/RecordTableComponentInstanceContext';
import { DropdownMenu } from '@/ui/layout/dropdown/components/DropdownMenu';
import { ViewComponentInstanceContext } from '@/views/states/contexts/ViewComponentInstanceContext';
import { ViewType } from '@/views/types/ViewType';
import { useEffect } from 'react';
import { MemoryRouter } from 'react-router-dom';
import { useSetRecoilState } from 'recoil';
import { IconsProviderDecorator } from '~/testing/decorators/IconsProviderDecorator';
import { ObjectMetadataItemsDecorator } from '~/testing/decorators/ObjectMetadataItemsDecorator';
import { SnackBarDecorator } from '~/testing/decorators/SnackBarDecorator';
import { generatedMockObjectMetadataItems } from '~/testing/mock-data/generatedMockObjectMetadataItems';
const instanceId = 'entity-options-scope';
const meta: Meta<typeof ObjectOptionsDropdownContent> = {
title:
'Modules/ObjectRecord/ObjectOptionsDropdown/ObjectOptionsDropdownContent',
component: ObjectOptionsDropdownContent,
decorators: [
(Story) => {
const setObjectMetadataItems = useSetRecoilState(
objectMetadataItemsState,
);
useEffect(() => {
setObjectMetadataItems(generatedMockObjectMetadataItems);
}, [setObjectMetadataItems]);
return (
<RecordTableComponentInstanceContext.Provider
value={{ instanceId, onColumnsChange: () => {} }}
>
<ViewComponentInstanceContext.Provider value={{ instanceId }}>
<ContextStoreComponentInstanceContext.Provider
value={{ instanceId }}
>
<MemoryRouter
initialEntries={['/one', '/two', { pathname: '/three' }]}
initialIndex={1}
>
<Story />
</MemoryRouter>
</ContextStoreComponentInstanceContext.Provider>
</ViewComponentInstanceContext.Provider>
</RecordTableComponentInstanceContext.Provider>
);
},
ObjectMetadataItemsDecorator,
SnackBarDecorator,
ComponentDecorator,
IconsProviderDecorator,
],
parameters: {
layout: 'centered',
},
};
export default meta;
type Story = StoryObj<typeof ObjectOptionsDropdownContent>;
const createStory = (contentId: ObjectOptionsContentId | null): Story => ({
decorators: [
(Story) => {
const companyObjectMetadataItem = generatedMockObjectMetadataItems.find(
(item) => item.nameSingular === 'company',
)!;
return (
<RecordIndexRootPropsContext.Provider
value={{
indexIdentifierUrl: () => '',
onIndexRecordsLoaded: () => {},
onCreateRecord: () => {},
objectNamePlural: 'companies',
objectNameSingular: 'company',
objectMetadataItem: companyObjectMetadataItem,
recordIndexId: instanceId,
}}
>
<ObjectOptionsDropdownContext.Provider
value={{
viewType: ViewType.Table,
objectMetadataItem: companyObjectMetadataItem,
recordIndexId: instanceId,
currentContentId: contentId,
onContentChange: () => {},
resetContent: () => {},
}}
>
<DropdownMenu>
<Story />
</DropdownMenu>
</ObjectOptionsDropdownContext.Provider>
</RecordIndexRootPropsContext.Provider>
);
},
],
});
export const Default = createStory(null);
export const ViewSettings = createStory('viewSettings');
export const Fields = createStory('fields');
export const HiddenFields = createStory('hiddenFields');
export const RecordGroups = createStory('recordGroups');
export const RecordGroupFields = createStory('recordGroupFields');
export const RecordGroupSort = createStory('recordGroupSort');
export const HiddenRecordGroups = createStory('hiddenRecordGroups');

View File

@@ -0,0 +1 @@
export const OBJECT_OPTIONS_DROPDOWN_ID = 'object-options-dropdown-id';

View File

@@ -0,0 +1,59 @@
import { useExportProcessRecordsForCSV } from '@/object-record/object-options-dropdown/hooks/useExportProcessRecordsForCSV';
import { renderHook } from '@testing-library/react';
import { act } from 'react';
import { FieldMetadataType } from '~/generated/graphql';
jest.mock('@/object-metadata/hooks/useObjectMetadataItem', () => ({
useObjectMetadataItem: jest.fn(() => ({
objectMetadataItem: {
fields: [
{ type: FieldMetadataType.Currency, name: 'price' },
{ type: FieldMetadataType.Text, name: 'name' },
],
},
})),
}));
describe('useExportProcessRecordsForCSV', () => {
it('processes records with currency fields correctly', () => {
const { result } = renderHook(() =>
useExportProcessRecordsForCSV('someObject'),
);
const records = [
{
__typename: 'ObjectRecord',
id: '1',
price: { amountMicros: 123456, currencyCode: 'USD' },
name: 'Item 1',
},
{
__typename: 'ObjectRecord',
id: '2',
price: { amountMicros: 789012, currencyCode: 'EUR' },
name: 'Item 2',
},
];
let processedRecords;
act(() => {
processedRecords = result.current.processRecordsForCSVExport(records);
});
expect(processedRecords).toEqual([
{
__typename: 'ObjectRecord',
id: '1',
price: { amountMicros: 0.123456, currencyCode: 'USD' },
name: 'Item 1',
},
{
__typename: 'ObjectRecord',
id: '2',
price: { amountMicros: 0.789012, currencyCode: 'EUR' },
name: 'Item 2',
},
]);
});
});

View File

@@ -0,0 +1,104 @@
import { useObjectOptionsForBoard } from '@/object-record/object-options-dropdown/hooks/useObjectOptionsForBoard';
import { recordIndexFieldDefinitionsState } from '@/object-record/record-index/states/recordIndexFieldDefinitionsState';
import { DropResult, ResponderProvided } from '@hello-pangea/dnd';
import { renderHook } from '@testing-library/react';
import { act } from 'react';
import { RecoilRoot } from 'recoil';
jest.mock('@/views/hooks/useSaveCurrentViewFields', () => ({
useSaveCurrentViewFields: jest.fn(() => ({
saveViewFields: jest.fn(),
})),
}));
jest.mock('@/views/hooks/useUpdateCurrentView', () => ({
useUpdateCurrentView: jest.fn(() => ({
updateCurrentView: jest.fn(),
})),
}));
jest.mock('@/object-metadata/hooks/useObjectMetadataItem', () => ({
useObjectMetadataItem: jest.fn(() => ({
objectMetadataItem: {
fields: [
{
id: 'field1',
name: 'field1',
label: 'Field 1',
isVisible: true,
position: 0,
},
{
id: 'field2',
name: 'field2',
label: 'Field 2',
isVisible: true,
position: 1,
},
],
},
})),
}));
describe('useObjectOptionsForBoard', () => {
const initialRecoilState = [
{ fieldMetadataId: 'field1', isVisible: true, position: 0 },
{ fieldMetadataId: 'field2', isVisible: true, position: 1 },
];
const renderWithRecoil = () =>
renderHook(
() =>
useObjectOptionsForBoard({
objectNameSingular: 'object',
recordBoardId: 'boardId',
viewBarId: 'viewBarId',
}),
{
wrapper: ({ children }) => (
<RecoilRoot
initializeState={({ set }) => {
set(recordIndexFieldDefinitionsState, initialRecoilState as any);
}}
>
{children}
</RecoilRoot>
),
},
);
it('reorders fields correctly', () => {
const { result } = renderWithRecoil();
const dropResult: DropResult = {
source: { droppableId: 'droppable', index: 1 },
destination: { droppableId: 'droppable', index: 2 },
draggableId: 'field1',
type: 'TYPE',
mode: 'FLUID',
reason: 'DROP',
combine: null,
};
const responderProvided: ResponderProvided = {
announce: jest.fn(),
};
act(() => {
result.current.handleReorderBoardFields(dropResult, responderProvided);
});
expect(result.current.visibleBoardFields).toEqual([
{
fieldMetadataId: 'field2',
isVisible: true,
position: 0,
},
{
fieldMetadataId: 'field1',
isVisible: true,
position: 1,
},
]);
});
});

View File

@@ -0,0 +1,89 @@
import { useObjectOptionsForTable } from '@/object-record/object-options-dropdown/hooks/useObjectOptionsForTable';
import { RecordTableComponentInstanceContext } from '@/object-record/record-table/states/context/RecordTableComponentInstanceContext';
import { tableColumnsComponentState } from '@/object-record/record-table/states/tableColumnsComponentState';
import { DropResult, ResponderProvided } from '@hello-pangea/dnd';
import { renderHook } from '@testing-library/react';
import { act } from 'react';
import { RecoilRoot } from 'recoil';
describe('useObjectOptionsForTable', () => {
const initialRecoilState = [
{ fieldMetadataId: 'field1', isVisible: true, position: 0 },
{ fieldMetadataId: 'field2', isVisible: true, position: 1 },
{ fieldMetadataId: 'field3', isVisible: true, position: 2 },
{ fieldMetadataId: 'field4', isVisible: true, position: 3 },
{ fieldMetadataId: 'field5', isVisible: true, position: 4 },
];
const renderWithRecoil = () =>
renderHook(() => useObjectOptionsForTable('instance-id'), {
wrapper: ({ children }) => (
<RecordTableComponentInstanceContext.Provider
value={{ instanceId: 'instance-id', onColumnsChange: jest.fn() }}
>
<RecoilRoot
initializeState={({ set }) => {
set(
tableColumnsComponentState.atomFamily({
instanceId: 'instance-id',
}),
initialRecoilState as any,
);
}}
>
{children}
</RecoilRoot>
</RecordTableComponentInstanceContext.Provider>
),
});
it('reorders table columns correctly', () => {
const { result } = renderWithRecoil();
const dropResult = {
source: { droppableId: 'droppable', index: 2 },
destination: { droppableId: 'droppable', index: 3 },
draggableId: 'field3',
type: 'TYPE',
mode: 'FLUID',
reason: 'DROP',
combine: null,
} as DropResult;
const responderProvided = {
announce: jest.fn(),
} as ResponderProvided;
act(() => {
result.current.handleReorderColumns(dropResult, responderProvided);
});
expect(result.current.visibleTableColumns).toEqual([
{
fieldMetadataId: 'field1',
isVisible: true,
position: 0,
},
{
fieldMetadataId: 'field3',
isVisible: true,
position: 1,
},
{
fieldMetadataId: 'field2',
isVisible: true,
position: 2,
},
{
fieldMetadataId: 'field4',
isVisible: true,
position: 3,
},
{
fieldMetadataId: 'field5',
isVisible: true,
position: 4,
},
]);
});
});

View File

@@ -0,0 +1,91 @@
import { renderHook } from '@testing-library/react';
import { act } from 'react';
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { useOptionsDropdown } from '@/object-record/object-options-dropdown/hooks/useOptionsDropdown';
import { ObjectOptionsDropdownContext } from '@/object-record/object-options-dropdown/states/contexts/ObjectOptionsDropdownContext';
import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown';
import { ViewType } from '@/views/types/ViewType';
jest.mock('@/ui/layout/dropdown/hooks/useDropdown', () => ({
useDropdown: jest.fn(() => ({
closeDropdown: jest.fn(),
})),
}));
describe('useOptionsDropdown', () => {
const mockOnContentChange = jest.fn();
const mockCloseDropdown = jest.fn();
const mockResetContent = jest.fn();
beforeEach(() => {
jest.mocked(useDropdown).mockReturnValue({
scopeId: 'mock-scope',
isDropdownOpen: false,
closeDropdown: mockCloseDropdown,
toggleDropdown: jest.fn(),
openDropdown: jest.fn(),
dropdownWidth: undefined,
setDropdownWidth: jest.fn(),
dropdownPlacement: null,
setDropdownPlacement: jest.fn(),
});
});
afterEach(() => {
jest.clearAllMocks();
});
const renderWithProvider = (contextValue: Partial<any> = {}) => {
const wrapper = ({ children }: any) => (
<ObjectOptionsDropdownContext.Provider
value={{
viewType: ViewType.Table,
objectMetadataItem: {
__typename: 'object',
id: '1',
nameSingular: 'company',
namePlural: 'companies',
labelSingular: 'Company',
labelPlural: 'Companies',
icon: 'IconBuildingSkyscraper',
fields: [{}],
} as ObjectMetadataItem,
recordIndexId: 'test-record-index',
currentContentId: 'recordGroups',
onContentChange: mockOnContentChange,
resetContent: mockResetContent,
...contextValue,
}}
>
{children}
</ObjectOptionsDropdownContext.Provider>
);
return renderHook(() => useOptionsDropdown(), { wrapper });
};
it('provides closeDropdown functionality from the context', () => {
const { result } = renderWithProvider();
act(() => {
result.current.closeDropdown();
});
expect(mockResetContent).toHaveBeenCalled();
expect(mockCloseDropdown).toHaveBeenCalled();
});
it('returns all context values', () => {
const { result } = renderWithProvider({
currentContentId: 'fields',
});
expect(result.current).toHaveProperty('currentContentId', 'fields');
expect(result.current).toHaveProperty(
'onContentChange',
mockOnContentChange,
);
expect(result.current).toHaveProperty('closeDropdown');
expect(result.current).toHaveProperty('resetContent', mockResetContent);
});
});

View File

@@ -0,0 +1,65 @@
import { useSearchRecordGroupField } from '@/object-record/object-options-dropdown/hooks/useSearchRecordGroupField';
import { RecordIndexRootPropsContext } from '@/object-record/record-index/contexts/RecordIndexRootPropsContext';
import { ViewComponentInstanceContext } from '@/views/states/contexts/ViewComponentInstanceContext';
import { renderHook } from '@testing-library/react';
import { act } from 'react';
import { RecoilRoot } from 'recoil';
import { FieldMetadataType } from '~/generated/graphql';
describe('useSearchRecordGroupField', () => {
const renderWithContext = (contextValue: any) =>
renderHook(() => useSearchRecordGroupField(), {
wrapper: ({ children }) => (
<RecoilRoot>
<RecordIndexRootPropsContext.Provider value={contextValue}>
<ViewComponentInstanceContext.Provider
value={{ instanceId: 'myViewInstanceId' }}
>
{children}
</ViewComponentInstanceContext.Provider>
</RecordIndexRootPropsContext.Provider>
</RecoilRoot>
),
});
it('filters fields correctly based on input', () => {
const mockContextValue = {
objectMetadataItem: {
fields: [
{ type: FieldMetadataType.Select, label: 'First' },
{ type: FieldMetadataType.Select, label: 'Second' },
{ type: FieldMetadataType.Text, label: 'Third' },
],
},
};
const { result } = renderWithContext(mockContextValue);
act(() => {
result.current.setRecordGroupFieldSearchInput('First');
});
expect(result.current.filteredRecordGroupFieldMetadataItems).toEqual([
{ type: FieldMetadataType.Select, label: 'First' },
]);
});
it('returns all select fields when search input is empty', () => {
const mockContextValue = {
objectMetadataItem: {
fields: [
{ type: FieldMetadataType.Select, label: 'First' },
{ type: FieldMetadataType.Select, label: 'Second' },
{ type: FieldMetadataType.Text, label: 'Third' },
],
},
};
const { result } = renderWithContext(mockContextValue);
expect(result.current.filteredRecordGroupFieldMetadataItems).toEqual([
{ type: FieldMetadataType.Select, label: 'First' },
{ type: FieldMetadataType.Select, label: 'Second' },
]);
});
});

View File

@@ -0,0 +1,39 @@
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
import { FieldCurrencyValue } from '@/object-record/record-field/types/FieldMetadata';
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
import { isDefined } from 'twenty-ui';
import { FieldMetadataType } from '~/generated-metadata/graphql';
import { convertCurrencyMicrosToCurrencyAmount } from '~/utils/convertCurrencyToCurrencyMicros';
export const useExportProcessRecordsForCSV = (objectNameSingular: string) => {
const { objectMetadataItem } = useObjectMetadataItem({
objectNameSingular,
});
const processRecordsForCSVExport = (records: ObjectRecord[]) => {
return records.map((record) => {
const currencyFields = objectMetadataItem.fields.filter(
(field) => field.type === FieldMetadataType.Currency,
);
const processedRecord = {
...record,
};
for (const currencyField of currencyFields) {
if (isDefined(record[currencyField.name])) {
processedRecord[currencyField.name] = {
amountMicros: convertCurrencyMicrosToCurrencyAmount(
record[currencyField.name].amountMicros,
),
currencyCode: record[currencyField.name].currencyCode,
} satisfies FieldCurrencyValue;
}
}
return processedRecord;
});
};
return { processRecordsForCSVExport };
};

View File

@@ -16,17 +16,17 @@ import { mapArrayToObject } from '~/utils/array/mapArrayToObject';
import { moveArrayItem } from '~/utils/array/moveArrayItem';
import { isDeeplyEqual } from '~/utils/isDeeplyEqual';
type useRecordIndexOptionsForBoardParams = {
type useObjectOptionsForBoardParams = {
objectNameSingular: string;
recordBoardId: string;
viewBarId: string;
};
export const useRecordIndexOptionsForBoard = ({
export const useObjectOptionsForBoard = ({
objectNameSingular,
recordBoardId,
viewBarId,
}: useRecordIndexOptionsForBoardParams) => {
}: useObjectOptionsForBoardParams) => {
const [recordIndexFieldDefinitions, setRecordIndexFieldDefinitions] =
useRecoilState(recordIndexFieldDefinitionsState);

View File

@@ -7,7 +7,7 @@ import { visibleTableColumnsComponentSelector } from '@/object-record/record-tab
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
import { moveArrayItem } from '~/utils/array/moveArrayItem';
export const useRecordIndexOptionsForTable = (recordTableId: string) => {
export const useObjectOptionsForTable = (recordTableId: string) => {
const hiddenTableColumns = useRecoilComponentValueV2(
hiddenTableColumnsComponentSelector,
recordTableId,

View File

@@ -0,0 +1,26 @@
import { OBJECT_OPTIONS_DROPDOWN_ID } from '@/object-record/object-options-dropdown/constants/ObjectOptionsDropdownId';
import { ObjectOptionsDropdownContext } from '@/object-record/object-options-dropdown/states/contexts/ObjectOptionsDropdownContext';
import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown';
import { useCallback, useContext } from 'react';
export const useOptionsDropdown = () => {
const { closeDropdown } = useDropdown(OBJECT_OPTIONS_DROPDOWN_ID);
const context = useContext(ObjectOptionsDropdownContext);
if (!context) {
throw new Error(
'useOptionsDropdown must be used within a ObjectOptionsDropdownContext.Provider',
);
}
const handleCloseDropdown = useCallback(() => {
context.resetContent();
closeDropdown();
}, [closeDropdown, context]);
return {
...context,
closeDropdown: handleCloseDropdown,
};
};

View File

@@ -0,0 +1,29 @@
import { objectOptionsDropdownSearchInputComponentState } from '@/object-record/object-options-dropdown/states/objectOptionsDropdownSearchInputComponentState';
import { RecordIndexRootPropsContext } from '@/object-record/record-index/contexts/RecordIndexRootPropsContext';
import { useRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentStateV2';
import { useContext, useMemo } from 'react';
import { FieldMetadataType } from '~/generated-metadata/graphql';
export const useSearchRecordGroupField = () => {
const { objectMetadataItem } = useContext(RecordIndexRootPropsContext);
const [recordGroupFieldSearchInput, setRecordGroupFieldSearchInput] =
useRecoilComponentStateV2(objectOptionsDropdownSearchInputComponentState);
const filteredRecordGroupFieldMetadataItems = useMemo(() => {
const searchInputLowerCase =
recordGroupFieldSearchInput.toLocaleLowerCase();
return objectMetadataItem.fields.filter(
(field) =>
field.type === FieldMetadataType.Select &&
field.label.toLocaleLowerCase().includes(searchInputLowerCase),
);
}, [objectMetadataItem.fields, recordGroupFieldSearchInput]);
return {
recordGroupFieldSearchInput,
setRecordGroupFieldSearchInput,
filteredRecordGroupFieldMetadataItems,
};
};

View File

@@ -0,0 +1,18 @@
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { ObjectOptionsContentId } from '@/object-record/object-options-dropdown/types/ObjectOptionsContentId';
import { ViewType } from '@/views/types/ViewType';
import { createContext } from 'react';
export type ObjectOptionsDropdownContextValue = {
recordIndexId: string;
objectMetadataItem: ObjectMetadataItem;
viewType: ViewType;
currentContentId: ObjectOptionsContentId | null;
onContentChange: (key: ObjectOptionsContentId) => void;
resetContent: () => void;
};
export const ObjectOptionsDropdownContext =
createContext<ObjectOptionsDropdownContextValue>(
{} as ObjectOptionsDropdownContextValue,
);

View File

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

View File

@@ -0,0 +1,8 @@
export type ObjectOptionsContentId =
| 'viewSettings'
| 'fields'
| 'hiddenFields'
| 'recordGroups'
| 'hiddenRecordGroups'
| 'recordGroupFields'
| 'recordGroupSort';

View File

@@ -2,12 +2,20 @@ import { useRecoilCallback } from 'recoil';
import { useRecordBoardStates } from '@/object-record/record-board/hooks/internal/useRecordBoardStates';
import { RecordGroupDefinition } from '@/object-record/record-group/types/RecordGroupDefinition';
import { sortRecordGroupDefinitions } from '@/object-record/record-group/utils/sortRecordGroupDefinitions';
import { recordIndexRecordGroupSortComponentState } from '@/object-record/record-index/states/recordIndexRecordGroupSortComponentState';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
import { isDeeplyEqual } from '~/utils/isDeeplyEqual';
export const useSetRecordBoardColumns = (recordBoardId?: string) => {
const { scopeId, columnIdsState, columnsFamilySelector } =
useRecordBoardStates(recordBoardId);
const recordGroupSort = useRecoilComponentValueV2(
recordIndexRecordGroupSortComponentState,
recordBoardId,
);
const setColumns = useRecoilCallback(
({ set, snapshot }) =>
(columns: RecordGroupDefinition[]) => {
@@ -15,7 +23,12 @@ export const useSetRecordBoardColumns = (recordBoardId?: string) => {
.getLoadable(columnIdsState)
.getValue();
const columnIds = columns
const sortedColumns = sortRecordGroupDefinitions(
columns,
recordGroupSort,
);
const columnIds = sortedColumns
.filter(({ isVisible }) => isVisible)
.map(({ id }) => id);
@@ -35,7 +48,7 @@ export const useSetRecordBoardColumns = (recordBoardId?: string) => {
set(columnsFamilySelector(column.id), column);
});
},
[columnsFamilySelector, columnIdsState],
[columnIdsState, recordGroupSort, columnsFamilySelector],
);
return {

View File

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

View File

@@ -19,9 +19,8 @@ import { toSpliced } from '~/utils/array/toSpliced';
import { turnIntoEmptyStringIfWhitespacesOnly } from '~/utils/string/turnIntoEmptyStringIfWhitespacesOnly';
const StyledDropdownMenu = styled(DropdownMenu)`
margin-left: -1px;
margin: -1px;
position: relative;
margin-top: -1px;
`;
type MultiItemFieldInputProps<T> = {
@@ -65,8 +64,12 @@ export const MultiItemFieldInput = <T,>({
};
const handleDropdownCloseOutside = (event: MouseEvent | TouchEvent) => {
onCancel?.();
event.stopImmediatePropagation();
if (inputValue?.trim().length > 0) {
handleSubmitInput();
} else {
onCancel?.();
}
};
useListenClickOutside({
@@ -202,10 +205,12 @@ export const MultiItemFieldInput = <T,>({
}
onEnter={handleSubmitInput}
rightComponent={
<LightIconButton
Icon={isAddingNewItem ? IconPlus : IconCheck}
onClick={handleSubmitInput}
/>
items.length ? (
<LightIconButton
Icon={isAddingNewItem ? IconPlus : IconCheck}
onClick={handleSubmitInput}
/>
) : null
}
/>
) : (

View File

@@ -18,6 +18,9 @@ export const isMatchingSelectFilter = ({
return value !== null;
}
}
case selectFilter.eq !== undefined: {
return value === selectFilter.eq;
}
default: {
throw new Error(
`Unexpected value for select filter : ${JSON.stringify(selectFilter)}`,

View File

@@ -0,0 +1,65 @@
import { IconEye, IconEyeOff, MenuItemDraggable, Tag } from 'twenty-ui';
import {
RecordGroupDefinition,
RecordGroupDefinitionType,
} from '@/object-record/record-group/types/RecordGroupDefinition';
import { isDefined } from '~/utils/isDefined';
type RecordGroupMenuItemDraggableProps = {
recordGroup: RecordGroupDefinition;
showDragGrip?: boolean;
isDraggable?: boolean;
onVisibilityChange: (viewGroup: RecordGroupDefinition) => void;
};
export const RecordGroupMenuItemDraggable = ({
recordGroup,
showDragGrip,
isDraggable,
onVisibilityChange,
}: RecordGroupMenuItemDraggableProps) => {
const isNoValue = recordGroup.type === RecordGroupDefinitionType.NoValue;
const getIconButtons = (recordGroup: RecordGroupDefinition) => {
const iconButtons = [
{
Icon: recordGroup.isVisible ? IconEyeOff : IconEye,
onClick: () => onVisibilityChange(recordGroup),
},
].filter(isDefined);
return iconButtons.length ? iconButtons : undefined;
};
return (
<MenuItemDraggable
key={recordGroup.id}
text={
<Tag
variant={
recordGroup.type !== RecordGroupDefinitionType.NoValue
? 'solid'
: 'outline'
}
color={
recordGroup.type !== RecordGroupDefinitionType.NoValue
? recordGroup.color
: 'transparent'
}
text={recordGroup.title}
weight={
recordGroup.type !== RecordGroupDefinitionType.NoValue
? 'regular'
: 'medium'
}
/>
}
accent={isNoValue || showDragGrip ? 'placeholder' : 'default'}
iconButtons={!isNoValue ? getIconButtons(recordGroup) : undefined}
showGrip={isNoValue ? true : showDragGrip}
isDragDisabled={isNoValue ? true : !isDraggable}
isHoverDisabled={isNoValue}
/>
);
};

View File

@@ -0,0 +1,106 @@
import {
DropResult,
OnDragEndResponder,
ResponderProvided,
} from '@hello-pangea/dnd';
import { useRef } from 'react';
import { RecordGroupMenuItemDraggable } from '@/object-record/record-group/components/RecordGroupMenuItemDraggable';
import {
RecordGroupDefinition,
RecordGroupDefinitionType,
} from '@/object-record/record-group/types/RecordGroupDefinition';
import { DraggableItem } from '@/ui/layout/draggable-list/components/DraggableItem';
import { DraggableList } from '@/ui/layout/draggable-list/components/DraggableList';
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
import { StyledDropdownMenuSubheader } from '@/ui/layout/dropdown/components/StyledDropdownMenuSubheader';
type RecordGroupsVisibilityDropdownSectionProps = {
recordGroups: RecordGroupDefinition[];
isDraggable: boolean;
onDragEnd?: OnDragEndResponder;
onVisibilityChange: (viewGroup: RecordGroupDefinition) => void;
title: string;
showSubheader?: boolean;
showDragGrip: boolean;
};
export const RecordGroupsVisibilityDropdownSection = ({
recordGroups,
isDraggable,
onDragEnd,
onVisibilityChange,
title,
showSubheader = true,
showDragGrip,
}: RecordGroupsVisibilityDropdownSectionProps) => {
const handleOnDrag = (result: DropResult, provided: ResponderProvided) => {
onDragEnd?.(result, provided);
};
const noValueRecordGroups =
recordGroups.filter(
(recordGroup) => recordGroup.type === RecordGroupDefinitionType.NoValue,
) ?? [];
const recordGroupsWithoutNoValueGroups = recordGroups.filter(
(recordGroup) => recordGroup.type !== RecordGroupDefinitionType.NoValue,
);
const ref = useRef<HTMLDivElement>(null);
return (
<div ref={ref}>
{showSubheader && (
<StyledDropdownMenuSubheader>{title}</StyledDropdownMenuSubheader>
)}
<DropdownMenuItemsContainer>
{!!recordGroups.length && (
<>
{!isDraggable ? (
recordGroupsWithoutNoValueGroups.map((recordGroup) => (
<RecordGroupMenuItemDraggable
recordGroup={recordGroup}
onVisibilityChange={onVisibilityChange}
showDragGrip={showDragGrip}
isDraggable={isDraggable}
/>
))
) : (
<DraggableList
onDragEnd={handleOnDrag}
draggableItems={
<>
{recordGroupsWithoutNoValueGroups.map(
(recordGroup, index) => (
<DraggableItem
key={recordGroup.id}
draggableId={recordGroup.id}
index={index + 1}
itemComponent={
<RecordGroupMenuItemDraggable
recordGroup={recordGroup}
onVisibilityChange={onVisibilityChange}
showDragGrip={showDragGrip}
isDraggable={isDraggable}
/>
}
/>
),
)}
</>
}
/>
)}
{noValueRecordGroups.map((recordGroup) => (
<RecordGroupMenuItemDraggable
recordGroup={recordGroup}
onVisibilityChange={onVisibilityChange}
/>
))}
</>
)}
</DropdownMenuItemsContainer>
</div>
);
};

View File

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

View File

@@ -1,45 +1,130 @@
import { useCallback } from 'react';
import { useRecordBoardStates } from '@/object-record/record-board/hooks/internal/useRecordBoardStates';
import { recordGroupDefinitionsComponentState } from '@/object-record/record-group/states/recordGroupDefinitionsComponentState';
import { RecordGroupDefinition } from '@/object-record/record-group/types/RecordGroupDefinition';
import { useRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentStateV2';
import { recordIndexRecordGroupHideComponentState } from '@/object-record/record-index/states/recordIndexRecordGroupHideComponentState';
import { tableRowIdsByGroupComponentFamilyState } from '@/object-record/record-table/states/tableRowIdsByGroupComponentFamilyState';
import { useRecoilComponentCallbackStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackStateV2';
import { getSnapshotValue } from '@/ui/utilities/state/utils/getSnapshotValue';
import { useSaveCurrentViewGroups } from '@/views/hooks/useSaveCurrentViewGroups';
import { ViewType } from '@/views/types/ViewType';
import { mapRecordGroupDefinitionsToViewGroups } from '@/views/utils/mapRecordGroupDefinitionsToViewGroups';
import { useRecoilCallback } from 'recoil';
type UseRecordGroupVisibilityParams = {
viewBarId: string;
viewType: ViewType;
};
export const useRecordGroupVisibility = ({
viewBarId,
viewType,
}: UseRecordGroupVisibilityParams) => {
const [recordGroupDefinitions, setRecordGroupDefinitions] =
useRecoilComponentStateV2(recordGroupDefinitionsComponentState);
const recordGroupDefinitionsState = useRecoilComponentCallbackStateV2(
recordGroupDefinitionsComponentState,
);
const tableRowIdsByGroupFamilyState = useRecoilComponentCallbackStateV2(
tableRowIdsByGroupComponentFamilyState,
viewBarId,
);
const { recordIdsByColumnIdFamilyState } = useRecordBoardStates(viewBarId);
const objectOptionsDropdownRecordGroupHideState =
useRecoilComponentCallbackStateV2(recordIndexRecordGroupHideComponentState);
const { saveViewGroups } = useSaveCurrentViewGroups(viewBarId);
const handleVisibilityChange = useCallback(
async (updatedRecordGroupDefinition: RecordGroupDefinition) => {
const updatedRecordGroupDefinitions = recordGroupDefinitions.map(
(groupDefinition) =>
groupDefinition.id === updatedRecordGroupDefinition.id
? {
...groupDefinition,
isVisible: !groupDefinition.isVisible,
}
: groupDefinition,
);
const handleVisibilityChange = useRecoilCallback(
({ snapshot, set }) =>
async (updatedRecordGroupDefinition: RecordGroupDefinition) => {
const recordGroupDefinitions = getSnapshotValue(
snapshot,
recordGroupDefinitionsState,
);
setRecordGroupDefinitions(updatedRecordGroupDefinitions);
const updatedRecordGroupDefinitions = recordGroupDefinitions.map(
(groupDefinition) =>
groupDefinition.id === updatedRecordGroupDefinition.id
? {
...groupDefinition,
isVisible: !groupDefinition.isVisible,
}
: groupDefinition,
);
saveViewGroups(
mapRecordGroupDefinitionsToViewGroups(updatedRecordGroupDefinitions),
);
},
[recordGroupDefinitions, setRecordGroupDefinitions, saveViewGroups],
set(recordGroupDefinitionsState, updatedRecordGroupDefinitions);
saveViewGroups(
mapRecordGroupDefinitionsToViewGroups(updatedRecordGroupDefinitions),
);
// If visibility is manually toggled, we should reset the hideEmptyRecordGroup state
set(objectOptionsDropdownRecordGroupHideState, false);
},
[
objectOptionsDropdownRecordGroupHideState,
recordGroupDefinitionsState,
saveViewGroups,
],
);
const handleHideEmptyRecordGroupChange = useRecoilCallback(
({ snapshot, set }) =>
async () => {
const recordGroupDefinitions = getSnapshotValue(
snapshot,
recordGroupDefinitionsState,
);
const currentHideState = getSnapshotValue(
snapshot,
objectOptionsDropdownRecordGroupHideState,
);
set(objectOptionsDropdownRecordGroupHideState, !currentHideState);
const updatedRecordGroupDefinitions = recordGroupDefinitions.map(
(recordGroup) => {
// TODO: Maybe we can improve that and only use one state for both table and board
const recordGroupRowIds =
viewType === ViewType.Table
? getSnapshotValue(
snapshot,
tableRowIdsByGroupFamilyState(recordGroup.id),
)
: getSnapshotValue(
snapshot,
recordIdsByColumnIdFamilyState(recordGroup.id),
);
if (recordGroupRowIds.length > 0) {
return recordGroup;
}
return {
...recordGroup,
isVisible: currentHideState,
};
},
);
saveViewGroups(
mapRecordGroupDefinitionsToViewGroups(updatedRecordGroupDefinitions),
);
},
[
recordGroupDefinitionsState,
objectOptionsDropdownRecordGroupHideState,
saveViewGroups,
viewType,
tableRowIdsByGroupFamilyState,
recordIdsByColumnIdFamilyState,
],
);
return {
handleVisibilityChange,
handleHideEmptyRecordGroupChange,
};
};

View File

@@ -2,6 +2,8 @@ import { useMemo } from 'react';
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
import { recordGroupDefinitionsComponentState } from '@/object-record/record-group/states/recordGroupDefinitionsComponentState';
import { sortRecordGroupDefinitions } from '@/object-record/record-group/utils/sortRecordGroupDefinitions';
import { recordIndexRecordGroupSortComponentState } from '@/object-record/record-index/states/recordIndexRecordGroupSortComponentState';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
type UseRecordGroupsParams = {
@@ -15,6 +17,10 @@ export const useRecordGroups = ({
recordGroupDefinitionsComponentState,
);
const recordGroupSort = useRecoilComponentValueV2(
recordIndexRecordGroupSortComponentState,
);
const { objectMetadataItem } = useObjectMetadataItem({
objectNameSingular,
});
@@ -35,14 +41,8 @@ export const useRecordGroups = ({
}, [objectMetadataItem, recordGroupDefinitions]);
const visibleRecordGroups = useMemo(
() =>
recordGroupDefinitions
.filter((boardGroup) => boardGroup.isVisible)
.sort(
(boardGroupA, boardGroupB) =>
boardGroupA.position - boardGroupB.position,
),
[recordGroupDefinitions],
() => sortRecordGroupDefinitions(recordGroupDefinitions, recordGroupSort),
[recordGroupDefinitions, recordGroupSort],
);
const hiddenRecordGroups = useMemo(

View File

@@ -0,0 +1,5 @@
export enum RecordGroupSort {
Manual = 'Manual',
Alphabetical = 'Alphabetical',
ReverseAlphabetical = 'Reverse Alphabetical',
}

View File

@@ -0,0 +1,31 @@
import { RecordGroupDefinition } from '@/object-record/record-group/types/RecordGroupDefinition';
import { RecordGroupSort } from '@/object-record/record-group/types/RecordGroupSort';
export const sortRecordGroupDefinitions = (
recordGroupDefinitions: RecordGroupDefinition[],
recordGroupSort: RecordGroupSort,
) => {
const visibleGroups = recordGroupDefinitions.filter(
(boardGroup) => boardGroup.isVisible,
);
const compareAlphabetical = (a: string, b: string, reverse = false) => {
if (a < b) return reverse ? 1 : -1;
if (a > b) return reverse ? -1 : 1;
return 0;
};
switch (recordGroupSort) {
case RecordGroupSort.Alphabetical:
return visibleGroups.sort((a, b) =>
compareAlphabetical(a.title.toLowerCase(), b.title.toLowerCase()),
);
case RecordGroupSort.ReverseAlphabetical:
return visibleGroups.sort((a, b) =>
compareAlphabetical(a.title.toLowerCase(), b.title.toLowerCase(), true),
);
case RecordGroupSort.Manual:
default:
return visibleGroups.sort((a, b) => a.position - b.position);
}
};

View File

@@ -30,7 +30,7 @@ export const RecordIndexBoardDataLoaderEffect = ({
recordIndexFieldDefinitionsState,
);
const recordIndexGroupDefinitions = useRecoilComponentValueV2(
const recordGroupDefinitions = useRecoilComponentValueV2(
recordGroupDefinitionsComponentState,
);
@@ -67,8 +67,8 @@ export const RecordIndexBoardDataLoaderEffect = ({
}, [objectNameSingular, setObjectSingularName]);
useEffect(() => {
setColumns(recordIndexGroupDefinitions);
}, [recordIndexGroupDefinitions, setColumns]);
setColumns(recordGroupDefinitions);
}, [recordGroupDefinitions, setColumns]);
// TODO: Remove this duplicate useEffect by ensuring it's not here because
// We want it to be triggered by a change of objectMetadataItem, which would be an anti-pattern

View File

@@ -2,13 +2,13 @@ import styled from '@emotion/styled';
import { useRecoilCallback, useRecoilState, useSetRecoilState } from 'recoil';
import { useColumnDefinitionsFromFieldMetadata } from '@/object-metadata/hooks/useColumnDefinitionsFromFieldMetadata';
import { ObjectOptionsDropdown } from '@/object-record/object-options-dropdown/components/ObjectOptionsDropdown';
import { RecordIndexBoardContainer } from '@/object-record/record-index/components/RecordIndexBoardContainer';
import { RecordIndexBoardDataLoader } from '@/object-record/record-index/components/RecordIndexBoardDataLoader';
import { RecordIndexBoardDataLoaderEffect } from '@/object-record/record-index/components/RecordIndexBoardDataLoaderEffect';
import { RecordIndexTableContainer } from '@/object-record/record-index/components/RecordIndexTableContainer';
import { RecordIndexTableContainerEffect } from '@/object-record/record-index/components/RecordIndexTableContainerEffect';
import { RecordIndexViewBarEffect } from '@/object-record/record-index/components/RecordIndexViewBarEffect';
import { RecordIndexOptionsDropdown } from '@/object-record/record-index/options/components/RecordIndexOptionsDropdown';
import { recordIndexFieldDefinitionsState } from '@/object-record/record-index/states/recordIndexFieldDefinitionsState';
import { recordIndexFiltersState } from '@/object-record/record-index/states/recordIndexFiltersState';
import { recordIndexIsCompactModeActiveState } from '@/object-record/record-index/states/recordIndexIsCompactModeActiveState';
@@ -162,7 +162,7 @@ export const RecordIndexContainer = () => {
<ViewBar
viewBarId={recordIndexId}
optionsDropdownButton={
<RecordIndexOptionsDropdown
<ObjectOptionsDropdown
recordIndexId={recordIndexId}
objectMetadataItem={objectMetadataItem}
viewType={recordIndexViewType ?? ViewType.Table}

View File

@@ -1,3 +1,4 @@
import { PageFavoriteFoldersDropdown } from '@/favorites/components/PageFavoriteFolderDropdown';
import { FAVORITE_FOLDER_PICKER_DROPDOWN_ID } from '@/favorites/favorite-folder-picker/constants/FavoriteFolderPickerDropdownId';
import { useFavorites } from '@/favorites/hooks/useFavorites';
import { useFilteredObjectMetadataItems } from '@/object-metadata/hooks/useFilteredObjectMetadataItems';
@@ -8,7 +9,6 @@ import { recordIndexViewTypeState } from '@/object-record/record-index/states/re
import { usePrefetchedData } from '@/prefetch/hooks/usePrefetchedData';
import { PrefetchKey } from '@/prefetch/types/PrefetchKey';
import { PageAddButton } from '@/ui/layout/page/components/PageAddButton';
import { PageFavoriteFoldersDropdown } from '@/ui/layout/page/components/PageFavoriteFolderDropdown';
import { PageHeader } from '@/ui/layout/page/components/PageHeader';
import { PageHotkeysEffect } from '@/ui/layout/page/components/PageHotkeysEffect';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
@@ -39,7 +39,7 @@ export const RecordIndexPageHeader = () => {
const view = views.find((view) => view.id === currentViewId);
const favorites = useFavorites();
const { sortedFavorites: favorites } = useFavorites();
const isFavorite = favorites.some(
(favorite) =>

View File

@@ -7,9 +7,9 @@ import {
} from '../useExportFetchRecords';
import { PERSON_FRAGMENT_WITH_DEPTH_ZERO_RELATIONS } from '@/object-record/hooks/__mocks__/personFragments';
import { useObjectOptionsForBoard } from '@/object-record/object-options-dropdown/hooks/useObjectOptionsForBoard';
import { useRecordBoard } from '@/object-record/record-board/hooks/useRecordBoard';
import { recordBoardKanbanFieldMetadataNameComponentState } from '@/object-record/record-board/states/recordBoardKanbanFieldMetadataNameComponentState';
import { useRecordIndexOptionsForBoard } from '@/object-record/record-index/options/hooks/useRecordIndexOptionsForBoard';
import { extractComponentState } from '@/ui/utilities/state/component-state/utils/extractComponentState';
import { ViewType } from '@/views/types/ViewType';
import { MockedResponse } from '@apollo/client/testing';
@@ -248,7 +248,7 @@ describe('useRecordData', () => {
}),
useRecordBoardHook: useRecordBoard(recordIndexId),
kanbanFieldName: useRecoilValue(kanbanFieldNameState),
kanbanData: useRecordIndexOptionsForBoard({
kanbanData: useObjectOptionsForBoard({
objectNameSingular: objectMetadataItem.nameSingular,
recordBoardId: recordIndexId,
viewBarId: recordIndexId,
@@ -338,7 +338,7 @@ describe('useRecordData', () => {
}),
setKanbanFieldName: useRecordBoard(recordIndexId),
kanbanFieldName: useRecoilValue(kanbanFieldNameState),
kanbanData: useRecordIndexOptionsForBoard({
kanbanData: useObjectOptionsForBoard({
objectNameSingular: objectMetadataItem.nameSingular,
recordBoardId: recordIndexId,
viewBarId: recordIndexId,

View File

@@ -86,7 +86,7 @@ describe('csvDownloader', () => {
describe('displayedExportProgress', () => {
it.each([
[undefined, undefined, 'percentage', 'Export View as CSV'],
[undefined, undefined, 'percentage', 'Export'],
[20, 50, 'percentage', 'Export (40%)'],
[0, 100, 'number', 'Export (0)'],
[10, 10, 'percentage', 'Export (100%)'],
@@ -96,7 +96,7 @@ describe('displayedExportProgress', () => {
'displays the export progress',
(exportedRecordCount, totalRecordCount, displayType, expected) => {
expect(
displayedExportProgress('all', {
displayedExportProgress({
exportedRecordCount,
totalRecordCount,
displayType: displayType as 'percentage' | 'number',

View File

@@ -11,10 +11,10 @@ import { contextStoreTargetedRecordsRuleComponentState } from '@/context-store/s
import { computeContextStoreFilters } from '@/context-store/utils/computeContextStoreFilters';
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { useLazyFindManyRecords } from '@/object-record/hooks/useLazyFindManyRecords';
import { EXPORT_TABLE_DATA_DEFAULT_PAGE_SIZE } from '@/object-record/object-options-dropdown/constants/ExportTableDataDefaultPageSize';
import { useObjectOptionsForBoard } from '@/object-record/object-options-dropdown/hooks/useObjectOptionsForBoard';
import { useRecordBoardStates } from '@/object-record/record-board/hooks/internal/useRecordBoardStates';
import { useFindManyParams } from '@/object-record/record-index/hooks/useLoadRecordIndexTable';
import { EXPORT_TABLE_DATA_DEFAULT_PAGE_SIZE } from '@/object-record/record-index/options/constants/ExportTableDataDefaultPageSize';
import { useRecordIndexOptionsForBoard } from '@/object-record/record-index/options/hooks/useRecordIndexOptionsForBoard';
import { visibleTableColumnsComponentSelector } from '@/object-record/record-table/states/selectors/visibleTableColumnsComponentSelector';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
import { ViewType } from '@/views/types/ViewType';
@@ -62,7 +62,7 @@ export const useExportFetchRecords = ({
});
const [previousRecordCount, setPreviousRecordCount] = useState(0);
const { hiddenBoardFields } = useRecordIndexOptionsForBoard({
const { hiddenBoardFields } = useObjectOptionsForBoard({
objectNameSingular: objectMetadataItem.nameSingular,
recordBoardId: recordIndexId,
viewBarId: recordIndexId,

View File

@@ -1,13 +1,13 @@
import { json2csv } from 'json-2-csv';
import { useMemo } from 'react';
import { EXPORT_TABLE_DATA_DEFAULT_PAGE_SIZE } from '@/object-record/object-options-dropdown/constants/ExportTableDataDefaultPageSize';
import { useExportProcessRecordsForCSV } from '@/object-record/object-options-dropdown/hooks/useExportProcessRecordsForCSV';
import { FieldMetadata } from '@/object-record/record-field/types/FieldMetadata';
import {
UseRecordDataOptions,
useExportFetchRecords,
} from '@/object-record/record-index/export/hooks/useExportFetchRecords';
import { useExportProcessRecordsForCSV } from '@/object-record/record-index/export/hooks/useExportProcessRecordsForCSV';
import { EXPORT_TABLE_DATA_DEFAULT_PAGE_SIZE } from '@/object-record/record-index/options/constants/ExportTableDataDefaultPageSize';
import { ColumnDefinition } from '@/object-record/record-table/types/ColumnDefinition';
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
import { RelationDefinitionType } from '~/generated-metadata/graphql';
@@ -107,12 +107,9 @@ const percentage = (part: number, whole: number): number => {
return Math.round((part / whole) * 100);
};
export const displayedExportProgress = (
mode: 'all' | 'selection' = 'all',
progress?: ExportProgress,
): string => {
export const displayedExportProgress = (progress?: ExportProgress): string => {
if (isUndefinedOrNull(progress?.exportedRecordCount)) {
return mode === 'all' ? 'Export View as CSV' : 'Export Selection as CSV';
return 'Export';
}
if (

View File

@@ -0,0 +1,110 @@
import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
import { useRecoilComponentCallbackStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackStateV2';
import { usePersistViewGroupRecords } from '@/views/hooks/internal/usePersistViewGroupRecords';
import { useGetViewFromCache } from '@/views/hooks/useGetViewFromCache';
import { currentViewIdComponentState } from '@/views/states/currentViewIdComponentState';
import { ViewGroup } from '@/views/types/ViewGroup';
import { useRecoilCallback } from 'recoil';
import { v4 } from 'uuid';
import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull';
type UseHandleRecordGroupFieldParams = {
viewBarComponentId: string;
};
export const useHandleRecordGroupField = ({
viewBarComponentId,
}: UseHandleRecordGroupFieldParams) => {
const { createViewGroupRecords, deleteViewGroupRecords } =
usePersistViewGroupRecords();
const currentViewIdCallbackState = useRecoilComponentCallbackStateV2(
currentViewIdComponentState,
viewBarComponentId,
);
const { getViewFromCache } = useGetViewFromCache();
const handleRecordGroupFieldChange = useRecoilCallback(
({ snapshot }) =>
async (fieldMetadataItem: FieldMetadataItem) => {
const currentViewId = snapshot
.getLoadable(currentViewIdCallbackState)
.getValue();
if (!currentViewId) {
return;
}
const view = await getViewFromCache(currentViewId);
if (isUndefinedOrNull(view)) {
return;
}
if (
isUndefinedOrNull(fieldMetadataItem.options) ||
fieldMetadataItem.options.length === 0
) {
return;
}
const existingGroupKeys = new Set(
view.viewGroups.map(
(group) => `${group.fieldMetadataId}:${group.fieldValue}`,
),
);
const viewGroupsToCreate = fieldMetadataItem.options
// Avoid creation of already existing view groups
.filter(
(option) =>
!existingGroupKeys.has(`${fieldMetadataItem.id}:${option.value}`),
)
.map(
(option, index) =>
({
__typename: 'ViewGroup',
id: v4(),
fieldValue: option.value,
isVisible: true,
position: index,
fieldMetadataId: fieldMetadataItem.id,
}) satisfies ViewGroup,
);
if (viewGroupsToCreate.length > 0) {
await createViewGroupRecords(viewGroupsToCreate, view);
}
},
[createViewGroupRecords, currentViewIdCallbackState, getViewFromCache],
);
const resetRecordGroupField = useRecoilCallback(
({ snapshot }) =>
async () => {
const currentViewId = snapshot
.getLoadable(currentViewIdCallbackState)
.getValue();
if (!currentViewId) {
return;
}
const view = await getViewFromCache(currentViewId);
if (isUndefinedOrNull(view)) {
return;
}
if (view.viewGroups.length === 0) {
return;
}
await deleteViewGroupRecords(view.viewGroups);
},
[deleteViewGroupRecords, currentViewIdCallbackState, getViewFromCache],
);
return { handleRecordGroupFieldChange, resetRecordGroupField };
};

View File

@@ -50,13 +50,13 @@ export const useLoadRecordIndexBoard = ({
recordIndexViewFilterGroupsState,
);
const recordIndexGroupDefinitions = useRecoilComponentValueV2(
const recordGroupDefinitions = useRecoilComponentValueV2(
recordGroupDefinitionsComponentState,
);
useEffect(() => {
setColumns(recordIndexGroupDefinitions);
}, [recordIndexGroupDefinitions, setColumns]);
setColumns(recordGroupDefinitions);
}, [recordGroupDefinitions, setColumns]);
const recordIndexFilters = useRecoilValue(recordIndexFiltersState);
const recordIndexSorts = useRecoilValue(recordIndexSortsState);

View File

@@ -57,7 +57,9 @@ export const useFindManyParams = (
);
if (!fieldMetadataItem) {
return {};
throw new Error(
`Field metadata item with id ${currentRecordGroupDefinition.fieldMetadataId} not found`,
);
}
return {

View File

@@ -1,36 +0,0 @@
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { RecordIndexOptionsDropdownButton } from '@/object-record/record-index/options/components/RecordIndexOptionsDropdownButton';
import { RecordIndexOptionsDropdownContent } from '@/object-record/record-index/options/components/RecordIndexOptionsDropdownContent';
import { RECORD_INDEX_OPTIONS_DROPDOWN_ID } from '@/object-record/record-index/options/constants/RecordIndexOptionsDropdownId';
import { TableOptionsHotkeyScope } from '@/object-record/record-table/types/TableOptionsHotkeyScope';
import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown';
import { ViewType } from '@/views/types/ViewType';
type RecordIndexOptionsDropdownProps = {
viewType: ViewType;
objectMetadataItem: ObjectMetadataItem;
recordIndexId: string;
};
export const RecordIndexOptionsDropdown = ({
recordIndexId,
objectMetadataItem,
viewType,
}: RecordIndexOptionsDropdownProps) => {
return (
<Dropdown
dropdownId={RECORD_INDEX_OPTIONS_DROPDOWN_ID}
clickableComponent={<RecordIndexOptionsDropdownButton />}
dropdownMenuWidth={'200px'}
dropdownHotkeyScope={{ scope: TableOptionsHotkeyScope.Dropdown }}
dropdownOffset={{ y: 8 }}
dropdownComponents={
<RecordIndexOptionsDropdownContent
viewType={viewType}
objectMetadataItem={objectMetadataItem}
recordIndexId={recordIndexId}
/>
}
/>
);
};

View File

@@ -1,379 +0,0 @@
import { useEffect, useState } from 'react';
import { Key } from 'ts-key-enum';
import {
IconBaselineDensitySmall,
IconChevronLeft,
IconEyeOff,
IconFileExport,
IconFileImport,
IconRotate2,
IconSettings,
IconTag,
MenuItem,
MenuItemNavigate,
MenuItemToggle,
UndecoratedLink,
useIcons,
} from 'twenty-ui';
import { useObjectNamePluralFromSingular } from '@/object-metadata/hooks/useObjectNamePluralFromSingular';
import { useHandleToggleTrashColumnFilter } from '@/object-record/record-index/hooks/useHandleToggleTrashColumnFilter';
import { RECORD_INDEX_OPTIONS_DROPDOWN_ID } from '@/object-record/record-index/options/constants/RecordIndexOptionsDropdownId';
import { contextStoreNumberOfSelectedRecordsComponentState } from '@/context-store/states/contextStoreNumberOfSelectedRecordsComponentState';
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { useRecordGroupReorder } from '@/object-record/record-group/hooks/useRecordGroupReorder';
import { useRecordGroupVisibility } from '@/object-record/record-group/hooks/useRecordGroupVisibility';
import { useRecordGroups } from '@/object-record/record-group/hooks/useRecordGroups';
import {
displayedExportProgress,
useExportRecords,
} from '@/object-record/record-index/export/hooks/useExportRecords';
import { useRecordIndexOptionsForBoard } from '@/object-record/record-index/options/hooks/useRecordIndexOptionsForBoard';
import { useRecordIndexOptionsForTable } from '@/object-record/record-index/options/hooks/useRecordIndexOptionsForTable';
import { TableOptionsHotkeyScope } from '@/object-record/record-table/types/TableOptionsHotkeyScope';
import { useOpenObjectRecordsSpreasheetImportDialog } from '@/object-record/spreadsheet-import/hooks/useOpenObjectRecordsSpreasheetImportDialog';
import { getSettingsPagePath } from '@/settings/utils/getSettingsPagePath';
import { SettingsPath } from '@/types/SettingsPath';
import { DropdownMenuHeader } from '@/ui/layout/dropdown/components/DropdownMenuHeader';
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownMenuSeparator';
import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown';
import { navigationMemorizedUrlState } from '@/ui/navigation/states/navigationMemorizedUrlState';
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
import { ScrollWrapper } from '@/ui/utilities/scroll/components/ScrollWrapper';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
import { ViewFieldsVisibilityDropdownSection } from '@/views/components/ViewFieldsVisibilityDropdownSection';
import { ViewGroupsVisibilityDropdownSection } from '@/views/components/ViewGroupsVisibilityDropdownSection';
import { useGetCurrentView } from '@/views/hooks/useGetCurrentView';
import { ViewType } from '@/views/types/ViewType';
import { useLocation } from 'react-router-dom';
import { useSetRecoilState } from 'recoil';
type RecordIndexOptionsMenu =
| 'viewGroups'
| 'hiddenViewGroups'
| 'fields'
| 'hiddenFields';
type RecordIndexOptionsDropdownContentProps = {
recordIndexId: string;
objectMetadataItem: ObjectMetadataItem;
viewType: ViewType;
};
// TODO: Break this component down
export const RecordIndexOptionsDropdownContent = ({
viewType,
recordIndexId,
objectMetadataItem,
}: RecordIndexOptionsDropdownContentProps) => {
const { currentViewWithCombinedFiltersAndSorts } = useGetCurrentView();
const { getIcon } = useIcons();
const { closeDropdown } = useDropdown(RECORD_INDEX_OPTIONS_DROPDOWN_ID);
const [currentMenu, setCurrentMenu] = useState<
RecordIndexOptionsMenu | undefined
>(undefined);
const resetMenu = () => setCurrentMenu(undefined);
const handleSelectMenu = (option: RecordIndexOptionsMenu) => {
setCurrentMenu(option);
};
const { objectNamePlural } = useObjectNamePluralFromSingular({
objectNameSingular: objectMetadataItem.nameSingular,
});
const settingsUrl = getSettingsPagePath(SettingsPath.ObjectDetail, {
objectSlug: objectNamePlural,
});
useScopedHotkeys(
[Key.Escape],
() => {
closeDropdown();
},
TableOptionsHotkeyScope.Dropdown,
);
const {
handleColumnVisibilityChange,
handleReorderColumns,
visibleTableColumns,
hiddenTableColumns,
} = useRecordIndexOptionsForTable(recordIndexId);
const { handleToggleTrashColumnFilter, toggleSoftDeleteFilterState } =
useHandleToggleTrashColumnFilter({
objectNameSingular: objectMetadataItem.nameSingular,
viewBarId: recordIndexId,
});
const {
visibleBoardFields,
hiddenBoardFields,
handleReorderBoardFields,
handleBoardFieldVisibilityChange,
isCompactModeActive,
setAndPersistIsCompactModeActive,
} = useRecordIndexOptionsForBoard({
objectNameSingular: objectMetadataItem.nameSingular,
recordBoardId: recordIndexId,
viewBarId: recordIndexId,
});
const {
hiddenRecordGroups,
visibleRecordGroups,
viewGroupFieldMetadataItem,
} = useRecordGroups({
objectNameSingular: objectMetadataItem.nameSingular,
});
const { handleVisibilityChange: handleRecordGroupVisibilityChange } =
useRecordGroupVisibility({
viewBarId: recordIndexId,
});
const { handleOrderChange: handleRecordGroupOrderChange } =
useRecordGroupReorder({
objectNameSingular: objectMetadataItem.nameSingular,
viewBarId: recordIndexId,
});
const viewGroupSettingsUrl = getSettingsPagePath(SettingsPath.ObjectDetail, {
id: viewGroupFieldMetadataItem?.name,
objectSlug: objectNamePlural,
});
const visibleRecordFields =
viewType === ViewType.Kanban ? visibleBoardFields : visibleTableColumns;
const hiddenRecordFields =
viewType === ViewType.Kanban ? hiddenBoardFields : hiddenTableColumns;
const handleReorderFields =
viewType === ViewType.Kanban
? handleReorderBoardFields
: handleReorderColumns;
const handleChangeFieldVisibility =
viewType === ViewType.Kanban
? handleBoardFieldVisibilityChange
: handleColumnVisibilityChange;
const { openObjectRecordsSpreasheetImportDialog } =
useOpenObjectRecordsSpreasheetImportDialog(objectMetadataItem.nameSingular);
const { progress, download } = useExportRecords({
delayMs: 100,
filename: `${objectMetadataItem.nameSingular}.csv`,
objectMetadataItem,
recordIndexId,
viewType,
});
const location = useLocation();
const setNavigationMemorizedUrl = useSetRecoilState(
navigationMemorizedUrlState,
);
const isViewGroupMenuItemVisible =
viewGroupFieldMetadataItem &&
(visibleRecordGroups.length > 0 || hiddenRecordGroups.length > 0);
const contextStoreNumberOfSelectedRecords = useRecoilComponentValueV2(
contextStoreNumberOfSelectedRecordsComponentState,
);
const mode = contextStoreNumberOfSelectedRecords > 0 ? 'selection' : 'all';
useEffect(() => {
if (currentMenu === 'hiddenViewGroups' && hiddenRecordGroups.length === 0) {
setCurrentMenu('viewGroups');
}
}, [hiddenRecordGroups, currentMenu]);
return (
<>
{!currentMenu && (
<DropdownMenuItemsContainer>
{isViewGroupMenuItemVisible && (
<MenuItem
onClick={() => handleSelectMenu('viewGroups')}
LeftIcon={getIcon(currentViewWithCombinedFiltersAndSorts?.icon)}
text={viewGroupFieldMetadataItem.label}
hasSubMenu
/>
)}
<MenuItem
onClick={() => handleSelectMenu('fields')}
LeftIcon={IconTag}
text="Fields"
hasSubMenu
/>
<MenuItem
onClick={() => openObjectRecordsSpreasheetImportDialog()}
LeftIcon={IconFileImport}
text="Import"
/>
<MenuItem
onClick={download}
LeftIcon={IconFileExport}
text={displayedExportProgress(mode, progress)}
/>
<MenuItem
onClick={() => {
handleToggleTrashColumnFilter();
toggleSoftDeleteFilterState(true);
closeDropdown();
}}
LeftIcon={IconRotate2}
text={`Deleted ${objectNamePlural}`}
/>
</DropdownMenuItemsContainer>
)}
{currentMenu === 'viewGroups' && (
<>
<DropdownMenuHeader StartIcon={IconChevronLeft} onClick={resetMenu}>
{viewGroupFieldMetadataItem?.label}
</DropdownMenuHeader>
<ViewGroupsVisibilityDropdownSection
title={viewGroupFieldMetadataItem?.label ?? ''}
viewGroups={visibleRecordGroups}
onDragEnd={handleRecordGroupOrderChange}
onVisibilityChange={handleRecordGroupVisibilityChange}
isDraggable
showSubheader={false}
showDragGrip={true}
/>
{hiddenRecordGroups.length > 0 && (
<>
<DropdownMenuSeparator />
<DropdownMenuItemsContainer>
<MenuItemNavigate
onClick={() => handleSelectMenu('hiddenViewGroups')}
LeftIcon={IconEyeOff}
text={`Hidden ${viewGroupFieldMetadataItem?.label ?? ''}`}
/>
</DropdownMenuItemsContainer>
</>
)}
</>
)}
{currentMenu === 'fields' && (
<>
<DropdownMenuHeader StartIcon={IconChevronLeft} onClick={resetMenu}>
Fields
</DropdownMenuHeader>
<ScrollWrapper contextProviderName="dropdownMenuItemsContainer">
<ViewFieldsVisibilityDropdownSection
title="Visible"
fields={visibleRecordFields}
isDraggable
onDragEnd={handleReorderFields}
onVisibilityChange={handleChangeFieldVisibility}
showSubheader={false}
showDragGrip={true}
/>
</ScrollWrapper>
<DropdownMenuSeparator />
<DropdownMenuItemsContainer>
<MenuItemNavigate
onClick={() => handleSelectMenu('hiddenFields')}
LeftIcon={IconEyeOff}
text="Hidden Fields"
/>
</DropdownMenuItemsContainer>
</>
)}
{currentMenu === 'hiddenViewGroups' && (
<>
<DropdownMenuHeader
StartIcon={IconChevronLeft}
onClick={() => setCurrentMenu('viewGroups')}
>
Hidden {viewGroupFieldMetadataItem?.label}
</DropdownMenuHeader>
<ViewGroupsVisibilityDropdownSection
title={`Hidden ${viewGroupFieldMetadataItem?.label}`}
viewGroups={hiddenRecordGroups}
onVisibilityChange={handleRecordGroupVisibilityChange}
isDraggable={false}
showSubheader={false}
showDragGrip={false}
/>
<DropdownMenuSeparator />
<UndecoratedLink
to={viewGroupSettingsUrl}
onClick={() => {
setNavigationMemorizedUrl(location.pathname + location.search);
closeDropdown();
}}
>
<DropdownMenuItemsContainer>
<MenuItem LeftIcon={IconSettings} text="Edit field values" />
</DropdownMenuItemsContainer>
</UndecoratedLink>
</>
)}
{currentMenu === 'hiddenFields' && (
<>
<DropdownMenuHeader
StartIcon={IconChevronLeft}
onClick={() => setCurrentMenu('fields')}
>
Hidden Fields
</DropdownMenuHeader>
{hiddenRecordFields.length > 0 && (
<ScrollWrapper contextProviderName="dropdownMenuItemsContainer">
<ViewFieldsVisibilityDropdownSection
title="Hidden"
fields={hiddenRecordFields}
isDraggable={false}
onVisibilityChange={handleChangeFieldVisibility}
showSubheader={false}
showDragGrip={false}
/>
</ScrollWrapper>
)}
<DropdownMenuSeparator />
<UndecoratedLink
to={settingsUrl}
onClick={() => {
setNavigationMemorizedUrl(location.pathname + location.search);
closeDropdown();
}}
>
<DropdownMenuItemsContainer>
<MenuItem LeftIcon={IconSettings} text="Edit Fields" />
</DropdownMenuItemsContainer>
</UndecoratedLink>
</>
)}
{viewType === ViewType.Kanban && !currentMenu && (
<>
<DropdownMenuSeparator />
<DropdownMenuItemsContainer>
<MenuItemToggle
LeftIcon={IconBaselineDensitySmall}
onToggleChange={() =>
setAndPersistIsCompactModeActive(
!isCompactModeActive,
currentViewWithCombinedFiltersAndSorts,
)
}
toggled={isCompactModeActive}
text="Compact view"
toggleSize="small"
/>
</DropdownMenuItemsContainer>
</>
)}
</>
);
};

View File

@@ -1,2 +0,0 @@
export const RECORD_INDEX_BOARD_OPTIONS_DROPDOWN_ID =
'record-index-table-options-dropdown-id';

Some files were not shown because too many files have changed in this diff Show More