Feat: revamp group by settings (#8503)
This PR fix #8202 that is revamping the `Options` settings for board and table. <img width="221" alt="Screenshot 2024-11-15 at 11 47 52 AM" src="https://github.com/user-attachments/assets/0b143c95-810d-408b-b19e-c2678cd5653a"> <img width="214" alt="Screenshot 2024-11-15 at 11 47 59 AM" src="https://github.com/user-attachments/assets/3468734a-8174-4e36-a8ee-08dad6c56227"> <img width="210" alt="Screenshot 2024-11-15 at 11 48 10 AM" src="https://github.com/user-attachments/assets/300628f5-6645-4f1c-af8a-befce2714716"> <img width="212" alt="Screenshot 2024-11-15 at 11 48 37 AM" src="https://github.com/user-attachments/assets/37a3db40-2146-45eb-bea4-44e1041f5bcf"> <img width="214" alt="Screenshot 2024-11-15 at 11 48 44 AM" src="https://github.com/user-attachments/assets/42d2adcc-8f03-4f28-928b-d3c3d54d388a"> <img width="213" alt="Screenshot 2024-11-15 at 11 48 51 AM" src="https://github.com/user-attachments/assets/90824568-b979-46a7-9841-ab8b9978e138"> <img width="211" alt="Screenshot 2024-11-15 at 11 49 00 AM" src="https://github.com/user-attachments/assets/fa22446a-b1db-4d97-9a45-0778bf09ae3c"> --------- Co-authored-by: Charles Bochet <charles@twenty.com>
This commit is contained in:
@ -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>
|
||||
}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,18 @@
|
||||
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 ObjectOptionsDropdownButton = () => {
|
||||
const { isDropdownOpen, toggleDropdown } = useDropdown(
|
||||
OBJECT_OPTIONS_DROPDOWN_ID,
|
||||
);
|
||||
|
||||
return (
|
||||
<StyledHeaderDropdownButton
|
||||
isUnfolded={isDropdownOpen}
|
||||
onClick={toggleDropdown}
|
||||
>
|
||||
Options
|
||||
</StyledHeaderDropdownButton>
|
||||
);
|
||||
};
|
||||
@ -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 />;
|
||||
}
|
||||
};
|
||||
@ -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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@ -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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@ -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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@ -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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@ -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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@ -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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@ -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>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
@ -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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@ -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');
|
||||
@ -0,0 +1 @@
|
||||
export const EXPORT_TABLE_DATA_DEFAULT_PAGE_SIZE = 200;
|
||||
@ -0,0 +1 @@
|
||||
export const OBJECT_OPTIONS_DROPDOWN_ID = 'object-options-dropdown-id';
|
||||
@ -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',
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
@ -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,
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
@ -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,
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
@ -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);
|
||||
});
|
||||
});
|
||||
@ -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' },
|
||||
]);
|
||||
});
|
||||
});
|
||||
@ -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 };
|
||||
};
|
||||
@ -0,0 +1,202 @@
|
||||
import { OnDragEndResponder } from '@hello-pangea/dnd';
|
||||
import { useCallback, useMemo } from 'react';
|
||||
import { useRecoilState } from 'recoil';
|
||||
|
||||
import { useColumnDefinitionsFromFieldMetadata } from '@/object-metadata/hooks/useColumnDefinitionsFromFieldMetadata';
|
||||
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
|
||||
import { useRecordBoard } from '@/object-record/record-board/hooks/useRecordBoard';
|
||||
import { FieldMetadata } from '@/object-record/record-field/types/FieldMetadata';
|
||||
import { recordIndexFieldDefinitionsState } from '@/object-record/record-index/states/recordIndexFieldDefinitionsState';
|
||||
import { ColumnDefinition } from '@/object-record/record-table/types/ColumnDefinition';
|
||||
import { useSaveCurrentViewFields } from '@/views/hooks/useSaveCurrentViewFields';
|
||||
import { useUpdateCurrentView } from '@/views/hooks/useUpdateCurrentView';
|
||||
import { GraphQLView } from '@/views/types/GraphQLView';
|
||||
import { mapBoardFieldDefinitionsToViewFields } from '@/views/utils/mapBoardFieldDefinitionsToViewFields';
|
||||
import { mapArrayToObject } from '~/utils/array/mapArrayToObject';
|
||||
import { moveArrayItem } from '~/utils/array/moveArrayItem';
|
||||
import { isDeeplyEqual } from '~/utils/isDeeplyEqual';
|
||||
|
||||
type useObjectOptionsForBoardParams = {
|
||||
objectNameSingular: string;
|
||||
recordBoardId: string;
|
||||
viewBarId: string;
|
||||
};
|
||||
|
||||
export const useObjectOptionsForBoard = ({
|
||||
objectNameSingular,
|
||||
recordBoardId,
|
||||
viewBarId,
|
||||
}: useObjectOptionsForBoardParams) => {
|
||||
const [recordIndexFieldDefinitions, setRecordIndexFieldDefinitions] =
|
||||
useRecoilState(recordIndexFieldDefinitionsState);
|
||||
|
||||
const { saveViewFields } = useSaveCurrentViewFields(viewBarId);
|
||||
const { updateCurrentView } = useUpdateCurrentView(viewBarId);
|
||||
const { isCompactModeActiveState } = useRecordBoard(recordBoardId);
|
||||
|
||||
const [isCompactModeActive, setIsCompactModeActive] = useRecoilState(
|
||||
isCompactModeActiveState,
|
||||
);
|
||||
|
||||
const { objectMetadataItem } = useObjectMetadataItem({
|
||||
objectNameSingular,
|
||||
});
|
||||
|
||||
const { columnDefinitions } =
|
||||
useColumnDefinitionsFromFieldMetadata(objectMetadataItem);
|
||||
|
||||
const availableColumnDefinitions = useMemo(
|
||||
() =>
|
||||
columnDefinitions.filter(({ isLabelIdentifier }) => !isLabelIdentifier),
|
||||
[columnDefinitions],
|
||||
);
|
||||
|
||||
const recordIndexFieldDefinitionsByKey = useMemo(
|
||||
() =>
|
||||
mapArrayToObject(
|
||||
recordIndexFieldDefinitions,
|
||||
({ fieldMetadataId }) => fieldMetadataId,
|
||||
),
|
||||
[recordIndexFieldDefinitions],
|
||||
);
|
||||
|
||||
const visibleBoardFields = useMemo(
|
||||
() =>
|
||||
recordIndexFieldDefinitions
|
||||
.filter((boardField) => boardField.isVisible)
|
||||
.sort(
|
||||
(boardFieldA, boardFieldB) =>
|
||||
boardFieldA.position - boardFieldB.position,
|
||||
),
|
||||
[recordIndexFieldDefinitions],
|
||||
);
|
||||
|
||||
const hiddenBoardFields = useMemo(
|
||||
() =>
|
||||
availableColumnDefinitions
|
||||
.filter(
|
||||
({ fieldMetadataId }) =>
|
||||
!recordIndexFieldDefinitionsByKey[fieldMetadataId]?.isVisible,
|
||||
)
|
||||
.map((availableColumnDefinition) => {
|
||||
const { fieldMetadataId } = availableColumnDefinition;
|
||||
const existingBoardField =
|
||||
recordIndexFieldDefinitionsByKey[fieldMetadataId];
|
||||
|
||||
return {
|
||||
...(existingBoardField || availableColumnDefinition),
|
||||
isVisible: false,
|
||||
};
|
||||
}),
|
||||
[availableColumnDefinitions, recordIndexFieldDefinitionsByKey],
|
||||
);
|
||||
|
||||
const handleReorderBoardFields: OnDragEndResponder = useCallback(
|
||||
(result) => {
|
||||
if (!result.destination) {
|
||||
return;
|
||||
}
|
||||
|
||||
const reorderedVisibleBoardFields = moveArrayItem(visibleBoardFields, {
|
||||
fromIndex: result.source.index - 1,
|
||||
toIndex: result.destination.index - 1,
|
||||
});
|
||||
|
||||
if (isDeeplyEqual(visibleBoardFields, reorderedVisibleBoardFields))
|
||||
return;
|
||||
|
||||
const updatedFields = [...reorderedVisibleBoardFields].map(
|
||||
(field, index) => ({ ...field, position: index }),
|
||||
);
|
||||
|
||||
setRecordIndexFieldDefinitions(updatedFields);
|
||||
saveViewFields(mapBoardFieldDefinitionsToViewFields(updatedFields));
|
||||
},
|
||||
[saveViewFields, setRecordIndexFieldDefinitions, visibleBoardFields],
|
||||
);
|
||||
|
||||
// Todo : this seems over complex and should at least be extracted to an util with unit test.
|
||||
// Let's refactor this as we introduce the new viewBar
|
||||
const handleBoardFieldVisibilityChange = useCallback(
|
||||
async (
|
||||
updatedFieldDefinition: Omit<
|
||||
ColumnDefinition<FieldMetadata>,
|
||||
'size' | 'position'
|
||||
>,
|
||||
) => {
|
||||
const isNewViewField = !(
|
||||
updatedFieldDefinition.fieldMetadataId in
|
||||
recordIndexFieldDefinitionsByKey
|
||||
);
|
||||
|
||||
let updatedFieldsDefinitions: ColumnDefinition<FieldMetadata>[];
|
||||
|
||||
if (isNewViewField) {
|
||||
const correspondingFieldDefinition = availableColumnDefinitions.find(
|
||||
(availableColumnDefinition) =>
|
||||
availableColumnDefinition.fieldMetadataId ===
|
||||
updatedFieldDefinition.fieldMetadataId,
|
||||
);
|
||||
|
||||
if (!correspondingFieldDefinition) return;
|
||||
|
||||
const lastVisibleBoardField =
|
||||
visibleBoardFields[visibleBoardFields.length - 1];
|
||||
|
||||
updatedFieldsDefinitions = [
|
||||
...recordIndexFieldDefinitions,
|
||||
{
|
||||
...correspondingFieldDefinition,
|
||||
position: (lastVisibleBoardField?.position || 0) + 1,
|
||||
isVisible: true,
|
||||
},
|
||||
];
|
||||
} else {
|
||||
updatedFieldsDefinitions = recordIndexFieldDefinitions.map(
|
||||
(existingFieldDefinition) =>
|
||||
existingFieldDefinition.fieldMetadataId ===
|
||||
updatedFieldDefinition.fieldMetadataId
|
||||
? {
|
||||
...existingFieldDefinition,
|
||||
isVisible: !existingFieldDefinition.isVisible,
|
||||
}
|
||||
: existingFieldDefinition,
|
||||
);
|
||||
}
|
||||
|
||||
setRecordIndexFieldDefinitions(updatedFieldsDefinitions);
|
||||
|
||||
saveViewFields(
|
||||
mapBoardFieldDefinitionsToViewFields(updatedFieldsDefinitions),
|
||||
);
|
||||
},
|
||||
[
|
||||
recordIndexFieldDefinitionsByKey,
|
||||
setRecordIndexFieldDefinitions,
|
||||
saveViewFields,
|
||||
availableColumnDefinitions,
|
||||
visibleBoardFields,
|
||||
recordIndexFieldDefinitions,
|
||||
],
|
||||
);
|
||||
|
||||
const setAndPersistIsCompactModeActive = useCallback(
|
||||
(isCompactModeActive: boolean, view: GraphQLView | undefined) => {
|
||||
if (!view) return;
|
||||
setIsCompactModeActive(isCompactModeActive);
|
||||
updateCurrentView({
|
||||
isCompact: isCompactModeActive,
|
||||
});
|
||||
},
|
||||
[setIsCompactModeActive, updateCurrentView],
|
||||
);
|
||||
|
||||
return {
|
||||
handleReorderBoardFields,
|
||||
handleBoardFieldVisibilityChange,
|
||||
visibleBoardFields,
|
||||
hiddenBoardFields,
|
||||
isCompactModeActive,
|
||||
setAndPersistIsCompactModeActive,
|
||||
};
|
||||
};
|
||||
@ -0,0 +1,50 @@
|
||||
import { OnDragEndResponder } from '@hello-pangea/dnd';
|
||||
import { useCallback } from 'react';
|
||||
|
||||
import { useTableColumns } from '@/object-record/record-table/hooks/useTableColumns';
|
||||
import { hiddenTableColumnsComponentSelector } from '@/object-record/record-table/states/selectors/hiddenTableColumnsComponentSelector';
|
||||
import { visibleTableColumnsComponentSelector } from '@/object-record/record-table/states/selectors/visibleTableColumnsComponentSelector';
|
||||
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
|
||||
import { moveArrayItem } from '~/utils/array/moveArrayItem';
|
||||
|
||||
export const useObjectOptionsForTable = (recordTableId: string) => {
|
||||
const hiddenTableColumns = useRecoilComponentValueV2(
|
||||
hiddenTableColumnsComponentSelector,
|
||||
recordTableId,
|
||||
);
|
||||
const visibleTableColumns = useRecoilComponentValueV2(
|
||||
visibleTableColumnsComponentSelector,
|
||||
recordTableId,
|
||||
);
|
||||
|
||||
const { handleColumnVisibilityChange, handleColumnReorder } = useTableColumns(
|
||||
{ recordTableId: recordTableId },
|
||||
);
|
||||
|
||||
const handleReorderColumns: OnDragEndResponder = useCallback(
|
||||
(result) => {
|
||||
if (
|
||||
!result.destination ||
|
||||
result.destination.index === 1 ||
|
||||
result.source.index === 1
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const reorderedFields = moveArrayItem(visibleTableColumns, {
|
||||
fromIndex: result.source.index - 1,
|
||||
toIndex: result.destination.index - 1,
|
||||
});
|
||||
|
||||
handleColumnReorder(reorderedFields);
|
||||
},
|
||||
[visibleTableColumns, handleColumnReorder],
|
||||
);
|
||||
|
||||
return {
|
||||
handleReorderColumns,
|
||||
handleColumnVisibilityChange,
|
||||
visibleTableColumns,
|
||||
hiddenTableColumns,
|
||||
};
|
||||
};
|
||||
@ -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,
|
||||
};
|
||||
};
|
||||
@ -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,
|
||||
};
|
||||
};
|
||||
@ -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,
|
||||
);
|
||||
@ -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,
|
||||
});
|
||||
@ -0,0 +1,8 @@
|
||||
export type ObjectOptionsContentId =
|
||||
| 'viewSettings'
|
||||
| 'fields'
|
||||
| 'hiddenFields'
|
||||
| 'recordGroups'
|
||||
| 'hiddenRecordGroups'
|
||||
| 'recordGroupFields'
|
||||
| 'recordGroupSort';
|
||||
Reference in New Issue
Block a user