Refactor MultipleObjectsPicker component (#10552)
Refactor to only have MultipleRecordPicker and SingleRecordPicker What's done: - SingleRecordPicker, MultipleRecordPicker - RelationToOneInput - RelationFromManyInput - usage in TableCell, InlineCell, RelationDetailSection, Workflow What's left: - Make a pass on the app, to make sure the hotkeyScopes, clickOutside are properly set - Fix flashing on ActivityTarget - add more tests on the code
This commit is contained in:
@ -5,7 +5,7 @@ import { EMPTY_QUERY } from '@/object-record/constants/EmptyQuery';
|
||||
import { RecordGqlOperationSignature } from '@/object-record/graphql/types/RecordGqlOperationSignature';
|
||||
import { useCombinedFindManyRecordsQueryVariables } from '@/object-record/multiple-objects/hooks/useCombinedFindManyRecordsQueryVariables';
|
||||
import { useGenerateCombinedFindManyRecordsQuery } from '@/object-record/multiple-objects/hooks/useGenerateCombinedFindManyRecordsQuery';
|
||||
import { MultiObjectRecordQueryResult } from '@/object-record/multiple-objects/types/MultiObjectRecordQueryResult';
|
||||
import { CombinedFindManyRecordsQueryResult } from '@/object-record/multiple-objects/types/CombinedFindManyRecordsQueryResult';
|
||||
|
||||
export const useCombinedFindManyRecords = ({
|
||||
operationSignatures,
|
||||
@ -22,7 +22,7 @@ export const useCombinedFindManyRecords = ({
|
||||
operationSignatures,
|
||||
});
|
||||
|
||||
const { data, loading } = useQuery<MultiObjectRecordQueryResult>(
|
||||
const { data, loading } = useQuery<CombinedFindManyRecordsQueryResult>(
|
||||
findManyQuery ?? EMPTY_QUERY,
|
||||
{
|
||||
skip,
|
||||
|
||||
@ -4,7 +4,7 @@ import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||
import { EMPTY_QUERY } from '@/object-record/constants/EmptyQuery';
|
||||
import { RecordGqlOperationSignature } from '@/object-record/graphql/types/RecordGqlOperationSignature';
|
||||
import { useGenerateCombinedFindManyRecordsQuery } from '@/object-record/multiple-objects/hooks/useGenerateCombinedFindManyRecordsQuery';
|
||||
import { MultiObjectRecordQueryResult } from '@/object-record/multiple-objects/types/MultiObjectRecordQueryResult';
|
||||
import { CombinedFindManyRecordsQueryResult } from '@/object-record/multiple-objects/types/CombinedFindManyRecordsQueryResult';
|
||||
|
||||
export const useCombinedGetTotalCount = ({
|
||||
objectMetadataItems,
|
||||
@ -28,7 +28,7 @@ export const useCombinedGetTotalCount = ({
|
||||
operationSignatures,
|
||||
});
|
||||
|
||||
const { data } = useQuery<MultiObjectRecordQueryResult>(
|
||||
const { data } = useQuery<CombinedFindManyRecordsQueryResult>(
|
||||
findManyQuery ?? EMPTY_QUERY,
|
||||
{
|
||||
skip,
|
||||
|
||||
@ -1,12 +1,8 @@
|
||||
import { gql } from '@apollo/client';
|
||||
import { isUndefined } from '@sniptt/guards';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
|
||||
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
|
||||
import { mapObjectMetadataToGraphQLQuery } from '@/object-metadata/utils/mapObjectMetadataToGraphQLQuery';
|
||||
import { RecordGqlOperationSignature } from '@/object-record/graphql/types/RecordGqlOperationSignature';
|
||||
import { getSearchRecordsQueryResponseField } from '@/object-record/utils/getSearchRecordsQueryResponseField';
|
||||
import { capitalize } from 'twenty-shared';
|
||||
import { generateCombinedSearchRecordsQuery } from '@/object-record/multiple-objects/utils/generateCombinedSearchRecordsQuery';
|
||||
import { isNonEmptyArray } from '~/utils/isNonEmptyArray';
|
||||
|
||||
export const useGenerateCombinedSearchRecordsQuery = ({
|
||||
@ -20,70 +16,8 @@ export const useGenerateCombinedSearchRecordsQuery = ({
|
||||
return null;
|
||||
}
|
||||
|
||||
const filterPerMetadataItemArray = operationSignatures
|
||||
.map(
|
||||
({ objectNameSingular }) =>
|
||||
`$filter${capitalize(objectNameSingular)}: ${capitalize(
|
||||
objectNameSingular,
|
||||
)}FilterInput`,
|
||||
)
|
||||
.join(', ');
|
||||
|
||||
const limitPerMetadataItemArray = operationSignatures
|
||||
.map(
|
||||
({ objectNameSingular }) =>
|
||||
`$limit${capitalize(objectNameSingular)}: Int`,
|
||||
)
|
||||
.join(', ');
|
||||
|
||||
const queryKeyWithObjectMetadataItemArray = operationSignatures.map(
|
||||
(queryKey) => {
|
||||
const objectMetadataItem = objectMetadataItems.find(
|
||||
(objectMetadataItem) =>
|
||||
objectMetadataItem.nameSingular === queryKey.objectNameSingular,
|
||||
);
|
||||
|
||||
if (isUndefined(objectMetadataItem)) {
|
||||
throw new Error(
|
||||
`Object metadata item not found for object name singular: ${queryKey.objectNameSingular}`,
|
||||
);
|
||||
}
|
||||
|
||||
return { ...queryKey, objectMetadataItem };
|
||||
},
|
||||
);
|
||||
|
||||
const filteredQueryKeyWithObjectMetadataItemArray =
|
||||
queryKeyWithObjectMetadataItemArray.filter(
|
||||
({ objectMetadataItem }) => objectMetadataItem.isSearchable,
|
||||
);
|
||||
|
||||
return gql`
|
||||
query CombinedSearchRecords(
|
||||
${filterPerMetadataItemArray},
|
||||
${limitPerMetadataItemArray},
|
||||
$search: String,
|
||||
) {
|
||||
${filteredQueryKeyWithObjectMetadataItemArray
|
||||
.map(
|
||||
({ objectMetadataItem }) =>
|
||||
`${getSearchRecordsQueryResponseField(objectMetadataItem.namePlural)}(filter: $filter${capitalize(
|
||||
objectMetadataItem.nameSingular,
|
||||
)},
|
||||
limit: $limit${capitalize(objectMetadataItem.nameSingular)},
|
||||
searchInput: $search
|
||||
){
|
||||
edges {
|
||||
node ${mapObjectMetadataToGraphQLQuery({
|
||||
objectMetadataItems: objectMetadataItems,
|
||||
objectMetadataItem,
|
||||
})}
|
||||
cursor
|
||||
}
|
||||
totalCount
|
||||
}`,
|
||||
)
|
||||
.join('\n')}
|
||||
}
|
||||
`;
|
||||
return generateCombinedSearchRecordsQuery({
|
||||
objectMetadataItems,
|
||||
operationSignatures,
|
||||
});
|
||||
};
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { RecordGqlConnection } from '@/object-record/graphql/types/RecordGqlConnection';
|
||||
|
||||
export type MultiObjectRecordQueryResult = {
|
||||
export type CombinedFindManyRecordsQueryResult = {
|
||||
[namePlural: string]: RecordGqlConnection;
|
||||
};
|
||||
@ -1,16 +0,0 @@
|
||||
import { MultiObjectRecordQueryResult } from '@/object-record/multiple-objects/types/MultiObjectRecordQueryResult';
|
||||
|
||||
export const formatMultiObjectRecordSearchResults = (
|
||||
searchResults: MultiObjectRecordQueryResult | undefined | null,
|
||||
): MultiObjectRecordQueryResult => {
|
||||
if (!searchResults) {
|
||||
return {};
|
||||
}
|
||||
|
||||
return Object.entries(searchResults).reduce((acc, [key, value]) => {
|
||||
let newKey = key.replace(/^search/, '');
|
||||
newKey = newKey.charAt(0).toLowerCase() + newKey.slice(1);
|
||||
acc[newKey] = value;
|
||||
return acc;
|
||||
}, {} as MultiObjectRecordQueryResult);
|
||||
};
|
||||
@ -0,0 +1,82 @@
|
||||
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||
import { mapObjectMetadataToGraphQLQuery } from '@/object-metadata/utils/mapObjectMetadataToGraphQLQuery';
|
||||
import { RecordGqlOperationSignature } from '@/object-record/graphql/types/RecordGqlOperationSignature';
|
||||
import { getSearchRecordsQueryResponseField } from '@/object-record/utils/getSearchRecordsQueryResponseField';
|
||||
import { isUndefined } from '@sniptt/guards';
|
||||
import gql from 'graphql-tag';
|
||||
import { capitalize } from 'twenty-shared';
|
||||
|
||||
export const generateCombinedSearchRecordsQuery = ({
|
||||
objectMetadataItems,
|
||||
operationSignatures,
|
||||
}: {
|
||||
objectMetadataItems: ObjectMetadataItem[];
|
||||
operationSignatures: RecordGqlOperationSignature[];
|
||||
}) => {
|
||||
const filterPerMetadataItemArray = operationSignatures
|
||||
.map(
|
||||
({ objectNameSingular }) =>
|
||||
`$filter${capitalize(objectNameSingular)}: ${capitalize(
|
||||
objectNameSingular,
|
||||
)}FilterInput`,
|
||||
)
|
||||
.join(', ');
|
||||
|
||||
const limitPerMetadataItemArray = operationSignatures
|
||||
.map(
|
||||
({ objectNameSingular }) =>
|
||||
`$limit${capitalize(objectNameSingular)}: Int`,
|
||||
)
|
||||
.join(', ');
|
||||
|
||||
const queryKeyWithObjectMetadataItemArray = operationSignatures.map(
|
||||
(queryKey) => {
|
||||
const objectMetadataItem = objectMetadataItems.find(
|
||||
(objectMetadataItem) =>
|
||||
objectMetadataItem.nameSingular === queryKey.objectNameSingular,
|
||||
);
|
||||
|
||||
if (isUndefined(objectMetadataItem)) {
|
||||
throw new Error(
|
||||
`Object metadata item not found for object name singular: ${queryKey.objectNameSingular}`,
|
||||
);
|
||||
}
|
||||
|
||||
return { ...queryKey, objectMetadataItem };
|
||||
},
|
||||
);
|
||||
|
||||
const filteredQueryKeyWithObjectMetadataItemArray =
|
||||
queryKeyWithObjectMetadataItemArray.filter(
|
||||
({ objectMetadataItem }) => objectMetadataItem.isSearchable,
|
||||
);
|
||||
|
||||
return gql`
|
||||
query CombinedSearchRecords(
|
||||
${filterPerMetadataItemArray},
|
||||
${limitPerMetadataItemArray},
|
||||
$search: String,
|
||||
) {
|
||||
${filteredQueryKeyWithObjectMetadataItemArray
|
||||
.map(
|
||||
({ objectMetadataItem }) =>
|
||||
`${getSearchRecordsQueryResponseField(objectMetadataItem.namePlural)}(filter: $filter${capitalize(
|
||||
objectMetadataItem.nameSingular,
|
||||
)},
|
||||
limit: $limit${capitalize(objectMetadataItem.nameSingular)},
|
||||
searchInput: $search
|
||||
){
|
||||
edges {
|
||||
node ${mapObjectMetadataToGraphQLQuery({
|
||||
objectMetadataItems: objectMetadataItems,
|
||||
objectMetadataItem,
|
||||
})}
|
||||
cursor
|
||||
}
|
||||
totalCount
|
||||
}`,
|
||||
)
|
||||
.join('\n')}
|
||||
}
|
||||
`;
|
||||
};
|
||||
@ -7,8 +7,8 @@ import { getFilterTypeFromFieldType } from '@/object-metadata/utils/formatFieldM
|
||||
import { fieldMetadataItemUsedInDropdownComponentSelector } from '@/object-record/object-filter-dropdown/states/fieldMetadataItemUsedInDropdownComponentSelector';
|
||||
import { selectedFilterComponentState } from '@/object-record/object-filter-dropdown/states/selectedFilterComponentState';
|
||||
import { selectedOperandInDropdownComponentState } from '@/object-record/object-filter-dropdown/states/selectedOperandInDropdownComponentState';
|
||||
import { RelationPickerHotkeyScope } from '@/object-record/record-field/meta-types/input/types/RelationPickerHotkeyScope';
|
||||
import { useApplyRecordFilter } from '@/object-record/record-filter/hooks/useApplyRecordFilter';
|
||||
import { SingleRecordPickerHotkeyScope } from '@/object-record/record-picker/single-record-picker/types/SingleRecordPickerHotkeyScope';
|
||||
import { BooleanDisplay } from '@/ui/field/display/components/BooleanDisplay';
|
||||
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
|
||||
import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown';
|
||||
@ -91,7 +91,7 @@ export const ObjectFilterDropdownBooleanSelect = () => {
|
||||
<SelectableList
|
||||
selectableListId="boolean-select"
|
||||
selectableItemIdArray={options.map((option) => option.toString())}
|
||||
hotkeyScope={RelationPickerHotkeyScope.RelationPicker}
|
||||
hotkeyScope={SingleRecordPickerHotkeyScope.SingleRecordPicker}
|
||||
onEnter={(itemId) => {
|
||||
handleOptionSelect(itemId === 'true');
|
||||
}}
|
||||
|
||||
@ -15,7 +15,7 @@ import { isCompositeField } from '@/object-record/object-filter-dropdown/utils/i
|
||||
import { currentRecordFiltersComponentState } from '@/object-record/record-filter/states/currentRecordFiltersComponentState';
|
||||
import { findDuplicateRecordFilterInNonAdvancedRecordFilters } from '@/object-record/record-filter/utils/findDuplicateRecordFilterInNonAdvancedRecordFilters';
|
||||
import { getRecordFilterOperands } from '@/object-record/record-filter/utils/getRecordFilterOperands';
|
||||
import { RecordPickerHotkeyScope } from '@/object-record/record-picker/types/RecordPickerHotkeyScope';
|
||||
import { SingleRecordPickerHotkeyScope } from '@/object-record/record-picker/single-record-picker/types/SingleRecordPickerHotkeyScope';
|
||||
import { useSelectableList } from '@/ui/layout/selectable-list/hooks/useSelectableList';
|
||||
import { useSetHotkeyScope } from '@/ui/utilities/hotkey/hooks/useSetHotkeyScope';
|
||||
import { useRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentStateV2';
|
||||
@ -87,7 +87,7 @@ export const ObjectFilterDropdownFilterSelectMenuItem = ({
|
||||
const filterType = getFilterTypeFromFieldType(fieldMetadataItem.type);
|
||||
|
||||
if (filterType === 'RELATION' || filterType === 'SELECT') {
|
||||
setHotkeyScope(RecordPickerHotkeyScope.RecordPicker);
|
||||
setHotkeyScope(SingleRecordPickerHotkeyScope.SingleRecordPicker);
|
||||
}
|
||||
|
||||
const defaultOperand = getRecordFilterOperands({
|
||||
|
||||
@ -18,9 +18,8 @@ import { objectFilterDropdownSearchInputComponentState } from '@/object-record/o
|
||||
import { objectFilterDropdownSelectedOptionValuesComponentState } from '@/object-record/object-filter-dropdown/states/objectFilterDropdownSelectedOptionValuesComponentState';
|
||||
import { selectedFilterComponentState } from '@/object-record/object-filter-dropdown/states/selectedFilterComponentState';
|
||||
import { selectedOperandInDropdownComponentState } from '@/object-record/object-filter-dropdown/states/selectedOperandInDropdownComponentState';
|
||||
import { RelationPickerHotkeyScope } from '@/object-record/record-field/meta-types/input/types/RelationPickerHotkeyScope';
|
||||
import { useApplyRecordFilter } from '@/object-record/record-filter/hooks/useApplyRecordFilter';
|
||||
import { RecordPickerHotkeyScope } from '@/object-record/record-picker/types/RecordPickerHotkeyScope';
|
||||
import { SingleRecordPickerHotkeyScope } from '@/object-record/record-picker/single-record-picker/types/SingleRecordPickerHotkeyScope';
|
||||
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
|
||||
import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
|
||||
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
|
||||
@ -102,7 +101,7 @@ export const ObjectFilterDropdownOptionSelect = () => {
|
||||
closeDropdown();
|
||||
resetSelectedItem();
|
||||
},
|
||||
RelationPickerHotkeyScope.RelationPicker,
|
||||
SingleRecordPickerHotkeyScope.SingleRecordPicker,
|
||||
[closeDropdown, resetSelectedItem],
|
||||
);
|
||||
|
||||
@ -165,7 +164,7 @@ export const ObjectFilterDropdownOptionSelect = () => {
|
||||
<SelectableList
|
||||
selectableListId={componentInstanceId}
|
||||
selectableItemIdArray={objectRecordsIds}
|
||||
hotkeyScope={RecordPickerHotkeyScope.RecordPicker}
|
||||
hotkeyScope={SingleRecordPickerHotkeyScope.SingleRecordPicker}
|
||||
onEnter={(itemId) => {
|
||||
const option = optionsInDropdown.find((option) => option.id === itemId);
|
||||
if (isDefined(option)) {
|
||||
|
||||
@ -12,7 +12,7 @@ import { selectedFilterComponentState } from '@/object-record/object-filter-drop
|
||||
import { selectedOperandInDropdownComponentState } from '@/object-record/object-filter-dropdown/states/selectedOperandInDropdownComponentState';
|
||||
import { useApplyRecordFilter } from '@/object-record/record-filter/hooks/useApplyRecordFilter';
|
||||
import { currentRecordFiltersComponentState } from '@/object-record/record-filter/states/currentRecordFiltersComponentState';
|
||||
import { RecordPickerHotkeyScope } from '@/object-record/record-picker/types/RecordPickerHotkeyScope';
|
||||
import { SingleRecordPickerHotkeyScope } from '@/object-record/record-picker/single-record-picker/types/SingleRecordPickerHotkeyScope';
|
||||
import { MultipleSelectDropdown } from '@/object-record/select/components/MultipleSelectDropdown';
|
||||
import { useRecordsForSelect } from '@/object-record/select/hooks/useRecordsForSelect';
|
||||
import { SelectableItem } from '@/object-record/select/types/SelectableItem';
|
||||
@ -232,7 +232,7 @@ export const ObjectFilterDropdownRecordSelect = ({
|
||||
)}
|
||||
<MultipleSelectDropdown
|
||||
selectableListId="object-filter-record-select-id"
|
||||
hotkeyScope={RecordPickerHotkeyScope.RecordPicker}
|
||||
hotkeyScope={SingleRecordPickerHotkeyScope.SingleRecordPicker}
|
||||
itemsToSelect={recordsToSelect}
|
||||
filteredSelectedItems={filteredSelectedRecords}
|
||||
selectedItems={selectedRecords}
|
||||
|
||||
@ -12,7 +12,7 @@ import { getActorSourceMultiSelectOptions } from '@/object-record/object-filter-
|
||||
import { useApplyRecordFilter } from '@/object-record/record-filter/hooks/useApplyRecordFilter';
|
||||
import { useRemoveRecordFilter } from '@/object-record/record-filter/hooks/useRemoveRecordFilter';
|
||||
import { currentRecordFiltersComponentState } from '@/object-record/record-filter/states/currentRecordFiltersComponentState';
|
||||
import { RecordPickerHotkeyScope } from '@/object-record/record-picker/types/RecordPickerHotkeyScope';
|
||||
import { SingleRecordPickerHotkeyScope } from '@/object-record/record-picker/single-record-picker/types/SingleRecordPickerHotkeyScope';
|
||||
import { MultipleSelectDropdown } from '@/object-record/select/components/MultipleSelectDropdown';
|
||||
import { SelectableItem } from '@/object-record/select/types/SelectableItem';
|
||||
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
|
||||
@ -144,7 +144,7 @@ export const ObjectFilterDropdownSourceSelect = ({
|
||||
return (
|
||||
<MultipleSelectDropdown
|
||||
selectableListId="object-filter-source-select-id"
|
||||
hotkeyScope={RecordPickerHotkeyScope.RecordPicker}
|
||||
hotkeyScope={SingleRecordPickerHotkeyScope.SingleRecordPicker}
|
||||
itemsToSelect={sourceTypes.filter(
|
||||
(item) =>
|
||||
!filteredSelectedItems.some((selected) => selected.id === item.id),
|
||||
|
||||
@ -8,7 +8,7 @@ import { selectedOperandInDropdownComponentState } from '@/object-record/object-
|
||||
import { getInitialFilterValue } from '@/object-record/object-filter-dropdown/utils/getInitialFilterValue';
|
||||
import { useApplyRecordFilter } from '@/object-record/record-filter/hooks/useApplyRecordFilter';
|
||||
import { getRecordFilterOperands } from '@/object-record/record-filter/utils/getRecordFilterOperands';
|
||||
import { RecordPickerHotkeyScope } from '@/object-record/record-picker/types/RecordPickerHotkeyScope';
|
||||
import { SingleRecordPickerHotkeyScope } from '@/object-record/record-picker/single-record-picker/types/SingleRecordPickerHotkeyScope';
|
||||
|
||||
import { useSetHotkeyScope } from '@/ui/utilities/hotkey/hooks/useSetHotkeyScope';
|
||||
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
|
||||
@ -67,7 +67,7 @@ export const useSelectFilterUsedInDropdown = (componentInstanceId?: string) => {
|
||||
fieldMetadataItem.type === 'RELATION' ||
|
||||
fieldMetadataItem.type === 'SELECT'
|
||||
) {
|
||||
setHotkeyScope(RecordPickerHotkeyScope.RecordPicker);
|
||||
setHotkeyScope(SingleRecordPickerHotkeyScope.SingleRecordPicker);
|
||||
}
|
||||
|
||||
const filterType = getFilterTypeFromFieldType(fieldMetadataItem.type);
|
||||
|
||||
@ -2,8 +2,7 @@ import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSi
|
||||
import { useCreateOneRecord } from '@/object-record/hooks/useCreateOneRecord';
|
||||
import { useAddNewCard } from '@/object-record/record-board/record-board-column/hooks/useAddNewCard';
|
||||
import { recordBoardNewRecordByColumnIdSelector } from '@/object-record/record-board/states/selectors/recordBoardNewRecordByColumnIdSelector';
|
||||
import { SingleRecordPicker } from '@/object-record/record-picker/components/SingleRecordPicker';
|
||||
import { RecordPickerComponentInstanceContext } from '@/object-record/record-picker/states/contexts/RecordPickerComponentInstanceContext';
|
||||
import { SingleRecordPicker } from '@/object-record/record-picker/single-record-picker/components/SingleRecordPicker';
|
||||
import { viewableRecordIdState } from '@/object-record/record-right-drawer/states/viewableRecordIdState';
|
||||
import { viewableRecordNameSingularState } from '@/object-record/record-right-drawer/states/viewableRecordNameSingularState';
|
||||
import { OverlayContainer } from '@/ui/layout/overlay/components/OverlayContainer';
|
||||
@ -68,22 +67,15 @@ export const RecordBoardColumnNewOpportunity = ({
|
||||
<>
|
||||
{newRecord.isCreating && newRecord.position === position && (
|
||||
<OverlayContainer>
|
||||
<RecordPickerComponentInstanceContext.Provider
|
||||
value={{
|
||||
instanceId: `add-new-card-record-picker-column-${columnId}`,
|
||||
}}
|
||||
>
|
||||
<SingleRecordPicker
|
||||
componentInstanceId={`add-new-card-record-picker-column-${columnId}`}
|
||||
onCancel={() => handleCreateSuccess(position, columnId, false)}
|
||||
onRecordSelected={(company) =>
|
||||
company ? handleEntitySelect(position, company) : null
|
||||
}
|
||||
objectNameSingular={CoreObjectNameSingular.Company}
|
||||
selectedRecordIds={[]}
|
||||
onCreate={createCompanyOpportunityAndOpenRightDrawer}
|
||||
/>
|
||||
</RecordPickerComponentInstanceContext.Provider>
|
||||
<SingleRecordPicker
|
||||
componentInstanceId={`add-new-card-record-picker-column-${columnId}`}
|
||||
onCancel={() => handleCreateSuccess(position, columnId, false)}
|
||||
onRecordSelected={(company) =>
|
||||
company ? handleEntitySelect(position, company) : null
|
||||
}
|
||||
objectNameSingular={CoreObjectNameSingular.Company}
|
||||
onCreate={createCompanyOpportunityAndOpenRightDrawer}
|
||||
/>
|
||||
</OverlayContainer>
|
||||
)}
|
||||
</>
|
||||
|
||||
@ -1,9 +1,9 @@
|
||||
import { RecordBoardContext } from '@/object-record/record-board/contexts/RecordBoardContext';
|
||||
import { RecordBoardColumnContext } from '@/object-record/record-board/record-board-column/contexts/RecordBoardColumnContext';
|
||||
import { recordBoardNewRecordByColumnIdSelector } from '@/object-record/record-board/states/selectors/recordBoardNewRecordByColumnIdSelector';
|
||||
import { RelationPickerHotkeyScope } from '@/object-record/record-field/meta-types/input/types/RelationPickerHotkeyScope';
|
||||
import { useRecordSelectSearch } from '@/object-record/record-picker/hooks/useRecordSelectSearch';
|
||||
import { SingleRecordPickerRecord } from '@/object-record/record-picker/types/SingleRecordPickerRecord';
|
||||
import { useSingleRecordPickerSearch } from '@/object-record/record-picker/single-record-picker/hooks/useSingleRecordPickerSearch';
|
||||
import { SingleRecordPickerHotkeyScope } from '@/object-record/record-picker/single-record-picker/types/SingleRecordPickerHotkeyScope';
|
||||
import { SingleRecordPickerRecord } from '@/object-record/record-picker/single-record-picker/types/SingleRecordPickerRecord';
|
||||
import { usePreviousHotkeyScope } from '@/ui/utilities/hotkey/hooks/usePreviousHotkeyScope';
|
||||
import { useCallback, useContext } from 'react';
|
||||
import { RecoilState, useRecoilCallback } from 'recoil';
|
||||
@ -26,7 +26,7 @@ export const useAddNewCard = ({
|
||||
const columnContext = useContext(RecordBoardColumnContext);
|
||||
const { createOneRecord, selectFieldMetadataItem, objectMetadataItem } =
|
||||
useContext(RecordBoardContext);
|
||||
const { resetSearchFilter } = useRecordSelectSearch(
|
||||
const { resetSearchFilter } = useSingleRecordPickerSearch(
|
||||
recordPickerComponentInstanceId,
|
||||
);
|
||||
|
||||
@ -139,7 +139,7 @@ export const useAddNewCard = ({
|
||||
addNewItem(set, columnDefinitionId, position, isOpportunity);
|
||||
if (isOpportunity) {
|
||||
setHotkeyScopeAndMemorizePreviousScope(
|
||||
RelationPickerHotkeyScope.RelationPicker,
|
||||
SingleRecordPickerHotkeyScope.SingleRecordPicker,
|
||||
);
|
||||
} else {
|
||||
createRecord(labelIdentifier, labelValue, position, isOpportunity);
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { SingleRecordPickerRecord } from '@/object-record/record-picker/types/SingleRecordPickerRecord';
|
||||
import { SingleRecordPickerRecord } from '@/object-record/record-picker/single-record-picker/types/SingleRecordPickerRecord';
|
||||
import { createComponentFamilyState } from '@/ui/utilities/state/component-state/utils/createComponentFamilyState';
|
||||
|
||||
export type NewCard = {
|
||||
|
||||
@ -17,6 +17,7 @@ import { isFieldRelationToOneObject } from '@/object-record/record-field/types/g
|
||||
import { getScopeIdFromComponentId } from '@/ui/utilities/recoil-scope/utils/getScopeIdFromComponentId';
|
||||
|
||||
import { ArrayFieldInput } from '@/object-record/record-field/meta-types/input/components/ArrayFieldInput';
|
||||
import { RecordFieldComponentInstanceContext } from '@/object-record/record-field/states/contexts/RecordFieldComponentInstanceContext';
|
||||
import { isFieldAddress } from '@/object-record/record-field/types/guards/isFieldAddress';
|
||||
import { isFieldArray } from '@/object-record/record-field/types/guards/isFieldArray';
|
||||
import { isFieldBoolean } from '@/object-record/record-field/types/guards/isFieldBoolean';
|
||||
@ -71,108 +72,114 @@ export const FieldInput = ({
|
||||
const { fieldDefinition } = useContext(FieldContext);
|
||||
|
||||
return (
|
||||
<RecordFieldInputScope
|
||||
recordFieldInputScopeId={getScopeIdFromComponentId(recordFieldInputdId)}
|
||||
<RecordFieldComponentInstanceContext.Provider
|
||||
value={{
|
||||
instanceId: recordFieldInputdId,
|
||||
}}
|
||||
>
|
||||
{isFieldRelationToOneObject(fieldDefinition) ? (
|
||||
<RelationToOneFieldInput onSubmit={onSubmit} onCancel={onCancel} />
|
||||
) : isFieldRelationFromManyObjects(fieldDefinition) ? (
|
||||
<RelationFromManyFieldInput onSubmit={onSubmit} />
|
||||
) : isFieldPhones(fieldDefinition) ? (
|
||||
<PhonesFieldInput
|
||||
onCancel={onCancel}
|
||||
onClickOutside={(event) => onClickOutside?.(() => {}, event)}
|
||||
/>
|
||||
) : isFieldText(fieldDefinition) ? (
|
||||
<TextFieldInput
|
||||
onEnter={onEnter}
|
||||
onEscape={onEscape}
|
||||
onClickOutside={onClickOutside}
|
||||
onTab={onTab}
|
||||
onShiftTab={onShiftTab}
|
||||
/>
|
||||
) : isFieldEmails(fieldDefinition) ? (
|
||||
<EmailsFieldInput
|
||||
onCancel={onCancel}
|
||||
onClickOutside={(event) => onClickOutside?.(() => {}, event)}
|
||||
/>
|
||||
) : isFieldFullName(fieldDefinition) ? (
|
||||
<FullNameFieldInput
|
||||
onEnter={onEnter}
|
||||
onEscape={onEscape}
|
||||
onClickOutside={onClickOutside}
|
||||
onTab={onTab}
|
||||
onShiftTab={onShiftTab}
|
||||
/>
|
||||
) : isFieldDateTime(fieldDefinition) ? (
|
||||
<DateTimeFieldInput
|
||||
onEnter={onEnter}
|
||||
onEscape={onEscape}
|
||||
onClickOutside={onClickOutside}
|
||||
onClear={onSubmit}
|
||||
onSubmit={onSubmit}
|
||||
/>
|
||||
) : isFieldDate(fieldDefinition) ? (
|
||||
<DateFieldInput
|
||||
onEnter={onEnter}
|
||||
onEscape={onEscape}
|
||||
onClickOutside={onClickOutside}
|
||||
onClear={onSubmit}
|
||||
onSubmit={onSubmit}
|
||||
/>
|
||||
) : isFieldNumber(fieldDefinition) ? (
|
||||
<NumberFieldInput
|
||||
onEnter={onEnter}
|
||||
onEscape={onEscape}
|
||||
onClickOutside={onClickOutside}
|
||||
onTab={onTab}
|
||||
onShiftTab={onShiftTab}
|
||||
/>
|
||||
) : isFieldLinks(fieldDefinition) ? (
|
||||
<LinksFieldInput
|
||||
onCancel={onCancel}
|
||||
onClickOutside={(event) => onClickOutside?.(() => {}, event)}
|
||||
/>
|
||||
) : isFieldCurrency(fieldDefinition) ? (
|
||||
<CurrencyFieldInput
|
||||
onEnter={onEnter}
|
||||
onEscape={onEscape}
|
||||
onClickOutside={onClickOutside}
|
||||
onTab={onTab}
|
||||
onShiftTab={onShiftTab}
|
||||
/>
|
||||
) : isFieldBoolean(fieldDefinition) ? (
|
||||
<BooleanFieldInput onSubmit={onSubmit} readonly={isReadOnly} />
|
||||
) : isFieldRating(fieldDefinition) ? (
|
||||
<RatingFieldInput onSubmit={onSubmit} />
|
||||
) : isFieldSelect(fieldDefinition) ? (
|
||||
<SelectFieldInput onSubmit={onSubmit} onCancel={onCancel} />
|
||||
) : isFieldMultiSelect(fieldDefinition) ? (
|
||||
<MultiSelectFieldInput onCancel={onCancel} />
|
||||
) : isFieldAddress(fieldDefinition) ? (
|
||||
<AddressFieldInput
|
||||
onEnter={onEnter}
|
||||
onEscape={onEscape}
|
||||
onClickOutside={onClickOutside}
|
||||
onTab={onTab}
|
||||
onShiftTab={onShiftTab}
|
||||
/>
|
||||
) : isFieldRawJson(fieldDefinition) ? (
|
||||
<RawJsonFieldInput
|
||||
onEnter={onEnter}
|
||||
onEscape={onEscape}
|
||||
onClickOutside={onClickOutside}
|
||||
onTab={onTab}
|
||||
onShiftTab={onShiftTab}
|
||||
/>
|
||||
) : isFieldArray(fieldDefinition) ? (
|
||||
<ArrayFieldInput
|
||||
onCancel={onCancel}
|
||||
onClickOutside={(event) => onClickOutside?.(() => {}, event)}
|
||||
/>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
</RecordFieldInputScope>
|
||||
<RecordFieldInputScope
|
||||
recordFieldInputScopeId={getScopeIdFromComponentId(recordFieldInputdId)}
|
||||
>
|
||||
{isFieldRelationToOneObject(fieldDefinition) ? (
|
||||
<RelationToOneFieldInput onSubmit={onSubmit} onCancel={onCancel} />
|
||||
) : isFieldRelationFromManyObjects(fieldDefinition) ? (
|
||||
<RelationFromManyFieldInput onSubmit={onSubmit} />
|
||||
) : isFieldPhones(fieldDefinition) ? (
|
||||
<PhonesFieldInput
|
||||
onCancel={onCancel}
|
||||
onClickOutside={(event) => onClickOutside?.(() => {}, event)}
|
||||
/>
|
||||
) : isFieldText(fieldDefinition) ? (
|
||||
<TextFieldInput
|
||||
onEnter={onEnter}
|
||||
onEscape={onEscape}
|
||||
onClickOutside={onClickOutside}
|
||||
onTab={onTab}
|
||||
onShiftTab={onShiftTab}
|
||||
/>
|
||||
) : isFieldEmails(fieldDefinition) ? (
|
||||
<EmailsFieldInput
|
||||
onCancel={onCancel}
|
||||
onClickOutside={(event) => onClickOutside?.(() => {}, event)}
|
||||
/>
|
||||
) : isFieldFullName(fieldDefinition) ? (
|
||||
<FullNameFieldInput
|
||||
onEnter={onEnter}
|
||||
onEscape={onEscape}
|
||||
onClickOutside={onClickOutside}
|
||||
onTab={onTab}
|
||||
onShiftTab={onShiftTab}
|
||||
/>
|
||||
) : isFieldDateTime(fieldDefinition) ? (
|
||||
<DateTimeFieldInput
|
||||
onEnter={onEnter}
|
||||
onEscape={onEscape}
|
||||
onClickOutside={onClickOutside}
|
||||
onClear={onSubmit}
|
||||
onSubmit={onSubmit}
|
||||
/>
|
||||
) : isFieldDate(fieldDefinition) ? (
|
||||
<DateFieldInput
|
||||
onEnter={onEnter}
|
||||
onEscape={onEscape}
|
||||
onClickOutside={onClickOutside}
|
||||
onClear={onSubmit}
|
||||
onSubmit={onSubmit}
|
||||
/>
|
||||
) : isFieldNumber(fieldDefinition) ? (
|
||||
<NumberFieldInput
|
||||
onEnter={onEnter}
|
||||
onEscape={onEscape}
|
||||
onClickOutside={onClickOutside}
|
||||
onTab={onTab}
|
||||
onShiftTab={onShiftTab}
|
||||
/>
|
||||
) : isFieldLinks(fieldDefinition) ? (
|
||||
<LinksFieldInput
|
||||
onCancel={onCancel}
|
||||
onClickOutside={(event) => onClickOutside?.(() => {}, event)}
|
||||
/>
|
||||
) : isFieldCurrency(fieldDefinition) ? (
|
||||
<CurrencyFieldInput
|
||||
onEnter={onEnter}
|
||||
onEscape={onEscape}
|
||||
onClickOutside={onClickOutside}
|
||||
onTab={onTab}
|
||||
onShiftTab={onShiftTab}
|
||||
/>
|
||||
) : isFieldBoolean(fieldDefinition) ? (
|
||||
<BooleanFieldInput onSubmit={onSubmit} readonly={isReadOnly} />
|
||||
) : isFieldRating(fieldDefinition) ? (
|
||||
<RatingFieldInput onSubmit={onSubmit} />
|
||||
) : isFieldSelect(fieldDefinition) ? (
|
||||
<SelectFieldInput onSubmit={onSubmit} onCancel={onCancel} />
|
||||
) : isFieldMultiSelect(fieldDefinition) ? (
|
||||
<MultiSelectFieldInput onCancel={onCancel} />
|
||||
) : isFieldAddress(fieldDefinition) ? (
|
||||
<AddressFieldInput
|
||||
onEnter={onEnter}
|
||||
onEscape={onEscape}
|
||||
onClickOutside={onClickOutside}
|
||||
onTab={onTab}
|
||||
onShiftTab={onShiftTab}
|
||||
/>
|
||||
) : isFieldRawJson(fieldDefinition) ? (
|
||||
<RawJsonFieldInput
|
||||
onEnter={onEnter}
|
||||
onEscape={onEscape}
|
||||
onClickOutside={onClickOutside}
|
||||
onTab={onTab}
|
||||
onShiftTab={onShiftTab}
|
||||
/>
|
||||
) : isFieldArray(fieldDefinition) ? (
|
||||
<ArrayFieldInput
|
||||
onCancel={onCancel}
|
||||
onClickOutside={(event) => onClickOutside?.(() => {}, event)}
|
||||
/>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
</RecordFieldInputScope>
|
||||
</RecordFieldComponentInstanceContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
@ -0,0 +1,46 @@
|
||||
import { useOpenRelationFromManyFieldInput } from '@/object-record/record-field/meta-types/input/hooks/useOpenRelationFromManyFieldInput';
|
||||
import { useOpenRelationToOneFieldInput } from '@/object-record/record-field/meta-types/input/hooks/useOpenRelationToOneFieldInput';
|
||||
import { FieldDefinition } from '@/object-record/record-field/types/FieldDefinition';
|
||||
import { FieldMetadata } from '@/object-record/record-field/types/FieldMetadata';
|
||||
import { isFieldRelationFromManyObjects } from '@/object-record/record-field/types/guards/isFieldRelationFromManyObjects';
|
||||
import { isFieldRelationToOneObject } from '@/object-record/record-field/types/guards/isFieldRelationToOneObject';
|
||||
import { isDefined } from 'twenty-shared';
|
||||
|
||||
export const useOpenFieldInputEditMode = () => {
|
||||
const { openRelationToOneFieldInput } = useOpenRelationToOneFieldInput();
|
||||
const { openRelationFromManyFieldInput } =
|
||||
useOpenRelationFromManyFieldInput();
|
||||
|
||||
const openFieldInput = ({
|
||||
fieldDefinition,
|
||||
recordId,
|
||||
}: {
|
||||
fieldDefinition: FieldDefinition<FieldMetadata>;
|
||||
recordId: string;
|
||||
}) => {
|
||||
if (isFieldRelationToOneObject(fieldDefinition)) {
|
||||
openRelationToOneFieldInput({
|
||||
fieldName: fieldDefinition.metadata.fieldName,
|
||||
recordId: recordId,
|
||||
});
|
||||
}
|
||||
|
||||
if (isFieldRelationFromManyObjects(fieldDefinition)) {
|
||||
if (
|
||||
isDefined(fieldDefinition.metadata.relationObjectMetadataNameSingular)
|
||||
) {
|
||||
openRelationFromManyFieldInput({
|
||||
fieldName: fieldDefinition.metadata.fieldName,
|
||||
objectNameSingular:
|
||||
fieldDefinition.metadata.relationObjectMetadataNameSingular,
|
||||
recordId: recordId,
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
openFieldInput: openFieldInput,
|
||||
closeFieldInput: () => {},
|
||||
};
|
||||
};
|
||||
@ -31,7 +31,6 @@ import { isFieldRichText } from '@/object-record/record-field/types/guards/isFie
|
||||
import { isFieldRichTextV2 } from '@/object-record/record-field/types/guards/isFieldRichTextV2';
|
||||
import { isFieldRichTextValue } from '@/object-record/record-field/types/guards/isFieldRichTextValue';
|
||||
import { isFieldRichTextV2Value } from '@/object-record/record-field/types/guards/isFieldRichTextValueV2';
|
||||
import { SingleRecordPickerRecord } from '@/object-record/record-picker/types/SingleRecordPickerRecord';
|
||||
import { getForeignKeyNameFromRelationFieldName } from '@/object-record/utils/getForeignKeyNameFromRelationFieldName';
|
||||
import { FieldContext } from '../contexts/FieldContext';
|
||||
import { isFieldBoolean } from '../types/guards/isFieldBoolean';
|
||||
@ -156,13 +155,12 @@ export const usePersistField = () => {
|
||||
);
|
||||
|
||||
if (fieldIsRelationToOneObject) {
|
||||
const value = valueToPersist as SingleRecordPickerRecord;
|
||||
updateRecord?.({
|
||||
variables: {
|
||||
where: { id: recordId },
|
||||
updateOneRecordInput: {
|
||||
[getForeignKeyNameFromRelationFieldName(fieldName)]:
|
||||
value?.id ?? null,
|
||||
valueToPersist?.id ?? null,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@ -7,14 +7,12 @@ import { FieldRelationValue } from '@/object-record/record-field/types/FieldMeta
|
||||
import { recordStoreFamilySelector } from '@/object-record/record-store/states/selectors/recordStoreFamilySelector';
|
||||
import { FieldMetadataType } from '~/generated-metadata/graphql';
|
||||
|
||||
import { SingleRecordPickerRecord } from '@/object-record/record-picker/types/SingleRecordPickerRecord';
|
||||
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
|
||||
import { FieldContext } from '../../contexts/FieldContext';
|
||||
import { assertFieldMetadata } from '../../types/guards/assertFieldMetadata';
|
||||
import { isFieldRelation } from '../../types/guards/isFieldRelation';
|
||||
|
||||
export const useRelationField = <
|
||||
T extends SingleRecordPickerRecord | SingleRecordPickerRecord[],
|
||||
>() => {
|
||||
export const useRelationField = <T extends ObjectRecord | ObjectRecord[]>() => {
|
||||
const { recordId, fieldDefinition, maxWidth } = useContext(FieldContext);
|
||||
const button = useGetButtonIcon();
|
||||
|
||||
|
||||
@ -2,14 +2,14 @@ import { useContext } from 'react';
|
||||
|
||||
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
|
||||
import { FieldContext } from '@/object-record/record-field/contexts/FieldContext';
|
||||
import { RelationFromManyFieldInputMultiRecordsEffect } from '@/object-record/record-field/meta-types/input/components/RelationFromManyFieldInputMultiRecordsEffect';
|
||||
import { useAddNewRecordAndOpenRightDrawer } from '@/object-record/record-field/meta-types/input/hooks/useAddNewRecordAndOpenRightDrawer';
|
||||
import { useUpdateRelationFromManyFieldInput } from '@/object-record/record-field/meta-types/input/hooks/useUpdateRelationFromManyFieldInput';
|
||||
import { recordFieldInputLayoutDirectionComponentState } from '@/object-record/record-field/states/recordFieldInputLayoutDirectionComponentState';
|
||||
import { FieldDefinition } from '@/object-record/record-field/types/FieldDefinition';
|
||||
import { FieldInputEvent } from '@/object-record/record-field/types/FieldInputEvent';
|
||||
import { FieldRelationMetadata } from '@/object-record/record-field/types/FieldMetadata';
|
||||
import { MultipleRecordPicker } from '@/object-record/record-picker/components/MultipleRecordPicker';
|
||||
import { RecordPickerComponentInstanceContext } from '@/object-record/record-picker/states/contexts/RecordPickerComponentInstanceContext';
|
||||
import { MultipleRecordPicker } from '@/object-record/record-picker/multiple-record-picker/components/MultipleRecordPicker';
|
||||
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
|
||||
|
||||
type RelationFromManyFieldInputProps = {
|
||||
onSubmit?: FieldInputEvent;
|
||||
@ -19,10 +19,9 @@ export const RelationFromManyFieldInput = ({
|
||||
onSubmit,
|
||||
}: RelationFromManyFieldInputProps) => {
|
||||
const { fieldDefinition, recordId } = useContext(FieldContext);
|
||||
const recordPickerInstanceId = `record-picker-${fieldDefinition.fieldMetadataId}`;
|
||||
const { updateRelation } = useUpdateRelationFromManyFieldInput({
|
||||
scopeId: recordPickerInstanceId,
|
||||
});
|
||||
const recordPickerInstanceId = `relation-from-many-field-input-${recordId}`;
|
||||
|
||||
const { updateRelation } = useUpdateRelationFromManyFieldInput();
|
||||
|
||||
const handleSubmit = () => {
|
||||
onSubmit?.(() => {});
|
||||
@ -50,19 +49,22 @@ export const RelationFromManyFieldInput = ({
|
||||
recordId,
|
||||
});
|
||||
|
||||
const layoutDirection = useRecoilComponentValueV2(
|
||||
recordFieldInputLayoutDirectionComponentState,
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<RecordPickerComponentInstanceContext.Provider
|
||||
value={{ instanceId: recordPickerInstanceId }}
|
||||
>
|
||||
<RelationFromManyFieldInputMultiRecordsEffect />
|
||||
<MultipleRecordPicker
|
||||
componentInstanceId={recordPickerInstanceId}
|
||||
onSubmit={handleSubmit}
|
||||
onChange={updateRelation}
|
||||
onCreate={createNewRecordAndOpenRightDrawer}
|
||||
/>
|
||||
</RecordPickerComponentInstanceContext.Provider>
|
||||
</>
|
||||
<MultipleRecordPicker
|
||||
componentInstanceId={recordPickerInstanceId}
|
||||
onSubmit={handleSubmit}
|
||||
onChange={updateRelation}
|
||||
onCreate={createNewRecordAndOpenRightDrawer}
|
||||
onClickOutside={handleSubmit}
|
||||
layoutDirection={
|
||||
layoutDirection === 'downward'
|
||||
? 'search-bar-on-top'
|
||||
: 'search-bar-on-bottom'
|
||||
}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@ -1,129 +0,0 @@
|
||||
import { useEffect, useMemo } from 'react';
|
||||
import { useRecoilCallback, useRecoilState, useSetRecoilState } from 'recoil';
|
||||
|
||||
import { useObjectRecordMultiSelectScopedStates } from '@/activities/hooks/useObjectRecordMultiSelectScopedStates';
|
||||
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
|
||||
import { useRelationField } from '@/object-record/record-field/meta-types/hooks/useRelationField';
|
||||
import { objectRecordMultiSelectComponentFamilyState } from '@/object-record/record-field/states/objectRecordMultiSelectComponentFamilyState';
|
||||
import { useRecordPickerRecordsOptions } from '@/object-record/record-picker/hooks/useRecordPickerRecordsOptions';
|
||||
import { RecordPickerComponentInstanceContext } from '@/object-record/record-picker/states/contexts/RecordPickerComponentInstanceContext';
|
||||
import { SingleRecordPickerRecord } from '@/object-record/record-picker/types/SingleRecordPickerRecord';
|
||||
import { ObjectRecordForSelect } from '@/object-record/types/ObjectRecordForSelect';
|
||||
import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
|
||||
import { isDeeplyEqual } from '~/utils/isDeeplyEqual';
|
||||
|
||||
export const RelationFromManyFieldInputMultiRecordsEffect = () => {
|
||||
const { fieldValue, fieldDefinition } =
|
||||
useRelationField<SingleRecordPickerRecord[]>();
|
||||
const instanceId = useAvailableComponentInstanceIdOrThrow(
|
||||
RecordPickerComponentInstanceContext,
|
||||
);
|
||||
const {
|
||||
objectRecordsIdsMultiSelectState,
|
||||
objectRecordMultiSelectCheckedRecordsIdsState,
|
||||
recordMultiSelectIsLoadingState,
|
||||
} = useObjectRecordMultiSelectScopedStates(instanceId);
|
||||
const [objectRecordsIdsMultiSelect, setObjectRecordsIdsMultiSelect] =
|
||||
useRecoilState(objectRecordsIdsMultiSelectState);
|
||||
|
||||
const { records } = useRecordPickerRecordsOptions({
|
||||
objectNameSingular:
|
||||
fieldDefinition.metadata.relationObjectMetadataNameSingular,
|
||||
});
|
||||
|
||||
const setRecordMultiSelectIsLoading = useSetRecoilState(
|
||||
recordMultiSelectIsLoadingState,
|
||||
);
|
||||
|
||||
const { objectMetadataItem } = useObjectMetadataItem({
|
||||
objectNameSingular:
|
||||
fieldDefinition.metadata.relationObjectMetadataNameSingular,
|
||||
});
|
||||
|
||||
const allRecords = useMemo(
|
||||
() => [
|
||||
...records.recordsToSelect.map((entity) => {
|
||||
const { record, ...recordIdentifier } = entity;
|
||||
return {
|
||||
objectMetadataItem: objectMetadataItem,
|
||||
record: record,
|
||||
recordIdentifier: recordIdentifier,
|
||||
};
|
||||
}),
|
||||
],
|
||||
[records.recordsToSelect, objectMetadataItem],
|
||||
);
|
||||
|
||||
const [
|
||||
objectRecordMultiSelectCheckedRecordsIds,
|
||||
setObjectRecordMultiSelectCheckedRecordsIds,
|
||||
] = useRecoilState(objectRecordMultiSelectCheckedRecordsIdsState);
|
||||
|
||||
const updateRecords = useRecoilCallback(
|
||||
({ snapshot, set }) =>
|
||||
(newRecords: ObjectRecordForSelect[]) => {
|
||||
for (const newRecord of newRecords) {
|
||||
const currentRecord = snapshot
|
||||
.getLoadable(
|
||||
objectRecordMultiSelectComponentFamilyState({
|
||||
scopeId: instanceId,
|
||||
familyKey: newRecord.record.id,
|
||||
}),
|
||||
)
|
||||
.getValue();
|
||||
|
||||
const newRecordWithSelected = {
|
||||
...newRecord,
|
||||
selected: objectRecordMultiSelectCheckedRecordsIds.includes(
|
||||
newRecord.record.id,
|
||||
),
|
||||
};
|
||||
|
||||
if (
|
||||
!isDeeplyEqual(
|
||||
newRecordWithSelected.selected,
|
||||
currentRecord?.selected,
|
||||
)
|
||||
) {
|
||||
set(
|
||||
objectRecordMultiSelectComponentFamilyState({
|
||||
scopeId: instanceId,
|
||||
familyKey: newRecordWithSelected.record.id,
|
||||
}),
|
||||
newRecordWithSelected,
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
[objectRecordMultiSelectCheckedRecordsIds, instanceId],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
updateRecords(allRecords);
|
||||
const allRecordsIds = allRecords.map((record) => record.record.id);
|
||||
if (!isDeeplyEqual(allRecordsIds, objectRecordsIdsMultiSelect)) {
|
||||
setObjectRecordsIdsMultiSelect(allRecordsIds);
|
||||
}
|
||||
}, [
|
||||
allRecords,
|
||||
objectRecordsIdsMultiSelect,
|
||||
setObjectRecordsIdsMultiSelect,
|
||||
updateRecords,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
setObjectRecordMultiSelectCheckedRecordsIds(
|
||||
fieldValue
|
||||
? fieldValue.map(
|
||||
(fieldValueItem: SingleRecordPickerRecord) => fieldValueItem.id,
|
||||
)
|
||||
: [],
|
||||
);
|
||||
}, [fieldValue, setObjectRecordMultiSelectCheckedRecordsIds]);
|
||||
|
||||
useEffect(() => {
|
||||
setRecordMultiSelectIsLoading(records.loading);
|
||||
}, [records.loading, setRecordMultiSelectIsLoading]);
|
||||
|
||||
return <></>;
|
||||
};
|
||||
@ -1,83 +0,0 @@
|
||||
import { useContext } from 'react';
|
||||
import { IconForbid } from 'twenty-ui';
|
||||
|
||||
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
|
||||
import { FieldContext } from '@/object-record/record-field/contexts/FieldContext';
|
||||
import { RelationPickerInitialValueEffect } from '@/object-record/record-field/meta-types/input/components/RelationPickerInitialValueEffect';
|
||||
import { useAddNewRecordAndOpenRightDrawer } from '@/object-record/record-field/meta-types/input/hooks/useAddNewRecordAndOpenRightDrawer';
|
||||
import { RelationPickerHotkeyScope } from '@/object-record/record-field/meta-types/input/types/RelationPickerHotkeyScope';
|
||||
import { FieldDefinition } from '@/object-record/record-field/types/FieldDefinition';
|
||||
import { FieldRelationMetadata } from '@/object-record/record-field/types/FieldMetadata';
|
||||
import { SingleRecordPicker } from '@/object-record/record-picker/components/SingleRecordPicker';
|
||||
import { SingleRecordPickerRecord } from '@/object-record/record-picker/types/SingleRecordPickerRecord';
|
||||
|
||||
export type RelationPickerProps = {
|
||||
selectedRecordId?: string;
|
||||
onSubmit: (selectedRecord: SingleRecordPickerRecord | null) => void;
|
||||
onCancel?: () => void;
|
||||
width?: number;
|
||||
excludedRecordIds?: string[];
|
||||
initialSearchFilter?: string | null;
|
||||
fieldDefinition: FieldDefinition<FieldRelationMetadata>;
|
||||
};
|
||||
|
||||
export const RelationPicker = ({
|
||||
selectedRecordId,
|
||||
onSubmit,
|
||||
onCancel,
|
||||
excludedRecordIds,
|
||||
width,
|
||||
initialSearchFilter,
|
||||
fieldDefinition,
|
||||
}: RelationPickerProps) => {
|
||||
const recordPickerInstanceId = RelationPickerHotkeyScope.RelationPicker;
|
||||
|
||||
const handleRecordSelected = (
|
||||
selectedRecord: SingleRecordPickerRecord | null | undefined,
|
||||
) => onSubmit(selectedRecord ?? null);
|
||||
|
||||
const { objectMetadataItem: relationObjectMetadataItem } =
|
||||
useObjectMetadataItem({
|
||||
objectNameSingular:
|
||||
fieldDefinition.metadata.relationObjectMetadataNameSingular,
|
||||
});
|
||||
|
||||
const relationFieldMetadataItem = relationObjectMetadataItem.fields.find(
|
||||
({ id }) => id === fieldDefinition.metadata.relationFieldMetadataId,
|
||||
);
|
||||
|
||||
const { recordId } = useContext(FieldContext);
|
||||
|
||||
const { createNewRecordAndOpenRightDrawer } =
|
||||
useAddNewRecordAndOpenRightDrawer({
|
||||
relationObjectMetadataNameSingular:
|
||||
fieldDefinition.metadata.relationObjectMetadataNameSingular,
|
||||
relationObjectMetadataItem,
|
||||
relationFieldMetadataItem,
|
||||
recordId,
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<RelationPickerInitialValueEffect
|
||||
initialValueForSearchFilter={initialSearchFilter}
|
||||
recordPickerInstanceId={recordPickerInstanceId}
|
||||
/>
|
||||
<SingleRecordPicker
|
||||
componentInstanceId={recordPickerInstanceId}
|
||||
EmptyIcon={IconForbid}
|
||||
emptyLabel={'No ' + fieldDefinition.label}
|
||||
onCancel={onCancel}
|
||||
onCreate={createNewRecordAndOpenRightDrawer}
|
||||
onRecordSelected={handleRecordSelected}
|
||||
width={width}
|
||||
objectNameSingular={
|
||||
fieldDefinition.metadata.relationObjectMetadataNameSingular
|
||||
}
|
||||
recordPickerInstanceId={recordPickerInstanceId}
|
||||
selectedRecordIds={selectedRecordId ? [selectedRecordId] : []}
|
||||
excludedRecordIds={excludedRecordIds}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@ -1,23 +0,0 @@
|
||||
import { recordPickerSearchFilterComponentState } from '@/object-record/record-picker/states/recordPickerSearchFilterComponentState';
|
||||
import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentStateV2';
|
||||
import { useEffect } from 'react';
|
||||
|
||||
// Todo: this effect should be deprecated to use sync hooks
|
||||
export const RelationPickerInitialValueEffect = ({
|
||||
initialValueForSearchFilter,
|
||||
recordPickerInstanceId,
|
||||
}: {
|
||||
initialValueForSearchFilter?: string | null;
|
||||
recordPickerInstanceId: string;
|
||||
}) => {
|
||||
const setRecordPickerSearchFilter = useSetRecoilComponentStateV2(
|
||||
recordPickerSearchFilterComponentState,
|
||||
recordPickerInstanceId,
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
setRecordPickerSearchFilter(initialValueForSearchFilter ?? '');
|
||||
}, [initialValueForSearchFilter, setRecordPickerSearchFilter]);
|
||||
|
||||
return <></>;
|
||||
};
|
||||
@ -1,8 +1,15 @@
|
||||
import { RelationPicker } from '@/object-record/record-field/meta-types/input/components/RelationPicker';
|
||||
import { usePersistField } from '../../../hooks/usePersistField';
|
||||
import { useRelationField } from '../../hooks/useRelationField';
|
||||
|
||||
import { SingleRecordPickerRecord } from '@/object-record/record-picker/types/SingleRecordPickerRecord';
|
||||
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
|
||||
import { useAddNewRecordAndOpenRightDrawer } from '@/object-record/record-field/meta-types/input/hooks/useAddNewRecordAndOpenRightDrawer';
|
||||
import { recordFieldInputLayoutDirectionComponentState } from '@/object-record/record-field/states/recordFieldInputLayoutDirectionComponentState';
|
||||
import { recordFieldInputLayoutDirectionLoadingComponentState } from '@/object-record/record-field/states/recordFieldInputLayoutDirectionLoadingComponentState';
|
||||
import { SingleRecordPicker } from '@/object-record/record-picker/single-record-picker/components/SingleRecordPicker';
|
||||
import { SingleRecordPickerRecord } from '@/object-record/record-picker/single-record-picker/types/SingleRecordPickerRecord';
|
||||
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
|
||||
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
|
||||
import { IconForbid } from 'twenty-ui';
|
||||
import { FieldInputEvent } from './DateTimeFieldInput';
|
||||
|
||||
export type RelationToOneFieldInputProps = {
|
||||
@ -14,22 +21,64 @@ export const RelationToOneFieldInput = ({
|
||||
onSubmit,
|
||||
onCancel,
|
||||
}: RelationToOneFieldInputProps) => {
|
||||
const { fieldDefinition, initialSearchValue, fieldValue } =
|
||||
useRelationField<SingleRecordPickerRecord>();
|
||||
const { fieldDefinition, recordId } = useRelationField<ObjectRecord>();
|
||||
|
||||
const persistField = usePersistField();
|
||||
|
||||
const handleSubmit = (newEntity: SingleRecordPickerRecord | null) => {
|
||||
onSubmit?.(() => persistField(newEntity?.record ?? null));
|
||||
};
|
||||
const recordPickerInstanceId = `relation-to-one-field-input-${recordId}-${fieldDefinition.metadata.fieldName}`;
|
||||
|
||||
const handleRecordSelected = (
|
||||
selectedRecord: SingleRecordPickerRecord | null | undefined,
|
||||
) => onSubmit?.(() => persistField(selectedRecord?.record ?? null));
|
||||
|
||||
const { objectMetadataItem: relationObjectMetadataItem } =
|
||||
useObjectMetadataItem({
|
||||
objectNameSingular:
|
||||
fieldDefinition.metadata.relationObjectMetadataNameSingular,
|
||||
});
|
||||
|
||||
const relationFieldMetadataItem = relationObjectMetadataItem.fields.find(
|
||||
({ id }) => id === fieldDefinition.metadata.relationFieldMetadataId,
|
||||
);
|
||||
|
||||
const { createNewRecordAndOpenRightDrawer } =
|
||||
useAddNewRecordAndOpenRightDrawer({
|
||||
relationObjectMetadataNameSingular:
|
||||
fieldDefinition.metadata.relationObjectMetadataNameSingular,
|
||||
relationObjectMetadataItem,
|
||||
relationFieldMetadataItem,
|
||||
recordId,
|
||||
});
|
||||
|
||||
const layoutDirection = useRecoilComponentValueV2(
|
||||
recordFieldInputLayoutDirectionComponentState,
|
||||
);
|
||||
|
||||
const isLoading = useRecoilComponentValueV2(
|
||||
recordFieldInputLayoutDirectionLoadingComponentState,
|
||||
);
|
||||
|
||||
if (isLoading) {
|
||||
return <></>;
|
||||
}
|
||||
|
||||
return (
|
||||
<RelationPicker
|
||||
fieldDefinition={fieldDefinition}
|
||||
selectedRecordId={fieldValue?.id}
|
||||
onSubmit={handleSubmit}
|
||||
<SingleRecordPicker
|
||||
componentInstanceId={recordPickerInstanceId}
|
||||
EmptyIcon={IconForbid}
|
||||
emptyLabel={'No ' + fieldDefinition.label}
|
||||
onCancel={onCancel}
|
||||
initialSearchFilter={initialSearchValue}
|
||||
onCreate={createNewRecordAndOpenRightDrawer}
|
||||
onRecordSelected={handleRecordSelected}
|
||||
objectNameSingular={
|
||||
fieldDefinition.metadata.relationObjectMetadataNameSingular
|
||||
}
|
||||
recordPickerInstanceId={recordPickerInstanceId}
|
||||
layoutDirection={
|
||||
layoutDirection === 'downward'
|
||||
? 'search-bar-on-top'
|
||||
: 'search-bar-on-bottom'
|
||||
}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@ -18,7 +18,10 @@ import {
|
||||
} from '~/testing/mock-data/users';
|
||||
|
||||
import { FieldContextProvider } from '@/object-record/record-field/meta-types/components/FieldContextProvider';
|
||||
import { RecordPickerComponentInstanceContext } from '@/object-record/record-picker/states/contexts/RecordPickerComponentInstanceContext';
|
||||
import { RecordFieldComponentInstanceContext } from '@/object-record/record-field/states/contexts/RecordFieldComponentInstanceContext';
|
||||
import { recordFieldInputLayoutDirectionLoadingComponentState } from '@/object-record/record-field/states/recordFieldInputLayoutDirectionLoadingComponentState';
|
||||
import { SingleRecordPickerComponentInstanceContext } from '@/object-record/record-picker/single-record-picker/states/contexts/SingleRecordPickerComponentInstanceContext';
|
||||
import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentStateV2';
|
||||
import { getCanvasElementForDropdownTesting } from 'twenty-ui';
|
||||
import {
|
||||
RelationToOneFieldInput,
|
||||
@ -30,11 +33,21 @@ const RelationWorkspaceSetterEffect = () => {
|
||||
const setCurrentWorkspaceMember = useSetRecoilState(
|
||||
currentWorkspaceMemberState,
|
||||
);
|
||||
const setRecordFieldInputLayoutDirectionLoading =
|
||||
useSetRecoilComponentStateV2(
|
||||
recordFieldInputLayoutDirectionLoadingComponentState,
|
||||
'relation-to-one-field-input-123-Relation',
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
setCurrentWorkspace(mockCurrentWorkspace);
|
||||
setCurrentWorkspaceMember(mockedWorkspaceMemberData);
|
||||
}, [setCurrentWorkspace, setCurrentWorkspaceMember]);
|
||||
setRecordFieldInputLayoutDirectionLoading(false);
|
||||
}, [
|
||||
setCurrentWorkspace,
|
||||
setCurrentWorkspaceMember,
|
||||
setRecordFieldInputLayoutDirectionLoading,
|
||||
]);
|
||||
|
||||
return <></>;
|
||||
};
|
||||
@ -74,12 +87,18 @@ const RelationToOneFieldInputWithContext = ({
|
||||
}}
|
||||
recordId={recordId}
|
||||
>
|
||||
<RecordPickerComponentInstanceContext.Provider
|
||||
value={{ instanceId: 'relation-to-one-field-input' }}
|
||||
<RecordFieldComponentInstanceContext.Provider
|
||||
value={{
|
||||
instanceId: 'relation-to-one-field-input-123-Relation',
|
||||
}}
|
||||
>
|
||||
<RelationWorkspaceSetterEffect />
|
||||
<RelationToOneFieldInput onSubmit={onSubmit} onCancel={onCancel} />
|
||||
</RecordPickerComponentInstanceContext.Provider>
|
||||
<SingleRecordPickerComponentInstanceContext.Provider
|
||||
value={{ instanceId: 'relation-to-one-field-input-123-Relation' }}
|
||||
>
|
||||
<RelationWorkspaceSetterEffect />
|
||||
<RelationToOneFieldInput onSubmit={onSubmit} onCancel={onCancel} />
|
||||
</SingleRecordPickerComponentInstanceContext.Provider>
|
||||
</RecordFieldComponentInstanceContext.Provider>
|
||||
</FieldContextProvider>
|
||||
<div data-testid="data-field-input-click-outside-div" />
|
||||
</div>
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import { useSetRecoilState } from 'recoil';
|
||||
import { v4 } from 'uuid';
|
||||
|
||||
import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu';
|
||||
import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
|
||||
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||
import { getLabelIdentifierFieldMetadataItem } from '@/object-metadata/utils/getLabelIdentifierFieldMetadataItem';
|
||||
@ -10,9 +11,11 @@ import { viewableRecordIdState } from '@/object-record/record-right-drawer/state
|
||||
import { viewableRecordNameSingularState } from '@/object-record/record-right-drawer/states/viewableRecordNameSingularState';
|
||||
import { useRightDrawer } from '@/ui/layout/right-drawer/hooks/useRightDrawer';
|
||||
import { RightDrawerPages } from '@/ui/layout/right-drawer/types/RightDrawerPages';
|
||||
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
|
||||
import { isDefined } from 'twenty-shared';
|
||||
import { IconEye } from 'twenty-ui';
|
||||
import {
|
||||
FeatureFlagKey,
|
||||
FieldMetadataType,
|
||||
RelationDefinitionType,
|
||||
} from '~/generated-metadata/graphql';
|
||||
@ -45,6 +48,10 @@ export const useAddNewRecordAndOpenRightDrawer = ({
|
||||
});
|
||||
|
||||
const { openRightDrawer } = useRightDrawer();
|
||||
const { openRecordInCommandMenu } = useCommandMenu();
|
||||
const isCommandMenuEnabled = useIsFeatureEnabled(
|
||||
FeatureFlagKey.IsCommandMenuV2Enabled,
|
||||
);
|
||||
|
||||
if (
|
||||
relationObjectMetadataNameSingular === 'workspaceMember' ||
|
||||
@ -110,10 +117,18 @@ export const useAddNewRecordAndOpenRightDrawer = ({
|
||||
|
||||
setViewableRecordId(newRecordId);
|
||||
setViewableRecordNameSingular(relationObjectMetadataNameSingular);
|
||||
openRightDrawer(RightDrawerPages.ViewRecord, {
|
||||
title: 'View Record',
|
||||
Icon: IconEye,
|
||||
});
|
||||
|
||||
if (isCommandMenuEnabled) {
|
||||
openRecordInCommandMenu({
|
||||
recordId: newRecordId,
|
||||
objectNameSingular: relationObjectMetadataNameSingular,
|
||||
});
|
||||
} else {
|
||||
openRightDrawer(RightDrawerPages.ViewRecord, {
|
||||
title: 'View Record',
|
||||
Icon: IconEye,
|
||||
});
|
||||
}
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
@ -0,0 +1,91 @@
|
||||
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
|
||||
import {
|
||||
FieldRelationFromManyValue,
|
||||
FieldRelationValue,
|
||||
} from '@/object-record/record-field/types/FieldMetadata';
|
||||
import { useMultipleRecordPickerPerformSearch } from '@/object-record/record-picker/multiple-record-picker/hooks/useMultipleRecordPickerPerformSearch';
|
||||
import { multipleRecordPickerPickableMorphItemsComponentState } from '@/object-record/record-picker/multiple-record-picker/states/multipleRecordPickerPickableMorphItemsComponentState';
|
||||
import { multipleRecordPickerSearchableObjectMetadataItemsComponentState } from '@/object-record/record-picker/multiple-record-picker/states/multipleRecordPickerSearchableObjectMetadataItemsComponentState';
|
||||
import { RecordPickerPickableMorphItem } from '@/object-record/record-picker/types/RecordPickerPickableMorphItem';
|
||||
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
|
||||
import { recordStoreFamilySelector } from '@/object-record/record-store/states/selectors/recordStoreFamilySelector';
|
||||
import { useRecoilCallback } from 'recoil';
|
||||
|
||||
export const useOpenRelationFromManyFieldInput = () => {
|
||||
const { performSearch } = useMultipleRecordPickerPerformSearch();
|
||||
|
||||
const openRelationFromManyFieldInput = useRecoilCallback(
|
||||
({ set, snapshot }) =>
|
||||
({
|
||||
fieldName,
|
||||
objectNameSingular,
|
||||
recordId,
|
||||
}: {
|
||||
fieldName: string;
|
||||
objectNameSingular: string;
|
||||
recordId: string;
|
||||
}) => {
|
||||
const recordPickerInstanceId = `relation-from-many-field-input-${recordId}`;
|
||||
|
||||
const fieldValue = snapshot
|
||||
.getLoadable<FieldRelationValue<FieldRelationFromManyValue>>(
|
||||
recordStoreFamilySelector({
|
||||
recordId,
|
||||
fieldName,
|
||||
}),
|
||||
)
|
||||
.getValue();
|
||||
|
||||
const objectMetadataItems = snapshot
|
||||
.getLoadable(objectMetadataItemsState)
|
||||
.getValue();
|
||||
|
||||
const objectMetadataItem = objectMetadataItems.find(
|
||||
(objectMetadataItem) =>
|
||||
objectMetadataItem.nameSingular === objectNameSingular,
|
||||
);
|
||||
|
||||
if (!objectMetadataItem) {
|
||||
return;
|
||||
}
|
||||
|
||||
const pickableMorphItems: RecordPickerPickableMorphItem[] =
|
||||
fieldValue.map((record) => {
|
||||
return {
|
||||
objectMetadataId: objectMetadataItem.id,
|
||||
recordId: record.id,
|
||||
isSelected: true,
|
||||
isMatchingSearchFilter: true,
|
||||
};
|
||||
});
|
||||
|
||||
for (const record of fieldValue) {
|
||||
set(recordStoreFamilyState(record.id), record);
|
||||
}
|
||||
|
||||
set(
|
||||
multipleRecordPickerPickableMorphItemsComponentState.atomFamily({
|
||||
instanceId: recordPickerInstanceId,
|
||||
}),
|
||||
pickableMorphItems,
|
||||
);
|
||||
|
||||
set(
|
||||
multipleRecordPickerSearchableObjectMetadataItemsComponentState.atomFamily(
|
||||
{ instanceId: recordPickerInstanceId },
|
||||
),
|
||||
[objectMetadataItem],
|
||||
);
|
||||
|
||||
performSearch({
|
||||
multipleRecordPickerInstanceId: recordPickerInstanceId,
|
||||
forceSearchFilter: '',
|
||||
forceSearchableObjectMetadataItems: [objectMetadataItem],
|
||||
forcePickableMorphItems: pickableMorphItems,
|
||||
});
|
||||
},
|
||||
[performSearch],
|
||||
);
|
||||
|
||||
return { openRelationFromManyFieldInput };
|
||||
};
|
||||
@ -0,0 +1,37 @@
|
||||
import {
|
||||
FieldRelationToOneValue,
|
||||
FieldRelationValue,
|
||||
} from '@/object-record/record-field/types/FieldMetadata';
|
||||
import { singleRecordPickerSelectedIdComponentState } from '@/object-record/record-picker/single-record-picker/states/singleRecordPickerSelectedIdComponentState';
|
||||
import { recordStoreFamilySelector } from '@/object-record/record-store/states/selectors/recordStoreFamilySelector';
|
||||
import { useRecoilCallback } from 'recoil';
|
||||
import { isDefined } from 'twenty-shared';
|
||||
|
||||
export const useOpenRelationToOneFieldInput = () => {
|
||||
const openRelationToOneFieldInput = useRecoilCallback(
|
||||
({ set, snapshot }) =>
|
||||
({ fieldName, recordId }: { fieldName: string; recordId: string }) => {
|
||||
const recordPickerInstanceId = `relation-to-one-field-input-${recordId}-${fieldName}`;
|
||||
const fieldValue = snapshot
|
||||
.getLoadable<FieldRelationValue<FieldRelationToOneValue>>(
|
||||
recordStoreFamilySelector({
|
||||
recordId,
|
||||
fieldName,
|
||||
}),
|
||||
)
|
||||
.getValue();
|
||||
|
||||
if (isDefined(fieldValue)) {
|
||||
set(
|
||||
singleRecordPickerSelectedIdComponentState.atomFamily({
|
||||
instanceId: recordPickerInstanceId,
|
||||
}),
|
||||
fieldValue.id,
|
||||
);
|
||||
}
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
return { openRelationToOneFieldInput };
|
||||
};
|
||||
@ -4,16 +4,12 @@ import { useRecoilCallback } from 'recoil';
|
||||
import { useAttachRelatedRecordFromRecord } from '@/object-record/hooks/useAttachRelatedRecordFromRecord';
|
||||
import { useDetachRelatedRecordFromRecord } from '@/object-record/hooks/useDetachRelatedRecordFromRecord';
|
||||
import { FieldContext } from '@/object-record/record-field/contexts/FieldContext';
|
||||
import { objectRecordMultiSelectCheckedRecordsIdsComponentState } from '@/object-record/record-field/states/objectRecordMultiSelectCheckedRecordsIdsComponentState';
|
||||
import { assertFieldMetadata } from '@/object-record/record-field/types/guards/assertFieldMetadata';
|
||||
import { isFieldRelation } from '@/object-record/record-field/types/guards/isFieldRelation';
|
||||
import { RecordPickerPickableMorphItem } from '@/object-record/record-picker/types/RecordPickerPickableMorphItem';
|
||||
import { FieldMetadataType } from '~/generated-metadata/graphql';
|
||||
|
||||
export const useUpdateRelationFromManyFieldInput = ({
|
||||
scopeId,
|
||||
}: {
|
||||
scopeId: string;
|
||||
}) => {
|
||||
export const useUpdateRelationFromManyFieldInput = () => {
|
||||
const { recordId, fieldDefinition } = useContext(FieldContext);
|
||||
|
||||
assertFieldMetadata(
|
||||
@ -41,49 +37,21 @@ export const useUpdateRelationFromManyFieldInput = ({
|
||||
});
|
||||
|
||||
const updateRelation = useRecoilCallback(
|
||||
({ snapshot, set }) =>
|
||||
async (objectRecordId: string) => {
|
||||
const previouslyCheckedRecordsIds = snapshot
|
||||
.getLoadable(
|
||||
objectRecordMultiSelectCheckedRecordsIdsComponentState({
|
||||
scopeId,
|
||||
}),
|
||||
)
|
||||
.getValue();
|
||||
|
||||
const isNewlySelected =
|
||||
!previouslyCheckedRecordsIds.includes(objectRecordId);
|
||||
if (isNewlySelected) {
|
||||
set(
|
||||
objectRecordMultiSelectCheckedRecordsIdsComponentState({
|
||||
scopeId,
|
||||
}),
|
||||
(prev) => [...prev, objectRecordId],
|
||||
);
|
||||
} else {
|
||||
set(
|
||||
objectRecordMultiSelectCheckedRecordsIdsComponentState({
|
||||
scopeId,
|
||||
}),
|
||||
(prev) => prev.filter((id) => id !== objectRecordId),
|
||||
);
|
||||
}
|
||||
|
||||
if (isNewlySelected) {
|
||||
await updateOneRecordAndAttachRelations({
|
||||
recordId,
|
||||
relatedRecordId: objectRecordId,
|
||||
});
|
||||
} else {
|
||||
await updateOneRecordAndDetachRelations({
|
||||
recordId,
|
||||
relatedRecordId: objectRecordId,
|
||||
});
|
||||
}
|
||||
},
|
||||
() => async (morphItem: RecordPickerPickableMorphItem) => {
|
||||
if (morphItem.isSelected) {
|
||||
await updateOneRecordAndAttachRelations({
|
||||
recordId,
|
||||
relatedRecordId: morphItem.recordId,
|
||||
});
|
||||
} else {
|
||||
await updateOneRecordAndDetachRelations({
|
||||
recordId,
|
||||
relatedRecordId: morphItem.recordId,
|
||||
});
|
||||
}
|
||||
},
|
||||
[
|
||||
recordId,
|
||||
scopeId,
|
||||
updateOneRecordAndAttachRelations,
|
||||
updateOneRecordAndDetachRelations,
|
||||
],
|
||||
|
||||
@ -1,4 +1,3 @@
|
||||
export enum RelationPickerHotkeyScope {
|
||||
RelationPicker = 'relation-picker',
|
||||
AddNew = 'add-new',
|
||||
}
|
||||
|
||||
@ -1,13 +0,0 @@
|
||||
import { createFamilyState } from '@/ui/utilities/state/utils/createFamilyState';
|
||||
|
||||
export type ActivityTargetObjectRecord = {
|
||||
activityTargetId: string | null;
|
||||
};
|
||||
|
||||
export const activityTargetObjectRecordFamilyState = createFamilyState<
|
||||
ActivityTargetObjectRecord,
|
||||
string
|
||||
>({
|
||||
key: 'activityTargetObjectRecordFamilyState',
|
||||
defaultValue: { activityTargetId: null },
|
||||
});
|
||||
@ -0,0 +1,7 @@
|
||||
import { ComponentStateKeyV2 } from '@/ui/utilities/state/component-state/types/ComponentStateKeyV2';
|
||||
import { createComponentInstanceContext } from '@/ui/utilities/state/component-state/utils/createComponentInstanceContext';
|
||||
|
||||
type RecordFieldComponentInstanceContextProps = ComponentStateKeyV2;
|
||||
|
||||
export const RecordFieldComponentInstanceContext =
|
||||
createComponentInstanceContext<RecordFieldComponentInstanceContextProps>();
|
||||
@ -1,7 +0,0 @@
|
||||
import { createComponentState } from '@/ui/utilities/state/component-state/utils/createComponentState';
|
||||
|
||||
export const objectRecordMultiSelectCheckedRecordsIdsComponentState =
|
||||
createComponentState<string[]>({
|
||||
key: 'objectRecordMultiSelectCheckedRecordsIdsComponentState',
|
||||
defaultValue: [],
|
||||
});
|
||||
@ -1,12 +0,0 @@
|
||||
import { ObjectRecordForSelect } from '@/object-record/types/ObjectRecordForSelect';
|
||||
import { createComponentFamilyState } from '@/ui/utilities/state/component-state/utils/createComponentFamilyState';
|
||||
|
||||
export type ObjectRecordAndSelected = ObjectRecordForSelect & {
|
||||
selected: boolean;
|
||||
};
|
||||
|
||||
export const objectRecordMultiSelectComponentFamilyState =
|
||||
createComponentFamilyState<ObjectRecordAndSelected | undefined, string>({
|
||||
key: 'objectRecordMultiSelectComponentFamilyState',
|
||||
defaultValue: undefined,
|
||||
});
|
||||
@ -1,8 +0,0 @@
|
||||
import { ObjectRecordForSelect } from '@/object-record/types/ObjectRecordForSelect';
|
||||
import { createComponentState } from '@/ui/utilities/state/component-state/utils/createComponentState';
|
||||
|
||||
export const objectRecordMultiSelectMatchesFilterRecordsIdsComponentState =
|
||||
createComponentState<ObjectRecordForSelect[]>({
|
||||
key: 'objectRecordMultiSelectMatchesFilterRecordsIdsComponentState',
|
||||
defaultValue: [],
|
||||
});
|
||||
@ -0,0 +1,10 @@
|
||||
import { RecordFieldComponentInstanceContext } from '@/object-record/record-field/states/contexts/RecordFieldComponentInstanceContext';
|
||||
import { FieldInputLayoutDirection } from '@/object-record/record-field/types/FieldInputLayoutDirection';
|
||||
import { createComponentStateV2 } from '@/ui/utilities/state/component-state/utils/createComponentStateV2';
|
||||
|
||||
export const recordFieldInputLayoutDirectionComponentState =
|
||||
createComponentStateV2<FieldInputLayoutDirection>({
|
||||
key: 'recordFieldInputLayoutDirectionComponentState',
|
||||
defaultValue: 'upward',
|
||||
componentInstanceContext: RecordFieldComponentInstanceContext,
|
||||
});
|
||||
@ -0,0 +1,9 @@
|
||||
import { RecordFieldComponentInstanceContext } from '@/object-record/record-field/states/contexts/RecordFieldComponentInstanceContext';
|
||||
import { createComponentStateV2 } from '@/ui/utilities/state/component-state/utils/createComponentStateV2';
|
||||
|
||||
export const recordFieldInputLayoutDirectionLoadingComponentState =
|
||||
createComponentStateV2<boolean>({
|
||||
key: 'recordFieldInputLayoutDirectionLoadingComponentState',
|
||||
defaultValue: true,
|
||||
componentInstanceContext: RecordFieldComponentInstanceContext,
|
||||
});
|
||||
@ -1,7 +0,0 @@
|
||||
import { createComponentState } from '@/ui/utilities/state/component-state/utils/createComponentState';
|
||||
|
||||
export const recordMultiSelectIsLoadingComponentState =
|
||||
createComponentState<boolean>({
|
||||
key: 'recordMultiSelectIsLoadingComponentState',
|
||||
defaultValue: false,
|
||||
});
|
||||
@ -0,0 +1 @@
|
||||
export type FieldInputLayoutDirection = 'upward' | 'downward';
|
||||
@ -2,7 +2,7 @@ import { ThemeColor } from 'twenty-ui';
|
||||
|
||||
import { RATING_VALUES } from '@/object-record/record-field/meta-types/constants/RatingValues';
|
||||
import { ZodHelperLiteral } from '@/object-record/record-field/types/ZodHelperLiteral';
|
||||
import { SingleRecordPickerRecord } from '@/object-record/record-picker/types/SingleRecordPickerRecord';
|
||||
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
|
||||
import { ConnectedAccountProvider } from 'twenty-shared';
|
||||
import * as z from 'zod';
|
||||
import { RelationDefinitionType } from '~/generated-metadata/graphql';
|
||||
@ -260,9 +260,9 @@ export type FieldRatingValue = (typeof RATING_VALUES)[number] | null;
|
||||
export type FieldSelectValue = string | null;
|
||||
export type FieldMultiSelectValue = string[] | null;
|
||||
|
||||
export type FieldRelationToOneValue = SingleRecordPickerRecord | null;
|
||||
export type FieldRelationToOneValue = ObjectRecord | null;
|
||||
|
||||
export type FieldRelationFromManyValue = SingleRecordPickerRecord[] | [];
|
||||
export type FieldRelationFromManyValue = ObjectRecord[];
|
||||
|
||||
export type FieldRelationValue<
|
||||
T extends FieldRelationToOneValue | FieldRelationFromManyValue,
|
||||
|
||||
@ -2,10 +2,10 @@ import { isFieldRelation } from '@/object-record/record-field/types/guards/isFie
|
||||
|
||||
import { RelationDefinitionType } from '~/generated-metadata/graphql';
|
||||
import { FieldDefinition } from '../FieldDefinition';
|
||||
import { FieldMetadata } from '../FieldMetadata';
|
||||
import { FieldMetadata, FieldRelationMetadata } from '../FieldMetadata';
|
||||
|
||||
export const isFieldRelationFromManyObjects = (
|
||||
field: Pick<FieldDefinition<FieldMetadata>, 'type' | 'metadata'>,
|
||||
): field is FieldDefinition<FieldMetadata> =>
|
||||
): field is FieldDefinition<FieldRelationMetadata> =>
|
||||
isFieldRelation(field) &&
|
||||
field.metadata.relationType === RelationDefinitionType.ONE_TO_MANY;
|
||||
|
||||
@ -8,17 +8,24 @@ import { FieldFocusContextProvider } from '@/object-record/record-field/contexts
|
||||
import { useGetButtonIcon } from '@/object-record/record-field/hooks/useGetButtonIcon';
|
||||
import { useIsFieldInputOnly } from '@/object-record/record-field/hooks/useIsFieldInputOnly';
|
||||
import { FieldInputEvent } from '@/object-record/record-field/types/FieldInputEvent';
|
||||
import { isFieldRelation } from '@/object-record/record-field/types/guards/isFieldRelation';
|
||||
|
||||
import { useInlineCell } from '../hooks/useInlineCell';
|
||||
|
||||
import { useIsFieldValueReadOnly } from '@/object-record/record-field/hooks/useIsFieldValueReadOnly';
|
||||
import { useOpenFieldInputEditMode } from '@/object-record/record-field/hooks/useOpenFieldInputEditMode';
|
||||
import { FieldInputClickOutsideEvent } from '@/object-record/record-field/meta-types/input/components/DateTimeFieldInput';
|
||||
import { RelationPickerHotkeyScope } from '@/object-record/record-field/meta-types/input/types/RelationPickerHotkeyScope';
|
||||
import { FieldDefinition } from '@/object-record/record-field/types/FieldDefinition';
|
||||
import { FieldMetadata } from '@/object-record/record-field/types/FieldMetadata';
|
||||
import { isFieldRelation } from '@/object-record/record-field/types/guards/isFieldRelation';
|
||||
import { isFieldSelect } from '@/object-record/record-field/types/guards/isFieldSelect';
|
||||
import { MultipleRecordPickerHotkeyScope } from '@/object-record/record-picker/multiple-record-picker/types/MultipleRecordPickerHotkeyScope';
|
||||
import { SingleRecordPickerHotkeyScope } from '@/object-record/record-picker/single-record-picker/types/SingleRecordPickerHotkeyScope';
|
||||
import { SelectFieldHotkeyScope } from '@/object-record/select/types/SelectFieldHotkeyScope';
|
||||
import { getDropdownFocusIdForRecordField } from '@/object-record/utils/getDropdownFocusIdForRecordField';
|
||||
import { getRecordFieldInputId } from '@/object-record/utils/getRecordFieldInputId';
|
||||
import { activeDropdownFocusIdState } from '@/ui/layout/dropdown/states/activeDropdownFocusIdState';
|
||||
import { useRecoilCallback } from 'recoil';
|
||||
import { RelationDefinitionType } from '~/generated-metadata/graphql';
|
||||
import { RecordInlineCellContainer } from './RecordInlineCellContainer';
|
||||
import {
|
||||
RecordInlineCellContext,
|
||||
@ -39,6 +46,7 @@ export const RecordInlineCell = ({ loading }: RecordInlineCellProps) => {
|
||||
onOpenEditMode,
|
||||
onCloseEditMode,
|
||||
} = useContext(FieldContext);
|
||||
|
||||
const buttonIcon = useGetButtonIcon();
|
||||
|
||||
const isFieldInputOnly = useIsFieldInputOnly();
|
||||
@ -101,13 +109,40 @@ export const RecordInlineCell = ({ loading }: RecordInlineCellProps) => {
|
||||
);
|
||||
|
||||
const { getIcon } = useIcons();
|
||||
const { openFieldInput, closeFieldInput } = useOpenFieldInputEditMode();
|
||||
|
||||
// TODO: deprecate this and use useOpenFieldInput hooks to set the hotkey scope
|
||||
const computedHotkeyScope = (
|
||||
columnDefinition: FieldDefinition<FieldMetadata>,
|
||||
) => {
|
||||
if (isFieldRelation(columnDefinition)) {
|
||||
if (
|
||||
columnDefinition.metadata.relationType ===
|
||||
RelationDefinitionType.MANY_TO_ONE
|
||||
) {
|
||||
return SingleRecordPickerHotkeyScope.SingleRecordPicker;
|
||||
}
|
||||
|
||||
if (
|
||||
columnDefinition.metadata.relationType ===
|
||||
RelationDefinitionType.ONE_TO_MANY
|
||||
) {
|
||||
return MultipleRecordPickerHotkeyScope.MultipleRecordPicker;
|
||||
}
|
||||
|
||||
return SingleRecordPickerHotkeyScope.SingleRecordPicker;
|
||||
}
|
||||
|
||||
if (isFieldSelect(columnDefinition)) {
|
||||
return SelectFieldHotkeyScope.SelectField;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
};
|
||||
|
||||
const RecordInlineCellContextValue: RecordInlineCellContextProps = {
|
||||
readonly: isFieldReadOnly,
|
||||
buttonIcon: buttonIcon,
|
||||
customEditHotkeyScope: isFieldRelation(fieldDefinition)
|
||||
? { scope: RelationPickerHotkeyScope.RelationPicker }
|
||||
: undefined,
|
||||
IconLabel: fieldDefinition.iconName
|
||||
? getIcon(fieldDefinition.iconName)
|
||||
: undefined,
|
||||
@ -135,8 +170,10 @@ export const RecordInlineCell = ({ loading }: RecordInlineCellProps) => {
|
||||
isDisplayModeFixHeight: isDisplayModeFixHeight,
|
||||
editModeContentOnly: isFieldInputOnly,
|
||||
loading: loading,
|
||||
onOpenEditMode,
|
||||
onCloseEditMode,
|
||||
customEditHotkeyScope: computedHotkeyScope(fieldDefinition),
|
||||
onOpenEditMode:
|
||||
onOpenEditMode ?? (() => openFieldInput({ fieldDefinition, recordId })),
|
||||
onCloseEditMode: onCloseEditMode ?? (() => closeFieldInput()),
|
||||
};
|
||||
|
||||
return (
|
||||
|
||||
@ -1,4 +1,3 @@
|
||||
import { HotkeyScope } from '@/ui/utilities/hotkey/types/HotkeyScope';
|
||||
import { createContext, ReactElement, useContext } from 'react';
|
||||
import { IconComponent } from 'twenty-ui';
|
||||
|
||||
@ -12,7 +11,7 @@ export type RecordInlineCellContextProps = {
|
||||
editModeContent?: ReactElement;
|
||||
editModeContentOnly?: boolean;
|
||||
displayModeContent?: ReactElement;
|
||||
customEditHotkeyScope?: HotkeyScope;
|
||||
customEditHotkeyScope?: string;
|
||||
isDisplayModeFixHeight?: boolean;
|
||||
disableHoverEffect?: boolean;
|
||||
loading?: boolean;
|
||||
|
||||
@ -1,7 +1,18 @@
|
||||
import { FieldContext } from '@/object-record/record-field/contexts/FieldContext';
|
||||
import { recordFieldInputLayoutDirectionComponentState } from '@/object-record/record-field/states/recordFieldInputLayoutDirectionComponentState';
|
||||
import { recordFieldInputLayoutDirectionLoadingComponentState } from '@/object-record/record-field/states/recordFieldInputLayoutDirectionLoadingComponentState';
|
||||
import { RecordInlineCellContext } from '@/object-record/record-inline-cell/components/RecordInlineCellContext';
|
||||
import { getRecordFieldInputId } from '@/object-record/utils/getRecordFieldInputId';
|
||||
import { OverlayContainer } from '@/ui/layout/overlay/components/OverlayContainer';
|
||||
import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentStateV2';
|
||||
import styled from '@emotion/styled';
|
||||
import { autoUpdate, flip, offset, useFloating } from '@floating-ui/react';
|
||||
import {
|
||||
MiddlewareState,
|
||||
autoUpdate,
|
||||
flip,
|
||||
offset,
|
||||
useFloating,
|
||||
} from '@floating-ui/react';
|
||||
import { useContext } from 'react';
|
||||
import { createPortal } from 'react-dom';
|
||||
|
||||
@ -24,6 +35,33 @@ export const RecordInlineCellEditMode = ({
|
||||
children,
|
||||
}: RecordInlineCellEditModeProps) => {
|
||||
const { isCentered } = useContext(RecordInlineCellContext);
|
||||
const { recordId, fieldDefinition } = useContext(FieldContext);
|
||||
|
||||
const instanceId = getRecordFieldInputId(
|
||||
recordId,
|
||||
fieldDefinition?.metadata?.fieldName,
|
||||
);
|
||||
|
||||
const setFieldInputLayoutDirection = useSetRecoilComponentStateV2(
|
||||
recordFieldInputLayoutDirectionComponentState,
|
||||
instanceId,
|
||||
);
|
||||
|
||||
const setFieldInputLayoutDirectionLoading = useSetRecoilComponentStateV2(
|
||||
recordFieldInputLayoutDirectionLoadingComponentState,
|
||||
instanceId,
|
||||
);
|
||||
|
||||
const setFieldInputLayoutDirectionMiddleware = {
|
||||
name: 'middleware',
|
||||
fn: async (state: MiddlewareState) => {
|
||||
setFieldInputLayoutDirection(
|
||||
state.placement.startsWith('bottom') ? 'downward' : 'upward',
|
||||
);
|
||||
setFieldInputLayoutDirectionLoading(false);
|
||||
return {};
|
||||
},
|
||||
};
|
||||
|
||||
const { refs, floatingStyles } = useFloating({
|
||||
placement: isCentered ? 'bottom' : 'bottom-start',
|
||||
@ -40,6 +78,7 @@ export const RecordInlineCellEditMode = ({
|
||||
crossAxis: -5,
|
||||
},
|
||||
),
|
||||
setFieldInputLayoutDirectionMiddleware,
|
||||
],
|
||||
whileElementsMounted: autoUpdate,
|
||||
});
|
||||
|
||||
@ -3,7 +3,6 @@ import { useRecoilState } from 'recoil';
|
||||
|
||||
import { FieldContext } from '@/object-record/record-field/contexts/FieldContext';
|
||||
import { usePreviousHotkeyScope } from '@/ui/utilities/hotkey/hooks/usePreviousHotkeyScope';
|
||||
import { HotkeyScope } from '@/ui/utilities/hotkey/types/HotkeyScope';
|
||||
import { isDefined } from 'twenty-shared';
|
||||
|
||||
import { useInitDraftValueV2 } from '@/object-record/record-field/hooks/useInitDraftValueV2';
|
||||
@ -48,16 +47,13 @@ export const useInlineCell = () => {
|
||||
goBackToPreviousDropdownFocusId();
|
||||
};
|
||||
|
||||
const openInlineCell = (customEditHotkeyScopeForField?: HotkeyScope) => {
|
||||
const openInlineCell = (customEditHotkeyScopeForField?: string) => {
|
||||
onOpenEditMode?.();
|
||||
setIsInlineCellInEditMode(true);
|
||||
initFieldInputDraftValue({ recordId, fieldDefinition });
|
||||
|
||||
if (isDefined(customEditHotkeyScopeForField)) {
|
||||
setHotkeyScopeAndMemorizePreviousScope(
|
||||
customEditHotkeyScopeForField.scope,
|
||||
customEditHotkeyScopeForField.customScopes,
|
||||
);
|
||||
setHotkeyScopeAndMemorizePreviousScope(customEditHotkeyScopeForField);
|
||||
} else {
|
||||
setHotkeyScopeAndMemorizePreviousScope(InlineCellHotkeyScope.InlineCell);
|
||||
}
|
||||
|
||||
@ -1,204 +0,0 @@
|
||||
import { useObjectRecordMultiSelectScopedStates } from '@/activities/hooks/useObjectRecordMultiSelectScopedStates';
|
||||
import { MultipleRecordPickerMenuItem } from '@/object-record/record-picker/components/MultipleRecordPickerMenuItem';
|
||||
import { RECORD_PICKER_SELECTABLE_LIST_COMPONENT_INSTANCE_ID } from '@/object-record/record-picker/constants/RecordPickerSelectableListComponentInstanceId';
|
||||
import { RecordPickerComponentInstanceContext } from '@/object-record/record-picker/states/contexts/RecordPickerComponentInstanceContext';
|
||||
import { recordPickerSearchFilterComponentState } from '@/object-record/record-picker/states/recordPickerSearchFilterComponentState';
|
||||
import { useHasObjectReadOnlyPermission } from '@/settings/roles/hooks/useHasObjectReadOnlyPermission';
|
||||
import { CreateNewButton } from '@/ui/input/relation-picker/components/CreateNewButton';
|
||||
import { DropdownMenuSkeletonItem } from '@/ui/input/relation-picker/components/skeletons/DropdownMenuSkeletonItem';
|
||||
import { DropdownMenu } from '@/ui/layout/dropdown/components/DropdownMenu';
|
||||
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
|
||||
import { DropdownMenuSearchInput } from '@/ui/layout/dropdown/components/DropdownMenuSearchInput';
|
||||
import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownMenuSeparator';
|
||||
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 { usePreviousHotkeyScope } from '@/ui/utilities/hotkey/hooks/usePreviousHotkeyScope';
|
||||
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
|
||||
import { useListenClickOutside } from '@/ui/utilities/pointer-event/hooks/useListenClickOutside';
|
||||
import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
|
||||
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
|
||||
import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentStateV2';
|
||||
import styled from '@emotion/styled';
|
||||
import { Placement } from '@floating-ui/react';
|
||||
import { useCallback, useRef } from 'react';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import { Key } from 'ts-key-enum';
|
||||
import { isDefined } from 'twenty-shared';
|
||||
import { IconPlus } from 'twenty-ui';
|
||||
import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull';
|
||||
|
||||
export const StyledSelectableItem = styled(SelectableItem)`
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
type MultipleRecordPickerProps = {
|
||||
onChange?: (changedRecordForSelectId: string) => void;
|
||||
onSubmit?: () => void;
|
||||
onCreate?: ((searchInput?: string) => void) | (() => void);
|
||||
dropdownPlacement?: Placement | null;
|
||||
componentInstanceId: string;
|
||||
};
|
||||
|
||||
export const MultipleRecordPicker = ({
|
||||
onChange,
|
||||
onSubmit,
|
||||
onCreate,
|
||||
dropdownPlacement,
|
||||
componentInstanceId,
|
||||
}: MultipleRecordPickerProps) => {
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const { goBackToPreviousHotkeyScope } = usePreviousHotkeyScope();
|
||||
|
||||
const instanceId = useAvailableComponentInstanceIdOrThrow(
|
||||
RecordPickerComponentInstanceContext,
|
||||
componentInstanceId,
|
||||
);
|
||||
|
||||
const { objectRecordsIdsMultiSelectState, recordMultiSelectIsLoadingState } =
|
||||
useObjectRecordMultiSelectScopedStates(instanceId);
|
||||
|
||||
const { resetSelectedItem } = useSelectableList(
|
||||
RECORD_PICKER_SELECTABLE_LIST_COMPONENT_INSTANCE_ID,
|
||||
);
|
||||
const recordMultiSelectIsLoading = useRecoilValue(
|
||||
recordMultiSelectIsLoadingState,
|
||||
);
|
||||
|
||||
const objectRecordsIdsMultiSelect = useRecoilValue(
|
||||
objectRecordsIdsMultiSelectState,
|
||||
);
|
||||
|
||||
const setSearchFilter = useSetRecoilComponentStateV2(
|
||||
recordPickerSearchFilterComponentState,
|
||||
instanceId,
|
||||
);
|
||||
|
||||
const recordPickerSearchFilter = useRecoilComponentValueV2(
|
||||
recordPickerSearchFilterComponentState,
|
||||
instanceId,
|
||||
);
|
||||
|
||||
const hasObjectReadOnlyPermission = useHasObjectReadOnlyPermission();
|
||||
|
||||
const handleSubmit = () => {
|
||||
onSubmit?.();
|
||||
goBackToPreviousHotkeyScope();
|
||||
resetSelectedItem();
|
||||
};
|
||||
|
||||
useScopedHotkeys(
|
||||
Key.Escape,
|
||||
() => {
|
||||
handleSubmit();
|
||||
},
|
||||
instanceId,
|
||||
[handleSubmit],
|
||||
);
|
||||
|
||||
useListenClickOutside({
|
||||
refs: [containerRef],
|
||||
callback: handleSubmit,
|
||||
listenerId: 'MULTI_RECORD_SELECT_LISTENER_ID',
|
||||
hotkeyScope: instanceId,
|
||||
});
|
||||
|
||||
const handleFilterChange = useCallback(
|
||||
(event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setSearchFilter(event.currentTarget.value);
|
||||
},
|
||||
[setSearchFilter],
|
||||
);
|
||||
|
||||
// TODO: refactor this in a separate component
|
||||
const results = (
|
||||
<DropdownMenuItemsContainer hasMaxHeight>
|
||||
<SelectableList
|
||||
selectableListId={RECORD_PICKER_SELECTABLE_LIST_COMPONENT_INSTANCE_ID}
|
||||
selectableItemIdArray={objectRecordsIdsMultiSelect}
|
||||
hotkeyScope={instanceId}
|
||||
onEnter={(selectedId) => {
|
||||
onChange?.(selectedId);
|
||||
resetSelectedItem();
|
||||
}}
|
||||
>
|
||||
{objectRecordsIdsMultiSelect?.map((recordId) => {
|
||||
return (
|
||||
<MultipleRecordPickerMenuItem
|
||||
key={recordId}
|
||||
objectRecordId={recordId}
|
||||
onChange={(recordId) => {
|
||||
onChange?.(recordId);
|
||||
resetSelectedItem();
|
||||
}}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</SelectableList>
|
||||
</DropdownMenuItemsContainer>
|
||||
);
|
||||
|
||||
const createNewButton = isDefined(onCreate) && (
|
||||
<CreateNewButton
|
||||
onClick={() => onCreate?.(recordPickerSearchFilter)}
|
||||
LeftIcon={IconPlus}
|
||||
text="Add New"
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<RecordPickerComponentInstanceContext.Provider
|
||||
value={{ instanceId: componentInstanceId }}
|
||||
>
|
||||
<DropdownMenu ref={containerRef} data-select-disable width={200}>
|
||||
{dropdownPlacement?.includes('end') && (
|
||||
<>
|
||||
{isDefined(onCreate) && !hasObjectReadOnlyPermission && (
|
||||
<DropdownMenuItemsContainer scrollable={false}>
|
||||
{createNewButton}
|
||||
</DropdownMenuItemsContainer>
|
||||
)}
|
||||
<DropdownMenuSeparator />
|
||||
{objectRecordsIdsMultiSelect?.length > 0 && results}
|
||||
{recordMultiSelectIsLoading && !recordPickerSearchFilter && (
|
||||
<>
|
||||
<DropdownMenuSkeletonItem />
|
||||
<DropdownMenuSeparator />
|
||||
</>
|
||||
)}
|
||||
{objectRecordsIdsMultiSelect?.length > 0 && (
|
||||
<DropdownMenuSeparator />
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
<DropdownMenuSearchInput
|
||||
value={recordPickerSearchFilter}
|
||||
onChange={handleFilterChange}
|
||||
autoFocus
|
||||
/>
|
||||
{(dropdownPlacement?.includes('start') ||
|
||||
isUndefinedOrNull(dropdownPlacement)) && (
|
||||
<>
|
||||
<DropdownMenuSeparator />
|
||||
{recordMultiSelectIsLoading && !recordPickerSearchFilter && (
|
||||
<>
|
||||
<DropdownMenuSkeletonItem />
|
||||
<DropdownMenuSeparator />
|
||||
</>
|
||||
)}
|
||||
{objectRecordsIdsMultiSelect?.length > 0 && results}
|
||||
{objectRecordsIdsMultiSelect?.length > 0 && (
|
||||
<DropdownMenuSeparator />
|
||||
)}
|
||||
{isDefined(onCreate) && (
|
||||
<DropdownMenuItemsContainer scrollable={false}>
|
||||
{createNewButton}
|
||||
</DropdownMenuItemsContainer>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</DropdownMenu>
|
||||
</RecordPickerComponentInstanceContext.Provider>
|
||||
);
|
||||
};
|
||||
@ -1,88 +0,0 @@
|
||||
import styled from '@emotion/styled';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import { Avatar, MenuItemMultiSelectAvatar } from 'twenty-ui';
|
||||
|
||||
import { useObjectRecordMultiSelectScopedStates } from '@/activities/hooks/useObjectRecordMultiSelectScopedStates';
|
||||
import { RECORD_PICKER_SELECTABLE_LIST_COMPONENT_INSTANCE_ID } from '@/object-record/record-picker/constants/RecordPickerSelectableListComponentInstanceId';
|
||||
import { RecordPickerComponentInstanceContext } from '@/object-record/record-picker/states/contexts/RecordPickerComponentInstanceContext';
|
||||
import { SelectableItem } from '@/ui/layout/selectable-list/components/SelectableItem';
|
||||
import { useSelectableList } from '@/ui/layout/selectable-list/hooks/useSelectableList';
|
||||
import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
|
||||
import { isDefined } from 'twenty-shared';
|
||||
|
||||
export const StyledSelectableItem = styled(SelectableItem)`
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
export const MultipleRecordPickerMenuItem = ({
|
||||
objectRecordId,
|
||||
onChange,
|
||||
}: {
|
||||
objectRecordId: string;
|
||||
onChange?: (changedRecordForSelectId: string) => void;
|
||||
}) => {
|
||||
const { isSelectedItemIdSelector } = useSelectableList(
|
||||
RECORD_PICKER_SELECTABLE_LIST_COMPONENT_INSTANCE_ID,
|
||||
);
|
||||
|
||||
const isSelectedByKeyboard = useRecoilValue(
|
||||
isSelectedItemIdSelector(objectRecordId),
|
||||
);
|
||||
const instanceId = useAvailableComponentInstanceIdOrThrow(
|
||||
RecordPickerComponentInstanceContext,
|
||||
);
|
||||
|
||||
const {
|
||||
objectRecordMultiSelectFamilyState,
|
||||
objectRecordMultiSelectCheckedRecordsIdsState,
|
||||
} = useObjectRecordMultiSelectScopedStates(instanceId);
|
||||
|
||||
const record = useRecoilValue(
|
||||
objectRecordMultiSelectFamilyState(objectRecordId),
|
||||
);
|
||||
|
||||
const objectRecordMultiSelectCheckedRecordsIds = useRecoilValue(
|
||||
objectRecordMultiSelectCheckedRecordsIdsState,
|
||||
);
|
||||
|
||||
if (!record) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const handleSelectChange = () => {
|
||||
onChange?.(objectRecordId);
|
||||
};
|
||||
|
||||
const { recordIdentifier } = record;
|
||||
|
||||
if (!isDefined(recordIdentifier)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const selected = objectRecordMultiSelectCheckedRecordsIds.find(
|
||||
(checkedObjectRecord) => checkedObjectRecord === objectRecordId,
|
||||
)
|
||||
? true
|
||||
: false;
|
||||
|
||||
return (
|
||||
<StyledSelectableItem itemId={objectRecordId} key={objectRecordId}>
|
||||
<MenuItemMultiSelectAvatar
|
||||
onSelectChange={(_isNewlySelectedValue) => handleSelectChange()}
|
||||
isKeySelected={isSelectedByKeyboard}
|
||||
selected={selected}
|
||||
avatar={
|
||||
<Avatar
|
||||
avatarUrl={recordIdentifier.avatarUrl}
|
||||
placeholderColorSeed={objectRecordId}
|
||||
placeholder={recordIdentifier.name}
|
||||
size="md"
|
||||
type={recordIdentifier.avatarType ?? 'rounded'}
|
||||
/>
|
||||
}
|
||||
text={recordIdentifier.name}
|
||||
/>
|
||||
</StyledSelectableItem>
|
||||
);
|
||||
};
|
||||
@ -1,2 +0,0 @@
|
||||
export const RECORD_PICKER_CLICK_OUTSIDE_LISTENER_ID =
|
||||
'record-picker-click-outside-listener';
|
||||
@ -1,2 +0,0 @@
|
||||
export const RECORD_PICKER_SELECTABLE_LIST_COMPONENT_INSTANCE_ID =
|
||||
'record-picker-selectable-list-component-instance-id';
|
||||
@ -0,0 +1,38 @@
|
||||
import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems';
|
||||
import { multipleRecordPickerSinglePickableMorphItemComponentFamilySelector } from '@/object-record/record-picker/multiple-record-picker/states/selectors/multipleRecordPickerSinglePickableMorphItemComponentFamilySelector';
|
||||
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
|
||||
import { useRecoilComponentFamilyValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentFamilyValueV2';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import { isDefined } from 'twenty-shared';
|
||||
|
||||
type UseRecordPickerGetRecordAndObjectMetadataItemFromRecordIdProps = {
|
||||
recordId: string;
|
||||
};
|
||||
|
||||
export const useRecordPickerGetRecordAndObjectMetadataItemFromRecordId = ({
|
||||
recordId,
|
||||
}: UseRecordPickerGetRecordAndObjectMetadataItemFromRecordIdProps) => {
|
||||
const { objectMetadataItems } = useObjectMetadataItems();
|
||||
|
||||
const pickableMorphItem = useRecoilComponentFamilyValueV2(
|
||||
multipleRecordPickerSinglePickableMorphItemComponentFamilySelector,
|
||||
recordId,
|
||||
);
|
||||
|
||||
const record = useRecoilValue(recordStoreFamilyState(recordId));
|
||||
|
||||
if (!isDefined(pickableMorphItem)) {
|
||||
return { record: null, objectMetadataItem: null };
|
||||
}
|
||||
|
||||
const objectMetadataItem = objectMetadataItems.find(
|
||||
(objectMetadataItem) =>
|
||||
objectMetadataItem.id === pickableMorphItem.objectMetadataId,
|
||||
);
|
||||
|
||||
if (!isDefined(objectMetadataItem)) {
|
||||
return { record: null, objectMetadataItem: null };
|
||||
}
|
||||
|
||||
return { record, objectMetadataItem };
|
||||
};
|
||||
@ -1,26 +0,0 @@
|
||||
import { recordPickerSearchFilterComponentState } from '@/object-record/record-picker/states/recordPickerSearchFilterComponentState';
|
||||
import { useFilteredSearchRecordQuery } from '@/search/hooks/useFilteredSearchRecordQuery';
|
||||
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
|
||||
|
||||
export const useRecordPickerRecordsOptions = ({
|
||||
objectNameSingular,
|
||||
selectedRecordIds = [],
|
||||
excludedRecordIds = [],
|
||||
}: {
|
||||
objectNameSingular: string;
|
||||
selectedRecordIds?: string[];
|
||||
excludedRecordIds?: string[];
|
||||
}) => {
|
||||
const recordPickerSearchFilter = useRecoilComponentValueV2(
|
||||
recordPickerSearchFilterComponentState,
|
||||
);
|
||||
|
||||
const records = useFilteredSearchRecordQuery({
|
||||
searchFilter: recordPickerSearchFilter,
|
||||
selectedIds: selectedRecordIds,
|
||||
excludedRecordIds: excludedRecordIds,
|
||||
objectNameSingular,
|
||||
});
|
||||
|
||||
return { records };
|
||||
};
|
||||
@ -0,0 +1,170 @@
|
||||
import { MultipleRecordPickerMenuItems } from '@/object-record/record-picker/multiple-record-picker/components/MultipleRecordPickerMenuItems';
|
||||
import { MultipleRecordPickerOnClickOutsideEffect } from '@/object-record/record-picker/multiple-record-picker/components/MultipleRecordPickerOnClickOutsideEffect';
|
||||
import { MultipleRecordPickerSearchInput } from '@/object-record/record-picker/multiple-record-picker/components/MultipleRecordPickerSearchInput';
|
||||
import { MultipleRecordPickerComponentInstanceContext } from '@/object-record/record-picker/multiple-record-picker/states/contexts/MultipleRecordPickerComponentInstanceContext';
|
||||
import { multipleRecordPickerIsLoadingComponentState } from '@/object-record/record-picker/multiple-record-picker/states/multipleRecordPickerIsLoadingComponentState';
|
||||
import { multipleRecordPickerSearchFilterComponentState } from '@/object-record/record-picker/multiple-record-picker/states/multipleRecordPickerSearchFilterComponentState';
|
||||
import { multipleRecordPickerPickableMorphItemsLengthComponentSelector } from '@/object-record/record-picker/multiple-record-picker/states/selectors/multipleRecordPickerPickableMorphItemsLengthComponentSelector';
|
||||
import { MultipleRecordPickerHotkeyScope } from '@/object-record/record-picker/multiple-record-picker/types/MultipleRecordPickerHotkeyScope';
|
||||
import { getMultipleRecordPickerSelectableListId } from '@/object-record/record-picker/multiple-record-picker/utils/getMultipleRecordPickerSelectableListId';
|
||||
import { RecordPickerLayoutDirection } from '@/object-record/record-picker/types/RecordPickerLayoutDirection';
|
||||
import { RecordPickerPickableMorphItem } from '@/object-record/record-picker/types/RecordPickerPickableMorphItem';
|
||||
import { useHasObjectReadOnlyPermission } from '@/settings/roles/hooks/useHasObjectReadOnlyPermission';
|
||||
import { CreateNewButton } from '@/ui/input/relation-picker/components/CreateNewButton';
|
||||
import { DropdownMenuSkeletonItem } from '@/ui/input/relation-picker/components/skeletons/DropdownMenuSkeletonItem';
|
||||
import { DropdownMenu } from '@/ui/layout/dropdown/components/DropdownMenu';
|
||||
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
|
||||
import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownMenuSeparator';
|
||||
import { SelectableItem } from '@/ui/layout/selectable-list/components/SelectableItem';
|
||||
import { useSelectableList } from '@/ui/layout/selectable-list/hooks/useSelectableList';
|
||||
import { usePreviousHotkeyScope } from '@/ui/utilities/hotkey/hooks/usePreviousHotkeyScope';
|
||||
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
|
||||
import { useRecoilComponentCallbackStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackStateV2';
|
||||
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
|
||||
import styled from '@emotion/styled';
|
||||
import { useRef } from 'react';
|
||||
import { useRecoilCallback } from 'recoil';
|
||||
import { Key } from 'ts-key-enum';
|
||||
import { isDefined } from 'twenty-shared';
|
||||
import { IconPlus } from 'twenty-ui';
|
||||
|
||||
export const StyledSelectableItem = styled(SelectableItem)`
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
type MultipleRecordPickerProps = {
|
||||
onChange?: (morphItem: RecordPickerPickableMorphItem) => void;
|
||||
onSubmit?: () => void;
|
||||
onCreate?: ((searchInput?: string) => void) | (() => void);
|
||||
layoutDirection?: RecordPickerLayoutDirection;
|
||||
componentInstanceId: string;
|
||||
onClickOutside: () => void;
|
||||
};
|
||||
|
||||
export const MultipleRecordPicker = ({
|
||||
onChange,
|
||||
onSubmit,
|
||||
onCreate,
|
||||
onClickOutside,
|
||||
layoutDirection = 'search-bar-on-bottom',
|
||||
componentInstanceId,
|
||||
}: MultipleRecordPickerProps) => {
|
||||
const { goBackToPreviousHotkeyScope } = usePreviousHotkeyScope();
|
||||
|
||||
const selectableListComponentInstanceId =
|
||||
getMultipleRecordPickerSelectableListId(componentInstanceId);
|
||||
|
||||
const { resetSelectedItem } = useSelectableList(
|
||||
selectableListComponentInstanceId,
|
||||
);
|
||||
|
||||
const multipleRecordPickerIsLoading = useRecoilComponentValueV2(
|
||||
multipleRecordPickerIsLoadingComponentState,
|
||||
componentInstanceId,
|
||||
);
|
||||
|
||||
const itemsLength = useRecoilComponentValueV2(
|
||||
multipleRecordPickerPickableMorphItemsLengthComponentSelector,
|
||||
componentInstanceId,
|
||||
);
|
||||
|
||||
const multipleRecordPickerSearchFilterState =
|
||||
useRecoilComponentCallbackStateV2(
|
||||
multipleRecordPickerSearchFilterComponentState,
|
||||
componentInstanceId,
|
||||
);
|
||||
|
||||
const hasObjectReadOnlyPermission = useHasObjectReadOnlyPermission();
|
||||
|
||||
const handleSubmit = () => {
|
||||
onSubmit?.();
|
||||
goBackToPreviousHotkeyScope();
|
||||
resetSelectedItem();
|
||||
};
|
||||
|
||||
useScopedHotkeys(
|
||||
Key.Escape,
|
||||
() => {
|
||||
handleSubmit();
|
||||
},
|
||||
MultipleRecordPickerHotkeyScope.MultipleRecordPicker,
|
||||
[handleSubmit],
|
||||
);
|
||||
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const handleCreateNewButtonClick = useRecoilCallback(
|
||||
({ snapshot }) => {
|
||||
return () => {
|
||||
const recordPickerSearchFilter = snapshot
|
||||
.getLoadable(multipleRecordPickerSearchFilterState)
|
||||
.getValue();
|
||||
onCreate?.(recordPickerSearchFilter);
|
||||
};
|
||||
},
|
||||
[multipleRecordPickerSearchFilterState, onCreate],
|
||||
);
|
||||
|
||||
const createNewButton = isDefined(onCreate) && (
|
||||
<CreateNewButton
|
||||
onClick={handleCreateNewButtonClick}
|
||||
LeftIcon={IconPlus}
|
||||
text="Add New"
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<MultipleRecordPickerComponentInstanceContext.Provider
|
||||
value={{ instanceId: componentInstanceId }}
|
||||
>
|
||||
<MultipleRecordPickerOnClickOutsideEffect
|
||||
containerRef={containerRef}
|
||||
onClickOutside={onClickOutside}
|
||||
/>
|
||||
<DropdownMenu ref={containerRef} data-select-disable width={200}>
|
||||
{layoutDirection === 'search-bar-on-bottom' && (
|
||||
<>
|
||||
{isDefined(onCreate) && !hasObjectReadOnlyPermission && (
|
||||
<DropdownMenuItemsContainer scrollable={false}>
|
||||
{createNewButton}
|
||||
</DropdownMenuItemsContainer>
|
||||
)}
|
||||
<DropdownMenuSeparator />
|
||||
{itemsLength > 0 && (
|
||||
<MultipleRecordPickerMenuItems onChange={onChange} />
|
||||
)}
|
||||
{multipleRecordPickerIsLoading && (
|
||||
<>
|
||||
<DropdownMenuSkeletonItem />
|
||||
<DropdownMenuSeparator />
|
||||
</>
|
||||
)}
|
||||
{itemsLength > 0 && <DropdownMenuSeparator />}
|
||||
</>
|
||||
)}
|
||||
<MultipleRecordPickerSearchInput />
|
||||
{layoutDirection === 'search-bar-on-top' && (
|
||||
<>
|
||||
<DropdownMenuSeparator />
|
||||
{multipleRecordPickerIsLoading && (
|
||||
<>
|
||||
<DropdownMenuSkeletonItem />
|
||||
<DropdownMenuSeparator />
|
||||
</>
|
||||
)}
|
||||
{itemsLength > 0 && (
|
||||
<MultipleRecordPickerMenuItems onChange={onChange} />
|
||||
)}
|
||||
{itemsLength > 0 && <DropdownMenuSeparator />}
|
||||
{isDefined(onCreate) && (
|
||||
<DropdownMenuItemsContainer scrollable={false}>
|
||||
{createNewButton}
|
||||
</DropdownMenuItemsContainer>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</DropdownMenu>
|
||||
</MultipleRecordPickerComponentInstanceContext.Provider>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,39 @@
|
||||
import styled from '@emotion/styled';
|
||||
|
||||
import { useRecordPickerGetRecordAndObjectMetadataItemFromRecordId } from '@/object-record/record-picker/hooks/useRecordPickerGetRecordAndObjectMetadataItemFromRecordId';
|
||||
import { MultipleRecordPickerMenuItemContent } from '@/object-record/record-picker/multiple-record-picker/components/MultipleRecordPickerMenuItemContent';
|
||||
import { RecordPickerPickableMorphItem } from '@/object-record/record-picker/types/RecordPickerPickableMorphItem';
|
||||
import { SelectableItem } from '@/ui/layout/selectable-list/components/SelectableItem';
|
||||
import { isDefined } from 'twenty-shared';
|
||||
|
||||
export const StyledSelectableItem = styled(SelectableItem)`
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
type MultipleRecordPickerMenuItemProps = {
|
||||
recordId: string;
|
||||
onChange: (morphItem: RecordPickerPickableMorphItem) => void;
|
||||
};
|
||||
|
||||
export const MultipleRecordPickerMenuItem = ({
|
||||
recordId,
|
||||
onChange,
|
||||
}: MultipleRecordPickerMenuItemProps) => {
|
||||
const { record, objectMetadataItem } =
|
||||
useRecordPickerGetRecordAndObjectMetadataItemFromRecordId({
|
||||
recordId,
|
||||
});
|
||||
|
||||
if (!isDefined(record) || !isDefined(objectMetadataItem)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<MultipleRecordPickerMenuItemContent
|
||||
record={record}
|
||||
objectMetadataItem={objectMetadataItem}
|
||||
onChange={onChange}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,92 @@
|
||||
import styled from '@emotion/styled';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import { Avatar, MenuItemMultiSelectAvatar } from 'twenty-ui';
|
||||
|
||||
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||
import { getObjectRecordIdentifier } from '@/object-metadata/utils/getObjectRecordIdentifier';
|
||||
import { MultipleRecordPickerComponentInstanceContext } from '@/object-record/record-picker/multiple-record-picker/states/contexts/MultipleRecordPickerComponentInstanceContext';
|
||||
import { multipleRecordPickerIsSelectedComponentFamilySelector } from '@/object-record/record-picker/multiple-record-picker/states/selectors/multipleRecordPickerIsSelectedComponentFamilySelector';
|
||||
import { getMultipleRecordPickerSelectableListId } from '@/object-record/record-picker/multiple-record-picker/utils/getMultipleRecordPickerSelectableListId';
|
||||
import { RecordPickerPickableMorphItem } from '@/object-record/record-picker/types/RecordPickerPickableMorphItem';
|
||||
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
|
||||
import { SelectableItem } from '@/ui/layout/selectable-list/components/SelectableItem';
|
||||
import { useSelectableList } from '@/ui/layout/selectable-list/hooks/useSelectableList';
|
||||
import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
|
||||
import { useRecoilComponentFamilyValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentFamilyValueV2';
|
||||
import { isDefined } from 'twenty-shared';
|
||||
|
||||
export const StyledSelectableItem = styled(SelectableItem)`
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
type MultipleRecordPickerMenuItemContentProps = {
|
||||
record: ObjectRecord;
|
||||
objectMetadataItem: ObjectMetadataItem;
|
||||
onChange: (morphItem: RecordPickerPickableMorphItem) => void;
|
||||
};
|
||||
|
||||
export const MultipleRecordPickerMenuItemContent = ({
|
||||
record,
|
||||
objectMetadataItem,
|
||||
onChange,
|
||||
}: MultipleRecordPickerMenuItemContentProps) => {
|
||||
const componentInstanceId = useAvailableComponentInstanceIdOrThrow(
|
||||
MultipleRecordPickerComponentInstanceContext,
|
||||
);
|
||||
|
||||
const selectableListComponentInstanceId =
|
||||
getMultipleRecordPickerSelectableListId(componentInstanceId);
|
||||
|
||||
const { isSelectedItemIdSelector } = useSelectableList(
|
||||
selectableListComponentInstanceId,
|
||||
);
|
||||
|
||||
const isSelectedByKeyboard = useRecoilValue(
|
||||
isSelectedItemIdSelector(record.id),
|
||||
);
|
||||
|
||||
const isRecordSelectedWithObjectItem = useRecoilComponentFamilyValueV2(
|
||||
multipleRecordPickerIsSelectedComponentFamilySelector,
|
||||
record.id,
|
||||
componentInstanceId,
|
||||
);
|
||||
|
||||
const handleSelectChange = (isSelected: boolean) => {
|
||||
onChange({
|
||||
recordId: record.id,
|
||||
objectMetadataId: objectMetadataItem.id,
|
||||
isSelected,
|
||||
isMatchingSearchFilter: true,
|
||||
});
|
||||
};
|
||||
|
||||
const recordIdentifier = getObjectRecordIdentifier({
|
||||
objectMetadataItem,
|
||||
record,
|
||||
});
|
||||
|
||||
if (!isDefined(recordIdentifier)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<StyledSelectableItem itemId={record.id} key={record.id}>
|
||||
<MenuItemMultiSelectAvatar
|
||||
onSelectChange={(isSelected) => handleSelectChange(isSelected)}
|
||||
isKeySelected={isSelectedByKeyboard}
|
||||
selected={isRecordSelectedWithObjectItem}
|
||||
avatar={
|
||||
<Avatar
|
||||
avatarUrl={recordIdentifier.avatarUrl}
|
||||
placeholderColorSeed={record.id}
|
||||
placeholder={recordIdentifier.name}
|
||||
size="md"
|
||||
type={recordIdentifier.avatarType ?? 'rounded'}
|
||||
/>
|
||||
}
|
||||
text={recordIdentifier.name}
|
||||
/>
|
||||
</StyledSelectableItem>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,138 @@
|
||||
import styled from '@emotion/styled';
|
||||
|
||||
import { MultipleRecordPickerMenuItem } from '@/object-record/record-picker/multiple-record-picker/components/MultipleRecordPickerMenuItem';
|
||||
import { MultipleRecordPickerComponentInstanceContext } from '@/object-record/record-picker/multiple-record-picker/states/contexts/MultipleRecordPickerComponentInstanceContext';
|
||||
import { multipleRecordPickerPickableMorphItemsComponentState } from '@/object-record/record-picker/multiple-record-picker/states/multipleRecordPickerPickableMorphItemsComponentState';
|
||||
import { multipleRecordPickerPickableRecordIdsMatchingSearchComponentSelector } from '@/object-record/record-picker/multiple-record-picker/states/selectors/multipleRecordPickerPickableRecordIdsMatchingSearchComponentSelector';
|
||||
import { multipleRecordPickerSinglePickableMorphItemComponentFamilySelector } from '@/object-record/record-picker/multiple-record-picker/states/selectors/multipleRecordPickerSinglePickableMorphItemComponentFamilySelector';
|
||||
import { MultipleRecordPickerHotkeyScope } from '@/object-record/record-picker/multiple-record-picker/types/MultipleRecordPickerHotkeyScope';
|
||||
import { getMultipleRecordPickerSelectableListId } from '@/object-record/record-picker/multiple-record-picker/utils/getMultipleRecordPickerSelectableListId';
|
||||
import { RecordPickerPickableMorphItem } from '@/object-record/record-picker/types/RecordPickerPickableMorphItem';
|
||||
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
|
||||
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 { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
|
||||
import { useRecoilComponentCallbackStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackStateV2';
|
||||
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
|
||||
import { useRecoilCallback } from 'recoil';
|
||||
import { isDefined } from 'twenty-shared';
|
||||
|
||||
export const StyledSelectableItem = styled(SelectableItem)`
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
type MultipleRecordPickerMenuItemsProps = {
|
||||
onChange?: (morphItem: RecordPickerPickableMorphItem) => void;
|
||||
};
|
||||
|
||||
export const MultipleRecordPickerMenuItems = ({
|
||||
onChange,
|
||||
}: MultipleRecordPickerMenuItemsProps) => {
|
||||
const componentInstanceId = useAvailableComponentInstanceIdOrThrow(
|
||||
MultipleRecordPickerComponentInstanceContext,
|
||||
);
|
||||
|
||||
const selectableListComponentInstanceId =
|
||||
getMultipleRecordPickerSelectableListId(componentInstanceId);
|
||||
|
||||
const pickableRecordIds = useRecoilComponentValueV2(
|
||||
multipleRecordPickerPickableRecordIdsMatchingSearchComponentSelector,
|
||||
componentInstanceId,
|
||||
);
|
||||
|
||||
const { resetSelectedItem } = useSelectableList(
|
||||
selectableListComponentInstanceId,
|
||||
);
|
||||
const singlePickableMorphItemFamilySelector =
|
||||
useRecoilComponentCallbackStateV2(
|
||||
multipleRecordPickerSinglePickableMorphItemComponentFamilySelector,
|
||||
componentInstanceId,
|
||||
);
|
||||
|
||||
const multipleRecordPickerPickableMorphItemsState =
|
||||
useRecoilComponentCallbackStateV2(
|
||||
multipleRecordPickerPickableMorphItemsComponentState,
|
||||
componentInstanceId,
|
||||
);
|
||||
|
||||
const handleChange = useRecoilCallback(
|
||||
({ snapshot, set }) => {
|
||||
return (morphItem: RecordPickerPickableMorphItem) => {
|
||||
const previousMorphItems = snapshot
|
||||
.getLoadable(multipleRecordPickerPickableMorphItemsState)
|
||||
.getValue();
|
||||
|
||||
const existingMorphItemIndex = previousMorphItems.findIndex(
|
||||
(item) => item.recordId === morphItem.recordId,
|
||||
);
|
||||
|
||||
const newMorphItems = [...previousMorphItems];
|
||||
|
||||
if (existingMorphItemIndex === -1) {
|
||||
newMorphItems.push(morphItem);
|
||||
} else {
|
||||
newMorphItems[existingMorphItemIndex] = morphItem;
|
||||
}
|
||||
|
||||
set(multipleRecordPickerPickableMorphItemsState, newMorphItems);
|
||||
};
|
||||
},
|
||||
[multipleRecordPickerPickableMorphItemsState],
|
||||
);
|
||||
|
||||
const handleEnter = useRecoilCallback(
|
||||
({ snapshot }) => {
|
||||
return (selectedId: string) => {
|
||||
const pickableMorphItem = snapshot
|
||||
.getLoadable(singlePickableMorphItemFamilySelector(selectedId))
|
||||
.getValue();
|
||||
|
||||
if (!isDefined(pickableMorphItem)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const selectedMorphItem = {
|
||||
...pickableMorphItem,
|
||||
isSelected: !pickableMorphItem.isSelected,
|
||||
};
|
||||
|
||||
handleChange(selectedMorphItem);
|
||||
onChange?.(selectedMorphItem);
|
||||
resetSelectedItem();
|
||||
};
|
||||
},
|
||||
[
|
||||
handleChange,
|
||||
onChange,
|
||||
resetSelectedItem,
|
||||
singlePickableMorphItemFamilySelector,
|
||||
],
|
||||
);
|
||||
|
||||
return (
|
||||
<DropdownMenuItemsContainer hasMaxHeight>
|
||||
<SelectableList
|
||||
selectableListId={selectableListComponentInstanceId}
|
||||
selectableItemIdArray={pickableRecordIds}
|
||||
hotkeyScope={MultipleRecordPickerHotkeyScope.MultipleRecordPicker}
|
||||
onEnter={handleEnter}
|
||||
>
|
||||
{pickableRecordIds.map((recordId) => {
|
||||
return (
|
||||
<MultipleRecordPickerMenuItem
|
||||
key={recordId}
|
||||
recordId={recordId}
|
||||
onChange={(morphItem) => {
|
||||
handleChange(morphItem);
|
||||
onChange?.(morphItem);
|
||||
resetSelectedItem();
|
||||
}}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</SelectableList>
|
||||
</DropdownMenuItemsContainer>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,24 @@
|
||||
import { MULTIPLE_RECORD_PICKER_CLICK_OUTSIDE_LISTENER_ID } from '@/object-record/record-picker/multiple-record-picker/constants/MultipleRecordPickerClickOutsideListenerId';
|
||||
import { useListenClickOutside } from '@/ui/utilities/pointer-event/hooks/useListenClickOutside';
|
||||
|
||||
export const MultipleRecordPickerOnClickOutsideEffect = ({
|
||||
containerRef,
|
||||
onClickOutside,
|
||||
}: {
|
||||
containerRef: React.RefObject<HTMLDivElement>;
|
||||
onClickOutside: () => void;
|
||||
}) => {
|
||||
useListenClickOutside({
|
||||
refs: [containerRef],
|
||||
callback: (event) => {
|
||||
event.stopImmediatePropagation();
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
|
||||
onClickOutside();
|
||||
},
|
||||
listenerId: MULTIPLE_RECORD_PICKER_CLICK_OUTSIDE_LISTENER_ID,
|
||||
});
|
||||
|
||||
return <></>;
|
||||
};
|
||||
@ -0,0 +1,45 @@
|
||||
import styled from '@emotion/styled';
|
||||
|
||||
import { useMultipleRecordPickerPerformSearch } from '@/object-record/record-picker/multiple-record-picker/hooks/useMultipleRecordPickerPerformSearch';
|
||||
import { MultipleRecordPickerComponentInstanceContext } from '@/object-record/record-picker/multiple-record-picker/states/contexts/MultipleRecordPickerComponentInstanceContext';
|
||||
import { multipleRecordPickerSearchFilterComponentState } from '@/object-record/record-picker/multiple-record-picker/states/multipleRecordPickerSearchFilterComponentState';
|
||||
import { DropdownMenuSearchInput } from '@/ui/layout/dropdown/components/DropdownMenuSearchInput';
|
||||
import { SelectableItem } from '@/ui/layout/selectable-list/components/SelectableItem';
|
||||
import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
|
||||
import { useRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentStateV2';
|
||||
import { useCallback } from 'react';
|
||||
|
||||
export const StyledSelectableItem = styled(SelectableItem)`
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
export const MultipleRecordPickerSearchInput = () => {
|
||||
const componentInstanceId = useAvailableComponentInstanceIdOrThrow(
|
||||
MultipleRecordPickerComponentInstanceContext,
|
||||
);
|
||||
|
||||
const [recordPickerSearchFilter, setRecordPickerSearchFilter] =
|
||||
useRecoilComponentStateV2(multipleRecordPickerSearchFilterComponentState);
|
||||
|
||||
const { performSearch } = useMultipleRecordPickerPerformSearch();
|
||||
|
||||
const handleFilterChange = useCallback(
|
||||
(event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setRecordPickerSearchFilter(event.currentTarget.value);
|
||||
performSearch({
|
||||
multipleRecordPickerInstanceId: componentInstanceId,
|
||||
forceSearchFilter: event.currentTarget.value,
|
||||
});
|
||||
},
|
||||
[componentInstanceId, performSearch, setRecordPickerSearchFilter],
|
||||
);
|
||||
|
||||
return (
|
||||
<DropdownMenuSearchInput
|
||||
value={recordPickerSearchFilter}
|
||||
onChange={handleFilterChange}
|
||||
autoFocus
|
||||
/>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,2 @@
|
||||
export const MULTIPLE_RECORD_PICKER_CLICK_OUTSIDE_LISTENER_ID =
|
||||
'multiple-record-picker-click-outside-listener';
|
||||
@ -0,0 +1,339 @@
|
||||
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||
import { CombinedFindManyRecordsQueryResult } from '@/object-record/multiple-objects/types/CombinedFindManyRecordsQueryResult';
|
||||
import { generateCombinedSearchRecordsQuery } from '@/object-record/multiple-objects/utils/generateCombinedSearchRecordsQuery';
|
||||
import { multipleRecordPickerPickableMorphItemsComponentState } from '@/object-record/record-picker/multiple-record-picker/states/multipleRecordPickerPickableMorphItemsComponentState';
|
||||
import { multipleRecordPickerSearchFilterComponentState } from '@/object-record/record-picker/multiple-record-picker/states/multipleRecordPickerSearchFilterComponentState';
|
||||
import { multipleRecordPickerSearchableObjectMetadataItemsComponentState } from '@/object-record/record-picker/multiple-record-picker/states/multipleRecordPickerSearchableObjectMetadataItemsComponentState';
|
||||
import { multipleRecordPickerformatQueryResultAsRecordsWithObjectMetadataId } from '@/object-record/record-picker/multiple-record-picker/utils/multipleRecordPickerformatQueryResultAsRecordWithObjectMetadataId';
|
||||
import { RecordPickerPickableMorphItem } from '@/object-record/record-picker/types/RecordPickerPickableMorphItem';
|
||||
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
|
||||
import { ApolloClient, useApolloClient } from '@apollo/client';
|
||||
import { isNonEmptyArray } from '@sniptt/guards';
|
||||
import { useRecoilCallback } from 'recoil';
|
||||
import { capitalize, isDefined } from 'twenty-shared';
|
||||
|
||||
export const useMultipleRecordPickerPerformSearch = () => {
|
||||
const client = useApolloClient();
|
||||
|
||||
const performSearch = useRecoilCallback(
|
||||
({ snapshot, set }) =>
|
||||
async ({
|
||||
multipleRecordPickerInstanceId,
|
||||
forceSearchFilter = '',
|
||||
forceSearchableObjectMetadataItems = [],
|
||||
forcePickableMorphItems = [],
|
||||
}: {
|
||||
multipleRecordPickerInstanceId: string;
|
||||
forceSearchFilter?: string;
|
||||
forceSearchableObjectMetadataItems?: ObjectMetadataItem[];
|
||||
forcePickableMorphItems?: RecordPickerPickableMorphItem[];
|
||||
}) => {
|
||||
const recordPickerSearchFilter = snapshot
|
||||
.getLoadable(
|
||||
multipleRecordPickerSearchFilterComponentState.atomFamily({
|
||||
instanceId: multipleRecordPickerInstanceId,
|
||||
}),
|
||||
)
|
||||
.getValue();
|
||||
|
||||
const searchFilter = forceSearchFilter ?? recordPickerSearchFilter;
|
||||
|
||||
const recordPickerSearchableObjectMetadataItems = snapshot
|
||||
.getLoadable(
|
||||
multipleRecordPickerSearchableObjectMetadataItemsComponentState.atomFamily(
|
||||
{ instanceId: multipleRecordPickerInstanceId },
|
||||
),
|
||||
)
|
||||
.getValue();
|
||||
|
||||
const searchableObjectMetadataItems =
|
||||
forceSearchableObjectMetadataItems.length > 0
|
||||
? forceSearchableObjectMetadataItems
|
||||
: recordPickerSearchableObjectMetadataItems;
|
||||
|
||||
const recordPickerPickableMorphItems = snapshot
|
||||
.getLoadable(
|
||||
multipleRecordPickerPickableMorphItemsComponentState.atomFamily({
|
||||
instanceId: multipleRecordPickerInstanceId,
|
||||
}),
|
||||
)
|
||||
.getValue();
|
||||
|
||||
const pickableMorphItems =
|
||||
forcePickableMorphItems.length > 0
|
||||
? forcePickableMorphItems
|
||||
: recordPickerPickableMorphItems;
|
||||
|
||||
const recordsWithObjectMetadataIdFilteredOnPickedRecords =
|
||||
await performSearchForPickedRecords({
|
||||
client,
|
||||
searchFilter,
|
||||
searchableObjectMetadataItems,
|
||||
pickableMorphItems,
|
||||
});
|
||||
|
||||
const recordsWithObjectMetadataIdExcludingPickedRecords =
|
||||
await performSearchExcludingPickedRecords({
|
||||
client,
|
||||
searchFilter,
|
||||
searchableObjectMetadataItems,
|
||||
pickableMorphItems,
|
||||
});
|
||||
|
||||
const pickedMorphItems = pickableMorphItems.filter(
|
||||
({ isSelected }) => isSelected,
|
||||
);
|
||||
|
||||
// We update the existing pickedMorphItems to be matching the search filter
|
||||
const updatedPickedMorphItems = pickedMorphItems.map((morphItem) => {
|
||||
const record =
|
||||
recordsWithObjectMetadataIdFilteredOnPickedRecords.find(
|
||||
({ record }) => record.id === morphItem.recordId,
|
||||
);
|
||||
|
||||
return {
|
||||
...morphItem,
|
||||
isMatchingSearchFilter: isDefined(record),
|
||||
};
|
||||
});
|
||||
|
||||
const recordsWithObjectMetadataIdFilteredOnPickedRecordsWithoutDuplicates =
|
||||
recordsWithObjectMetadataIdFilteredOnPickedRecords.filter(
|
||||
({ record }) =>
|
||||
!updatedPickedMorphItems.some(
|
||||
({ recordId }) => recordId === record.id,
|
||||
),
|
||||
);
|
||||
|
||||
const recordsWithObjectMetadataIdExcludingPickedRecordsWithoutDuplicates =
|
||||
recordsWithObjectMetadataIdExcludingPickedRecords.filter(
|
||||
({ record }) =>
|
||||
!recordsWithObjectMetadataIdFilteredOnPickedRecords.some(
|
||||
({ record: recordFilteredOnPickedRecords }) =>
|
||||
recordFilteredOnPickedRecords.id === record.id,
|
||||
) &&
|
||||
!pickedMorphItems.some(({ recordId }) => recordId === record.id),
|
||||
);
|
||||
|
||||
const morphItems = [
|
||||
...updatedPickedMorphItems,
|
||||
...recordsWithObjectMetadataIdFilteredOnPickedRecordsWithoutDuplicates.map(
|
||||
({ record, objectMetadataItem }) => ({
|
||||
isMatchingSearchFilter: true,
|
||||
isSelected: true,
|
||||
objectMetadataId: objectMetadataItem.id,
|
||||
recordId: record.id,
|
||||
}),
|
||||
),
|
||||
...recordsWithObjectMetadataIdExcludingPickedRecordsWithoutDuplicates.map(
|
||||
({ record, objectMetadataItem }) => ({
|
||||
isMatchingSearchFilter: true,
|
||||
isSelected: false,
|
||||
objectMetadataId: objectMetadataItem.id,
|
||||
recordId: record.id,
|
||||
}),
|
||||
),
|
||||
];
|
||||
|
||||
set(
|
||||
multipleRecordPickerPickableMorphItemsComponentState.atomFamily({
|
||||
instanceId: multipleRecordPickerInstanceId,
|
||||
}),
|
||||
morphItems,
|
||||
);
|
||||
|
||||
[
|
||||
...recordsWithObjectMetadataIdFilteredOnPickedRecords,
|
||||
...recordsWithObjectMetadataIdExcludingPickedRecordsWithoutDuplicates,
|
||||
].forEach(({ record }) => {
|
||||
set(recordStoreFamilyState(record.id), record);
|
||||
});
|
||||
},
|
||||
[client],
|
||||
);
|
||||
|
||||
return { performSearch };
|
||||
};
|
||||
|
||||
const performSearchForPickedRecords = async ({
|
||||
client,
|
||||
searchFilter,
|
||||
searchableObjectMetadataItems,
|
||||
pickableMorphItems,
|
||||
}: {
|
||||
client: ApolloClient<object>;
|
||||
searchFilter: string;
|
||||
searchableObjectMetadataItems: ObjectMetadataItem[];
|
||||
pickableMorphItems: RecordPickerPickableMorphItem[];
|
||||
}) => {
|
||||
const pickedMorphItems = pickableMorphItems.filter(
|
||||
({ isSelected }) => isSelected,
|
||||
);
|
||||
|
||||
const filterPerMetadataItemFilteredOnPickedRecordId = Object.fromEntries(
|
||||
searchableObjectMetadataItems
|
||||
.map(({ id, nameSingular }) => {
|
||||
const pickedRecordIdsForMetadataItem = pickedMorphItems
|
||||
.filter(
|
||||
({ objectMetadataId, isSelected }) =>
|
||||
objectMetadataId === id && isSelected,
|
||||
)
|
||||
.map(({ recordId }) => recordId);
|
||||
|
||||
if (!isNonEmptyArray(pickedRecordIdsForMetadataItem)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return [
|
||||
`filter${capitalize(nameSingular)}`,
|
||||
{
|
||||
id: {
|
||||
in: pickedRecordIdsForMetadataItem,
|
||||
},
|
||||
},
|
||||
];
|
||||
})
|
||||
.filter(isDefined),
|
||||
);
|
||||
|
||||
const searchableObjectMetadataItemsFilteredOnPickedRecordId =
|
||||
searchableObjectMetadataItems.filter(({ nameSingular }) =>
|
||||
isDefined(
|
||||
filterPerMetadataItemFilteredOnPickedRecordId[
|
||||
`filter${capitalize(nameSingular)}`
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
if (!isNonEmptyArray(searchableObjectMetadataItemsFilteredOnPickedRecordId)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const combinedSearchRecordsQueryFilteredOnPickedRecords =
|
||||
generateCombinedSearchRecordsQuery({
|
||||
objectMetadataItems:
|
||||
searchableObjectMetadataItemsFilteredOnPickedRecordId,
|
||||
operationSignatures:
|
||||
searchableObjectMetadataItemsFilteredOnPickedRecordId.map(
|
||||
(objectMetadataItem) => ({
|
||||
objectNameSingular: objectMetadataItem.nameSingular,
|
||||
variables: {},
|
||||
}),
|
||||
),
|
||||
});
|
||||
|
||||
const limitPerMetadataItem = Object.fromEntries(
|
||||
searchableObjectMetadataItems
|
||||
.map(({ nameSingular }) => {
|
||||
return [`limit${capitalize(nameSingular)}`, 10];
|
||||
})
|
||||
.filter(isDefined),
|
||||
);
|
||||
|
||||
const { data: combinedSearchRecordFilteredOnPickedRecordsQueryResult } =
|
||||
await client.query<CombinedFindManyRecordsQueryResult>({
|
||||
query: combinedSearchRecordsQueryFilteredOnPickedRecords,
|
||||
variables: {
|
||||
search: searchFilter,
|
||||
...limitPerMetadataItem,
|
||||
...filterPerMetadataItemFilteredOnPickedRecordId,
|
||||
},
|
||||
});
|
||||
|
||||
const {
|
||||
recordsWithObjectMetadataId:
|
||||
recordsWithObjectMetadataIdFilteredOnPickedRecords,
|
||||
} = multipleRecordPickerformatQueryResultAsRecordsWithObjectMetadataId({
|
||||
objectMetadataItems: searchableObjectMetadataItems,
|
||||
searchQueryResult: combinedSearchRecordFilteredOnPickedRecordsQueryResult,
|
||||
});
|
||||
|
||||
return recordsWithObjectMetadataIdFilteredOnPickedRecords;
|
||||
};
|
||||
|
||||
const performSearchExcludingPickedRecords = async ({
|
||||
client,
|
||||
searchFilter,
|
||||
searchableObjectMetadataItems,
|
||||
pickableMorphItems,
|
||||
}: {
|
||||
client: ApolloClient<object>;
|
||||
searchFilter: string;
|
||||
searchableObjectMetadataItems: ObjectMetadataItem[];
|
||||
pickableMorphItems: RecordPickerPickableMorphItem[];
|
||||
}) => {
|
||||
if (searchableObjectMetadataItems.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const pickedMorphItems = pickableMorphItems.filter(
|
||||
({ isSelected }) => isSelected,
|
||||
);
|
||||
|
||||
const filterPerMetadataItemExcludingPickedRecordId = Object.fromEntries(
|
||||
searchableObjectMetadataItems
|
||||
.map(({ id, nameSingular }) => {
|
||||
const pickedRecordIdsForMetadataItem = pickedMorphItems
|
||||
.filter(
|
||||
({ objectMetadataId, isSelected }) =>
|
||||
objectMetadataId === id && isSelected,
|
||||
)
|
||||
.map(({ recordId }) => recordId);
|
||||
|
||||
if (!isNonEmptyArray(pickedRecordIdsForMetadataItem)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return [
|
||||
`filter${capitalize(nameSingular)}`,
|
||||
{
|
||||
not: {
|
||||
id: {
|
||||
in: pickedRecordIdsForMetadataItem,
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
})
|
||||
.filter(isDefined),
|
||||
);
|
||||
|
||||
const combinedSearchRecordsQueryExcludingPickedRecords =
|
||||
generateCombinedSearchRecordsQuery({
|
||||
objectMetadataItems: searchableObjectMetadataItems,
|
||||
operationSignatures: searchableObjectMetadataItems.map(
|
||||
(objectMetadataItem) => ({
|
||||
objectNameSingular: objectMetadataItem.nameSingular,
|
||||
variables: {},
|
||||
}),
|
||||
),
|
||||
});
|
||||
|
||||
const limitPerMetadataItem = Object.fromEntries(
|
||||
searchableObjectMetadataItems
|
||||
.map(({ nameSingular }) => {
|
||||
return [`limit${capitalize(nameSingular)}`, 10];
|
||||
})
|
||||
.filter(isDefined),
|
||||
);
|
||||
|
||||
const { data: combinedSearchRecordExcludingPickedRecordsQueryResult } =
|
||||
await client.query<CombinedFindManyRecordsQueryResult>({
|
||||
query: combinedSearchRecordsQueryExcludingPickedRecords,
|
||||
variables: {
|
||||
search: searchFilter,
|
||||
...limitPerMetadataItem,
|
||||
...filterPerMetadataItemExcludingPickedRecordId,
|
||||
},
|
||||
});
|
||||
|
||||
const {
|
||||
recordsWithObjectMetadataId:
|
||||
recordsWithObjectMetadataIdExcludingPickedRecords,
|
||||
} = multipleRecordPickerformatQueryResultAsRecordsWithObjectMetadataId({
|
||||
objectMetadataItems: searchableObjectMetadataItems,
|
||||
searchQueryResult: combinedSearchRecordExcludingPickedRecordsQueryResult,
|
||||
});
|
||||
|
||||
return recordsWithObjectMetadataIdExcludingPickedRecords;
|
||||
};
|
||||
@ -0,0 +1,7 @@
|
||||
import { ComponentStateKeyV2 } from '@/ui/utilities/state/component-state/types/ComponentStateKeyV2';
|
||||
import { createComponentInstanceContext } from '@/ui/utilities/state/component-state/utils/createComponentInstanceContext';
|
||||
|
||||
type MultipleRecordPickerComponentInstanceContextProps = ComponentStateKeyV2;
|
||||
|
||||
export const MultipleRecordPickerComponentInstanceContext =
|
||||
createComponentInstanceContext<MultipleRecordPickerComponentInstanceContextProps>();
|
||||
@ -0,0 +1,9 @@
|
||||
import { MultipleRecordPickerComponentInstanceContext } from '@/object-record/record-picker/multiple-record-picker/states/contexts/MultipleRecordPickerComponentInstanceContext';
|
||||
import { createComponentStateV2 } from '@/ui/utilities/state/component-state/utils/createComponentStateV2';
|
||||
|
||||
export const multipleRecordPickerIsLoadingComponentState =
|
||||
createComponentStateV2<boolean>({
|
||||
key: 'multipleRecordPickerIsLoadingComponentState',
|
||||
defaultValue: false,
|
||||
componentInstanceContext: MultipleRecordPickerComponentInstanceContext,
|
||||
});
|
||||
@ -0,0 +1,10 @@
|
||||
import { MultipleRecordPickerComponentInstanceContext } from '@/object-record/record-picker/multiple-record-picker/states/contexts/MultipleRecordPickerComponentInstanceContext';
|
||||
import { RecordPickerPickableMorphItem } from '@/object-record/record-picker/types/RecordPickerPickableMorphItem';
|
||||
import { createComponentStateV2 } from '@/ui/utilities/state/component-state/utils/createComponentStateV2';
|
||||
|
||||
export const multipleRecordPickerPickableMorphItemsComponentState =
|
||||
createComponentStateV2<RecordPickerPickableMorphItem[]>({
|
||||
key: 'multipleRecordPickerPickableMorphItemsComponentState',
|
||||
defaultValue: [],
|
||||
componentInstanceContext: MultipleRecordPickerComponentInstanceContext,
|
||||
});
|
||||
@ -0,0 +1,9 @@
|
||||
import { MultipleRecordPickerComponentInstanceContext } from '@/object-record/record-picker/multiple-record-picker/states/contexts/MultipleRecordPickerComponentInstanceContext';
|
||||
import { createComponentStateV2 } from '@/ui/utilities/state/component-state/utils/createComponentStateV2';
|
||||
|
||||
export const multipleRecordPickerSearchFilterComponentState =
|
||||
createComponentStateV2<string>({
|
||||
key: 'multipleRecordPickerSearchFilterComponentState',
|
||||
defaultValue: '',
|
||||
componentInstanceContext: MultipleRecordPickerComponentInstanceContext,
|
||||
});
|
||||
@ -0,0 +1,10 @@
|
||||
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||
import { MultipleRecordPickerComponentInstanceContext } from '@/object-record/record-picker/multiple-record-picker/states/contexts/MultipleRecordPickerComponentInstanceContext';
|
||||
import { createComponentStateV2 } from '@/ui/utilities/state/component-state/utils/createComponentStateV2';
|
||||
|
||||
export const multipleRecordPickerSearchableObjectMetadataItemsComponentState =
|
||||
createComponentStateV2<ObjectMetadataItem[]>({
|
||||
key: 'multipleRecordPickerSearchableObjectMetadataItemsComponentState',
|
||||
defaultValue: [],
|
||||
componentInstanceContext: MultipleRecordPickerComponentInstanceContext,
|
||||
});
|
||||
@ -0,0 +1,24 @@
|
||||
import { MultipleRecordPickerComponentInstanceContext } from '@/object-record/record-picker/multiple-record-picker/states/contexts/MultipleRecordPickerComponentInstanceContext';
|
||||
import { multipleRecordPickerPickableMorphItemsComponentState } from '@/object-record/record-picker/multiple-record-picker/states/multipleRecordPickerPickableMorphItemsComponentState';
|
||||
import { createComponentFamilySelectorV2 } from '@/ui/utilities/state/component-state/utils/createComponentFamilySelectorV2';
|
||||
|
||||
export const multipleRecordPickerIsSelectedComponentFamilySelector =
|
||||
createComponentFamilySelectorV2<boolean, string>({
|
||||
key: 'visibleRecordGroupIdsComponentFamilySelector',
|
||||
componentInstanceContext: MultipleRecordPickerComponentInstanceContext,
|
||||
get:
|
||||
({ instanceId, familyKey: recordId }) =>
|
||||
({ get }) => {
|
||||
const pickableMorphItems = get(
|
||||
multipleRecordPickerPickableMorphItemsComponentState.atomFamily({
|
||||
instanceId,
|
||||
}),
|
||||
);
|
||||
|
||||
const pickableMorphItem = pickableMorphItems.find(
|
||||
({ recordId: itemRecordId }) => itemRecordId === recordId,
|
||||
);
|
||||
|
||||
return pickableMorphItem?.isSelected ?? false;
|
||||
},
|
||||
});
|
||||
@ -0,0 +1,20 @@
|
||||
import { MultipleRecordPickerComponentInstanceContext } from '@/object-record/record-picker/multiple-record-picker/states/contexts/MultipleRecordPickerComponentInstanceContext';
|
||||
import { multipleRecordPickerPickableMorphItemsComponentState } from '@/object-record/record-picker/multiple-record-picker/states/multipleRecordPickerPickableMorphItemsComponentState';
|
||||
import { createComponentSelectorV2 } from '@/ui/utilities/state/component-state/utils/createComponentSelectorV2';
|
||||
|
||||
export const multipleRecordPickerPickableMorphItemsLengthComponentSelector =
|
||||
createComponentSelectorV2({
|
||||
key: 'multipleRecordPickerPickableMorphItemsLengthComponentSelector',
|
||||
componentInstanceContext: MultipleRecordPickerComponentInstanceContext,
|
||||
get:
|
||||
({ instanceId }) =>
|
||||
({ get }) => {
|
||||
const pickableMorphItems = get(
|
||||
multipleRecordPickerPickableMorphItemsComponentState.atomFamily({
|
||||
instanceId,
|
||||
}),
|
||||
);
|
||||
|
||||
return pickableMorphItems.length;
|
||||
},
|
||||
});
|
||||
@ -0,0 +1,22 @@
|
||||
import { MultipleRecordPickerComponentInstanceContext } from '@/object-record/record-picker/multiple-record-picker/states/contexts/MultipleRecordPickerComponentInstanceContext';
|
||||
import { multipleRecordPickerPickableMorphItemsComponentState } from '@/object-record/record-picker/multiple-record-picker/states/multipleRecordPickerPickableMorphItemsComponentState';
|
||||
import { createComponentSelectorV2 } from '@/ui/utilities/state/component-state/utils/createComponentSelectorV2';
|
||||
|
||||
export const multipleRecordPickerPickableRecordIdsMatchingSearchComponentSelector =
|
||||
createComponentSelectorV2({
|
||||
key: 'multipleRecordPickerPickableRecordIdsMatchingSearchComponentSelector',
|
||||
componentInstanceContext: MultipleRecordPickerComponentInstanceContext,
|
||||
get:
|
||||
({ instanceId }) =>
|
||||
({ get }) => {
|
||||
const pickableMorphItems = get(
|
||||
multipleRecordPickerPickableMorphItemsComponentState.atomFamily({
|
||||
instanceId,
|
||||
}),
|
||||
);
|
||||
|
||||
return pickableMorphItems
|
||||
.filter(({ isMatchingSearchFilter }) => isMatchingSearchFilter)
|
||||
.map(({ recordId }) => recordId);
|
||||
},
|
||||
});
|
||||
@ -0,0 +1,28 @@
|
||||
import { MultipleRecordPickerComponentInstanceContext } from '@/object-record/record-picker/multiple-record-picker/states/contexts/MultipleRecordPickerComponentInstanceContext';
|
||||
import { multipleRecordPickerPickableMorphItemsComponentState } from '@/object-record/record-picker/multiple-record-picker/states/multipleRecordPickerPickableMorphItemsComponentState';
|
||||
import { RecordPickerPickableMorphItem } from '@/object-record/record-picker/types/RecordPickerPickableMorphItem';
|
||||
import { createComponentFamilySelectorV2 } from '@/ui/utilities/state/component-state/utils/createComponentFamilySelectorV2';
|
||||
|
||||
export const multipleRecordPickerSinglePickableMorphItemComponentFamilySelector =
|
||||
createComponentFamilySelectorV2<
|
||||
RecordPickerPickableMorphItem | undefined,
|
||||
string
|
||||
>({
|
||||
key: 'multipleRecordPickerSinglePickableMorphItemComponentFamilySelector',
|
||||
componentInstanceContext: MultipleRecordPickerComponentInstanceContext,
|
||||
get:
|
||||
({ instanceId, familyKey: recordId }) =>
|
||||
({ get }) => {
|
||||
const pickableMorphItems = get(
|
||||
multipleRecordPickerPickableMorphItemsComponentState.atomFamily({
|
||||
instanceId,
|
||||
}),
|
||||
);
|
||||
|
||||
const pickableMorphItem = pickableMorphItems.find(
|
||||
({ recordId: itemRecordId }) => itemRecordId === recordId,
|
||||
);
|
||||
|
||||
return pickableMorphItem;
|
||||
},
|
||||
});
|
||||
@ -0,0 +1,3 @@
|
||||
export enum MultipleRecordPickerHotkeyScope {
|
||||
MultipleRecordPicker = 'multiple-record-picker',
|
||||
}
|
||||
@ -1,4 +1,4 @@
|
||||
import { SingleRecordPickerRecord } from '@/object-record/record-picker/types/SingleRecordPickerRecord';
|
||||
import { SingleRecordPickerRecord } from '@/object-record/record-picker/single-record-picker/types/SingleRecordPickerRecord';
|
||||
|
||||
export type MultipleRecordPickerRecords<
|
||||
CustomRecordForRecordPicker extends SingleRecordPickerRecord,
|
||||
@ -0,0 +1,5 @@
|
||||
export const getMultipleRecordPickerSelectableListId = (
|
||||
multipleRecordPickerComponentInstanceId: string,
|
||||
) => {
|
||||
return `${multipleRecordPickerComponentInstanceId}-selectable-list`;
|
||||
};
|
||||
@ -0,0 +1,16 @@
|
||||
import { CombinedFindManyRecordsQueryResult } from '@/object-record/multiple-objects/types/CombinedFindManyRecordsQueryResult';
|
||||
|
||||
export const multiRecordPickerFormatSearchResults = (
|
||||
searchResults: CombinedFindManyRecordsQueryResult | undefined | null,
|
||||
): CombinedFindManyRecordsQueryResult => {
|
||||
if (!searchResults) {
|
||||
return {};
|
||||
}
|
||||
|
||||
return Object.entries(searchResults).reduce((acc, [key, value]) => {
|
||||
let newKey = key.replace(/^search/, '');
|
||||
newKey = newKey.charAt(0).toLowerCase() + newKey.slice(1);
|
||||
acc[newKey] = value;
|
||||
return acc;
|
||||
}, {} as CombinedFindManyRecordsQueryResult);
|
||||
};
|
||||
@ -0,0 +1,35 @@
|
||||
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||
import { CombinedFindManyRecordsQueryResult } from '@/object-record/multiple-objects/types/CombinedFindManyRecordsQueryResult';
|
||||
import { multiRecordPickerFormatSearchResults } from '@/object-record/record-picker/multiple-record-picker/utils/multiRecordPickerFormatSearchResults';
|
||||
import { isDefined } from 'twenty-shared';
|
||||
|
||||
export const multipleRecordPickerformatQueryResultAsRecordsWithObjectMetadataId =
|
||||
({
|
||||
objectMetadataItems,
|
||||
searchQueryResult,
|
||||
}: {
|
||||
objectMetadataItems: ObjectMetadataItem[];
|
||||
searchQueryResult: CombinedFindManyRecordsQueryResult;
|
||||
}) => {
|
||||
const formattedSearchQueryResult =
|
||||
multiRecordPickerFormatSearchResults(searchQueryResult);
|
||||
|
||||
const recordsWithObjectMetadataId = Object.entries(
|
||||
formattedSearchQueryResult,
|
||||
).flatMap(([namePlural, objectRecordConnection]) => {
|
||||
const objectMetadataItem = objectMetadataItems.find(
|
||||
(objectMetadataItem) => objectMetadataItem.namePlural === namePlural,
|
||||
);
|
||||
|
||||
if (!isDefined(objectMetadataItem)) return [];
|
||||
|
||||
return objectRecordConnection.edges.map(({ node }) => ({
|
||||
objectMetadataItem,
|
||||
record: node,
|
||||
}));
|
||||
});
|
||||
|
||||
return {
|
||||
recordsWithObjectMetadataId,
|
||||
};
|
||||
};
|
||||
@ -3,12 +3,14 @@ import { useRef } from 'react';
|
||||
import {
|
||||
SingleRecordPickerMenuItemsWithSearch,
|
||||
SingleRecordPickerMenuItemsWithSearchProps,
|
||||
} from '@/object-record/record-picker/components/SingleRecordPickerMenuItemsWithSearch';
|
||||
import { RecordPickerComponentInstanceContext } from '@/object-record/record-picker/states/contexts/RecordPickerComponentInstanceContext';
|
||||
} from '@/object-record/record-picker/single-record-picker/components/SingleRecordPickerMenuItemsWithSearch';
|
||||
import { SingleRecordPickerComponentInstanceContext } from '@/object-record/record-picker/single-record-picker/states/contexts/SingleRecordPickerComponentInstanceContext';
|
||||
import { DropdownMenu } from '@/ui/layout/dropdown/components/DropdownMenu';
|
||||
import { useListenClickOutside } from '@/ui/utilities/pointer-event/hooks/useListenClickOutside';
|
||||
import { isDefined } from 'twenty-shared';
|
||||
|
||||
export const SINGLE_RECORD_PICKER_LISTENER_ID = 'single-record-select';
|
||||
|
||||
export type SingleRecordPickerProps = {
|
||||
width?: number;
|
||||
componentInstanceId: string;
|
||||
@ -22,9 +24,9 @@ export const SingleRecordPicker = ({
|
||||
onCreate,
|
||||
onRecordSelected,
|
||||
objectNameSingular,
|
||||
selectedRecordIds,
|
||||
width = 200,
|
||||
componentInstanceId,
|
||||
layoutDirection,
|
||||
}: SingleRecordPickerProps) => {
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
@ -41,11 +43,11 @@ export const SingleRecordPicker = ({
|
||||
onCancel();
|
||||
}
|
||||
},
|
||||
listenerId: 'single-record-select',
|
||||
listenerId: SINGLE_RECORD_PICKER_LISTENER_ID,
|
||||
});
|
||||
|
||||
return (
|
||||
<RecordPickerComponentInstanceContext.Provider
|
||||
<SingleRecordPickerComponentInstanceContext.Provider
|
||||
value={{ instanceId: componentInstanceId }}
|
||||
>
|
||||
<DropdownMenu ref={containerRef} width={width} data-select-disable>
|
||||
@ -58,10 +60,10 @@ export const SingleRecordPicker = ({
|
||||
onCreate,
|
||||
onRecordSelected,
|
||||
objectNameSingular,
|
||||
selectedRecordIds,
|
||||
layoutDirection,
|
||||
}}
|
||||
/>
|
||||
</DropdownMenu>
|
||||
</RecordPickerComponentInstanceContext.Provider>
|
||||
</SingleRecordPickerComponentInstanceContext.Provider>
|
||||
);
|
||||
};
|
||||
@ -2,10 +2,12 @@ import styled from '@emotion/styled';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import { Avatar, MenuItemSelectAvatar } from 'twenty-ui';
|
||||
|
||||
import { RECORD_PICKER_SELECTABLE_LIST_COMPONENT_INSTANCE_ID } from '@/object-record/record-picker/constants/RecordPickerSelectableListComponentInstanceId';
|
||||
import { SingleRecordPickerRecord } from '@/object-record/record-picker/types/SingleRecordPickerRecord';
|
||||
import { SingleRecordPickerComponentInstanceContext } from '@/object-record/record-picker/single-record-picker/states/contexts/SingleRecordPickerComponentInstanceContext';
|
||||
import { SingleRecordPickerRecord } from '@/object-record/record-picker/single-record-picker/types/SingleRecordPickerRecord';
|
||||
import { getSingleRecordPickerSelectableListId } from '@/object-record/record-picker/single-record-picker/utils/getSingleRecordPickerSelectableListId';
|
||||
import { SelectableItem } from '@/ui/layout/selectable-list/components/SelectableItem';
|
||||
import { useSelectableList } from '@/ui/layout/selectable-list/hooks/useSelectableList';
|
||||
import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
|
||||
|
||||
type SingleRecordPickerMenuItemProps = {
|
||||
record: SingleRecordPickerRecord;
|
||||
@ -22,11 +24,20 @@ export const SingleRecordPickerMenuItem = ({
|
||||
onRecordSelected,
|
||||
selectedRecord,
|
||||
}: SingleRecordPickerMenuItemProps) => {
|
||||
const recordPickerComponentInstanceId =
|
||||
useAvailableComponentInstanceIdOrThrow(
|
||||
SingleRecordPickerComponentInstanceContext,
|
||||
);
|
||||
|
||||
const selectableListComponentInstanceId =
|
||||
getSingleRecordPickerSelectableListId(recordPickerComponentInstanceId);
|
||||
|
||||
const { isSelectedItemIdSelector } = useSelectableList(
|
||||
RECORD_PICKER_SELECTABLE_LIST_COMPONENT_INSTANCE_ID,
|
||||
selectableListComponentInstanceId,
|
||||
);
|
||||
|
||||
const isSelectedItemId = useRecoilValue(isSelectedItemIdSelector(record.id));
|
||||
|
||||
return (
|
||||
<StyledSelectableItem itemId={record.id} key={record.id}>
|
||||
<MenuItemSelectAvatar
|
||||
@ -1,4 +1,4 @@
|
||||
import { isNonEmptyString } from '@sniptt/guards';
|
||||
import { isNonEmptyString, isUndefined } from '@sniptt/guards';
|
||||
import { useRef } from 'react';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import { Key } from 'ts-key-enum';
|
||||
@ -11,10 +11,15 @@ import { useSelectableList } from '@/ui/layout/selectable-list/hooks/useSelectab
|
||||
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
|
||||
import { isDefined } from 'twenty-shared';
|
||||
|
||||
import { SingleRecordPickerMenuItem } from '@/object-record/record-picker/components/SingleRecordPickerMenuItem';
|
||||
import { RECORD_PICKER_SELECTABLE_LIST_COMPONENT_INSTANCE_ID } from '@/object-record/record-picker/constants/RecordPickerSelectableListComponentInstanceId';
|
||||
import { RecordPickerHotkeyScope } from '@/object-record/record-picker/types/RecordPickerHotkeyScope';
|
||||
import { SingleRecordPickerRecord } from '@/object-record/record-picker/types/SingleRecordPickerRecord';
|
||||
import { SingleRecordPickerMenuItem } from '@/object-record/record-picker/single-record-picker/components/SingleRecordPickerMenuItem';
|
||||
import { SingleRecordPickerComponentInstanceContext } from '@/object-record/record-picker/single-record-picker/states/contexts/SingleRecordPickerComponentInstanceContext';
|
||||
import { singleRecordPickerSelectedIdComponentState } from '@/object-record/record-picker/single-record-picker/states/singleRecordPickerSelectedIdComponentState';
|
||||
import { SingleRecordPickerHotkeyScope } from '@/object-record/record-picker/single-record-picker/types/SingleRecordPickerHotkeyScope';
|
||||
import { SingleRecordPickerRecord } from '@/object-record/record-picker/single-record-picker/types/SingleRecordPickerRecord';
|
||||
import { getSingleRecordPickerSelectableListId } from '@/object-record/record-picker/single-record-picker/utils/getSingleRecordPickerSelectableListId';
|
||||
import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
|
||||
import { useRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentStateV2';
|
||||
import styled from '@emotion/styled';
|
||||
|
||||
export type SingleRecordPickerMenuItemsProps = {
|
||||
EmptyIcon?: IconComponent;
|
||||
@ -26,9 +31,12 @@ export type SingleRecordPickerMenuItemsProps = {
|
||||
selectedRecord?: SingleRecordPickerRecord;
|
||||
hotkeyScope?: string;
|
||||
isFiltered: boolean;
|
||||
shouldSelectEmptyOption?: boolean;
|
||||
};
|
||||
|
||||
const StyledContainer = styled.div`
|
||||
display: flex;
|
||||
`;
|
||||
|
||||
export const SingleRecordPickerMenuItems = ({
|
||||
EmptyIcon,
|
||||
emptyLabel,
|
||||
@ -37,9 +45,8 @@ export const SingleRecordPickerMenuItems = ({
|
||||
onCancel,
|
||||
onRecordSelected,
|
||||
selectedRecord,
|
||||
hotkeyScope = RecordPickerHotkeyScope.RecordPicker,
|
||||
hotkeyScope = SingleRecordPickerHotkeyScope.SingleRecordPicker,
|
||||
isFiltered,
|
||||
shouldSelectEmptyOption,
|
||||
}: SingleRecordPickerMenuItemsProps) => {
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
@ -60,8 +67,16 @@ export const SingleRecordPickerMenuItems = ({
|
||||
isDefined(entity) && isNonEmptyString(entity.name),
|
||||
);
|
||||
|
||||
const recordPickerComponentInstanceId =
|
||||
useAvailableComponentInstanceIdOrThrow(
|
||||
SingleRecordPickerComponentInstanceContext,
|
||||
);
|
||||
|
||||
const selectableListComponentInstanceId =
|
||||
getSingleRecordPickerSelectableListId(recordPickerComponentInstanceId);
|
||||
|
||||
const { isSelectedItemIdSelector, resetSelectedItem } = useSelectableList(
|
||||
RECORD_PICKER_SELECTABLE_LIST_COMPONENT_INSTANCE_ID,
|
||||
selectableListComponentInstanceId,
|
||||
);
|
||||
|
||||
const isSelectedSelectNoneButton = useRecoilValue(
|
||||
@ -79,17 +94,21 @@ export const SingleRecordPickerMenuItems = ({
|
||||
);
|
||||
|
||||
const selectableItemIds = recordsInDropdown.map((entity) => entity.id);
|
||||
const [selectedRecordId, setSelectedRecordId] = useRecoilComponentStateV2(
|
||||
singleRecordPickerSelectedIdComponentState,
|
||||
);
|
||||
|
||||
return (
|
||||
<div ref={containerRef}>
|
||||
<StyledContainer ref={containerRef}>
|
||||
<SelectableList
|
||||
selectableListId={RECORD_PICKER_SELECTABLE_LIST_COMPONENT_INSTANCE_ID}
|
||||
selectableListId={selectableListComponentInstanceId}
|
||||
selectableItemIdArray={selectableItemIds}
|
||||
hotkeyScope={hotkeyScope}
|
||||
onEnter={(itemId) => {
|
||||
const recordIndex = recordsInDropdown.findIndex(
|
||||
(record) => record.id === itemId,
|
||||
);
|
||||
setSelectedRecordId(itemId);
|
||||
onRecordSelected(recordsInDropdown[recordIndex]);
|
||||
resetSelectedItem();
|
||||
}}
|
||||
@ -107,10 +126,13 @@ export const SingleRecordPickerMenuItems = ({
|
||||
emptyLabel && (
|
||||
<MenuItemSelect
|
||||
key={record.id}
|
||||
onClick={() => onRecordSelected()}
|
||||
onClick={() => {
|
||||
setSelectedRecordId(undefined);
|
||||
onRecordSelected();
|
||||
}}
|
||||
LeftIcon={EmptyIcon}
|
||||
text={emptyLabel}
|
||||
selected={shouldSelectEmptyOption === true}
|
||||
selected={isUndefined(selectedRecordId)}
|
||||
hovered={isSelectedSelectNoneButton}
|
||||
/>
|
||||
)
|
||||
@ -131,6 +153,6 @@ export const SingleRecordPickerMenuItems = ({
|
||||
)}
|
||||
</DropdownMenuItemsContainer>
|
||||
</SelectableList>
|
||||
</div>
|
||||
</StyledContainer>
|
||||
);
|
||||
};
|
||||
@ -1,11 +1,12 @@
|
||||
import {
|
||||
SingleRecordPickerMenuItems,
|
||||
SingleRecordPickerMenuItemsProps,
|
||||
} from '@/object-record/record-picker/components/SingleRecordPickerMenuItems';
|
||||
import { useRecordPickerRecordsOptions } from '@/object-record/record-picker/hooks/useRecordPickerRecordsOptions';
|
||||
import { useRecordSelectSearch } from '@/object-record/record-picker/hooks/useRecordSelectSearch';
|
||||
import { RecordPickerComponentInstanceContext } from '@/object-record/record-picker/states/contexts/RecordPickerComponentInstanceContext';
|
||||
import { recordPickerSearchFilterComponentState } from '@/object-record/record-picker/states/recordPickerSearchFilterComponentState';
|
||||
} from '@/object-record/record-picker/single-record-picker/components/SingleRecordPickerMenuItems';
|
||||
import { useSingleRecordPickerRecords } from '@/object-record/record-picker/single-record-picker/hooks/useSingleRecordPickerRecords';
|
||||
import { useSingleRecordPickerSearch } from '@/object-record/record-picker/single-record-picker/hooks/useSingleRecordPickerSearch';
|
||||
import { SingleRecordPickerComponentInstanceContext } from '@/object-record/record-picker/single-record-picker/states/contexts/SingleRecordPickerComponentInstanceContext';
|
||||
import { singleRecordPickerSearchFilterComponentState } from '@/object-record/record-picker/single-record-picker/states/singleRecordPickerSearchFilterComponentState';
|
||||
import { RecordPickerLayoutDirection } from '@/object-record/record-picker/types/RecordPickerLayoutDirection';
|
||||
import { useHasObjectReadOnlyPermission } from '@/settings/roles/hooks/useHasObjectReadOnlyPermission';
|
||||
import { CreateNewButton } from '@/ui/input/relation-picker/components/CreateNewButton';
|
||||
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
|
||||
@ -13,18 +14,15 @@ import { DropdownMenuSearchInput } from '@/ui/layout/dropdown/components/Dropdow
|
||||
import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownMenuSeparator';
|
||||
import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
|
||||
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
|
||||
import { Placement } from '@floating-ui/react';
|
||||
import { isDefined } from 'twenty-shared';
|
||||
import { IconPlus } from 'twenty-ui';
|
||||
import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull';
|
||||
|
||||
export type SingleRecordPickerMenuItemsWithSearchProps = {
|
||||
excludedRecordIds?: string[];
|
||||
onCreate?: ((searchInput?: string) => void) | (() => void);
|
||||
objectNameSingular: string;
|
||||
recordPickerInstanceId?: string;
|
||||
selectedRecordIds: string[];
|
||||
dropdownPlacement?: Placement | null;
|
||||
layoutDirection?: RecordPickerLayoutDirection;
|
||||
} & Pick<
|
||||
SingleRecordPickerMenuItemsProps,
|
||||
| 'EmptyIcon'
|
||||
@ -42,25 +40,23 @@ export const SingleRecordPickerMenuItemsWithSearch = ({
|
||||
onCreate,
|
||||
onRecordSelected,
|
||||
objectNameSingular,
|
||||
selectedRecordIds,
|
||||
dropdownPlacement,
|
||||
layoutDirection = 'search-bar-on-top',
|
||||
}: SingleRecordPickerMenuItemsWithSearchProps) => {
|
||||
const { handleSearchFilterChange } = useRecordSelectSearch();
|
||||
const { handleSearchFilterChange } = useSingleRecordPickerSearch();
|
||||
|
||||
const recordPickerInstanceId = useAvailableComponentInstanceIdOrThrow(
|
||||
RecordPickerComponentInstanceContext,
|
||||
SingleRecordPickerComponentInstanceContext,
|
||||
);
|
||||
|
||||
const hasObjectReadOnlyPermission = useHasObjectReadOnlyPermission();
|
||||
|
||||
const recordPickerSearchFilter = useRecoilComponentValueV2(
|
||||
recordPickerSearchFilterComponentState,
|
||||
singleRecordPickerSearchFilterComponentState,
|
||||
recordPickerInstanceId,
|
||||
);
|
||||
|
||||
const { records } = useRecordPickerRecordsOptions({
|
||||
const { records } = useSingleRecordPickerRecords({
|
||||
objectNameSingular,
|
||||
selectedRecordIds,
|
||||
excludedRecordIds,
|
||||
});
|
||||
|
||||
@ -72,12 +68,9 @@ export const SingleRecordPickerMenuItemsWithSearch = ({
|
||||
/>
|
||||
);
|
||||
|
||||
const shouldDisplayDropdownMenuItems =
|
||||
records.recordsToSelect.length + records.selectedRecords?.length > 0;
|
||||
|
||||
return (
|
||||
<>
|
||||
{dropdownPlacement?.includes('end') && (
|
||||
{layoutDirection === 'search-bar-on-bottom' && (
|
||||
<>
|
||||
{isDefined(onCreate) && !hasObjectReadOnlyPermission && (
|
||||
<DropdownMenuItemsContainer scrollable={false}>
|
||||
@ -85,22 +78,18 @@ export const SingleRecordPickerMenuItemsWithSearch = ({
|
||||
</DropdownMenuItemsContainer>
|
||||
)}
|
||||
{records.recordsToSelect.length > 0 && <DropdownMenuSeparator />}
|
||||
{shouldDisplayDropdownMenuItems && (
|
||||
<SingleRecordPickerMenuItems
|
||||
recordsToSelect={records.recordsToSelect}
|
||||
loading={records.loading}
|
||||
selectedRecord={records.selectedRecords?.[0]}
|
||||
shouldSelectEmptyOption={selectedRecordIds?.length === 0}
|
||||
hotkeyScope={recordPickerInstanceId}
|
||||
isFiltered={!!recordPickerSearchFilter}
|
||||
{...{
|
||||
EmptyIcon,
|
||||
emptyLabel,
|
||||
onCancel,
|
||||
onRecordSelected,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<SingleRecordPickerMenuItems
|
||||
recordsToSelect={records.recordsToSelect}
|
||||
loading={records.loading}
|
||||
selectedRecord={records.selectedRecords?.[0]}
|
||||
isFiltered={!!recordPickerSearchFilter}
|
||||
{...{
|
||||
EmptyIcon,
|
||||
emptyLabel,
|
||||
onCancel,
|
||||
onRecordSelected,
|
||||
}}
|
||||
/>
|
||||
<DropdownMenuSeparator />
|
||||
</>
|
||||
)}
|
||||
@ -109,26 +98,21 @@ export const SingleRecordPickerMenuItemsWithSearch = ({
|
||||
autoFocus
|
||||
role="combobox"
|
||||
/>
|
||||
{(dropdownPlacement?.includes('start') ||
|
||||
isUndefinedOrNull(dropdownPlacement)) && (
|
||||
{layoutDirection === 'search-bar-on-top' && (
|
||||
<>
|
||||
<DropdownMenuSeparator />
|
||||
{shouldDisplayDropdownMenuItems && (
|
||||
<SingleRecordPickerMenuItems
|
||||
recordsToSelect={records.recordsToSelect}
|
||||
loading={records.loading}
|
||||
selectedRecord={records.selectedRecords?.[0]}
|
||||
shouldSelectEmptyOption={selectedRecordIds?.length === 0}
|
||||
hotkeyScope={recordPickerInstanceId}
|
||||
isFiltered={!!recordPickerSearchFilter}
|
||||
{...{
|
||||
EmptyIcon,
|
||||
emptyLabel,
|
||||
onCancel,
|
||||
onRecordSelected,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<SingleRecordPickerMenuItems
|
||||
recordsToSelect={records.recordsToSelect}
|
||||
loading={records.loading}
|
||||
selectedRecord={records.selectedRecords?.[0]}
|
||||
isFiltered={!!recordPickerSearchFilter}
|
||||
{...{
|
||||
EmptyIcon,
|
||||
emptyLabel,
|
||||
onCancel,
|
||||
onRecordSelected,
|
||||
}}
|
||||
/>
|
||||
{records.recordsToSelect.length > 0 && isDefined(onCreate) && (
|
||||
<DropdownMenuSeparator />
|
||||
)}
|
||||
@ -10,8 +10,8 @@ import { graphqlMocks } from '~/testing/graphqlMocks';
|
||||
import { allMockPersonRecords } from '~/testing/mock-data/people';
|
||||
import { sleep } from '~/utils/sleep';
|
||||
|
||||
import { SingleRecordPicker } from '@/object-record/record-picker/components/SingleRecordPicker';
|
||||
import { SingleRecordPickerRecord } from '../../types/SingleRecordPickerRecord';
|
||||
import { SingleRecordPicker } from '@/object-record/record-picker/single-record-picker/components/SingleRecordPicker';
|
||||
import { SingleRecordPickerRecord } from '@/object-record/record-picker/single-record-picker/types/SingleRecordPickerRecord';
|
||||
|
||||
const records = allMockPersonRecords.map<SingleRecordPickerRecord>(
|
||||
(person) => ({
|
||||
@ -34,7 +34,6 @@ const meta: Meta<typeof SingleRecordPicker> = {
|
||||
],
|
||||
args: {
|
||||
objectNameSingular: CoreObjectNameSingular.WorkspaceMember,
|
||||
selectedRecordIds: [],
|
||||
componentInstanceId: 'single-record-picker',
|
||||
},
|
||||
argTypes: {
|
||||
@ -0,0 +1,2 @@
|
||||
export const SINGLE_RECORD_PICKER_CLICK_OUTSIDE_LISTENER_ID =
|
||||
'single-record-picker-click-outside-listener';
|
||||
@ -2,25 +2,25 @@ import { act, renderHook } from '@testing-library/react';
|
||||
import { ChangeEvent } from 'react';
|
||||
import { RecoilRoot } from 'recoil';
|
||||
|
||||
import { useRecordSelectSearch } from '@/object-record/record-picker/hooks/useRecordSelectSearch';
|
||||
import { RecordPickerComponentInstanceContext } from '@/object-record/record-picker/states/contexts/RecordPickerComponentInstanceContext';
|
||||
import { recordPickerSearchFilterComponentState } from '@/object-record/record-picker/states/recordPickerSearchFilterComponentState';
|
||||
import { useSingleRecordPickerSearch } from '@/object-record/record-picker/single-record-picker/hooks/useSingleRecordPickerSearch';
|
||||
import { SingleRecordPickerComponentInstanceContext } from '@/object-record/record-picker/single-record-picker/states/contexts/SingleRecordPickerComponentInstanceContext';
|
||||
import { singleRecordPickerSearchFilterComponentState } from '@/object-record/record-picker/single-record-picker/states/singleRecordPickerSearchFilterComponentState';
|
||||
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
|
||||
|
||||
const instanceId = 'instanceId';
|
||||
const Wrapper = ({ children }: { children: React.ReactNode }) => (
|
||||
<RecordPickerComponentInstanceContext.Provider value={{ instanceId }}>
|
||||
<SingleRecordPickerComponentInstanceContext.Provider value={{ instanceId }}>
|
||||
<RecoilRoot>{children}</RecoilRoot>
|
||||
</RecordPickerComponentInstanceContext.Provider>
|
||||
</SingleRecordPickerComponentInstanceContext.Provider>
|
||||
);
|
||||
|
||||
describe('useRecordSelectSearch', () => {
|
||||
describe('useSingleRecordPickerRecords', () => {
|
||||
it('should update searchFilter after change event', async () => {
|
||||
const { result } = renderHook(
|
||||
() => {
|
||||
const recordSelectSearchHook = useRecordSelectSearch(instanceId);
|
||||
const recordSelectSearchHook = useSingleRecordPickerSearch(instanceId);
|
||||
const internallyStoredFilter = useRecoilComponentValueV2(
|
||||
recordPickerSearchFilterComponentState,
|
||||
singleRecordPickerSearchFilterComponentState,
|
||||
instanceId,
|
||||
);
|
||||
return { recordSelectSearchHook, internallyStoredFilter };
|
||||
@ -0,0 +1,29 @@
|
||||
import { singleRecordPickerSearchFilterComponentState } from '@/object-record/record-picker/single-record-picker/states/singleRecordPickerSearchFilterComponentState';
|
||||
import { singleRecordPickerSelectedIdComponentState } from '@/object-record/record-picker/single-record-picker/states/singleRecordPickerSelectedIdComponentState';
|
||||
import { useFilteredSearchRecordQuery } from '@/search/hooks/useFilteredSearchRecordQuery';
|
||||
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
|
||||
|
||||
export const useSingleRecordPickerRecords = ({
|
||||
objectNameSingular,
|
||||
excludedRecordIds = [],
|
||||
}: {
|
||||
objectNameSingular: string;
|
||||
excludedRecordIds?: string[];
|
||||
}) => {
|
||||
const recordPickerSearchFilter = useRecoilComponentValueV2(
|
||||
singleRecordPickerSearchFilterComponentState,
|
||||
);
|
||||
|
||||
const selectedRecordId = useRecoilComponentValueV2(
|
||||
singleRecordPickerSelectedIdComponentState,
|
||||
);
|
||||
|
||||
const records = useFilteredSearchRecordQuery({
|
||||
searchFilter: recordPickerSearchFilter,
|
||||
selectedIds: selectedRecordId ? [selectedRecordId] : [],
|
||||
excludedRecordIds: excludedRecordIds,
|
||||
objectNameSingular,
|
||||
});
|
||||
|
||||
return { records };
|
||||
};
|
||||
@ -1,26 +1,26 @@
|
||||
import { RecordPickerComponentInstanceContext } from '@/object-record/record-picker/states/contexts/RecordPickerComponentInstanceContext';
|
||||
import { recordPickerPreselectedIdComponentState } from '@/object-record/record-picker/states/recordPickerPreselectedIdComponentState';
|
||||
import { recordPickerSearchFilterComponentState } from '@/object-record/record-picker/states/recordPickerSearchFilterComponentState';
|
||||
import { SingleRecordPickerComponentInstanceContext } from '@/object-record/record-picker/single-record-picker/states/contexts/SingleRecordPickerComponentInstanceContext';
|
||||
import { singleRecordPickerSearchFilterComponentState } from '@/object-record/record-picker/single-record-picker/states/singleRecordPickerSearchFilterComponentState';
|
||||
import { singleRecordPickerSelectedIdComponentState } from '@/object-record/record-picker/single-record-picker/states/singleRecordPickerSelectedIdComponentState';
|
||||
import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
|
||||
import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentStateV2';
|
||||
import { useDebouncedCallback } from 'use-debounce';
|
||||
|
||||
export const useRecordSelectSearch = (
|
||||
export const useSingleRecordPickerSearch = (
|
||||
recordPickerComponentInstanceIdFromProps?: string,
|
||||
) => {
|
||||
const recordPickerComponentInstanceId =
|
||||
useAvailableComponentInstanceIdOrThrow(
|
||||
RecordPickerComponentInstanceContext,
|
||||
SingleRecordPickerComponentInstanceContext,
|
||||
recordPickerComponentInstanceIdFromProps,
|
||||
);
|
||||
|
||||
const setRecordPickerSearchFilter = useSetRecoilComponentStateV2(
|
||||
recordPickerSearchFilterComponentState,
|
||||
singleRecordPickerSearchFilterComponentState,
|
||||
recordPickerComponentInstanceId,
|
||||
);
|
||||
|
||||
const setRecordPickerPreselectedId = useSetRecoilComponentStateV2(
|
||||
recordPickerPreselectedIdComponentState,
|
||||
const setRecordPickerSelectedId = useSetRecoilComponentStateV2(
|
||||
singleRecordPickerSelectedIdComponentState,
|
||||
recordPickerComponentInstanceId,
|
||||
);
|
||||
const debouncedSetSearchFilter = useDebouncedCallback(
|
||||
@ -33,14 +33,14 @@ export const useRecordSelectSearch = (
|
||||
|
||||
const resetSearchFilter = () => {
|
||||
debouncedSetSearchFilter('');
|
||||
setRecordPickerPreselectedId('');
|
||||
setRecordPickerSelectedId(undefined);
|
||||
};
|
||||
|
||||
const handleSearchFilterChange = (
|
||||
event: React.ChangeEvent<HTMLInputElement>,
|
||||
) => {
|
||||
debouncedSetSearchFilter(event.currentTarget.value);
|
||||
setRecordPickerPreselectedId('');
|
||||
setRecordPickerSelectedId(undefined);
|
||||
};
|
||||
|
||||
return {
|
||||
@ -1,4 +1,4 @@
|
||||
import { createComponentInstanceContext } from '@/ui/utilities/state/component-state/utils/createComponentInstanceContext';
|
||||
|
||||
export const RecordPickerComponentInstanceContext =
|
||||
export const SingleRecordPickerComponentInstanceContext =
|
||||
createComponentInstanceContext();
|
||||
@ -0,0 +1,9 @@
|
||||
import { SingleRecordPickerComponentInstanceContext } from '@/object-record/record-picker/single-record-picker/states/contexts/SingleRecordPickerComponentInstanceContext';
|
||||
import { createComponentStateV2 } from '@/ui/utilities/state/component-state/utils/createComponentStateV2';
|
||||
|
||||
export const singleRecordPickerSearchFilterComponentState =
|
||||
createComponentStateV2<string>({
|
||||
key: 'singleRecordPickerSearchFilterComponentState',
|
||||
defaultValue: '',
|
||||
componentInstanceContext: SingleRecordPickerComponentInstanceContext,
|
||||
});
|
||||
@ -0,0 +1,10 @@
|
||||
import { SingleRecordPickerComponentInstanceContext } from '@/object-record/record-picker/single-record-picker/states/contexts/SingleRecordPickerComponentInstanceContext';
|
||||
import { RecordPickerSearchQuery } from '@/object-record/record-picker/single-record-picker/types/SingleRecordPickerSearchQuery';
|
||||
import { createComponentStateV2 } from '@/ui/utilities/state/component-state/utils/createComponentStateV2';
|
||||
|
||||
export const singleRecordPickerSearchQueryComponentState =
|
||||
createComponentStateV2<RecordPickerSearchQuery | null>({
|
||||
key: 'singleRecordPickerSearchQueryComponentState',
|
||||
defaultValue: null,
|
||||
componentInstanceContext: SingleRecordPickerComponentInstanceContext,
|
||||
});
|
||||
@ -0,0 +1,9 @@
|
||||
import { SingleRecordPickerComponentInstanceContext } from '@/object-record/record-picker/single-record-picker/states/contexts/SingleRecordPickerComponentInstanceContext';
|
||||
import { createComponentStateV2 } from '@/ui/utilities/state/component-state/utils/createComponentStateV2';
|
||||
|
||||
export const singleRecordPickerSelectedIdComponentState =
|
||||
createComponentStateV2<string | undefined>({
|
||||
key: 'singleRecordPickerSelectedIdComponentState',
|
||||
defaultValue: undefined,
|
||||
componentInstanceContext: SingleRecordPickerComponentInstanceContext,
|
||||
});
|
||||
@ -0,0 +1,3 @@
|
||||
export enum SingleRecordPickerHotkeyScope {
|
||||
SingleRecordPicker = 'single-record-picker',
|
||||
}
|
||||
@ -0,0 +1,5 @@
|
||||
export const getSingleRecordPickerSelectableListId = (
|
||||
singleRecordPickerComponentInstanceId: string,
|
||||
) => {
|
||||
return `${singleRecordPickerComponentInstanceId}-selectable-list`;
|
||||
};
|
||||
@ -1,10 +0,0 @@
|
||||
import { RecordPickerComponentInstanceContext } from '@/object-record/record-picker/states/contexts/RecordPickerComponentInstanceContext';
|
||||
import { createComponentStateV2 } from '@/ui/utilities/state/component-state/utils/createComponentStateV2';
|
||||
|
||||
export const recordPickerPreselectedIdComponentState = createComponentStateV2<
|
||||
string | undefined
|
||||
>({
|
||||
key: 'recordPickerPreselectedIdComponentState',
|
||||
defaultValue: undefined,
|
||||
componentInstanceContext: RecordPickerComponentInstanceContext,
|
||||
});
|
||||
@ -1,9 +0,0 @@
|
||||
import { RecordPickerComponentInstanceContext } from '@/object-record/record-picker/states/contexts/RecordPickerComponentInstanceContext';
|
||||
import { createComponentStateV2 } from '@/ui/utilities/state/component-state/utils/createComponentStateV2';
|
||||
|
||||
export const recordPickerSearchFilterComponentState =
|
||||
createComponentStateV2<string>({
|
||||
key: 'recordPickerSearchFilterComponentState',
|
||||
defaultValue: '',
|
||||
componentInstanceContext: RecordPickerComponentInstanceContext,
|
||||
});
|
||||
@ -1,10 +0,0 @@
|
||||
import { RecordPickerComponentInstanceContext } from '@/object-record/record-picker/states/contexts/RecordPickerComponentInstanceContext';
|
||||
import { RecordPickerSearchQuery } from '@/object-record/record-picker/types/RecordPickerSearchQuery';
|
||||
import { createComponentStateV2 } from '@/ui/utilities/state/component-state/utils/createComponentStateV2';
|
||||
|
||||
export const recordPickerSearchQueryComponentState =
|
||||
createComponentStateV2<RecordPickerSearchQuery | null>({
|
||||
key: 'recordPickerSearchQueryComponentState',
|
||||
defaultValue: null,
|
||||
componentInstanceContext: RecordPickerComponentInstanceContext,
|
||||
});
|
||||
@ -1,3 +0,0 @@
|
||||
export enum RecordPickerHotkeyScope {
|
||||
RecordPicker = 'record-picker',
|
||||
}
|
||||
@ -0,0 +1,3 @@
|
||||
export type RecordPickerLayoutDirection =
|
||||
| 'search-bar-on-top'
|
||||
| 'search-bar-on-bottom';
|
||||
@ -0,0 +1,6 @@
|
||||
export type RecordPickerPickableMorphItem = {
|
||||
recordId: string;
|
||||
objectMetadataId: string;
|
||||
isSelected: boolean;
|
||||
isMatchingSearchFilter: boolean;
|
||||
};
|
||||
@ -8,15 +8,18 @@ import { useUpdateOneRecord } from '@/object-record/hooks/useUpdateOneRecord';
|
||||
import { FieldContext } from '@/object-record/record-field/contexts/FieldContext';
|
||||
import { useIsFieldValueReadOnly } from '@/object-record/record-field/hooks/useIsFieldValueReadOnly';
|
||||
import { usePersistField } from '@/object-record/record-field/hooks/usePersistField';
|
||||
import { RelationFromManyFieldInputMultiRecordsEffect } from '@/object-record/record-field/meta-types/input/components/RelationFromManyFieldInputMultiRecordsEffect';
|
||||
import { useAddNewRecordAndOpenRightDrawer } from '@/object-record/record-field/meta-types/input/hooks/useAddNewRecordAndOpenRightDrawer';
|
||||
import { useUpdateRelationFromManyFieldInput } from '@/object-record/record-field/meta-types/input/hooks/useUpdateRelationFromManyFieldInput';
|
||||
import { FieldRelationMetadata } from '@/object-record/record-field/types/FieldMetadata';
|
||||
import { MultipleRecordPicker } from '@/object-record/record-picker/components/MultipleRecordPicker';
|
||||
import { SingleRecordPickerMenuItemsWithSearch } from '@/object-record/record-picker/components/SingleRecordPickerMenuItemsWithSearch';
|
||||
import { RecordPickerComponentInstanceContext } from '@/object-record/record-picker/states/contexts/RecordPickerComponentInstanceContext';
|
||||
import { recordPickerSearchFilterComponentState } from '@/object-record/record-picker/states/recordPickerSearchFilterComponentState';
|
||||
import { SingleRecordPickerRecord } from '@/object-record/record-picker/types/SingleRecordPickerRecord';
|
||||
import { MultipleRecordPicker } from '@/object-record/record-picker/multiple-record-picker/components/MultipleRecordPicker';
|
||||
import { useMultipleRecordPickerPerformSearch } from '@/object-record/record-picker/multiple-record-picker/hooks/useMultipleRecordPickerPerformSearch';
|
||||
import { multipleRecordPickerPickableMorphItemsComponentState } from '@/object-record/record-picker/multiple-record-picker/states/multipleRecordPickerPickableMorphItemsComponentState';
|
||||
import { multipleRecordPickerSearchFilterComponentState } from '@/object-record/record-picker/multiple-record-picker/states/multipleRecordPickerSearchFilterComponentState';
|
||||
import { multipleRecordPickerSearchableObjectMetadataItemsComponentState } from '@/object-record/record-picker/multiple-record-picker/states/multipleRecordPickerSearchableObjectMetadataItemsComponentState';
|
||||
import { SingleRecordPicker } from '@/object-record/record-picker/single-record-picker/components/SingleRecordPicker';
|
||||
import { singleRecordPickerSearchFilterComponentState } from '@/object-record/record-picker/single-record-picker/states/singleRecordPickerSearchFilterComponentState';
|
||||
import { singleRecordPickerSelectedIdComponentState } from '@/object-record/record-picker/single-record-picker/states/singleRecordPickerSelectedIdComponentState';
|
||||
import { SingleRecordPickerRecord } from '@/object-record/record-picker/single-record-picker/types/SingleRecordPickerRecord';
|
||||
import { RecordDetailRelationRecordsList } from '@/object-record/record-show/record-detail-section/components/RecordDetailRelationRecordsList';
|
||||
import { RecordDetailSection } from '@/object-record/record-show/record-detail-section/components/RecordDetailSection';
|
||||
import { RecordDetailSectionHeader } from '@/object-record/record-show/record-detail-section/components/RecordDetailSectionHeader';
|
||||
@ -78,21 +81,44 @@ export const RecordDetailRelationSection = ({
|
||||
? [fieldValue as ObjectRecord]
|
||||
: ((fieldValue as ObjectRecord[]) ?? []);
|
||||
|
||||
const relationRecordIds = relationRecords.map(({ id }) => id);
|
||||
|
||||
const dropdownId = `record-field-card-relation-picker-${fieldDefinition.fieldMetadataId}-${recordId}`;
|
||||
|
||||
const { closeDropdown, isDropdownOpen, dropdownPlacement } =
|
||||
useDropdown(dropdownId);
|
||||
|
||||
const setRecordPickerSearchFilter = useSetRecoilComponentStateV2(
|
||||
recordPickerSearchFilterComponentState,
|
||||
const setMultipleRecordPickerSearchFilter = useSetRecoilComponentStateV2(
|
||||
multipleRecordPickerSearchFilterComponentState,
|
||||
dropdownId,
|
||||
);
|
||||
|
||||
const setMultipleRecordPickerPickableMorphItems =
|
||||
useSetRecoilComponentStateV2(
|
||||
multipleRecordPickerPickableMorphItemsComponentState,
|
||||
dropdownId,
|
||||
);
|
||||
|
||||
const setMultipleRecordPickerSearchableObjectMetadataItems =
|
||||
useSetRecoilComponentStateV2(
|
||||
multipleRecordPickerSearchableObjectMetadataItemsComponentState,
|
||||
dropdownId,
|
||||
);
|
||||
|
||||
const { performSearch: multipleRecordPickerPerformSearch } =
|
||||
useMultipleRecordPickerPerformSearch();
|
||||
|
||||
const setSingleRecordPickerSearchFilter = useSetRecoilComponentStateV2(
|
||||
singleRecordPickerSearchFilterComponentState,
|
||||
dropdownId,
|
||||
);
|
||||
|
||||
const setSingleRecordPickerSelectedId = useSetRecoilComponentStateV2(
|
||||
singleRecordPickerSelectedIdComponentState,
|
||||
dropdownId,
|
||||
);
|
||||
|
||||
const handleCloseRelationPickerDropdown = useCallback(() => {
|
||||
setRecordPickerSearchFilter('');
|
||||
}, [setRecordPickerSearchFilter]);
|
||||
setMultipleRecordPickerSearchFilter('');
|
||||
}, [setMultipleRecordPickerSearchFilter]);
|
||||
|
||||
const persistField = usePersistField();
|
||||
const { updateOneRecord: updateOneRelationRecord } = useUpdateOneRecord({
|
||||
@ -119,9 +145,7 @@ export const RecordDetailRelationSection = ({
|
||||
});
|
||||
};
|
||||
|
||||
const { updateRelation } = useUpdateRelationFromManyFieldInput({
|
||||
scopeId: dropdownId,
|
||||
});
|
||||
const { updateRelation } = useUpdateRelationFromManyFieldInput();
|
||||
|
||||
const indexViewId = useRecoilValue(
|
||||
prefetchIndexViewIdFromObjectMetadataItemFamilySelector({
|
||||
@ -170,6 +194,40 @@ export const RecordDetailRelationSection = ({
|
||||
|
||||
const relationRecordsCount = relationRecords.length;
|
||||
|
||||
const handleOpenRelationPickerDropdown = () => {
|
||||
if (isToOneObject) {
|
||||
setSingleRecordPickerSearchFilter('');
|
||||
setSingleRecordPickerSelectedId(relationRecords[0].id);
|
||||
}
|
||||
|
||||
if (isToManyObjects) {
|
||||
setMultipleRecordPickerSearchableObjectMetadataItems([
|
||||
relationObjectMetadataItem,
|
||||
]);
|
||||
setMultipleRecordPickerSearchFilter('');
|
||||
setMultipleRecordPickerPickableMorphItems(
|
||||
relationRecords.map((record) => ({
|
||||
recordId: record.id,
|
||||
objectMetadataId: relationObjectMetadataItem.id,
|
||||
isSelected: true,
|
||||
isMatchingSearchFilter: true,
|
||||
})),
|
||||
);
|
||||
|
||||
multipleRecordPickerPerformSearch({
|
||||
multipleRecordPickerInstanceId: dropdownId,
|
||||
forceSearchFilter: '',
|
||||
forceSearchableObjectMetadataItems: [relationObjectMetadataItem],
|
||||
forcePickableMorphItems: relationRecords.map((record) => ({
|
||||
recordId: record.id,
|
||||
objectMetadataId: relationObjectMetadataItem.id,
|
||||
isSelected: true,
|
||||
isMatchingSearchFilter: true,
|
||||
})),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<RecordDetailSection>
|
||||
<RecordDetailSectionHeader
|
||||
@ -194,6 +252,7 @@ export const RecordDetailRelationSection = ({
|
||||
dropdownId={dropdownId}
|
||||
dropdownPlacement="left-start"
|
||||
onClose={handleCloseRelationPickerDropdown}
|
||||
onOpen={handleOpenRelationPickerDropdown}
|
||||
clickableComponent={
|
||||
<LightIconButton
|
||||
className="displayOnHover"
|
||||
@ -202,35 +261,37 @@ export const RecordDetailRelationSection = ({
|
||||
/>
|
||||
}
|
||||
dropdownComponents={
|
||||
<RecordPickerComponentInstanceContext.Provider
|
||||
value={{ instanceId: dropdownId }}
|
||||
>
|
||||
{isToOneObject ? (
|
||||
<SingleRecordPickerMenuItemsWithSearch
|
||||
EmptyIcon={IconForbid}
|
||||
onRecordSelected={handleRelationPickerEntitySelected}
|
||||
selectedRecordIds={relationRecordIds}
|
||||
objectNameSingular={relationObjectMetadataNameSingular}
|
||||
recordPickerInstanceId={dropdownId}
|
||||
onCreate={createNewRecordAndOpenRightDrawer}
|
||||
dropdownPlacement={dropdownPlacement}
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
<RelationFromManyFieldInputMultiRecordsEffect />
|
||||
<MultipleRecordPicker
|
||||
componentInstanceId={dropdownId}
|
||||
onCreate={() => {
|
||||
closeDropdown();
|
||||
createNewRecordAndOpenRightDrawer?.();
|
||||
}}
|
||||
onChange={updateRelation}
|
||||
onSubmit={closeDropdown}
|
||||
dropdownPlacement={dropdownPlacement}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</RecordPickerComponentInstanceContext.Provider>
|
||||
isToOneObject ? (
|
||||
<SingleRecordPicker
|
||||
componentInstanceId={dropdownId}
|
||||
EmptyIcon={IconForbid}
|
||||
onRecordSelected={handleRelationPickerEntitySelected}
|
||||
objectNameSingular={relationObjectMetadataNameSingular}
|
||||
recordPickerInstanceId={dropdownId}
|
||||
onCreate={createNewRecordAndOpenRightDrawer}
|
||||
layoutDirection={
|
||||
dropdownPlacement?.includes('end')
|
||||
? 'search-bar-on-bottom'
|
||||
: 'search-bar-on-top'
|
||||
}
|
||||
/>
|
||||
) : (
|
||||
<MultipleRecordPicker
|
||||
componentInstanceId={dropdownId}
|
||||
onCreate={() => {
|
||||
closeDropdown();
|
||||
createNewRecordAndOpenRightDrawer?.();
|
||||
}}
|
||||
onChange={updateRelation}
|
||||
onSubmit={closeDropdown}
|
||||
onClickOutside={closeDropdown}
|
||||
layoutDirection={
|
||||
dropdownPlacement?.includes('end')
|
||||
? 'search-bar-on-bottom'
|
||||
: 'search-bar-on-top'
|
||||
}
|
||||
/>
|
||||
)
|
||||
}
|
||||
dropdownHotkeyScope={{ scope: dropdownId }}
|
||||
/>
|
||||
|
||||
@ -1,7 +1,18 @@
|
||||
import { FieldContext } from '@/object-record/record-field/contexts/FieldContext';
|
||||
import { recordFieldInputLayoutDirectionComponentState } from '@/object-record/record-field/states/recordFieldInputLayoutDirectionComponentState';
|
||||
import { recordFieldInputLayoutDirectionLoadingComponentState } from '@/object-record/record-field/states/recordFieldInputLayoutDirectionLoadingComponentState';
|
||||
import { getRecordFieldInputId } from '@/object-record/utils/getRecordFieldInputId';
|
||||
import { OverlayContainer } from '@/ui/layout/overlay/components/OverlayContainer';
|
||||
import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentStateV2';
|
||||
import styled from '@emotion/styled';
|
||||
import { autoUpdate, flip, offset, useFloating } from '@floating-ui/react';
|
||||
import { ReactElement } from 'react';
|
||||
import {
|
||||
MiddlewareState,
|
||||
autoUpdate,
|
||||
flip,
|
||||
offset,
|
||||
useFloating,
|
||||
} from '@floating-ui/react';
|
||||
import { ReactElement, useContext } from 'react';
|
||||
|
||||
const StyledEditableCellEditModeContainer = styled.div<RecordTableCellEditModeProps>`
|
||||
align-items: center;
|
||||
@ -22,15 +33,45 @@ export type RecordTableCellEditModeProps = {
|
||||
export const RecordTableCellEditMode = ({
|
||||
children,
|
||||
}: RecordTableCellEditModeProps) => {
|
||||
const { recordId, fieldDefinition } = useContext(FieldContext);
|
||||
|
||||
const instanceId = getRecordFieldInputId(
|
||||
recordId,
|
||||
fieldDefinition?.metadata?.fieldName,
|
||||
);
|
||||
|
||||
const setFieldInputLayoutDirection = useSetRecoilComponentStateV2(
|
||||
recordFieldInputLayoutDirectionComponentState,
|
||||
instanceId,
|
||||
);
|
||||
|
||||
const setFieldInputLayoutDirectionLoading = useSetRecoilComponentStateV2(
|
||||
recordFieldInputLayoutDirectionLoadingComponentState,
|
||||
instanceId,
|
||||
);
|
||||
|
||||
const setFieldInputLayoutDirectionMiddleware = {
|
||||
name: 'middleware',
|
||||
fn: async (state: MiddlewareState) => {
|
||||
setFieldInputLayoutDirection(
|
||||
state.placement.startsWith('bottom') ? 'downward' : 'upward',
|
||||
);
|
||||
setFieldInputLayoutDirectionLoading(false);
|
||||
return {};
|
||||
},
|
||||
};
|
||||
|
||||
const { refs, floatingStyles } = useFloating({
|
||||
placement: 'top-start',
|
||||
placement: 'bottom-start',
|
||||
middleware: [
|
||||
flip(),
|
||||
offset({
|
||||
mainAxis: -33,
|
||||
crossAxis: -3,
|
||||
}),
|
||||
setFieldInputLayoutDirectionMiddleware,
|
||||
],
|
||||
|
||||
whileElementsMounted: autoUpdate,
|
||||
});
|
||||
|
||||
|
||||
@ -2,16 +2,20 @@ import { ReactNode, useContext } from 'react';
|
||||
|
||||
import { isLabelIdentifierField } from '@/object-metadata/utils/isLabelIdentifierField';
|
||||
import { FieldContext } from '@/object-record/record-field/contexts/FieldContext';
|
||||
import { RelationPickerHotkeyScope } from '@/object-record/record-field/meta-types/input/types/RelationPickerHotkeyScope';
|
||||
import { FieldMetadata } from '@/object-record/record-field/types/FieldMetadata';
|
||||
import { isFieldRelation } from '@/object-record/record-field/types/guards/isFieldRelation';
|
||||
import { isFieldSelect } from '@/object-record/record-field/types/guards/isFieldSelect';
|
||||
import { useRecordIndexContextOrThrow } from '@/object-record/record-index/contexts/RecordIndexContext';
|
||||
import { MultipleRecordPickerHotkeyScope } from '@/object-record/record-picker/multiple-record-picker/types/MultipleRecordPickerHotkeyScope';
|
||||
import { SingleRecordPickerHotkeyScope } from '@/object-record/record-picker/single-record-picker/types/SingleRecordPickerHotkeyScope';
|
||||
import { RecordUpdateContext } from '@/object-record/record-table/contexts/EntityUpdateMutationHookContext';
|
||||
import { RecordTableCellContext } from '@/object-record/record-table/contexts/RecordTableCellContext';
|
||||
import { useRecordTableContextOrThrow } from '@/object-record/record-table/contexts/RecordTableContext';
|
||||
import { useRecordTableRowContextOrThrow } from '@/object-record/record-table/contexts/RecordTableRowContext';
|
||||
import { ColumnDefinition } from '@/object-record/record-table/types/ColumnDefinition';
|
||||
import { TableHotkeyScope } from '@/object-record/record-table/types/TableHotkeyScope';
|
||||
import { SelectFieldHotkeyScope } from '@/object-record/select/types/SelectFieldHotkeyScope';
|
||||
import { RelationDefinitionType } from '~/generated-metadata/graphql';
|
||||
import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull';
|
||||
|
||||
export const RecordTableCellFieldContextWrapper = ({
|
||||
@ -33,11 +37,36 @@ export const RecordTableCellFieldContextWrapper = ({
|
||||
return null;
|
||||
}
|
||||
|
||||
const customHotkeyScope = isFieldRelation(columnDefinition)
|
||||
? RelationPickerHotkeyScope.RelationPicker
|
||||
: isFieldSelect(columnDefinition)
|
||||
? SelectFieldHotkeyScope.SelectField
|
||||
: TableHotkeyScope.CellEditMode;
|
||||
// TODO: deprecate this and use useOpenFieldInput hooks to set the hotkey scope
|
||||
const computedHotkeyScope = (
|
||||
columnDefinition: ColumnDefinition<FieldMetadata>,
|
||||
) => {
|
||||
if (isFieldRelation(columnDefinition)) {
|
||||
if (
|
||||
columnDefinition.metadata.relationType ===
|
||||
RelationDefinitionType.MANY_TO_ONE
|
||||
) {
|
||||
return SingleRecordPickerHotkeyScope.SingleRecordPicker;
|
||||
}
|
||||
|
||||
if (
|
||||
columnDefinition.metadata.relationType ===
|
||||
RelationDefinitionType.ONE_TO_MANY
|
||||
) {
|
||||
return MultipleRecordPickerHotkeyScope.MultipleRecordPicker;
|
||||
}
|
||||
|
||||
return SingleRecordPickerHotkeyScope.SingleRecordPicker;
|
||||
}
|
||||
|
||||
if (isFieldSelect(columnDefinition)) {
|
||||
return SelectFieldHotkeyScope.SelectField;
|
||||
}
|
||||
|
||||
return TableHotkeyScope.CellEditMode;
|
||||
};
|
||||
|
||||
const customHotkeyScope = computedHotkeyScope(columnDefinition);
|
||||
|
||||
return (
|
||||
<FieldContext.Provider
|
||||
|
||||
@ -20,6 +20,7 @@ import { getSnapshotValue } from '@/ui/utilities/state/utils/getSnapshotValue';
|
||||
import { isDefined } from 'twenty-shared';
|
||||
|
||||
import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu';
|
||||
import { useOpenFieldInputEditMode } from '@/object-record/record-field/hooks/useOpenFieldInputEditMode';
|
||||
import { useRecordIndexContextOrThrow } from '@/object-record/record-index/contexts/RecordIndexContext';
|
||||
import { recordIndexOpenRecordInSelector } from '@/object-record/record-index/states/selectors/recordIndexOpenRecordInSelector';
|
||||
import { viewableRecordNameSingularState } from '@/object-record/record-right-drawer/states/viewableRecordNameSingularState';
|
||||
@ -80,6 +81,8 @@ export const useOpenRecordTableCellV2 = (tableScopeId: string) => {
|
||||
|
||||
const { openRecordInCommandMenu } = useCommandMenu();
|
||||
|
||||
const { openFieldInput } = useOpenFieldInputEditMode();
|
||||
|
||||
const openTableCell = useRecoilCallback(
|
||||
({ snapshot, set }) =>
|
||||
({
|
||||
@ -152,6 +155,11 @@ export const useOpenRecordTableCellV2 = (tableScopeId: string) => {
|
||||
|
||||
setDragSelectionStartEnabled(false);
|
||||
|
||||
openFieldInput({
|
||||
fieldDefinition,
|
||||
recordId,
|
||||
});
|
||||
|
||||
moveEditModeToTableCellPosition(cellPosition);
|
||||
|
||||
initDraftValue({
|
||||
@ -185,6 +193,7 @@ export const useOpenRecordTableCellV2 = (tableScopeId: string) => {
|
||||
[
|
||||
getClickOutsideListenerIsActivatedState,
|
||||
setDragSelectionStartEnabled,
|
||||
openFieldInput,
|
||||
moveEditModeToTableCellPosition,
|
||||
initDraftValue,
|
||||
toggleClickOutsideListener,
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user