From 33f23cc0c1ed1ee3c78464dcd42991d878cf97de Mon Sep 17 00:00:00 2001 From: Jaanus Sellin Date: Mon, 5 May 2025 10:15:43 +0300 Subject: [PATCH] feat: recently used segments (#9881) --- .../FeatureStrategySegment.tsx | 2 + .../RecentlyUsedSegments.tsx | 68 +++++++++++++ .../useRecentlyUsedSegments.test.tsx | 99 +++++++++++++++++++ .../useRecentlyUsedSegments.ts | 28 ++++++ .../useFeatureStrategyApi.ts | 30 +++--- 5 files changed, 215 insertions(+), 12 deletions(-) create mode 100644 frontend/src/component/feature/FeatureStrategy/FeatureStrategySegment/RecentlyUsedSegments/RecentlyUsedSegments.tsx create mode 100644 frontend/src/component/feature/FeatureStrategy/FeatureStrategySegment/RecentlyUsedSegments/useRecentlyUsedSegments.test.tsx create mode 100644 frontend/src/component/feature/FeatureStrategy/FeatureStrategySegment/RecentlyUsedSegments/useRecentlyUsedSegments.ts diff --git a/frontend/src/component/feature/FeatureStrategy/FeatureStrategySegment/FeatureStrategySegment.tsx b/frontend/src/component/feature/FeatureStrategy/FeatureStrategySegment/FeatureStrategySegment.tsx index 5d668de868..362763093f 100644 --- a/frontend/src/component/feature/FeatureStrategy/FeatureStrategySegment/FeatureStrategySegment.tsx +++ b/frontend/src/component/feature/FeatureStrategy/FeatureStrategySegment/FeatureStrategySegment.tsx @@ -11,6 +11,7 @@ import { useSegmentLimits } from 'hooks/api/getters/useSegmentLimits/useSegmentL import { Box, styled, Typography } from '@mui/material'; import { HelpIcon } from 'component/common/HelpIcon/HelpIcon'; import { useUiFlag } from 'hooks/useUiFlag'; +import { RecentlyUsedSegments } from './RecentlyUsedSegments/RecentlyUsedSegments'; interface IFeatureStrategySegmentProps { segments: ISegment[]; @@ -105,6 +106,7 @@ export const FeatureStrategySegment = ({ segments={selectedSegments} setSegments={setSelectedSegments} /> + ); }; diff --git a/frontend/src/component/feature/FeatureStrategy/FeatureStrategySegment/RecentlyUsedSegments/RecentlyUsedSegments.tsx b/frontend/src/component/feature/FeatureStrategy/FeatureStrategySegment/RecentlyUsedSegments/RecentlyUsedSegments.tsx new file mode 100644 index 0000000000..96ead01def --- /dev/null +++ b/frontend/src/component/feature/FeatureStrategy/FeatureStrategySegment/RecentlyUsedSegments/RecentlyUsedSegments.tsx @@ -0,0 +1,68 @@ +import { styled, Typography } from '@mui/material'; +import { useRecentlyUsedSegments } from './useRecentlyUsedSegments'; +import type { ISegment } from 'interfaces/segment'; +import { FeatureStrategySegmentChip } from '../FeatureStrategySegmentChip'; +import { useSegments } from 'hooks/api/getters/useSegments/useSegments'; +import { useUiFlag } from 'hooks/useUiFlag'; + +type RecentlyUsedSegmentsProps = { + setSegments?: React.Dispatch>; +}; + +const StyledContainer = styled('div')(({ theme }) => ({ + marginTop: theme.spacing(3), +})); + +const StyledHeader = styled(Typography)(({ theme }) => ({ + fontSize: theme.fontSizes.smallerBody, + color: theme.palette.text.secondary, + marginBottom: theme.spacing(1), +})); + +const StyledSegmentsContainer = styled('div')(({ theme }) => ({ + display: 'flex', + flexWrap: 'wrap', + gap: theme.spacing(1), +})); + +export const RecentlyUsedSegments = ({ + setSegments, +}: RecentlyUsedSegmentsProps) => { + const { items: recentlyUsedSegmentIds } = useRecentlyUsedSegments(); + const { segments: allSegments } = useSegments(); + const addEditStrategyEnabled = useUiFlag('addEditStrategy'); + + if ( + !addEditStrategyEnabled || + recentlyUsedSegmentIds.length === 0 || + !setSegments || + !allSegments + ) { + return null; + } + + const segmentObjects = recentlyUsedSegmentIds + .map((id) => allSegments.find((segment) => segment.id === id)) + .filter((segment) => segment !== undefined) as ISegment[]; + + if (segmentObjects.length === 0) { + return null; + } + + return ( + + Recently used segments + + {segmentObjects.map((segment) => ( + {}} + /> + ))} + + + ); +}; diff --git a/frontend/src/component/feature/FeatureStrategy/FeatureStrategySegment/RecentlyUsedSegments/useRecentlyUsedSegments.test.tsx b/frontend/src/component/feature/FeatureStrategy/FeatureStrategySegment/RecentlyUsedSegments/useRecentlyUsedSegments.test.tsx new file mode 100644 index 0000000000..b624240568 --- /dev/null +++ b/frontend/src/component/feature/FeatureStrategy/FeatureStrategySegment/RecentlyUsedSegments/useRecentlyUsedSegments.test.tsx @@ -0,0 +1,99 @@ +import { useRecentlyUsedSegments } from './useRecentlyUsedSegments'; +import { renderHook, act } from '@testing-library/react'; + +describe('useRecentlyUsedSegments', () => { + beforeEach(() => { + window.localStorage.clear(); + }); + + it('should initialize with empty array when no items in localStorage', () => { + const { result } = renderHook(() => useRecentlyUsedSegments()); + + expect(result.current.items).toEqual([]); + }); + + it('should initialize with initial items if provided', () => { + const initialItems = [1]; + const { result } = renderHook(() => + useRecentlyUsedSegments(initialItems), + ); + + expect(result.current.items).toEqual(initialItems); + }); + + it('should add new items to the beginning of the list', () => { + const { result } = renderHook(() => useRecentlyUsedSegments()); + + act(() => { + result.current.addItem(1); + }); + expect(result.current.items[0]).toBe(1); + + act(() => { + result.current.addItem(2); + }); + expect(result.current.items[0]).toBe(2); + expect(result.current.items[1]).toBe(1); + }); + + it('should handle array of segment IDs', () => { + const { result } = renderHook(() => useRecentlyUsedSegments()); + + act(() => { + result.current.addItem([1, 2, 3]); + }); + + expect(result.current.items.length).toBe(3); + expect(result.current.items[0]).toBe(3); + expect(result.current.items[1]).toBe(2); + expect(result.current.items[2]).toBe(1); + }); + + it('should limit stored items to maximum of 3', () => { + const { result } = renderHook(() => useRecentlyUsedSegments()); + + act(() => { + result.current.addItem(1); + result.current.addItem(2); + result.current.addItem(3); + result.current.addItem(4); + }); + + expect(result.current.items.length).toBe(3); + expect(result.current.items[0]).toBe(4); + expect(result.current.items[1]).toBe(3); + expect(result.current.items[2]).toBe(2); + }); + + it('should not add duplicate segment IDs', () => { + const { result } = renderHook(() => useRecentlyUsedSegments()); + + act(() => { + result.current.addItem(1); + result.current.addItem(2); + }); + expect(result.current.items.length).toBe(2); + + act(() => { + result.current.addItem(1); + }); + + expect(result.current.items.length).toBe(2); + expect(result.current.items[0]).toBe(1); + expect(result.current.items[1]).toBe(2); + }); + + it('should persist items to localStorage', () => { + const { result } = renderHook(() => useRecentlyUsedSegments()); + + act(() => { + result.current.addItem(1); + }); + + const { result: newResult } = renderHook(() => + useRecentlyUsedSegments(), + ); + + expect(newResult.current.items[0]).toBe(1); + }); +}); diff --git a/frontend/src/component/feature/FeatureStrategy/FeatureStrategySegment/RecentlyUsedSegments/useRecentlyUsedSegments.ts b/frontend/src/component/feature/FeatureStrategy/FeatureStrategySegment/RecentlyUsedSegments/useRecentlyUsedSegments.ts new file mode 100644 index 0000000000..9ed16d9ddd --- /dev/null +++ b/frontend/src/component/feature/FeatureStrategy/FeatureStrategySegment/RecentlyUsedSegments/useRecentlyUsedSegments.ts @@ -0,0 +1,28 @@ +import { useLocalStorageState } from 'hooks/useLocalStorageState'; + +export const useRecentlyUsedSegments = (initialItems: number[] = []) => { + const [items, setItems] = useLocalStorageState( + 'recently-used-segments', + initialItems, + ); + + const addItem = (newItem: number | number[]) => { + setItems((prevItems) => { + const itemsToAdd = Array.isArray(newItem) ? newItem : [newItem]; + + let updatedItems = [...prevItems]; + itemsToAdd.forEach((id) => { + updatedItems = updatedItems.filter( + (existingId) => existingId !== id, + ); + updatedItems = [id, ...updatedItems]; + }); + return updatedItems.slice(0, 3); + }); + }; + + return { + items, + addItem, + }; +}; diff --git a/frontend/src/hooks/api/actions/useFeatureStrategyApi/useFeatureStrategyApi.ts b/frontend/src/hooks/api/actions/useFeatureStrategyApi/useFeatureStrategyApi.ts index ed8affa633..1d12cf93b1 100644 --- a/frontend/src/hooks/api/actions/useFeatureStrategyApi/useFeatureStrategyApi.ts +++ b/frontend/src/hooks/api/actions/useFeatureStrategyApi/useFeatureStrategyApi.ts @@ -5,6 +5,7 @@ import type { } from 'interfaces/strategy'; import useAPI from '../useApi/useApi'; import { useRecentlyUsedConstraints } from 'component/feature/FeatureStrategy/FeatureStrategyConstraints/RecentlyUsedConstraints/useRecentlyUsedConstraints'; +import { useRecentlyUsedSegments } from 'component/feature/FeatureStrategy/FeatureStrategySegment/RecentlyUsedSegments/useRecentlyUsedSegments'; import { useUiFlag } from 'hooks/useUiFlag'; const useFeatureStrategyApi = () => { @@ -14,6 +15,7 @@ const useFeatureStrategyApi = () => { const { addItem: addToRecentlyUsedConstraints } = useRecentlyUsedConstraints(); + const { addItem: addToRecentlyUsedSegments } = useRecentlyUsedSegments(); const addEditStrategyEnabled = useUiFlag('addEditStrategy'); const addStrategyToFeature = async ( @@ -30,12 +32,14 @@ const useFeatureStrategyApi = () => { ); const result = await makeRequest(req.caller, req.id); - if ( - addEditStrategyEnabled && - payload.constraints && - payload.constraints.length > 0 - ) { - addToRecentlyUsedConstraints(payload.constraints); + if (addEditStrategyEnabled) { + if (payload.constraints && payload.constraints.length > 0) { + addToRecentlyUsedConstraints(payload.constraints); + } + + if (payload.segments && payload.segments.length > 0) { + addToRecentlyUsedSegments(payload.segments); + } } return result.json(); @@ -71,12 +75,14 @@ const useFeatureStrategyApi = () => { ); await makeRequest(req.caller, req.id); - if ( - addEditStrategyEnabled && - payload.constraints && - payload.constraints.length > 0 - ) { - addToRecentlyUsedConstraints(payload.constraints); + if (addEditStrategyEnabled) { + if (payload.constraints && payload.constraints.length > 0) { + addToRecentlyUsedConstraints(payload.constraints); + } + + if (payload.segments && payload.segments.length > 0) { + addToRecentlyUsedSegments(payload.segments); + } } };