From 64cef963bc66ec13fa279236909f043f7463d586 Mon Sep 17 00:00:00 2001 From: Lucas Bordeau Date: Wed, 23 Aug 2023 18:57:08 +0200 Subject: [PATCH] Feat/add opportunity (#1267) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Renamed AuthAutoRouter * Moved RecoilScope * Refactored old WithTopBarContainer to make it less transclusive * Created new add opportunity button and refactored DropdownButton * Added tests * Update front/src/modules/companies/components/CompanyProgressPicker.tsx Co-authored-by: Thaïs * Update front/src/modules/companies/components/CompanyProgressPicker.tsx Co-authored-by: Thaïs * Update front/src/modules/companies/components/CompanyProgressPicker.tsx Co-authored-by: Thaïs * Update front/src/modules/companies/components/CompanyProgressPicker.tsx Co-authored-by: Thaïs * Update front/src/modules/ui/dropdown/components/DropdownButton.tsx Co-authored-by: Thaïs * Update front/src/modules/ui/dropdown/components/DropdownButton.tsx Co-authored-by: Thaïs * Update front/src/modules/ui/dropdown/components/DropdownButton.tsx Co-authored-by: Thaïs * Update front/src/modules/ui/layout/components/PageHeader.tsx Co-authored-by: Thaïs * Update front/src/pages/opportunities/Opportunities.tsx Co-authored-by: Thaïs * Fix lint * Fix lint --------- Co-authored-by: Charles Bochet Co-authored-by: Charles Bochet Co-authored-by: Thaïs --- front/src/index.tsx | 4 +- .../companies/components/CompanyPicker.tsx | 8 +- .../components/CompanyProgressPicker.tsx | 146 ++++++++++++++++++ .../components/NewCompanyProgressButton.tsx | 68 +++----- .../hooks/useCreateCompanyProgress.ts | 47 ++++++ .../pipeline/components/PipelineAddButton.tsx | 79 ++++++++++ ...tton.tsx => DropdownButton_Deprecated.tsx} | 2 +- .../ui/dropdown/components/DropdownButton.tsx | 58 +++++++ .../ui/dropdown/components/HotkeyEffect.tsx | 19 +++ .../ui/dropdown/hooks/useDropdownButton.ts | 47 ++++++ .../states/isDropdownButtonOpenScopedState.ts | 6 + .../components/FilterDropdownButton.tsx | 7 +- .../hooks/useEntitySelectSearch.ts | 5 + .../modules/ui/layout/components/PageBody.tsx | 15 ++ .../ui/layout/components/PageContainer.tsx | 15 ++ .../ui/layout/components/PageHeader.tsx | 109 +++++++++++++ .../hotkey/hooks/usePreviousHotkeyScope.ts | 40 +++-- .../internal/previousHotkeyScopeState.ts | 8 + .../recoil-scope/components/RecoilScope.tsx | 2 +- .../src/pages/opportunities/Opportunities.tsx | 46 +++--- .../__stories__/Opportunities.stories.tsx | 52 +++++++ .../opportunitiesBoardOptions.tsx | 7 +- ...uthAutoRouter.tsx => PageChangeEffect.tsx} | 3 +- 23 files changed, 696 insertions(+), 97 deletions(-) create mode 100644 front/src/modules/companies/components/CompanyProgressPicker.tsx create mode 100644 front/src/modules/companies/hooks/useCreateCompanyProgress.ts create mode 100644 front/src/modules/pipeline/components/PipelineAddButton.tsx rename front/src/modules/ui/button/components/{DropdownButton.tsx => DropdownButton_Deprecated.tsx} (98%) create mode 100644 front/src/modules/ui/dropdown/components/DropdownButton.tsx create mode 100644 front/src/modules/ui/dropdown/components/HotkeyEffect.tsx create mode 100644 front/src/modules/ui/dropdown/hooks/useDropdownButton.ts create mode 100644 front/src/modules/ui/dropdown/states/isDropdownButtonOpenScopedState.ts create mode 100644 front/src/modules/ui/layout/components/PageBody.tsx create mode 100644 front/src/modules/ui/layout/components/PageContainer.tsx create mode 100644 front/src/modules/ui/layout/components/PageHeader.tsx create mode 100644 front/src/modules/ui/utilities/hotkey/states/internal/previousHotkeyScopeState.ts rename front/src/sync-hooks/{AuthAutoRouter.tsx => PageChangeEffect.tsx} (98%) diff --git a/front/src/index.tsx b/front/src/index.tsx index ccbbce2e6..72884e847 100644 --- a/front/src/index.tsx +++ b/front/src/index.tsx @@ -13,7 +13,7 @@ import { UserProvider } from '@/users/components/UserProvider'; import '@emotion/react'; -import { AuthAutoRouter } from './sync-hooks/AuthAutoRouter'; +import { PageChangeEffect } from './sync-hooks/PageChangeEffect'; import { App } from './App'; import './index.css'; @@ -29,7 +29,7 @@ root.render( - + diff --git a/front/src/modules/companies/components/CompanyPicker.tsx b/front/src/modules/companies/components/CompanyPicker.tsx index a52f0e16f..9864c2172 100644 --- a/front/src/modules/companies/components/CompanyPicker.tsx +++ b/front/src/modules/companies/components/CompanyPicker.tsx @@ -1,3 +1,5 @@ +import { useEffect } from 'react'; + import { SingleEntitySelect } from '@/ui/input/relation-picker/components/SingleEntitySelect'; import { relationPickerSearchFilterScopedState } from '@/ui/input/relation-picker/states/relationPickerSearchFilterScopedState'; import { EntityForSelect } from '@/ui/input/relation-picker/types/EntityForSelect'; @@ -12,7 +14,7 @@ export type OwnProps = { }; export function CompanyPicker({ companyId, onSubmit, onCancel }: OwnProps) { - const [searchFilter] = useRecoilScopedState( + const [searchFilter, setSearchFilter] = useRecoilScopedState( relationPickerSearchFilterScopedState, ); @@ -27,6 +29,10 @@ export function CompanyPicker({ companyId, onSubmit, onCancel }: OwnProps) { onSubmit(selectedCompany ?? null); } + useEffect(() => { + setSearchFilter(''); + }, [setSearchFilter]); + return ( void; + onCancel?: () => void; +}; + +export function CompanyProgressPicker({ + companyId, + onSubmit, + onCancel, +}: OwnProps) { + const containerRef = useRef(null); + + const { searchFilter, handleSearchFilterChange } = useEntitySelectSearch(); + + const companies = useFilteredSearchCompanyQuery({ + searchFilter, + selectedIds: companyId ? [companyId] : [], + }); + + const [isProgressSelectionUnfolded, setIsProgressSelectionUnfolded] = + useState(false); + + const [selectedPipelineStageId, setSelectedPipelineStageId] = useState< + string | null + >(null); + + useListenClickOutside({ + refs: [containerRef], + callback: (event) => { + event.stopImmediatePropagation(); + event.stopPropagation(); + event.preventDefault(); + + onCancel?.(); + }, + }); + + const theme = useTheme(); + + const [currentPipeline] = useRecoilState(currentPipelineState); + + const currentPipelineStages = useMemo( + () => currentPipeline?.pipelineStages ?? [], + [currentPipeline], + ); + + function handlePipelineStageChange(newPipelineStageId: string) { + setSelectedPipelineStageId(newPipelineStageId); + setIsProgressSelectionUnfolded(false); + } + + async function handleEntitySelected( + selectedCompany: EntityForSelect | null | undefined, + ) { + onSubmit(selectedCompany ?? null, selectedPipelineStageId); + } + + useEffect(() => { + if (currentPipelineStages?.[0]?.id) { + setSelectedPipelineStageId(currentPipelineStages?.[0]?.id); + } + }, [currentPipelineStages]); + + const selectedPipelineStage = useMemo( + () => + currentPipelineStages.find( + (pipelineStage) => pipelineStage.id === selectedPipelineStageId, + ), + [currentPipelineStages, selectedPipelineStageId], + ); + + return ( + + {isProgressSelectionUnfolded ? ( + + {currentPipelineStages.map((pipelineStage, index) => ( + { + handlePipelineStageChange(pipelineStage.id); + }} + > + {pipelineStage.name} + + ))} + + ) : ( + <> + } + onClick={() => setIsProgressSelectionUnfolded(true)} + > + {selectedPipelineStage?.name} + + + + + + + + + )} + + ); +} diff --git a/front/src/modules/companies/components/NewCompanyProgressButton.tsx b/front/src/modules/companies/components/NewCompanyProgressButton.tsx index 09b75293e..5c632cac0 100644 --- a/front/src/modules/companies/components/NewCompanyProgressButton.tsx +++ b/front/src/modules/companies/components/NewCompanyProgressButton.tsx @@ -1,75 +1,45 @@ import { useCallback, useContext, useState } from 'react'; -import { getOperationName } from '@apollo/client/utilities'; -import { useRecoilCallback, useRecoilState } from 'recoil'; -import { v4 as uuidv4 } from 'uuid'; -import { GET_PIPELINE_PROGRESS } from '@/pipeline/graphql/queries/getPipelineProgress'; -import { GET_PIPELINES } from '@/pipeline/graphql/queries/getPipelines'; -import { currentPipelineState } from '@/pipeline/states/currentPipelineState'; import { NewButton } from '@/ui/board/components/NewButton'; import { BoardColumnIdContext } from '@/ui/board/contexts/BoardColumnIdContext'; -import { boardCardIdsByColumnIdFamilyState } from '@/ui/board/states/boardCardIdsByColumnIdFamilyState'; import { SingleEntitySelect } from '@/ui/input/relation-picker/components/SingleEntitySelect'; import { relationPickerSearchFilterScopedState } from '@/ui/input/relation-picker/states/relationPickerSearchFilterScopedState'; import { RelationPickerHotkeyScope } from '@/ui/input/relation-picker/types/RelationPickerHotkeyScope'; +import { useSnackBar } from '@/ui/snack-bar/hooks/useSnackBar'; import { usePreviousHotkeyScope } from '@/ui/utilities/hotkey/hooks/usePreviousHotkeyScope'; +import { RecoilScope } from '@/ui/utilities/recoil-scope/components/RecoilScope'; import { useRecoilScopedState } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedState'; -import { useCreateOneCompanyPipelineProgressMutation } from '~/generated/graphql'; +import { useCreateCompanyProgress } from '../hooks/useCreateCompanyProgress'; import { useFilteredSearchCompanyQuery } from '../hooks/useFilteredSearchCompanyQuery'; export function NewCompanyProgressButton() { const [isCreatingCard, setIsCreatingCard] = useState(false); - const [pipeline] = useRecoilState(currentPipelineState); const pipelineStageId = useContext(BoardColumnIdContext); + const { enqueueSnackBar } = useSnackBar(); + const { goBackToPreviousHotkeyScope, setHotkeyScopeAndMemorizePreviousScope, } = usePreviousHotkeyScope(); - const [createOneCompanyPipelineProgress] = - useCreateOneCompanyPipelineProgressMutation({ - refetchQueries: [ - getOperationName(GET_PIPELINE_PROGRESS) ?? '', - getOperationName(GET_PIPELINES) ?? '', - ], - }); + const createCompanyProgress = useCreateCompanyProgress(); - const handleEntitySelect = useRecoilCallback( - ({ set }) => - async (company: any) => { - if (!company) return; + function handleEntitySelect(company: any) { + setIsCreatingCard(false); + goBackToPreviousHotkeyScope(); - if (!pipelineStageId) throw new Error('pipelineStageId is not defined'); + if (!pipelineStageId) { + enqueueSnackBar('Pipeline stage id is not defined', { + variant: 'error', + }); - setIsCreatingCard(false); + throw new Error('Pipeline stage id is not defined'); + } - goBackToPreviousHotkeyScope(); - - const newUuid = uuidv4(); - - set(boardCardIdsByColumnIdFamilyState(pipelineStageId), (oldValue) => [ - ...oldValue, - newUuid, - ]); - - await createOneCompanyPipelineProgress({ - variables: { - uuid: newUuid, - pipelineStageId: pipelineStageId, - pipelineId: pipeline?.id ?? '', - companyId: company.id ?? '', - }, - }); - }, - [ - goBackToPreviousHotkeyScope, - createOneCompanyPipelineProgress, - pipelineStageId, - pipeline?.id, - ], - ); + createCompanyProgress(company, pipelineStageId); + } const handleNewClick = useCallback(() => { setIsCreatingCard(true); @@ -89,7 +59,7 @@ export function NewCompanyProgressButton() { const companies = useFilteredSearchCompanyQuery({ searchFilter }); return ( - <> + {isCreatingCard ? ( handleEntitySelect(value)} @@ -104,6 +74,6 @@ export function NewCompanyProgressButton() { ) : ( )} - + ); } diff --git a/front/src/modules/companies/hooks/useCreateCompanyProgress.ts b/front/src/modules/companies/hooks/useCreateCompanyProgress.ts new file mode 100644 index 000000000..46135450a --- /dev/null +++ b/front/src/modules/companies/hooks/useCreateCompanyProgress.ts @@ -0,0 +1,47 @@ +import { getOperationName } from '@apollo/client/utilities'; +import { useRecoilCallback, useRecoilState } from 'recoil'; +import { v4 } from 'uuid'; + +import { GET_PIPELINE_PROGRESS } from '@/pipeline/graphql/queries/getPipelineProgress'; +import { GET_PIPELINES } from '@/pipeline/graphql/queries/getPipelines'; +import { currentPipelineState } from '@/pipeline/states/currentPipelineState'; +import { boardCardIdsByColumnIdFamilyState } from '@/ui/board/states/boardCardIdsByColumnIdFamilyState'; +import { useCreateOneCompanyPipelineProgressMutation } from '~/generated/graphql'; + +export function useCreateCompanyProgress() { + const [createOneCompanyPipelineProgress] = + useCreateOneCompanyPipelineProgressMutation({ + refetchQueries: [ + getOperationName(GET_PIPELINE_PROGRESS) ?? '', + getOperationName(GET_PIPELINES) ?? '', + ], + }); + + const [pipeline] = useRecoilState(currentPipelineState); + + return useRecoilCallback( + ({ set }) => + async (companyId: string, pipelineStageId: string) => { + if (!pipeline?.id) { + throw new Error('Pipeline not found'); + } + + const newUuid = v4(); + + set(boardCardIdsByColumnIdFamilyState(pipelineStageId), (oldValue) => [ + ...oldValue, + newUuid, + ]); + + await createOneCompanyPipelineProgress({ + variables: { + uuid: newUuid, + pipelineStageId: pipelineStageId, + pipelineId: pipeline?.id ?? '', + companyId: companyId, + }, + }); + }, + [createOneCompanyPipelineProgress, pipeline], + ); +} diff --git a/front/src/modules/pipeline/components/PipelineAddButton.tsx b/front/src/modules/pipeline/components/PipelineAddButton.tsx new file mode 100644 index 000000000..b36fcf84c --- /dev/null +++ b/front/src/modules/pipeline/components/PipelineAddButton.tsx @@ -0,0 +1,79 @@ +import { CompanyProgressPicker } from '@/companies/components/CompanyProgressPicker'; +import { useCreateCompanyProgress } from '@/companies/hooks/useCreateCompanyProgress'; +import { PageHotkeyScope } from '@/types/PageHotkeyScope'; +import { IconButton } from '@/ui/button/components/IconButton'; +import { DropdownButton } from '@/ui/dropdown/components/DropdownButton'; +import { useDropdownButton } from '@/ui/dropdown/hooks/useDropdownButton'; +import { IconPlus } from '@/ui/icon/index'; +import { EntityForSelect } from '@/ui/input/relation-picker/types/EntityForSelect'; +import { RelationPickerHotkeyScope } from '@/ui/input/relation-picker/types/RelationPickerHotkeyScope'; +import { useSnackBar } from '@/ui/snack-bar/hooks/useSnackBar'; + +export function PipelineAddButton() { + const { enqueueSnackBar } = useSnackBar(); + + const { closeDropdownButton } = useDropdownButton(); + + const createCompanyProgress = useCreateCompanyProgress(); + + function handleCompanySelected( + selectedCompany: EntityForSelect | null, + selectedPipelineStageId: string | null, + ) { + if (!selectedCompany?.id) { + enqueueSnackBar( + 'There was a problem with the company selection, please retry.', + { + variant: 'error', + }, + ); + + console.error( + 'There was a problem with the company selection, please retry.', + ); + return; + } + + if (!selectedPipelineStageId) { + enqueueSnackBar( + 'There was a problem with the pipeline stage selection, please retry.', + { + variant: 'error', + }, + ); + + console.error('There was a problem with the pipeline stage selection.'); + return; + } + + createCompanyProgress(selectedCompany.id, selectedPipelineStageId); + } + + return ( + } + size="large" + data-testid="add-company-progress-button" + textColor={'secondary'} + variant="border" + /> + } + dropdownComponents={ + + } + hotkey={{ + key: 'c', + scope: PageHotkeyScope.OpportunitiesPage, + }} + dropdownScopeToSet={{ + scope: RelationPickerHotkeyScope.RelationPicker, + }} + /> + ); +} diff --git a/front/src/modules/ui/button/components/DropdownButton.tsx b/front/src/modules/ui/button/components/DropdownButton_Deprecated.tsx similarity index 98% rename from front/src/modules/ui/button/components/DropdownButton.tsx rename to front/src/modules/ui/button/components/DropdownButton_Deprecated.tsx index 6721d22b7..9814518c0 100644 --- a/front/src/modules/ui/button/components/DropdownButton.tsx +++ b/front/src/modules/ui/button/components/DropdownButton_Deprecated.tsx @@ -76,7 +76,7 @@ const StyledDropdownMenu = styled.div` width: 100%; `; -export function DropdownButton({ +export function DropdownButton_Deprecated({ options, selectedOptionKey, onSelection, diff --git a/front/src/modules/ui/dropdown/components/DropdownButton.tsx b/front/src/modules/ui/dropdown/components/DropdownButton.tsx new file mode 100644 index 000000000..d26639b69 --- /dev/null +++ b/front/src/modules/ui/dropdown/components/DropdownButton.tsx @@ -0,0 +1,58 @@ +import { Keys } from 'react-hotkeys-hook'; +import styled from '@emotion/styled'; +import { flip, offset, useFloating } from '@floating-ui/react'; + +import { HotkeyScope } from '@/ui/utilities/hotkey/types/HotkeyScope'; + +import { useDropdownButton } from '../hooks/useDropdownButton'; + +import { HotkeyEffect } from './HotkeyEffect'; + +const StyledContainer = styled.div` + position: relative; + z-index: 100; +`; + +type OwnProps = { + buttonComponents: JSX.Element | JSX.Element[]; + dropdownComponents: JSX.Element | JSX.Element[]; + hotkey?: { + key: Keys; + scope: string; + }; + dropdownScopeToSet?: HotkeyScope; +}; + +export function DropdownButton({ + buttonComponents, + dropdownComponents, + hotkey, + dropdownScopeToSet, +}: OwnProps) { + const { isDropdownButtonOpen, toggleDropdownButton } = useDropdownButton(); + + const { refs, floatingStyles } = useFloating({ + placement: 'bottom-end', + middleware: [flip(), offset()], + }); + + function handleButtonClick() { + toggleDropdownButton(dropdownScopeToSet); + } + + return ( + + {hotkey && ( + + )} +
+ {buttonComponents} +
+ {isDropdownButtonOpen && ( +
+ {dropdownComponents} +
+ )} +
+ ); +} diff --git a/front/src/modules/ui/dropdown/components/HotkeyEffect.tsx b/front/src/modules/ui/dropdown/components/HotkeyEffect.tsx new file mode 100644 index 000000000..2d9d2018f --- /dev/null +++ b/front/src/modules/ui/dropdown/components/HotkeyEffect.tsx @@ -0,0 +1,19 @@ +import { Keys } from 'react-hotkeys-hook'; + +import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys'; + +type OwnProps = { + hotkey: { + key: Keys; + scope: string; + }; + onHotkeyTriggered: () => void; +}; + +export function HotkeyEffect({ hotkey, onHotkeyTriggered }: OwnProps) { + useScopedHotkeys(hotkey.key, () => onHotkeyTriggered(), hotkey.scope, [ + onHotkeyTriggered, + ]); + + return <>; +} diff --git a/front/src/modules/ui/dropdown/hooks/useDropdownButton.ts b/front/src/modules/ui/dropdown/hooks/useDropdownButton.ts new file mode 100644 index 000000000..e92f1f2eb --- /dev/null +++ b/front/src/modules/ui/dropdown/hooks/useDropdownButton.ts @@ -0,0 +1,47 @@ +import { usePreviousHotkeyScope } from '@/ui/utilities/hotkey/hooks/usePreviousHotkeyScope'; +import { HotkeyScope } from '@/ui/utilities/hotkey/types/HotkeyScope'; +import { useRecoilScopedState } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedState'; + +import { isDropdownButtonOpenScopedState } from '../states/isDropdownButtonOpenScopedState'; + +export function useDropdownButton() { + const { + setHotkeyScopeAndMemorizePreviousScope, + goBackToPreviousHotkeyScope, + } = usePreviousHotkeyScope(); + + const [isDropdownButtonOpen, setIsDropdownButtonOpen] = useRecoilScopedState( + isDropdownButtonOpenScopedState, + ); + + function closeDropdownButton() { + goBackToPreviousHotkeyScope(); + setIsDropdownButtonOpen(false); + } + + function openDropdownButton(hotkeyScopeToSet?: HotkeyScope) { + setIsDropdownButtonOpen(true); + + if (hotkeyScopeToSet) { + setHotkeyScopeAndMemorizePreviousScope( + hotkeyScopeToSet.scope, + hotkeyScopeToSet.customScopes, + ); + } + } + + function toggleDropdownButton(hotkeyScopeToSet?: HotkeyScope) { + if (isDropdownButtonOpen) { + closeDropdownButton(); + } else { + openDropdownButton(hotkeyScopeToSet); + } + } + + return { + isDropdownButtonOpen, + closeDropdownButton, + toggleDropdownButton, + openDropdownButton, + }; +} diff --git a/front/src/modules/ui/dropdown/states/isDropdownButtonOpenScopedState.ts b/front/src/modules/ui/dropdown/states/isDropdownButtonOpenScopedState.ts new file mode 100644 index 000000000..a46b7befe --- /dev/null +++ b/front/src/modules/ui/dropdown/states/isDropdownButtonOpenScopedState.ts @@ -0,0 +1,6 @@ +import { atomFamily } from 'recoil'; + +export const isDropdownButtonOpenScopedState = atomFamily({ + key: 'isDropdownButtonOpenScopedState', + default: false, +}); diff --git a/front/src/modules/ui/filter-n-sort/components/FilterDropdownButton.tsx b/front/src/modules/ui/filter-n-sort/components/FilterDropdownButton.tsx index e09675afd..2d560d9d5 100644 --- a/front/src/modules/ui/filter-n-sort/components/FilterDropdownButton.tsx +++ b/front/src/modules/ui/filter-n-sort/components/FilterDropdownButton.tsx @@ -27,8 +27,11 @@ export function FilterDropdownButton({ availableFiltersScopedState, context, ); - return availableFilters.length === 1 && - availableFilters[0].type === 'entity' ? ( + + const hasOnlyOneEntityFilter = + availableFilters.length === 1 && availableFilters[0].type === 'entity'; + + return hasOnlyOneEntityFilter ? ( { + setSearchFilter(''); + }, [setSearchFilter]); + return { searchFilter, handleSearchFilterChange, diff --git a/front/src/modules/ui/layout/components/PageBody.tsx b/front/src/modules/ui/layout/components/PageBody.tsx new file mode 100644 index 000000000..bc8e1757c --- /dev/null +++ b/front/src/modules/ui/layout/components/PageBody.tsx @@ -0,0 +1,15 @@ +import { PAGE_BAR_MIN_HEIGHT } from '../page-bar/components/PageBar'; + +import { RightDrawerContainer } from './RightDrawerContainer'; + +type OwnProps = { + children: JSX.Element | JSX.Element[]; +}; + +export function PageBody({ children }: OwnProps) { + return ( + + {children} + + ); +} diff --git a/front/src/modules/ui/layout/components/PageContainer.tsx b/front/src/modules/ui/layout/components/PageContainer.tsx new file mode 100644 index 000000000..5116513ba --- /dev/null +++ b/front/src/modules/ui/layout/components/PageContainer.tsx @@ -0,0 +1,15 @@ +import styled from '@emotion/styled'; + +type OwnProps = { + children: JSX.Element | JSX.Element[]; +}; + +const StyledContainer = styled.div` + display: flex; + flex-direction: column; + width: 100%; +`; + +export function PageContainer({ children }: OwnProps) { + return {children}; +} diff --git a/front/src/modules/ui/layout/components/PageHeader.tsx b/front/src/modules/ui/layout/components/PageHeader.tsx new file mode 100644 index 000000000..7005cb508 --- /dev/null +++ b/front/src/modules/ui/layout/components/PageHeader.tsx @@ -0,0 +1,109 @@ +import { ReactNode, useCallback } from 'react'; +import { useNavigate } from 'react-router-dom'; +import styled from '@emotion/styled'; +import { useRecoilValue } from 'recoil'; + +import { IconButton } from '@/ui/button/components/IconButton'; +import { IconChevronLeft } from '@/ui/icon/index'; +import NavCollapseButton from '@/ui/navbar/components/NavCollapseButton'; +import { navbarIconSize } from '@/ui/navbar/constants'; +import { OverflowingTextWithTooltip } from '@/ui/tooltip/OverflowingTextWithTooltip'; +import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile'; + +import { isNavbarOpenedState } from '../states/isNavbarOpenedState'; + +export const PAGE_BAR_MIN_HEIGHT = 40; + +const StyledTopBarContainer = styled.div` + align-items: center; + background: ${({ theme }) => theme.background.noisy}; + color: ${({ theme }) => theme.font.color.primary}; + display: flex; + flex-direction: row; + font-size: ${({ theme }) => theme.font.size.lg}; + justify-content: space-between; + min-height: ${PAGE_BAR_MIN_HEIGHT}px; + padding: ${({ theme }) => theme.spacing(2)}; + padding-left: 0; + padding-right: ${({ theme }) => theme.spacing(3)}; +`; + +const StyledLeftContainer = styled.div` + align-items: center; + display: flex; + flex-direction: row; + width: 100%; +`; + +const StyledTitleContainer = styled.div` + display: flex; + font-size: ${({ theme }) => theme.font.size.md}; + margin-left: ${({ theme }) => theme.spacing(1)}; + max-width: 50%; +`; + +const StyledTopBarButtonContainer = styled.div` + margin-right: ${({ theme }) => theme.spacing(1)}; +`; + +const StyledBackIconButton = styled(IconButton)` + margin-right: ${({ theme }) => theme.spacing(1)}; +`; + +const StyledTopBarIconStyledTitleContainer = styled.div` + align-items: center; + display: flex; + flex-direction: row; + padding-left: ${({ theme }) => theme.spacing(2)}; + width: 100%; +`; + +const StyledPageActionContainer = styled.div` + display: inline-flex; + gap: ${({ theme }) => theme.spacing(2)}; +`; + +type OwnProps = { + title: string; + hasBackButton?: boolean; + icon: ReactNode; + children: JSX.Element | JSX.Element[]; +}; + +export function PageHeader({ title, hasBackButton, icon, children }: OwnProps) { + const navigate = useNavigate(); + const navigateBack = useCallback(() => navigate(-1), [navigate]); + + const isNavbarOpened = useRecoilValue(isNavbarOpenedState); + + const iconSize = useIsMobile() + ? navbarIconSize.mobile + : navbarIconSize.desktop; + + return ( + + + {!isNavbarOpened && ( + + + + )} + {hasBackButton && ( + + } + onClick={navigateBack} + /> + + )} + + {icon} + + + + + + {children} + + ); +} diff --git a/front/src/modules/ui/utilities/hotkey/hooks/usePreviousHotkeyScope.ts b/front/src/modules/ui/utilities/hotkey/hooks/usePreviousHotkeyScope.ts index b4df92490..48f30a4ce 100644 --- a/front/src/modules/ui/utilities/hotkey/hooks/usePreviousHotkeyScope.ts +++ b/front/src/modules/ui/utilities/hotkey/hooks/usePreviousHotkeyScope.ts @@ -1,38 +1,46 @@ -import { useState } from 'react'; import { useRecoilCallback } from 'recoil'; import { currentHotkeyScopeState } from '../states/internal/currentHotkeyScopeState'; +import { previousHotkeyScopeState } from '../states/internal/previousHotkeyScopeState'; import { CustomHotkeyScopes } from '../types/CustomHotkeyScope'; -import { HotkeyScope } from '../types/HotkeyScope'; import { useSetHotkeyScope } from './useSetHotkeyScope'; export function usePreviousHotkeyScope() { - const [previousHotkeyScope, setPreviousHotkeyScope] = - useState(); - const setHotkeyScope = useSetHotkeyScope(); - function goBackToPreviousHotkeyScope() { - if (previousHotkeyScope) { - setHotkeyScope( - previousHotkeyScope.scope, - previousHotkeyScope.customScopes, - ); - } - } + const goBackToPreviousHotkeyScope = useRecoilCallback( + ({ snapshot, set }) => + () => { + const previousHotkeyScope = snapshot + .getLoadable(previousHotkeyScopeState) + .valueOrThrow(); + + if (!previousHotkeyScope) { + return; + } + + setHotkeyScope( + previousHotkeyScope.scope, + previousHotkeyScope.customScopes, + ); + + set(previousHotkeyScopeState, null); + }, + [], + ); const setHotkeyScopeAndMemorizePreviousScope = useRecoilCallback( - ({ snapshot }) => + ({ snapshot, set }) => (scope: string, customScopes?: CustomHotkeyScopes) => { const currentHotkeyScope = snapshot .getLoadable(currentHotkeyScopeState) .valueOrThrow(); setHotkeyScope(scope, customScopes); - setPreviousHotkeyScope(currentHotkeyScope); + set(previousHotkeyScopeState, currentHotkeyScope); }, - [setPreviousHotkeyScope], + [], ); return { diff --git a/front/src/modules/ui/utilities/hotkey/states/internal/previousHotkeyScopeState.ts b/front/src/modules/ui/utilities/hotkey/states/internal/previousHotkeyScopeState.ts new file mode 100644 index 000000000..fd076d8e2 --- /dev/null +++ b/front/src/modules/ui/utilities/hotkey/states/internal/previousHotkeyScopeState.ts @@ -0,0 +1,8 @@ +import { atom } from 'recoil'; + +import { HotkeyScope } from '../../types/HotkeyScope'; + +export const previousHotkeyScopeState = atom({ + key: 'previousHotkeyScopeState', + default: null, +}); diff --git a/front/src/modules/ui/utilities/recoil-scope/components/RecoilScope.tsx b/front/src/modules/ui/utilities/recoil-scope/components/RecoilScope.tsx index 01cfa9dfc..d568b4e69 100644 --- a/front/src/modules/ui/utilities/recoil-scope/components/RecoilScope.tsx +++ b/front/src/modules/ui/utilities/recoil-scope/components/RecoilScope.tsx @@ -12,7 +12,7 @@ export function RecoilScope({ scopeId?: string; SpecificContext?: Context; }) { - const currentScopeId = useRef(scopeId || v4()); + const currentScopeId = useRef(scopeId ?? v4()); return SpecificContext ? ( diff --git a/front/src/pages/opportunities/Opportunities.tsx b/front/src/pages/opportunities/Opportunities.tsx index ec73d7719..0cd3fbc02 100644 --- a/front/src/pages/opportunities/Opportunities.tsx +++ b/front/src/pages/opportunities/Opportunities.tsx @@ -3,14 +3,17 @@ import { useTheme } from '@emotion/react'; import { HooksCompanyBoard } from '@/companies/components/HooksCompanyBoard'; import { CompanyBoardRecoilScopeContext } from '@/companies/states/recoil-scope-contexts/CompanyBoardRecoilScopeContext'; +import { PipelineAddButton } from '@/pipeline/components/PipelineAddButton'; import { EntityBoard } from '@/ui/board/components/EntityBoard'; import { EntityBoardActionBar } from '@/ui/board/components/EntityBoardActionBar'; import { EntityBoardContextMenu } from '@/ui/board/components/EntityBoardContextMenu'; import { BoardOptionsContext } from '@/ui/board/contexts/BoardOptionsContext'; import { reduceSortsToOrderBy } from '@/ui/filter-n-sort/helpers'; import { SelectedSortType } from '@/ui/filter-n-sort/types/interface'; -import { IconTargetArrow } from '@/ui/icon/index'; -import { WithTopBarContainer } from '@/ui/layout/components/WithTopBarContainer'; +import { IconTargetArrow } from '@/ui/icon'; +import { PageBody } from '@/ui/layout/components/PageBody'; +import { PageContainer } from '@/ui/layout/components/PageContainer'; +import { PageHeader } from '@/ui/layout/components/PageHeader'; import { RecoilScope } from '@/ui/utilities/recoil-scope/components/RecoilScope'; import { PipelineProgressOrderByWithRelationInput, @@ -64,22 +67,29 @@ export function Opportunities() { } return ( - } - > - - - - - - + + } + > + + - - + + + + + + + + + + + + ); } diff --git a/front/src/pages/opportunities/__stories__/Opportunities.stories.tsx b/front/src/pages/opportunities/__stories__/Opportunities.stories.tsx index 6fef38518..b1b2e0ccc 100644 --- a/front/src/pages/opportunities/__stories__/Opportunities.stories.tsx +++ b/front/src/pages/opportunities/__stories__/Opportunities.stories.tsx @@ -31,3 +31,55 @@ export const Default: Story = { await canvas.findByText('All opportunities'); }, }; + +export const AddCompanyFromHeader: Story = { + play: async ({ canvasElement, step }) => { + const canvas = within(canvasElement); + + await step('Click on the add company button', async () => { + const button = await canvas.findByTestId('add-company-progress-button'); + + await button.click(); + + await canvas.findByText('Algolia'); + }); + + await step('Change pipeline stage', async () => { + const dropdownMenu = within( + await canvas.findByTestId('company-progress-dropdown-menu'), + ); + + const pipelineStageButton = await canvas.findByTestId( + 'selected-pipeline-stage', + ); + + await pipelineStageButton.click(); + + const menuItem1 = await canvas.findByTestId('select-pipeline-stage-1'); + + await menuItem1.click(); + + await dropdownMenu.findByText('Screening'); + }); + + await step('Change pipeline stage', async () => { + const dropdownMenu = within( + await canvas.findByTestId('company-progress-dropdown-menu'), + ); + + const pipelineStageButton = await canvas.findByTestId( + 'selected-pipeline-stage', + ); + + await pipelineStageButton.click(); + + const menuItem1 = await canvas.findByTestId('select-pipeline-stage-1'); + + await menuItem1.click(); + + await dropdownMenu.findByText('Screening'); + }); + + // TODO: mock add company mutation and add step for company creation + }, +}; diff --git a/front/src/pages/opportunities/opportunitiesBoardOptions.tsx b/front/src/pages/opportunities/opportunitiesBoardOptions.tsx index d75dd6d47..3685f8b12 100644 --- a/front/src/pages/opportunities/opportunitiesBoardOptions.tsx +++ b/front/src/pages/opportunities/opportunitiesBoardOptions.tsx @@ -1,17 +1,12 @@ import { CompanyBoardCard } from '@/companies/components/CompanyBoardCard'; import { NewCompanyProgressButton } from '@/companies/components/NewCompanyProgressButton'; import { BoardOptions } from '@/ui/board/types/BoardOptions'; -import { RecoilScope } from '@/ui/utilities/recoil-scope/components/RecoilScope'; import { opportunitiesFilters } from './opportunities-filters'; import { opportunitiesSorts } from './opportunities-sorts'; export const opportunitiesBoardOptions: BoardOptions = { - newCardComponent: ( - - - - ), + newCardComponent: , cardComponent: , filters: opportunitiesFilters, sorts: opportunitiesSorts, diff --git a/front/src/sync-hooks/AuthAutoRouter.tsx b/front/src/sync-hooks/PageChangeEffect.tsx similarity index 98% rename from front/src/sync-hooks/AuthAutoRouter.tsx rename to front/src/sync-hooks/PageChangeEffect.tsx index ac9d5d98d..71fe89815 100644 --- a/front/src/sync-hooks/AuthAutoRouter.tsx +++ b/front/src/sync-hooks/PageChangeEffect.tsx @@ -21,7 +21,8 @@ import { ActivityType } from '~/generated/graphql'; import { useIsMatchingLocation } from '../hooks/useIsMatchingLocation'; -export function AuthAutoRouter() { +// TODO: break down into smaller functions and / or hooks +export function PageChangeEffect() { const navigate = useNavigate(); const isMatchingLocation = useIsMatchingLocation(); const { enqueueSnackBar } = useSnackBar();