Skip to content

Commit 11717de

Browse files
andrevitalbmbanting
authored andcommitted
feat: Brand intro banner (M2-9068) (#629)
1 parent ce1bdac commit 11717de

28 files changed

+674
-36
lines changed

src/app/providers/theme-provider.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ const theme = createTheme({
4848
},
4949
'.MuiAlert-icon': {
5050
marginLeft: 'auto',
51+
opacity: 1,
5152
},
5253
'.MuiAlert-message': {
5354
padding: 0,

src/app/store.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { PreloadedState, combineReducers, configureStore, Reducer } from '@reduxjs/toolkit';
1+
import { combineReducers, configureStore, PreloadedState, Reducer } from '@reduxjs/toolkit';
22
import { RenderOptions } from '@testing-library/react';
33
import persistReducer from 'redux-persist/es/persistReducer';
44
import persistStore from 'redux-persist/es/persistStore';
@@ -8,6 +8,7 @@ import sessionStorage from 'redux-persist/lib/storage/session';
88
import { appletModel } from '~/entities/applet';
99
import { bannerModel } from '~/entities/banner';
1010
import { BannersStore } from '~/entities/banner/model';
11+
import { defaultBannersModel } from '~/entities/defaultBanners';
1112
import { userModel } from '~/entities/user';
1213
import { AutoCompletionModel } from '~/features/AutoCompletion';
1314
import { RootState } from '~/shared/utils';
@@ -30,6 +31,7 @@ export const rootReducer = combineReducers({
3031
bannerPersistConfig,
3132
bannerModel.reducer,
3233
) as unknown as Reducer<BannersStore>,
34+
defaultBanners: defaultBannersModel.reducer,
3335
autoCompletion: AutoCompletionModel.reducer,
3436
});
3537

src/assets/curious_icon--white.png

305 KB
Loading

src/entities/banner/model/hooks/useBanners.ts

Lines changed: 15 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { BannerType, actions } from '..';
1+
import { BannerOrder, BannerType, actions } from '..';
22

33
import { BannerProps } from '~/shared/ui/Banners/Banner';
44
import { useAppDispatch } from '~/shared/utils';
@@ -11,31 +11,35 @@ export const useBanners = () => {
1111
const dispatch = useAppDispatch();
1212

1313
/** Displays a banner having the given key, and content text or BannerProps */
14-
const addBanner = (key: BannerType, banner: BannerContent) => {
14+
const addBanner = (
15+
key: BannerType,
16+
banner: BannerContent,
17+
order: BannerOrder = BannerOrder.Default,
18+
) => {
1519
const bannerProps =
1620
banner === null || typeof banner === 'string' ? { children: banner } : banner;
1721

18-
dispatch(actions.addBanner({ key, bannerProps }));
22+
dispatch(actions.addBanner({ key, bannerProps, order }));
1923
};
2024

2125
/** Shorthand function for adding a success banner */
22-
const addSuccessBanner = (banner: BannerContent) => {
23-
addBanner('SuccessBanner', banner);
26+
const addSuccessBanner = (banner: BannerContent, order: BannerOrder = BannerOrder.Default) => {
27+
addBanner('SuccessBanner', banner, order);
2428
};
2529

2630
/** Shorthand function for adding an error banner */
27-
const addErrorBanner = (banner: BannerContent) => {
28-
addBanner('ErrorBanner', banner);
31+
const addErrorBanner = (banner: BannerContent, order: BannerOrder = BannerOrder.Default) => {
32+
addBanner('ErrorBanner', banner, order);
2933
};
3034

3135
/** Shorthand function for adding a warning banner */
32-
const addWarningBanner = (banner: BannerContent) => {
33-
addBanner('WarningBanner', banner);
36+
const addWarningBanner = (banner: BannerContent, order: BannerOrder = BannerOrder.Default) => {
37+
addBanner('WarningBanner', banner, order);
3438
};
3539

3640
/** Shorthand function for adding an info banner */
37-
const addInfoBanner = (banner: BannerContent) => {
38-
addBanner('InfoBanner', banner);
41+
const addInfoBanner = (banner: BannerContent, order: BannerOrder = BannerOrder.Default) => {
42+
addBanner('InfoBanner', banner, order);
3943
};
4044

4145
const removeBanner = (key: BannerType) => {

src/entities/banner/model/types.ts

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,22 @@
11
import { BannerProps } from '~/shared/ui/Banners/Banner';
22

3-
export const Banners = ['SuccessBanner', 'ErrorBanner', 'WarningBanner', 'InfoBanner'] as const;
3+
export const Banners = [
4+
'SuccessBanner',
5+
'ErrorBanner',
6+
'WarningBanner',
7+
'InfoBanner',
8+
'RebrandBanner',
9+
] as const;
410
export type BannerType = (typeof Banners)[number];
511

12+
export enum BannerOrder {
13+
Top = 0,
14+
Default = 1,
15+
Bottom = 2,
16+
}
17+
618
export type BannerPayload = {
719
key: BannerType;
820
bannerProps?: BannerProps;
21+
order: BannerOrder;
922
};

src/entities/defaultBanners/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export * as defaultBannersModel from './model';
Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
import { renderHook } from '@testing-library/react';
2+
import { beforeEach, describe, expect, it, vi } from 'vitest';
3+
4+
import { useDefaultBanners } from './useDefaultBanners';
5+
import { useRebrandBanner } from './useRebrandBanner';
6+
7+
import { dismissedBannersSelector } from '~/entities/defaultBanners/model/selectors';
8+
import { userModel } from '~/entities/user';
9+
import { useAppSelector } from '~/shared/utils';
10+
11+
// Mock dependencies
12+
vi.mock('~/entities/user', () => ({
13+
userModel: {
14+
hooks: {
15+
useAuthorization: vi.fn(),
16+
},
17+
},
18+
}));
19+
20+
vi.mock('~/shared/utils', () => ({
21+
useAppSelector: vi.fn(),
22+
}));
23+
24+
vi.mock('~/entities/defaultBanners/model/selectors', () => ({
25+
dismissedBannersSelector: 'dismissedBannersSelector',
26+
}));
27+
28+
vi.mock('./useRebrandBanner', () => ({
29+
useRebrandBanner: vi.fn(),
30+
}));
31+
32+
describe('useDefaultBanners', () => {
33+
// Setup mock values
34+
const mockUserId = 'test-user-123';
35+
const mockDismissedBanners = {
36+
[`user-${mockUserId}`]: ['RebrandBanner'],
37+
global: [],
38+
};
39+
40+
beforeEach(() => {
41+
// Reset all mocks
42+
vi.clearAllMocks();
43+
44+
// Setup default mock returns
45+
(userModel.hooks.useAuthorization as unknown as ReturnType<typeof vi.fn>).mockReturnValue({
46+
isAuthorized: false,
47+
user: null,
48+
});
49+
50+
(useAppSelector as unknown as ReturnType<typeof vi.fn>).mockReturnValue(mockDismissedBanners);
51+
52+
(useRebrandBanner as unknown as ReturnType<typeof vi.fn>).mockReturnValue(undefined);
53+
});
54+
55+
it('should use global banner scope when user is not authorized', () => {
56+
// Setup
57+
(userModel.hooks.useAuthorization as unknown as ReturnType<typeof vi.fn>).mockReturnValue({
58+
isAuthorized: false,
59+
user: null,
60+
});
61+
62+
// Execute
63+
renderHook(() => useDefaultBanners());
64+
65+
// Verify
66+
expect(useAppSelector).toHaveBeenCalledWith(dismissedBannersSelector);
67+
expect(useRebrandBanner).toHaveBeenCalledWith(mockDismissedBanners, 'global');
68+
});
69+
70+
it('should use user-specific banner scope when user is authorized', () => {
71+
// Setup
72+
(userModel.hooks.useAuthorization as unknown as ReturnType<typeof vi.fn>).mockReturnValue({
73+
isAuthorized: true,
74+
user: { id: mockUserId },
75+
});
76+
77+
// Execute
78+
renderHook(() => useDefaultBanners());
79+
80+
// Verify
81+
expect(useAppSelector).toHaveBeenCalledWith(dismissedBannersSelector);
82+
expect(useRebrandBanner).toHaveBeenCalledWith(mockDismissedBanners, `user-${mockUserId}`);
83+
});
84+
85+
it('should handle undefined user id when authorized', () => {
86+
// Setup
87+
(userModel.hooks.useAuthorization as unknown as ReturnType<typeof vi.fn>).mockReturnValue({
88+
isAuthorized: true,
89+
user: { id: undefined },
90+
});
91+
92+
// Execute
93+
renderHook(() => useDefaultBanners());
94+
95+
// Verify
96+
expect(useRebrandBanner).toHaveBeenCalledWith(mockDismissedBanners, 'user-undefined');
97+
});
98+
99+
it('should handle null user when authorized', () => {
100+
// Setup
101+
(userModel.hooks.useAuthorization as unknown as ReturnType<typeof vi.fn>).mockReturnValue({
102+
isAuthorized: true,
103+
user: null,
104+
});
105+
106+
// Execute
107+
renderHook(() => useDefaultBanners());
108+
109+
// Verify
110+
expect(useRebrandBanner).toHaveBeenCalledWith(mockDismissedBanners, 'user-undefined');
111+
});
112+
113+
it('should handle empty dismissed banners', () => {
114+
// Setup
115+
const emptyDismissedBanners = {};
116+
(useAppSelector as unknown as ReturnType<typeof vi.fn>).mockReturnValue(emptyDismissedBanners);
117+
118+
// Execute
119+
renderHook(() => useDefaultBanners());
120+
121+
// Verify
122+
expect(useRebrandBanner).toHaveBeenCalledWith(emptyDismissedBanners, 'global');
123+
});
124+
});
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import { useRebrandBanner } from './useRebrandBanner';
2+
3+
import { dismissedBannersSelector } from '~/entities/defaultBanners/model/selectors';
4+
import { userModel } from '~/entities/user';
5+
import { useAppSelector } from '~/shared/utils';
6+
7+
export const useDefaultBanners = () => {
8+
const { isAuthorized, user } = userModel.hooks.useAuthorization();
9+
const userId = user?.id;
10+
11+
const bannerScope = isAuthorized ? `user-${userId}` : 'global';
12+
13+
const dismissed = useAppSelector(dismissedBannersSelector);
14+
15+
useRebrandBanner(dismissed, bannerScope);
16+
};

0 commit comments

Comments
 (0)