diff --git a/package.json b/package.json index db4fc8a7fd03..2bbe5f5608b0 100644 --- a/package.json +++ b/package.json @@ -111,6 +111,7 @@ "lodash.isequal": "^4.5.0", "lodash.isobject": "^3.0.2", "lodash.kebabcase": "^4.1.1", + "lodash.mapvalues": "^4.6.0", "lodash.merge": "^4.6.2", "lodash.omit": "^4.5.0", "lodash.pick": "^4.4.0", @@ -225,6 +226,7 @@ "@types/lodash.isequal": "^4.5.7", "@types/lodash.isobject": "^3.0.7", "@types/lodash.kebabcase": "^4.1.7", + "@types/lodash.mapvalues": "^4.6.9", "@types/lodash.snakecase": "^4.1.7", "@types/lodash.upperfirst": "^4.3.7", "@types/luxon": "^3.3.0", diff --git a/packages/twenty-front/src/modules/activities/calendar/components/Calendar.tsx b/packages/twenty-front/src/modules/activities/calendar/components/Calendar.tsx index 85d9c603d7a4..97ad87b4e9d6 100644 --- a/packages/twenty-front/src/modules/activities/calendar/components/Calendar.tsx +++ b/packages/twenty-front/src/modules/activities/calendar/components/Calendar.tsx @@ -1,8 +1,11 @@ import styled from '@emotion/styled'; -import { startOfMonth } from 'date-fns'; +import { format, getYear, startOfMonth } from 'date-fns'; +import mapValues from 'lodash.mapvalues'; import { CalendarMonthCard } from '@/activities/calendar/components/CalendarMonthCard'; import { sortCalendarEventsDesc } from '@/activities/calendar/utils/sortCalendarEvents'; +import { H3Title } from '@/ui/display/typography/components/H3Title'; +import { Section } from '@/ui/layout/section/components/Section'; import { mockedCalendarEvents } from '~/testing/mock-data/calendar'; import { groupArrayItemsBy } from '~/utils/array/groupArrayItemsBy'; import { sortDesc } from '~/utils/sort'; @@ -16,6 +19,10 @@ const StyledContainer = styled.div` width: 100%; `; +const StyledYear = styled.span` + color: ${({ theme }) => theme.font.color.light}; +`; + export const Calendar = () => { const sortedCalendarEvents = [...mockedCalendarEvents].sort( sortCalendarEventsDesc, @@ -27,18 +34,32 @@ export const Calendar = () => { const sortedMonthTimes = Object.keys(calendarEventsByMonthTime) .map(Number) .sort(sortDesc); + const monthTimesByYear = groupArrayItemsBy(sortedMonthTimes, getYear); + const lastMonthTimeByYear = mapValues(monthTimesByYear, (monthTimes = []) => + Math.max(...monthTimes), + ); return ( {sortedMonthTimes.map((monthTime) => { const monthCalendarEvents = calendarEventsByMonthTime[monthTime]; + const year = getYear(monthTime); + const isLastMonthOfYear = lastMonthTimeByYear[year] === monthTime; + const monthLabel = format(monthTime, 'MMMM'); return ( !!monthCalendarEvents?.length && ( - +
+ + {monthLabel} + {isLastMonthOfYear && {year}} + + } + /> + +
) ); })} diff --git a/packages/twenty-front/src/modules/object-metadata/constants/LabelIdentifierFieldMetadataTypes.ts b/packages/twenty-front/src/modules/object-metadata/constants/LabelIdentifierFieldMetadataTypes.ts new file mode 100644 index 000000000000..fe25319198b2 --- /dev/null +++ b/packages/twenty-front/src/modules/object-metadata/constants/LabelIdentifierFieldMetadataTypes.ts @@ -0,0 +1,6 @@ +import { FieldMetadataType } from '~/generated-metadata/graphql'; + +export const LABEL_IDENTIFIER_FIELD_METADATA_TYPES = [ + FieldMetadataType.Number, + FieldMetadataType.Text, +]; diff --git a/packages/twenty-front/src/modules/object-metadata/utils/getActiveFieldMetadataItems.ts b/packages/twenty-front/src/modules/object-metadata/utils/getActiveFieldMetadataItems.ts new file mode 100644 index 000000000000..e174c2b00489 --- /dev/null +++ b/packages/twenty-front/src/modules/object-metadata/utils/getActiveFieldMetadataItems.ts @@ -0,0 +1,9 @@ +import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; + +export const getActiveFieldMetadataItems = ( + objectMetadataItem: Pick, +) => + objectMetadataItem.fields.filter( + (fieldMetadataItem) => + fieldMetadataItem.isActive && !fieldMetadataItem.isSystem, + ); diff --git a/packages/twenty-front/src/modules/object-metadata/utils/getDisabledFieldMetadataItems.ts b/packages/twenty-front/src/modules/object-metadata/utils/getDisabledFieldMetadataItems.ts new file mode 100644 index 000000000000..52f781ccc203 --- /dev/null +++ b/packages/twenty-front/src/modules/object-metadata/utils/getDisabledFieldMetadataItems.ts @@ -0,0 +1,9 @@ +import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; + +export const getDisabledFieldMetadataItems = ( + objectMetadataItem: Pick, +) => + objectMetadataItem.fields.filter( + (fieldMetadataItem) => + !fieldMetadataItem.isActive && !fieldMetadataItem.isSystem, + ); diff --git a/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/CurrencyFieldInput.tsx b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/CurrencyFieldInput.tsx index 5ab4617eddb8..eff716fb142e 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/CurrencyFieldInput.tsx +++ b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/CurrencyFieldInput.tsx @@ -1,5 +1,5 @@ import { CurrencyCode } from '@/object-record/record-field/types/CurrencyCode'; -import { TextInput } from '@/ui/field/input/components/TextInput'; +import { CurrencyInput } from '@/ui/field/input/components/CurrencyInput'; import { FieldInputOverlay } from '../../../../../ui/field/input/components/FieldInputOverlay'; import { useCurrencyField } from '../../hooks/useCurrencyField'; @@ -79,10 +79,18 @@ export const CurrencyFieldInput = ({ }); }; + const handleSelect = (newValue: string) => { + setDraftValue({ + amount: draftValue?.amount ?? '', + currencyCode: newValue as CurrencyCode, + }); + }; + return ( - ); diff --git a/packages/twenty-front/src/modules/object-record/record-field/utils/computeDraftValueFromFieldValue.ts b/packages/twenty-front/src/modules/object-record/record-field/utils/computeDraftValueFromFieldValue.ts index ee11e822cbfa..72c024f7afe9 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/utils/computeDraftValueFromFieldValue.ts +++ b/packages/twenty-front/src/modules/object-record/record-field/utils/computeDraftValueFromFieldValue.ts @@ -1,4 +1,3 @@ -import { CurrencyCode } from '@/object-record/record-field/types/CurrencyCode'; import { FieldDefinition } from '@/object-record/record-field/types/FieldDefinition'; import { FieldInputDraftValue } from '@/object-record/record-field/types/FieldInputDraftValue'; import { FieldMetadata } from '@/object-record/record-field/types/FieldMetadata'; @@ -31,7 +30,7 @@ export const computeDraftValueFromFieldValue = ({ return { amount: fieldValue?.amountMicros ? fieldValue.amountMicros / 1000000 : '', - currenyCode: CurrencyCode.USD, + currencyCode: fieldValue?.currencyCode ?? '', } as unknown as FieldInputDraftValue; } if (isFieldRelation(fieldDefinition)) { diff --git a/packages/twenty-front/src/modules/settings/data-model/components/SettingsDataModelCardTitle.tsx b/packages/twenty-front/src/modules/settings/data-model/components/SettingsDataModelCardTitle.tsx new file mode 100644 index 000000000000..9fbc11715a5f --- /dev/null +++ b/packages/twenty-front/src/modules/settings/data-model/components/SettingsDataModelCardTitle.tsx @@ -0,0 +1,11 @@ +import styled from '@emotion/styled'; + +const StyledTitle = styled.h3` + color: ${({ theme }) => theme.font.color.extraLight}; + font-size: ${({ theme }) => theme.font.size.sm}; + font-weight: ${({ theme }) => theme.font.weight.medium}; + margin: 0; + margin-bottom: ${({ theme }) => theme.spacing(4)}; +`; + +export { StyledTitle as SettingsDataModelCardTitle }; diff --git a/packages/twenty-front/src/modules/settings/data-model/objects/forms/components/SettingsDataModelObjectIdentifiersForm.tsx b/packages/twenty-front/src/modules/settings/data-model/objects/forms/components/SettingsDataModelObjectIdentifiersForm.tsx new file mode 100644 index 000000000000..6762b52693cf --- /dev/null +++ b/packages/twenty-front/src/modules/settings/data-model/objects/forms/components/SettingsDataModelObjectIdentifiersForm.tsx @@ -0,0 +1,101 @@ +import { useMemo } from 'react'; +import { Controller, useFormContext } from 'react-hook-form'; +import styled from '@emotion/styled'; +import { z } from 'zod'; + +import { LABEL_IDENTIFIER_FIELD_METADATA_TYPES } from '@/object-metadata/constants/LabelIdentifierFieldMetadataTypes'; +import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; +import { getActiveFieldMetadataItems } from '@/object-metadata/utils/getActiveFieldMetadataItems'; +import { objectMetadataItemSchema } from '@/object-metadata/validation-schemas/objectMetadataItemSchema'; +import { IconCircleOff } from '@/ui/display/icon'; +import { useIcons } from '@/ui/display/icon/hooks/useIcons'; +import { Select, SelectOption } from '@/ui/input/components/Select'; + +export const settingsDataModelObjectIdentifiersFormSchema = + objectMetadataItemSchema.pick({ + labelIdentifierFieldMetadataId: true, + imageIdentifierFieldMetadataId: true, + }); + +export type SettingsDataModelObjectIdentifiersFormValues = z.infer< + typeof settingsDataModelObjectIdentifiersFormSchema +>; + +type SettingsDataModelObjectIdentifiersFormProps = { + objectMetadataItem: ObjectMetadataItem; +}; + +const StyledContainer = styled.div` + display: flex; + gap: ${({ theme }) => theme.spacing(4)}; +`; + +export const SettingsDataModelObjectIdentifiersForm = ({ + objectMetadataItem, +}: SettingsDataModelObjectIdentifiersFormProps) => { + const { control } = + useFormContext(); + const { getIcon } = useIcons(); + + const labelIdentifierFieldOptions = useMemo( + () => + getActiveFieldMetadataItems(objectMetadataItem) + .filter( + ({ id, type }) => + LABEL_IDENTIFIER_FIELD_METADATA_TYPES.includes(type) || + objectMetadataItem.labelIdentifierFieldMetadataId === id, + ) + .map>((fieldMetadataItem) => ({ + Icon: getIcon(fieldMetadataItem.icon), + label: fieldMetadataItem.label, + value: fieldMetadataItem.id, + })), + [getIcon, objectMetadataItem], + ); + const imageIdentifierFieldOptions: SelectOption[] = []; + + const emptyOption: SelectOption = { + Icon: IconCircleOff, + label: 'None', + value: null, + }; + + return ( + + {[ + { + label: 'Record label', + fieldName: 'labelIdentifierFieldMetadataId' as const, + options: labelIdentifierFieldOptions, + }, + { + label: 'Record image', + fieldName: 'imageIdentifierFieldMetadataId' as const, + options: imageIdentifierFieldOptions, + }, + ].map(({ fieldName, label, options }) => ( + { + return ( +