Settings Option Card component (#8456)

fixes - #8195

---------

Co-authored-by: Charles Bochet <charles@twenty.com>
This commit is contained in:
nitin
2024-11-18 14:52:33 +05:30
committed by GitHub
parent ade1c57ff4
commit 2f5dc26545
56 changed files with 931 additions and 920 deletions

View File

@ -1,11 +1,10 @@
import styled from '@emotion/styled';
import { Controller, useFormContext } from 'react-hook-form';
import { IconCheck, IconX, CardContent } from 'twenty-ui';
import { IconCheck, IconX } from 'twenty-ui';
import { z } from 'zod';
import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
import { SettingsOptionCardContentSelect } from '@/settings/components/SettingsOptions/SettingsOptionCardContentSelect';
import { useBooleanSettingsFormInitialValues } from '@/settings/data-model/fields/forms/boolean/hooks/useBooleanSettingsFormInitialValues';
import { Select } from '@/ui/input/components/Select';
import { isDefined } from '~/utils/isDefined';
export const settingsDataModelFieldBooleanFormSchema = z.object({
@ -21,18 +20,6 @@ type SettingsDataModelFieldBooleanFormProps = {
fieldMetadataItem: Pick<FieldMetadataItem, 'defaultValue'>;
};
const StyledContainer = styled(CardContent)`
padding-bottom: ${({ theme }) => theme.spacing(3.5)};
`;
const StyledLabel = styled.span`
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;
`;
export const SettingsDataModelFieldBooleanForm = ({
className,
fieldMetadataItem,
@ -45,37 +32,36 @@ export const SettingsDataModelFieldBooleanForm = ({
});
return (
<StyledContainer>
<StyledLabel>Default Value</StyledLabel>
<Controller
name="defaultValue"
control={control}
defaultValue={initialDefaultValue}
render={({ field: { onChange, value } }) => (
<Select
className={className}
fullWidth
// TODO: temporary fix - disabling edition because after editing the defaultValue,
// newly created records are not taking into account the updated defaultValue properly.
disabled={isEditMode}
dropdownId="object-field-default-value-select"
value={value}
onChange={onChange}
options={[
{
value: true,
label: 'True',
Icon: IconCheck,
},
{
value: false,
label: 'False',
Icon: IconX,
},
]}
/>
)}
/>
</StyledContainer>
<Controller
name="defaultValue"
control={control}
defaultValue={initialDefaultValue}
render={({ field: { onChange, value } }) => (
<SettingsOptionCardContentSelect
Icon={IconCheck}
title="Default Value"
description="Select the default value for this boolean field"
value={value}
onChange={onChange}
selectClassName={className}
// TODO: temporary fix - disabling edition because after editing the defaultValue,
// newly created records are not taking into account the updated defaultValue properly.
disabled={isEditMode}
dropdownId="object-field-default-value-select-boolean"
options={[
{
value: true,
label: 'True',
Icon: IconCheck,
},
{
value: false,
label: 'False',
Icon: IconX,
},
]}
/>
)}
/>
);
};

View File

@ -1,91 +0,0 @@
import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import { createPortal } from 'react-dom';
import {
AppTooltip,
IconComponent,
IconInfoCircle,
Toggle,
TooltipDelay,
} from 'twenty-ui';
const StyledContainer = styled.div<{ disabled?: boolean }>`
align-items: center;
background-color: ${({ theme }) => theme.background.transparent.lighter};
border: 1px solid ${({ theme }) => theme.border.color.medium};
box-sizing: border-box;
border-radius: ${({ theme }) => theme.border.radius.sm};
color: ${({ disabled, theme }) =>
disabled ? theme.font.color.tertiary : theme.font.color.primary};
display: flex;
gap: ${({ theme }) => theme.spacing(1)};
height: ${({ theme }) => theme.spacing(8)};
justify-content: space-between;
padding: 0 ${({ theme }) => theme.spacing(2)};
`;
const StyledGroup = styled.div`
align-items: center;
display: flex;
gap: ${({ theme }) => theme.spacing(2)};
`;
interface SettingsDataModelFieldToggleProps {
disabled?: boolean;
Icon?: IconComponent;
label: string;
tooltip?: string;
value?: boolean;
onChange: (value: boolean) => void;
}
export const SettingsDataModelFieldToggle = ({
disabled,
Icon,
label,
tooltip,
value,
onChange,
}: SettingsDataModelFieldToggleProps) => {
const theme = useTheme();
const infoCircleElementId = `info-circle-id-${Math.random().toString(36).slice(2)}`;
return (
<StyledContainer>
<StyledGroup>
{Icon && (
<Icon color={theme.font.color.tertiary} size={theme.icon.size.md} />
)}
{label}
</StyledGroup>
<StyledGroup>
{tooltip && (
<IconInfoCircle
id={infoCircleElementId}
size={theme.icon.size.md}
color={theme.font.color.tertiary}
/>
)}
{tooltip &&
createPortal(
<AppTooltip
anchorSelect={`#${infoCircleElementId}`}
content={tooltip}
offset={5}
noArrow
place="bottom"
positionStrategy="absolute"
delay={TooltipDelay.shortDelay}
/>,
document.body,
)}
<Toggle
disabled={disabled}
value={value}
onChange={onChange}
toggleSize="small"
/>
</StyledGroup>
</StyledContainer>
);
};

View File

@ -3,10 +3,10 @@ import { z } from 'zod';
import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
import { currencyFieldDefaultValueSchema } from '@/object-record/record-field/validation-schemas/currencyFieldDefaultValueSchema';
import { SettingsOptionCardContentSelect } from '@/settings/components/SettingsOptions/SettingsOptionCardContentSelect';
import { SETTINGS_FIELD_CURRENCY_CODES } from '@/settings/data-model/constants/SettingsFieldCurrencyCodes';
import { useCurrencySettingsFormInitialValues } from '@/settings/data-model/fields/forms/currency/hooks/useCurrencySettingsFormInitialValues';
import { Select } from '@/ui/input/components/Select';
import { CardContent } from 'twenty-ui';
import { IconCurrencyDollar } from 'twenty-ui';
import { applySimpleQuotesToString } from '~/utils/string/applySimpleQuotesToString';
export const settingsDataModelFieldCurrencyFormSchema = z.object({
@ -41,7 +41,7 @@ export const SettingsDataModelFieldCurrencyForm = ({
useCurrencySettingsFormInitialValues({ fieldMetadataItem });
return (
<CardContent>
<>
<Controller
name="defaultValue.amountMicros"
control={control}
@ -53,17 +53,19 @@ export const SettingsDataModelFieldCurrencyForm = ({
control={control}
defaultValue={initialCurrencyCodeValue}
render={({ field: { onChange, value } }) => (
<Select
fullWidth
disabled={disabled}
label="Default Unit"
dropdownId="currency-unit-select"
<SettingsOptionCardContentSelect
Icon={IconCurrencyDollar}
title="Default Value"
description="Choose the default currency that will apply"
value={value}
options={OPTIONS}
onChange={onChange}
disabled={disabled}
fullWidth
dropdownId="object-field-default-value-select-currency"
options={OPTIONS}
/>
)}
/>
</CardContent>
</>
);
};

View File

@ -2,10 +2,9 @@ import { Controller, useFormContext } from 'react-hook-form';
import { z } from 'zod';
import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
import { StyledFormCardTitle } from '@/settings/data-model/fields/components/StyledFormCardTitle';
import { SettingsDataModelFieldToggle } from '@/settings/data-model/fields/forms/components/SettingsDataModelFieldToggle';
import { SettingsOptionCardContentToggle } from '@/settings/components/SettingsOptions/SettingsOptionCardContentToggle';
import { useDateSettingsFormInitialValues } from '@/settings/data-model/fields/forms/date/hooks/useDateSettingsFormInitialValues';
import { IconClockShare, CardContent } from 'twenty-ui';
import { IconSlash } from 'twenty-ui';
export const settingsDataModelFieldDateFormSchema = z.object({
settings: z
@ -36,27 +35,19 @@ export const SettingsDataModelFieldDateForm = ({
});
return (
<CardContent>
<Controller
name="settings.displayAsRelativeDate"
control={control}
defaultValue={initialDisplayAsRelativeDateValue}
render={({ field: { onChange, value } }) => (
<>
<StyledFormCardTitle>Options</StyledFormCardTitle>
<SettingsDataModelFieldToggle
label="Display as relative date"
Icon={IconClockShare}
onChange={onChange}
value={value}
disabled={disabled}
tooltip={
'Show dates in a human-friendly format. Example: "13 mins ago" instead of "Jul 30, 2024 7:11pm"'
}
/>
</>
)}
/>
</CardContent>
<Controller
name="settings.displayAsRelativeDate"
control={control}
defaultValue={initialDisplayAsRelativeDateValue}
render={({ field: { onChange, value } }) => (
<SettingsOptionCardContentToggle
Icon={IconSlash}
title="Display as relative date"
checked={value ?? false}
disabled={disabled}
onChange={onChange}
/>
)}
/>
);
};

View File

@ -1,167 +0,0 @@
import styled from '@emotion/styled';
import { TextInput } from '@/ui/input/components/TextInput';
import { Button, IconInfoCircle, IconMinus, IconPlus } from 'twenty-ui';
import { castAsNumberOrNull } from '~/utils/cast-as-number-or-null';
type SettingsDataModelFieldNumberDecimalsInputProps = {
value: number;
onChange: (value: number) => void;
disabled?: boolean;
};
const StyledCounterContainer = styled.div`
align-items: center;
background: ${({ theme }) => theme.background.noisy};
border: 1px solid ${({ theme }) => theme.border.color.medium};
border-radius: 4px;
display: flex;
flex-direction: column;
flex: 1;
gap: ${({ theme }) => theme.spacing(1)};
justify-content: center;
`;
const StyledExampleText = styled.div`
color: ${({ theme }) => theme.font.color.primary};
width: 100%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font-weight: ${({ theme }) => theme.font.weight.regular};
`;
const StyledCounterControlsIcons = styled.div`
align-items: center;
display: flex;
gap: ${({ theme }) => theme.spacing(2)};
`;
const StyledCounterInnerContainer = styled.div`
align-items: center;
align-self: stretch;
display: flex;
gap: ${({ theme }) => theme.spacing(1)};
padding: ${({ theme }) => theme.spacing(2)};
height: 24px;
`;
const StyledTextInput = styled(TextInput)`
width: ${({ theme }) => theme.spacing(16)};
input {
width: ${({ theme }) => theme.spacing(16)};
height: ${({ theme }) => theme.spacing(6)};
text-align: center;
font-weight: ${({ theme }) => theme.font.weight.medium};
background: ${({ theme }) => theme.background.noisy};
}
input ~ div {
padding-right: ${({ theme }) => theme.spacing(0)};
border-radius: ${({ theme }) => theme.spacing(1)};
background: ${({ theme }) => theme.background.noisy};
}
`;
const StyledTitle = styled.div`
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)};
`;
const StyledControlButton = styled(Button)`
height: ${({ theme }) => theme.spacing(6)};
width: ${({ theme }) => theme.spacing(6)};
padding: 0;
justify-content: center;
svg {
height: ${({ theme }) => theme.spacing(4)};
width: ${({ theme }) => theme.spacing(4)};
}
`;
const StyledInfoButton = styled(Button)`
height: ${({ theme }) => theme.spacing(6)};
width: ${({ theme }) => theme.spacing(6)};
padding: 0;
justify-content: center;
svg {
color: ${({ theme }) => theme.font.color.extraLight};
height: ${({ theme }) => theme.spacing(4)};
width: ${({ theme }) => theme.spacing(4)};
}
`;
const MIN_VALUE = 0;
const MAX_VALUE = 100;
export const SettingsDataModelFieldNumberDecimalsInput = ({
value,
onChange,
disabled,
}: SettingsDataModelFieldNumberDecimalsInputProps) => {
const exampleValue = (1000).toFixed(value);
const handleIncrementCounter = () => {
if (value < MAX_VALUE) {
const newValue = value + 1;
onChange(newValue);
}
};
const handleDecrementCounter = () => {
if (value > MIN_VALUE) {
const newValue = value - 1;
onChange(newValue);
}
};
const handleTextInputChange = (value: string) => {
const castedNumber = castAsNumberOrNull(value);
if (castedNumber === null) {
onChange(MIN_VALUE);
return;
}
if (castedNumber < MIN_VALUE) {
return;
}
if (castedNumber > MAX_VALUE) {
onChange(MAX_VALUE);
return;
}
onChange(castedNumber);
};
return (
<>
<StyledTitle>Number of decimals</StyledTitle>
<StyledCounterContainer>
<StyledCounterInnerContainer>
<StyledExampleText>Example: {exampleValue}</StyledExampleText>
<StyledCounterControlsIcons>
<StyledInfoButton variant="tertiary" Icon={IconInfoCircle} />
<StyledControlButton
variant="secondary"
onClick={handleDecrementCounter}
Icon={IconMinus}
disabled={disabled}
/>
<StyledTextInput
name="decimals"
fullWidth
value={value.toString()}
onChange={(value) => handleTextInputChange(value)}
disabled={disabled}
/>
<StyledControlButton
variant="secondary"
onClick={handleIncrementCounter}
Icon={IconPlus}
disabled={disabled}
/>
</StyledCounterControlsIcons>
</StyledCounterInnerContainer>
</StyledCounterContainer>
</>
);
};

View File

@ -3,10 +3,9 @@ import { z } from 'zod';
import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
import { numberFieldDefaultValueSchema } from '@/object-record/record-field/validation-schemas/numberFieldDefaultValueSchema';
import { SettingsDataModelFieldNumberDecimalsInput } from '@/settings/data-model/fields/forms/number/components/SettingsDataModelFieldNumberDecimalInput';
import { Select } from '@/ui/input/components/Select';
import styled from '@emotion/styled';
import { CardContent, IconNumber9, IconPercentage } from 'twenty-ui';
import { SettingsOptionCardContentCounter } from '@/settings/components/SettingsOptions/SettingsOptionCardContentCounter';
import { SettingsOptionCardContentSelect } from '@/settings/components/SettingsOptions/SettingsOptionCardContentSelect';
import { IconDecimal, IconEye, IconNumber9, IconPercentage } from 'twenty-ui';
import { DEFAULT_DECIMAL_VALUE } from '~/utils/format/number';
export const settingsDataModelFieldNumberFormSchema = z.object({
@ -17,13 +16,6 @@ export type SettingsDataModelFieldNumberFormValues = z.infer<
typeof settingsDataModelFieldNumberFormSchema
>;
const StyledFormCardTitle = styled.div`
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)};
`;
type SettingsDataModelFieldNumberFormProps = {
disabled?: boolean;
fieldMetadataItem: Pick<
@ -39,52 +31,54 @@ export const SettingsDataModelFieldNumberForm = ({
const { control } = useFormContext<SettingsDataModelFieldNumberFormValues>();
return (
<CardContent>
<Controller
name="settings"
defaultValue={{
decimals:
fieldMetadataItem?.settings?.decimals ?? DEFAULT_DECIMAL_VALUE,
type: fieldMetadataItem?.settings?.type || 'number',
}}
control={control}
render={({ field: { onChange, value } }) => {
const count = value?.decimals ?? 0;
const type = value?.type ?? 'number';
<Controller
name="settings"
defaultValue={{
decimals:
fieldMetadataItem?.settings?.decimals ?? DEFAULT_DECIMAL_VALUE,
type: fieldMetadataItem?.settings?.type ?? 'number',
}}
control={control}
render={({ field: { onChange, value } }) => {
const count = value?.decimals ?? 0;
const type = value?.type ?? 'number';
return (
<>
<StyledFormCardTitle>Type</StyledFormCardTitle>
<Select
disabled={disabled}
dropdownId="selectNumberTypes"
options={[
{
label: 'Number',
value: 'number',
Icon: IconNumber9,
},
{
label: 'Percentage',
value: 'percentage',
Icon: IconPercentage,
},
]}
value={type}
onChange={(value) => onChange({ type: value, decimals: count })}
withSearchInput={false}
dropdownWidthAuto={true}
/>
<br />
<SettingsDataModelFieldNumberDecimalsInput
value={count}
onChange={(value) => onChange({ type: type, decimals: value })}
disabled={disabled}
/>
</>
);
}}
/>
</CardContent>
return (
<>
<SettingsOptionCardContentSelect
Icon={IconEye}
dropdownId="number-type"
title="Number type"
description="The number type you want to use, e.g. percentage"
value={type}
onChange={(value) => onChange({ type: value, decimals: count })}
disabled={disabled}
options={[
{
Icon: IconNumber9,
label: 'Number',
value: 'number',
},
{
Icon: IconPercentage,
label: 'Percentage',
value: 'percentage',
},
]}
/>
<SettingsOptionCardContentCounter
Icon={IconDecimal}
title="Number of decimals"
description={`Example: ${(1000).toFixed(count)}`}
value={count}
onChange={(value) => onChange({ type: type, decimals: value })}
disabled={disabled}
minValue={0}
maxValue={100} // needs to be changed
/>
</>
);
}}
/>
);
};

View File

@ -28,9 +28,8 @@ 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 { AdvancedSettingsWrapper } from '@/settings/components/AdvancedSettingsWrapper';
import { isAdvancedModeEnabledState } from '@/ui/navigation/navigation-drawer/states/isAdvancedModeEnabledState';
import { AnimatePresence, motion } from 'framer-motion';
import { useRecoilValue } from 'recoil';
import { SettingsDataModelFieldSelectFormOptionRow } from './SettingsDataModelFieldSelectFormOptionRow';
@ -251,26 +250,14 @@ export const SettingsDataModelFieldSelectForm = ({
<>
<StyledContainer>
<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 values</StyledApiKey>
</StyledApiKeyContainer>
</motion.div>
)}
</AnimatePresence>
<AdvancedSettingsWrapper dimension="width" hideIcon={true}>
<StyledApiKeyContainer>
<StyledIconContainer>
<StyledIconTool size={12} color={MAIN_COLORS.yellow} />
</StyledIconContainer>
<StyledApiKey>API values</StyledApiKey>
</StyledApiKeyContainer>
</AdvancedSettingsWrapper>
<StyledOptionsLabel
isAdvancedModeEnabled={isAdvancedModeEnabled}
>

View File

@ -1,3 +1,11 @@
import { FieldMetadataItemOption } from '@/object-metadata/types/FieldMetadataItem';
import { AdvancedSettingsWrapper } from '@/settings/components/AdvancedSettingsWrapper';
import { OPTION_VALUE_MAXIMUM_LENGTH } from '@/settings/data-model/constants/OptionValueMaximumLength';
import { TextInput } from '@/ui/input/components/TextInput';
import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown';
import { DropdownMenu } from '@/ui/layout/dropdown/components/DropdownMenu';
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown';
import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import { useMemo } from 'react';
@ -14,18 +22,6 @@ import {
MenuItemSelectColor,
} from 'twenty-ui';
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 { TextInput } from '@/ui/input/components/TextInput';
import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown';
import { DropdownMenu } from '@/ui/layout/dropdown/components/DropdownMenu';
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown';
import { isAdvancedModeEnabledState } from '@/ui/navigation/navigation-drawer/states/isAdvancedModeEnabledState';
import { AnimatePresence, motion } from 'framer-motion';
import { useRecoilValue } from 'recoil';
import { computeOptionValueFromLabel } from '~/pages/settings/data-model/utils/compute-option-value-from-label.utils';
type SettingsDataModelFieldSelectFormOptionRowProps = {
@ -83,7 +79,6 @@ export const SettingsDataModelFieldSelectFormOptionRow = ({
option,
isNewRow,
}: SettingsDataModelFieldSelectFormOptionRowProps) => {
const isAdvancedModeEnabled = useRecoilValue(isAdvancedModeEnabledState);
const theme = useTheme();
const dropdownIds = useMemo(() => {
@ -111,28 +106,19 @@ export const SettingsDataModelFieldSelectFormOptionRow = ({
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: computeOptionValueFromLabel(input),
})
}
RightIcon={isDefault ? IconCheck : undefined}
maxLength={OPTION_VALUE_MAXIMUM_LENGTH}
/>
</motion.div>
)}
</AnimatePresence>
<AdvancedSettingsWrapper dimension="width" hideIcon={true}>
<StyledOptionInput
value={option.value}
onChange={(input) =>
onChange({
...option,
value: computeOptionValueFromLabel(input),
})
}
RightIcon={isDefault ? IconCheck : undefined}
maxLength={OPTION_VALUE_MAXIMUM_LENGTH}
/>
</AdvancedSettingsWrapper>
<Dropdown
dropdownId={dropdownIds.color}
dropdownPlacement="bottom-start"