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 = ({

View File

@ -4,6 +4,7 @@ import { useRecoilValue } from 'recoil';
import {
IconApps,
IconButton,
IconButtonSize,
IconButtonVariant,
IconComponent,
LightIconButton,
@ -21,8 +22,8 @@ import { useSelectableList } from '@/ui/layout/selectable-list/hooks/useSelectab
import { usePreviousHotkeyScope } from '@/ui/utilities/hotkey/hooks/usePreviousHotkeyScope';
import { arrayToChunks } from '~/utils/array/arrayToChunks';
import { IconPickerHotkeyScope } from '../types/IconPickerHotkeyScope';
import { t } from '@lingui/core/macro';
import { IconPickerHotkeyScope } from '../types/IconPickerHotkeyScope';
export type IconPickerProps = {
disabled?: boolean;
@ -34,6 +35,7 @@ export type IconPickerProps = {
onOpen?: () => void;
variant?: IconButtonVariant;
className?: string;
size?: IconButtonSize;
};
const StyledMenuIconItemsContainer = styled.div`
@ -91,6 +93,7 @@ export const IconPicker = ({
onOpen,
variant = 'secondary',
className,
size = 'medium',
}: IconPickerProps) => {
const [searchString, setSearchString] = useState('');
const {
@ -168,6 +171,7 @@ export const IconPicker = ({
disabled={disabled}
Icon={icon}
variant={variant}
size={size}
/>
}
dropdownMenuWidth={176}

View File

@ -49,13 +49,25 @@ const StyledAdornmentContainer = styled.div<{
font-size: ${({ theme }) => theme.font.size.md};
font-weight: ${({ theme }) => theme.font.weight.medium};
height: ${({ sizeVariant }) =>
sizeVariant === 'sm' ? '20px' : sizeVariant === 'md' ? '28px' : '32px'};
sizeVariant === 'xs'
? '20px'
: sizeVariant === 'sm'
? '24px'
: sizeVariant === 'md'
? '28px'
: '32px'};
justify-content: center;
min-width: fit-content;
padding: ${({ theme }) => theme.spacing(2)};
width: auto;
line-height: ${({ sizeVariant }) =>
sizeVariant === 'sm' ? '20px' : sizeVariant === 'md' ? '28px' : '32px'};
sizeVariant === 'xs'
? '20px'
: sizeVariant === 'sm'
? '24px'
: sizeVariant === 'md'
? '28px'
: '32px'};
${({ position }) =>
position === 'left' ? 'border-right: none;' : 'border-left: none;'}
@ -97,14 +109,26 @@ const StyledInput = styled.input<
font-weight: ${({ theme, inheritFontStyles }) =>
inheritFontStyles ? 'inherit' : theme.font.weight.regular};
height: ${({ sizeVariant }) =>
sizeVariant === 'sm' ? '20px' : sizeVariant === 'md' ? '28px' : '32px'};
sizeVariant === 'xs'
? '20px'
: sizeVariant === 'sm'
? '24px'
: sizeVariant === 'md'
? '28px'
: '32px'};
line-height: ${({ sizeVariant }) =>
sizeVariant === 'sm' ? '20px' : sizeVariant === 'md' ? '28px' : '32px'};
sizeVariant === 'xs'
? '20px'
: sizeVariant === 'sm'
? '24px'
: sizeVariant === 'md'
? '28px'
: '32px'};
outline: none;
padding: ${({ theme, sizeVariant, autoGrow }) =>
autoGrow
? theme.spacing(1)
: sizeVariant === 'sm'
: sizeVariant === 'xs'
? `${theme.spacing(2)} 0`
: theme.spacing(2)};
padding-left: ${({ theme, LeftIcon, autoGrow }) =>
@ -152,9 +176,9 @@ const StyledLeftIconContainer = styled.div<{ sizeVariant: TextInputV2Size }>`
display: flex;
justify-content: center;
padding-left: ${({ theme, sizeVariant }) =>
sizeVariant === 'sm'
sizeVariant === 'xs'
? theme.spacing(0.5)
: sizeVariant === 'md'
: sizeVariant === 'md' || sizeVariant === 'sm'
? theme.spacing(1)
: theme.spacing(2)};
position: absolute;
@ -191,7 +215,7 @@ const StyledTrailingIcon = styled.div<{
const INPUT_TYPE_PASSWORD = 'password';
export type TextInputV2Size = 'sm' | 'md' | 'lg';
export type TextInputV2Size = 'xs' | 'sm' | 'md' | 'lg';
export type TextInputV2ComponentProps = Omit<
InputHTMLAttributes<HTMLInputElement>,
@ -378,7 +402,13 @@ const StyledAutogrowWrapper = styled(AutogrowWrapper)<{
}>`
border: 1px solid transparent;
height: ${({ sizeVariant }) =>
sizeVariant === 'sm' ? '20px' : sizeVariant === 'md' ? '28px' : '32px'};
sizeVariant === 'xs'
? '20px'
: sizeVariant === 'sm'
? '24px'
: sizeVariant === 'md'
? '28px'
: '32px'};
padding: 0 ${({ theme }) => theme.spacing(1.25)};
box-sizing: border-box;
`;

View File

@ -4,6 +4,7 @@ import { IconChevronLeft } from 'twenty-ui';
import { IconPicker } from '@/ui/input/components/IconPicker';
import { TextInputV2 } from '@/ui/input/components/TextInputV2';
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 { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownMenuSeparator';
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
@ -21,14 +22,12 @@ import { viewPickerInputNameComponentState } from '@/views/view-picker/states/vi
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 { DropdownMenuHeaderLeftComponent } from '@/ui/layout/dropdown/components/DropdownMenuHeader/internal/DropdownMenuHeaderLeftComponent';
export const ViewPickerContentEditMode = () => {
const { setViewPickerMode } = useViewPickerMode();
const [viewPickerInputName, setViewPickerInputName] =
useRecoilComponentStateV2(viewPickerInputNameComponentState);
const [viewPickerSelectedIcon, setViewPickerSelectedIcon] =
useRecoilComponentStateV2(viewPickerSelectedIconComponentState);