Option-menu-imporovements (#11309)

- icon switching : if switching layout from table to kanban
and if icon is the table icon (the default one) then automatically
update the icon to the kanban icon

- add tooltip on Layout default view to better explain why it's
unavaialble


Fixes [#689](https://github.com/twentyhq/core-team-issues/issues/689)
Fixes [#688](https://github.com/twentyhq/core-team-issues/issues/688)
Fixes [#686](https://github.com/twentyhq/core-team-issues/issues/686)
This commit is contained in:
Guillim
2025-04-01 12:31:40 +02:00
committed by GitHub
parent bc81853095
commit 9cbc2e3df0
8 changed files with 82 additions and 30 deletions

View File

@ -1,7 +1,6 @@
import { import {
IconBaselineDensitySmall, IconBaselineDensitySmall,
IconChevronLeft, IconChevronLeft,
IconLayoutKanban,
IconLayoutList, IconLayoutList,
IconLayoutNavbar, IconLayoutNavbar,
IconLayoutSidebarRight, IconLayoutSidebarRight,
@ -9,6 +8,7 @@ import {
MenuItem, MenuItem,
MenuItemSelect, MenuItemSelect,
MenuItemToggle, MenuItemToggle,
OverflowingTextWithTooltip,
} from 'twenty-ui'; } from 'twenty-ui';
import { useObjectOptionsForBoard } from '@/object-record/object-options-dropdown/hooks/useObjectOptionsForBoard'; import { useObjectOptionsForBoard } from '@/object-record/object-options-dropdown/hooks/useObjectOptionsForBoard';
@ -24,7 +24,7 @@ import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
import { useGetCurrentViewOnly } from '@/views/hooks/useGetCurrentViewOnly'; import { useGetCurrentViewOnly } from '@/views/hooks/useGetCurrentViewOnly';
import { ViewOpenRecordInType } from '@/views/types/ViewOpenRecordInType'; import { ViewOpenRecordInType } from '@/views/types/ViewOpenRecordInType';
import { ViewType } from '@/views/types/ViewType'; import { ViewType, viewTypeIconMapping } from '@/views/types/ViewType';
import { useGetAvailableFieldsForKanban } from '@/views/view-picker/hooks/useGetAvailableFieldsForKanban'; import { useGetAvailableFieldsForKanban } from '@/views/view-picker/hooks/useGetAvailableFieldsForKanban';
import { useLingui } from '@lingui/react/macro'; import { useLingui } from '@lingui/react/macro';
import { useRecoilValue } from 'recoil'; import { useRecoilValue } from 'recoil';
@ -75,6 +75,7 @@ export const ObjectOptionsDropdownLayoutContent = () => {
}; };
const isDefaultView = currentView?.key === 'INDEX'; const isDefaultView = currentView?.key === 'INDEX';
const nbsp = '\u00A0';
return ( return (
<> <>
@ -101,15 +102,20 @@ export const ObjectOptionsDropdownLayoutContent = () => {
}} }}
/> />
<MenuItemSelect <MenuItemSelect
LeftIcon={IconLayoutKanban} LeftIcon={viewTypeIconMapping(ViewType.Kanban)}
text={t`Kanban`} text={t`Kanban`}
disabled={isDefaultView} disabled={isDefaultView}
contextualText={ contextualText={
isDefaultView isDefaultView ? (
? t`Not available for default view` <>
: availableFieldsForKanban.length === 0 {nbsp}·{nbsp}
? t`Create Select...` <OverflowingTextWithTooltip
: undefined text={t`Not available for default view`}
/>
</>
) : availableFieldsForKanban.length === 0 ? (
t`Create Select...`
) : undefined
} }
selected={currentView?.type === ViewType.Kanban} selected={currentView?.type === ViewType.Kanban}
onClick={handleSelectKanbanViewType} onClick={handleSelectKanbanViewType}

View File

@ -2,10 +2,8 @@ import { Key } from 'ts-key-enum';
import { import {
AppTooltip, AppTooltip,
IconCopy, IconCopy,
IconLayoutKanban,
IconLayoutList, IconLayoutList,
IconListDetails, IconListDetails,
IconTable,
IconTrash, IconTrash,
MenuItem, MenuItem,
} from 'twenty-ui'; } from 'twenty-ui';
@ -23,7 +21,7 @@ import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentStateV2'; import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentStateV2';
import { useGetCurrentViewOnly } from '@/views/hooks/useGetCurrentViewOnly'; import { useGetCurrentViewOnly } from '@/views/hooks/useGetCurrentViewOnly';
import { ViewType } from '@/views/types/ViewType'; import { ViewType, viewTypeIconMapping } from '@/views/types/ViewType';
import { useDeleteViewFromCurrentState } from '@/views/view-picker/hooks/useDeleteViewFromCurrentState'; import { useDeleteViewFromCurrentState } from '@/views/view-picker/hooks/useDeleteViewFromCurrentState';
import { viewPickerReferenceViewIdComponentState } from '@/views/view-picker/states/viewPickerReferenceViewIdComponentState'; import { viewPickerReferenceViewIdComponentState } from '@/views/view-picker/states/viewPickerReferenceViewIdComponentState';
import { useTheme } from '@emotion/react'; import { useTheme } from '@emotion/react';
@ -85,9 +83,7 @@ export const ObjectOptionsDropdownMenuContent = () => {
<DropdownMenuItemsContainer scrollable={false}> <DropdownMenuItemsContainer scrollable={false}>
<MenuItem <MenuItem
onClick={() => onContentChange('layout')} onClick={() => onContentChange('layout')}
LeftIcon={ LeftIcon={viewTypeIconMapping(currentView?.type ?? ViewType.Table)}
currentView?.type === ViewType.Table ? IconTable : IconLayoutKanban
}
text={t`Layout`} text={t`Layout`}
contextualText={`${capitalize(currentView?.type ?? '')}`} contextualText={`${capitalize(currentView?.type ?? '')}`}
hasSubMenu hasSubMenu

View File

@ -61,6 +61,8 @@ export const ObjectOptionsDropdownMenuViewName = ({
const [viewPickerSelectedIcon, setViewPickerSelectedIcon] = const [viewPickerSelectedIcon, setViewPickerSelectedIcon] =
useRecoilComponentStateV2(viewPickerSelectedIconComponentState); useRecoilComponentStateV2(viewPickerSelectedIconComponentState);
setViewPickerSelectedIcon(currentView.icon);
const viewPickerIsPersisting = useRecoilComponentValueV2( const viewPickerIsPersisting = useRecoilComponentValueV2(
viewPickerIsPersistingComponentState, viewPickerIsPersistingComponentState,
); );

View File

@ -9,7 +9,7 @@ import { usePersistViewGroupRecords } from '@/views/hooks/internal/usePersistVie
import { useUpdateCurrentView } from '@/views/hooks/useUpdateCurrentView'; import { useUpdateCurrentView } from '@/views/hooks/useUpdateCurrentView';
import { GraphQLView } from '@/views/types/GraphQLView'; import { GraphQLView } from '@/views/types/GraphQLView';
import { ViewGroup } from '@/views/types/ViewGroup'; import { ViewGroup } from '@/views/types/ViewGroup';
import { ViewType } from '@/views/types/ViewType'; import { ViewType, viewTypeIconMapping } from '@/views/types/ViewType';
import { useGetAvailableFieldsForKanban } from '@/views/view-picker/hooks/useGetAvailableFieldsForKanban'; import { useGetAvailableFieldsForKanban } from '@/views/view-picker/hooks/useGetAvailableFieldsForKanban';
import { useCallback } from 'react'; import { useCallback } from 'react';
import { useRecoilCallback, useSetRecoilState } from 'recoil'; import { useRecoilCallback, useSetRecoilState } from 'recoil';
@ -122,13 +122,20 @@ export const useSetViewTypeFromLayoutOptionsMenu = () => {
); );
} }
setRecordIndexViewType(viewType); setRecordIndexViewType(viewType);
await updateCurrentView(updateCurrentViewParams);
break; if (shouldChangeIcon(currentView.icon, currentView.type)) {
updateCurrentViewParams.icon =
viewTypeIconMapping(viewType).displayName;
}
return await updateCurrentView(updateCurrentViewParams);
} }
case ViewType.Table: case ViewType.Table:
setRecordIndexViewType(viewType); setRecordIndexViewType(viewType);
await updateCurrentView(updateCurrentViewParams); if (shouldChangeIcon(currentView.icon, currentView.type)) {
break; updateCurrentViewParams.icon =
viewTypeIconMapping(viewType).displayName;
}
return await updateCurrentView(updateCurrentViewParams);
default: { default: {
return assertUnreachable(viewType); return assertUnreachable(viewType);
} }
@ -144,6 +151,25 @@ export const useSetViewTypeFromLayoutOptionsMenu = () => {
], ],
); );
const shouldChangeIcon = (
oldIcon: string,
oldViewType: ViewType,
): boolean => {
if (
oldViewType === ViewType.Kanban &&
oldIcon === viewTypeIconMapping(ViewType.Kanban).displayName
) {
return true;
}
if (
oldViewType === ViewType.Table &&
oldIcon === viewTypeIconMapping(ViewType.Table).displayName
) {
return true;
}
return false;
};
return { return {
setAndPersistViewType, setAndPersistViewType,
}; };

View File

@ -1,4 +1,21 @@
import { IconComponent, IconLayoutKanban, IconTable } from 'twenty-ui';
export enum ViewType { export enum ViewType {
Table = 'table', Table = 'table',
Kanban = 'kanban', Kanban = 'kanban',
} }
const VIEW_TYPE_ICON_MAPPING = [
{ icon: IconLayoutKanban, value: ViewType.Kanban },
{ icon: IconTable, value: ViewType.Table },
] as const satisfies {
icon: IconComponent;
value: ViewType;
}[];
export const viewTypeIconMapping = (viewType?: ViewType) => {
return (
VIEW_TYPE_ICON_MAPPING.find((type) => type.value === viewType)?.icon ??
IconTable
);
};

View File

@ -1,12 +1,13 @@
import styled from '@emotion/styled'; import styled from '@emotion/styled';
import { Key } from 'ts-key-enum'; import { Key } from 'ts-key-enum';
import { IconLayoutKanban, IconTable, IconX } from 'twenty-ui'; import { IconX } from 'twenty-ui';
import { useObjectMetadataItemById } from '@/object-metadata/hooks/useObjectMetadataItemById'; import { useObjectMetadataItemById } from '@/object-metadata/hooks/useObjectMetadataItemById';
import { IconPicker } from '@/ui/input/components/IconPicker'; import { IconPicker } from '@/ui/input/components/IconPicker';
import { Select } from '@/ui/input/components/Select'; import { Select } from '@/ui/input/components/Select';
import { TextInputV2 } from '@/ui/input/components/TextInputV2'; import { TextInputV2 } from '@/ui/input/components/TextInputV2';
import { DropdownMenuHeader } from '@/ui/layout/dropdown/components/DropdownMenuHeader/DropdownMenuHeader'; import { DropdownMenuHeader } from '@/ui/layout/dropdown/components/DropdownMenuHeader/DropdownMenuHeader';
import { DropdownMenuHeaderLeftComponent } from '@/ui/layout/dropdown/components/DropdownMenuHeader/internal/DropdownMenuHeaderLeftComponent';
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer'; import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownMenuSeparator'; import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownMenuSeparator';
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys'; import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
@ -16,7 +17,7 @@ import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/
import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentStateV2'; import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentStateV2';
import { viewObjectMetadataIdComponentState } from '@/views/states/viewObjectMetadataIdComponentState'; import { viewObjectMetadataIdComponentState } from '@/views/states/viewObjectMetadataIdComponentState';
import { ViewsHotkeyScope } from '@/views/types/ViewsHotkeyScope'; import { ViewsHotkeyScope } from '@/views/types/ViewsHotkeyScope';
import { ViewType } from '@/views/types/ViewType'; import { ViewType, viewTypeIconMapping } from '@/views/types/ViewType';
import { ViewPickerCreateButton } from '@/views/view-picker/components/ViewPickerCreateButton'; import { ViewPickerCreateButton } from '@/views/view-picker/components/ViewPickerCreateButton';
import { ViewPickerIconAndNameContainer } from '@/views/view-picker/components/ViewPickerIconAndNameContainer'; import { ViewPickerIconAndNameContainer } from '@/views/view-picker/components/ViewPickerIconAndNameContainer';
import { ViewPickerSaveButtonContainer } from '@/views/view-picker/components/ViewPickerSaveButtonContainer'; import { ViewPickerSaveButtonContainer } from '@/views/view-picker/components/ViewPickerSaveButtonContainer';
@ -32,9 +33,8 @@ import { viewPickerIsPersistingComponentState } from '@/views/view-picker/states
import { viewPickerKanbanFieldMetadataIdComponentState } from '@/views/view-picker/states/viewPickerKanbanFieldMetadataIdComponentState'; import { viewPickerKanbanFieldMetadataIdComponentState } from '@/views/view-picker/states/viewPickerKanbanFieldMetadataIdComponentState';
import { viewPickerSelectedIconComponentState } from '@/views/view-picker/states/viewPickerSelectedIconComponentState'; import { viewPickerSelectedIconComponentState } from '@/views/view-picker/states/viewPickerSelectedIconComponentState';
import { viewPickerTypeComponentState } from '@/views/view-picker/states/viewPickerTypeComponentState'; import { viewPickerTypeComponentState } from '@/views/view-picker/states/viewPickerTypeComponentState';
import { useMemo, useState } from 'react';
import { useLingui } from '@lingui/react/macro'; import { useLingui } from '@lingui/react/macro';
import { DropdownMenuHeaderLeftComponent } from '@/ui/layout/dropdown/components/DropdownMenuHeader/internal/DropdownMenuHeaderLeftComponent'; import { useMemo, useState } from 'react';
const StyledNoKanbanFieldAvailableContainer = styled.div` const StyledNoKanbanFieldAvailableContainer = styled.div`
color: ${({ theme }) => theme.font.color.light}; color: ${({ theme }) => theme.font.color.light};
@ -101,8 +101,7 @@ export const ViewPickerContentCreateMode = () => {
ViewsHotkeyScope.ListDropdown, ViewsHotkeyScope.ListDropdown,
); );
const defaultIcon = const defaultIcon = viewTypeIconMapping(viewPickerType).displayName;
viewPickerType === ViewType.Kanban ? 'IconLayoutKanban' : 'IconTable';
const selectedIcon = useMemo(() => { const selectedIcon = useMemo(() => {
if (hasManuallySelectedIcon) { if (hasManuallySelectedIcon) {
@ -165,11 +164,15 @@ export const ViewPickerContentCreateMode = () => {
setViewPickerType(value); setViewPickerType(value);
}} }}
options={[ options={[
{ value: ViewType.Table, label: t`Table`, Icon: IconTable }, {
value: ViewType.Table,
label: t`Table`,
Icon: viewTypeIconMapping(ViewType.Table),
},
{ {
value: ViewType.Kanban, value: ViewType.Kanban,
label: t`Kanban`, label: t`Kanban`,
Icon: IconLayoutKanban, Icon: viewTypeIconMapping(ViewType.Kanban),
}, },
]} ]}
dropdownId={VIEW_PICKER_VIEW_TYPE_DROPDOWN_ID} dropdownId={VIEW_PICKER_VIEW_TYPE_DROPDOWN_ID}

View File

@ -5,7 +5,7 @@ import { prefetchViewsFromObjectMetadataItemFamilySelector } from '@/prefetch/st
import { useRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentStateV2'; import { useRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentStateV2';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentStateV2'; import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentStateV2';
import { ViewType } from '@/views/types/ViewType'; import { viewTypeIconMapping } from '@/views/types/ViewType';
import { useGetAvailableFieldsForKanban } from '@/views/view-picker/hooks/useGetAvailableFieldsForKanban'; import { useGetAvailableFieldsForKanban } from '@/views/view-picker/hooks/useGetAvailableFieldsForKanban';
import { useViewPickerMode } from '@/views/view-picker/hooks/useViewPickerMode'; import { useViewPickerMode } from '@/views/view-picker/hooks/useViewPickerMode';
import { viewPickerInputNameComponentState } from '@/views/view-picker/states/viewPickerInputNameComponentState'; import { viewPickerInputNameComponentState } from '@/views/view-picker/states/viewPickerInputNameComponentState';
@ -66,7 +66,8 @@ export const ViewPickerContentEffect = () => {
!viewPickerIsDirty !viewPickerIsDirty
) { ) {
const defaultIcon = const defaultIcon =
viewPickerType === ViewType.Kanban ? 'IconLayoutKanban' : 'IconTable'; viewTypeIconMapping(viewPickerType).displayName ?? 'IconTable';
if (viewPickerMode === 'create-empty') { if (viewPickerMode === 'create-empty') {
setViewPickerSelectedIcon(defaultIcon); setViewPickerSelectedIcon(defaultIcon);
} else { } else {

View File

@ -2,6 +2,7 @@ import { css, useTheme } from '@emotion/react';
import styled from '@emotion/styled'; import styled from '@emotion/styled';
import { IconCheck, IconChevronRight, IconComponent } from '@ui/display'; import { IconCheck, IconChevronRight, IconComponent } from '@ui/display';
import { ReactNode } from 'react';
import { MenuItemLeftContent } from '../internals/components/MenuItemLeftContent'; import { MenuItemLeftContent } from '../internals/components/MenuItemLeftContent';
import { StyledMenuItemBase } from '../internals/components/StyledMenuItemBase'; import { StyledMenuItemBase } from '../internals/components/StyledMenuItemBase';
@ -47,7 +48,7 @@ type MenuItemSelectProps = {
disabled?: boolean; disabled?: boolean;
hovered?: boolean; hovered?: boolean;
hasSubMenu?: boolean; hasSubMenu?: boolean;
contextualText?: string; contextualText?: ReactNode;
}; };
export const MenuItemSelect = ({ export const MenuItemSelect = ({