diff --git a/packages/twenty-front/src/modules/object-record/advanced-filter/command-menu/components/AdvancedFilterCommandMenuValueFormCompositeFieldInput.tsx b/packages/twenty-front/src/modules/object-record/advanced-filter/command-menu/components/AdvancedFilterCommandMenuValueFormCompositeFieldInput.tsx index 807d3ded95..435771aa41 100644 --- a/packages/twenty-front/src/modules/object-record/advanced-filter/command-menu/components/AdvancedFilterCommandMenuValueFormCompositeFieldInput.tsx +++ b/packages/twenty-front/src/modules/object-record/advanced-filter/command-menu/components/AdvancedFilterCommandMenuValueFormCompositeFieldInput.tsx @@ -1,9 +1,12 @@ +import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; import { AdvancedFilterContext } from '@/object-record/advanced-filter/states/context/AdvancedFilterContext'; import { subFieldNameUsedInDropdownComponentState } from '@/object-record/object-filter-dropdown/states/subFieldNameUsedInDropdownComponentState'; import { FormCountryMultiSelectInput } from '@/object-record/record-field/ui/form-types/components/FormCountryMultiSelectInput'; import { FormMultiSelectFieldInput } from '@/object-record/record-field/ui/form-types/components/FormMultiSelectFieldInput'; import { FormNumberFieldInput } from '@/object-record/record-field/ui/form-types/components/FormNumberFieldInput'; +import { FormRelationToOneFieldInput } from '@/object-record/record-field/ui/form-types/components/FormRelationToOneFieldInput'; import { FormTextFieldInput } from '@/object-record/record-field/ui/form-types/components/FormTextFieldInput'; + import { type RecordFilter } from '@/object-record/record-filter/types/RecordFilter'; import { CURRENCIES } from '@/settings/data-model/constants/Currencies'; import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue'; @@ -22,9 +25,11 @@ const ACTOR_SOURCE_OPTIONS: SelectOption[] = Object.values( export const AdvancedFilterCommandMenuValueFormCompositeFieldInput = ({ recordFilter, onChange, + onClear, }: { recordFilter: RecordFilter; onChange: (newValue: JsonValue) => void; + onClear: () => void; }) => { const { VariablePicker } = useContext(AdvancedFilterContext); @@ -96,6 +101,15 @@ export const AdvancedFilterCommandMenuValueFormCompositeFieldInput = ({ readonly={readonly} VariablePicker={VariablePicker} /> + ) : recordFilter.subFieldName === 'workspaceMemberId' ? ( + ) : ( { + applyObjectFilterDropdownFilterValue(''); + }; + const handleRelativeDateFilterChange = (newValue: RelativeDateFilter) => { applyObjectFilterDropdownFilterValue(stringifyRelativeDateFilter(newValue)); }; @@ -144,6 +148,7 @@ export const AdvancedFilterCommandMenuValueFormInput = ({ ); } diff --git a/packages/twenty-front/src/modules/object-record/advanced-filter/components/AdvancedFilterDropdownFilterInput.tsx b/packages/twenty-front/src/modules/object-record/advanced-filter/components/AdvancedFilterDropdownFilterInput.tsx index 28ef8b6472..a539ee8116 100644 --- a/packages/twenty-front/src/modules/object-record/advanced-filter/components/AdvancedFilterDropdownFilterInput.tsx +++ b/packages/twenty-front/src/modules/object-record/advanced-filter/components/AdvancedFilterDropdownFilterInput.tsx @@ -1,3 +1,4 @@ +import { ObjectFilterDropdownActorSelect } from '@/object-record/object-filter-dropdown/components/ObjectFilterDropdownActorSelect'; import { ObjectFilterDropdownOptionSelect } from '@/object-record/object-filter-dropdown/components/ObjectFilterDropdownOptionSelect'; import { ObjectFilterDropdownRatingInput } from '@/object-record/object-filter-dropdown/components/ObjectFilterDropdownRatingInput'; import { ObjectFilterDropdownRecordSelect } from '@/object-record/object-filter-dropdown/components/ObjectFilterDropdownRecordSelect'; @@ -14,6 +15,7 @@ import { ObjectFilterDropdownDateTimeInput } from '@/object-record/object-filter import { ObjectFilterDropdownTextInput } from '@/object-record/object-filter-dropdown/components/ObjectFilterDropdownTextInput'; import { subFieldNameUsedInDropdownComponentState } from '@/object-record/object-filter-dropdown/states/subFieldNameUsedInDropdownComponentState'; import { isFilterOnActorSourceSubField } from '@/object-record/object-filter-dropdown/utils/isFilterOnActorSourceSubField'; +import { isFilterOnActorWorkspaceMemberSubField } from '@/object-record/object-filter-dropdown/utils/isFilterOnActorWorkspaceMemberSubField'; import { type RecordFilter } from '@/object-record/record-filter/types/RecordFilter'; import { DropdownContent } from '@/ui/layout/dropdown/components/DropdownContent'; import { GenericDropdownContentWidth } from '@/ui/layout/dropdown/constants/GenericDropdownContentWidth'; @@ -40,6 +42,9 @@ export const AdvancedFilterDropdownFilterInput = ({ subFieldNameUsedInDropdown, ); + const isActorWorkspaceMemberCompositeFilter = + isFilterOnActorWorkspaceMemberSubField(subFieldNameUsedInDropdown); + return ( <> {filterType === 'ADDRESS' && @@ -64,6 +69,14 @@ export const AdvancedFilterDropdownFilterInput = ({ {filterType === 'ACTOR' && (isActorSourceCompositeFilter ? ( + ) : isActorWorkspaceMemberCompositeFilter ? ( + + + + + ) : ( ))} diff --git a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownActorSelect.tsx b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownActorSelect.tsx new file mode 100644 index 0000000000..d1d5803d76 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownActorSelect.tsx @@ -0,0 +1,173 @@ +import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; +import { ObjectFilterDropdownRecordPinnedItems } from '@/object-record/object-filter-dropdown/components/ObjectFilterDropdownRecordPinnedItems'; +import { CURRENT_WORKSPACE_MEMBER_SELECTABLE_ITEM_ID } from '@/object-record/object-filter-dropdown/constants/CurrentWorkspaceMemberSelectableItemId'; +import { useApplyObjectFilterDropdownFilterValue } from '@/object-record/object-filter-dropdown/hooks/useApplyObjectFilterDropdownFilterValue'; +import { useObjectFilterDropdownFilterValue } from '@/object-record/object-filter-dropdown/hooks/useObjectFilterDropdownFilterValue'; +import { objectFilterDropdownSearchInputComponentState } from '@/object-record/object-filter-dropdown/states/objectFilterDropdownSearchInputComponentState'; +import { MultipleSelectDropdown } from '@/object-record/select/components/MultipleSelectDropdown'; +import { useRecordsForSelect } from '@/object-record/select/hooks/useRecordsForSelect'; +import { type SelectableItem } from '@/object-record/select/types/SelectableItem'; +import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownMenuSeparator'; +import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue'; +import { type RelationFilterValue } from '@/views/view-filter-value/types/RelationFilterValue'; +import { t } from '@lingui/core/macro'; +import { + arrayOfUuidOrVariableSchema, + jsonRelationFilterValueSchema, +} from 'twenty-shared/utils'; +import { IconUserCircle } from 'twenty-ui/display'; + +export const EMPTY_ACTOR_FILTER_VALUE: string = JSON.stringify({ + isCurrentWorkspaceMemberSelected: false, + selectedRecordIds: [], +} satisfies RelationFilterValue); + +export const MAX_WORKSPACE_MEMBERS_TO_DISPLAY = 3; + +type ObjectFilterDropdownActorSelectProps = { + dropdownId: string; +}; + +export const ObjectFilterDropdownActorSelect = ({ + dropdownId, +}: ObjectFilterDropdownActorSelectProps) => { + const { objectFilterDropdownFilterValue } = + useObjectFilterDropdownFilterValue(); + + const { applyObjectFilterDropdownFilterValue } = + useApplyObjectFilterDropdownFilterValue(); + + const objectFilterDropdownSearchInput = useRecoilComponentValue( + objectFilterDropdownSearchInputComponentState, + ); + + const { isCurrentWorkspaceMemberSelected, selectedRecordIds } = + jsonRelationFilterValueSchema + .catch({ + isCurrentWorkspaceMemberSelected: false, + selectedRecordIds: arrayOfUuidOrVariableSchema.parse( + objectFilterDropdownFilterValue, + ), + }) + .parse(objectFilterDropdownFilterValue); + + const { loading, filteredSelectedRecords, recordsToSelect, selectedRecords } = + useRecordsForSelect({ + searchFilterText: objectFilterDropdownSearchInput, + selectedIds: selectedRecordIds, + objectNameSingular: CoreObjectNameSingular.WorkspaceMember, + limit: 10, + allowRequestsToTwentyIcons: false, + }); + + const currentWorkspaceMemberSelectableItem: SelectableItem = { + id: CURRENT_WORKSPACE_MEMBER_SELECTABLE_ITEM_ID, + name: 'Me', + isSelected: isCurrentWorkspaceMemberSelected ?? false, + AvatarIcon: IconUserCircle, + }; + + const pinnedSelectableItems: SelectableItem[] = [ + currentWorkspaceMemberSelectableItem, + ]; + + const filteredPinnedSelectableItems = pinnedSelectableItems.filter((item) => + item.name + .toLowerCase() + .includes(objectFilterDropdownSearchInput.toLowerCase()), + ); + + const handleMultipleRecordSelectChange = ( + itemToSelect: SelectableItem, + isNewSelectedValue: boolean, + ) => { + if (loading) { + return; + } + + const isItemCurrentWorkspaceMember = + itemToSelect.id === CURRENT_WORKSPACE_MEMBER_SELECTABLE_ITEM_ID; + + const selectedRecordIdsWithAddedRecord = [ + ...selectedRecordIds, + itemToSelect.id, + ]; + + const selectedRecordIdsWithRemovedRecord = selectedRecordIds.filter( + (id) => id !== itemToSelect.id, + ); + + const newSelectedRecordIds = isItemCurrentWorkspaceMember + ? selectedRecordIds + : isNewSelectedValue + ? selectedRecordIdsWithAddedRecord + : selectedRecordIdsWithRemovedRecord; + + const newIsCurrentWorkspaceMemberSelected = isItemCurrentWorkspaceMember + ? isNewSelectedValue + : isCurrentWorkspaceMemberSelected; + + const selectedRecordNames = [ + ...recordsToSelect, + ...selectedRecords, + ...filteredSelectedRecords, + ] + .filter( + (record, index, self) => + self.findIndex((r) => r.id === record.id) === index, + ) + .filter((record) => newSelectedRecordIds.includes(record.id)) + .map((record) => record.name); + + const selectedPinnedItemNames = newIsCurrentWorkspaceMemberSelected + ? [currentWorkspaceMemberSelectableItem.name] + : []; + + const selectedItemNames = [ + ...selectedPinnedItemNames, + ...selectedRecordNames, + ]; + + const numberOfSelectedItems = selectedItemNames.length; + + const filterDisplayValue = + numberOfSelectedItems > MAX_WORKSPACE_MEMBERS_TO_DISPLAY + ? t`${numberOfSelectedItems} workspace members` + : selectedItemNames.join(', '); + + const newFilterValue = + newSelectedRecordIds.length > 0 || newIsCurrentWorkspaceMemberSelected + ? JSON.stringify({ + isCurrentWorkspaceMemberSelected: + newIsCurrentWorkspaceMemberSelected, + selectedRecordIds: newSelectedRecordIds, + } satisfies RelationFilterValue) + : ''; + + applyObjectFilterDropdownFilterValue(newFilterValue, filterDisplayValue); + }; + + return ( + <> + {filteredPinnedSelectableItems.length > 0 && ( + <> + + + + )} + + + ); +}; diff --git a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/utils/getSubMenuOptions.ts b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/utils/getSubMenuOptions.ts index ea268d59b9..acf8c04fc5 100644 --- a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/utils/getSubMenuOptions.ts +++ b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/utils/getSubMenuOptions.ts @@ -14,6 +14,11 @@ export const getSubMenuOptions = (subMenu: FilterableFieldType | null) => { icon: 'IconId', type: 'ACTOR', }, + { + name: 'Workspace Member', + icon: 'IconUserCircle', + type: 'WORKSPACE_MEMBER', + }, ]; default: return []; diff --git a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/utils/isFilterOnActorWorkspaceMemberSubField.ts b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/utils/isFilterOnActorWorkspaceMemberSubField.ts new file mode 100644 index 0000000000..53683738cc --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/utils/isFilterOnActorWorkspaceMemberSubField.ts @@ -0,0 +1,7 @@ +import { type FieldActorValue } from '@/object-record/record-field/ui/types/FieldMetadata'; + +export const isFilterOnActorWorkspaceMemberSubField = ( + subFieldName?: string | null | undefined, +) => { + return subFieldName === ('workspaceMemberId' satisfies keyof FieldActorValue); +}; diff --git a/packages/twenty-front/src/modules/object-record/record-field/ui/components/FormFieldInput.tsx b/packages/twenty-front/src/modules/object-record/record-field/ui/components/FormFieldInput.tsx index b718f6ae02..8e8f09ba7b 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/ui/components/FormFieldInput.tsx +++ b/packages/twenty-front/src/modules/object-record/record-field/ui/components/FormFieldInput.tsx @@ -59,6 +59,7 @@ type FormFieldInputProps = { field: Pick, 'label' | 'metadata' | 'type'>; defaultValue: JsonValue; onChange: (value: JsonValue) => void; + onClear?: () => void; VariablePicker?: VariablePickerComponent; readonly?: boolean; placeholder?: string; @@ -71,6 +72,7 @@ export const FormFieldInput = ({ field, defaultValue, onChange, + onClear, VariablePicker, readonly, placeholder, @@ -234,6 +236,7 @@ export const FormFieldInput = ({ defaultValue={ defaultValue as FieldRelationValue | string } + onClear={onClear} onChange={onChange} VariablePicker={VariablePicker} readonly={readonly} diff --git a/packages/twenty-front/src/modules/object-record/record-field/ui/form-types/components/FormMultiSelectFieldInput.tsx b/packages/twenty-front/src/modules/object-record/record-field/ui/form-types/components/FormMultiSelectFieldInput.tsx index c876bf1313..151e350c69 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/ui/form-types/components/FormMultiSelectFieldInput.tsx +++ b/packages/twenty-front/src/modules/object-record/record-field/ui/form-types/components/FormMultiSelectFieldInput.tsx @@ -1,5 +1,5 @@ -import { t } from '@lingui/core/macro'; import styled from '@emotion/styled'; +import { t } from '@lingui/core/macro'; import { FormFieldInputContainer } from '@/object-record/record-field/ui/form-types/components/FormFieldInputContainer'; import { FormFieldInputInnerContainer } from '@/object-record/record-field/ui/form-types/components/FormFieldInputInnerContainer'; diff --git a/packages/twenty-front/src/modules/object-record/record-field/ui/form-types/components/FormRelationToOneFieldInput.tsx b/packages/twenty-front/src/modules/object-record/record-field/ui/form-types/components/FormRelationToOneFieldInput.tsx index a414859534..6b2af086e1 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/ui/form-types/components/FormRelationToOneFieldInput.tsx +++ b/packages/twenty-front/src/modules/object-record/record-field/ui/form-types/components/FormRelationToOneFieldInput.tsx @@ -13,6 +13,7 @@ export type FormRelationToOneFieldInputProps = { objectNameSingular?: string; defaultValue?: FieldRelationValue | string; onChange: (value: JsonValue) => void; + onClear?: () => void; readonly?: boolean; VariablePicker?: VariablePickerComponent; }; @@ -27,6 +28,7 @@ export const FormRelationToOneFieldInput = ({ label, objectNameSingular, onChange, + onClear, defaultValue, readonly, VariablePicker, @@ -41,6 +43,7 @@ export const FormRelationToOneFieldInput = ({ : defaultValue } onChange={onChange} + onClear={onClear} objectNameSingulars={[objectNameSingular]} disabled={readonly} VariablePicker={VariablePicker} diff --git a/packages/twenty-front/src/modules/object-record/record-field/ui/form-types/components/FormSingleRecordPicker.tsx b/packages/twenty-front/src/modules/object-record/record-field/ui/form-types/components/FormSingleRecordPicker.tsx index ce3585df51..06f3d3074a 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/ui/form-types/components/FormSingleRecordPicker.tsx +++ b/packages/twenty-front/src/modules/object-record/record-field/ui/form-types/components/FormSingleRecordPicker.tsx @@ -63,6 +63,7 @@ export type FormSingleRecordPickerProps = { label?: string; defaultValue?: RecordId | Variable; onChange: (value: RecordId | Variable | null) => void; + onClear?: () => void; objectNameSingulars: string[]; disabled?: boolean; testId?: string; @@ -74,6 +75,7 @@ export const FormSingleRecordPicker = ({ defaultValue, objectNameSingulars, onChange, + onClear, disabled, testId, VariablePicker, @@ -127,7 +129,7 @@ export const FormSingleRecordPicker = ({ selectedMorphItem: RecordPickerPickableMorphItem | null | undefined, ) => { if (!isNonEmptyString(selectedMorphItem?.recordId)) { - onChange(null); + onClear?.(); return; } @@ -143,7 +145,7 @@ export const FormSingleRecordPicker = ({ const handleUnlinkVariable = (event?: React.MouseEvent) => { // Prevents the dropdown to open when clicking on the chip event?.stopPropagation(); - onChange(null); + onClear?.(); }; const setRecordPickerSelectedId = useSetRecoilComponentState( diff --git a/packages/twenty-front/src/modules/object-record/record-filter/constants/IconNameBySubField.ts b/packages/twenty-front/src/modules/object-record/record-filter/constants/IconNameBySubField.ts index 72c0bb2946..1a278db8d6 100644 --- a/packages/twenty-front/src/modules/object-record/record-filter/constants/IconNameBySubField.ts +++ b/packages/twenty-front/src/modules/object-record/record-filter/constants/IconNameBySubField.ts @@ -7,6 +7,7 @@ export const ICON_NAME_BY_SUB_FIELD: Partial< amountMicros: 'IconNumber95Small', name: 'IconTextSize', source: 'IconTransferIn', + workspaceMemberId: 'IconUserCircle', primaryEmail: 'IconMail', additionalEmails: 'IconMailPlus', primaryLinkLabel: 'IconTextSize', diff --git a/packages/twenty-front/src/modules/object-record/record-filter/utils/getRecordFilterOperands.ts b/packages/twenty-front/src/modules/object-record/record-filter/utils/getRecordFilterOperands.ts index 85b8600ee8..e99cb36566 100644 --- a/packages/twenty-front/src/modules/object-record/record-filter/utils/getRecordFilterOperands.ts +++ b/packages/twenty-front/src/modules/object-record/record-filter/utils/getRecordFilterOperands.ts @@ -1,4 +1,5 @@ import { isFilterOnActorSourceSubField } from '@/object-record/object-filter-dropdown/utils/isFilterOnActorSourceSubField'; +import { isFilterOnActorWorkspaceMemberSubField } from '@/object-record/object-filter-dropdown/utils/isFilterOnActorWorkspaceMemberSubField'; import { type CompositeFieldSubFieldName } from '@/settings/data-model/types/CompositeFieldSubFieldName'; import { FieldMetadataType, @@ -201,7 +202,10 @@ export const getRecordFilterOperands = ({ case 'SELECT': return FILTER_OPERANDS_MAP.SELECT; case 'ACTOR': { - if (isFilterOnActorSourceSubField(subFieldName)) { + if ( + isFilterOnActorSourceSubField(subFieldName) || + isFilterOnActorWorkspaceMemberSubField(subFieldName) + ) { return [ RecordFilterOperand.IS, RecordFilterOperand.IS_NOT, diff --git a/packages/twenty-front/src/modules/object-record/record-filter/utils/isRecordMatchingFilter.ts b/packages/twenty-front/src/modules/object-record/record-filter/utils/isRecordMatchingFilter.ts index f91dd70755..f6524d72dd 100644 --- a/packages/twenty-front/src/modules/object-record/record-filter/utils/isRecordMatchingFilter.ts +++ b/packages/twenty-front/src/modules/object-record/record-filter/utils/isRecordMatchingFilter.ts @@ -370,6 +370,13 @@ export const isRecordMatchingFilter = ({ case FieldMetadataType.ACTOR: { const actorFilter = filterValue as ActorFilter; + if (isDefined(actorFilter.workspaceMemberId)) { + return isMatchingUUIDFilter({ + uuidFilter: actorFilter.workspaceMemberId, + value: record[filterKey].workspaceMemberId, + }); + } + return ( actorFilter.name === undefined || isMatchingStringFilter({ diff --git a/packages/twenty-front/src/modules/settings/data-model/constants/CompositeFieldSubFieldLabel.ts b/packages/twenty-front/src/modules/settings/data-model/constants/CompositeFieldSubFieldLabel.ts index 60360c3a93..2bd4332522 100644 --- a/packages/twenty-front/src/modules/settings/data-model/constants/CompositeFieldSubFieldLabel.ts +++ b/packages/twenty-front/src/modules/settings/data-model/constants/CompositeFieldSubFieldLabel.ts @@ -40,7 +40,7 @@ export const COMPOSITE_FIELD_SUB_FIELD_LABELS: { [FieldMetadataType.ACTOR]: { source: 'Source', name: 'Name', - workspaceMemberId: 'Workspace Member ID', + workspaceMemberId: 'Workspace Member', context: 'Context', }, [FieldMetadataType.RICH_TEXT_V2]: { diff --git a/packages/twenty-front/src/modules/settings/data-model/constants/SettingsCompositeFieldTypeConfigs.ts b/packages/twenty-front/src/modules/settings/data-model/constants/SettingsCompositeFieldTypeConfigs.ts index 07271bf318..19f2ee496c 100644 --- a/packages/twenty-front/src/modules/settings/data-model/constants/SettingsCompositeFieldTypeConfigs.ts +++ b/packages/twenty-front/src/modules/settings/data-model/constants/SettingsCompositeFieldTypeConfigs.ts @@ -464,7 +464,7 @@ export const SETTINGS_COMPOSITE_FIELD_TYPE_CONFIGS = { COMPOSITE_FIELD_SUB_FIELD_LABELS[FieldMetadataType.ACTOR] .workspaceMemberId, isImportable: true, - isFilterable: false, + isFilterable: true, isIncludedInUniqueConstraint: false, }, { diff --git a/packages/twenty-front/src/modules/workflow/workflow-steps/filters/components/WorkflowStepFilterValueCompositeInput.tsx b/packages/twenty-front/src/modules/workflow/workflow-steps/filters/components/WorkflowStepFilterValueCompositeInput.tsx index e463d2da7b..e8ef3b4785 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-steps/filters/components/WorkflowStepFilterValueCompositeInput.tsx +++ b/packages/twenty-front/src/modules/workflow/workflow-steps/filters/components/WorkflowStepFilterValueCompositeInput.tsx @@ -1,8 +1,13 @@ +import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; import { FormCountryMultiSelectInput } from '@/object-record/record-field/ui/form-types/components/FormCountryMultiSelectInput'; import { FormMultiSelectFieldInput } from '@/object-record/record-field/ui/form-types/components/FormMultiSelectFieldInput'; import { FormNumberFieldInput } from '@/object-record/record-field/ui/form-types/components/FormNumberFieldInput'; -import { FormSingleRecordPicker } from '@/object-record/record-field/ui/form-types/components/FormSingleRecordPicker'; +import { FormRelationToOneFieldInput } from '@/object-record/record-field/ui/form-types/components/FormRelationToOneFieldInput'; import { FormTextFieldInput } from '@/object-record/record-field/ui/form-types/components/FormTextFieldInput'; +import { + type FieldRelationToOneValue, + type FieldRelationValue, +} from '@/object-record/record-field/ui/types/FieldMetadata'; import { CURRENCIES } from '@/settings/data-model/constants/Currencies'; import { WorkflowStepFilterContext } from '@/workflow/workflow-steps/filters/states/context/WorkflowStepFilterContext'; import { WorkflowVariablePicker } from '@/workflow/workflow-variables/components/WorkflowVariablePicker'; @@ -21,9 +26,11 @@ const ACTOR_SOURCE_OPTIONS: SelectOption[] = Object.values( export const WorkflowStepFilterValueCompositeInput = ({ stepFilter, onChange, + onClear, }: { stepFilter: StepFilter; onChange: (newValue: JsonValue) => void; + onClear: () => void; }) => { const { readonly } = useContext(WorkflowStepFilterContext); const { type: filterType, compositeFieldSubFieldName: subFieldName } = @@ -90,12 +97,17 @@ export const WorkflowStepFilterValueCompositeInput = ({ VariablePicker={WorkflowVariablePicker} /> ) : subFieldName === 'workspaceMemberId' ? ( - + | string + } onChange={onChange} - disabled={readonly} + onClear={onClear} VariablePicker={WorkflowVariablePicker} + readonly={readonly} /> ) : ( { + upsertStepFilterSettings({ + stepFilterToUpsert: { + ...stepFilter, + value: '', + }, + }); + }; + const handleRelativeDateFilterChange = ( newRelativeDateFilter: RelativeDateFilter, ) => { @@ -182,6 +191,7 @@ export const WorkflowStepFilterValueInput = ({ ); } @@ -269,6 +279,7 @@ export const WorkflowStepFilterValueInput = ({ field={field} defaultValue={stepFilter.value} onChange={handleValueChange} + onClear={handleClearValue} readonly={readonly} VariablePicker={WorkflowVariablePicker} placeholder={t`Enter value`} diff --git a/packages/twenty-server/src/engine/workspace-manager/dev-seeder/data/constants/opportunity-data-seeds.constant.ts b/packages/twenty-server/src/engine/workspace-manager/dev-seeder/data/constants/opportunity-data-seeds.constant.ts index 59dc61e181..0ce21bb5f8 100644 --- a/packages/twenty-server/src/engine/workspace-manager/dev-seeder/data/constants/opportunity-data-seeds.constant.ts +++ b/packages/twenty-server/src/engine/workspace-manager/dev-seeder/data/constants/opportunity-data-seeds.constant.ts @@ -1,6 +1,11 @@ +import { isDefined } from 'twenty-shared/utils'; + import { COMPANY_DATA_SEED_IDS } from 'src/engine/workspace-manager/dev-seeder/data/constants/company-data-seeds.constant'; import { PERSON_DATA_SEED_IDS } from 'src/engine/workspace-manager/dev-seeder/data/constants/person-data-seeds.constant'; -import { WORKSPACE_MEMBER_DATA_SEED_IDS } from 'src/engine/workspace-manager/dev-seeder/data/constants/workspace-member-data-seeds.constant'; +import { + WORKSPACE_MEMBER_DATA_SEED_IDS, + WORKSPACE_MEMBER_DATA_SEEDS, +} from 'src/engine/workspace-manager/dev-seeder/data/constants/workspace-member-data-seeds.constant'; type OpportunityDataSeed = { id: string; @@ -177,7 +182,17 @@ const GENERATE_OPPORTUNITY_SEEDS = (): OpportunityDataSeed[] => { CLOSE_DATE.setDate(CLOSE_DATE.getDate() + DAYS_AHEAD); - OPPORTUNITY_SEEDS.push({ + const workspaceMemberId = Object.values(WORKSPACE_MEMBER_DATA_SEED_IDS)[ + INDEX % 4 + ]; + const workspaceMember = WORKSPACE_MEMBER_DATA_SEEDS.find( + (workspaceMember) => workspaceMember.id === workspaceMemberId, + ); + const workspaceMemberName = isDefined(workspaceMember) + ? `${workspaceMember?.nameFirstName} ${workspaceMember?.nameLastName}` + : 'Unkonwn'; + + const rawSeed: OpportunityDataSeed = { id: OPPORTUNITY_DATA_SEED_IDS[`ID_${INDEX}`], name: TEMPLATE.name, amountAmountMicros: TEMPLATE.amount * 1000000, @@ -195,12 +210,22 @@ const GENERATE_OPPORTUNITY_SEEDS = (): OpportunityDataSeed[] => { ] || COMPANY_DATA_SEED_IDS.ID_1, ownerId: WORKSPACE_MEMBER_DATA_SEED_IDS.TIM, createdBySource: 'MANUAL', - createdByWorkspaceMemberId: WORKSPACE_MEMBER_DATA_SEED_IDS.TIM, - createdByName: 'Tim Cook', updatedBySource: 'MANUAL', - updatedByWorkspaceMemberId: WORKSPACE_MEMBER_DATA_SEED_IDS.TIM, - updatedByName: 'Tim Cook', - }); + createdByWorkspaceMemberId: workspaceMemberId, + createdByName: workspaceMemberName, + updatedByWorkspaceMemberId: workspaceMemberId, + updatedByName: workspaceMemberName, + }; + + const opportunityDataSeedWithSQLColumnOrder: OpportunityDataSeed = + Object.fromEntries( + OPPORTUNITY_DATA_SEED_COLUMNS.map((column) => [ + column, + rawSeed[column as keyof OpportunityDataSeed], + ]), + ) as OpportunityDataSeed; + + OPPORTUNITY_SEEDS.push(opportunityDataSeedWithSQLColumnOrder); } return OPPORTUNITY_SEEDS; diff --git a/packages/twenty-server/src/engine/workspace-manager/dev-seeder/data/constants/person-data-seeds.constant.ts b/packages/twenty-server/src/engine/workspace-manager/dev-seeder/data/constants/person-data-seeds.constant.ts index 4aa10a95df..2eee11d797 100644 --- a/packages/twenty-server/src/engine/workspace-manager/dev-seeder/data/constants/person-data-seeds.constant.ts +++ b/packages/twenty-server/src/engine/workspace-manager/dev-seeder/data/constants/person-data-seeds.constant.ts @@ -1,5 +1,10 @@ +import { isDefined } from 'twenty-shared/utils'; + import { COMPANY_DATA_SEED_IDS } from 'src/engine/workspace-manager/dev-seeder/data/constants/company-data-seeds.constant'; -import { WORKSPACE_MEMBER_DATA_SEED_IDS } from 'src/engine/workspace-manager/dev-seeder/data/constants/workspace-member-data-seeds.constant'; +import { + WORKSPACE_MEMBER_DATA_SEED_IDS, + WORKSPACE_MEMBER_DATA_SEEDS, +} from 'src/engine/workspace-manager/dev-seeder/data/constants/workspace-member-data-seeds.constant'; type PersonDataSeed = { id: string; @@ -11,12 +16,12 @@ type PersonDataSeed = { linkedinLinkPrimaryLinkUrl: string; jobTitle: string; companyId: string; - createdBySource: string; - createdByWorkspaceMemberId: string; - createdByName: string; phonesPrimaryPhoneNumber: string; phonesPrimaryPhoneCountryCode: string; phonesPrimaryPhoneCallingCode: string; + createdBySource: string; + createdByWorkspaceMemberId: string; + createdByName: string; updatedBySource: string; updatedByWorkspaceMemberId: string; updatedByName: string; @@ -33,12 +38,12 @@ export const PERSON_DATA_SEED_COLUMNS: (keyof PersonDataSeed)[] = [ 'linkedinLinkPrimaryLinkUrl', 'jobTitle', 'companyId', - 'createdBySource', - 'createdByWorkspaceMemberId', - 'createdByName', 'phonesPrimaryPhoneNumber', 'phonesPrimaryPhoneCountryCode', 'phonesPrimaryPhoneCallingCode', + 'createdBySource', + 'createdByWorkspaceMemberId', + 'createdByName', 'updatedBySource', 'updatedByWorkspaceMemberId', 'updatedByName', @@ -21654,13 +21659,37 @@ const PERSON_DATA_SEEDS_RAW = [ ]; export const PERSON_DATA_SEEDS: PersonDataSeed[] = PERSON_DATA_SEEDS_RAW.map( - (person, index) => ({ - ...person, - updatedBySource: person.createdBySource, - updatedByWorkspaceMemberId: person.createdByWorkspaceMemberId, - updatedByName: person.createdByName, - position: index + 1, - }), + (person, index) => { + const workspaceMemberId = Object.values(WORKSPACE_MEMBER_DATA_SEED_IDS)[ + index % 4 + ]; + const workspaceMember = WORKSPACE_MEMBER_DATA_SEEDS.find( + (workspaceMember) => workspaceMember.id === workspaceMemberId, + ); + const workspaceMemberName = isDefined(workspaceMember) + ? `${workspaceMember?.nameFirstName} ${workspaceMember?.nameLastName}` + : 'Unkonwn'; + + const dataSeed: PersonDataSeed = { + ...person, + createdBySource: person.createdBySource, + createdByWorkspaceMemberId: workspaceMemberId, + createdByName: workspaceMemberName, + updatedBySource: person.createdBySource, + updatedByWorkspaceMemberId: workspaceMemberId, + updatedByName: workspaceMemberName, + position: index + 1, + }; + + const personDataSeedWithSQLColumnOrder: PersonDataSeed = Object.fromEntries( + PERSON_DATA_SEED_COLUMNS.map((column) => [ + column, + dataSeed[column as keyof PersonDataSeed], + ]), + ) as PersonDataSeed; + + return personDataSeedWithSQLColumnOrder; + }, ); // Map for O(1) lookups by person ID diff --git a/packages/twenty-shared/src/types/RecordGqlOperationFilter.ts b/packages/twenty-shared/src/types/RecordGqlOperationFilter.ts index d4c612d86e..11d4b3a730 100644 --- a/packages/twenty-shared/src/types/RecordGqlOperationFilter.ts +++ b/packages/twenty-shared/src/types/RecordGqlOperationFilter.ts @@ -103,6 +103,7 @@ export type LinksFilter = { export type ActorFilter = { name?: StringFilter; source?: SelectFilter; + workspaceMemberId?: UUIDFilter; }; export type EmailsFilter = { diff --git a/packages/twenty-shared/src/utils/filter/turnRecordFilterIntoGqlOperationFilter.ts b/packages/twenty-shared/src/utils/filter/turnRecordFilterIntoGqlOperationFilter.ts index 104d848a6a..e41bb4b1df 100644 --- a/packages/twenty-shared/src/utils/filter/turnRecordFilterIntoGqlOperationFilter.ts +++ b/packages/twenty-shared/src/utils/filter/turnRecordFilterIntoGqlOperationFilter.ts @@ -1153,6 +1153,71 @@ export const turnRecordFilterIntoRecordGqlOperationFilter = ({ } } + if (subFieldName === 'workspaceMemberId') { + const { isCurrentWorkspaceMemberSelected, selectedRecordIds } = + jsonRelationFilterValueSchema + .catch({ + isCurrentWorkspaceMemberSelected: false, + selectedRecordIds: arrayOfUuidOrVariableSchema.parse( + recordFilter.value, + ), + }) + .parse(recordFilter.value); + + const workspaceMemberIds = isCurrentWorkspaceMemberSelected + ? [ + ...selectedRecordIds, + filterValueDependencies?.currentWorkspaceMemberId, + ].filter(isDefined) + : selectedRecordIds; + + if (!isDefined(workspaceMemberIds) || workspaceMemberIds.length === 0) { + return; + } + + switch (recordFilter.operand) { + case RecordFilterOperand.IS: + return { + [correspondingFieldMetadataItem.name]: { + workspaceMemberId: { + in: workspaceMemberIds, + } satisfies UUIDFilter, + }, + }; + case RecordFilterOperand.IS_NOT: { + return { + or: [ + { + not: { + [correspondingFieldMetadataItem.name]: { + workspaceMemberId: { + in: workspaceMemberIds, + } satisfies UUIDFilter, + }, + }, + }, + { + [correspondingFieldMetadataItem.name]: { + workspaceMemberId: { + is: 'NULL', + } satisfies UUIDFilter, + }, + }, + ], + }; + } + default: { + const fieldForRecordFilter = fieldMetadataItems.find( + (field) => field.id === recordFilter.fieldMetadataId, + ); + + throw new Error( + `Unknown operand ${recordFilter.operand} for ${fieldForRecordFilter?.label ?? ''} filter`, + ); + } + } + } + switch (recordFilter.operand) { case RecordFilterOperand.CONTAINS: return {