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 { DropResult } from '@hello-pangea/dnd';
|
||||
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 {
|
||||
@ -24,6 +24,10 @@ import { moveArrayItem } from '~/utils/array/moveArrayItem';
|
||||
import { toSpliced } from '~/utils/array/toSpliced';
|
||||
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';
|
||||
|
||||
export const settingsDataModelFieldSelectFormSchema = z.object({
|
||||
@ -56,13 +60,49 @@ const StyledContainer = styled(CardContent)`
|
||||
padding-bottom: ${({ theme }) => theme.spacing(3.5)};
|
||||
`;
|
||||
|
||||
const StyledLabel = styled.span`
|
||||
const StyledOptionsLabel = styled.div<{
|
||||
isAdvancedModeEnabled: boolean;
|
||||
}>`
|
||||
color: ${({ theme }) => theme.font.color.light};
|
||||
display: block;
|
||||
font-size: ${({ theme }) => theme.font.size.xs};
|
||||
font-weight: ${({ theme }) => theme.font.weight.semiBold};
|
||||
margin-bottom: 6px;
|
||||
margin-bottom: ${({ theme }) => theme.spacing(1.5)};
|
||||
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)`
|
||||
@ -80,6 +120,7 @@ export const SettingsDataModelFieldSelectForm = ({
|
||||
}: SettingsDataModelFieldSelectFormProps) => {
|
||||
const { initialDefaultValue, initialOptions } =
|
||||
useSelectSettingsFormInitialValues({ fieldMetadataItem });
|
||||
const isAdvancedModeEnabled = useRecoilValue(isAdvancedModeEnabledState);
|
||||
|
||||
const {
|
||||
control,
|
||||
@ -205,7 +246,33 @@ export const SettingsDataModelFieldSelectForm = ({
|
||||
render={({ field: { onChange, value: options } }) => (
|
||||
<>
|
||||
<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
|
||||
onDragEnd={(result) => handleDragEnd(options, result, onChange)}
|
||||
draggableItems={
|
||||
|
||||
@ -13,6 +13,7 @@ import {
|
||||
import { v4 } from 'uuid';
|
||||
|
||||
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 { getOptionValueFromLabel } from '@/settings/data-model/fields/forms/select/utils/getOptionValueFromLabel';
|
||||
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 { MenuItem } from '@/ui/navigation/menu-item/components/MenuItem';
|
||||
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 = {
|
||||
className?: string;
|
||||
@ -45,19 +49,29 @@ const StyledRow = styled.div`
|
||||
|
||||
const StyledColorSample = styled(ColorSample)`
|
||||
cursor: pointer;
|
||||
margin-left: 9px;
|
||||
margin-right: 14px;
|
||||
margin-top: ${({ theme }) => theme.spacing(1)};
|
||||
margin-bottom: ${({ theme }) => theme.spacing(1)};
|
||||
|
||||
margin-right: ${({ theme }) => theme.spacing(3.5)};
|
||||
margin-left: ${({ theme }) => theme.spacing(3.5)};
|
||||
`;
|
||||
|
||||
const StyledOptionInput = styled(TextInput)`
|
||||
flex: 1 0 auto;
|
||||
margin-right: ${({ theme }) => theme.spacing(2)};
|
||||
|
||||
flex-grow: 1;
|
||||
width: 100%;
|
||||
& input {
|
||||
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 = ({
|
||||
className,
|
||||
isDefault,
|
||||
@ -69,6 +83,7 @@ export const SettingsDataModelFieldSelectFormOptionRow = ({
|
||||
option,
|
||||
isNewRow,
|
||||
}: SettingsDataModelFieldSelectFormOptionRowProps) => {
|
||||
const isAdvancedModeEnabled = useRecoilValue(isAdvancedModeEnabledState);
|
||||
const theme = useTheme();
|
||||
|
||||
const dropdownIds = useMemo(() => {
|
||||
@ -90,11 +105,34 @@ export const SettingsDataModelFieldSelectFormOptionRow = ({
|
||||
|
||||
return (
|
||||
<StyledRow className={className}>
|
||||
<IconGripVertical
|
||||
<StyledIconGripVertical
|
||||
style={{ minWidth: theme.icon.size.md }}
|
||||
size={theme.icon.size.md}
|
||||
stroke={theme.icon.stroke.sm}
|
||||
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
|
||||
dropdownId={dropdownIds.color}
|
||||
dropdownPlacement="bottom-start"
|
||||
@ -122,13 +160,18 @@ export const SettingsDataModelFieldSelectFormOptionRow = ({
|
||||
/>
|
||||
<StyledOptionInput
|
||||
value={option.label}
|
||||
onChange={(label) =>
|
||||
onChange={(label) => {
|
||||
const optionNameHasBeenEdited = !(
|
||||
option.value === getOptionValueFromLabel(option.label)
|
||||
);
|
||||
onChange({
|
||||
...option,
|
||||
label,
|
||||
value: getOptionValueFromLabel(label),
|
||||
})
|
||||
}
|
||||
value: optionNameHasBeenEdited
|
||||
? option.value
|
||||
: getOptionValueFromLabel(label),
|
||||
});
|
||||
}}
|
||||
RightIcon={isDefault ? IconCheck : undefined}
|
||||
maxLength={OPTION_VALUE_MAXIMUM_LENGTH}
|
||||
onInputEnter={handleInputEnter}
|
||||
@ -141,7 +184,9 @@ export const SettingsDataModelFieldSelectFormOptionRow = ({
|
||||
dropdownHotkeyScope={{
|
||||
scope: dropdownIds.actions,
|
||||
}}
|
||||
clickableComponent={<LightIconButton Icon={IconDotsVertical} />}
|
||||
clickableComponent={
|
||||
<StyledLightIconButton accent="tertiary" Icon={IconDotsVertical} />
|
||||
}
|
||||
dropdownComponents={
|
||||
<DropdownMenu>
|
||||
<DropdownMenuItemsContainer>
|
||||
|
||||
@ -3,7 +3,7 @@ import { isDefined } from 'twenty-ui';
|
||||
|
||||
const transitionValues = {
|
||||
transition: {
|
||||
opactity: { duration: 0.2 },
|
||||
opacity: { duration: 0.2 },
|
||||
height: { duration: 0.4 },
|
||||
},
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user