Option-menu-v2-input (#11116)

Adding the possibility to change the view name and incon from the
Options menu dropdown

---------

Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
This commit is contained in:
Guillim
2025-03-25 15:13:13 +01:00
committed by GitHub
parent acead6169c
commit 877d6e9304
14 changed files with 256 additions and 66 deletions

View File

@ -5,22 +5,22 @@ import {
MenuItemSelect,
} from 'twenty-ui';
import { useObjectOptions } from '@/object-record/object-options-dropdown/hooks/useObjectOptions';
import { useOptionsDropdown } from '@/object-record/object-options-dropdown/hooks/useOptionsDropdown';
import { useUpdateObjectViewOptions } from '@/object-record/object-options-dropdown/hooks/useUpdateObjectViewOptions';
import { recordIndexOpenRecordInState } from '@/object-record/record-index/states/recordIndexOpenRecordInState';
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 { useGetCurrentViewOnly } from '@/views/hooks/useGetCurrentViewOnly';
import { ViewOpenRecordInType } from '@/views/types/ViewOpenRecordInType';
import { t } from '@lingui/core/macro';
import { useRecoilValue } from 'recoil';
import { DropdownMenuHeaderLeftComponent } from '@/ui/layout/dropdown/components/DropdownMenuHeader/internal/DropdownMenuHeaderLeftComponent';
export const ObjectOptionsDropdownLayoutOpenInContent = () => {
const { onContentChange } = useOptionsDropdown();
const recordIndexOpenRecordIn = useRecoilValue(recordIndexOpenRecordInState);
const { currentView } = useGetCurrentViewOnly();
const { setAndPersistOpenRecordIn } = useObjectOptions();
const { setAndPersistOpenRecordIn } = useUpdateObjectViewOptions();
return (
<>

View File

@ -4,21 +4,19 @@ import {
IconCopy,
IconLayoutKanban,
IconLayoutList,
IconList,
IconListDetails,
IconTable,
IconTrash,
MenuItem,
useIcons,
} from 'twenty-ui';
import { ObjectOptionsDropdownMenuViewName } from '@/object-record/object-options-dropdown/components/ObjectOptionsDropdownMenuViewName';
import { useObjectOptionsForBoard } from '@/object-record/object-options-dropdown/hooks/useObjectOptionsForBoard';
import { useOptionsDropdown } from '@/object-record/object-options-dropdown/hooks/useOptionsDropdown';
import { recordGroupFieldMetadataComponentState } from '@/object-record/record-group/states/recordGroupFieldMetadataComponentState';
import { TableOptionsHotkeyScope } from '@/object-record/record-table/types/TableOptionsHotkeyScope';
import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar';
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
import { DropdownMenuHeader } from '@/ui/layout/dropdown/components/DropdownMenuHeader/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';
@ -30,19 +28,14 @@ import { useDeleteViewFromCurrentState } from '@/views/view-picker/hooks/useDele
import { viewPickerReferenceViewIdComponentState } from '@/views/view-picker/states/viewPickerReferenceViewIdComponentState';
import { useTheme } from '@emotion/react';
import { useLingui } from '@lingui/react/macro';
import { DropdownMenuHeaderLeftComponent } from '@/ui/layout/dropdown/components/DropdownMenuHeader/internal/DropdownMenuHeaderLeftComponent';
import { capitalize, isDefined } from 'twenty-shared/utils';
export const ObjectOptionsDropdownMenuContent = () => {
const { t } = useLingui();
const { recordIndexId, objectMetadataItem, onContentChange, closeDropdown } =
useOptionsDropdown();
const { getIcon } = useIcons();
const { currentView } = useGetCurrentViewOnly();
const CurrentViewIcon = currentView?.icon ? getIcon(currentView.icon) : null;
const recordGroupFieldMetadata = useRecoilComponentValueV2(
recordGroupFieldMetadataComponentState,
);
@ -83,14 +76,10 @@ export const ObjectOptionsDropdownMenuContent = () => {
return (
<>
<DropdownMenuHeader
StartComponent={
<DropdownMenuHeaderLeftComponent Icon={CurrentViewIcon ?? IconList} />
}
>
{currentView?.name}
</DropdownMenuHeader>
{currentView && (
<ObjectOptionsDropdownMenuViewName currentView={currentView} />
)}
<DropdownMenuSeparator />
<DropdownMenuItemsContainer scrollable={false}>
<MenuItem
onClick={() => onContentChange('layout')}

View File

@ -0,0 +1,133 @@
import { Key } from 'ts-key-enum';
import { useUpdateObjectViewOptions } from '@/object-record/object-options-dropdown/hooks/useUpdateObjectViewOptions';
import { IconPicker } from '@/ui/input/components/IconPicker';
import { TextInputV2 } from '@/ui/input/components/TextInputV2';
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
import { useRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentStateV2';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentStateV2';
import { View } from '@/views/types/View';
import { ViewsHotkeyScope } from '@/views/types/ViewsHotkeyScope';
import { useUpdateViewFromCurrentState } from '@/views/view-picker/hooks/useUpdateViewFromCurrentState';
import { viewPickerIsDirtyComponentState } from '@/views/view-picker/states/viewPickerIsDirtyComponentState';
import { viewPickerIsPersistingComponentState } from '@/views/view-picker/states/viewPickerIsPersistingComponentState';
import { viewPickerSelectedIconComponentState } from '@/views/view-picker/states/viewPickerSelectedIconComponentState';
import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import { OverflowingTextWithTooltip } from '@ui/display';
import { useState } from 'react';
import { useIcons } from 'twenty-ui';
import { useDebouncedCallback } from 'use-debounce';
const StyledDropdownMenuIconAndNameContainer = styled.div`
align-items: center;
display: flex;
margin-left: 0;
margin-right: 0;
gap: ${({ theme }) => theme.spacing(1)};
`;
const StyledMenuTitleContainer = styled.div`
align-items: center;
display: flex;
gap: ${({ theme }) => theme.spacing(1)};
padding: ${({ theme }) => theme.spacing(1)};
`;
const StyledMenuIconContainer = styled.div`
align-items: center;
display: flex;
height: ${({ theme }) => theme.spacing(6)};
justify-content: center;
width: ${({ theme }) => theme.spacing(6)};
`;
const StyledMainText = styled.div`
flex-shrink: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
max-width: 100%;
`;
export const ObjectOptionsDropdownMenuViewName = ({
currentView,
}: {
currentView: View;
}) => {
const [viewPickerSelectedIcon, setViewPickerSelectedIcon] =
useRecoilComponentStateV2(viewPickerSelectedIconComponentState);
const viewPickerIsPersisting = useRecoilComponentValueV2(
viewPickerIsPersistingComponentState,
);
const setViewPickerIsDirty = useSetRecoilComponentStateV2(
viewPickerIsDirtyComponentState,
);
const { setAndPersistViewName, setAndPersistViewIcon } =
useUpdateObjectViewOptions();
const { updateViewFromCurrentState } = useUpdateViewFromCurrentState();
const [viewName, setViewName] = useState(currentView?.name);
useScopedHotkeys(
Key.Enter,
async () => {
if (viewPickerIsPersisting) {
return;
}
await updateViewFromCurrentState();
},
ViewsHotkeyScope.ListDropdown,
);
const handleIconChange = ({ iconKey }: { iconKey: string }) => {
setViewPickerIsDirty(true);
setViewPickerSelectedIcon(iconKey);
setAndPersistViewIcon(iconKey, currentView);
};
const handleViewNameChange = useDebouncedCallback((value: string) => {
setAndPersistViewName(value, currentView);
}, 500);
const theme = useTheme();
const { getIcon } = useIcons();
const MainIcon = getIcon(currentView?.icon);
return (
<>
{currentView?.key === 'INDEX' && (
<StyledMenuTitleContainer>
<StyledMenuIconContainer>
<MainIcon size={theme.icon.size.md} stroke={theme.icon.stroke.sm} />
</StyledMenuIconContainer>
<StyledMainText>
<OverflowingTextWithTooltip text={currentView?.name} />
</StyledMainText>
</StyledMenuTitleContainer>
)}
{currentView?.key !== 'INDEX' && (
<DropdownMenuItemsContainer>
<StyledDropdownMenuIconAndNameContainer>
<IconPicker
size="small"
onChange={handleIconChange}
selectedIconKey={viewPickerSelectedIcon}
/>
<TextInputV2
value={viewName}
onChange={(value) => {
setViewName(value);
handleViewNameChange(value);
}}
autoGrow={false}
sizeVariant="sm"
/>
</StyledDropdownMenuIconAndNameContainer>
</DropdownMenuItemsContainer>
)}
</>
);
};

View File

@ -1,29 +0,0 @@
import { recordIndexOpenRecordInState } from '@/object-record/record-index/states/recordIndexOpenRecordInState';
import { useUpdateCurrentView } from '@/views/hooks/useUpdateCurrentView';
import { GraphQLView } from '@/views/types/GraphQLView';
import { ViewOpenRecordInType } from '@/views/types/ViewOpenRecordInType';
import { useCallback } from 'react';
import { useSetRecoilState } from 'recoil';
export const useObjectOptions = () => {
const setRecordIndexOpenRecordIn = useSetRecoilState(
recordIndexOpenRecordInState,
);
const { updateCurrentView } = useUpdateCurrentView();
const setAndPersistOpenRecordIn = useCallback(
(openRecordIn: ViewOpenRecordInType, view: GraphQLView | undefined) => {
if (!view) return;
setRecordIndexOpenRecordIn(openRecordIn);
updateCurrentView({
openRecordIn,
});
},
[setRecordIndexOpenRecordIn, updateCurrentView],
);
return {
setAndPersistOpenRecordIn,
};
};

View File

@ -0,0 +1,64 @@
import { recordIndexOpenRecordInState } from '@/object-record/record-index/states/recordIndexOpenRecordInState';
import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentStateV2';
import { useUpdateCurrentView } from '@/views/hooks/useUpdateCurrentView';
import { GraphQLView } from '@/views/types/GraphQLView';
import { ViewOpenRecordInType } from '@/views/types/ViewOpenRecordInType';
import { viewPickerInputNameComponentState } from '@/views/view-picker/states/viewPickerInputNameComponentState';
import { viewPickerSelectedIconComponentState } from '@/views/view-picker/states/viewPickerSelectedIconComponentState';
import { useCallback } from 'react';
import { useSetRecoilState } from 'recoil';
export const useUpdateObjectViewOptions = () => {
const setRecordIndexOpenRecordIn = useSetRecoilState(
recordIndexOpenRecordInState,
);
const setRecordIndexViewName = useSetRecoilComponentStateV2(
viewPickerInputNameComponentState,
);
const setRecordIndexViewIcon = useSetRecoilComponentStateV2(
viewPickerSelectedIconComponentState,
);
const { updateCurrentView } = useUpdateCurrentView();
const setAndPersistOpenRecordIn = useCallback(
(openRecordIn: ViewOpenRecordInType, view: GraphQLView | undefined) => {
if (!view) return;
setRecordIndexOpenRecordIn(openRecordIn);
updateCurrentView({
openRecordIn,
});
},
[setRecordIndexOpenRecordIn, updateCurrentView],
);
const setAndPersistViewName = useCallback(
(viewName: string, view: GraphQLView | undefined) => {
if (!view) return;
setRecordIndexViewName(viewName);
updateCurrentView({
name: viewName,
});
},
[setRecordIndexViewName, updateCurrentView],
);
const setAndPersistViewIcon = useCallback(
(viewIcon: string, view: GraphQLView | undefined) => {
if (!view) return;
setRecordIndexViewIcon(viewIcon);
updateCurrentView({
icon: viewIcon,
});
},
[setRecordIndexViewIcon, updateCurrentView],
);
return {
setAndPersistOpenRecordIn,
setAndPersistViewName,
setAndPersistViewIcon,
};
};

View File

@ -89,7 +89,7 @@ export const ObjectRecordShowPageBreadcrumb = ({
isDisplayModeFixHeight: true,
}}
>
<RecordTitleCell sizeVariant="sm" />
<RecordTitleCell sizeVariant="xs" />
</FieldContext.Provider>
</StyledTitle>
</StyledEditableTitleContainer>

View File

@ -20,7 +20,7 @@ import { getRecordTitleCellId } from '@/object-record/record-title-cell/utils/ge
type RecordTitleCellProps = {
loading?: boolean;
sizeVariant?: 'sm' | 'md';
sizeVariant?: 'xs' | 'md';
};
export const RecordTitleCell = ({

View File

@ -16,7 +16,7 @@ type RecordTitleCellFieldInputProps = {
onEscape?: FieldInputEvent;
onTab?: FieldInputEvent;
onShiftTab?: FieldInputEvent;
sizeVariant?: 'sm' | 'md';
sizeVariant?: 'xs' | 'md';
};
export const RecordTitleCellFieldInput = ({

View File

@ -14,7 +14,7 @@ type RecordTitleCellTextFieldInputProps = {
onEscape?: FieldInputEvent;
onTab?: FieldInputEvent;
onShiftTab?: FieldInputEvent;
sizeVariant?: 'sm' | 'md';
sizeVariant?: 'xs' | 'md';
};
export const RecordTitleCellTextFieldInput = ({

View File

@ -37,7 +37,7 @@ type RecordTitleDoubleTextInputProps = {
) => void;
onChange?: (newDoubleTextValue: FieldDoubleText) => void;
onPaste?: (newDoubleTextValue: FieldDoubleText) => void;
sizeVariant?: 'sm' | 'md';
sizeVariant?: 'xs' | 'md';
};
export const RecordTitleDoubleTextInput = ({

View File

@ -15,7 +15,7 @@ type RecordTitleFullNameFieldInputProps = {
onEscape?: FieldInputEvent;
onTab?: FieldInputEvent;
onShiftTab?: FieldInputEvent;
sizeVariant?: 'sm' | 'md';
sizeVariant?: 'xs' | 'md';
};
export const RecordTitleFullNameFieldInput = ({