diff --git a/packages/twenty-emails/package.json b/packages/twenty-emails/package.json index 8e0785846bce0..36af44f3da6eb 100644 --- a/packages/twenty-emails/package.json +++ b/packages/twenty-emails/package.json @@ -1,6 +1,6 @@ { "name": "twenty-emails", - "version": "0.24.0", + "version": "0.24.2", "description": "", "author": "", "private": true, diff --git a/packages/twenty-front/package.json b/packages/twenty-front/package.json index ed72dc01963c8..434934ed719dc 100644 --- a/packages/twenty-front/package.json +++ b/packages/twenty-front/package.json @@ -1,6 +1,6 @@ { "name": "twenty-front", - "version": "0.24.0", + "version": "0.24.2", "private": true, "type": "module", "scripts": { diff --git a/packages/twenty-front/src/index.css b/packages/twenty-front/src/index.css index 2d696388b83bb..808fd8917db4e 100644 --- a/packages/twenty-front/src/index.css +++ b/packages/twenty-front/src/index.css @@ -12,4 +12,4 @@ html { /* https://stackoverflow.com/questions/44543157/how-to-hide-the-google-invisible-recaptcha-badge */ .grecaptcha-badge { visibility: hidden !important; -} \ No newline at end of file +} diff --git a/packages/twenty-front/src/modules/activities/components/RichTextEditor.tsx b/packages/twenty-front/src/modules/activities/components/RichTextEditor.tsx index 32c344988140b..c6842395f64fd 100644 --- a/packages/twenty-front/src/modules/activities/components/RichTextEditor.tsx +++ b/packages/twenty-front/src/modules/activities/components/RichTextEditor.tsx @@ -256,10 +256,16 @@ export const RichTextEditor = ({ const handleBodyChangeDebounced = useDebouncedCallback(handleBodyChange, 500); + // See https://github.com/twentyhq/twenty/issues/6724 for explanation + const setActivityBodyDebouncedToAvoidDragBug = useDebouncedCallback( + setActivityBody, + 100, + ); + const handleEditorChange = () => { const newStringifiedBody = JSON.stringify(editor.document) ?? ''; - setActivityBody(newStringifiedBody); + setActivityBodyDebouncedToAvoidDragBug(newStringifiedBody); handleBodyChangeDebounced(newStringifiedBody); }; diff --git a/packages/twenty-front/src/modules/activities/tasks/components/TaskGroups.tsx b/packages/twenty-front/src/modules/activities/tasks/components/TaskGroups.tsx index 4503fd9913b34..a6e4999773bc7 100644 --- a/packages/twenty-front/src/modules/activities/tasks/components/TaskGroups.tsx +++ b/packages/twenty-front/src/modules/activities/tasks/components/TaskGroups.tsx @@ -91,22 +91,24 @@ export const TaskGroups = ({ ); } + const sortedTasksByStatus = Object.entries( + groupBy(tasks, ({ status }) => status), + ).toSorted(([statusA], [statusB]) => statusB.localeCompare(statusA)); + return ( - {Object.entries(groupBy(tasks, ({ status }) => status)).map( - ([status, tasksByStatus]: [string, Task[]]) => ( - - ) - } - /> - ), - )} + {sortedTasksByStatus.map(([status, tasksByStatus]: [string, Task[]]) => ( + + ) + } + /> + ))} ); }; diff --git a/packages/twenty-front/src/modules/activities/tasks/components/TaskRow.tsx b/packages/twenty-front/src/modules/activities/tasks/components/TaskRow.tsx index 798bb077d7972..52c1731db039e 100644 --- a/packages/twenty-front/src/modules/activities/tasks/components/TaskRow.tsx +++ b/packages/twenty-front/src/modules/activities/tasks/components/TaskRow.tsx @@ -36,6 +36,8 @@ const StyledTaskBody = styled.div` max-width: 100%; flex: 1; overflow: hidden; + + padding-bottom: ${({ theme }) => theme.spacing(0.25)}; `; const StyledTaskTitle = styled.div<{ @@ -44,10 +46,13 @@ const StyledTaskTitle = styled.div<{ color: ${({ theme }) => theme.font.color.primary}; font-weight: ${({ theme }) => theme.font.weight.medium}; padding: 0 ${({ theme }) => theme.spacing(2)}; + padding-bottom: ${({ theme }) => theme.spacing(0.25)}; text-decoration: ${({ completed }) => (completed ? 'line-through' : 'none')}; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; + + align-items: center; `; const StyledDueDate = styled.div<{ @@ -71,8 +76,10 @@ const StyledPlaceholder = styled.div` `; const StyledLeftSideContainer = styled.div` + align-items: center; display: flex; flex: 1; + overflow: hidden; `; diff --git a/packages/twenty-front/src/modules/activities/timelineActivities/components/EventRow.tsx b/packages/twenty-front/src/modules/activities/timelineActivities/components/EventRow.tsx index 902a7b728db16..e046316132df0 100644 --- a/packages/twenty-front/src/modules/activities/timelineActivities/components/EventRow.tsx +++ b/packages/twenty-front/src/modules/activities/timelineActivities/components/EventRow.tsx @@ -3,7 +3,8 @@ import { useContext } from 'react'; import { useRecoilValue } from 'recoil'; import { TimelineActivityContext } from '@/activities/timelineActivities/contexts/TimelineActivityContext'; -import { useLinkedObject } from '@/activities/timelineActivities/hooks/useLinkedObject'; + +import { useLinkedObjectObjectMetadataItem } from '@/activities/timelineActivities/hooks/useLinkedObjectObjectMetadataItem'; import { EventIconDynamicComponent } from '@/activities/timelineActivities/rows/components/EventIconDynamicComponent'; import { EventRowDynamicComponent } from '@/activities/timelineActivities/rows/components/EventRowDynamicComponent'; import { TimelineActivity } from '@/activities/timelineActivities/types/TimelineActivity'; @@ -14,6 +15,7 @@ import { beautifyPastDateRelativeToNow } from '~/utils/date-utils'; import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull'; const StyledTimelineItemContainer = styled.div` + color: ${({ theme }) => theme.font.color.primary}; display: flex; gap: ${({ theme }) => theme.spacing(4)}; height: 'auto'; @@ -99,7 +101,7 @@ export const EventRow = ({ const { labelIdentifierValue } = useContext(TimelineActivityContext); const beautifiedCreatedAt = beautifyPastDateRelativeToNow(event.createdAt); - const linkedObjectMetadataItem = useLinkedObject( + const linkedObjectMetadataItem = useLinkedObjectObjectMetadataItem( event.linkedObjectMetadataId, ); diff --git a/packages/twenty-front/src/modules/activities/timelineActivities/components/TimelineActivities.tsx b/packages/twenty-front/src/modules/activities/timelineActivities/components/TimelineActivities.tsx index b94921254ff9f..bbda464681f91 100644 --- a/packages/twenty-front/src/modules/activities/timelineActivities/components/TimelineActivities.tsx +++ b/packages/twenty-front/src/modules/activities/timelineActivities/components/TimelineActivities.tsx @@ -46,7 +46,7 @@ export const TimelineActivities = ({ const isTimelineActivitiesEmpty = !timelineActivities || timelineActivities.length === 0; - if (loading && isTimelineActivitiesEmpty) { + if (loading) { return ; } diff --git a/packages/twenty-front/src/modules/activities/timelineActivities/hooks/useLinkedObject.ts b/packages/twenty-front/src/modules/activities/timelineActivities/hooks/useLinkedObjectObjectMetadataItem.ts similarity index 86% rename from packages/twenty-front/src/modules/activities/timelineActivities/hooks/useLinkedObject.ts rename to packages/twenty-front/src/modules/activities/timelineActivities/hooks/useLinkedObjectObjectMetadataItem.ts index b628b56907d5f..9f230bef62b94 100644 --- a/packages/twenty-front/src/modules/activities/timelineActivities/hooks/useLinkedObject.ts +++ b/packages/twenty-front/src/modules/activities/timelineActivities/hooks/useLinkedObjectObjectMetadataItem.ts @@ -3,7 +3,7 @@ import { useRecoilValue } from 'recoil'; import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState'; import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; -export const useLinkedObject = (id: string) => { +export const useLinkedObjectObjectMetadataItem = (id: string) => { const objectMetadataItems: ObjectMetadataItem[] = useRecoilValue( objectMetadataItemsState, ); diff --git a/packages/twenty-front/src/modules/activities/timelineActivities/hooks/useLinkedObjectsTitle.ts b/packages/twenty-front/src/modules/activities/timelineActivities/hooks/useLinkedObjectsTitle.ts new file mode 100644 index 0000000000000..2df7bc03a4c25 --- /dev/null +++ b/packages/twenty-front/src/modules/activities/timelineActivities/hooks/useLinkedObjectsTitle.ts @@ -0,0 +1,43 @@ +import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; +import { useCombinedFindManyRecords } from '@/object-record/multiple-objects/hooks/useCombinedFindManyRecords'; +import { isNonEmptyArray } from '@sniptt/guards'; + +export const useLinkedObjectsTitle = (linkedObjectIds: string[]) => { + const { loading } = useCombinedFindManyRecords({ + skip: !isNonEmptyArray(linkedObjectIds), + operationSignatures: [ + { + objectNameSingular: CoreObjectNameSingular.Task, + variables: { + filter: { + id: { + in: linkedObjectIds ?? [], + }, + }, + }, + fields: { + id: true, + title: true, + }, + }, + { + objectNameSingular: CoreObjectNameSingular.Note, + variables: { + filter: { + id: { + in: linkedObjectIds ?? [], + }, + }, + }, + fields: { + id: true, + title: true, + }, + }, + ], + }); + + return { + loading, + }; +}; diff --git a/packages/twenty-front/src/modules/activities/timelineActivities/hooks/useTimelineActivities.tsx b/packages/twenty-front/src/modules/activities/timelineActivities/hooks/useTimelineActivities.ts similarity index 71% rename from packages/twenty-front/src/modules/activities/timelineActivities/hooks/useTimelineActivities.tsx rename to packages/twenty-front/src/modules/activities/timelineActivities/hooks/useTimelineActivities.ts index 843586660bf3f..fb65053c151ce 100644 --- a/packages/twenty-front/src/modules/activities/timelineActivities/hooks/useTimelineActivities.tsx +++ b/packages/twenty-front/src/modules/activities/timelineActivities/hooks/useTimelineActivities.ts @@ -1,3 +1,4 @@ +import { useLinkedObjectsTitle } from '@/activities/timelineActivities/hooks/useLinkedObjectsTitle'; import { TimelineActivity } from '@/activities/timelineActivities/types/TimelineActivity'; import { ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity'; import { getActivityTargetObjectFieldIdName } from '@/activities/utils/getActivityTargetObjectFieldIdName'; @@ -19,10 +20,10 @@ export const useTimelineActivities = ( }); const { - records: TimelineActivities, - loading, + records: timelineActivities, + loading: loadingTimelineActivities, fetchMoreRecords, - } = useFindManyRecords({ + } = useFindManyRecords({ objectNameSingular: CoreObjectNameSingular.TimelineActivity, filter: { [targetableObjectFieldIdName]: { @@ -38,8 +39,17 @@ export const useTimelineActivities = ( fetchPolicy: 'cache-and-network', }); + const activityIds = timelineActivities + .filter((timelineActivity) => timelineActivity.name.match(/note|task/i)) + .map((timelineActivity) => timelineActivity.linkedRecordId); + + const { loading: loadingLinkedObjectsTitle } = + useLinkedObjectsTitle(activityIds); + + const loading = loadingTimelineActivities || loadingLinkedObjectsTitle; + return { - timelineActivities: TimelineActivities as TimelineActivity[], + timelineActivities, loading, fetchMoreRecords, }; diff --git a/packages/twenty-front/src/modules/activities/timelineActivities/rows/activity/components/EventRowActivity.tsx b/packages/twenty-front/src/modules/activities/timelineActivities/rows/activity/components/EventRowActivity.tsx index 30c13a7214ba0..1c1f34e43ade7 100644 --- a/packages/twenty-front/src/modules/activities/timelineActivities/rows/activity/components/EventRowActivity.tsx +++ b/packages/twenty-front/src/modules/activities/timelineActivities/rows/activity/components/EventRowActivity.tsx @@ -1,5 +1,4 @@ import styled from '@emotion/styled'; -import { useRecoilState } from 'recoil'; import { useOpenActivityRightDrawer } from '@/activities/hooks/useOpenActivityRightDrawer'; import { @@ -8,15 +7,21 @@ import { StyledEventRowItemColumn, } from '@/activities/timelineActivities/rows/components/EventRowDynamicComponent'; import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; -import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState'; +import { useGetRecordFromCache } from '@/object-record/cache/hooks/useGetRecordFromCache'; +import { isNonEmptyString } from '@sniptt/guards'; type EventRowActivityProps = EventRowDynamicComponentProps; const StyledLinkedActivity = styled.span` + color: ${({ theme }) => theme.font.color.primary}; cursor: pointer; text-decoration: underline; `; +export const StyledEventRowItemText = styled.span` + color: ${({ theme }) => theme.font.color.primary}; +`; + export const EventRowActivity = ({ event, authorFullName, @@ -30,9 +35,21 @@ export const EventRowActivity = ({ throw new Error('Could not find linked record id for event'); } - const [activityInStore] = useRecoilState( - recordStoreFamilyState(event.linkedRecordId), - ); + const getActivityFromCache = useGetRecordFromCache({ + objectNameSingular, + recordGqlFields: { + id: true, + title: true, + }, + }); + + const activityInStore = getActivityFromCache(event.linkedRecordId); + + const activityTitle = isNonEmptyString(activityInStore?.title) + ? activityInStore?.title + : isNonEmptyString(event.linkedRecordCachedName) + ? event.linkedRecordCachedName + : 'Untitled'; const openActivityRightDrawer = useOpenActivityRightDrawer({ objectNameSingular, @@ -44,15 +61,11 @@ export const EventRowActivity = ({ {`${eventAction} a related ${eventObject}`} - {activityInStore ? ( - openActivityRightDrawer(event.linkedRecordId)} - > - {event.linkedRecordCachedName} - - ) : ( - {event.linkedRecordCachedName} - )} + openActivityRightDrawer(event.linkedRecordId)} + > + {activityTitle} + ); }; diff --git a/packages/twenty-front/src/modules/activities/timelineActivities/types/TimelineActivityLinkedObject.ts b/packages/twenty-front/src/modules/activities/timelineActivities/types/TimelineActivityLinkedObject.ts new file mode 100644 index 0000000000000..9fd28f828f435 --- /dev/null +++ b/packages/twenty-front/src/modules/activities/timelineActivities/types/TimelineActivityLinkedObject.ts @@ -0,0 +1 @@ +export type TimelineActivityLinkedObject = 'note' | 'task'; diff --git a/packages/twenty-front/src/modules/activities/timelineActivities/utils/filterTimelineActivityByLinkedObjectTypes.ts b/packages/twenty-front/src/modules/activities/timelineActivities/utils/filterTimelineActivityByLinkedObjectTypes.ts new file mode 100644 index 0000000000000..455ceca01c0a0 --- /dev/null +++ b/packages/twenty-front/src/modules/activities/timelineActivities/utils/filterTimelineActivityByLinkedObjectTypes.ts @@ -0,0 +1,12 @@ +import { TimelineActivity } from '@/activities/timelineActivities/types/TimelineActivity'; +import { TimelineActivityLinkedObject } from '@/activities/timelineActivities/types/TimelineActivityLinkedObject'; + +export const filterTimelineActivityByLinkedObjectTypes = + (linkedObjectTypes: TimelineActivityLinkedObject[]) => + (timelineActivity: TimelineActivity) => { + return linkedObjectTypes.some((linkedObjectType) => { + const linkedObjectPartInName = timelineActivity.name.split('.')[0]; + + return linkedObjectPartInName.includes(linkedObjectType); + }); + }; diff --git a/packages/twenty-front/src/modules/object-record/multiple-objects/hooks/useCombinedFindManyRecords.ts b/packages/twenty-front/src/modules/object-record/multiple-objects/hooks/useCombinedFindManyRecords.ts index 95385c3777222..b147b5535f711 100644 --- a/packages/twenty-front/src/modules/object-record/multiple-objects/hooks/useCombinedFindManyRecords.ts +++ b/packages/twenty-front/src/modules/object-record/multiple-objects/hooks/useCombinedFindManyRecords.ts @@ -11,13 +11,13 @@ export const useCombinedFindManyRecords = ({ skip = false, }: { operationSignatures: RecordGqlOperationSignature[]; - skip: boolean; + skip?: boolean; }) => { const findManyQuery = useGenerateCombinedFindManyRecordsQuery({ operationSignatures, }); - const { data } = useQuery( + const { data, loading } = useQuery( findManyQuery ?? EMPTY_QUERY, { skip, @@ -35,5 +35,6 @@ export const useCombinedFindManyRecords = ({ return { result: resultWithoutConnection, + loading, }; }; diff --git a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownFilterSelect.tsx b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownFilterSelect.tsx index aa402d2606fdf..caf6db6212bff 100644 --- a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownFilterSelect.tsx +++ b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownFilterSelect.tsx @@ -1,15 +1,18 @@ import styled from '@emotion/styled'; import { useState } from 'react'; import { useRecoilValue } from 'recoil'; -import { useIcons } from 'twenty-ui'; import { useFilterDropdown } from '@/object-record/object-filter-dropdown/hooks/useFilterDropdown'; -import { RelationPickerHotkeyScope } from '@/object-record/relation-picker/types/RelationPickerHotkeyScope'; import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer'; -import { MenuItem } from '@/ui/navigation/menu-item/components/MenuItem'; -import { useSetHotkeyScope } from '@/ui/utilities/hotkey/hooks/useSetHotkeyScope'; -import { getOperandsForFilterType } from '../utils/getOperandsForFilterType'; +import { ObjectFilterDropdownFilterSelectMenuItem } from '@/object-record/object-filter-dropdown/components/ObjectFilterDropdownFilterSelectMenuItem'; +import { OBJECT_FILTER_DROPDOWN_ID } from '@/object-record/object-filter-dropdown/constants/ObjectFilterDropdownId'; +import { useSelectFilter } from '@/object-record/object-filter-dropdown/hooks/useSelectFilter'; +import { FiltersHotkeyScope } from '@/object-record/object-filter-dropdown/types/FiltersHotkeyScope'; +import { SelectableItem } from '@/ui/layout/selectable-list/components/SelectableItem'; +import { SelectableList } from '@/ui/layout/selectable-list/components/SelectableList'; +import { useSelectableList } from '@/ui/layout/selectable-list/hooks/useSelectableList'; +import { isDefined } from 'twenty-ui'; export const StyledInput = styled.input` background: transparent; @@ -39,20 +42,40 @@ export const StyledInput = styled.input` export const ObjectFilterDropdownFilterSelect = () => { const [searchText, setSearchText] = useState(''); - const { - setFilterDefinitionUsedInDropdown, - setSelectedOperandInDropdown, - setObjectFilterDropdownSearchInput, - availableFilterDefinitionsState, - } = useFilterDropdown(); + + const { availableFilterDefinitionsState } = useFilterDropdown(); const availableFilterDefinitions = useRecoilValue( availableFilterDefinitionsState, ); - const { getIcon } = useIcons(); + const sortedAvailableFilterDefinitions = [...availableFilterDefinitions] + .sort((a, b) => a.label.localeCompare(b.label)) + .filter((item) => + item.label.toLocaleLowerCase().includes(searchText.toLocaleLowerCase()), + ); + + const selectableListItemIds = sortedAvailableFilterDefinitions.map( + (item) => item.fieldMetadataId, + ); + + const { selectFilter } = useSelectFilter(); + + const { resetSelectedItem } = useSelectableList(OBJECT_FILTER_DROPDOWN_ID); + + const handleEnter = (itemId: string) => { + const selectedFilterDefinition = sortedAvailableFilterDefinitions.find( + (item) => item.fieldMetadataId === itemId, + ); + + if (!isDefined(selectedFilterDefinition)) { + return; + } + + resetSelectedItem(); - const setHotkeyScope = useSetHotkeyScope(); + selectFilter({ filterDefinition: selectedFilterDefinition }); + }; return ( <> @@ -64,39 +87,27 @@ export const ObjectFilterDropdownFilterSelect = () => { setSearchText(event.target.value) } /> - - {[...availableFilterDefinitions] - .sort((a, b) => a.label.localeCompare(b.label)) - .filter((item) => - item.label - .toLocaleLowerCase() - .includes(searchText.toLocaleLowerCase()), - ) - .map((availableFilterDefinition, index) => ( - { - setFilterDefinitionUsedInDropdown(availableFilterDefinition); - - if ( - availableFilterDefinition.type === 'RELATION' || - availableFilterDefinition.type === 'SELECT' - ) { - setHotkeyScope(RelationPickerHotkeyScope.RelationPicker); - } - - setSelectedOperandInDropdown( - getOperandsForFilterType(availableFilterDefinition.type)?.[0], - ); - - setObjectFilterDropdownSearchInput(''); - }} - LeftIcon={getIcon(availableFilterDefinition.iconName)} - text={availableFilterDefinition.label} - /> - ))} - + + + {sortedAvailableFilterDefinitions.map( + (availableFilterDefinition, index) => ( + + + + ), + )} + + ); }; diff --git a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownFilterSelectMenuItem.tsx b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownFilterSelectMenuItem.tsx new file mode 100644 index 0000000000000..eb04750c9b8ca --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownFilterSelectMenuItem.tsx @@ -0,0 +1,43 @@ +import { OBJECT_FILTER_DROPDOWN_ID } from '@/object-record/object-filter-dropdown/constants/ObjectFilterDropdownId'; +import { useSelectFilter } from '@/object-record/object-filter-dropdown/hooks/useSelectFilter'; +import { FilterDefinition } from '@/object-record/object-filter-dropdown/types/FilterDefinition'; +import { useSelectableList } from '@/ui/layout/selectable-list/hooks/useSelectableList'; +import { MenuItemSelect } from '@/ui/navigation/menu-item/components/MenuItemSelect'; +import { useRecoilValue } from 'recoil'; +import { useIcons } from 'twenty-ui'; + +export type ObjectFilterDropdownFilterSelectMenuItemProps = { + filterDefinition: FilterDefinition; +}; + +export const ObjectFilterDropdownFilterSelectMenuItem = ({ + filterDefinition, +}: ObjectFilterDropdownFilterSelectMenuItemProps) => { + const { selectFilter } = useSelectFilter(); + + const { isSelectedItemIdSelector, resetSelectedItem } = useSelectableList( + OBJECT_FILTER_DROPDOWN_ID, + ); + + const isSelectedItem = useRecoilValue( + isSelectedItemIdSelector(filterDefinition.fieldMetadataId), + ); + + const { getIcon } = useIcons(); + + const handleClick = () => { + resetSelectedItem(); + + selectFilter({ filterDefinition }); + }; + + return ( + + ); +}; diff --git a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownOptionSelect.tsx b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownOptionSelect.tsx index 9ced7ee64e8a9..213f828cfa0b4 100644 --- a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownOptionSelect.tsx +++ b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownOptionSelect.tsx @@ -41,7 +41,7 @@ export const ObjectFilterDropdownOptionSelect = () => { selectableListScopeId: MULTI_OBJECT_RECORD_SELECT_SELECTABLE_LIST_ID, }); - const { handleResetSelectedPosition } = useSelectableList( + const { resetSelectedItem } = useSelectableList( MULTI_OBJECT_RECORD_SELECT_SELECTABLE_LIST_ID, ); @@ -90,10 +90,10 @@ export const ObjectFilterDropdownOptionSelect = () => { [Key.Escape], () => { closeDropdown(); - handleResetSelectedPosition(); + resetSelectedItem(); }, RelationPickerHotkeyScope.RelationPicker, - [closeDropdown, handleResetSelectedPosition], + [closeDropdown, resetSelectedItem], ); const handleMultipleOptionSelectChange = ( @@ -137,7 +137,7 @@ export const ObjectFilterDropdownOptionSelect = () => { value: newFilterValue, }); } - handleResetSelectedPosition(); + resetSelectedItem(); }; const optionsInDropdown = selectableOptions?.filter((option) => diff --git a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/hooks/useSelectFilter.ts b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/hooks/useSelectFilter.ts new file mode 100644 index 0000000000000..3954d8d6dc9f9 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/hooks/useSelectFilter.ts @@ -0,0 +1,40 @@ +import { useFilterDropdown } from '@/object-record/object-filter-dropdown/hooks/useFilterDropdown'; +import { FilterDefinition } from '@/object-record/object-filter-dropdown/types/FilterDefinition'; +import { getOperandsForFilterType } from '@/object-record/object-filter-dropdown/utils/getOperandsForFilterType'; +import { RelationPickerHotkeyScope } from '@/object-record/relation-picker/types/RelationPickerHotkeyScope'; +import { useSetHotkeyScope } from '@/ui/utilities/hotkey/hooks/useSetHotkeyScope'; + +type SelectFilterParams = { + filterDefinition: FilterDefinition; +}; + +export const useSelectFilter = () => { + const { + setFilterDefinitionUsedInDropdown, + setSelectedOperandInDropdown, + setObjectFilterDropdownSearchInput, + } = useFilterDropdown(); + + const setHotkeyScope = useSetHotkeyScope(); + + const selectFilter = ({ filterDefinition }: SelectFilterParams) => { + setFilterDefinitionUsedInDropdown(filterDefinition); + + if ( + filterDefinition.type === 'RELATION' || + filterDefinition.type === 'SELECT' + ) { + setHotkeyScope(RelationPickerHotkeyScope.RelationPicker); + } + + setSelectedOperandInDropdown( + getOperandsForFilterType(filterDefinition.type)?.[0], + ); + + setObjectFilterDropdownSearchInput(''); + }; + + return { + selectFilter, + }; +}; diff --git a/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/MultiSelectFieldInput.tsx b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/MultiSelectFieldInput.tsx index c6134f911a2dc..e95eeb5e21bdf 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/MultiSelectFieldInput.tsx +++ b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/MultiSelectFieldInput.tsx @@ -33,7 +33,7 @@ export const MultiSelectFieldInput = ({ const { selectedItemIdState } = useSelectableListStates({ selectableListScopeId: MULTI_OBJECT_RECORD_SELECT_SELECTABLE_LIST_ID, }); - const { handleResetSelectedPosition } = useSelectableList( + const { resetSelectedItem } = useSelectableList( MULTI_OBJECT_RECORD_SELECT_SELECTABLE_LIST_ID, ); const { persistField, fieldDefinition, fieldValues, hotkeyScope } = @@ -46,7 +46,9 @@ export const MultiSelectFieldInput = ({ fieldValues?.includes(option.value), ); - const optionsInDropDown = fieldDefinition.metadata.options; + const filteredOptionsInDropDown = fieldDefinition.metadata.options.filter( + (option) => option.label.toLowerCase().includes(searchFilter.toLowerCase()), + ); const formatNewSelectedOptions = (value: string) => { const selectedOptionsValues = selectedOptions.map( @@ -65,10 +67,10 @@ export const MultiSelectFieldInput = ({ Key.Escape, () => { onCancel?.(); - handleResetSelectedPosition(); + resetSelectedItem(); }, hotkeyScope, - [onCancel, handleResetSelectedPosition], + [onCancel, resetSelectedItem], ); useListenClickOutside({ @@ -83,11 +85,11 @@ export const MultiSelectFieldInput = ({ if (weAreNotInAnHTMLInput && isDefined(onCancel)) { onCancel(); } - handleResetSelectedPosition(); + resetSelectedItem(); }, }); - const optionIds = optionsInDropDown.map((option) => option.value); + const optionIds = filteredOptionsInDropDown.map((option) => option.value); return ( { - const option = optionsInDropDown.find( + const option = filteredOptionsInDropDown.find( (option) => option.value === itemId, ); if (isDefined(option)) { @@ -112,7 +114,7 @@ export const MultiSelectFieldInput = ({ /> - {optionsInDropDown.map((option) => { + {filteredOptionsInDropDown.map((option) => { return ( ([]); - const { handleResetSelectedPosition } = useSelectableList( + const { resetSelectedItem } = useSelectableList( SINGLE_ENTITY_SELECT_BASE_LIST, ); const clearField = useClearField(); @@ -44,17 +44,17 @@ export const SelectFieldInput = ({ const handleSubmit = (option: SelectOption) => { onSubmit?.(() => persistField(option?.value)); - handleResetSelectedPosition(); + resetSelectedItem(); }; useScopedHotkeys( Key.Escape, () => { onCancel?.(); - handleResetSelectedPosition(); + resetSelectedItem(); }, hotkeyScope, - [onCancel, handleResetSelectedPosition], + [onCancel, resetSelectedItem], ); const optionIds = [ @@ -74,7 +74,7 @@ export const SelectFieldInput = ({ ); if (isDefined(option)) { onSubmit?.(() => persistField(option.value)); - handleResetSelectedPosition(); + resetSelectedItem(); } }} > diff --git a/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexPageHeader.tsx b/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexPageHeader.tsx index 5039ec7a3447e..b2296a6f3b0fd 100644 --- a/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexPageHeader.tsx +++ b/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexPageHeader.tsx @@ -1,8 +1,8 @@ -import { useParams } from 'react-router-dom'; import { useRecoilValue } from 'recoil'; import { useIcons } from 'twenty-ui'; import { useFilteredObjectMetadataItems } from '@/object-metadata/hooks/useFilteredObjectMetadataItems'; +import { RecordIndexPageKanbanAddButton } from '@/object-record/record-index/components/RecordIndexPageKanbanAddButton'; import { recordIndexViewTypeState } from '@/object-record/record-index/states/recordIndexViewTypeState'; import { PageAddButton } from '@/ui/layout/page/PageAddButton'; import { PageHeader } from '@/ui/layout/page/PageHeader'; @@ -12,13 +12,15 @@ import { capitalize } from '~/utils/string/capitalize'; type RecordIndexPageHeaderProps = { createRecord: () => void; + recordIndexId: string; + objectNamePlural: string; }; export const RecordIndexPageHeader = ({ createRecord, + recordIndexId, + objectNamePlural, }: RecordIndexPageHeaderProps) => { - const objectNamePlural = useParams().objectNamePlural ?? ''; - const { findObjectMetadataItemByNamePlural } = useFilteredObjectMetadataItems(); @@ -32,7 +34,7 @@ export const RecordIndexPageHeader = ({ const recordIndexViewType = useRecoilValue(recordIndexViewTypeState); - const canAddRecord = + const isTable = recordIndexViewType === ViewType.Table && !objectMetadataItem?.isRemote; const pageHeaderTitle = @@ -41,7 +43,14 @@ export const RecordIndexPageHeader = ({ return ( - {canAddRecord && } + {isTable ? ( + + ) : ( + + )} ); }; diff --git a/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexPageKanbanAddButton.tsx b/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexPageKanbanAddButton.tsx new file mode 100644 index 0000000000000..6299546e03b6d --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexPageKanbanAddButton.tsx @@ -0,0 +1,158 @@ +import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; +import { useRecordBoardStates } from '@/object-record/record-board/hooks/internal/useRecordBoardStates'; +import { RecordBoardColumnDefinition } from '@/object-record/record-board/types/RecordBoardColumnDefinition'; +import { RecordIndexPageKanbanAddMenuItem } from '@/object-record/record-index/components/RecordIndexPageKanbanAddMenuItem'; +import { useRecordIndexPageKanbanAddButton } from '@/object-record/record-index/hooks/useRecordIndexPageKanbanAddButton'; +import { SingleEntitySelect } from '@/object-record/relation-picker/components/SingleEntitySelect'; +import { useEntitySelectSearch } from '@/object-record/relation-picker/hooks/useEntitySelectSearch'; +import { EntityForSelect } from '@/object-record/relation-picker/types/EntityForSelect'; +import { RelationPickerHotkeyScope } from '@/object-record/relation-picker/types/RelationPickerHotkeyScope'; +import { IconButton } from '@/ui/input/button/components/IconButton'; +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 { usePreviousHotkeyScope } from '@/ui/utilities/hotkey/hooks/usePreviousHotkeyScope'; +import styled from '@emotion/styled'; +import { useCallback, useState } from 'react'; +import { useRecoilValue } from 'recoil'; +import { IconPlus, isDefined } from 'twenty-ui'; + +const StyledDropdownMenuItemsContainer = styled(DropdownMenuItemsContainer)` + width: 100%; +`; + +const StyledDropDownMenu = styled(DropdownMenu)` + width: 200px; +`; + +type RecordIndexPageKanbanAddButtonProps = { + recordIndexId: string; + objectNamePlural: string; +}; + +export const RecordIndexPageKanbanAddButton = ({ + recordIndexId, + objectNamePlural, +}: RecordIndexPageKanbanAddButtonProps) => { + const dropdownId = `record-index-page-add-button-dropdown`; + const [isSelectingCompany, setIsSelectingCompany] = useState(false); + const [selectedColumnDefinition, setSelectedColumnDefinition] = + useState(); + + const { columnIdsState } = useRecordBoardStates(recordIndexId); + const columnIds = useRecoilValue(columnIdsState); + + const { + setHotkeyScopeAndMemorizePreviousScope, + goBackToPreviousHotkeyScope, + } = usePreviousHotkeyScope(); + const { resetSearchFilter } = useEntitySelectSearch({ + relationPickerScopeId: 'relation-picker', + }); + + const { closeDropdown } = useDropdown(dropdownId); + + const { + selectFieldMetadataItem, + isOpportunity, + createOpportunity, + createRecordWithoutCompany, + } = useRecordIndexPageKanbanAddButton({ + objectNamePlural, + }); + + const handleItemClick = useCallback( + (columnDefinition: RecordBoardColumnDefinition) => { + if (isOpportunity) { + setIsSelectingCompany(true); + setSelectedColumnDefinition(columnDefinition); + setHotkeyScopeAndMemorizePreviousScope( + RelationPickerHotkeyScope.RelationPicker, + ); + } else { + createRecordWithoutCompany(columnDefinition); + closeDropdown(); + } + }, + [ + isOpportunity, + createRecordWithoutCompany, + setHotkeyScopeAndMemorizePreviousScope, + closeDropdown, + ], + ); + + const handleEntitySelect = useCallback( + (company?: EntityForSelect) => { + setIsSelectingCompany(false); + goBackToPreviousHotkeyScope(); + resetSearchFilter(); + if (isDefined(company) && isDefined(selectedColumnDefinition)) { + createOpportunity(company, selectedColumnDefinition); + } + closeDropdown(); + }, + [ + createOpportunity, + goBackToPreviousHotkeyScope, + resetSearchFilter, + selectedColumnDefinition, + closeDropdown, + ], + ); + + const handleCancel = useCallback(() => { + resetSearchFilter(); + goBackToPreviousHotkeyScope(); + setIsSelectingCompany(false); + }, [goBackToPreviousHotkeyScope, resetSearchFilter]); + + if (!selectFieldMetadataItem) { + return null; + } + + return ( + + } + dropdownId={dropdownId} + dropdownComponents={ + + {isOpportunity && isSelectingCompany ? ( + + ) : ( + + {columnIds.map((columnId) => ( + + ))} + + )} + + } + dropdownHotkeyScope={{ scope: dropdownId }} + /> + ); +}; diff --git a/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexPageKanbanAddMenuItem.tsx b/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexPageKanbanAddMenuItem.tsx new file mode 100644 index 0000000000000..a99b3abfd8622 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexPageKanbanAddMenuItem.tsx @@ -0,0 +1,55 @@ +import { RecordBoardColumnDefinitionType } from '@/object-record/record-board/types/RecordBoardColumnDefinition'; +import { useRecordIndexPageKanbanAddMenuItem } from '@/object-record/record-index/hooks/useRecordIndexPageKanbanAddMenuItem'; +import { MenuItem } from '@/ui/navigation/menu-item/components/MenuItem'; +import styled from '@emotion/styled'; +import { Tag } from 'twenty-ui'; + +const StyledMenuItem = styled(MenuItem)` + width: 200px; +`; + +type RecordIndexPageKanbanAddMenuItemProps = { + columnId: string; + recordIndexId: string; + onItemClick: (columnDefinition: any) => void; +}; + +export const RecordIndexPageKanbanAddMenuItem = ({ + columnId, + recordIndexId, + onItemClick, +}: RecordIndexPageKanbanAddMenuItemProps) => { + const { columnDefinition } = useRecordIndexPageKanbanAddMenuItem( + recordIndexId, + columnId, + ); + if (!columnDefinition) { + return null; + } + + return ( + + } + onClick={() => onItemClick(columnDefinition)} + /> + ); +}; diff --git a/packages/twenty-front/src/modules/object-record/record-index/hooks/useRecordIndexPageKanbanAddButton.ts b/packages/twenty-front/src/modules/object-record/record-index/hooks/useRecordIndexPageKanbanAddButton.ts new file mode 100644 index 0000000000000..1d64225bf4854 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-index/hooks/useRecordIndexPageKanbanAddButton.ts @@ -0,0 +1,65 @@ +import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; +import { useObjectNameSingularFromPlural } from '@/object-metadata/hooks/useObjectNameSingularFromPlural'; +import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; +import { useCreateOneRecord } from '@/object-record/hooks/useCreateOneRecord'; +import { RecordBoardColumnDefinition } from '@/object-record/record-board/types/RecordBoardColumnDefinition'; +import { recordIndexKanbanFieldMetadataIdState } from '@/object-record/record-index/states/recordIndexKanbanFieldMetadataIdState'; +import { EntityForSelect } from '@/object-record/relation-picker/types/EntityForSelect'; +import { useRecoilValue } from 'recoil'; +import { isDefined } from 'twenty-ui'; + +type useRecordIndexPageKanbanAddButtonProps = { + objectNamePlural: string; +}; + +export const useRecordIndexPageKanbanAddButton = ({ + objectNamePlural, +}: useRecordIndexPageKanbanAddButtonProps) => { + const { objectNameSingular } = useObjectNameSingularFromPlural({ + objectNamePlural, + }); + const { objectMetadataItem } = useObjectMetadataItem({ objectNameSingular }); + + const recordIndexKanbanFieldMetadataId = useRecoilValue( + recordIndexKanbanFieldMetadataIdState, + ); + const { createOneRecord } = useCreateOneRecord({ objectNameSingular }); + + const selectFieldMetadataItem = objectMetadataItem.fields.find( + (field) => field.id === recordIndexKanbanFieldMetadataId, + ); + const isOpportunity = + objectMetadataItem.nameSingular === CoreObjectNameSingular.Opportunity; + + const createOpportunity = ( + company: EntityForSelect, + columnDefinition: RecordBoardColumnDefinition, + ) => { + if (isDefined(selectFieldMetadataItem)) { + createOneRecord({ + name: company.name, + companyId: company.id, + position: 'first', + [selectFieldMetadataItem.name]: columnDefinition?.value, + }); + } + }; + + const createRecordWithoutCompany = ( + columnDefinition: RecordBoardColumnDefinition, + ) => { + if (isDefined(selectFieldMetadataItem)) { + createOneRecord({ + [selectFieldMetadataItem.name]: columnDefinition?.value, + position: 'first', + }); + } + }; + + return { + selectFieldMetadataItem, + isOpportunity, + createOpportunity, + createRecordWithoutCompany, + }; +}; diff --git a/packages/twenty-front/src/modules/object-record/record-index/hooks/useRecordIndexPageKanbanAddMenuItem.ts b/packages/twenty-front/src/modules/object-record/record-index/hooks/useRecordIndexPageKanbanAddMenuItem.ts new file mode 100644 index 0000000000000..8e5604cb0fe85 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-index/hooks/useRecordIndexPageKanbanAddMenuItem.ts @@ -0,0 +1,12 @@ +import { useRecordBoardStates } from '@/object-record/record-board/hooks/internal/useRecordBoardStates'; +import { useRecoilValue } from 'recoil'; + +export const useRecordIndexPageKanbanAddMenuItem = ( + recordIndexId: string, + columnId: string, +) => { + const { columnsFamilySelector } = useRecordBoardStates(recordIndexId); + const columnDefinition = useRecoilValue(columnsFamilySelector(columnId)); + + return { columnDefinition }; +}; diff --git a/packages/twenty-front/src/modules/object-record/record-inline-cell/components/RecordInlineCellContainer.tsx b/packages/twenty-front/src/modules/object-record/record-inline-cell/components/RecordInlineCellContainer.tsx index 4c12e25417fea..c77c19fa88c4a 100644 --- a/packages/twenty-front/src/modules/object-record/record-inline-cell/components/RecordInlineCellContainer.tsx +++ b/packages/twenty-front/src/modules/object-record/record-inline-cell/components/RecordInlineCellContainer.tsx @@ -1,13 +1,17 @@ import { useTheme } from '@emotion/react'; import styled from '@emotion/styled'; import { ReactElement, useContext } from 'react'; -import { AppTooltip, IconComponent, TooltipDelay } from 'twenty-ui'; +import { + AppTooltip, + IconComponent, + TooltipDelay, + OverflowingTextWithTooltip, +} from 'twenty-ui'; import { FieldContext } from '@/object-record/record-field/contexts/FieldContext'; import { useFieldFocus } from '@/object-record/record-field/hooks/useFieldFocus'; import { RecordInlineCellValue } from '@/object-record/record-inline-cell/components/RecordInlineCellValue'; import { getRecordFieldInputId } from '@/object-record/utils/getRecordFieldInputId'; -import { EllipsisDisplay } from '@/ui/field/display/components/EllipsisDisplay'; import { HotkeyScope } from '@/ui/utilities/hotkey/types/HotkeyScope'; import { useRecordInlineCellContext } from './RecordInlineCellContext'; @@ -120,7 +124,7 @@ export const RecordInlineCellContainer = () => { )} {showLabel && label && ( - {label} + )} {/* TODO: Displaying Tooltips on the board is causing performance issues https://react-tooltip.com/docs/examples/render */} diff --git a/packages/twenty-front/src/modules/object-record/record-inline-cell/components/RecordInlineCellEditMode.tsx b/packages/twenty-front/src/modules/object-record/record-inline-cell/components/RecordInlineCellEditMode.tsx index 0bd0815cd4d47..7d6c6e4d50ed7 100644 --- a/packages/twenty-front/src/modules/object-record/record-inline-cell/components/RecordInlineCellEditMode.tsx +++ b/packages/twenty-front/src/modules/object-record/record-inline-cell/components/RecordInlineCellEditMode.tsx @@ -19,7 +19,7 @@ const StyledInlineCellInput = styled.div` display: flex; min-height: 32px; - min-width: 320px; + min-width: 240px; width: inherit; diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellDisplayContainer.tsx b/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellDisplayContainer.tsx index 20941b7c63a6a..84a332a62f964 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellDisplayContainer.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellDisplayContainer.tsx @@ -18,6 +18,7 @@ const StyledInnerContainer = styled.div` height: 100%; overflow: hidden; width: 100%; + flex-wrap: wrap; `; export type EditableCellDisplayContainerProps = { diff --git a/packages/twenty-front/src/modules/object-record/relation-picker/components/MultiRecordSelect.tsx b/packages/twenty-front/src/modules/object-record/relation-picker/components/MultiRecordSelect.tsx index 87c0532208fef..f325edecc22f7 100644 --- a/packages/twenty-front/src/modules/object-record/relation-picker/components/MultiRecordSelect.tsx +++ b/packages/twenty-front/src/modules/object-record/relation-picker/components/MultiRecordSelect.tsx @@ -48,7 +48,7 @@ export const MultiRecordSelect = ({ const { objectRecordsIdsMultiSelectState, recordMultiSelectIsLoadingState } = useObjectRecordMultiSelectScopedStates(relationPickerScopedId); - const { handleResetSelectedPosition } = useSelectableList( + const { resetSelectedItem } = useSelectableList( MULTI_OBJECT_RECORD_SELECT_SELECTABLE_LIST_ID, ); const recordMultiSelectIsLoading = useRecoilValue( @@ -79,10 +79,10 @@ export const MultiRecordSelect = ({ () => { onSubmit?.(); goBackToPreviousHotkeyScope(); - handleResetSelectedPosition(); + resetSelectedItem(); }, relationPickerScopedId, - [onSubmit, goBackToPreviousHotkeyScope, handleResetSelectedPosition], + [onSubmit, goBackToPreviousHotkeyScope, resetSelectedItem], ); const debouncedOnCreate = useDebouncedCallback( @@ -123,7 +123,7 @@ export const MultiRecordSelect = ({ hotkeyScope={relationPickerScopedId} onEnter={(selectedId) => { onChange?.(selectedId); - handleResetSelectedPosition(); + resetSelectedItem(); }} > {objectRecordsIdsMultiSelect?.map((recordId) => { @@ -133,7 +133,7 @@ export const MultiRecordSelect = ({ objectRecordId={recordId} onChange={(recordId) => { onChange?.(recordId); - handleResetSelectedPosition(); + resetSelectedItem(); }} /> ); diff --git a/packages/twenty-front/src/modules/object-record/relation-picker/components/SelectableMenuItemSelect.tsx b/packages/twenty-front/src/modules/object-record/relation-picker/components/SelectableMenuItemSelect.tsx index 204467062765f..eef1523d15058 100644 --- a/packages/twenty-front/src/modules/object-record/relation-picker/components/SelectableMenuItemSelect.tsx +++ b/packages/twenty-front/src/modules/object-record/relation-picker/components/SelectableMenuItemSelect.tsx @@ -27,7 +27,6 @@ export const SelectableMenuItemSelect = ({ ); const isSelectedItemId = useRecoilValue(isSelectedItemIdSelector(entity.id)); - return ( { - handleResetSelectedPosition(); + resetSelectedItem(); onCancel?.(); }, hotkeyScope, - [onCancel, handleResetSelectedPosition], + [onCancel, resetSelectedItem], ); const selectableItemIds = entitiesInDropdown.map((entity) => entity.id); @@ -134,7 +135,7 @@ export const SingleEntitySelectMenuItems = ({ ); onEntitySelected(entitiesInDropdown[entityIndex]); } - handleResetSelectedPosition(); + resetSelectedItem(); }} > diff --git a/packages/twenty-front/src/modules/object-record/select/components/MultipleRecordSelectDropdown.tsx b/packages/twenty-front/src/modules/object-record/select/components/MultipleRecordSelectDropdown.tsx index e8a286ad4d36d..3914a41633fe5 100644 --- a/packages/twenty-front/src/modules/object-record/select/components/MultipleRecordSelectDropdown.tsx +++ b/packages/twenty-front/src/modules/object-record/select/components/MultipleRecordSelectDropdown.tsx @@ -40,7 +40,7 @@ export const MultipleRecordSelectDropdown = ({ selectableListScopeId: selectableListId, }); - const { handleResetSelectedPosition } = useSelectableList(selectableListId); + const { resetSelectedItem } = useSelectableList(selectableListId); const selectedItemId = useRecoilValue(selectedItemIdState); @@ -75,10 +75,10 @@ export const MultipleRecordSelectDropdown = ({ [Key.Escape], () => { closeDropdown(); - handleResetSelectedPosition(); + resetSelectedItem(); }, hotkeyScope, - [closeDropdown, handleResetSelectedPosition], + [closeDropdown, resetSelectedItem], ); const showNoResult = @@ -105,7 +105,7 @@ export const MultipleRecordSelectDropdown = ({ recordsInDropdown[record], !recordIsSelectedInDropwdown, ); - handleResetSelectedPosition(); + resetSelectedItem(); }} > @@ -116,7 +116,7 @@ export const MultipleRecordSelectDropdown = ({ selected={record.isSelected} isKeySelected={record.id === selectedItemId} onSelectChange={(newCheckedValue) => { - handleResetSelectedPosition(); + resetSelectedItem(); handleRecordSelectChange(record, newCheckedValue); }} avatar={ diff --git a/packages/twenty-front/src/modules/settings/components/SettingsPageContainer.tsx b/packages/twenty-front/src/modules/settings/components/SettingsPageContainer.tsx index 2fc661c7a756c..d33f1493d775c 100644 --- a/packages/twenty-front/src/modules/settings/components/SettingsPageContainer.tsx +++ b/packages/twenty-front/src/modules/settings/components/SettingsPageContainer.tsx @@ -1,11 +1,9 @@ -import styled from '@emotion/styled'; -import { ReactNode } from 'react'; - import { OBJECT_SETTINGS_WIDTH } from '@/settings/data-model/constants/ObjectSettings'; import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile'; -import { isDefined } from '~/utils/isDefined'; - import { ScrollWrapper } from '@/ui/utilities/scroll/components/ScrollWrapper'; +import styled from '@emotion/styled'; +import { ReactNode } from 'react'; +import { isDefined } from '~/utils/isDefined'; const StyledSettingsPageContainer = styled.div<{ width?: number }>` display: flex; @@ -24,17 +22,12 @@ const StyledSettingsPageContainer = styled.div<{ width?: number }>` }}; `; -const StyledScrollWrapper = styled(ScrollWrapper)` - background-color: ${({ theme }) => theme.background.secondary}; - border-radius: ${({ theme }) => theme.border.radius.md}; -`; - export const SettingsPageContainer = ({ children, }: { children: ReactNode; }) => ( - + {children} - + ); diff --git a/packages/twenty-front/src/modules/settings/data-model/components/SettingsDataModelNewFieldBreadcrumbDropDown.tsx b/packages/twenty-front/src/modules/settings/data-model/components/SettingsDataModelNewFieldBreadcrumbDropDown.tsx new file mode 100644 index 0000000000000..0521676c7cf02 --- /dev/null +++ b/packages/twenty-front/src/modules/settings/data-model/components/SettingsDataModelNewFieldBreadcrumbDropDown.tsx @@ -0,0 +1,103 @@ +import { Button } from '@/ui/input/button/components/Button'; +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 { MenuItem } from '@/ui/navigation/menu-item/components/MenuItem'; +import { useTheme } from '@emotion/react'; +import styled from '@emotion/styled'; +import { IconChevronDown } from 'twenty-ui'; + +type SettingsDataModelNewFieldBreadcrumbDropDownProps = { + isConfigureStep: boolean; + onBreadcrumbClick: (isConfigureStep: boolean) => void; +}; + +const StyledContainer = styled.div` + align-items: center; + color: ${({ theme }) => theme.font.color.secondary}; + cursor: pointer; + display: flex; + font-size: ${({ theme }) => theme.font.size.md}; +`; +const StyledButtonContainer = styled.div` + position: relative; + width: 100%; +`; + +const StyledDownChevron = styled(IconChevronDown)` + color: ${({ theme }) => theme.font.color.primary}; + position: absolute; + right: ${({ theme }) => theme.spacing(1.5)}; + top: 50%; + transform: translateY(-50%); +`; + +const StyledMenuItem = styled(MenuItem)<{ selected?: boolean }>` + background: ${({ theme, selected }) => + selected ? theme.background.quaternary : 'transparent'}; + cursor: pointer; +`; + +const StyledSpan = styled.span` + margin-left: ${({ theme }) => theme.spacing(2)}; +`; + +const StyledButton = styled(Button)` + color: ${({ theme }) => theme.font.color.primary}; + padding-right: ${({ theme }) => theme.spacing(6)}; +`; + +export const SettingsDataModelNewFieldBreadcrumbDropDown = ({ + isConfigureStep, + onBreadcrumbClick, +}: SettingsDataModelNewFieldBreadcrumbDropDownProps) => { + const dropdownId = `settings-object-new-field-breadcrumb-dropdown`; + + const { closeDropdown } = useDropdown(dropdownId); + + const handleClick = (step: boolean) => { + onBreadcrumbClick(step); + closeDropdown(); + }; + const theme = useTheme(); + + return ( + + New Field - + + + {isConfigureStep ? ( + + ) : ( + + )} + + } + dropdownComponents={ + + + handleClick(false)} + selected={!isConfigureStep} + /> + handleClick(true)} + selected={isConfigureStep} + /> + + + } + dropdownHotkeyScope={{ + scope: dropdownId, + }} + /> + + ); +}; diff --git a/packages/twenty-front/src/modules/settings/data-model/constants/SettingsFieldTypeCategories.ts b/packages/twenty-front/src/modules/settings/data-model/constants/SettingsFieldTypeCategories.ts new file mode 100644 index 0000000000000..07f8aa28cc9af --- /dev/null +++ b/packages/twenty-front/src/modules/settings/data-model/constants/SettingsFieldTypeCategories.ts @@ -0,0 +1,7 @@ +import { SettingsFieldTypeCategoryType } from '@/settings/data-model/types/SettingsFieldTypeCategoryType'; + +export const SETTINGS_FIELD_TYPE_CATEGORIES: SettingsFieldTypeCategoryType[] = [ + 'Basic', + 'Relation', + 'Advanced', +]; diff --git a/packages/twenty-front/src/modules/settings/data-model/constants/SettingsFieldTypeCategoryDescriptions.ts b/packages/twenty-front/src/modules/settings/data-model/constants/SettingsFieldTypeCategoryDescriptions.ts new file mode 100644 index 0000000000000..aac9c81638f96 --- /dev/null +++ b/packages/twenty-front/src/modules/settings/data-model/constants/SettingsFieldTypeCategoryDescriptions.ts @@ -0,0 +1,10 @@ +import { SettingsFieldTypeCategoryType } from '@/settings/data-model/types/SettingsFieldTypeCategoryType'; + +export const SETTINGS_FIELD_TYPE_CATEGORY_DESCRIPTIONS: Record< + SettingsFieldTypeCategoryType, + string +> = { + Basic: 'All the basic field types you need to start', + Advanced: 'More advanced fields for advanced projects', + Relation: 'Create a relation with another object', +}; diff --git a/packages/twenty-front/src/modules/settings/data-model/constants/SettingsFieldTypeConfigs.ts b/packages/twenty-front/src/modules/settings/data-model/constants/SettingsFieldTypeConfigs.ts index 8ca31f6e7cba0..ae770b4a3f398 100644 --- a/packages/twenty-front/src/modules/settings/data-model/constants/SettingsFieldTypeConfigs.ts +++ b/packages/twenty-front/src/modules/settings/data-model/constants/SettingsFieldTypeConfigs.ts @@ -23,6 +23,7 @@ import { import { CurrencyCode } from '@/object-record/record-field/types/CurrencyCode'; import { DEFAULT_DATE_VALUE } from '@/settings/data-model/constants/DefaultDateValue'; +import { SettingsFieldTypeCategoryType } from '@/settings/data-model/types/SettingsFieldTypeCategoryType'; import { SettingsSupportedFieldType } from '@/settings/data-model/types/SettingsSupportedFieldType'; import { FieldMetadataType } from '~/generated-metadata/graphql'; @@ -32,6 +33,7 @@ export type SettingsFieldTypeConfig = { label: string; Icon: IconComponent; exampleValue?: unknown; + category: SettingsFieldTypeCategoryType; }; export const SETTINGS_FIELD_TYPE_CONFIGS = { @@ -39,75 +41,94 @@ export const SETTINGS_FIELD_TYPE_CONFIGS = { label: 'Unique ID', Icon: IconKey, exampleValue: '00000000-0000-0000-0000-000000000000', + category: 'Advanced', }, [FieldMetadataType.Text]: { label: 'Text', Icon: IconTextSize, exampleValue: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vestibulum magna enim, dapibus non enim in, lacinia faucibus nunc. Sed interdum ante sed felis facilisis, eget ultricies neque molestie. Mauris auctor, justo eu volutpat cursus, libero erat tempus nulla, non sodales lorem lacus a est.', + category: 'Basic', }, [FieldMetadataType.Numeric]: { label: 'Numeric', Icon: IconNumbers, exampleValue: 2000, + category: 'Basic', }, [FieldMetadataType.Number]: { label: 'Number', Icon: IconNumbers, exampleValue: 2000, + category: 'Basic', }, [FieldMetadataType.Link]: { label: 'Link', Icon: IconLink, exampleValue: { url: 'www.twenty.com', label: '' }, + category: 'Basic', }, [FieldMetadataType.Links]: { label: 'Links', Icon: IconLink, exampleValue: { primaryLinkUrl: 'twenty.com', primaryLinkLabel: '' }, + category: 'Basic', }, [FieldMetadataType.Boolean]: { label: 'True/False', Icon: IconCheck, exampleValue: true, + category: 'Basic', }, [FieldMetadataType.DateTime]: { label: 'Date and Time', Icon: IconCalendarTime, exampleValue: DEFAULT_DATE_VALUE.toISOString(), + category: 'Basic', }, [FieldMetadataType.Date]: { label: 'Date', Icon: IconCalendarEvent, exampleValue: DEFAULT_DATE_VALUE.toISOString(), + category: 'Basic', }, [FieldMetadataType.Select]: { label: 'Select', Icon: IconTag, + category: 'Basic', }, [FieldMetadataType.MultiSelect]: { label: 'Multi-select', Icon: IconTags, + category: 'Basic', }, [FieldMetadataType.Currency]: { label: 'Currency', Icon: IconCoins, exampleValue: { amountMicros: 2000000000, currencyCode: CurrencyCode.USD }, + category: 'Basic', }, [FieldMetadataType.Relation]: { label: 'Relation', Icon: IconRelationManyToMany, + category: 'Relation', + }, + [FieldMetadataType.Email]: { + label: 'Email', + Icon: IconMail, + category: 'Basic', }, - [FieldMetadataType.Email]: { label: 'Email', Icon: IconMail }, [FieldMetadataType.Emails]: { label: 'Emails', Icon: IconMail, exampleValue: { primaryEmail: 'john@twenty.com' }, + category: 'Basic', }, [FieldMetadataType.Phone]: { label: 'Phone', Icon: IconPhone, exampleValue: '+1234-567-890', + category: 'Basic', }, [FieldMetadataType.Phones]: { label: 'Phones', @@ -116,16 +137,19 @@ export const SETTINGS_FIELD_TYPE_CONFIGS = { primaryPhoneNumber: '234-567-890', primaryPhoneCountryCode: '+1', }, + category: 'Basic', }, [FieldMetadataType.Rating]: { label: 'Rating', Icon: IconTwentyStar, exampleValue: '3', + category: 'Basic', }, [FieldMetadataType.FullName]: { label: 'Full Name', Icon: IconUser, exampleValue: { firstName: 'John', lastName: 'Doe' }, + category: 'Advanced', }, [FieldMetadataType.Address]: { label: 'Address', @@ -140,20 +164,25 @@ export const SETTINGS_FIELD_TYPE_CONFIGS = { addressLat: 34.0522, addressLng: -118.2437, }, + category: 'Basic', }, [FieldMetadataType.RawJson]: { label: 'JSON', Icon: IconJson, exampleValue: { key: 'value' }, + + category: 'Basic', }, [FieldMetadataType.RichText]: { label: 'Rich Text', Icon: IconFilePencil, exampleValue: { key: 'value' }, + category: 'Basic', }, [FieldMetadataType.Actor]: { label: 'Actor', Icon: IconCreativeCommonsSa, + category: 'Basic', }, } as const satisfies Record< SettingsSupportedFieldType, diff --git a/packages/twenty-front/src/modules/settings/data-model/fields/forms/components/SettingsDataModelFieldAboutForm.tsx b/packages/twenty-front/src/modules/settings/data-model/fields/forms/components/SettingsDataModelFieldAboutForm.tsx deleted file mode 100644 index ee825e7761f28..0000000000000 --- a/packages/twenty-front/src/modules/settings/data-model/fields/forms/components/SettingsDataModelFieldAboutForm.tsx +++ /dev/null @@ -1,104 +0,0 @@ -import styled from '@emotion/styled'; -import { Controller, useFormContext } from 'react-hook-form'; -import { z } from 'zod'; - -import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem'; -import { fieldMetadataItemSchema } from '@/object-metadata/validation-schemas/fieldMetadataItemSchema'; -import { getErrorMessageFromError } from '@/settings/data-model/fields/forms/utils/errorMessages'; -import { IconPicker } from '@/ui/input/components/IconPicker'; -import { TextArea } from '@/ui/input/components/TextArea'; -import { TextInput } from '@/ui/input/components/TextInput'; - -export const settingsDataModelFieldAboutFormSchema = ( - existingLabels?: string[], -) => { - return fieldMetadataItemSchema(existingLabels || []).pick({ - description: true, - icon: true, - label: true, - }); -}; - -// Correctly infer the type from the returned schema -type SettingsDataModelFieldAboutFormValues = z.infer< - ReturnType ->; - -type SettingsDataModelFieldAboutFormProps = { - disabled?: boolean; - fieldMetadataItem?: FieldMetadataItem; - maxLength?: number; -}; - -const StyledInputsContainer = styled.div` - display: flex; - gap: ${({ theme }) => theme.spacing(2)}; - margin-bottom: ${({ theme }) => theme.spacing(2)}; - width: 100%; -`; - -const LABEL = 'label'; - -export const SettingsDataModelFieldAboutForm = ({ - disabled, - fieldMetadataItem, - maxLength, -}: SettingsDataModelFieldAboutFormProps) => { - const { - control, - trigger, - formState: { errors }, - } = useFormContext(); - return ( - <> - - ( - onChange(iconKey)} - variant="primary" - /> - )} - /> - ( - { - onChange(e); - trigger(LABEL); - }} - error={getErrorMessageFromError(errors.label?.message)} - disabled={disabled} - maxLength={maxLength} - fullWidth - /> - )} - /> - - ( -