New field currency (#4338)
Closes #4122 --------- Co-authored-by: Lucas Bordeau <bordeau.lucas@gmail.com>
This commit is contained in:
@ -3,7 +3,9 @@ import { ApolloProvider as ApolloProviderBase } from '@apollo/client';
|
|||||||
import { useApolloFactory } from '@/apollo/hooks/useApolloFactory';
|
import { useApolloFactory } from '@/apollo/hooks/useApolloFactory';
|
||||||
|
|
||||||
export const ApolloProvider = ({ children }: React.PropsWithChildren) => {
|
export const ApolloProvider = ({ children }: React.PropsWithChildren) => {
|
||||||
const apolloClient = useApolloFactory();
|
const apolloClient = useApolloFactory({
|
||||||
|
connectToDevTools: true,
|
||||||
|
});
|
||||||
|
|
||||||
// This will attach the right apollo client to Apollo Dev Tools
|
// This will attach the right apollo client to Apollo Dev Tools
|
||||||
window.__APOLLO_CLIENT__ = apolloClient;
|
window.__APOLLO_CLIENT__ = apolloClient;
|
||||||
|
|||||||
@ -10,6 +10,7 @@ export const ApolloMetadataClientProvider = ({
|
|||||||
}) => {
|
}) => {
|
||||||
const apolloMetadataClient = useApolloFactory({
|
const apolloMetadataClient = useApolloFactory({
|
||||||
uri: `${REACT_APP_SERVER_BASE_URL}/metadata`,
|
uri: `${REACT_APP_SERVER_BASE_URL}/metadata`,
|
||||||
|
connectToDevTools: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@ -8,6 +8,7 @@ import { getRecordConnectionFromRecords } from '@/object-record/cache/utils/getR
|
|||||||
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
|
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
|
||||||
import { FieldMetadataType } from '~/generated-metadata/graphql';
|
import { FieldMetadataType } from '~/generated-metadata/graphql';
|
||||||
import { isDefined } from '~/utils/isDefined';
|
import { isDefined } from '~/utils/isDefined';
|
||||||
|
import { lowerAndCapitalize } from '~/utils/string/lowerAndCapitalize';
|
||||||
|
|
||||||
export const getRecordNodeFromRecord = <T extends ObjectRecord>({
|
export const getRecordNodeFromRecord = <T extends ObjectRecord>({
|
||||||
objectMetadataItems,
|
objectMetadataItems,
|
||||||
@ -92,46 +93,61 @@ export const getRecordNodeFromRecord = <T extends ObjectRecord>({
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
if (field.type === 'RELATION') {
|
switch (field.type) {
|
||||||
if (
|
case FieldMetadataType.Relation: {
|
||||||
isUndefined(
|
if (
|
||||||
|
isUndefined(
|
||||||
|
field.relationDefinition?.targetObjectMetadata.nameSingular,
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isNull(value)) {
|
||||||
|
return [fieldName, null];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isUndefined(value?.id)) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const typeName = getObjectTypename(
|
||||||
field.relationDefinition?.targetObjectMetadata.nameSingular,
|
field.relationDefinition?.targetObjectMetadata.nameSingular,
|
||||||
)
|
);
|
||||||
) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isNull(value)) {
|
if (computeReferences) {
|
||||||
return [fieldName, null];
|
return [
|
||||||
}
|
fieldName,
|
||||||
|
{
|
||||||
|
__ref: `${typeName}:${value.id}`,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
if (isUndefined(value?.id)) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
const typeName = getObjectTypename(
|
|
||||||
field.relationDefinition?.targetObjectMetadata.nameSingular,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (computeReferences) {
|
|
||||||
return [
|
return [
|
||||||
fieldName,
|
fieldName,
|
||||||
{
|
{
|
||||||
__ref: `${typeName}:${value.id}`,
|
__typename: typeName,
|
||||||
|
...value,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
case FieldMetadataType.Link:
|
||||||
return [
|
case FieldMetadataType.Address:
|
||||||
fieldName,
|
case FieldMetadataType.FullName:
|
||||||
{
|
case FieldMetadataType.Currency: {
|
||||||
__typename: typeName,
|
return [
|
||||||
...value,
|
fieldName,
|
||||||
},
|
{
|
||||||
];
|
...value,
|
||||||
|
__typename: lowerAndCapitalize(field.type),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
default: {
|
||||||
|
return [fieldName, value];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return [fieldName, value];
|
|
||||||
})
|
})
|
||||||
.filter(isDefined),
|
.filter(isDefined),
|
||||||
) as T; // Todo fix typing once we have investigated apollo edges / nodes removal
|
) as T; // Todo fix typing once we have investigated apollo edges / nodes removal
|
||||||
|
|||||||
@ -5,11 +5,5 @@ import { useCurrencyField } from '../../hooks/useCurrencyField';
|
|||||||
export const CurrencyFieldDisplay = () => {
|
export const CurrencyFieldDisplay = () => {
|
||||||
const { fieldValue } = useCurrencyField();
|
const { fieldValue } = useCurrencyField();
|
||||||
|
|
||||||
return (
|
return <CurrencyDisplay currencyValue={fieldValue} />;
|
||||||
<CurrencyDisplay
|
|
||||||
amount={
|
|
||||||
fieldValue?.amountMicros ? fieldValue.amountMicros / 1000000 : null
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|||||||
@ -23,7 +23,7 @@ export const SettingsObjectFieldCurrencyForm = ({
|
|||||||
<Select
|
<Select
|
||||||
fullWidth
|
fullWidth
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
label="Unit"
|
label="Default Unit"
|
||||||
dropdownId="currency-unit-select"
|
dropdownId="currency-unit-select"
|
||||||
value={values.currencyCode}
|
value={values.currencyCode}
|
||||||
options={Object.entries(SETTINGS_FIELD_CURRENCY_CODES).map(
|
options={Object.entries(SETTINGS_FIELD_CURRENCY_CODES).map(
|
||||||
|
|||||||
@ -1,10 +1,51 @@
|
|||||||
|
import { useTheme } from '@emotion/react';
|
||||||
|
import styled from '@emotion/styled';
|
||||||
|
|
||||||
|
import { FieldCurrencyValue } from '@/object-record/record-field/types/FieldMetadata';
|
||||||
|
import { SETTINGS_FIELD_CURRENCY_CODES } from '@/settings/data-model/constants/SettingsFieldCurrencyCodes';
|
||||||
|
import { formatAmount } from '~/utils/format/formatAmount';
|
||||||
|
import { isDefined } from '~/utils/isDefined';
|
||||||
|
|
||||||
import { EllipsisDisplay } from './EllipsisDisplay';
|
import { EllipsisDisplay } from './EllipsisDisplay';
|
||||||
|
|
||||||
type CurrencyDisplayProps = {
|
type CurrencyDisplayProps = {
|
||||||
amount?: number | null;
|
currencyValue: FieldCurrencyValue | null | undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
// TODO: convert currencyCode to currency symbol
|
const StyledEllipsisDisplay = styled(EllipsisDisplay)`
|
||||||
export const CurrencyDisplay = ({ amount }: CurrencyDisplayProps) => {
|
align-items: center;
|
||||||
return <EllipsisDisplay>{amount}</EllipsisDisplay>;
|
display: flex;
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const CurrencyDisplay = ({ currencyValue }: CurrencyDisplayProps) => {
|
||||||
|
const theme = useTheme();
|
||||||
|
|
||||||
|
const shouldDisplayCurrency = isDefined(currencyValue?.currencyCode);
|
||||||
|
|
||||||
|
const CurrencyIcon = isDefined(currencyValue?.currencyCode)
|
||||||
|
? SETTINGS_FIELD_CURRENCY_CODES[currencyValue?.currencyCode]?.Icon
|
||||||
|
: null;
|
||||||
|
|
||||||
|
const amountToDisplay = isDefined(currencyValue?.amountMicros)
|
||||||
|
? currencyValue.amountMicros / 1000000
|
||||||
|
: 0;
|
||||||
|
|
||||||
|
if (!shouldDisplayCurrency) {
|
||||||
|
return <StyledEllipsisDisplay>{0}</StyledEllipsisDisplay>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<StyledEllipsisDisplay>
|
||||||
|
{isDefined(CurrencyIcon) && (
|
||||||
|
<>
|
||||||
|
<CurrencyIcon
|
||||||
|
color={theme.font.color.primary}
|
||||||
|
size={theme.icon.size.md}
|
||||||
|
stroke={theme.icon.stroke.sm}
|
||||||
|
/>{' '}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{amountToDisplay !== 0 ? formatAmount(amountToDisplay) : ''}
|
||||||
|
</StyledEllipsisDisplay>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -11,11 +11,15 @@ const StyledEllipsisDisplay = styled.div<{ maxWidth?: number }>`
|
|||||||
type EllipsisDisplayProps = {
|
type EllipsisDisplayProps = {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
maxWidth?: number;
|
maxWidth?: number;
|
||||||
|
className?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const EllipsisDisplay = ({
|
export const EllipsisDisplay = ({
|
||||||
children,
|
children,
|
||||||
maxWidth,
|
maxWidth,
|
||||||
|
className,
|
||||||
}: EllipsisDisplayProps) => (
|
}: EllipsisDisplayProps) => (
|
||||||
<StyledEllipsisDisplay style={{ maxWidth }}>{children}</StyledEllipsisDisplay>
|
<StyledEllipsisDisplay style={{ maxWidth }} className={className}>
|
||||||
|
{children}
|
||||||
|
</StyledEllipsisDisplay>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -26,15 +26,18 @@ export const useGetViewFromCache = () => {
|
|||||||
|
|
||||||
const view = {
|
const view = {
|
||||||
...viewWithConnections,
|
...viewWithConnections,
|
||||||
viewFilters: viewWithConnections.viewFilters?.edges.map(
|
viewFilters:
|
||||||
(edge: ObjectRecordEdge) => edge.node,
|
viewWithConnections.viewFilters?.edges?.map(
|
||||||
),
|
(edge: ObjectRecordEdge) => edge.node,
|
||||||
viewSorts: viewWithConnections.viewSorts?.edges.map(
|
) ?? [],
|
||||||
(edge: ObjectRecordEdge) => edge.node,
|
viewSorts:
|
||||||
),
|
viewWithConnections.viewSorts?.edges?.map(
|
||||||
viewFields: viewWithConnections.viewFields?.edges.map(
|
(edge: ObjectRecordEdge) => edge.node,
|
||||||
(edge: ObjectRecordEdge) => edge.node,
|
) ?? [],
|
||||||
),
|
viewFields:
|
||||||
|
viewWithConnections.viewFields?.edges?.map(
|
||||||
|
(edge: ObjectRecordEdge) => edge.node,
|
||||||
|
) ?? [],
|
||||||
} as GraphQLView;
|
} as GraphQLView;
|
||||||
|
|
||||||
return view;
|
return view;
|
||||||
|
|||||||
@ -1,28 +1,28 @@
|
|||||||
import { amountFormat } from '../amountFormat';
|
import { formatAmount } from '../formatAmount';
|
||||||
|
|
||||||
describe('amountFormat', () => {
|
describe('amountFormat', () => {
|
||||||
it('formats numbers less than 1000 correctly', () => {
|
it('formats numbers less than 1000 correctly', () => {
|
||||||
expect(amountFormat(500)).toBe('500');
|
expect(formatAmount(500)).toBe('500');
|
||||||
expect(amountFormat(123.456)).toBe('123.5');
|
expect(formatAmount(123.456)).toBe('123.5');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('formats numbers between 1000 and 999999 correctly', () => {
|
it('formats numbers between 1000 and 999999 correctly', () => {
|
||||||
expect(amountFormat(1500)).toBe('1.5k');
|
expect(formatAmount(1500)).toBe('1.5k');
|
||||||
expect(amountFormat(789456)).toBe('789.5k');
|
expect(formatAmount(789456)).toBe('789.5k');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('formats numbers between 1000000 and 999999999 correctly', () => {
|
it('formats numbers between 1000000 and 999999999 correctly', () => {
|
||||||
expect(amountFormat(2000000)).toBe('2m');
|
expect(formatAmount(2000000)).toBe('2m');
|
||||||
expect(amountFormat(654987654)).toBe('655m');
|
expect(formatAmount(654987654)).toBe('655m');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('formats numbers greater than or equal to 1000000000 correctly', () => {
|
it('formats numbers greater than or equal to 1000000000 correctly', () => {
|
||||||
expect(amountFormat(1200000000)).toBe('1.2b');
|
expect(formatAmount(1200000000)).toBe('1.2b');
|
||||||
expect(amountFormat(987654321987)).toBe('987.7b');
|
expect(formatAmount(987654321987)).toBe('987.7b');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('handles numbers with decimal places correctly', () => {
|
it('handles numbers with decimal places correctly', () => {
|
||||||
expect(amountFormat(123.456)).toBe('123.5');
|
expect(formatAmount(123.456)).toBe('123.5');
|
||||||
expect(amountFormat(789.0123)).toBe('789');
|
expect(formatAmount(789.0123)).toBe('789');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -1,11 +0,0 @@
|
|||||||
export const amountFormat = (number: number) => {
|
|
||||||
if (number < 1000) {
|
|
||||||
return number.toFixed(1).replace(/\.?0+$/, '');
|
|
||||||
} else if (number < 1000000) {
|
|
||||||
return (number / 1000).toFixed(1).replace(/\.?0+$/, '') + 'k';
|
|
||||||
} else if (number < 1000000000) {
|
|
||||||
return (number / 1000000).toFixed(1).replace(/\.?0+$/, '') + 'm';
|
|
||||||
} else {
|
|
||||||
return (number / 1000000000).toFixed(1).replace(/\.?0+$/, '') + 'b';
|
|
||||||
}
|
|
||||||
};
|
|
||||||
11
packages/twenty-front/src/utils/format/formatAmount.ts
Normal file
11
packages/twenty-front/src/utils/format/formatAmount.ts
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
export const formatAmount = (amount: number) => {
|
||||||
|
if (amount < 1000) {
|
||||||
|
return amount.toFixed(1).replace(/\.?0+$/, '');
|
||||||
|
} else if (amount < 1000000) {
|
||||||
|
return (amount / 1000).toFixed(1).replace(/\.?0+$/, '') + 'k';
|
||||||
|
} else if (amount < 1000000000) {
|
||||||
|
return (amount / 1000000).toFixed(1).replace(/\.?0+$/, '') + 'm';
|
||||||
|
} else {
|
||||||
|
return (amount / 1000000000).toFixed(1).replace(/\.?0+$/, '') + 'b';
|
||||||
|
}
|
||||||
|
};
|
||||||
@ -0,0 +1,9 @@
|
|||||||
|
import { isNonEmptyString } from '@sniptt/guards';
|
||||||
|
|
||||||
|
export const lowerAndCapitalize = (stringToCapitalize: string) => {
|
||||||
|
if (!isNonEmptyString(stringToCapitalize)) return '';
|
||||||
|
|
||||||
|
const loweredString = stringToCapitalize.toLowerCase();
|
||||||
|
|
||||||
|
return loweredString[0].toUpperCase() + loweredString.slice(1);
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user