Migrate to a monorepo structure (#2909)
This commit is contained in:
@ -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>
|
||||
);
|
||||
};
|
||||
@ -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>
|
||||
);
|
||||
};
|
||||
@ -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}
|
||||
/>
|
||||
);
|
||||
@ -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>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
@ -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 <></>;
|
||||
};
|
||||
@ -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} />
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
@ -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>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user