Migrate to a monorepo structure (#2909)

This commit is contained in:
Charles Bochet
2023-12-10 18:10:54 +01:00
committed by GitHub
parent a70a9281eb
commit 5bdca9de6c
2304 changed files with 37152 additions and 25869 deletions

View File

@ -0,0 +1,187 @@
import { useState } from 'react';
import { useMutation } from '@apollo/client';
import { getOperationName } from '@apollo/client/utilities';
import styled from '@emotion/styled';
import { flip, offset, useFloating } from '@floating-ui/react';
import { v4 } from 'uuid';
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
import { FieldDoubleText } from '@/object-record/field/types/FieldDoubleText';
import { RelationPicker } from '@/object-record/relation-picker/components/RelationPicker';
import { EntityForSelect } from '@/object-record/relation-picker/types/EntityForSelect';
import { RelationPickerHotkeyScope } from '@/object-record/relation-picker/types/RelationPickerHotkeyScope';
import { IconPlus } from '@/ui/display/icon';
import { DoubleTextInput } from '@/ui/field/input/components/DoubleTextInput';
import { LightIconButton } from '@/ui/input/button/components/LightIconButton';
import { usePreviousHotkeyScope } from '@/ui/utilities/hotkey/hooks/usePreviousHotkeyScope';
import { RecoilScope } from '@/ui/utilities/recoil-scope/components/RecoilScope';
import { FieldMetadataType } from '~/generated-metadata/graphql';
const StyledContainer = styled.div`
position: static;
`;
const StyledInputContainer = styled.div`
background-color: transparent;
box-shadow: ${({ theme }) => theme.boxShadow.strong};
display: flex;
gap: ${({ theme }) => theme.spacing(0.5)};
width: ${({ theme }) => theme.spacing(62.5)};
& input,
div {
background-color: ${({ theme }) => theme.background.primary};
width: 100%;
}
div {
border-radius: ${({ theme }) => theme.spacing(1)};
overflow: hidden;
}
input {
display: flex;
flex-grow: 1;
padding: ${({ theme }) => theme.spacing(2)};
}
`;
export const AddPersonToCompany = ({
companyId,
peopleIds,
}: {
companyId: string;
peopleIds?: string[];
}) => {
const [isDropdownOpen, setIsDropdownOpen] = useState(false);
const [isCreationDropdownOpen, setIsCreationDropdownOpen] = useState(false);
const { refs, floatingStyles } = useFloating({
open: isDropdownOpen,
placement: 'right-start',
middleware: [flip(), offset({ mainAxis: 30, crossAxis: 0 })],
});
const handleEscape = () => {
if (isCreationDropdownOpen) setIsCreationDropdownOpen(false);
if (isDropdownOpen) setIsDropdownOpen(false);
};
const {
setHotkeyScopeAndMemorizePreviousScope,
goBackToPreviousHotkeyScope,
} = usePreviousHotkeyScope();
const {
findManyRecordsQuery,
updateOneRecordMutation,
createOneRecordMutation,
} = useObjectMetadataItem({
objectNameSingular: 'person',
});
const [updatePerson] = useMutation(updateOneRecordMutation);
const [createPerson] = useMutation(createOneRecordMutation);
const handlePersonSelected =
(companyId: string) => async (newPerson: EntityForSelect | null) => {
if (!newPerson) return;
await updatePerson({
variables: {
idToUpdate: newPerson.id,
input: {
companyId: companyId,
},
},
refetchQueries: [getOperationName(findManyRecordsQuery) ?? ''],
});
handleClosePicker();
};
const handleClosePicker = () => {
if (isDropdownOpen) {
setIsDropdownOpen(false);
goBackToPreviousHotkeyScope();
}
};
const handleOpenPicker = () => {
if (!isDropdownOpen) {
setIsDropdownOpen(true);
setHotkeyScopeAndMemorizePreviousScope(
RelationPickerHotkeyScope.RelationPicker,
);
}
};
const handleCreatePerson = async ({
firstValue,
secondValue,
}: FieldDoubleText) => {
if (!firstValue && !secondValue) return;
const newPersonId = v4();
await createPerson({
variables: {
input: {
companyId: companyId,
id: newPersonId,
name: {
firstName: firstValue,
lastName: secondValue,
},
},
},
refetchQueries: [getOperationName(findManyRecordsQuery) ?? ''],
});
setIsCreationDropdownOpen(false);
};
return (
<RecoilScope>
<StyledContainer ref={refs.setReference}>
<LightIconButton
Icon={IconPlus}
onClick={handleOpenPicker}
size="small"
accent="tertiary"
/>
{isDropdownOpen && (
<div ref={refs.setFloating} style={floatingStyles}>
{isCreationDropdownOpen ? (
<StyledInputContainer>
<DoubleTextInput
firstValue=""
secondValue=""
firstValuePlaceholder="First Name"
secondValuePlaceholder="Last Name"
onClickOutside={handleEscape}
onEnter={handleCreatePerson}
onEscape={handleEscape}
hotkeyScope={RelationPickerHotkeyScope.RelationPicker}
/>
</StyledInputContainer>
) : (
<RelationPicker
recordId={''}
onSubmit={handlePersonSelected(companyId)}
onCancel={handleClosePicker}
excludeRecordIds={peopleIds ?? []}
fieldDefinition={{
label: 'Person',
iconName: 'IconUser',
fieldMetadataId: '',
type: FieldMetadataType.Relation,
metadata: {
relationObjectMetadataNameSingular: 'person',
relationObjectMetadataNamePlural: 'people',
fieldName: 'person',
},
}}
/>
)}
</div>
)}
</StyledContainer>
</RecoilScope>
);
};

View File

@ -0,0 +1,258 @@
import { ReactNode, useContext } from 'react';
import styled from '@emotion/styled';
import { useRecoilState, useRecoilValue } from 'recoil';
import { FieldContext } from '@/object-record/field/contexts/FieldContext';
import { useUpdateOneRecord } from '@/object-record/hooks/useUpdateOneRecord';
import { BoardCardIdContext } from '@/object-record/record-board/contexts/BoardCardIdContext';
import { useCurrentRecordBoardCardSelectedInternal } from '@/object-record/record-board/hooks/internal/useCurrentRecordBoardCardSelectedInternal';
import { useRecordBoardScopedStates } from '@/object-record/record-board/hooks/internal/useRecordBoardScopedStates';
import { isRecordBoardCardInCompactViewFamilyState } from '@/object-record/record-board/states/isRecordBoardCardInCompactViewFamilyState';
import { RecordInlineCell } from '@/object-record/record-inline-cell/components/RecordInlineCell';
import { InlineCellHotkeyScope } from '@/object-record/record-inline-cell/types/InlineCellHotkeyScope';
import { EntityChipVariant } from '@/ui/display/chip/components/EntityChip';
import { IconEye } from '@/ui/display/icon/index';
import { LightIconButton } from '@/ui/input/button/components/LightIconButton';
import { Checkbox, CheckboxVariant } from '@/ui/input/components/Checkbox';
import { AnimatedEaseInOut } from '@/ui/utilities/animation/components/AnimatedEaseInOut';
import { getLogoUrlFromDomainName } from '~/utils';
import { companyProgressesFamilyState } from '../states/companyProgressesFamilyState';
import { CompanyChip } from './CompanyChip';
const StyledBoardCard = styled.div<{ selected: boolean }>`
background-color: ${({ theme, selected }) =>
selected ? theme.accent.quaternary : theme.background.secondary};
border: 1px solid
${({ theme, selected }) =>
selected ? theme.accent.secondary : theme.border.color.medium};
border-radius: ${({ theme }) => theme.border.radius.sm};
box-shadow: ${({ theme }) => theme.boxShadow.light};
color: ${({ theme }) => theme.font.color.primary};
&:hover {
background-color: ${({ theme, selected }) =>
selected && theme.accent.tertiary};
border: 1px solid
${({ theme, selected }) =>
selected ? theme.accent.primary : theme.border.color.medium};
}
cursor: pointer;
.checkbox-container {
transition: all ease-in-out 160ms;
opacity: ${({ selected }) => (selected ? 1 : 0)};
}
&:hover .checkbox-container {
opacity: 1;
}
.compact-icon-container {
transition: all ease-in-out 160ms;
opacity: 0;
}
&:hover .compact-icon-container {
opacity: 1;
}
`;
const StyledBoardCardWrapper = styled.div`
padding-bottom: ${({ theme }) => theme.spacing(2)};
width: 100%;
`;
const StyledBoardCardHeader = styled.div<{
showCompactView: boolean;
}>`
align-items: center;
display: flex;
flex-direction: row;
font-weight: ${({ theme }) => theme.font.weight.medium};
height: 24px;
padding-bottom: ${({ theme, showCompactView }) =>
theme.spacing(showCompactView ? 0 : 1)};
padding-left: ${({ theme }) => theme.spacing(2)};
padding-right: ${({ theme }) => theme.spacing(2)};
padding-top: ${({ theme }) => theme.spacing(2)};
transition: padding ease-in-out 160ms;
img {
height: ${({ theme }) => theme.icon.size.md}px;
margin-right: ${({ theme }) => theme.spacing(2)};
object-fit: cover;
width: ${({ theme }) => theme.icon.size.md}px;
}
`;
const StyledBoardCardBody = styled.div`
display: flex;
flex-direction: column;
gap: ${({ theme }) => theme.spacing(0.5)};
padding-bottom: ${({ theme }) => theme.spacing(2)};
padding-left: ${({ theme }) => theme.spacing(2.5)};
padding-right: ${({ theme }) => theme.spacing(2)};
span {
align-items: center;
display: flex;
flex-direction: row;
svg {
color: ${({ theme }) => theme.font.color.tertiary};
margin-right: ${({ theme }) => theme.spacing(2)};
}
}
`;
const StyledCheckboxContainer = styled.div`
display: flex;
flex: 1;
justify-content: end;
`;
const StyledFieldContainer = styled.div`
display: flex;
flex-direction: row;
width: 100%;
`;
const StyledCompactIconContainer = styled.div`
align-items: center;
display: flex;
justify-content: center;
`;
export const CompanyBoardCard = () => {
const { isCurrentCardSelected, setCurrentCardSelected } =
useCurrentRecordBoardCardSelectedInternal();
const boardCardId = useContext(BoardCardIdContext);
const [companyProgress] = useRecoilState(
companyProgressesFamilyState(boardCardId ?? ''),
);
const { isCompactViewEnabledState, visibleBoardCardFieldsSelector } =
useRecordBoardScopedStates();
const [isCompactViewEnabled] = useRecoilState(isCompactViewEnabledState);
const [isCardInCompactView, setIsCardInCompactView] = useRecoilState(
isRecordBoardCardInCompactViewFamilyState(boardCardId ?? ''),
);
const showCompactView = isCompactViewEnabled && isCardInCompactView;
const { opportunity, company } = companyProgress ?? {};
const visibleBoardCardFields = useRecoilValue(visibleBoardCardFieldsSelector);
const useUpdateOneRecordMutation: () => [(params: any) => any, any] = () => {
const { updateOneRecord: updateOneOpportunity } = useUpdateOneRecord({
objectNameSingular: 'opportunity',
});
const updateEntity = ({
variables,
}: {
variables: {
where: { id: string };
data: {
[fieldName: string]: any;
};
};
}) => {
updateOneOpportunity?.({
idToUpdate: variables.where.id,
input: variables.data,
});
};
return [updateEntity, { loading: false }];
};
// boardCardId check can be moved to a wrapper to avoid unnecessary logic above
if (!company || !opportunity || !boardCardId) {
return null;
}
const PreventSelectOnClickContainer = ({
children,
}: {
children: ReactNode;
}) => (
<StyledFieldContainer
onClick={(e) => {
e.stopPropagation();
}}
>
{children}
</StyledFieldContainer>
);
const OnMouseLeaveBoard = () => {
setIsCardInCompactView(true);
};
return (
<StyledBoardCardWrapper>
<StyledBoardCard
selected={isCurrentCardSelected}
onMouseLeave={OnMouseLeaveBoard}
onClick={() => setCurrentCardSelected(!isCurrentCardSelected)}
>
<StyledBoardCardHeader showCompactView={showCompactView}>
<CompanyChip
id={company.id}
name={company.name}
avatarUrl={getLogoUrlFromDomainName(company.domainName)}
variant={EntityChipVariant.Transparent}
/>
{showCompactView && (
<StyledCompactIconContainer className="compact-icon-container">
<LightIconButton
Icon={IconEye}
accent="tertiary"
onClick={(e) => {
e.stopPropagation();
setIsCardInCompactView(false);
}}
/>
</StyledCompactIconContainer>
)}
<StyledCheckboxContainer className="checkbox-container">
<Checkbox
checked={isCurrentCardSelected}
onChange={() => setCurrentCardSelected(!isCurrentCardSelected)}
variant={CheckboxVariant.Secondary}
/>
</StyledCheckboxContainer>
</StyledBoardCardHeader>
<StyledBoardCardBody>
<AnimatedEaseInOut isOpen={!showCompactView} initial={false}>
{visibleBoardCardFields.map((viewField) => (
<PreventSelectOnClickContainer key={viewField.fieldMetadataId}>
<FieldContext.Provider
value={{
entityId: boardCardId,
recoilScopeId: boardCardId + viewField.fieldMetadataId,
isLabelIdentifier: false,
fieldDefinition: {
fieldMetadataId: viewField.fieldMetadataId,
label: viewField.label,
iconName: viewField.iconName,
type: viewField.type,
metadata: viewField.metadata,
},
useUpdateEntityMutation: useUpdateOneRecordMutation,
hotkeyScope: InlineCellHotkeyScope.InlineCell,
}}
>
<RecordInlineCell />
</FieldContext.Provider>
</PreventSelectOnClickContainer>
))}
</AnimatedEaseInOut>
</StyledBoardCardBody>
</StyledBoardCard>
</StyledBoardCardWrapper>
);
};

View File

@ -0,0 +1,27 @@
import {
EntityChip,
EntityChipVariant,
} from '@/ui/display/chip/components/EntityChip';
type CompanyChipProps = {
id: string;
name: string;
avatarUrl?: string;
variant?: EntityChipVariant;
};
export const CompanyChip = ({
id,
name,
avatarUrl,
variant = EntityChipVariant.Regular,
}: CompanyChipProps) => (
<EntityChip
entityId={id}
linkToEntity={`/object/company/${id}`}
name={name}
avatarType="squared"
avatarUrl={avatarUrl}
variant={variant}
/>
);

View File

@ -0,0 +1,94 @@
import { useQuery } from '@apollo/client';
import styled from '@emotion/styled';
import { isNonEmptyArray } from '@sniptt/guards';
import { Company } from '@/companies/types/Company';
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
import { mapPaginatedRecordsToRecords } from '@/object-record/utils/mapPaginatedRecordsToRecords';
import { PeopleCard } from '@/people/components/PeopleCard';
import { AddPersonToCompany } from './AddPersonToCompany';
export type CompanyTeamProps = {
company: Pick<Company, 'id'>;
};
const StyledContainer = styled.div`
display: flex;
flex-direction: column;
gap: ${({ theme }) => theme.spacing(2)};
margin-bottom: ${({ theme }) => theme.spacing(6)};
`;
const StyledTitleContainer = styled.div`
align-items: center;
color: ${({ theme }) => theme.font.color.primary};
display: flex;
justify-content: space-between;
padding-bottom: ${({ theme }) => theme.spacing(0)};
padding-top: ${({ theme }) => theme.spacing(3)};
`;
const StyledListContainer = styled.div`
align-items: flex-start;
border: 1px solid ${({ theme }) => theme.border.color.medium};
border-radius: ${({ theme }) => theme.spacing(1)};
box-sizing: border-box;
display: flex;
flex-direction: column;
overflow: auto;
width: 100%;
`;
const StyledTitle = styled.div`
color: ${({ theme }) => theme.font.color.primary};
font-weight: ${({ theme }) => theme.font.weight.medium};
line-height: ${({ theme }) => theme.text.lineHeight.lg};
`;
export const CompanyTeam = ({ company }: { company: any }) => {
const { findManyRecordsQuery } = useObjectMetadataItem({
objectNameSingular: 'person',
});
const { data } = useQuery(findManyRecordsQuery, {
variables: {
filter: {
companyId: {
eq: company.id,
},
},
},
});
const people = mapPaginatedRecordsToRecords({
objectNamePlural: 'people',
pagedRecords: data ?? [],
});
const peopleIds = people.map((person) => person.id);
const hasPeople = isNonEmptyArray(peopleIds);
return (
<>
{hasPeople && (
<StyledContainer>
<StyledTitleContainer>
<StyledTitle>Team</StyledTitle>
<AddPersonToCompany companyId={company.id} peopleIds={peopleIds} />
</StyledTitleContainer>
<StyledListContainer>
{people.map((person: any) => (
<PeopleCard
key={person.id}
person={person}
hasBottomBorder={person.id !== people.length - 1}
/>
))}
</StyledListContainer>
</StyledContainer>
)}
</>
);
};

View File

@ -0,0 +1,141 @@
import { useEffect } from 'react';
import { useRecoilValue } from 'recoil';
import { useColumnDefinitionsFromFieldMetadata } from '@/object-metadata/hooks/useColumnDefinitionsFromFieldMetadata';
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
import { useRecordBoardScopedStates } from '@/object-record/record-board/hooks/internal/useRecordBoardScopedStates';
import { availableRecordBoardCardFieldsScopedState } from '@/object-record/record-board/states/availableRecordBoardCardFieldsScopedState';
import { recordBoardCardFieldsScopedState } from '@/object-record/record-board/states/recordBoardCardFieldsScopedState';
import { recordBoardFiltersScopedState } from '@/object-record/record-board/states/recordBoardFiltersScopedState';
import { recordBoardSortsScopedState } from '@/object-record/record-board/states/recordBoardSortsScopedState';
import { filterAvailableTableColumns } from '@/object-record/utils/filterAvailableTableColumns';
import { useSetRecoilScopedStateV2 } from '@/ui/utilities/recoil-scope/hooks/useSetRecoilScopedStateV2';
import { useViewScopedStates } from '@/views/hooks/internal/useViewScopedStates';
import { useViewBar } from '@/views/hooks/useViewBar';
import { ViewType } from '@/views/types/ViewType';
import { mapViewFieldsToBoardFieldDefinitions } from '@/views/utils/mapViewFieldsToBoardFieldDefinitions';
type HooksCompanyBoardEffectProps = {
viewBarId: string;
recordBoardId: string;
};
export const HooksCompanyBoardEffect = ({
viewBarId,
recordBoardId,
}: HooksCompanyBoardEffectProps) => {
const {
setAvailableFilterDefinitions,
setAvailableSortDefinitions,
setAvailableFieldDefinitions,
setViewObjectMetadataId,
setViewType,
} = useViewBar({ viewBarId });
const { objectMetadataItem } = useObjectMetadataItem({
objectNameSingular: 'opportunity',
});
const { columnDefinitions, filterDefinitions, sortDefinitions } =
useColumnDefinitionsFromFieldMetadata(objectMetadataItem);
const setAvailableBoardCardFields = useSetRecoilScopedStateV2(
availableRecordBoardCardFieldsScopedState,
'company-board-view',
);
useEffect(() => {
if (!objectMetadataItem) {
return;
}
setAvailableFilterDefinitions?.(filterDefinitions);
setAvailableSortDefinitions?.(sortDefinitions);
setAvailableFieldDefinitions?.(columnDefinitions);
}, [
columnDefinitions,
filterDefinitions,
objectMetadataItem,
setAvailableFieldDefinitions,
setAvailableFilterDefinitions,
setAvailableSortDefinitions,
sortDefinitions,
]);
useEffect(() => {
const availableTableColumns = columnDefinitions.filter(
filterAvailableTableColumns,
);
setAvailableBoardCardFields(availableTableColumns);
}, [columnDefinitions, setAvailableBoardCardFields]);
useEffect(() => {
if (!objectMetadataItem) {
return;
}
setViewObjectMetadataId?.(objectMetadataItem.id);
setViewType?.(ViewType.Kanban);
}, [objectMetadataItem, setViewObjectMetadataId, setViewType]);
const {
currentViewFieldsState,
currentViewFiltersState,
currentViewSortsState,
} = useViewScopedStates({ viewScopeId: viewBarId });
const currentViewFields = useRecoilValue(currentViewFieldsState);
const currentViewFilters = useRecoilValue(currentViewFiltersState);
const currentViewSorts = useRecoilValue(currentViewSortsState);
//TODO: Modify to use scopeId
const setBoardCardFields = useSetRecoilScopedStateV2(
recordBoardCardFieldsScopedState,
'company-board',
);
const setBoardCardFilters = useSetRecoilScopedStateV2(
recordBoardFiltersScopedState,
'company-board',
);
const setBoardCardSorts = useSetRecoilScopedStateV2(
recordBoardSortsScopedState,
'company-board',
);
useEffect(() => {
if (currentViewFields) {
setBoardCardFields(
mapViewFieldsToBoardFieldDefinitions(
currentViewFields,
columnDefinitions,
),
);
}
}, [columnDefinitions, currentViewFields, setBoardCardFields]);
useEffect(() => {
if (currentViewFilters) {
setBoardCardFilters(currentViewFilters);
}
}, [currentViewFilters, setBoardCardFilters]);
useEffect(() => {
if (currentViewSorts) {
setBoardCardSorts(currentViewSorts);
}
}, [currentViewSorts, setBoardCardSorts]);
const { setEntityCountInCurrentView } = useViewBar({ viewBarId });
const { savedOpportunitiesState } = useRecordBoardScopedStates({
recordBoardScopeId: recordBoardId,
});
const savedOpportunities = useRecoilValue(savedOpportunitiesState);
useEffect(() => {
setEntityCountInCurrentView(savedOpportunities.length);
}, [savedOpportunities.length, setEntityCountInCurrentView]);
return <></>;
};

View File

@ -0,0 +1,96 @@
import { useCallback, useContext, useState } from 'react';
import { useQuery } from '@apollo/client';
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
import { NewButton } from '@/object-record/record-board/components/NewButton';
import { BoardColumnContext } from '@/object-record/record-board/contexts/BoardColumnContext';
import { useCreateOpportunity } from '@/object-record/record-board/hooks/internal/useCreateOpportunity';
import { SingleEntitySelect } from '@/object-record/relation-picker/components/SingleEntitySelect';
import { useRelationPicker } from '@/object-record/relation-picker/hooks/useRelationPicker';
import { RelationPickerHotkeyScope } from '@/object-record/relation-picker/types/RelationPickerHotkeyScope';
import { useFilteredSearchEntityQuery } from '@/search/hooks/useFilteredSearchEntityQuery';
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
import { usePreviousHotkeyScope } from '@/ui/utilities/hotkey/hooks/usePreviousHotkeyScope';
export const NewOpportunityButton = () => {
const [isCreatingCard, setIsCreatingCard] = useState(false);
const column = useContext(BoardColumnContext);
const pipelineStepId = column?.columnDefinition.id || '';
const { enqueueSnackBar } = useSnackBar();
const createOpportunity = useCreateOpportunity();
const {
goBackToPreviousHotkeyScope,
setHotkeyScopeAndMemorizePreviousScope,
} = usePreviousHotkeyScope();
const handleEntitySelect = (company: any) => {
setIsCreatingCard(false);
goBackToPreviousHotkeyScope();
if (!pipelineStepId) {
enqueueSnackBar('Pipeline stage id is not defined', {
variant: 'error',
});
throw new Error('Pipeline stage id is not defined');
}
createOpportunity(company.id, pipelineStepId);
};
const handleNewClick = useCallback(() => {
setIsCreatingCard(true);
setHotkeyScopeAndMemorizePreviousScope(
RelationPickerHotkeyScope.RelationPicker,
);
}, [setIsCreatingCard, setHotkeyScopeAndMemorizePreviousScope]);
const handleCancel = () => {
goBackToPreviousHotkeyScope();
setIsCreatingCard(false);
};
const { relationPickerSearchFilter } = useRelationPicker();
// TODO: refactor useFilteredSearchEntityQuery
const { findManyRecordsQuery } = useObjectMetadataItem({
objectNameSingular: 'company',
});
const useFindManyQuery = (options: any) =>
useQuery(findManyRecordsQuery, options);
const { identifiersMapper, searchQuery } = useRelationPicker();
const filteredSearchEntityResults = useFilteredSearchEntityQuery({
queryHook: useFindManyQuery,
filters: [
{
fieldNames: searchQuery?.computeFilterFields?.('company') ?? [],
filter: relationPickerSearchFilter,
},
],
orderByField: 'createdAt',
selectedIds: [],
mappingFunction: (record: any) => identifiersMapper?.(record, 'company'),
objectNameSingular: 'company',
});
return (
<>
{isCreatingCard ? (
<SingleEntitySelect
disableBackgroundBlur
entitiesToSelect={filteredSearchEntityResults.entitiesToSelect}
loading={filteredSearchEntityResults.loading}
onCancel={handleCancel}
onEntitySelected={handleEntitySelect}
selectedEntity={filteredSearchEntityResults.selectedEntities[0]}
/>
) : (
<NewButton onClick={handleNewClick} />
)}
</>
);
};

View File

@ -0,0 +1,141 @@
import { useEffect, useMemo, useRef, useState } from 'react';
import { useQuery } from '@apollo/client';
import { useRecoilValue } from 'recoil';
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
import { SingleEntitySelectBase } from '@/object-record/relation-picker/components/SingleEntitySelectBase';
import { useEntitySelectSearch } from '@/object-record/relation-picker/hooks/useEntitySelectSearch';
import { useRelationPicker } from '@/object-record/relation-picker/hooks/useRelationPicker';
import { EntityForSelect } from '@/object-record/relation-picker/types/EntityForSelect';
import { currentPipelineStepsState } from '@/pipeline/states/currentPipelineStepsState';
import { useFilteredSearchEntityQuery } from '@/search/hooks/useFilteredSearchEntityQuery';
import { IconChevronDown } from '@/ui/display/icon';
import { DropdownMenu } from '@/ui/layout/dropdown/components/DropdownMenu';
import { DropdownMenuHeader } from '@/ui/layout/dropdown/components/DropdownMenuHeader';
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 { MenuItem } from '@/ui/navigation/menu-item/components/MenuItem';
import { RecoilScope } from '@/ui/utilities/recoil-scope/components/RecoilScope';
export type OpportunityPickerProps = {
companyId: string | null;
onSubmit: (
newCompanyId: EntityForSelect | null,
newPipelineStepId: string | null,
) => void;
onCancel?: () => void;
};
export const OpportunityPicker = ({
onSubmit,
onCancel,
}: OpportunityPickerProps) => {
const containerRef = useRef<HTMLDivElement>(null);
const { searchFilter, handleSearchFilterChange } = useEntitySelectSearch();
// TODO: refactor useFilteredSearchEntityQuery
const { findManyRecordsQuery: findManyCompanies } = useObjectMetadataItem({
objectNameSingular: 'company',
});
const useFindManyQuery = (options: any) =>
useQuery(findManyCompanies, options);
const { identifiersMapper, searchQuery } = useRelationPicker();
const filteredSearchEntityResults = useFilteredSearchEntityQuery({
queryHook: useFindManyQuery,
filters: [
{
fieldNames: searchQuery?.computeFilterFields?.('company') ?? [],
filter: searchFilter,
},
],
orderByField: 'createdAt',
selectedIds: [],
mappingFunction: (record: any) => identifiersMapper?.(record, 'company'),
objectNameSingular: 'company',
});
const [isProgressSelectionUnfolded, setIsProgressSelectionUnfolded] =
useState(false);
const [selectedPipelineStepId, setSelectedPipelineStepId] = useState<
string | null
>(null);
const currentPipelineSteps = useRecoilValue(currentPipelineStepsState);
const handlePipelineStepChange = (newPipelineStepId: string) => {
setSelectedPipelineStepId(newPipelineStepId);
setIsProgressSelectionUnfolded(false);
};
const handleEntitySelected = async (
selectedCompany: EntityForSelect | null | undefined,
) => {
onSubmit(selectedCompany ?? null, selectedPipelineStepId);
};
useEffect(() => {
if (currentPipelineSteps?.[0]?.id) {
setSelectedPipelineStepId(currentPipelineSteps?.[0]?.id);
}
}, [currentPipelineSteps]);
const selectedPipelineStep = useMemo(
() =>
currentPipelineSteps.find(
(pipelineStep: any) => pipelineStep.id === selectedPipelineStepId,
),
[currentPipelineSteps, selectedPipelineStepId],
);
return (
<DropdownMenu
ref={containerRef}
data-testid={`company-progress-dropdown-menu`}
>
{isProgressSelectionUnfolded ? (
<DropdownMenuItemsContainer>
{currentPipelineSteps.map((pipelineStep: any, index: number) => (
<MenuItem
key={pipelineStep.id}
testId={`select-pipeline-stage-${index}`}
onClick={() => {
handlePipelineStepChange(pipelineStep.id);
}}
text={pipelineStep.name}
/>
))}
</DropdownMenuItemsContainer>
) : (
<>
<DropdownMenuHeader
testId="selected-pipeline-stage"
EndIcon={IconChevronDown}
onClick={() => setIsProgressSelectionUnfolded(true)}
>
{selectedPipelineStep?.name}
</DropdownMenuHeader>
<DropdownMenuSeparator />
<DropdownMenuSearchInput
value={searchFilter}
onChange={handleSearchFilterChange}
autoFocus
/>
<DropdownMenuSeparator />
<RecoilScope>
<SingleEntitySelectBase
entitiesToSelect={filteredSearchEntityResults.entitiesToSelect}
loading={filteredSearchEntityResults.loading}
onCancel={onCancel}
onEntitySelected={handleEntitySelected}
selectedEntity={filteredSearchEntityResults.selectedEntities[0]}
/>
</RecoilScope>
</>
)}
</DropdownMenu>
);
};