mirror of
https://github.com/lingble/twenty.git
synced 2025-10-30 04:12:28 +00:00
Merge branch 'main' into c--refactor-graphql-query-runner--move-api-event-emit-before-gql-processing
This commit is contained in:
@@ -1,4 +1,5 @@
|
|||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
|
set -e
|
||||||
|
|
||||||
# Check if the initialization has already been done and that we enabled automatic migration
|
# 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
|
if [ "${ENABLE_DB_MIGRATIONS}" = "true" ] && [ ! -f /app/docker-data/db_status ]; then
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ export const WorkflowRunActionEffect = () => {
|
|||||||
addActionMenuEntry({
|
addActionMenuEntry({
|
||||||
type: 'workflow-run',
|
type: 'workflow-run',
|
||||||
key: `workflow-run-${activeWorkflowVersion.id}`,
|
key: `workflow-run-${activeWorkflowVersion.id}`,
|
||||||
|
scope: 'global',
|
||||||
label: capitalize(activeWorkflowVersion.workflow.name),
|
label: capitalize(activeWorkflowVersion.workflow.name),
|
||||||
position: index,
|
position: index,
|
||||||
Icon: IconSettingsAutomation,
|
Icon: IconSettingsAutomation,
|
||||||
|
|||||||
@@ -37,8 +37,8 @@ export const DeleteRecordsActionEffect = ({
|
|||||||
objectNameSingular: objectMetadataItem.nameSingular,
|
objectNameSingular: objectMetadataItem.nameSingular,
|
||||||
});
|
});
|
||||||
|
|
||||||
const favorites = useFavorites();
|
const { sortedFavorites: favorites } = useFavorites();
|
||||||
const deleteFavorite = useDeleteFavorite();
|
const { deleteFavorite } = useDeleteFavorite();
|
||||||
|
|
||||||
const contextStoreNumberOfSelectedRecords = useRecoilComponentValueV2(
|
const contextStoreNumberOfSelectedRecords = useRecoilComponentValueV2(
|
||||||
contextStoreNumberOfSelectedRecordsComponentState,
|
contextStoreNumberOfSelectedRecordsComponentState,
|
||||||
@@ -106,6 +106,7 @@ export const DeleteRecordsActionEffect = ({
|
|||||||
if (canDelete) {
|
if (canDelete) {
|
||||||
addActionMenuEntry({
|
addActionMenuEntry({
|
||||||
type: 'standard',
|
type: 'standard',
|
||||||
|
scope: 'record-selection',
|
||||||
key: 'delete',
|
key: 'delete',
|
||||||
label: 'Delete',
|
label: 'Delete',
|
||||||
position,
|
position,
|
||||||
|
|||||||
@@ -32,12 +32,10 @@ export const ExportRecordsActionEffect = ({
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
addActionMenuEntry({
|
addActionMenuEntry({
|
||||||
type: 'standard',
|
type: 'standard',
|
||||||
|
scope: 'record-selection',
|
||||||
key: 'export',
|
key: 'export',
|
||||||
position,
|
position,
|
||||||
label: displayedExportProgress(
|
label: displayedExportProgress(progress),
|
||||||
contextStoreNumberOfSelectedRecords > 0 ? 'selection' : 'all',
|
|
||||||
progress,
|
|
||||||
),
|
|
||||||
Icon: IconDatabaseExport,
|
Icon: IconDatabaseExport,
|
||||||
accent: 'default',
|
accent: 'default',
|
||||||
onClick: () => download(),
|
onClick: () => download(),
|
||||||
|
|||||||
@@ -23,11 +23,11 @@ export const ManageFavoritesActionEffect = ({
|
|||||||
contextStoreTargetedRecordsRuleComponentState,
|
contextStoreTargetedRecordsRuleComponentState,
|
||||||
);
|
);
|
||||||
|
|
||||||
const favorites = useFavorites();
|
const { sortedFavorites: favorites } = useFavorites();
|
||||||
|
|
||||||
const createFavorite = useCreateFavorite();
|
const { createFavorite } = useCreateFavorite();
|
||||||
|
|
||||||
const deleteFavorite = useDeleteFavorite();
|
const { deleteFavorite } = useDeleteFavorite();
|
||||||
|
|
||||||
const selectedRecordId =
|
const selectedRecordId =
|
||||||
contextStoreTargetedRecordsRule.mode === 'selection'
|
contextStoreTargetedRecordsRule.mode === 'selection'
|
||||||
@@ -51,6 +51,7 @@ export const ManageFavoritesActionEffect = ({
|
|||||||
|
|
||||||
addActionMenuEntry({
|
addActionMenuEntry({
|
||||||
type: 'standard',
|
type: 'standard',
|
||||||
|
scope: 'record-selection',
|
||||||
key: 'manage-favorites',
|
key: 'manage-favorites',
|
||||||
label: isFavorite ? 'Remove from favorites' : 'Add to favorites',
|
label: isFavorite ? 'Remove from favorites' : 'Add to favorites',
|
||||||
position,
|
position,
|
||||||
|
|||||||
@@ -57,6 +57,7 @@ export const WorkflowRunRecordActionEffect = ({
|
|||||||
addActionMenuEntry({
|
addActionMenuEntry({
|
||||||
type: 'workflow-run',
|
type: 'workflow-run',
|
||||||
key: `workflow-run-${activeWorkflowVersion.id}`,
|
key: `workflow-run-${activeWorkflowVersion.id}`,
|
||||||
|
scope: 'record-selection',
|
||||||
label: capitalize(activeWorkflowVersion.workflow.name),
|
label: capitalize(activeWorkflowVersion.workflow.name),
|
||||||
position: index,
|
position: index,
|
||||||
Icon: IconSettingsAutomation,
|
Icon: IconSettingsAutomation,
|
||||||
|
|||||||
@@ -65,21 +65,25 @@ export const RightDrawerActionMenuDropdown = () => {
|
|||||||
}}
|
}}
|
||||||
dropdownComponents={
|
dropdownComponents={
|
||||||
<DropdownMenuItemsContainer>
|
<DropdownMenuItemsContainer>
|
||||||
{actionMenuEntries.map((item, index) => (
|
{actionMenuEntries
|
||||||
<MenuItem
|
.filter(
|
||||||
key={index}
|
(actionMenuEntry) => actionMenuEntry.scope === 'record-selection',
|
||||||
LeftIcon={item.Icon}
|
)
|
||||||
onClick={() => {
|
.map((actionMenuEntry, index) => (
|
||||||
closeDropdown(
|
<MenuItem
|
||||||
getRightDrawerActionMenuDropdownIdFromActionMenuId(
|
key={index}
|
||||||
actionMenuId,
|
LeftIcon={actionMenuEntry.Icon}
|
||||||
),
|
onClick={() => {
|
||||||
);
|
closeDropdown(
|
||||||
item.onClick?.();
|
getRightDrawerActionMenuDropdownIdFromActionMenuId(
|
||||||
}}
|
actionMenuId,
|
||||||
text={item.label}
|
),
|
||||||
/>
|
);
|
||||||
))}
|
actionMenuEntry.onClick?.();
|
||||||
|
}}
|
||||||
|
text={actionMenuEntry.label}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
</DropdownMenuItemsContainer>
|
</DropdownMenuItemsContainer>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -48,6 +48,7 @@ const meta: Meta<typeof RecordIndexActionMenuBar> = {
|
|||||||
|
|
||||||
map.set('delete', {
|
map.set('delete', {
|
||||||
isPinned: true,
|
isPinned: true,
|
||||||
|
scope: 'record-selection',
|
||||||
type: 'standard',
|
type: 'standard',
|
||||||
key: 'delete',
|
key: 'delete',
|
||||||
label: 'Delete',
|
label: 'Delete',
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ export const Default: Story = {
|
|||||||
args: {
|
args: {
|
||||||
entry: {
|
entry: {
|
||||||
type: 'standard',
|
type: 'standard',
|
||||||
|
scope: 'record-selection',
|
||||||
key: 'delete',
|
key: 'delete',
|
||||||
label: 'Delete',
|
label: 'Delete',
|
||||||
position: 0,
|
position: 0,
|
||||||
@@ -35,6 +36,7 @@ export const WithDangerAccent: Story = {
|
|||||||
args: {
|
args: {
|
||||||
entry: {
|
entry: {
|
||||||
type: 'standard',
|
type: 'standard',
|
||||||
|
scope: 'record-selection',
|
||||||
key: 'delete',
|
key: 'delete',
|
||||||
label: 'Delete',
|
label: 'Delete',
|
||||||
position: 0,
|
position: 0,
|
||||||
@@ -49,6 +51,7 @@ export const WithInteraction: Story = {
|
|||||||
args: {
|
args: {
|
||||||
entry: {
|
entry: {
|
||||||
type: 'standard',
|
type: 'standard',
|
||||||
|
scope: 'record-selection',
|
||||||
key: 'markAsDone',
|
key: 'markAsDone',
|
||||||
label: 'Mark as done',
|
label: 'Mark as done',
|
||||||
position: 0,
|
position: 0,
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ const meta: Meta<typeof RecordIndexActionMenuDropdown> = {
|
|||||||
|
|
||||||
map.set('delete', {
|
map.set('delete', {
|
||||||
type: 'standard',
|
type: 'standard',
|
||||||
|
scope: 'record-selection',
|
||||||
key: 'delete',
|
key: 'delete',
|
||||||
label: 'Delete',
|
label: 'Delete',
|
||||||
position: 0,
|
position: 0,
|
||||||
@@ -51,6 +52,7 @@ const meta: Meta<typeof RecordIndexActionMenuDropdown> = {
|
|||||||
|
|
||||||
map.set('markAsDone', {
|
map.set('markAsDone', {
|
||||||
type: 'standard',
|
type: 'standard',
|
||||||
|
scope: 'record-selection',
|
||||||
key: 'markAsDone',
|
key: 'markAsDone',
|
||||||
label: 'Mark as done',
|
label: 'Mark as done',
|
||||||
position: 1,
|
position: 1,
|
||||||
@@ -60,6 +62,7 @@ const meta: Meta<typeof RecordIndexActionMenuDropdown> = {
|
|||||||
|
|
||||||
map.set('addToFavorites', {
|
map.set('addToFavorites', {
|
||||||
type: 'standard',
|
type: 'standard',
|
||||||
|
scope: 'record-selection',
|
||||||
key: 'addToFavorites',
|
key: 'addToFavorites',
|
||||||
label: 'Add to favorites',
|
label: 'Add to favorites',
|
||||||
position: 2,
|
position: 2,
|
||||||
|
|||||||
@@ -55,6 +55,7 @@ const meta: Meta<typeof RightDrawerActionMenuDropdown> = {
|
|||||||
|
|
||||||
map.set('addToFavorites', {
|
map.set('addToFavorites', {
|
||||||
type: 'standard',
|
type: 'standard',
|
||||||
|
scope: 'record-selection',
|
||||||
key: 'addToFavorites',
|
key: 'addToFavorites',
|
||||||
label: 'Add to favorites',
|
label: 'Add to favorites',
|
||||||
position: 0,
|
position: 0,
|
||||||
@@ -64,6 +65,7 @@ const meta: Meta<typeof RightDrawerActionMenuDropdown> = {
|
|||||||
|
|
||||||
map.set('export', {
|
map.set('export', {
|
||||||
type: 'standard',
|
type: 'standard',
|
||||||
|
scope: 'record-selection',
|
||||||
key: 'export',
|
key: 'export',
|
||||||
label: 'Export',
|
label: 'Export',
|
||||||
position: 1,
|
position: 1,
|
||||||
@@ -73,6 +75,7 @@ const meta: Meta<typeof RightDrawerActionMenuDropdown> = {
|
|||||||
|
|
||||||
map.set('delete', {
|
map.set('delete', {
|
||||||
type: 'standard',
|
type: 'standard',
|
||||||
|
scope: 'record-selection',
|
||||||
key: 'delete',
|
key: 'delete',
|
||||||
label: 'Delete',
|
label: 'Delete',
|
||||||
position: 2,
|
position: 2,
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { IconComponent, MenuItemAccent } from 'twenty-ui';
|
|||||||
|
|
||||||
export type ActionMenuEntry = {
|
export type ActionMenuEntry = {
|
||||||
type: 'standard' | 'workflow-run';
|
type: 'standard' | 'workflow-run';
|
||||||
|
scope: 'global' | 'record-selection';
|
||||||
key: string;
|
key: string;
|
||||||
label: string;
|
label: string;
|
||||||
position: number;
|
position: number;
|
||||||
|
|||||||
@@ -61,9 +61,9 @@ export const CurrentWorkspaceMemberFavorites = ({
|
|||||||
const selectedFavoriteIndex = folder.favorites.findIndex((favorite) =>
|
const selectedFavoriteIndex = folder.favorites.findIndex((favorite) =>
|
||||||
isLocationMatchingFavorite(currentPath, currentViewPath, favorite),
|
isLocationMatchingFavorite(currentPath, currentViewPath, favorite),
|
||||||
);
|
);
|
||||||
const handleReorderFavorite = useReorderFavorite();
|
const { handleReorderFavorite } = useReorderFavorite();
|
||||||
|
|
||||||
const deleteFavorite = useDeleteFavorite();
|
const { deleteFavorite } = useDeleteFavorite();
|
||||||
|
|
||||||
const favoriteFolderContentLength = folder.favorites.length;
|
const favoriteFolderContentLength = folder.favorites.length;
|
||||||
|
|
||||||
@@ -154,6 +154,7 @@ export const CurrentWorkspaceMemberFavorites = ({
|
|||||||
key={favorite.id}
|
key={favorite.id}
|
||||||
draggableId={favorite.id}
|
draggableId={favorite.id}
|
||||||
index={index}
|
index={index}
|
||||||
|
isInsideScrollableContainer
|
||||||
itemComponent={
|
itemComponent={
|
||||||
<NavigationDrawerSubItem
|
<NavigationDrawerSubItem
|
||||||
key={favorite.id}
|
key={favorite.id}
|
||||||
|
|||||||
@@ -4,8 +4,8 @@ import { useRecoilState, useRecoilValue } from 'recoil';
|
|||||||
import {
|
import {
|
||||||
IconFolderPlus,
|
IconFolderPlus,
|
||||||
IconHeartOff,
|
IconHeartOff,
|
||||||
isDefined,
|
|
||||||
LightIconButton,
|
LightIconButton,
|
||||||
|
isDefined,
|
||||||
} from 'twenty-ui';
|
} from 'twenty-ui';
|
||||||
|
|
||||||
import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState';
|
import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState';
|
||||||
@@ -32,9 +32,9 @@ export const CurrentWorkspaceMemberFavoritesFolders = () => {
|
|||||||
const currentViewPath = useLocation().pathname + useLocation().search;
|
const currentViewPath = useLocation().pathname + useLocation().search;
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const currentWorkspaceMember = useRecoilValue(currentWorkspaceMemberState);
|
const currentWorkspaceMember = useRecoilValue(currentWorkspaceMemberState);
|
||||||
const favorites = useFavorites();
|
const { sortedFavorites: favorites } = useFavorites();
|
||||||
const deleteFavorite = useDeleteFavorite();
|
const { deleteFavorite } = useDeleteFavorite();
|
||||||
const handleReorderFavorite = useReorderFavorite();
|
const { handleReorderFavorite } = useReorderFavorite();
|
||||||
const [isFavoriteFolderCreating, setIsFavoriteFolderCreating] =
|
const [isFavoriteFolderCreating, setIsFavoriteFolderCreating] =
|
||||||
useRecoilState(isFavoriteFolderCreatingState);
|
useRecoilState(isFavoriteFolderCreatingState);
|
||||||
|
|
||||||
@@ -50,24 +50,25 @@ export const CurrentWorkspaceMemberFavoritesFolders = () => {
|
|||||||
const toggleNewFolder = () => {
|
const toggleNewFolder = () => {
|
||||||
setIsFavoriteFolderCreating((current) => !current);
|
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)) {
|
if (loading && isDefined(currentWorkspaceMember)) {
|
||||||
return <FavoritesSkeletonLoader />;
|
return <FavoritesSkeletonLoader />;
|
||||||
}
|
}
|
||||||
|
|
||||||
const currentWorkspaceMemberFavorites = favorites.filter(
|
const orphanFavorites = favorites.filter(
|
||||||
(favorite) => favorite.workspaceMemberId === currentWorkspaceMember?.id,
|
|
||||||
);
|
|
||||||
|
|
||||||
const orphanFavorites = currentWorkspaceMemberFavorites.filter(
|
|
||||||
(favorite) => !favorite.favoriteFolderId,
|
(favorite) => !favorite.favoriteFolderId,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (
|
if (!shouldDisplayFavorites) {
|
||||||
(!currentWorkspaceMemberFavorites ||
|
|
||||||
currentWorkspaceMemberFavorites.length === 0) &&
|
|
||||||
!isFavoriteFolderCreating
|
|
||||||
) {
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -104,6 +105,7 @@ export const CurrentWorkspaceMemberFavoritesFolders = () => {
|
|||||||
key={favorite.id}
|
key={favorite.id}
|
||||||
draggableId={favorite.id}
|
draggableId={favorite.id}
|
||||||
index={index}
|
index={index}
|
||||||
|
isInsideScrollableContainer={true}
|
||||||
itemComponent={
|
itemComponent={
|
||||||
<NavigationDrawerItem
|
<NavigationDrawerItem
|
||||||
key={favorite.id}
|
key={favorite.id}
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ export const FavoriteFolderNavigationDrawerItemDropdown = ({
|
|||||||
dropdownHotkeyScope={{
|
dropdownHotkeyScope={{
|
||||||
scope: FavoriteFolderHotkeyScope.FavoriteFolderRightIconDropdown,
|
scope: FavoriteFolderHotkeyScope.FavoriteFolderRightIconDropdown,
|
||||||
}}
|
}}
|
||||||
|
usePortal
|
||||||
data-select-disable
|
data-select-disable
|
||||||
clickableComponent={
|
clickableComponent={
|
||||||
<LightIconButton Icon={IconDotsVertical} accent="tertiary" />
|
<LightIconButton Icon={IconDotsVertical} accent="tertiary" />
|
||||||
|
|||||||
@@ -18,8 +18,8 @@ export const FavoriteFolders = ({
|
|||||||
}: FavoriteFoldersProps) => {
|
}: FavoriteFoldersProps) => {
|
||||||
const [newFolderName, setNewFolderName] = useState('');
|
const [newFolderName, setNewFolderName] = useState('');
|
||||||
|
|
||||||
const favoritesByFolder = useFavoritesByFolder();
|
const { favoritesByFolder } = useFavoritesByFolder();
|
||||||
const createFavoriteFolder = useCreateFavoriteFolder();
|
const { createNewFavoriteFolder } = useCreateFavoriteFolder();
|
||||||
|
|
||||||
const [isFavoriteFolderCreating, setIsFavoriteFolderCreating] =
|
const [isFavoriteFolderCreating, setIsFavoriteFolderCreating] =
|
||||||
useRecoilState(isFavoriteFolderCreatingState);
|
useRecoilState(isFavoriteFolderCreatingState);
|
||||||
@@ -33,12 +33,12 @@ export const FavoriteFolders = ({
|
|||||||
|
|
||||||
setIsFavoriteFolderCreating(false);
|
setIsFavoriteFolderCreating(false);
|
||||||
setNewFolderName('');
|
setNewFolderName('');
|
||||||
await createFavoriteFolder(value);
|
await createNewFavoriteFolder(value);
|
||||||
return true;
|
return true;
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleClickOutside = async (
|
const handleClickOutside = async (
|
||||||
event: MouseEvent | TouchEvent,
|
_event: MouseEvent | TouchEvent,
|
||||||
value: string,
|
value: string,
|
||||||
) => {
|
) => {
|
||||||
if (!value) {
|
if (!value) {
|
||||||
@@ -48,7 +48,7 @@ export const FavoriteFolders = ({
|
|||||||
|
|
||||||
setIsFavoriteFolderCreating(false);
|
setIsFavoriteFolderCreating(false);
|
||||||
setNewFolderName('');
|
setNewFolderName('');
|
||||||
await createFavoriteFolder(value);
|
await createNewFavoriteFolder(value);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleCancelFavoriteFolderCreation = () => {
|
const handleCancelFavoriteFolderCreation = () => {
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
|
import { PageFavoriteButton } from '@/favorites/components/PageFavoriteButton';
|
||||||
import { FavoriteFolderPicker } from '@/favorites/favorite-folder-picker/components/FavoriteFolderPicker';
|
import { FavoriteFolderPicker } from '@/favorites/favorite-folder-picker/components/FavoriteFolderPicker';
|
||||||
import { FavoriteFolderPickerEffect } from '@/favorites/favorite-folder-picker/components/FavoriteFolderPickerEffect';
|
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 { ObjectRecord } from '@/object-record/types/ObjectRecord';
|
||||||
import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown';
|
import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown';
|
||||||
import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown';
|
import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown';
|
||||||
import { DropdownScope } from '@/ui/layout/dropdown/scopes/DropdownScope';
|
import { DropdownScope } from '@/ui/layout/dropdown/scopes/DropdownScope';
|
||||||
import { PageFavoriteButton } from '@/ui/layout/page/components/PageFavoriteButton';
|
|
||||||
|
|
||||||
type PageFavoriteFoldersDropdownProps = {
|
type PageFavoriteFoldersDropdownProps = {
|
||||||
dropdownId: string;
|
dropdownId: string;
|
||||||
@@ -23,7 +23,9 @@ export const PageFavoriteFoldersDropdown = ({
|
|||||||
const { closeDropdown } = useDropdown(dropdownId);
|
const { closeDropdown } = useDropdown(dropdownId);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<FavoriteFolderPickerScope favoriteFoldersScopeId={dropdownId}>
|
<FavoriteFolderPickerComponentInstanceContext
|
||||||
|
favoriteFoldersScopeId={dropdownId}
|
||||||
|
>
|
||||||
<DropdownScope dropdownScopeId={dropdownId}>
|
<DropdownScope dropdownScopeId={dropdownId}>
|
||||||
<Dropdown
|
<Dropdown
|
||||||
dropdownId={dropdownId}
|
dropdownId={dropdownId}
|
||||||
@@ -44,6 +46,6 @@ export const PageFavoriteFoldersDropdown = ({
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</DropdownScope>
|
</DropdownScope>
|
||||||
</FavoriteFolderPickerScope>
|
</FavoriteFolderPickerComponentInstanceContext>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -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;
|
|
||||||
@@ -36,7 +36,7 @@ export const FavoriteFolderPicker = ({
|
|||||||
FavoriteFolderPickerInstanceContext,
|
FavoriteFolderPickerInstanceContext,
|
||||||
);
|
);
|
||||||
|
|
||||||
const { getFoldersByIds, toggleFolderSelection } = useFavoriteFolderPicker({
|
const { favoriteFolders, toggleFolderSelection } = useFavoriteFolderPicker({
|
||||||
record,
|
record,
|
||||||
objectNameSingular,
|
objectNameSingular,
|
||||||
});
|
});
|
||||||
@@ -45,8 +45,7 @@ export const FavoriteFolderPicker = ({
|
|||||||
favoriteFolderSearchFilterComponentState,
|
favoriteFolderSearchFilterComponentState,
|
||||||
);
|
);
|
||||||
|
|
||||||
const folders = getFoldersByIds();
|
const filteredFolders = favoriteFolders.filter((folder) =>
|
||||||
const filteredFolders = folders.filter((folder) =>
|
|
||||||
folder.name
|
folder.name
|
||||||
.toLowerCase()
|
.toLowerCase()
|
||||||
.includes(favoriteFoldersSearchFilter.toLowerCase()),
|
.includes(favoriteFoldersSearchFilter.toLowerCase()),
|
||||||
@@ -94,7 +93,7 @@ export const FavoriteFolderPicker = ({
|
|||||||
<DropdownMenuSeparator />
|
<DropdownMenuSeparator />
|
||||||
<DropdownMenuItemsContainer hasMaxHeight>
|
<DropdownMenuItemsContainer hasMaxHeight>
|
||||||
<FavoriteFolderPickerList
|
<FavoriteFolderPickerList
|
||||||
folders={folders}
|
folders={favoriteFolders}
|
||||||
toggleFolderSelection={toggleFolderSelection}
|
toggleFolderSelection={toggleFolderSelection}
|
||||||
/>
|
/>
|
||||||
</DropdownMenuItemsContainer>
|
</DropdownMenuItemsContainer>
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ export const FavoriteFolderPickerEffect = ({
|
|||||||
|
|
||||||
const { favoriteFolders } = usePrefetchedFavoritesFoldersData();
|
const { favoriteFolders } = usePrefetchedFavoritesFoldersData();
|
||||||
|
|
||||||
const favorites = useFavorites();
|
const { sortedFavorites: favorites } = useFavorites();
|
||||||
const setCheckedState = useSetRecoilComponentStateV2(
|
const setCheckedState = useSetRecoilComponentStateV2(
|
||||||
favoriteFolderPickerCheckedComponentState,
|
favoriteFolderPickerCheckedComponentState,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -14,10 +14,6 @@ const StyledFooter = styled.div`
|
|||||||
border-top: 1px solid ${({ theme }) => theme.border.color.light};
|
border-top: 1px solid ${({ theme }) => theme.border.color.light};
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const StyledIconPlus = styled(IconPlus)`
|
|
||||||
padding-left: ${({ theme }) => theme.spacing(1)};
|
|
||||||
`;
|
|
||||||
|
|
||||||
export const FavoriteFolderPickerFooter = () => {
|
export const FavoriteFolderPickerFooter = () => {
|
||||||
const [, setIsFavoriteFolderCreating] = useRecoilState(
|
const [, setIsFavoriteFolderCreating] = useRecoilState(
|
||||||
isFavoriteFolderCreatingState,
|
isFavoriteFolderCreatingState,
|
||||||
@@ -35,7 +31,7 @@ export const FavoriteFolderPickerFooter = () => {
|
|||||||
closeDropdown();
|
closeDropdown();
|
||||||
}}
|
}}
|
||||||
text="Add folder"
|
text="Add folder"
|
||||||
LeftIcon={() => <StyledIconPlus size={theme.icon.size.md} />}
|
LeftIcon={() => <IconPlus size={theme.icon.size.md} />}
|
||||||
/>
|
/>
|
||||||
</DropdownMenuItemsContainer>
|
</DropdownMenuItemsContainer>
|
||||||
</StyledFooter>
|
</StyledFooter>
|
||||||
|
|||||||
@@ -4,8 +4,7 @@ import { FavoriteFolder } from '@/favorites/types/FavoriteFolder';
|
|||||||
import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownMenuSeparator';
|
import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownMenuSeparator';
|
||||||
import { useRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentStateV2';
|
import { useRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentStateV2';
|
||||||
import styled from '@emotion/styled';
|
import styled from '@emotion/styled';
|
||||||
import { MenuItem } from 'twenty-ui';
|
import { MenuItem, MenuItemMultiSelect } from 'twenty-ui';
|
||||||
import { FavoriteFolderMenuItemMultiSelect } from './FavoriteFolderMenuItemMultiSelect';
|
|
||||||
|
|
||||||
const StyledItemsContainer = styled.div`
|
const StyledItemsContainer = styled.div`
|
||||||
width: 100%;
|
width: 100%;
|
||||||
@@ -30,6 +29,7 @@ export const FavoriteFolderPickerList = ({
|
|||||||
const [favoriteFoldersSearchFilter] = useRecoilComponentStateV2(
|
const [favoriteFoldersSearchFilter] = useRecoilComponentStateV2(
|
||||||
favoriteFolderSearchFilterComponentState,
|
favoriteFolderSearchFilterComponentState,
|
||||||
);
|
);
|
||||||
|
|
||||||
const [favoriteFolderPickerChecked] = useRecoilComponentStateV2(
|
const [favoriteFolderPickerChecked] = useRecoilComponentStateV2(
|
||||||
favoriteFolderPickerCheckedComponentState,
|
favoriteFolderPickerCheckedComponentState,
|
||||||
);
|
);
|
||||||
@@ -47,7 +47,7 @@ export const FavoriteFolderPickerList = ({
|
|||||||
return (
|
return (
|
||||||
<StyledItemsContainer>
|
<StyledItemsContainer>
|
||||||
{showNoFolderOption && (
|
{showNoFolderOption && (
|
||||||
<FavoriteFolderMenuItemMultiSelect
|
<MenuItemMultiSelect
|
||||||
key={`menu-${NO_FOLDER_ID}`}
|
key={`menu-${NO_FOLDER_ID}`}
|
||||||
onSelectChange={() => toggleFolderSelection(NO_FOLDER_ID)}
|
onSelectChange={() => toggleFolderSelection(NO_FOLDER_ID)}
|
||||||
selected={favoriteFolderPickerChecked.includes(NO_FOLDER_ID)}
|
selected={favoriteFolderPickerChecked.includes(NO_FOLDER_ID)}
|
||||||
@@ -60,7 +60,7 @@ export const FavoriteFolderPickerList = ({
|
|||||||
)}
|
)}
|
||||||
{filteredFolders.length > 0
|
{filteredFolders.length > 0
|
||||||
? filteredFolders.map((folder) => (
|
? filteredFolders.map((folder) => (
|
||||||
<FavoriteFolderMenuItemMultiSelect
|
<MenuItemMultiSelect
|
||||||
key={`menu-${folder.id}`}
|
key={`menu-${folder.id}`}
|
||||||
onSelectChange={() => toggleFolderSelection(folder.id)}
|
onSelectChange={() => toggleFolderSelection(folder.id)}
|
||||||
selected={favoriteFolderPickerChecked.includes(folder.id)}
|
selected={favoriteFolderPickerChecked.includes(folder.id)}
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { favoriteFolderIdsPickerComponentState } from '@/favorites/favorite-folder-picker/states/favoriteFolderIdPickerComponentState';
|
|
||||||
import { favoriteFolderPickerCheckedComponentState } from '@/favorites/favorite-folder-picker/states/favoriteFolderPickerCheckedComponentState';
|
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 { useCreateFavorite } from '@/favorites/hooks/useCreateFavorite';
|
||||||
import { useDeleteFavorite } from '@/favorites/hooks/useDeleteFavorite';
|
import { useDeleteFavorite } from '@/favorites/hooks/useDeleteFavorite';
|
||||||
import { useFavorites } from '@/favorites/hooks/useFavorites';
|
import { useFavorites } from '@/favorites/hooks/useFavorites';
|
||||||
@@ -8,7 +7,7 @@ import { useFavorites } from '@/favorites/hooks/useFavorites';
|
|||||||
import { FavoriteFolder } from '@/favorites/types/FavoriteFolder';
|
import { FavoriteFolder } from '@/favorites/types/FavoriteFolder';
|
||||||
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
|
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
|
||||||
import { useRecoilComponentCallbackStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackStateV2';
|
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 { useRecoilCallback } from 'recoil';
|
||||||
import { isDefined } from 'twenty-ui';
|
import { isDefined } from 'twenty-ui';
|
||||||
|
|
||||||
@@ -17,46 +16,26 @@ type useFavoriteFolderPickerProps = {
|
|||||||
objectNameSingular: string;
|
objectNameSingular: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
type FolderOperations = {
|
type useFavoriteFolderPickerReturnType = {
|
||||||
getFoldersByIds: () => FavoriteFolder[];
|
favoriteFolders: FavoriteFolder[];
|
||||||
toggleFolderSelection: (folderId: string) => Promise<void>;
|
toggleFolderSelection: (folderId: string) => Promise<void>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useFavoriteFolderPicker = ({
|
export const useFavoriteFolderPicker = ({
|
||||||
record,
|
record,
|
||||||
objectNameSingular,
|
objectNameSingular,
|
||||||
}: useFavoriteFolderPickerProps): FolderOperations => {
|
}: useFavoriteFolderPickerProps): useFavoriteFolderPickerReturnType => {
|
||||||
const [favoriteFolderIdsPicker] = useRecoilComponentStateV2(
|
|
||||||
favoriteFolderIdsPickerComponentState,
|
|
||||||
);
|
|
||||||
|
|
||||||
const favoriteFoldersMultiSelectCheckedState =
|
const favoriteFoldersMultiSelectCheckedState =
|
||||||
useRecoilComponentCallbackStateV2(
|
useRecoilComponentCallbackStateV2(
|
||||||
favoriteFolderPickerCheckedComponentState,
|
favoriteFolderPickerCheckedComponentState,
|
||||||
);
|
);
|
||||||
|
|
||||||
const favoriteFolderPickerFamilyState = useRecoilComponentCallbackStateV2(
|
const { sortedFavorites: favorites } = useFavorites();
|
||||||
favoriteFolderPickerComponentFamilyState,
|
const { createFavorite } = useCreateFavorite();
|
||||||
);
|
const { deleteFavorite } = useDeleteFavorite();
|
||||||
|
|
||||||
const favorites = useFavorites();
|
const favoriteFolders = useRecoilComponentValueV2(
|
||||||
const createFavorite = useCreateFavorite();
|
favoriteFoldersComponentSelector,
|
||||||
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 toggleFolderSelection = useRecoilCallback(
|
const toggleFolderSelection = useRecoilCallback(
|
||||||
@@ -123,7 +102,7 @@ export const useFavoriteFolderPicker = ({
|
|||||||
);
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
getFoldersByIds,
|
favoriteFolders,
|
||||||
toggleFolderSelection,
|
toggleFolderSelection,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,15 +1,15 @@
|
|||||||
import { FavoriteFolderPickerInstanceContext } from '@/favorites/favorite-folder-picker/states/context/FavoriteFolderPickerInstanceContext';
|
import { FavoriteFolderPickerInstanceContext } from '@/favorites/favorite-folder-picker/states/context/FavoriteFolderPickerInstanceContext';
|
||||||
import { ReactNode } from 'react';
|
import { ReactNode } from 'react';
|
||||||
|
|
||||||
type FavoriteFolderPickerScopeProps = {
|
type FavoriteFolderPickerComponentInstanceContextProps = {
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
favoriteFoldersScopeId: string;
|
favoriteFoldersScopeId: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const FavoriteFolderPickerScope = ({
|
export const FavoriteFolderPickerComponentInstanceContext = ({
|
||||||
children,
|
children,
|
||||||
favoriteFoldersScopeId,
|
favoriteFoldersScopeId,
|
||||||
}: FavoriteFolderPickerScopeProps) => {
|
}: FavoriteFolderPickerComponentInstanceContextProps) => {
|
||||||
return (
|
return (
|
||||||
<FavoriteFolderPickerInstanceContext.Provider
|
<FavoriteFolderPickerInstanceContext.Provider
|
||||||
value={{ instanceId: favoriteFoldersScopeId }}
|
value={{ instanceId: favoriteFoldersScopeId }}
|
||||||
|
|||||||
@@ -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,
|
|
||||||
});
|
|
||||||
@@ -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));
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -44,7 +44,10 @@ describe('useCreateFavorite', () => {
|
|||||||
{ wrapper: Wrapper },
|
{ wrapper: Wrapper },
|
||||||
);
|
);
|
||||||
|
|
||||||
result.current(favoriteTargetObjectRecord, CoreObjectNameSingular.Person);
|
result.current.createFavorite(
|
||||||
|
favoriteTargetObjectRecord,
|
||||||
|
CoreObjectNameSingular.Person,
|
||||||
|
);
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
expect(mocks[0].result).toHaveBeenCalled();
|
expect(mocks[0].result).toHaveBeenCalled();
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ describe('useDeleteFavorite', () => {
|
|||||||
{ wrapper: Wrapper },
|
{ wrapper: Wrapper },
|
||||||
);
|
);
|
||||||
|
|
||||||
result.current(favoriteId);
|
result.current.deleteFavorite(favoriteId);
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
expect(mocks[1].result).toHaveBeenCalled();
|
expect(mocks[1].result).toHaveBeenCalled();
|
||||||
|
|||||||
@@ -38,6 +38,6 @@ describe('useFavorites', () => {
|
|||||||
{ wrapper: Wrapper },
|
{ wrapper: Wrapper },
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(result.current).toEqual(sortedFavorites);
|
expect(result.current.sortedFavorites).toEqual(sortedFavorites);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -54,7 +54,10 @@ describe('useReorderFavorite', () => {
|
|||||||
announce: () => {},
|
announce: () => {},
|
||||||
};
|
};
|
||||||
|
|
||||||
result.current(dragAndDropResult, responderProvided);
|
result.current.handleReorderFavorite(
|
||||||
|
dragAndDropResult,
|
||||||
|
responderProvided,
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
|
|||||||
@@ -34,5 +34,5 @@ export const useCreateFavorite = () => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
return createFavorite;
|
return { createFavorite };
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -28,5 +28,5 @@ export const useCreateFavoriteFolder = () => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
return createNewFavoriteFolder;
|
return { createNewFavoriteFolder };
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -10,5 +10,5 @@ export const useDeleteFavorite = () => {
|
|||||||
deleteOneRecord(favoriteId);
|
deleteOneRecord(favoriteId);
|
||||||
};
|
};
|
||||||
|
|
||||||
return deleteFavorite;
|
return { deleteFavorite };
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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 { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
||||||
|
import { useReadFindManyRecordsQueryInCache } from '@/object-record/cache/hooks/useReadFindManyRecordsQueryInCache';
|
||||||
import { useDeleteOneRecord } from '@/object-record/hooks/useDeleteOneRecord';
|
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 = () => {
|
export const useDeleteFavoriteFolder = () => {
|
||||||
const { deleteOneRecord } = useDeleteOneRecord({
|
const { deleteOneRecord } = useDeleteOneRecord({
|
||||||
objectNameSingular: CoreObjectNameSingular.FavoriteFolder,
|
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> => {
|
const deleteFavoriteFolder = async (folderId: string): Promise<void> => {
|
||||||
await deleteOneRecord(folderId);
|
await deleteOneRecord(folderId);
|
||||||
|
|
||||||
const updatedFavorites = [
|
const allFavorites = readFindManyRecordsQueryInCache<Favorite>({
|
||||||
...favorites.filter((favorite) => favorite.favoriteFolderId !== folderId),
|
queryVariables: {},
|
||||||
...workspaceFavorites,
|
recordGqlFields: PREFETCH_CONFIG[
|
||||||
];
|
PrefetchKey.AllFavorites
|
||||||
|
].operationSignatureFactory({ objectMetadataItem }).fields,
|
||||||
|
});
|
||||||
|
|
||||||
upsertFavorites(updatedFavorites);
|
const updatedFavorites = allFavorites.filter(
|
||||||
|
(favorite) => favorite.favoriteFolderId !== folderId,
|
||||||
|
);
|
||||||
|
|
||||||
|
upsertRecordsInCache(updatedFavorites);
|
||||||
};
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -52,5 +52,5 @@ export const useFavorites = () => {
|
|||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
return sortedFavorites;
|
return { sortedFavorites };
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,62 +1,30 @@
|
|||||||
import { sortFavorites } from '@/favorites/utils/sortFavorites';
|
import { sortFavorites } from '@/favorites/utils/sortFavorites';
|
||||||
import { useGetObjectRecordIdentifierByNameSingular } from '@/object-metadata/hooks/useGetObjectRecordIdentifierByNameSingular';
|
import { useFavoritesMetadata } from './useFavoritesMetadata';
|
||||||
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 { usePrefetchedFavoritesData } from './usePrefetchedFavoritesData';
|
import { usePrefetchedFavoritesData } from './usePrefetchedFavoritesData';
|
||||||
import { usePrefetchedFavoritesFoldersData } from './usePrefetchedFavoritesFoldersData';
|
import { usePrefetchedFavoritesFoldersData } from './usePrefetchedFavoritesFoldersData';
|
||||||
|
|
||||||
export const useFavoritesByFolder = () => {
|
export const useFavoritesByFolder = () => {
|
||||||
const { favorites } = usePrefetchedFavoritesData();
|
const { favorites } = usePrefetchedFavoritesData();
|
||||||
const { favoriteFolders } = usePrefetchedFavoritesFoldersData();
|
const { favoriteFolders } = usePrefetchedFavoritesFoldersData();
|
||||||
const { records: views } = usePrefetchedData<View>(PrefetchKey.AllViews);
|
const {
|
||||||
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,
|
|
||||||
views,
|
views,
|
||||||
objectMetadataItems,
|
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 };
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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,
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -12,7 +12,7 @@ export const useReorderFavorite = () => {
|
|||||||
objectNameSingular: CoreObjectNameSingular.Favorite,
|
objectNameSingular: CoreObjectNameSingular.Favorite,
|
||||||
});
|
});
|
||||||
|
|
||||||
const reorderFavorite: OnDragEndResponder = (result) => {
|
const handleReorderFavorite: OnDragEndResponder = (result) => {
|
||||||
if (!result.destination) return;
|
if (!result.destination) return;
|
||||||
|
|
||||||
const draggedFavoriteId = result.draggableId;
|
const draggedFavoriteId = result.draggableId;
|
||||||
@@ -37,5 +37,5 @@ export const useReorderFavorite = () => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
return reorderFavorite;
|
return { handleReorderFavorite };
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,50 +1,28 @@
|
|||||||
import { sortFavorites } from '@/favorites/utils/sortFavorites';
|
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 { useMemo } from 'react';
|
||||||
import { useRecoilValue } from 'recoil';
|
import { useFavoritesMetadata } from './useFavoritesMetadata';
|
||||||
import { FieldMetadataType } from '~/generated-metadata/graphql';
|
|
||||||
import { usePrefetchedFavoritesData } from './usePrefetchedFavoritesData';
|
import { usePrefetchedFavoritesData } from './usePrefetchedFavoritesData';
|
||||||
|
|
||||||
export const useSortedFavorites = () => {
|
export const useSortedFavorites = () => {
|
||||||
const { favorites, workspaceFavorites } = usePrefetchedFavoritesData();
|
const { favorites, workspaceFavorites } = usePrefetchedFavoritesData();
|
||||||
const { records: views } = usePrefetchedData<View>(PrefetchKey.AllViews);
|
const {
|
||||||
const objectMetadataItems = useRecoilValue(objectMetadataItemsState);
|
views,
|
||||||
const { objectMetadataItem: favoriteObjectMetadataItem } =
|
objectMetadataItems,
|
||||||
useObjectMetadataItem({
|
getObjectRecordIdentifierByNameSingular,
|
||||||
objectNameSingular: CoreObjectNameSingular.Favorite,
|
favoriteRelationFields,
|
||||||
});
|
} = useFavoritesMetadata();
|
||||||
|
|
||||||
const getObjectRecordIdentifierByNameSingular =
|
|
||||||
useGetObjectRecordIdentifierByNameSingular();
|
|
||||||
|
|
||||||
const favoriteRelationFieldMetadataItems = useMemo(
|
|
||||||
() =>
|
|
||||||
favoriteObjectMetadataItem.fields.filter(
|
|
||||||
(fieldMetadataItem) =>
|
|
||||||
fieldMetadataItem.type === FieldMetadataType.Relation &&
|
|
||||||
fieldMetadataItem.name !== 'workspaceMember' &&
|
|
||||||
fieldMetadataItem.name !== 'favoriteFolder',
|
|
||||||
),
|
|
||||||
[favoriteObjectMetadataItem.fields],
|
|
||||||
);
|
|
||||||
|
|
||||||
const favoritesSorted = useMemo(() => {
|
const favoritesSorted = useMemo(() => {
|
||||||
return sortFavorites(
|
return sortFavorites(
|
||||||
favorites,
|
favorites,
|
||||||
favoriteRelationFieldMetadataItems,
|
favoriteRelationFields,
|
||||||
getObjectRecordIdentifierByNameSingular,
|
getObjectRecordIdentifierByNameSingular,
|
||||||
true,
|
true,
|
||||||
views,
|
views,
|
||||||
objectMetadataItems,
|
objectMetadataItems,
|
||||||
);
|
);
|
||||||
}, [
|
}, [
|
||||||
favoriteRelationFieldMetadataItems,
|
favoriteRelationFields,
|
||||||
favorites,
|
favorites,
|
||||||
getObjectRecordIdentifierByNameSingular,
|
getObjectRecordIdentifierByNameSingular,
|
||||||
views,
|
views,
|
||||||
@@ -54,14 +32,14 @@ export const useSortedFavorites = () => {
|
|||||||
const workspaceFavoritesSorted = useMemo(() => {
|
const workspaceFavoritesSorted = useMemo(() => {
|
||||||
return sortFavorites(
|
return sortFavorites(
|
||||||
workspaceFavorites.filter((favorite) => favorite.viewId),
|
workspaceFavorites.filter((favorite) => favorite.viewId),
|
||||||
favoriteRelationFieldMetadataItems,
|
favoriteRelationFields,
|
||||||
getObjectRecordIdentifierByNameSingular,
|
getObjectRecordIdentifierByNameSingular,
|
||||||
false,
|
false,
|
||||||
views,
|
views,
|
||||||
objectMetadataItems,
|
objectMetadataItems,
|
||||||
);
|
);
|
||||||
}, [
|
}, [
|
||||||
favoriteRelationFieldMetadataItems,
|
favoriteRelationFields,
|
||||||
getObjectRecordIdentifierByNameSingular,
|
getObjectRecordIdentifierByNameSingular,
|
||||||
workspaceFavorites,
|
workspaceFavorites,
|
||||||
views,
|
views,
|
||||||
|
|||||||
@@ -52,5 +52,5 @@ export const useWorkspaceFavorites = () => {
|
|||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
return sortedWorkspaceFavorites;
|
return { sortedWorkspaceFavorites };
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -6,23 +6,20 @@ import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu';
|
|||||||
import { CurrentWorkspaceMemberFavoritesFolders } from '@/favorites/components/CurrentWorkspaceMemberFavoritesFolders';
|
import { CurrentWorkspaceMemberFavoritesFolders } from '@/favorites/components/CurrentWorkspaceMemberFavoritesFolders';
|
||||||
import { WorkspaceFavorites } from '@/favorites/components/WorkspaceFavorites';
|
import { WorkspaceFavorites } from '@/favorites/components/WorkspaceFavorites';
|
||||||
import { NavigationDrawerOpenedSection } from '@/object-metadata/components/NavigationDrawerOpenedSection';
|
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 { NavigationDrawerItem } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerItem';
|
||||||
import { NavigationDrawerSection } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerSection';
|
import { NavigationDrawerSection } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerSection';
|
||||||
import { isNavigationDrawerExpandedState } from '@/ui/navigation/states/isNavigationDrawerExpanded';
|
import { isNavigationDrawerExpandedState } from '@/ui/navigation/states/isNavigationDrawerExpanded';
|
||||||
import { navigationDrawerExpandedMemorizedState } from '@/ui/navigation/states/navigationDrawerExpandedMemorizedState';
|
import { navigationDrawerExpandedMemorizedState } from '@/ui/navigation/states/navigationDrawerExpandedMemorizedState';
|
||||||
import { navigationMemorizedUrlState } from '@/ui/navigation/states/navigationMemorizedUrlState';
|
import { navigationMemorizedUrlState } from '@/ui/navigation/states/navigationMemorizedUrlState';
|
||||||
import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile';
|
import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile';
|
||||||
|
import { ScrollWrapper } from '@/ui/utilities/scroll/components/ScrollWrapper';
|
||||||
import styled from '@emotion/styled';
|
import styled from '@emotion/styled';
|
||||||
|
|
||||||
const StyledMainSection = styled(NavigationDrawerSection)`
|
const StyledMainSection = styled(NavigationDrawerSection)`
|
||||||
min-height: fit-content;
|
min-height: fit-content;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const StyledContainer = styled.div`
|
|
||||||
overflow-x: hidden;
|
|
||||||
overflow-y: auto;
|
|
||||||
`;
|
|
||||||
export const MainNavigationDrawerItems = () => {
|
export const MainNavigationDrawerItems = () => {
|
||||||
const isMobile = useIsMobile();
|
const isMobile = useIsMobile();
|
||||||
const { toggleCommandMenu } = useCommandMenu();
|
const { toggleCommandMenu } = useCommandMenu();
|
||||||
@@ -59,15 +56,16 @@ export const MainNavigationDrawerItems = () => {
|
|||||||
/>
|
/>
|
||||||
</StyledMainSection>
|
</StyledMainSection>
|
||||||
)}
|
)}
|
||||||
<StyledContainer>
|
<ScrollWrapper
|
||||||
|
contextProviderName="navigationDrawer"
|
||||||
|
enableXScroll={false}
|
||||||
|
scrollHide={true}
|
||||||
|
>
|
||||||
<NavigationDrawerOpenedSection />
|
<NavigationDrawerOpenedSection />
|
||||||
|
|
||||||
<CurrentWorkspaceMemberFavoritesFolders />
|
<CurrentWorkspaceMemberFavoritesFolders />
|
||||||
|
|
||||||
<WorkspaceFavorites />
|
<WorkspaceFavorites />
|
||||||
|
<RemoteNavigationDrawerSection />
|
||||||
<NavigationDrawerSectionForObjectMetadataItemsWrapper isRemote={true} />
|
</ScrollWrapper>
|
||||||
</StyledContainer>
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -7,7 +7,8 @@ import { View } from '@/views/types/View';
|
|||||||
export const useFilteredObjectMetadataItemsForWorkspaceFavorites = () => {
|
export const useFilteredObjectMetadataItemsForWorkspaceFavorites = () => {
|
||||||
const { records: views } = usePrefetchedData<View>(PrefetchKey.AllViews);
|
const { records: views } = usePrefetchedData<View>(PrefetchKey.AllViews);
|
||||||
|
|
||||||
const workspaceFavorites = useWorkspaceFavorites();
|
const { sortedWorkspaceFavorites: workspaceFavorites } =
|
||||||
|
useWorkspaceFavorites();
|
||||||
|
|
||||||
const workspaceFavoriteIds = new Set(
|
const workspaceFavoriteIds = new Set(
|
||||||
workspaceFavorites.map((favorite) => favorite.recordId),
|
workspaceFavorites.map((favorite) => favorite.recordId),
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import { NavigationDrawerAnimatedCollapseWrapper } from '@/ui/navigation/navigat
|
|||||||
import { NavigationDrawerSection } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerSection';
|
import { NavigationDrawerSection } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerSection';
|
||||||
import { NavigationDrawerSectionTitle } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerSectionTitle';
|
import { NavigationDrawerSectionTitle } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerSectionTitle';
|
||||||
import { useNavigationSection } from '@/ui/navigation/navigation-drawer/hooks/useNavigationSection';
|
import { useNavigationSection } from '@/ui/navigation/navigation-drawer/hooks/useNavigationSection';
|
||||||
import { ScrollWrapper } from '@/ui/utilities/scroll/components/ScrollWrapper';
|
|
||||||
import styled from '@emotion/styled';
|
import styled from '@emotion/styled';
|
||||||
import { useRecoilValue } from 'recoil';
|
import { useRecoilValue } from 'recoil';
|
||||||
|
|
||||||
@@ -78,19 +77,15 @@ export const NavigationDrawerSectionForObjectMetadataItems = ({
|
|||||||
onClick={() => toggleNavigationSection()}
|
onClick={() => toggleNavigationSection()}
|
||||||
/>
|
/>
|
||||||
</NavigationDrawerAnimatedCollapseWrapper>
|
</NavigationDrawerAnimatedCollapseWrapper>
|
||||||
<ScrollWrapper contextProviderName="navigationDrawer">
|
<StyledObjectsMetaDataItemsWrapper>
|
||||||
<StyledObjectsMetaDataItemsWrapper>
|
{isNavigationSectionOpen &&
|
||||||
{isNavigationSectionOpen &&
|
objectMetadataItemsForNavigationItems.map((objectMetadataItem) => (
|
||||||
objectMetadataItemsForNavigationItems.map(
|
<NavigationDrawerItemForObjectMetadataItem
|
||||||
(objectMetadataItem) => (
|
key={`navigation-drawer-item-${objectMetadataItem.id}`}
|
||||||
<NavigationDrawerItemForObjectMetadataItem
|
objectMetadataItem={objectMetadataItem}
|
||||||
key={`navigation-drawer-item-${objectMetadataItem.id}`}
|
/>
|
||||||
objectMetadataItem={objectMetadataItem}
|
))}
|
||||||
/>
|
</StyledObjectsMetaDataItemsWrapper>
|
||||||
),
|
|
||||||
)}
|
|
||||||
</StyledObjectsMetaDataItemsWrapper>
|
|
||||||
</ScrollWrapper>
|
|
||||||
</NavigationDrawerSection>
|
</NavigationDrawerSection>
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -7,16 +7,12 @@ import { NavigationDrawerSectionForObjectMetadataItemsSkeletonLoader } from '@/o
|
|||||||
import { useFilteredObjectMetadataItems } from '@/object-metadata/hooks/useFilteredObjectMetadataItems';
|
import { useFilteredObjectMetadataItems } from '@/object-metadata/hooks/useFilteredObjectMetadataItems';
|
||||||
import { useIsPrefetchLoading } from '@/prefetch/hooks/useIsPrefetchLoading';
|
import { useIsPrefetchLoading } from '@/prefetch/hooks/useIsPrefetchLoading';
|
||||||
|
|
||||||
export const NavigationDrawerSectionForObjectMetadataItemsWrapper = ({
|
export const RemoteNavigationDrawerSection = () => {
|
||||||
isRemote,
|
|
||||||
}: {
|
|
||||||
isRemote: boolean;
|
|
||||||
}) => {
|
|
||||||
const currentUser = useRecoilValue(currentUserState);
|
const currentUser = useRecoilValue(currentUserState);
|
||||||
|
|
||||||
const { activeObjectMetadataItems } = useFilteredObjectMetadataItems();
|
const { activeObjectMetadataItems } = useFilteredObjectMetadataItems();
|
||||||
const filteredActiveObjectMetadataItems = activeObjectMetadataItems.filter(
|
const filteredActiveObjectMetadataItems = activeObjectMetadataItems.filter(
|
||||||
(item) => (isRemote ? item.isRemote : !item.isRemote),
|
(item) => item.isRemote,
|
||||||
);
|
);
|
||||||
const loading = useIsPrefetchLoading();
|
const loading = useIsPrefetchLoading();
|
||||||
|
|
||||||
@@ -26,9 +22,9 @@ export const NavigationDrawerSectionForObjectMetadataItemsWrapper = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<NavigationDrawerSectionForObjectMetadataItems
|
<NavigationDrawerSectionForObjectMetadataItems
|
||||||
sectionTitle={isRemote ? 'Remote' : 'Workspace'}
|
sectionTitle={'Remote'}
|
||||||
objectMetadataItems={filteredActiveObjectMetadataItems}
|
objectMetadataItems={filteredActiveObjectMetadataItems}
|
||||||
isRemote={isRemote}
|
isRemote={true}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -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');
|
|
||||||
},
|
|
||||||
};
|
|
||||||
@@ -109,6 +109,7 @@ export type PhonesFilter = {
|
|||||||
export type SelectFilter = {
|
export type SelectFilter = {
|
||||||
is?: IsFilter;
|
is?: IsFilter;
|
||||||
in?: string[];
|
in?: string[];
|
||||||
|
eq?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type MultiSelectFilter = {
|
export type MultiSelectFilter = {
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import { useRecoilValue } from 'recoil';
|
|||||||
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
|
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
|
||||||
import { mapObjectMetadataToGraphQLQuery } from '@/object-metadata/utils/mapObjectMetadataToGraphQLQuery';
|
import { mapObjectMetadataToGraphQLQuery } from '@/object-metadata/utils/mapObjectMetadataToGraphQLQuery';
|
||||||
import { RecordGqlOperationSignature } from '@/object-record/graphql/types/RecordGqlOperationSignature';
|
import { RecordGqlOperationSignature } from '@/object-record/graphql/types/RecordGqlOperationSignature';
|
||||||
import { generateDepthOneRecordGqlFields } from '@/object-record/graphql/utils/generateDepthOneRecordGqlFields';
|
|
||||||
import { getSearchRecordsQueryResponseField } from '@/object-record/utils/getSearchRecordsQueryResponseField';
|
import { getSearchRecordsQueryResponseField } from '@/object-record/utils/getSearchRecordsQueryResponseField';
|
||||||
import { isObjectMetadataItemSearchable } from '@/object-record/utils/isObjectMetadataItemSearchable';
|
import { isObjectMetadataItemSearchable } from '@/object-record/utils/isObjectMetadataItemSearchable';
|
||||||
import { isNonEmptyArray } from '~/utils/isNonEmptyArray';
|
import { isNonEmptyArray } from '~/utils/isNonEmptyArray';
|
||||||
@@ -68,7 +67,7 @@ export const useGenerateCombinedSearchRecordsQuery = ({
|
|||||||
) {
|
) {
|
||||||
${filteredQueryKeyWithObjectMetadataItemArray
|
${filteredQueryKeyWithObjectMetadataItemArray
|
||||||
.map(
|
.map(
|
||||||
({ objectMetadataItem, fields }) =>
|
({ objectMetadataItem }) =>
|
||||||
`${getSearchRecordsQueryResponseField(objectMetadataItem.namePlural)}(filter: $filter${capitalize(
|
`${getSearchRecordsQueryResponseField(objectMetadataItem.namePlural)}(filter: $filter${capitalize(
|
||||||
objectMetadataItem.nameSingular,
|
objectMetadataItem.nameSingular,
|
||||||
)},
|
)},
|
||||||
@@ -79,11 +78,6 @@ export const useGenerateCombinedSearchRecordsQuery = ({
|
|||||||
node ${mapObjectMetadataToGraphQLQuery({
|
node ${mapObjectMetadataToGraphQLQuery({
|
||||||
objectMetadataItems: objectMetadataItems,
|
objectMetadataItems: objectMetadataItems,
|
||||||
objectMetadataItem,
|
objectMetadataItem,
|
||||||
recordGqlFields:
|
|
||||||
fields ??
|
|
||||||
generateDepthOneRecordGqlFields({
|
|
||||||
objectMetadataItem,
|
|
||||||
}),
|
|
||||||
})}
|
})}
|
||||||
cursor
|
cursor
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -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 { StyledHeaderDropdownButton } from '@/ui/layout/dropdown/components/StyledHeaderDropdownButton';
|
||||||
import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown';
|
import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown';
|
||||||
|
|
||||||
export const RecordIndexOptionsDropdownButton = () => {
|
export const ObjectOptionsDropdownButton = () => {
|
||||||
const { isDropdownOpen, toggleDropdown } = useDropdown(
|
const { isDropdownOpen, toggleDropdown } = useDropdown(
|
||||||
RECORD_INDEX_OPTIONS_DROPDOWN_ID,
|
OBJECT_OPTIONS_DROPDOWN_ID,
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -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 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 };
|
||||||
|
};
|
||||||
@@ -16,17 +16,17 @@ import { mapArrayToObject } from '~/utils/array/mapArrayToObject';
|
|||||||
import { moveArrayItem } from '~/utils/array/moveArrayItem';
|
import { moveArrayItem } from '~/utils/array/moveArrayItem';
|
||||||
import { isDeeplyEqual } from '~/utils/isDeeplyEqual';
|
import { isDeeplyEqual } from '~/utils/isDeeplyEqual';
|
||||||
|
|
||||||
type useRecordIndexOptionsForBoardParams = {
|
type useObjectOptionsForBoardParams = {
|
||||||
objectNameSingular: string;
|
objectNameSingular: string;
|
||||||
recordBoardId: string;
|
recordBoardId: string;
|
||||||
viewBarId: string;
|
viewBarId: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useRecordIndexOptionsForBoard = ({
|
export const useObjectOptionsForBoard = ({
|
||||||
objectNameSingular,
|
objectNameSingular,
|
||||||
recordBoardId,
|
recordBoardId,
|
||||||
viewBarId,
|
viewBarId,
|
||||||
}: useRecordIndexOptionsForBoardParams) => {
|
}: useObjectOptionsForBoardParams) => {
|
||||||
const [recordIndexFieldDefinitions, setRecordIndexFieldDefinitions] =
|
const [recordIndexFieldDefinitions, setRecordIndexFieldDefinitions] =
|
||||||
useRecoilState(recordIndexFieldDefinitionsState);
|
useRecoilState(recordIndexFieldDefinitionsState);
|
||||||
|
|
||||||
@@ -7,7 +7,7 @@ import { visibleTableColumnsComponentSelector } from '@/object-record/record-tab
|
|||||||
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
|
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
|
||||||
import { moveArrayItem } from '~/utils/array/moveArrayItem';
|
import { moveArrayItem } from '~/utils/array/moveArrayItem';
|
||||||
|
|
||||||
export const useRecordIndexOptionsForTable = (recordTableId: string) => {
|
export const useObjectOptionsForTable = (recordTableId: string) => {
|
||||||
const hiddenTableColumns = useRecoilComponentValueV2(
|
const hiddenTableColumns = useRecoilComponentValueV2(
|
||||||
hiddenTableColumnsComponentSelector,
|
hiddenTableColumnsComponentSelector,
|
||||||
recordTableId,
|
recordTableId,
|
||||||
@@ -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';
|
||||||
@@ -2,12 +2,20 @@ import { useRecoilCallback } from 'recoil';
|
|||||||
|
|
||||||
import { useRecordBoardStates } from '@/object-record/record-board/hooks/internal/useRecordBoardStates';
|
import { useRecordBoardStates } from '@/object-record/record-board/hooks/internal/useRecordBoardStates';
|
||||||
import { RecordGroupDefinition } from '@/object-record/record-group/types/RecordGroupDefinition';
|
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';
|
import { isDeeplyEqual } from '~/utils/isDeeplyEqual';
|
||||||
|
|
||||||
export const useSetRecordBoardColumns = (recordBoardId?: string) => {
|
export const useSetRecordBoardColumns = (recordBoardId?: string) => {
|
||||||
const { scopeId, columnIdsState, columnsFamilySelector } =
|
const { scopeId, columnIdsState, columnsFamilySelector } =
|
||||||
useRecordBoardStates(recordBoardId);
|
useRecordBoardStates(recordBoardId);
|
||||||
|
|
||||||
|
const recordGroupSort = useRecoilComponentValueV2(
|
||||||
|
recordIndexRecordGroupSortComponentState,
|
||||||
|
recordBoardId,
|
||||||
|
);
|
||||||
|
|
||||||
const setColumns = useRecoilCallback(
|
const setColumns = useRecoilCallback(
|
||||||
({ set, snapshot }) =>
|
({ set, snapshot }) =>
|
||||||
(columns: RecordGroupDefinition[]) => {
|
(columns: RecordGroupDefinition[]) => {
|
||||||
@@ -15,7 +23,12 @@ export const useSetRecordBoardColumns = (recordBoardId?: string) => {
|
|||||||
.getLoadable(columnIdsState)
|
.getLoadable(columnIdsState)
|
||||||
.getValue();
|
.getValue();
|
||||||
|
|
||||||
const columnIds = columns
|
const sortedColumns = sortRecordGroupDefinitions(
|
||||||
|
columns,
|
||||||
|
recordGroupSort,
|
||||||
|
);
|
||||||
|
|
||||||
|
const columnIds = sortedColumns
|
||||||
.filter(({ isVisible }) => isVisible)
|
.filter(({ isVisible }) => isVisible)
|
||||||
.map(({ id }) => id);
|
.map(({ id }) => id);
|
||||||
|
|
||||||
@@ -35,7 +48,7 @@ export const useSetRecordBoardColumns = (recordBoardId?: string) => {
|
|||||||
set(columnsFamilySelector(column.id), column);
|
set(columnsFamilySelector(column.id), column);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
[columnsFamilySelector, columnIdsState],
|
[columnIdsState, recordGroupSort, columnsFamilySelector],
|
||||||
);
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { useRecordGroupActions } from '@/object-record/record-group/hooks/useRec
|
|||||||
import { DropdownMenu } from '@/ui/layout/dropdown/components/DropdownMenu';
|
import { DropdownMenu } from '@/ui/layout/dropdown/components/DropdownMenu';
|
||||||
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
|
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
|
||||||
import { useListenClickOutside } from '@/ui/utilities/pointer-event/hooks/useListenClickOutside';
|
import { useListenClickOutside } from '@/ui/utilities/pointer-event/hooks/useListenClickOutside';
|
||||||
|
import { ViewType } from '@/views/types/ViewType';
|
||||||
import { MenuItem } from 'twenty-ui';
|
import { MenuItem } from 'twenty-ui';
|
||||||
|
|
||||||
const StyledMenuContainer = styled.div`
|
const StyledMenuContainer = styled.div`
|
||||||
@@ -25,7 +26,9 @@ export const RecordBoardColumnDropdownMenu = ({
|
|||||||
}: RecordBoardColumnDropdownMenuProps) => {
|
}: RecordBoardColumnDropdownMenuProps) => {
|
||||||
const boardColumnMenuRef = useRef<HTMLDivElement>(null);
|
const boardColumnMenuRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
const recordGroupActions = useRecordGroupActions();
|
const recordGroupActions = useRecordGroupActions({
|
||||||
|
viewType: ViewType.Kanban,
|
||||||
|
});
|
||||||
|
|
||||||
const closeMenu = useCallback(() => {
|
const closeMenu = useCallback(() => {
|
||||||
onClose();
|
onClose();
|
||||||
|
|||||||
@@ -19,9 +19,8 @@ import { toSpliced } from '~/utils/array/toSpliced';
|
|||||||
import { turnIntoEmptyStringIfWhitespacesOnly } from '~/utils/string/turnIntoEmptyStringIfWhitespacesOnly';
|
import { turnIntoEmptyStringIfWhitespacesOnly } from '~/utils/string/turnIntoEmptyStringIfWhitespacesOnly';
|
||||||
|
|
||||||
const StyledDropdownMenu = styled(DropdownMenu)`
|
const StyledDropdownMenu = styled(DropdownMenu)`
|
||||||
margin-left: -1px;
|
margin: -1px;
|
||||||
position: relative;
|
position: relative;
|
||||||
margin-top: -1px;
|
|
||||||
`;
|
`;
|
||||||
|
|
||||||
type MultiItemFieldInputProps<T> = {
|
type MultiItemFieldInputProps<T> = {
|
||||||
@@ -65,8 +64,12 @@ export const MultiItemFieldInput = <T,>({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleDropdownCloseOutside = (event: MouseEvent | TouchEvent) => {
|
const handleDropdownCloseOutside = (event: MouseEvent | TouchEvent) => {
|
||||||
onCancel?.();
|
|
||||||
event.stopImmediatePropagation();
|
event.stopImmediatePropagation();
|
||||||
|
if (inputValue?.trim().length > 0) {
|
||||||
|
handleSubmitInput();
|
||||||
|
} else {
|
||||||
|
onCancel?.();
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
useListenClickOutside({
|
useListenClickOutside({
|
||||||
@@ -202,10 +205,12 @@ export const MultiItemFieldInput = <T,>({
|
|||||||
}
|
}
|
||||||
onEnter={handleSubmitInput}
|
onEnter={handleSubmitInput}
|
||||||
rightComponent={
|
rightComponent={
|
||||||
<LightIconButton
|
items.length ? (
|
||||||
Icon={isAddingNewItem ? IconPlus : IconCheck}
|
<LightIconButton
|
||||||
onClick={handleSubmitInput}
|
Icon={isAddingNewItem ? IconPlus : IconCheck}
|
||||||
/>
|
onClick={handleSubmitInput}
|
||||||
|
/>
|
||||||
|
) : null
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
@@ -18,6 +18,9 @@ export const isMatchingSelectFilter = ({
|
|||||||
return value !== null;
|
return value !== null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
case selectFilter.eq !== undefined: {
|
||||||
|
return value === selectFilter.eq;
|
||||||
|
}
|
||||||
default: {
|
default: {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`Unexpected value for select filter : ${JSON.stringify(selectFilter)}`,
|
`Unexpected value for select filter : ${JSON.stringify(selectFilter)}`,
|
||||||
|
|||||||
@@ -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}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -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>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -8,12 +8,19 @@ import { RecordGroupAction } from '@/object-record/record-group/types/RecordGrou
|
|||||||
import { RecordGroupDefinitionType } from '@/object-record/record-group/types/RecordGroupDefinition';
|
import { RecordGroupDefinitionType } from '@/object-record/record-group/types/RecordGroupDefinition';
|
||||||
import { RecordIndexRootPropsContext } from '@/object-record/record-index/contexts/RecordIndexRootPropsContext';
|
import { RecordIndexRootPropsContext } from '@/object-record/record-index/contexts/RecordIndexRootPropsContext';
|
||||||
import { navigationMemorizedUrlState } from '@/ui/navigation/states/navigationMemorizedUrlState';
|
import { navigationMemorizedUrlState } from '@/ui/navigation/states/navigationMemorizedUrlState';
|
||||||
|
import { ViewType } from '@/views/types/ViewType';
|
||||||
import { useCallback, useContext, useMemo } from 'react';
|
import { useCallback, useContext, useMemo } from 'react';
|
||||||
import { useLocation, useNavigate } from 'react-router-dom';
|
import { useLocation, useNavigate } from 'react-router-dom';
|
||||||
import { useSetRecoilState } from 'recoil';
|
import { useSetRecoilState } from 'recoil';
|
||||||
import { IconEyeOff, IconSettings, isDefined } from 'twenty-ui';
|
import { IconEyeOff, IconSettings, isDefined } from 'twenty-ui';
|
||||||
|
|
||||||
export const useRecordGroupActions = () => {
|
type UseRecordGroupActionsParams = {
|
||||||
|
viewType: ViewType;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useRecordGroupActions = ({
|
||||||
|
viewType,
|
||||||
|
}: UseRecordGroupActionsParams) => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
|
|
||||||
@@ -36,6 +43,7 @@ export const useRecordGroupActions = () => {
|
|||||||
const { handleVisibilityChange: handleRecordGroupVisibilityChange } =
|
const { handleVisibilityChange: handleRecordGroupVisibilityChange } =
|
||||||
useRecordGroupVisibility({
|
useRecordGroupVisibility({
|
||||||
viewBarId: recordIndexId,
|
viewBarId: recordIndexId,
|
||||||
|
viewType,
|
||||||
});
|
});
|
||||||
|
|
||||||
const setNavigationMemorizedUrl = useSetRecoilState(
|
const setNavigationMemorizedUrl = useSetRecoilState(
|
||||||
|
|||||||
@@ -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 { recordGroupDefinitionsComponentState } from '@/object-record/record-group/states/recordGroupDefinitionsComponentState';
|
||||||
import { RecordGroupDefinition } from '@/object-record/record-group/types/RecordGroupDefinition';
|
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 { useSaveCurrentViewGroups } from '@/views/hooks/useSaveCurrentViewGroups';
|
||||||
|
import { ViewType } from '@/views/types/ViewType';
|
||||||
import { mapRecordGroupDefinitionsToViewGroups } from '@/views/utils/mapRecordGroupDefinitionsToViewGroups';
|
import { mapRecordGroupDefinitionsToViewGroups } from '@/views/utils/mapRecordGroupDefinitionsToViewGroups';
|
||||||
|
import { useRecoilCallback } from 'recoil';
|
||||||
|
|
||||||
type UseRecordGroupVisibilityParams = {
|
type UseRecordGroupVisibilityParams = {
|
||||||
viewBarId: string;
|
viewBarId: string;
|
||||||
|
viewType: ViewType;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useRecordGroupVisibility = ({
|
export const useRecordGroupVisibility = ({
|
||||||
viewBarId,
|
viewBarId,
|
||||||
|
viewType,
|
||||||
}: UseRecordGroupVisibilityParams) => {
|
}: UseRecordGroupVisibilityParams) => {
|
||||||
const [recordGroupDefinitions, setRecordGroupDefinitions] =
|
const recordGroupDefinitionsState = useRecoilComponentCallbackStateV2(
|
||||||
useRecoilComponentStateV2(recordGroupDefinitionsComponentState);
|
recordGroupDefinitionsComponentState,
|
||||||
|
);
|
||||||
|
|
||||||
|
const tableRowIdsByGroupFamilyState = useRecoilComponentCallbackStateV2(
|
||||||
|
tableRowIdsByGroupComponentFamilyState,
|
||||||
|
viewBarId,
|
||||||
|
);
|
||||||
|
|
||||||
|
const { recordIdsByColumnIdFamilyState } = useRecordBoardStates(viewBarId);
|
||||||
|
|
||||||
|
const objectOptionsDropdownRecordGroupHideState =
|
||||||
|
useRecoilComponentCallbackStateV2(recordIndexRecordGroupHideComponentState);
|
||||||
|
|
||||||
const { saveViewGroups } = useSaveCurrentViewGroups(viewBarId);
|
const { saveViewGroups } = useSaveCurrentViewGroups(viewBarId);
|
||||||
|
|
||||||
const handleVisibilityChange = useCallback(
|
const handleVisibilityChange = useRecoilCallback(
|
||||||
async (updatedRecordGroupDefinition: RecordGroupDefinition) => {
|
({ snapshot, set }) =>
|
||||||
const updatedRecordGroupDefinitions = recordGroupDefinitions.map(
|
async (updatedRecordGroupDefinition: RecordGroupDefinition) => {
|
||||||
(groupDefinition) =>
|
const recordGroupDefinitions = getSnapshotValue(
|
||||||
groupDefinition.id === updatedRecordGroupDefinition.id
|
snapshot,
|
||||||
? {
|
recordGroupDefinitionsState,
|
||||||
...groupDefinition,
|
);
|
||||||
isVisible: !groupDefinition.isVisible,
|
|
||||||
}
|
|
||||||
: groupDefinition,
|
|
||||||
);
|
|
||||||
|
|
||||||
setRecordGroupDefinitions(updatedRecordGroupDefinitions);
|
const updatedRecordGroupDefinitions = recordGroupDefinitions.map(
|
||||||
|
(groupDefinition) =>
|
||||||
|
groupDefinition.id === updatedRecordGroupDefinition.id
|
||||||
|
? {
|
||||||
|
...groupDefinition,
|
||||||
|
isVisible: !groupDefinition.isVisible,
|
||||||
|
}
|
||||||
|
: groupDefinition,
|
||||||
|
);
|
||||||
|
|
||||||
saveViewGroups(
|
set(recordGroupDefinitionsState, updatedRecordGroupDefinitions);
|
||||||
mapRecordGroupDefinitionsToViewGroups(updatedRecordGroupDefinitions),
|
|
||||||
);
|
saveViewGroups(
|
||||||
},
|
mapRecordGroupDefinitionsToViewGroups(updatedRecordGroupDefinitions),
|
||||||
[recordGroupDefinitions, setRecordGroupDefinitions, saveViewGroups],
|
);
|
||||||
|
|
||||||
|
// 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 {
|
return {
|
||||||
handleVisibilityChange,
|
handleVisibilityChange,
|
||||||
|
handleHideEmptyRecordGroupChange,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ import { useMemo } from 'react';
|
|||||||
|
|
||||||
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
|
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
|
||||||
import { recordGroupDefinitionsComponentState } from '@/object-record/record-group/states/recordGroupDefinitionsComponentState';
|
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';
|
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
|
||||||
|
|
||||||
type UseRecordGroupsParams = {
|
type UseRecordGroupsParams = {
|
||||||
@@ -15,6 +17,10 @@ export const useRecordGroups = ({
|
|||||||
recordGroupDefinitionsComponentState,
|
recordGroupDefinitionsComponentState,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const recordGroupSort = useRecoilComponentValueV2(
|
||||||
|
recordIndexRecordGroupSortComponentState,
|
||||||
|
);
|
||||||
|
|
||||||
const { objectMetadataItem } = useObjectMetadataItem({
|
const { objectMetadataItem } = useObjectMetadataItem({
|
||||||
objectNameSingular,
|
objectNameSingular,
|
||||||
});
|
});
|
||||||
@@ -35,14 +41,8 @@ export const useRecordGroups = ({
|
|||||||
}, [objectMetadataItem, recordGroupDefinitions]);
|
}, [objectMetadataItem, recordGroupDefinitions]);
|
||||||
|
|
||||||
const visibleRecordGroups = useMemo(
|
const visibleRecordGroups = useMemo(
|
||||||
() =>
|
() => sortRecordGroupDefinitions(recordGroupDefinitions, recordGroupSort),
|
||||||
recordGroupDefinitions
|
[recordGroupDefinitions, recordGroupSort],
|
||||||
.filter((boardGroup) => boardGroup.isVisible)
|
|
||||||
.sort(
|
|
||||||
(boardGroupA, boardGroupB) =>
|
|
||||||
boardGroupA.position - boardGroupB.position,
|
|
||||||
),
|
|
||||||
[recordGroupDefinitions],
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const hiddenRecordGroups = useMemo(
|
const hiddenRecordGroups = useMemo(
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
export enum RecordGroupSort {
|
||||||
|
Manual = 'Manual',
|
||||||
|
Alphabetical = 'Alphabetical',
|
||||||
|
ReverseAlphabetical = 'Reverse Alphabetical',
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -30,7 +30,7 @@ export const RecordIndexBoardDataLoaderEffect = ({
|
|||||||
recordIndexFieldDefinitionsState,
|
recordIndexFieldDefinitionsState,
|
||||||
);
|
);
|
||||||
|
|
||||||
const recordIndexGroupDefinitions = useRecoilComponentValueV2(
|
const recordGroupDefinitions = useRecoilComponentValueV2(
|
||||||
recordGroupDefinitionsComponentState,
|
recordGroupDefinitionsComponentState,
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -67,8 +67,8 @@ export const RecordIndexBoardDataLoaderEffect = ({
|
|||||||
}, [objectNameSingular, setObjectSingularName]);
|
}, [objectNameSingular, setObjectSingularName]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setColumns(recordIndexGroupDefinitions);
|
setColumns(recordGroupDefinitions);
|
||||||
}, [recordIndexGroupDefinitions, setColumns]);
|
}, [recordGroupDefinitions, setColumns]);
|
||||||
|
|
||||||
// TODO: Remove this duplicate useEffect by ensuring it's not here because
|
// 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
|
// We want it to be triggered by a change of objectMetadataItem, which would be an anti-pattern
|
||||||
|
|||||||
@@ -2,13 +2,13 @@ import styled from '@emotion/styled';
|
|||||||
import { useRecoilCallback, useRecoilState, useSetRecoilState } from 'recoil';
|
import { useRecoilCallback, useRecoilState, useSetRecoilState } from 'recoil';
|
||||||
|
|
||||||
import { useColumnDefinitionsFromFieldMetadata } from '@/object-metadata/hooks/useColumnDefinitionsFromFieldMetadata';
|
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 { RecordIndexBoardContainer } from '@/object-record/record-index/components/RecordIndexBoardContainer';
|
||||||
import { RecordIndexBoardDataLoader } from '@/object-record/record-index/components/RecordIndexBoardDataLoader';
|
import { RecordIndexBoardDataLoader } from '@/object-record/record-index/components/RecordIndexBoardDataLoader';
|
||||||
import { RecordIndexBoardDataLoaderEffect } from '@/object-record/record-index/components/RecordIndexBoardDataLoaderEffect';
|
import { RecordIndexBoardDataLoaderEffect } from '@/object-record/record-index/components/RecordIndexBoardDataLoaderEffect';
|
||||||
import { RecordIndexTableContainer } from '@/object-record/record-index/components/RecordIndexTableContainer';
|
import { RecordIndexTableContainer } from '@/object-record/record-index/components/RecordIndexTableContainer';
|
||||||
import { RecordIndexTableContainerEffect } from '@/object-record/record-index/components/RecordIndexTableContainerEffect';
|
import { RecordIndexTableContainerEffect } from '@/object-record/record-index/components/RecordIndexTableContainerEffect';
|
||||||
import { RecordIndexViewBarEffect } from '@/object-record/record-index/components/RecordIndexViewBarEffect';
|
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 { recordIndexFieldDefinitionsState } from '@/object-record/record-index/states/recordIndexFieldDefinitionsState';
|
||||||
import { recordIndexFiltersState } from '@/object-record/record-index/states/recordIndexFiltersState';
|
import { recordIndexFiltersState } from '@/object-record/record-index/states/recordIndexFiltersState';
|
||||||
import { recordIndexIsCompactModeActiveState } from '@/object-record/record-index/states/recordIndexIsCompactModeActiveState';
|
import { recordIndexIsCompactModeActiveState } from '@/object-record/record-index/states/recordIndexIsCompactModeActiveState';
|
||||||
@@ -162,7 +162,7 @@ export const RecordIndexContainer = () => {
|
|||||||
<ViewBar
|
<ViewBar
|
||||||
viewBarId={recordIndexId}
|
viewBarId={recordIndexId}
|
||||||
optionsDropdownButton={
|
optionsDropdownButton={
|
||||||
<RecordIndexOptionsDropdown
|
<ObjectOptionsDropdown
|
||||||
recordIndexId={recordIndexId}
|
recordIndexId={recordIndexId}
|
||||||
objectMetadataItem={objectMetadataItem}
|
objectMetadataItem={objectMetadataItem}
|
||||||
viewType={recordIndexViewType ?? ViewType.Table}
|
viewType={recordIndexViewType ?? ViewType.Table}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { PageFavoriteFoldersDropdown } from '@/favorites/components/PageFavoriteFolderDropdown';
|
||||||
import { FAVORITE_FOLDER_PICKER_DROPDOWN_ID } from '@/favorites/favorite-folder-picker/constants/FavoriteFolderPickerDropdownId';
|
import { FAVORITE_FOLDER_PICKER_DROPDOWN_ID } from '@/favorites/favorite-folder-picker/constants/FavoriteFolderPickerDropdownId';
|
||||||
import { useFavorites } from '@/favorites/hooks/useFavorites';
|
import { useFavorites } from '@/favorites/hooks/useFavorites';
|
||||||
import { useFilteredObjectMetadataItems } from '@/object-metadata/hooks/useFilteredObjectMetadataItems';
|
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 { usePrefetchedData } from '@/prefetch/hooks/usePrefetchedData';
|
||||||
import { PrefetchKey } from '@/prefetch/types/PrefetchKey';
|
import { PrefetchKey } from '@/prefetch/types/PrefetchKey';
|
||||||
import { PageAddButton } from '@/ui/layout/page/components/PageAddButton';
|
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 { PageHeader } from '@/ui/layout/page/components/PageHeader';
|
||||||
import { PageHotkeysEffect } from '@/ui/layout/page/components/PageHotkeysEffect';
|
import { PageHotkeysEffect } from '@/ui/layout/page/components/PageHotkeysEffect';
|
||||||
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
|
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 view = views.find((view) => view.id === currentViewId);
|
||||||
|
|
||||||
const favorites = useFavorites();
|
const { sortedFavorites: favorites } = useFavorites();
|
||||||
|
|
||||||
const isFavorite = favorites.some(
|
const isFavorite = favorites.some(
|
||||||
(favorite) =>
|
(favorite) =>
|
||||||
|
|||||||
@@ -7,9 +7,9 @@ import {
|
|||||||
} from '../useExportFetchRecords';
|
} from '../useExportFetchRecords';
|
||||||
|
|
||||||
import { PERSON_FRAGMENT_WITH_DEPTH_ZERO_RELATIONS } from '@/object-record/hooks/__mocks__/personFragments';
|
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 { useRecordBoard } from '@/object-record/record-board/hooks/useRecordBoard';
|
||||||
import { recordBoardKanbanFieldMetadataNameComponentState } from '@/object-record/record-board/states/recordBoardKanbanFieldMetadataNameComponentState';
|
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 { extractComponentState } from '@/ui/utilities/state/component-state/utils/extractComponentState';
|
||||||
import { ViewType } from '@/views/types/ViewType';
|
import { ViewType } from '@/views/types/ViewType';
|
||||||
import { MockedResponse } from '@apollo/client/testing';
|
import { MockedResponse } from '@apollo/client/testing';
|
||||||
@@ -248,7 +248,7 @@ describe('useRecordData', () => {
|
|||||||
}),
|
}),
|
||||||
useRecordBoardHook: useRecordBoard(recordIndexId),
|
useRecordBoardHook: useRecordBoard(recordIndexId),
|
||||||
kanbanFieldName: useRecoilValue(kanbanFieldNameState),
|
kanbanFieldName: useRecoilValue(kanbanFieldNameState),
|
||||||
kanbanData: useRecordIndexOptionsForBoard({
|
kanbanData: useObjectOptionsForBoard({
|
||||||
objectNameSingular: objectMetadataItem.nameSingular,
|
objectNameSingular: objectMetadataItem.nameSingular,
|
||||||
recordBoardId: recordIndexId,
|
recordBoardId: recordIndexId,
|
||||||
viewBarId: recordIndexId,
|
viewBarId: recordIndexId,
|
||||||
@@ -338,7 +338,7 @@ describe('useRecordData', () => {
|
|||||||
}),
|
}),
|
||||||
setKanbanFieldName: useRecordBoard(recordIndexId),
|
setKanbanFieldName: useRecordBoard(recordIndexId),
|
||||||
kanbanFieldName: useRecoilValue(kanbanFieldNameState),
|
kanbanFieldName: useRecoilValue(kanbanFieldNameState),
|
||||||
kanbanData: useRecordIndexOptionsForBoard({
|
kanbanData: useObjectOptionsForBoard({
|
||||||
objectNameSingular: objectMetadataItem.nameSingular,
|
objectNameSingular: objectMetadataItem.nameSingular,
|
||||||
recordBoardId: recordIndexId,
|
recordBoardId: recordIndexId,
|
||||||
viewBarId: recordIndexId,
|
viewBarId: recordIndexId,
|
||||||
|
|||||||
@@ -86,7 +86,7 @@ describe('csvDownloader', () => {
|
|||||||
|
|
||||||
describe('displayedExportProgress', () => {
|
describe('displayedExportProgress', () => {
|
||||||
it.each([
|
it.each([
|
||||||
[undefined, undefined, 'percentage', 'Export View as CSV'],
|
[undefined, undefined, 'percentage', 'Export'],
|
||||||
[20, 50, 'percentage', 'Export (40%)'],
|
[20, 50, 'percentage', 'Export (40%)'],
|
||||||
[0, 100, 'number', 'Export (0)'],
|
[0, 100, 'number', 'Export (0)'],
|
||||||
[10, 10, 'percentage', 'Export (100%)'],
|
[10, 10, 'percentage', 'Export (100%)'],
|
||||||
@@ -96,7 +96,7 @@ describe('displayedExportProgress', () => {
|
|||||||
'displays the export progress',
|
'displays the export progress',
|
||||||
(exportedRecordCount, totalRecordCount, displayType, expected) => {
|
(exportedRecordCount, totalRecordCount, displayType, expected) => {
|
||||||
expect(
|
expect(
|
||||||
displayedExportProgress('all', {
|
displayedExportProgress({
|
||||||
exportedRecordCount,
|
exportedRecordCount,
|
||||||
totalRecordCount,
|
totalRecordCount,
|
||||||
displayType: displayType as 'percentage' | 'number',
|
displayType: displayType as 'percentage' | 'number',
|
||||||
|
|||||||
@@ -11,10 +11,10 @@ import { contextStoreTargetedRecordsRuleComponentState } from '@/context-store/s
|
|||||||
import { computeContextStoreFilters } from '@/context-store/utils/computeContextStoreFilters';
|
import { computeContextStoreFilters } from '@/context-store/utils/computeContextStoreFilters';
|
||||||
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||||
import { useLazyFindManyRecords } from '@/object-record/hooks/useLazyFindManyRecords';
|
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 { useRecordBoardStates } from '@/object-record/record-board/hooks/internal/useRecordBoardStates';
|
||||||
import { useFindManyParams } from '@/object-record/record-index/hooks/useLoadRecordIndexTable';
|
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 { visibleTableColumnsComponentSelector } from '@/object-record/record-table/states/selectors/visibleTableColumnsComponentSelector';
|
||||||
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
|
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
|
||||||
import { ViewType } from '@/views/types/ViewType';
|
import { ViewType } from '@/views/types/ViewType';
|
||||||
@@ -62,7 +62,7 @@ export const useExportFetchRecords = ({
|
|||||||
});
|
});
|
||||||
const [previousRecordCount, setPreviousRecordCount] = useState(0);
|
const [previousRecordCount, setPreviousRecordCount] = useState(0);
|
||||||
|
|
||||||
const { hiddenBoardFields } = useRecordIndexOptionsForBoard({
|
const { hiddenBoardFields } = useObjectOptionsForBoard({
|
||||||
objectNameSingular: objectMetadataItem.nameSingular,
|
objectNameSingular: objectMetadataItem.nameSingular,
|
||||||
recordBoardId: recordIndexId,
|
recordBoardId: recordIndexId,
|
||||||
viewBarId: recordIndexId,
|
viewBarId: recordIndexId,
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
import { json2csv } from 'json-2-csv';
|
import { json2csv } from 'json-2-csv';
|
||||||
import { useMemo } from 'react';
|
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 { FieldMetadata } from '@/object-record/record-field/types/FieldMetadata';
|
||||||
import {
|
import {
|
||||||
UseRecordDataOptions,
|
UseRecordDataOptions,
|
||||||
useExportFetchRecords,
|
useExportFetchRecords,
|
||||||
} from '@/object-record/record-index/export/hooks/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 { ColumnDefinition } from '@/object-record/record-table/types/ColumnDefinition';
|
||||||
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
|
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
|
||||||
import { RelationDefinitionType } from '~/generated-metadata/graphql';
|
import { RelationDefinitionType } from '~/generated-metadata/graphql';
|
||||||
@@ -107,12 +107,9 @@ const percentage = (part: number, whole: number): number => {
|
|||||||
return Math.round((part / whole) * 100);
|
return Math.round((part / whole) * 100);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const displayedExportProgress = (
|
export const displayedExportProgress = (progress?: ExportProgress): string => {
|
||||||
mode: 'all' | 'selection' = 'all',
|
|
||||||
progress?: ExportProgress,
|
|
||||||
): string => {
|
|
||||||
if (isUndefinedOrNull(progress?.exportedRecordCount)) {
|
if (isUndefinedOrNull(progress?.exportedRecordCount)) {
|
||||||
return mode === 'all' ? 'Export View as CSV' : 'Export Selection as CSV';
|
return 'Export';
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
|
|||||||
@@ -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 };
|
||||||
|
};
|
||||||
@@ -50,13 +50,13 @@ export const useLoadRecordIndexBoard = ({
|
|||||||
recordIndexViewFilterGroupsState,
|
recordIndexViewFilterGroupsState,
|
||||||
);
|
);
|
||||||
|
|
||||||
const recordIndexGroupDefinitions = useRecoilComponentValueV2(
|
const recordGroupDefinitions = useRecoilComponentValueV2(
|
||||||
recordGroupDefinitionsComponentState,
|
recordGroupDefinitionsComponentState,
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setColumns(recordIndexGroupDefinitions);
|
setColumns(recordGroupDefinitions);
|
||||||
}, [recordIndexGroupDefinitions, setColumns]);
|
}, [recordGroupDefinitions, setColumns]);
|
||||||
|
|
||||||
const recordIndexFilters = useRecoilValue(recordIndexFiltersState);
|
const recordIndexFilters = useRecoilValue(recordIndexFiltersState);
|
||||||
const recordIndexSorts = useRecoilValue(recordIndexSortsState);
|
const recordIndexSorts = useRecoilValue(recordIndexSortsState);
|
||||||
|
|||||||
@@ -57,7 +57,9 @@ export const useFindManyParams = (
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (!fieldMetadataItem) {
|
if (!fieldMetadataItem) {
|
||||||
return {};
|
throw new Error(
|
||||||
|
`Field metadata item with id ${currentRecordGroupDefinition.fieldMetadataId} not found`,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -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}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -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>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -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
Reference in New Issue
Block a user