Avanced Settings: Custom API names for Select & Multi-Select Keys (#7489)
### Description - text input was changed because it renders an empty div as the right icon, but the margin and padding affect the layout - we have duplicated code on ExpandedWidthAnimationVariants.ts, because of an eslint rule that prevents more than one const definition, can we ignore the rule? - ### Demo <https://www.loom.com/share/17a37bf5549a4a23ba12343b6046ec6b?sid=4cf297f3-66db-44c9-9a9b-7bde89b90d02> ### Refs <https://github.com/twentyhq/twenty/issues/6146> --------- Co-authored-by: gitstart-twenty <gitstart-twenty@users.noreply.github.com> Co-authored-by: gitstart-twenty <140154534+gitstart-twenty@users.noreply.github.com> Co-authored-by: Marie Stoppa <marie.stoppa@essec.edu>
This commit is contained in:
committed by
GitHub
parent
3ecf9552a5
commit
7ceaa879fe
@ -0,0 +1,4 @@
|
|||||||
|
export const ADVANCED_SETTINGS_ANIMATION_DURATION = {
|
||||||
|
opacity: 0.2,
|
||||||
|
size: 0.4,
|
||||||
|
};
|
||||||
@ -0,0 +1,31 @@
|
|||||||
|
import { ADVANCED_SETTINGS_ANIMATION_DURATION } from '@/settings/constants/AdvancedSettingsAnimationDurations';
|
||||||
|
|
||||||
|
export const EXPANDED_WIDTH_ANIMATION_VARIANTS = {
|
||||||
|
initial: {
|
||||||
|
opacity: 0,
|
||||||
|
width: 0,
|
||||||
|
overflow: 'hidden',
|
||||||
|
transition: {
|
||||||
|
opacity: { duration: ADVANCED_SETTINGS_ANIMATION_DURATION.opacity },
|
||||||
|
width: { duration: ADVANCED_SETTINGS_ANIMATION_DURATION.size },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
animate: {
|
||||||
|
opacity: 1,
|
||||||
|
width: '100%',
|
||||||
|
overflow: 'hidden',
|
||||||
|
transition: {
|
||||||
|
opacity: { duration: ADVANCED_SETTINGS_ANIMATION_DURATION.opacity },
|
||||||
|
width: { duration: ADVANCED_SETTINGS_ANIMATION_DURATION.size },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
exit: {
|
||||||
|
opacity: 0,
|
||||||
|
width: 0,
|
||||||
|
overflow: 'hidden',
|
||||||
|
transition: {
|
||||||
|
opacity: { duration: ADVANCED_SETTINGS_ANIMATION_DURATION.opacity },
|
||||||
|
width: { duration: ADVANCED_SETTINGS_ANIMATION_DURATION.size },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
@ -1,7 +1,7 @@
|
|||||||
import styled from '@emotion/styled';
|
import styled from '@emotion/styled';
|
||||||
import { DropResult } from '@hello-pangea/dnd';
|
import { DropResult } from '@hello-pangea/dnd';
|
||||||
import { Controller, useFormContext } from 'react-hook-form';
|
import { Controller, useFormContext } from 'react-hook-form';
|
||||||
import { IconPlus } from 'twenty-ui';
|
import { IconPlus, IconTool, MAIN_COLORS } from 'twenty-ui';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@ -24,6 +24,10 @@ import { moveArrayItem } from '~/utils/array/moveArrayItem';
|
|||||||
import { toSpliced } from '~/utils/array/toSpliced';
|
import { toSpliced } from '~/utils/array/toSpliced';
|
||||||
import { applySimpleQuotesToString } from '~/utils/string/applySimpleQuotesToString';
|
import { applySimpleQuotesToString } from '~/utils/string/applySimpleQuotesToString';
|
||||||
|
|
||||||
|
import { EXPANDED_WIDTH_ANIMATION_VARIANTS } from '@/settings/constants/ExpandedWidthAnimationVariants';
|
||||||
|
import { isAdvancedModeEnabledState } from '@/ui/navigation/navigation-drawer/states/isAdvancedModeEnabledState';
|
||||||
|
import { AnimatePresence, motion } from 'framer-motion';
|
||||||
|
import { useRecoilValue } from 'recoil';
|
||||||
import { SettingsDataModelFieldSelectFormOptionRow } from './SettingsDataModelFieldSelectFormOptionRow';
|
import { SettingsDataModelFieldSelectFormOptionRow } from './SettingsDataModelFieldSelectFormOptionRow';
|
||||||
|
|
||||||
export const settingsDataModelFieldSelectFormSchema = z.object({
|
export const settingsDataModelFieldSelectFormSchema = z.object({
|
||||||
@ -56,13 +60,49 @@ const StyledContainer = styled(CardContent)`
|
|||||||
padding-bottom: ${({ theme }) => theme.spacing(3.5)};
|
padding-bottom: ${({ theme }) => theme.spacing(3.5)};
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const StyledLabel = styled.span`
|
const StyledOptionsLabel = styled.div<{
|
||||||
|
isAdvancedModeEnabled: boolean;
|
||||||
|
}>`
|
||||||
color: ${({ theme }) => theme.font.color.light};
|
color: ${({ theme }) => theme.font.color.light};
|
||||||
display: block;
|
|
||||||
font-size: ${({ theme }) => theme.font.size.xs};
|
font-size: ${({ theme }) => theme.font.size.xs};
|
||||||
font-weight: ${({ theme }) => theme.font.weight.semiBold};
|
font-weight: ${({ theme }) => theme.font.weight.semiBold};
|
||||||
margin-bottom: 6px;
|
margin-bottom: ${({ theme }) => theme.spacing(1.5)};
|
||||||
margin-top: ${({ theme }) => theme.spacing(1)};
|
margin-top: ${({ theme }) => theme.spacing(1)};
|
||||||
|
width: 100%;
|
||||||
|
margin-left: ${({ theme, isAdvancedModeEnabled }) =>
|
||||||
|
theme.spacing(isAdvancedModeEnabled ? 10 : 0)};
|
||||||
|
`;
|
||||||
|
|
||||||
|
const StyledApiKeyContainer = styled.div`
|
||||||
|
display: flex;
|
||||||
|
gap: ${({ theme }) => theme.spacing(2)};
|
||||||
|
width: 100%;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const StyledApiKey = styled.span`
|
||||||
|
color: ${({ theme }) => theme.font.color.light};
|
||||||
|
font-size: ${({ theme }) => theme.font.size.xs};
|
||||||
|
font-weight: ${({ theme }) => theme.font.weight.semiBold};
|
||||||
|
margin-bottom: ${({ theme }) => theme.spacing(1.5)};
|
||||||
|
margin-top: ${({ theme }) => theme.spacing(1)};
|
||||||
|
width: 100%;
|
||||||
|
white-space: nowrap;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const StyledLabelContainer = styled.div`
|
||||||
|
display: flex;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const StyledIconContainer = styled.div`
|
||||||
|
border-right: 1px solid ${MAIN_COLORS.yellow};
|
||||||
|
display: flex;
|
||||||
|
|
||||||
|
margin-bottom: ${({ theme }) => theme.spacing(1.5)};
|
||||||
|
margin-top: ${({ theme }) => theme.spacing(1)};
|
||||||
|
`;
|
||||||
|
|
||||||
|
const StyledIconTool = styled(IconTool)`
|
||||||
|
margin-right: ${({ theme }) => theme.spacing(0.5)};
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const StyledFooter = styled(CardFooter)`
|
const StyledFooter = styled(CardFooter)`
|
||||||
@ -80,6 +120,7 @@ export const SettingsDataModelFieldSelectForm = ({
|
|||||||
}: SettingsDataModelFieldSelectFormProps) => {
|
}: SettingsDataModelFieldSelectFormProps) => {
|
||||||
const { initialDefaultValue, initialOptions } =
|
const { initialDefaultValue, initialOptions } =
|
||||||
useSelectSettingsFormInitialValues({ fieldMetadataItem });
|
useSelectSettingsFormInitialValues({ fieldMetadataItem });
|
||||||
|
const isAdvancedModeEnabled = useRecoilValue(isAdvancedModeEnabledState);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
control,
|
control,
|
||||||
@ -205,7 +246,33 @@ export const SettingsDataModelFieldSelectForm = ({
|
|||||||
render={({ field: { onChange, value: options } }) => (
|
render={({ field: { onChange, value: options } }) => (
|
||||||
<>
|
<>
|
||||||
<StyledContainer>
|
<StyledContainer>
|
||||||
<StyledLabel>Options</StyledLabel>
|
<StyledLabelContainer>
|
||||||
|
<AnimatePresence>
|
||||||
|
{isAdvancedModeEnabled && (
|
||||||
|
<motion.div
|
||||||
|
initial="initial"
|
||||||
|
animate="animate"
|
||||||
|
exit="exit"
|
||||||
|
variants={EXPANDED_WIDTH_ANIMATION_VARIANTS}
|
||||||
|
>
|
||||||
|
<StyledApiKeyContainer>
|
||||||
|
<StyledIconContainer>
|
||||||
|
<StyledIconTool
|
||||||
|
size={12}
|
||||||
|
color={MAIN_COLORS.yellow}
|
||||||
|
/>
|
||||||
|
</StyledIconContainer>
|
||||||
|
<StyledApiKey>API keys</StyledApiKey>
|
||||||
|
</StyledApiKeyContainer>
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
</AnimatePresence>
|
||||||
|
<StyledOptionsLabel
|
||||||
|
isAdvancedModeEnabled={isAdvancedModeEnabled}
|
||||||
|
>
|
||||||
|
Options
|
||||||
|
</StyledOptionsLabel>
|
||||||
|
</StyledLabelContainer>
|
||||||
<DraggableList
|
<DraggableList
|
||||||
onDragEnd={(result) => handleDragEnd(options, result, onChange)}
|
onDragEnd={(result) => handleDragEnd(options, result, onChange)}
|
||||||
draggableItems={
|
draggableItems={
|
||||||
|
|||||||
@ -13,6 +13,7 @@ import {
|
|||||||
import { v4 } from 'uuid';
|
import { v4 } from 'uuid';
|
||||||
|
|
||||||
import { FieldMetadataItemOption } from '@/object-metadata/types/FieldMetadataItem';
|
import { FieldMetadataItemOption } from '@/object-metadata/types/FieldMetadataItem';
|
||||||
|
import { EXPANDED_WIDTH_ANIMATION_VARIANTS } from '@/settings/constants/ExpandedWidthAnimationVariants';
|
||||||
import { OPTION_VALUE_MAXIMUM_LENGTH } from '@/settings/data-model/constants/OptionValueMaximumLength';
|
import { OPTION_VALUE_MAXIMUM_LENGTH } from '@/settings/data-model/constants/OptionValueMaximumLength';
|
||||||
import { getOptionValueFromLabel } from '@/settings/data-model/fields/forms/select/utils/getOptionValueFromLabel';
|
import { getOptionValueFromLabel } from '@/settings/data-model/fields/forms/select/utils/getOptionValueFromLabel';
|
||||||
import { LightIconButton } from '@/ui/input/button/components/LightIconButton';
|
import { LightIconButton } from '@/ui/input/button/components/LightIconButton';
|
||||||
@ -23,6 +24,9 @@ import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/Drop
|
|||||||
import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown';
|
import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown';
|
||||||
import { MenuItem } from '@/ui/navigation/menu-item/components/MenuItem';
|
import { MenuItem } from '@/ui/navigation/menu-item/components/MenuItem';
|
||||||
import { MenuItemSelectColor } from '@/ui/navigation/menu-item/components/MenuItemSelectColor';
|
import { MenuItemSelectColor } from '@/ui/navigation/menu-item/components/MenuItemSelectColor';
|
||||||
|
import { isAdvancedModeEnabledState } from '@/ui/navigation/navigation-drawer/states/isAdvancedModeEnabledState';
|
||||||
|
import { AnimatePresence, motion } from 'framer-motion';
|
||||||
|
import { useRecoilValue } from 'recoil';
|
||||||
|
|
||||||
type SettingsDataModelFieldSelectFormOptionRowProps = {
|
type SettingsDataModelFieldSelectFormOptionRowProps = {
|
||||||
className?: string;
|
className?: string;
|
||||||
@ -45,19 +49,29 @@ const StyledRow = styled.div`
|
|||||||
|
|
||||||
const StyledColorSample = styled(ColorSample)`
|
const StyledColorSample = styled(ColorSample)`
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
margin-left: 9px;
|
margin-top: ${({ theme }) => theme.spacing(1)};
|
||||||
margin-right: 14px;
|
margin-bottom: ${({ theme }) => theme.spacing(1)};
|
||||||
|
|
||||||
|
margin-right: ${({ theme }) => theme.spacing(3.5)};
|
||||||
|
margin-left: ${({ theme }) => theme.spacing(3.5)};
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const StyledOptionInput = styled(TextInput)`
|
const StyledOptionInput = styled(TextInput)`
|
||||||
flex: 1 0 auto;
|
flex-grow: 1;
|
||||||
margin-right: ${({ theme }) => theme.spacing(2)};
|
width: 100%;
|
||||||
|
|
||||||
& input {
|
& input {
|
||||||
height: ${({ theme }) => theme.spacing(6)};
|
height: ${({ theme }) => theme.spacing(6)};
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
const StyledIconGripVertical = styled(IconGripVertical)`
|
||||||
|
margin-right: ${({ theme }) => theme.spacing(0.75)};
|
||||||
|
`;
|
||||||
|
|
||||||
|
const StyledLightIconButton = styled(LightIconButton)`
|
||||||
|
margin-left: ${({ theme }) => theme.spacing(2)};
|
||||||
|
`;
|
||||||
|
|
||||||
export const SettingsDataModelFieldSelectFormOptionRow = ({
|
export const SettingsDataModelFieldSelectFormOptionRow = ({
|
||||||
className,
|
className,
|
||||||
isDefault,
|
isDefault,
|
||||||
@ -69,6 +83,7 @@ export const SettingsDataModelFieldSelectFormOptionRow = ({
|
|||||||
option,
|
option,
|
||||||
isNewRow,
|
isNewRow,
|
||||||
}: SettingsDataModelFieldSelectFormOptionRowProps) => {
|
}: SettingsDataModelFieldSelectFormOptionRowProps) => {
|
||||||
|
const isAdvancedModeEnabled = useRecoilValue(isAdvancedModeEnabledState);
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
|
|
||||||
const dropdownIds = useMemo(() => {
|
const dropdownIds = useMemo(() => {
|
||||||
@ -90,11 +105,34 @@ export const SettingsDataModelFieldSelectFormOptionRow = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<StyledRow className={className}>
|
<StyledRow className={className}>
|
||||||
<IconGripVertical
|
<StyledIconGripVertical
|
||||||
|
style={{ minWidth: theme.icon.size.md }}
|
||||||
size={theme.icon.size.md}
|
size={theme.icon.size.md}
|
||||||
stroke={theme.icon.stroke.sm}
|
stroke={theme.icon.stroke.sm}
|
||||||
color={theme.font.color.extraLight}
|
color={theme.font.color.extraLight}
|
||||||
/>
|
/>
|
||||||
|
<AnimatePresence>
|
||||||
|
{isAdvancedModeEnabled && (
|
||||||
|
<motion.div
|
||||||
|
initial="initial"
|
||||||
|
animate="animate"
|
||||||
|
exit="exit"
|
||||||
|
variants={EXPANDED_WIDTH_ANIMATION_VARIANTS}
|
||||||
|
>
|
||||||
|
<StyledOptionInput
|
||||||
|
value={option.value}
|
||||||
|
onChange={(input) =>
|
||||||
|
onChange({
|
||||||
|
...option,
|
||||||
|
value: getOptionValueFromLabel(input),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
RightIcon={isDefault ? IconCheck : undefined}
|
||||||
|
maxLength={OPTION_VALUE_MAXIMUM_LENGTH}
|
||||||
|
/>
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
</AnimatePresence>
|
||||||
<Dropdown
|
<Dropdown
|
||||||
dropdownId={dropdownIds.color}
|
dropdownId={dropdownIds.color}
|
||||||
dropdownPlacement="bottom-start"
|
dropdownPlacement="bottom-start"
|
||||||
@ -122,13 +160,18 @@ export const SettingsDataModelFieldSelectFormOptionRow = ({
|
|||||||
/>
|
/>
|
||||||
<StyledOptionInput
|
<StyledOptionInput
|
||||||
value={option.label}
|
value={option.label}
|
||||||
onChange={(label) =>
|
onChange={(label) => {
|
||||||
|
const optionNameHasBeenEdited = !(
|
||||||
|
option.value === getOptionValueFromLabel(option.label)
|
||||||
|
);
|
||||||
onChange({
|
onChange({
|
||||||
...option,
|
...option,
|
||||||
label,
|
label,
|
||||||
value: getOptionValueFromLabel(label),
|
value: optionNameHasBeenEdited
|
||||||
})
|
? option.value
|
||||||
}
|
: getOptionValueFromLabel(label),
|
||||||
|
});
|
||||||
|
}}
|
||||||
RightIcon={isDefault ? IconCheck : undefined}
|
RightIcon={isDefault ? IconCheck : undefined}
|
||||||
maxLength={OPTION_VALUE_MAXIMUM_LENGTH}
|
maxLength={OPTION_VALUE_MAXIMUM_LENGTH}
|
||||||
onInputEnter={handleInputEnter}
|
onInputEnter={handleInputEnter}
|
||||||
@ -141,7 +184,9 @@ export const SettingsDataModelFieldSelectFormOptionRow = ({
|
|||||||
dropdownHotkeyScope={{
|
dropdownHotkeyScope={{
|
||||||
scope: dropdownIds.actions,
|
scope: dropdownIds.actions,
|
||||||
}}
|
}}
|
||||||
clickableComponent={<LightIconButton Icon={IconDotsVertical} />}
|
clickableComponent={
|
||||||
|
<StyledLightIconButton accent="tertiary" Icon={IconDotsVertical} />
|
||||||
|
}
|
||||||
dropdownComponents={
|
dropdownComponents={
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
<DropdownMenuItemsContainer>
|
<DropdownMenuItemsContainer>
|
||||||
|
|||||||
@ -3,7 +3,7 @@ import { isDefined } from 'twenty-ui';
|
|||||||
|
|
||||||
const transitionValues = {
|
const transitionValues = {
|
||||||
transition: {
|
transition: {
|
||||||
opactity: { duration: 0.2 },
|
opacity: { duration: 0.2 },
|
||||||
height: { duration: 0.4 },
|
height: { duration: 0.4 },
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user