-
Notifications
You must be signed in to change notification settings - Fork 2.5k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Current workspace member filter (#8016) #9182
base: main
Are you sure you want to change the base?
Changes from all commits
354c94a
d86416a
08c5279
84b6baf
ca61bb3
90a2163
109c632
705fe49
d4f4a68
0de105a
3d2c4fc
b466d66
b3d1740
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -2,19 +2,22 @@ import { ContextStoreTargetedRecordsRule } from '@/context-store/states/contextS | |
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; | ||
import { RecordGqlOperationFilter } from '@/object-record/graphql/types/RecordGqlOperationFilter'; | ||
import { Filter } from '@/object-record/object-filter-dropdown/types/Filter'; | ||
import { FilterValueDependencies } from '@/object-record/record-filter/types/FilterValueDependencies'; | ||
import { computeViewRecordGqlOperationFilter } from '@/object-record/record-filter/utils/computeViewRecordGqlOperationFilter'; | ||
import { makeAndFilterVariables } from '@/object-record/utils/makeAndFilterVariables'; | ||
|
||
export const computeContextStoreFilters = ( | ||
contextStoreTargetedRecordsRule: ContextStoreTargetedRecordsRule, | ||
contextStoreFilters: Filter[], | ||
objectMetadataItem: ObjectMetadataItem, | ||
filterValueDependencies: FilterValueDependencies, | ||
) => { | ||
let queryFilter: RecordGqlOperationFilter | undefined; | ||
|
||
if (contextStoreTargetedRecordsRule.mode === 'exclusion') { | ||
queryFilter = makeAndFilterVariables([ | ||
computeViewRecordGqlOperationFilter( | ||
filterValueDependencies, | ||
contextStoreFilters, | ||
objectMetadataItem?.fields ?? [], | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. style: optional chaining on fields is redundant since empty array is already the fallback |
||
[], | ||
|
@@ -39,6 +42,7 @@ export const computeContextStoreFilters = ( | |
}, | ||
} | ||
: computeViewRecordGqlOperationFilter( | ||
filterValueDependencies, | ||
contextStoreFilters, | ||
objectMetadataItem?.fields ?? [], | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. style: optional chaining on fields is redundant since empty array is already the fallback |
||
[], | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,45 @@ | ||
import { StyledMultipleSelectDropdownAvatarChip } from '@/object-record/select/components/StyledMultipleSelectDropdownAvatarChip'; | ||
import { SelectableItem } from '@/object-record/select/types/SelectableItem'; | ||
import styled from '@emotion/styled'; | ||
import { MenuItemMultiSelectAvatar } from 'twenty-ui'; | ||
|
||
const StyledPinnedItemsContainer = styled.div` | ||
display: flex; | ||
flex-direction: column; | ||
padding: ${({ theme }) => theme.spacing(1)}; | ||
`; | ||
|
||
export const ObjectFilterDropdownRecordPinnedItems = (props: { | ||
selectableItems: SelectableItem[]; | ||
onChange: ( | ||
selectableItem: SelectableItem, | ||
isNewCheckedValue: boolean, | ||
) => void; | ||
}) => { | ||
return ( | ||
<StyledPinnedItemsContainer> | ||
{props.selectableItems.map((selectableItem) => { | ||
return ( | ||
<MenuItemMultiSelectAvatar | ||
key={selectableItem.id} | ||
selected={selectableItem.isSelected} | ||
onSelectChange={(newCheckedValue) => { | ||
props.onChange(selectableItem, newCheckedValue); | ||
}} | ||
Comment on lines
+26
to
+28
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. style: ensure onChange handler is memoized in parent component to prevent unnecessary re-renders |
||
avatar={ | ||
<StyledMultipleSelectDropdownAvatarChip | ||
className="avatar-icon-container" | ||
name={selectableItem.name} | ||
avatarUrl={selectableItem.avatarUrl} | ||
LeftIcon={selectableItem.AvatarIcon} | ||
avatarType={selectableItem.avatarType} | ||
isIconInverted={selectableItem.isIconInverted} | ||
placeholderColorSeed={selectableItem.id} | ||
/> | ||
} | ||
/> | ||
); | ||
})} | ||
</StyledPinnedItemsContainer> | ||
); | ||
}; |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -2,16 +2,27 @@ import { useState } from 'react'; | |
import { useRecoilValue } from 'recoil'; | ||
import { v4 } from 'uuid'; | ||
|
||
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; | ||
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 { useFilterDropdown } from '@/object-record/object-filter-dropdown/hooks/useFilterDropdown'; | ||
import { RelationPickerHotkeyScope } from '@/object-record/relation-picker/types/RelationPickerHotkeyScope'; | ||
import { MultipleSelectDropdown } from '@/object-record/select/components/MultipleSelectDropdown'; | ||
import { useRecordsForSelect } from '@/object-record/select/hooks/useRecordsForSelect'; | ||
import { SelectableItem } from '@/object-record/select/types/SelectableItem'; | ||
import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownMenuSeparator'; | ||
import { useDeleteCombinedViewFilters } from '@/views/hooks/useDeleteCombinedViewFilters'; | ||
import { useGetCurrentView } from '@/views/hooks/useGetCurrentView'; | ||
import { RelationFilterValue } from '@/views/view-filter-value/types/RelationFilterValue'; | ||
import { relationFilterValueSchema } from '@/views/view-filter-value/validation-schemas/relationFilterValueSchema'; | ||
import { IconUserCircle } from 'twenty-ui'; | ||
import { isDefined } from '~/utils/isDefined'; | ||
|
||
export const EMPTY_FILTER_VALUE = '[]'; | ||
export const EMPTY_FILTER_VALUE: string = JSON.stringify({ | ||
isCurrentWorkspaceMemberSelected: false, | ||
selectedRecordIds: [], | ||
} satisfies RelationFilterValue); | ||
|
||
export const MAX_RECORDS_TO_DISPLAY = 3; | ||
|
||
type ObjectFilterDropdownRecordSelectProps = { | ||
|
@@ -54,9 +65,27 @@ export const ObjectFilterDropdownRecordSelect = ({ | |
|
||
const selectedFilter = useRecoilValue(selectedFilterState); | ||
|
||
const { isCurrentWorkspaceMemberSelected, selectedRecordIds } = | ||
relationFilterValueSchema | ||
.catch({ | ||
isCurrentWorkspaceMemberSelected: false, | ||
selectedRecordIds: [], | ||
}) | ||
.parse(selectedFilter?.value); | ||
|
||
const objectNameSingular = | ||
filterDefinitionUsedInDropdown?.relationObjectMetadataNameSingular; | ||
|
||
if (!isDefined(objectNameSingular)) { | ||
throw new Error('relationObjectMetadataNameSingular is not defined'); | ||
} | ||
|
||
const { objectMetadataItem } = useObjectMetadataItem({ | ||
objectNameSingular: objectNameSingular, | ||
}); | ||
|
||
const objectLabelPlural = objectMetadataItem?.labelPlural; | ||
|
||
if (!isDefined(objectNameSingular)) { | ||
throw new Error('objectNameSingular is not defined'); | ||
} | ||
Comment on lines
89
to
91
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. logic: Duplicate check for objectNameSingular - already checked on line 79 |
||
|
@@ -69,27 +98,53 @@ export const ObjectFilterDropdownRecordSelect = ({ | |
limit: 10, | ||
}); | ||
|
||
const currentWorkspaceMemberSelectableItem: SelectableItem = { | ||
id: CURRENT_WORKSPACE_MEMBER_SELECTABLE_ITEM_ID, | ||
name: 'Me', | ||
isSelected: isCurrentWorkspaceMemberSelected, | ||
AvatarIcon: IconUserCircle, | ||
}; | ||
|
||
const pinnedSelectableItems: SelectableItem[] = | ||
objectNameSingular === 'workspaceMember' | ||
? [currentWorkspaceMemberSelectableItem] | ||
: []; | ||
|
||
const filteredPinnedSelectableItems = pinnedSelectableItems.filter((item) => | ||
item.name | ||
.toLowerCase() | ||
.includes(objectFilterDropdownSearchInput.toLowerCase()), | ||
); | ||
|
||
const handleMultipleRecordSelectChange = ( | ||
recordToSelect: SelectableItem, | ||
newSelectedValue: boolean, | ||
itemToSelect: SelectableItem, | ||
isNewSelectedValue: boolean, | ||
) => { | ||
if (loading) { | ||
return; | ||
} | ||
|
||
const newSelectedRecordIds = newSelectedValue | ||
? [...objectFilterDropdownSelectedRecordIds, recordToSelect.id] | ||
: objectFilterDropdownSelectedRecordIds.filter( | ||
(id) => id !== recordToSelect.id, | ||
); | ||
const isItemCurrentWorkspaceMember = | ||
itemToSelect.id === CURRENT_WORKSPACE_MEMBER_SELECTABLE_ITEM_ID; | ||
|
||
if (newSelectedRecordIds.length === 0) { | ||
emptyFilterButKeepDefinition(); | ||
deleteCombinedViewFilter(fieldId); | ||
return; | ||
} | ||
const selectedRecordIdsWithAddedRecord = [ | ||
...objectFilterDropdownSelectedRecordIds, | ||
itemToSelect.id, | ||
]; | ||
const selectedRecordIdsWithRemovedRecord = | ||
objectFilterDropdownSelectedRecordIds.filter( | ||
(id) => id !== itemToSelect.id, | ||
); | ||
|
||
const newSelectedRecordIds = isItemCurrentWorkspaceMember | ||
? objectFilterDropdownSelectedRecordIds | ||
: isNewSelectedValue | ||
? selectedRecordIdsWithAddedRecord | ||
: selectedRecordIdsWithRemovedRecord; | ||
|
||
setObjectFilterDropdownSelectedRecordIds(newSelectedRecordIds); | ||
const newIsCurrentWorkspaceMemberSelected = isItemCurrentWorkspaceMember | ||
? isNewSelectedValue | ||
: isCurrentWorkspaceMemberSelected; | ||
|
||
const selectedRecordNames = [ | ||
...recordsToSelect, | ||
|
@@ -103,19 +158,32 @@ export const ObjectFilterDropdownRecordSelect = ({ | |
.filter((record) => newSelectedRecordIds.includes(record.id)) | ||
.map((record) => record.name); | ||
|
||
const selectedPinnedItemNames = newIsCurrentWorkspaceMemberSelected | ||
? [currentWorkspaceMemberSelectableItem.name] | ||
: []; | ||
|
||
const selectedItemNames = [ | ||
...selectedPinnedItemNames, | ||
...selectedRecordNames, | ||
]; | ||
|
||
const filterDisplayValue = | ||
selectedRecordNames.length > MAX_RECORDS_TO_DISPLAY | ||
? `${selectedRecordNames.length} companies` | ||
: selectedRecordNames.join(', '); | ||
selectedItemNames.length > MAX_RECORDS_TO_DISPLAY | ||
? `${selectedItemNames.length} ${objectLabelPlural.toLowerCase()}` | ||
: selectedItemNames.join(', '); | ||
|
||
if ( | ||
isDefined(filterDefinitionUsedInDropdown) && | ||
isDefined(selectedOperandInDropdown) | ||
) { | ||
const newFilterValue = | ||
newSelectedRecordIds.length > 0 | ||
? JSON.stringify(newSelectedRecordIds) | ||
: EMPTY_FILTER_VALUE; | ||
newSelectedRecordIds.length > 0 || newIsCurrentWorkspaceMemberSelected | ||
? JSON.stringify({ | ||
isCurrentWorkspaceMemberSelected: | ||
newIsCurrentWorkspaceMemberSelected, | ||
selectedRecordIds: newSelectedRecordIds, | ||
} satisfies RelationFilterValue) | ||
: ''; | ||
Comment on lines
179
to
+186
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. logic: Empty string used for empty filter instead of EMPTY_FILTER_VALUE constant defined on line 21 |
||
|
||
const viewFilter = | ||
currentViewWithCombinedFiltersAndSorts?.viewFilters.find( | ||
|
@@ -139,15 +207,26 @@ export const ObjectFilterDropdownRecordSelect = ({ | |
}; | ||
|
||
return ( | ||
<MultipleSelectDropdown | ||
selectableListId="object-filter-record-select-id" | ||
hotkeyScope={RelationPickerHotkeyScope.RelationPicker} | ||
itemsToSelect={recordsToSelect} | ||
filteredSelectedItems={filteredSelectedRecords} | ||
selectedItems={selectedRecords} | ||
onChange={handleMultipleRecordSelectChange} | ||
searchFilter={objectFilterDropdownSearchInput} | ||
loadingItems={loading} | ||
/> | ||
<> | ||
{filteredPinnedSelectableItems.length > 0 && ( | ||
<> | ||
<ObjectFilterDropdownRecordPinnedItems | ||
selectableItems={filteredPinnedSelectableItems} | ||
onChange={handleMultipleRecordSelectChange} | ||
/> | ||
<DropdownMenuSeparator /> | ||
</> | ||
)} | ||
<MultipleSelectDropdown | ||
selectableListId="object-filter-record-select-id" | ||
hotkeyScope={RelationPickerHotkeyScope.RelationPicker} | ||
itemsToSelect={recordsToSelect} | ||
filteredSelectedItems={filteredSelectedRecords} | ||
selectedItems={selectedRecords} | ||
onChange={handleMultipleRecordSelectChange} | ||
searchFilter={objectFilterDropdownSearchInput} | ||
loadingItems={loading} | ||
/> | ||
</> | ||
); | ||
}; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
export const CURRENT_WORKSPACE_MEMBER_SELECTABLE_ITEM_ID = | ||
'CURRENT_WORKSPACE_MEMBER'; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState'; | ||
import { FilterValueDependencies } from '@/object-record/record-filter/types/FilterValueDependencies'; | ||
import { useRecoilValue } from 'recoil'; | ||
|
||
export const useFilterValueDependencies = (): { | ||
filterValueDependencies: FilterValueDependencies; | ||
} => { | ||
Comment on lines
+5
to
+7
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. style: hook returns nested object structure that could be flattened to reduce unnecessary nesting |
||
const { id: currentWorkspaceMemberId } = | ||
useRecoilValue(currentWorkspaceMemberState) ?? {}; | ||
|
||
return { | ||
filterValueDependencies: { | ||
currentWorkspaceMemberId, | ||
}, | ||
}; | ||
}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
logic: filterValueDependencies is now required by computeContextStoreFilters but no error handling if dependencies are undefined