Feature : Adding percentage option to Input Number (#8481)
fixing #7375 --------- Co-authored-by: guillim <guillaume@twenty.com>
This commit is contained in:
@ -54,6 +54,5 @@ export const formatFieldMetadataItemAsFieldDefinition = ({
|
|||||||
metadata: fieldDefintionMetadata,
|
metadata: fieldDefintionMetadata,
|
||||||
type: field.type,
|
type: field.type,
|
||||||
}),
|
}),
|
||||||
settings: field.settings,
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@ -361,7 +361,6 @@ export const RecordBoardCard = ({
|
|||||||
metadata: fieldDefinition.metadata,
|
metadata: fieldDefinition.metadata,
|
||||||
type: fieldDefinition.type,
|
type: fieldDefinition.type,
|
||||||
}),
|
}),
|
||||||
settings: fieldDefinition.settings,
|
|
||||||
},
|
},
|
||||||
useUpdateRecord: useUpdateOneRecordHook,
|
useUpdateRecord: useUpdateOneRecordHook,
|
||||||
hotkeyScope: InlineCellHotkeyScope.InlineCell,
|
hotkeyScope: InlineCellHotkeyScope.InlineCell,
|
||||||
|
|||||||
@ -1,12 +1,16 @@
|
|||||||
import { useNumberFieldDisplay } from '@/object-record/record-field/meta-types/hooks/useNumberFieldDisplay';
|
import { useNumberFieldDisplay } from '@/object-record/record-field/meta-types/hooks/useNumberFieldDisplay';
|
||||||
import { NumberDisplay } from '@/ui/field/display/components/NumberDisplay';
|
import { NumberDisplay } from '@/ui/field/display/components/NumberDisplay';
|
||||||
|
import { formatNumber } from '~/utils/format/number';
|
||||||
|
|
||||||
export const NumberFieldDisplay = () => {
|
export const NumberFieldDisplay = () => {
|
||||||
const { fieldValue, fieldDefinition } = useNumberFieldDisplay();
|
const { fieldValue, fieldDefinition } = useNumberFieldDisplay();
|
||||||
return (
|
const decimals = fieldDefinition.metadata.settings?.decimals;
|
||||||
<NumberDisplay
|
const type = fieldDefinition.metadata.settings?.type;
|
||||||
value={fieldValue}
|
const value =
|
||||||
decimals={fieldDefinition.settings?.decimals}
|
type === 'percentage' && fieldValue
|
||||||
/>
|
? `${formatNumber(Number(fieldValue) * 100, decimals)}%`
|
||||||
);
|
: fieldValue
|
||||||
|
? formatNumber(Number(fieldValue), decimals)
|
||||||
|
: null;
|
||||||
|
return <NumberDisplay value={value} decimals={decimals} />;
|
||||||
};
|
};
|
||||||
|
|||||||
@ -11,6 +11,7 @@ import {
|
|||||||
castAsNumberOrNull,
|
castAsNumberOrNull,
|
||||||
} from '~/utils/cast-as-number-or-null';
|
} from '~/utils/cast-as-number-or-null';
|
||||||
|
|
||||||
|
import { isNull } from '@sniptt/guards';
|
||||||
import { FieldContext } from '../../contexts/FieldContext';
|
import { FieldContext } from '../../contexts/FieldContext';
|
||||||
import { usePersistField } from '../../hooks/usePersistField';
|
import { usePersistField } from '../../hooks/usePersistField';
|
||||||
import { assertFieldMetadata } from '../../types/guards/assertFieldMetadata';
|
import { assertFieldMetadata } from '../../types/guards/assertFieldMetadata';
|
||||||
@ -33,12 +34,23 @@ export const useNumberField = () => {
|
|||||||
const persistField = usePersistField();
|
const persistField = usePersistField();
|
||||||
|
|
||||||
const persistNumberField = (newValue: string) => {
|
const persistNumberField = (newValue: string) => {
|
||||||
|
if (fieldDefinition?.metadata?.settings?.type === 'percentage') {
|
||||||
|
newValue = newValue.replaceAll('%', '');
|
||||||
|
if (!canBeCastAsNumberOrNull(newValue)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const castedValue = castAsNumberOrNull(newValue);
|
||||||
|
if (!isNull(castedValue)) {
|
||||||
|
persistField(castedValue / 100);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
persistField(null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (!canBeCastAsNumberOrNull(newValue)) {
|
if (!canBeCastAsNumberOrNull(newValue)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const castedValue = castAsNumberOrNull(newValue);
|
const castedValue = castAsNumberOrNull(newValue);
|
||||||
|
|
||||||
persistField(castedValue);
|
persistField(castedValue);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -16,7 +16,4 @@ export type FieldDefinition<T extends FieldMetadata> = {
|
|||||||
infoTooltipContent?: string;
|
infoTooltipContent?: string;
|
||||||
defaultValue?: any;
|
defaultValue?: any;
|
||||||
editButtonIcon?: IconComponent;
|
editButtonIcon?: IconComponent;
|
||||||
settings?: {
|
|
||||||
decimals?: number;
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
|
|||||||
@ -10,17 +10,20 @@ import { CurrencyCode } from './CurrencyCode';
|
|||||||
export type FieldUuidMetadata = {
|
export type FieldUuidMetadata = {
|
||||||
objectMetadataNameSingular?: string;
|
objectMetadataNameSingular?: string;
|
||||||
fieldName: string;
|
fieldName: string;
|
||||||
|
settings?: Record<string, never>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type FieldBooleanMetadata = {
|
export type FieldBooleanMetadata = {
|
||||||
objectMetadataNameSingular?: string;
|
objectMetadataNameSingular?: string;
|
||||||
fieldName: string;
|
fieldName: string;
|
||||||
|
settings?: Record<string, never>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type FieldTextMetadata = {
|
export type FieldTextMetadata = {
|
||||||
objectMetadataNameSingular?: string;
|
objectMetadataNameSingular?: string;
|
||||||
placeHolder: string;
|
placeHolder: string;
|
||||||
fieldName: string;
|
fieldName: string;
|
||||||
|
settings?: Record<string, never>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type FieldDateTimeMetadata = {
|
export type FieldDateTimeMetadata = {
|
||||||
@ -46,17 +49,23 @@ export type FieldNumberMetadata = {
|
|||||||
fieldName: string;
|
fieldName: string;
|
||||||
placeHolder: string;
|
placeHolder: string;
|
||||||
isPositive?: boolean;
|
isPositive?: boolean;
|
||||||
|
settings?: {
|
||||||
|
decimals?: number;
|
||||||
|
type?: 'percentage' | 'number';
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export type FieldLinkMetadata = {
|
export type FieldLinkMetadata = {
|
||||||
objectMetadataNameSingular?: string;
|
objectMetadataNameSingular?: string;
|
||||||
placeHolder: string;
|
placeHolder: string;
|
||||||
fieldName: string;
|
fieldName: string;
|
||||||
|
settings?: Record<string, never>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type FieldLinksMetadata = {
|
export type FieldLinksMetadata = {
|
||||||
objectMetadataNameSingular?: string;
|
objectMetadataNameSingular?: string;
|
||||||
fieldName: string;
|
fieldName: string;
|
||||||
|
settings?: Record<string, never>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type FieldCurrencyMetadata = {
|
export type FieldCurrencyMetadata = {
|
||||||
@ -64,56 +73,66 @@ export type FieldCurrencyMetadata = {
|
|||||||
fieldName: string;
|
fieldName: string;
|
||||||
placeHolder: string;
|
placeHolder: string;
|
||||||
isPositive?: boolean;
|
isPositive?: boolean;
|
||||||
|
settings?: Record<string, never>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type FieldFullNameMetadata = {
|
export type FieldFullNameMetadata = {
|
||||||
objectMetadataNameSingular?: string;
|
objectMetadataNameSingular?: string;
|
||||||
placeHolder: string;
|
placeHolder: string;
|
||||||
fieldName: string;
|
fieldName: string;
|
||||||
|
settings?: Record<string, never>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type FieldEmailMetadata = {
|
export type FieldEmailMetadata = {
|
||||||
objectMetadataNameSingular?: string;
|
objectMetadataNameSingular?: string;
|
||||||
placeHolder: string;
|
placeHolder: string;
|
||||||
fieldName: string;
|
fieldName: string;
|
||||||
|
settings?: Record<string, never>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type FieldEmailsMetadata = {
|
export type FieldEmailsMetadata = {
|
||||||
objectMetadataNameSingular?: string;
|
objectMetadataNameSingular?: string;
|
||||||
fieldName: string;
|
fieldName: string;
|
||||||
|
settings?: Record<string, never>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type FieldPhoneMetadata = {
|
export type FieldPhoneMetadata = {
|
||||||
objectMetadataNameSingular?: string;
|
objectMetadataNameSingular?: string;
|
||||||
placeHolder: string;
|
placeHolder: string;
|
||||||
fieldName: string;
|
fieldName: string;
|
||||||
|
settings?: Record<string, never>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type FieldRatingMetadata = {
|
export type FieldRatingMetadata = {
|
||||||
objectMetadataNameSingular?: string;
|
objectMetadataNameSingular?: string;
|
||||||
fieldName: string;
|
fieldName: string;
|
||||||
|
settings?: Record<string, never>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type FieldAddressMetadata = {
|
export type FieldAddressMetadata = {
|
||||||
objectMetadataNameSingular?: string;
|
objectMetadataNameSingular?: string;
|
||||||
placeHolder: string;
|
placeHolder: string;
|
||||||
fieldName: string;
|
fieldName: string;
|
||||||
|
settings?: Record<string, never>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type FieldRawJsonMetadata = {
|
export type FieldRawJsonMetadata = {
|
||||||
objectMetadataNameSingular?: string;
|
objectMetadataNameSingular?: string;
|
||||||
fieldName: string;
|
fieldName: string;
|
||||||
placeHolder: string;
|
placeHolder: string;
|
||||||
|
settings?: Record<string, never>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type FieldRichTextMetadata = {
|
export type FieldRichTextMetadata = {
|
||||||
objectMetadataNameSingular?: string;
|
objectMetadataNameSingular?: string;
|
||||||
fieldName: string;
|
fieldName: string;
|
||||||
|
settings?: Record<string, never>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type FieldPositionMetadata = {
|
export type FieldPositionMetadata = {
|
||||||
objectMetadataNameSingular?: string;
|
objectMetadataNameSingular?: string;
|
||||||
fieldName: string;
|
fieldName: string;
|
||||||
|
settings?: Record<string, never>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type FieldRelationMetadata = {
|
export type FieldRelationMetadata = {
|
||||||
@ -125,6 +144,7 @@ export type FieldRelationMetadata = {
|
|||||||
relationType?: RelationDefinitionType;
|
relationType?: RelationDefinitionType;
|
||||||
targetFieldMetadataName?: string;
|
targetFieldMetadataName?: string;
|
||||||
useEditButton?: boolean;
|
useEditButton?: boolean;
|
||||||
|
settings?: Record<string, never>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type FieldSelectMetadata = {
|
export type FieldSelectMetadata = {
|
||||||
@ -132,33 +152,39 @@ export type FieldSelectMetadata = {
|
|||||||
fieldName: string;
|
fieldName: string;
|
||||||
options: { label: string; color: ThemeColor; value: string }[];
|
options: { label: string; color: ThemeColor; value: string }[];
|
||||||
isNullable: boolean;
|
isNullable: boolean;
|
||||||
|
settings?: Record<string, never>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type FieldMultiSelectMetadata = {
|
export type FieldMultiSelectMetadata = {
|
||||||
objectMetadataNameSingular?: string;
|
objectMetadataNameSingular?: string;
|
||||||
fieldName: string;
|
fieldName: string;
|
||||||
options: { label: string; color: ThemeColor; value: string }[];
|
options: { label: string; color: ThemeColor; value: string }[];
|
||||||
|
settings?: Record<string, never>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type FieldActorMetadata = {
|
export type FieldActorMetadata = {
|
||||||
objectMetadataNameSingular?: string;
|
objectMetadataNameSingular?: string;
|
||||||
fieldName: string;
|
fieldName: string;
|
||||||
|
settings?: Record<string, never>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type FieldArrayMetadata = {
|
export type FieldArrayMetadata = {
|
||||||
objectMetadataNameSingular?: string;
|
objectMetadataNameSingular?: string;
|
||||||
fieldName: string;
|
fieldName: string;
|
||||||
values: { label: string; value: string }[];
|
values: { label: string; value: string }[];
|
||||||
|
settings?: Record<string, never>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type FieldPhonesMetadata = {
|
export type FieldPhonesMetadata = {
|
||||||
objectMetadataNameSingular?: string;
|
objectMetadataNameSingular?: string;
|
||||||
fieldName: string;
|
fieldName: string;
|
||||||
|
settings?: Record<string, never>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type FieldTsVectorMetadata = {
|
export type FieldTsVectorMetadata = {
|
||||||
objectMetadataNameSingular?: string;
|
objectMetadataNameSingular?: string;
|
||||||
fieldName: string;
|
fieldName: string;
|
||||||
|
settings?: Record<string, never>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type FieldMetadata =
|
export type FieldMetadata =
|
||||||
|
|||||||
@ -3,6 +3,8 @@ import { FieldInputDraftValue } from '@/object-record/record-field/types/FieldIn
|
|||||||
import { FieldMetadata } from '@/object-record/record-field/types/FieldMetadata';
|
import { FieldMetadata } from '@/object-record/record-field/types/FieldMetadata';
|
||||||
import { isFieldCurrency } from '@/object-record/record-field/types/guards/isFieldCurrency';
|
import { isFieldCurrency } from '@/object-record/record-field/types/guards/isFieldCurrency';
|
||||||
import { isFieldCurrencyValue } from '@/object-record/record-field/types/guards/isFieldCurrencyValue';
|
import { isFieldCurrencyValue } from '@/object-record/record-field/types/guards/isFieldCurrencyValue';
|
||||||
|
import { isFieldNumber } from '@/object-record/record-field/types/guards/isFieldNumber';
|
||||||
|
import { isFieldNumberValue } from '@/object-record/record-field/types/guards/isFieldNumberValue';
|
||||||
import { isFieldRawJson } from '@/object-record/record-field/types/guards/isFieldRawJson';
|
import { isFieldRawJson } from '@/object-record/record-field/types/guards/isFieldRawJson';
|
||||||
import { isFieldRawJsonValue } from '@/object-record/record-field/types/guards/isFieldRawJsonValue';
|
import { isFieldRawJsonValue } from '@/object-record/record-field/types/guards/isFieldRawJsonValue';
|
||||||
import { isFieldRelation } from '@/object-record/record-field/types/guards/isFieldRelation';
|
import { isFieldRelation } from '@/object-record/record-field/types/guards/isFieldRelation';
|
||||||
@ -12,7 +14,7 @@ import { isDefined } from '~/utils/isDefined';
|
|||||||
import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull';
|
import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull';
|
||||||
|
|
||||||
type computeDraftValueFromFieldValueParams<FieldValue> = {
|
type computeDraftValueFromFieldValueParams<FieldValue> = {
|
||||||
fieldDefinition: Pick<FieldDefinition<FieldMetadata>, 'type'>;
|
fieldDefinition: Pick<FieldDefinition<FieldMetadata>, 'type' | 'metadata'>;
|
||||||
fieldValue: FieldValue;
|
fieldValue: FieldValue;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -40,6 +42,18 @@ export const computeDraftValueFromFieldValue = <FieldValue>({
|
|||||||
} as unknown as FieldInputDraftValue<FieldValue>;
|
} as unknown as FieldInputDraftValue<FieldValue>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
isFieldNumber(fieldDefinition) &&
|
||||||
|
isFieldNumberValue(fieldValue) &&
|
||||||
|
fieldDefinition.metadata.settings?.type === 'percentage'
|
||||||
|
) {
|
||||||
|
return (isUndefinedOrNull(fieldValue)
|
||||||
|
? ''
|
||||||
|
: (
|
||||||
|
fieldValue * 100
|
||||||
|
).toString()) as unknown as FieldInputDraftValue<FieldValue>;
|
||||||
|
}
|
||||||
|
|
||||||
if (isFieldRelation(fieldDefinition)) {
|
if (isFieldRelation(fieldDefinition)) {
|
||||||
return computeEmptyDraftValue<FieldValue>({ fieldDefinition });
|
return computeEmptyDraftValue<FieldValue>({ fieldDefinition });
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,4 +2,5 @@ import { z } from 'zod';
|
|||||||
|
|
||||||
export const numberFieldDefaultValueSchema = z.object({
|
export const numberFieldDefaultValueSchema = z.object({
|
||||||
decimals: z.number().nullable(),
|
decimals: z.number().nullable(),
|
||||||
|
type: z.enum(['percentage', 'number']).nullable(),
|
||||||
});
|
});
|
||||||
|
|||||||
@ -304,9 +304,6 @@ describe('useRecordData', () => {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
position: expect.any(Number),
|
position: expect.any(Number),
|
||||||
settings: {
|
|
||||||
displayAsRelativeDate: true,
|
|
||||||
},
|
|
||||||
showLabel: undefined,
|
showLabel: undefined,
|
||||||
size: 100,
|
size: 100,
|
||||||
type: 'DATE_TIME',
|
type: 'DATE_TIME',
|
||||||
|
|||||||
@ -4,7 +4,9 @@ import { z } from 'zod';
|
|||||||
import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
|
import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
|
||||||
import { numberFieldDefaultValueSchema } from '@/object-record/record-field/validation-schemas/numberFieldDefaultValueSchema';
|
import { numberFieldDefaultValueSchema } from '@/object-record/record-field/validation-schemas/numberFieldDefaultValueSchema';
|
||||||
import { SettingsDataModelFieldNumberDecimalsInput } from '@/settings/data-model/fields/forms/number/components/SettingsDataModelFieldNumberDecimalInput';
|
import { SettingsDataModelFieldNumberDecimalsInput } from '@/settings/data-model/fields/forms/number/components/SettingsDataModelFieldNumberDecimalInput';
|
||||||
import { CardContent } from 'twenty-ui';
|
import { Select } from '@/ui/input/components/Select';
|
||||||
|
import styled from '@emotion/styled';
|
||||||
|
import { CardContent, IconNumber9, IconPercentage } from 'twenty-ui';
|
||||||
import { DEFAULT_DECIMAL_VALUE } from '~/utils/format/number';
|
import { DEFAULT_DECIMAL_VALUE } from '~/utils/format/number';
|
||||||
|
|
||||||
export const settingsDataModelFieldNumberFormSchema = z.object({
|
export const settingsDataModelFieldNumberFormSchema = z.object({
|
||||||
@ -15,6 +17,13 @@ export type SettingsDataModelFieldNumberFormValues = z.infer<
|
|||||||
typeof settingsDataModelFieldNumberFormSchema
|
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 = {
|
type SettingsDataModelFieldNumberFormProps = {
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
fieldMetadataItem: Pick<
|
fieldMetadataItem: Pick<
|
||||||
@ -36,17 +45,43 @@ export const SettingsDataModelFieldNumberForm = ({
|
|||||||
defaultValue={{
|
defaultValue={{
|
||||||
decimals:
|
decimals:
|
||||||
fieldMetadataItem?.settings?.decimals ?? DEFAULT_DECIMAL_VALUE,
|
fieldMetadataItem?.settings?.decimals ?? DEFAULT_DECIMAL_VALUE,
|
||||||
|
type: fieldMetadataItem?.settings?.type || 'number',
|
||||||
}}
|
}}
|
||||||
control={control}
|
control={control}
|
||||||
render={({ field: { onChange, value } }) => {
|
render={({ field: { onChange, value } }) => {
|
||||||
const count = value?.decimals ?? 0;
|
const count = value?.decimals ?? 0;
|
||||||
|
const type = value?.type ?? 'number';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SettingsDataModelFieldNumberDecimalsInput
|
<>
|
||||||
value={count}
|
<StyledFormCardTitle>Type</StyledFormCardTitle>
|
||||||
onChange={(value) => onChange({ decimals: value })}
|
<Select
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
></SettingsDataModelFieldNumberDecimalsInput>
|
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}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@ -11,6 +11,7 @@ type SettingsObjectFieldDataTypeProps = {
|
|||||||
to?: string;
|
to?: string;
|
||||||
Icon?: IconComponent;
|
Icon?: IconComponent;
|
||||||
label?: string;
|
label?: string;
|
||||||
|
labelDetail?: string;
|
||||||
value: SettingsFieldType;
|
value: SettingsFieldType;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -50,11 +51,15 @@ const StyledLabelContainer = styled.div`
|
|||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
const StyledSpan = styled.span`
|
||||||
|
color: ${({ theme }) => theme.font.color.extraLight};
|
||||||
|
`;
|
||||||
export const SettingsObjectFieldDataType = ({
|
export const SettingsObjectFieldDataType = ({
|
||||||
to,
|
to,
|
||||||
value,
|
value,
|
||||||
Icon: IconFromProps,
|
Icon: IconFromProps,
|
||||||
label: labelFromProps,
|
label: labelFromProps,
|
||||||
|
labelDetail,
|
||||||
}: SettingsObjectFieldDataTypeProps) => {
|
}: SettingsObjectFieldDataTypeProps) => {
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
|
|
||||||
@ -70,7 +75,9 @@ export const SettingsObjectFieldDataType = ({
|
|||||||
return (
|
return (
|
||||||
<StyledDataType as={to ? Link : 'div'} to={to} value={value}>
|
<StyledDataType as={to ? Link : 'div'} to={to} value={value}>
|
||||||
<StyledIcon size={theme.icon.size.sm} />
|
<StyledIcon size={theme.icon.size.sm} />
|
||||||
<StyledLabelContainer>{label}</StyledLabelContainer>
|
<StyledLabelContainer>
|
||||||
|
{label} <StyledSpan>{labelDetail && `· ${labelDetail}`}</StyledSpan>
|
||||||
|
</StyledLabelContainer>
|
||||||
</StyledDataType>
|
</StyledDataType>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -90,7 +90,6 @@ export const SettingsObjectFieldItemTableRow = ({
|
|||||||
() => getRelationMetadata({ fieldMetadataItem }),
|
() => getRelationMetadata({ fieldMetadataItem }),
|
||||||
[fieldMetadataItem, getRelationMetadata],
|
[fieldMetadataItem, getRelationMetadata],
|
||||||
) ?? {};
|
) ?? {};
|
||||||
|
|
||||||
const fieldType = fieldMetadataItem.type;
|
const fieldType = fieldMetadataItem.type;
|
||||||
const isFieldTypeSupported = isFieldTypeSupportedInSettings(fieldType);
|
const isFieldTypeSupported = isFieldTypeSupportedInSettings(fieldType);
|
||||||
|
|
||||||
@ -234,6 +233,9 @@ export const SettingsObjectFieldItemTableRow = ({
|
|||||||
? relationObjectMetadataItem?.labelSingular
|
? relationObjectMetadataItem?.labelSingular
|
||||||
: relationObjectMetadataItem?.labelPlural
|
: relationObjectMetadataItem?.labelPlural
|
||||||
}
|
}
|
||||||
|
labelDetail={
|
||||||
|
fieldMetadataItem.settings?.type === 'percentage' ? '%' : undefined
|
||||||
|
}
|
||||||
to={
|
to={
|
||||||
relationObjectMetadataItem?.namePlural &&
|
relationObjectMetadataItem?.namePlural &&
|
||||||
!relationObjectMetadataItem.isSystem
|
!relationObjectMetadataItem.isSystem
|
||||||
|
|||||||
@ -1,5 +1,3 @@
|
|||||||
import { formatNumber } from '~/utils/format/number';
|
|
||||||
|
|
||||||
import { EllipsisDisplay } from './EllipsisDisplay';
|
import { EllipsisDisplay } from './EllipsisDisplay';
|
||||||
|
|
||||||
type NumberDisplayProps = {
|
type NumberDisplayProps = {
|
||||||
@ -7,8 +5,6 @@ type NumberDisplayProps = {
|
|||||||
decimals?: number;
|
decimals?: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const NumberDisplay = ({ value, decimals }: NumberDisplayProps) => (
|
export const NumberDisplay = ({ value }: NumberDisplayProps) => (
|
||||||
<EllipsisDisplay>
|
<EllipsisDisplay>{value}</EllipsisDisplay>
|
||||||
{value && formatNumber(Number(value), decimals)}
|
|
||||||
</EllipsisDisplay>
|
|
||||||
);
|
);
|
||||||
|
|||||||
@ -50,7 +50,7 @@ export const mapViewFieldsToColumnDefinitions = ({
|
|||||||
isSortable: correspondingColumnDefinition.isSortable,
|
isSortable: correspondingColumnDefinition.isSortable,
|
||||||
isFilterable: correspondingColumnDefinition.isFilterable,
|
isFilterable: correspondingColumnDefinition.isFilterable,
|
||||||
defaultValue: correspondingColumnDefinition.defaultValue,
|
defaultValue: correspondingColumnDefinition.defaultValue,
|
||||||
settings: correspondingColumnDefinition.settings,
|
settings: correspondingColumnDefinition.metadata.settings,
|
||||||
} as ColumnDefinition<FieldMetadata>;
|
} as ColumnDefinition<FieldMetadata>;
|
||||||
})
|
})
|
||||||
.filter(isDefined);
|
.filter(isDefined);
|
||||||
|
|||||||
@ -1,5 +1,14 @@
|
|||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
|
|
||||||
|
import { plainToInstance } from 'class-transformer';
|
||||||
|
import {
|
||||||
|
IsEnum,
|
||||||
|
IsInt,
|
||||||
|
IsOptional,
|
||||||
|
Min,
|
||||||
|
validateOrReject,
|
||||||
|
} from 'class-validator';
|
||||||
|
|
||||||
import { FieldMetadataSettings } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata-settings.interface';
|
import { FieldMetadataSettings } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata-settings.interface';
|
||||||
|
|
||||||
import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
|
import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
|
||||||
@ -8,13 +17,29 @@ import {
|
|||||||
FieldMetadataExceptionCode,
|
FieldMetadataExceptionCode,
|
||||||
} from 'src/engine/metadata-modules/field-metadata/field-metadata.exception';
|
} from 'src/engine/metadata-modules/field-metadata/field-metadata.exception';
|
||||||
|
|
||||||
|
enum ValueType {
|
||||||
|
PERCENTAGE = 'percentage',
|
||||||
|
NUMBER = 'number',
|
||||||
|
}
|
||||||
|
|
||||||
|
class SettingsValidation {
|
||||||
|
@IsOptional()
|
||||||
|
@IsInt()
|
||||||
|
@Min(0)
|
||||||
|
decimals?: number;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsEnum(ValueType)
|
||||||
|
type?: 'percentage' | 'number';
|
||||||
|
}
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class FieldMetadataValidationService<
|
export class FieldMetadataValidationService<
|
||||||
T extends FieldMetadataType | 'default' = 'default',
|
T extends FieldMetadataType | 'default' = 'default',
|
||||||
> {
|
> {
|
||||||
constructor() {}
|
constructor() {}
|
||||||
|
|
||||||
validateSettingsOrThrow({
|
async validateSettingsOrThrow({
|
||||||
fieldType,
|
fieldType,
|
||||||
settings,
|
settings,
|
||||||
}: {
|
}: {
|
||||||
@ -23,26 +48,28 @@ export class FieldMetadataValidationService<
|
|||||||
}) {
|
}) {
|
||||||
switch (fieldType) {
|
switch (fieldType) {
|
||||||
case FieldMetadataType.NUMBER:
|
case FieldMetadataType.NUMBER:
|
||||||
this.validateNumberSettings(settings);
|
await this.validateNumberSettings(settings);
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private validateNumberSettings(settings: FieldMetadataSettings<T>) {
|
private async validateNumberSettings(settings: any) {
|
||||||
if ('decimals' in settings) {
|
try {
|
||||||
const { decimals } = settings;
|
const settingsInstance = plainToInstance(SettingsValidation, settings);
|
||||||
|
|
||||||
if (
|
await validateOrReject(settingsInstance);
|
||||||
decimals !== undefined &&
|
} catch (errors) {
|
||||||
(decimals < 0 || !Number.isInteger(decimals))
|
const errorMessages = errors
|
||||||
) {
|
.map((error: any) => Object.values(error.constraints))
|
||||||
throw new FieldMetadataException(
|
.flat()
|
||||||
`Decimals value "${decimals}" must be a positive integer`,
|
.join(', ');
|
||||||
FieldMetadataExceptionCode.INVALID_FIELD_INPUT,
|
|
||||||
);
|
throw new FieldMetadataException(
|
||||||
}
|
`Value for settings is invalid: ${errorMessages}`,
|
||||||
|
FieldMetadataExceptionCode.INVALID_FIELD_INPUT,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -410,7 +410,7 @@ export class FieldMetadataService extends TypeOrmQueryService<FieldMetadataEntit
|
|||||||
// We're running field update under a transaction, so we can rollback if migration fails
|
// We're running field update under a transaction, so we can rollback if migration fails
|
||||||
await fieldMetadataRepository.update(id, fieldMetadataForUpdate);
|
await fieldMetadataRepository.update(id, fieldMetadataForUpdate);
|
||||||
|
|
||||||
const updatedFieldMetadata = await fieldMetadataRepository.findOne({
|
const [updatedFieldMetadata] = await fieldMetadataRepository.find({
|
||||||
where: { id },
|
where: { id },
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -13,6 +13,7 @@ type FieldMetadataDefaultSettings = {
|
|||||||
type FieldMetadataNumberSettings = {
|
type FieldMetadataNumberSettings = {
|
||||||
dataType: NumberDataType;
|
dataType: NumberDataType;
|
||||||
decimals?: number;
|
decimals?: number;
|
||||||
|
type?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
type FieldMetadataDateSettings = {
|
type FieldMetadataDateSettings = {
|
||||||
|
|||||||
@ -176,9 +176,11 @@ export {
|
|||||||
IconMouse2,
|
IconMouse2,
|
||||||
IconNorthStar,
|
IconNorthStar,
|
||||||
IconNotes,
|
IconNotes,
|
||||||
|
IconNumber9,
|
||||||
IconNumbers,
|
IconNumbers,
|
||||||
IconPaperclip,
|
IconPaperclip,
|
||||||
IconPencil,
|
IconPencil,
|
||||||
|
IconPercentage,
|
||||||
IconPhone,
|
IconPhone,
|
||||||
IconPhoto,
|
IconPhoto,
|
||||||
IconPhotoUp,
|
IconPhotoUp,
|
||||||
|
|||||||
Reference in New Issue
Block a user