feat: added workspace member filter for actor fields (#16628)
Closes #16619 This PR adds support for filtering ACTOR fields by `Workspace Member` subfield, enabling users to filter records by who created them with a `Me` option. I replicated the same UX and code structure as the Relation field filter for consistency. Each filter selection triggers a server-side GraphQL call. But there is client-side filtering in `isRecordMatchingFilter.ts` which instantly filters already-loaded records while the server is fetching new results. I replicated this behaviour from how `FULL_NAME` and other composite fields handle filtering. UX decision that were taken by me (Let me know if changes are needed) : - The filter shows comma separated selected workspace member names until 3 members are selected. After that it shows [no of selected members] workspace members. <img width="643" height="283" alt="Screenshot 2025-12-17 at 8 01 23 PM" src="https://github.com/user-attachments/assets/18d524f4-979b-4d92-ad64-aa7b63be7897" /> <img width="774" height="309" alt="Screenshot 2025-12-17 at 8 01 33 PM" src="https://github.com/user-attachments/assets/779f374f-1501-48f9-afa2-a78628e067b1" /> <img width="737" height="397" alt="Screenshot 2025-12-17 at 8 01 40 PM" src="https://github.com/user-attachments/assets/4e8b963e-8a0f-467d-91ae-48bca4e1d842" /> <img width="697" height="334" alt="Screenshot 2025-12-17 at 8 01 53 PM" src="https://github.com/user-attachments/assets/c9a0e4bb-48b4-486b-a4f9-d7c49ff3852c" /> <img width="684" height="343" alt="Screenshot 2025-12-17 at 8 02 04 PM" src="https://github.com/user-attachments/assets/d5c45eba-06a3-40fc-b9d0-d585dc8bc136" /> --------- Co-authored-by: Lucas Bordeau <bordeau.lucas@gmail.com>
This commit is contained in:
+14
@@ -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' ? (
|
||||
<FormRelationToOneFieldInput
|
||||
objectNameSingular={CoreObjectNameSingular.WorkspaceMember}
|
||||
defaultValue={recordFilter.value}
|
||||
onChange={onChange}
|
||||
onClear={onClear}
|
||||
VariablePicker={VariablePicker}
|
||||
readonly={readonly}
|
||||
/>
|
||||
) : (
|
||||
<FormTextFieldInput
|
||||
defaultValue={recordFilter.value}
|
||||
|
||||
+6
-1
@@ -19,9 +19,9 @@ import {
|
||||
import { currentRecordFiltersComponentState } from '@/object-record/record-filter/states/currentRecordFiltersComponentState';
|
||||
import { RecordFilterOperand } from '@/object-record/record-filter/types/RecordFilterOperand';
|
||||
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
|
||||
import { useFeatureFlagsMap } from '@/workspace/hooks/useFeatureFlagsMap';
|
||||
import { stringifyRelativeDateFilter } from '@/views/view-filter-value/utils/stringifyRelativeDateFilter';
|
||||
import { WORKFLOW_TIMEZONE } from '@/workflow/constants/WorkflowTimeZone';
|
||||
import { useFeatureFlagsMap } from '@/workspace/hooks/useFeatureFlagsMap';
|
||||
import { isObject, isString } from '@sniptt/guards';
|
||||
import { useContext } from 'react';
|
||||
import { FieldMetadataType } from 'twenty-shared/types';
|
||||
@@ -77,6 +77,10 @@ export const AdvancedFilterCommandMenuValueFormInput = ({
|
||||
}
|
||||
};
|
||||
|
||||
const handleClear = () => {
|
||||
applyObjectFilterDropdownFilterValue('');
|
||||
};
|
||||
|
||||
const handleRelativeDateFilterChange = (newValue: RelativeDateFilter) => {
|
||||
applyObjectFilterDropdownFilterValue(stringifyRelativeDateFilter(newValue));
|
||||
};
|
||||
@@ -144,6 +148,7 @@ export const AdvancedFilterCommandMenuValueFormInput = ({
|
||||
<AdvancedFilterCommandMenuValueFormCompositeFieldInput
|
||||
recordFilter={recordFilter}
|
||||
onChange={handleChange}
|
||||
onClear={handleClear}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
+13
@@ -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 ? (
|
||||
<ObjectFilterDropdownSourceSelect dropdownId={filterDropdownId} />
|
||||
) : isActorWorkspaceMemberCompositeFilter ? (
|
||||
<DropdownContent
|
||||
widthInPixels={GenericDropdownContentWidth.ExtraLarge}
|
||||
>
|
||||
<ObjectFilterDropdownSearchInput />
|
||||
<DropdownMenuSeparator />
|
||||
<ObjectFilterDropdownActorSelect dropdownId={filterDropdownId} />
|
||||
</DropdownContent>
|
||||
) : (
|
||||
<ObjectFilterDropdownTextInput filterDropdownId={filterDropdownId} />
|
||||
))}
|
||||
|
||||
+173
@@ -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 && (
|
||||
<>
|
||||
<ObjectFilterDropdownRecordPinnedItems
|
||||
selectableItems={filteredPinnedSelectableItems}
|
||||
onChange={handleMultipleRecordSelectChange}
|
||||
/>
|
||||
<DropdownMenuSeparator />
|
||||
</>
|
||||
)}
|
||||
<MultipleSelectDropdown
|
||||
selectableListId="object-filter-actor-select-id"
|
||||
focusId={dropdownId}
|
||||
itemsToSelect={recordsToSelect}
|
||||
filteredSelectedItems={filteredSelectedRecords}
|
||||
selectedItems={selectedRecords}
|
||||
onChange={handleMultipleRecordSelectChange}
|
||||
searchFilter={objectFilterDropdownSearchInput}
|
||||
loadingItems={loading}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
+5
@@ -14,6 +14,11 @@ export const getSubMenuOptions = (subMenu: FilterableFieldType | null) => {
|
||||
icon: 'IconId',
|
||||
type: 'ACTOR',
|
||||
},
|
||||
{
|
||||
name: 'Workspace Member',
|
||||
icon: 'IconUserCircle',
|
||||
type: 'WORKSPACE_MEMBER',
|
||||
},
|
||||
];
|
||||
default:
|
||||
return [];
|
||||
|
||||
+7
@@ -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);
|
||||
};
|
||||
+3
@@ -59,6 +59,7 @@ type FormFieldInputProps = {
|
||||
field: Pick<FieldDefinition<FieldMetadata>, '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<FieldRelationToOneValue> | string
|
||||
}
|
||||
onClear={onClear}
|
||||
onChange={onChange}
|
||||
VariablePicker={VariablePicker}
|
||||
readonly={readonly}
|
||||
|
||||
+1
-1
@@ -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';
|
||||
|
||||
+3
@@ -13,6 +13,7 @@ export type FormRelationToOneFieldInputProps = {
|
||||
objectNameSingular?: string;
|
||||
defaultValue?: FieldRelationValue<FieldRelationToOneValue> | 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}
|
||||
|
||||
+4
-2
@@ -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<HTMLDivElement>) => {
|
||||
// Prevents the dropdown to open when clicking on the chip
|
||||
event?.stopPropagation();
|
||||
onChange(null);
|
||||
onClear?.();
|
||||
};
|
||||
|
||||
const setRecordPickerSelectedId = useSetRecoilComponentState(
|
||||
|
||||
+1
@@ -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',
|
||||
|
||||
+5
-1
@@ -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,
|
||||
|
||||
+7
@@ -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({
|
||||
|
||||
+1
-1
@@ -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]: {
|
||||
|
||||
+1
-1
@@ -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,
|
||||
},
|
||||
{
|
||||
|
||||
+17
-5
@@ -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' ? (
|
||||
<FormSingleRecordPicker
|
||||
objectNameSingulars={['workspaceMember']}
|
||||
defaultValue={stepFilter.value}
|
||||
<FormRelationToOneFieldInput
|
||||
objectNameSingular={CoreObjectNameSingular.WorkspaceMember}
|
||||
defaultValue={
|
||||
stepFilter.value as
|
||||
| FieldRelationValue<FieldRelationToOneValue>
|
||||
| string
|
||||
}
|
||||
onChange={onChange}
|
||||
disabled={readonly}
|
||||
onClear={onClear}
|
||||
VariablePicker={WorkflowVariablePicker}
|
||||
readonly={readonly}
|
||||
/>
|
||||
) : (
|
||||
<FormTextFieldInput
|
||||
|
||||
+11
@@ -93,6 +93,15 @@ export const WorkflowStepFilterValueInput = ({
|
||||
});
|
||||
};
|
||||
|
||||
const handleClearValue = () => {
|
||||
upsertStepFilterSettings({
|
||||
stepFilterToUpsert: {
|
||||
...stepFilter,
|
||||
value: '',
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const handleRelativeDateFilterChange = (
|
||||
newRelativeDateFilter: RelativeDateFilter,
|
||||
) => {
|
||||
@@ -182,6 +191,7 @@ export const WorkflowStepFilterValueInput = ({
|
||||
<WorkflowStepFilterValueCompositeInput
|
||||
stepFilter={stepFilter}
|
||||
onChange={handleValueChange}
|
||||
onClear={handleClearValue}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -269,6 +279,7 @@ export const WorkflowStepFilterValueInput = ({
|
||||
field={field}
|
||||
defaultValue={stepFilter.value}
|
||||
onChange={handleValueChange}
|
||||
onClear={handleClearValue}
|
||||
readonly={readonly}
|
||||
VariablePicker={WorkflowVariablePicker}
|
||||
placeholder={t`Enter value`}
|
||||
|
||||
+32
-7
@@ -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;
|
||||
|
||||
+43
-14
@@ -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
|
||||
|
||||
@@ -103,6 +103,7 @@ export type LinksFilter = {
|
||||
export type ActorFilter = {
|
||||
name?: StringFilter;
|
||||
source?: SelectFilter;
|
||||
workspaceMemberId?: UUIDFilter;
|
||||
};
|
||||
|
||||
export type EmailsFilter = {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user