mirror of
				https://github.com/Unleash/unleash.git
				synced 2025-10-27 11:02:16 +01:00 
			
		
		
		
	feat: sort favorites on the backend (#5326)
Now favorites will be always on first page, if pinned.
This commit is contained in:
		
							parent
							
								
									0f7360c1e8
								
							
						
					
					
						commit
						5d762dcb39
					
				| @ -81,6 +81,7 @@ export default class FeatureSearchController extends Controller { | ||||
|                 limit = '50', | ||||
|                 sortOrder, | ||||
|                 sortBy, | ||||
|                 favoritesFirst, | ||||
|             } = req.query; | ||||
|             const userId = req.user.id; | ||||
|             const normalizedTag = tag?.map((tag) => tag.split(':')); | ||||
| @ -97,6 +98,7 @@ export default class FeatureSearchController extends Controller { | ||||
|             const normalizedSortBy: string = sortBy ? sortBy : 'createdAt'; | ||||
|             const normalizedSortOrder = | ||||
|                 sortOrder === 'asc' || sortOrder === 'desc' ? sortOrder : 'asc'; | ||||
|             const normalizedFavoritesFirst = favoritesFirst === 'true'; | ||||
|             const { features, total } = await this.featureSearchService.search({ | ||||
|                 query, | ||||
|                 projectId, | ||||
| @ -108,6 +110,7 @@ export default class FeatureSearchController extends Controller { | ||||
|                 limit: normalizedLimit, | ||||
|                 sortBy: normalizedSortBy, | ||||
|                 sortOrder: normalizedSortOrder, | ||||
|                 favoritesFirst: normalizedFavoritesFirst, | ||||
|             }); | ||||
| 
 | ||||
|             res.json({ features, total }); | ||||
|  | ||||
| @ -1,6 +1,7 @@ | ||||
| import dbInit, { ITestDb } from '../../../test/e2e/helpers/database-init'; | ||||
| import { | ||||
|     IUnleashTest, | ||||
|     setupAppWithAuth, | ||||
|     setupAppWithCustomConfig, | ||||
| } from '../../../test/e2e/helpers/test-helper'; | ||||
| import getLogger from '../../../test/fixtures/no-logger'; | ||||
| @ -11,7 +12,7 @@ let db: ITestDb; | ||||
| 
 | ||||
| beforeAll(async () => { | ||||
|     db = await dbInit('feature_search', getLogger); | ||||
|     app = await setupAppWithCustomConfig( | ||||
|     app = await setupAppWithAuth( | ||||
|         db.stores, | ||||
|         { | ||||
|             experimental: { | ||||
| @ -23,6 +24,13 @@ beforeAll(async () => { | ||||
|         }, | ||||
|         db.rawDatabase, | ||||
|     ); | ||||
| 
 | ||||
|     await app.request | ||||
|         .post(`/auth/demo/login`) | ||||
|         .send({ | ||||
|             email: 'user@getunleash.io', | ||||
|         }) | ||||
|         .expect(200); | ||||
| }); | ||||
| 
 | ||||
| afterAll(async () => { | ||||
| @ -48,12 +56,13 @@ const sortFeatures = async ( | ||||
|         sortBy = '', | ||||
|         sortOrder = '', | ||||
|         projectId = 'default', | ||||
|         favoritesFirst = 'false', | ||||
|     }: FeatureSearchQueryParameters, | ||||
|     expectedCode = 200, | ||||
| ) => { | ||||
|     return app.request | ||||
|         .get( | ||||
|             `/api/admin/search/features?sortBy=${sortBy}&sortOrder=${sortOrder}&projectId=${projectId}`, | ||||
|             `/api/admin/search/features?sortBy=${sortBy}&sortOrder=${sortOrder}&projectId=${projectId}&favoritesFirst=${favoritesFirst}`, | ||||
|         ) | ||||
|         .expect(expectedCode); | ||||
| }; | ||||
| @ -149,8 +158,14 @@ test('should paginate with offset', async () => { | ||||
| }); | ||||
| 
 | ||||
| test('should filter features by type', async () => { | ||||
|     await app.createFeature({ name: 'my_feature_a', type: 'release' }); | ||||
|     await app.createFeature({ name: 'my_feature_b', type: 'experimental' }); | ||||
|     await app.createFeature({ | ||||
|         name: 'my_feature_a', | ||||
|         type: 'release', | ||||
|     }); | ||||
|     await app.createFeature({ | ||||
|         name: 'my_feature_b', | ||||
|         type: 'experimental', | ||||
|     }); | ||||
| 
 | ||||
|     const { body } = await filterFeaturesByType([ | ||||
|         'experimental', | ||||
| @ -165,7 +180,10 @@ test('should filter features by type', async () => { | ||||
| test('should filter features by tag', async () => { | ||||
|     await app.createFeature('my_feature_a'); | ||||
|     await app.createFeature('my_feature_b'); | ||||
|     await app.addTag('my_feature_a', { type: 'simple', value: 'my_tag' }); | ||||
|     await app.addTag('my_feature_a', { | ||||
|         type: 'simple', | ||||
|         value: 'my_tag', | ||||
|     }); | ||||
| 
 | ||||
|     const { body } = await filterFeaturesByTag(['simple:my_tag']); | ||||
| 
 | ||||
| @ -193,7 +211,10 @@ test('should filter features by environment status', async () => { | ||||
| test('should filter by partial tag', async () => { | ||||
|     await app.createFeature('my_feature_a'); | ||||
|     await app.createFeature('my_feature_b'); | ||||
|     await app.addTag('my_feature_a', { type: 'simple', value: 'my_tag' }); | ||||
|     await app.addTag('my_feature_a', { | ||||
|         type: 'simple', | ||||
|         value: 'my_tag', | ||||
|     }); | ||||
| 
 | ||||
|     const { body } = await filterFeaturesByTag(['simple']); | ||||
| 
 | ||||
| @ -205,7 +226,10 @@ test('should filter by partial tag', async () => { | ||||
| test('should search matching features by tag', async () => { | ||||
|     await app.createFeature('my_feature_a'); | ||||
|     await app.createFeature('my_feature_b'); | ||||
|     await app.addTag('my_feature_a', { type: 'simple', value: 'my_tag' }); | ||||
|     await app.addTag('my_feature_a', { | ||||
|         type: 'simple', | ||||
|         value: 'my_tag', | ||||
|     }); | ||||
| 
 | ||||
|     const { body: fullMatch } = await searchFeatures({ | ||||
|         query: 'simple:my_tag', | ||||
| @ -230,8 +254,14 @@ test('should search matching features by tag', async () => { | ||||
| 
 | ||||
| test('should return all feature tags', async () => { | ||||
|     await app.createFeature('my_feature_a'); | ||||
|     await app.addTag('my_feature_a', { type: 'simple', value: 'my_tag' }); | ||||
|     await app.addTag('my_feature_a', { type: 'simple', value: 'second_tag' }); | ||||
|     await app.addTag('my_feature_a', { | ||||
|         type: 'simple', | ||||
|         value: 'my_tag', | ||||
|     }); | ||||
|     await app.addTag('my_feature_a', { | ||||
|         type: 'simple', | ||||
|         value: 'second_tag', | ||||
|     }); | ||||
| 
 | ||||
|     const { body } = await searchFeatures({}); | ||||
| 
 | ||||
| @ -240,8 +270,14 @@ test('should return all feature tags', async () => { | ||||
|             { | ||||
|                 name: 'my_feature_a', | ||||
|                 tags: [ | ||||
|                     { type: 'simple', value: 'my_tag' }, | ||||
|                     { type: 'simple', value: 'second_tag' }, | ||||
|                     { | ||||
|                         type: 'simple', | ||||
|                         value: 'my_tag', | ||||
|                     }, | ||||
|                     { | ||||
|                         type: 'simple', | ||||
|                         value: 'second_tag', | ||||
|                     }, | ||||
|                 ], | ||||
|             }, | ||||
|         ], | ||||
| @ -281,6 +317,7 @@ test('should sort features', async () => { | ||||
|     await app.createFeature('my_feature_c'); | ||||
|     await app.createFeature('my_feature_b'); | ||||
|     await app.enableFeature('my_feature_c', 'default'); | ||||
|     await app.favoriteFeature('my_feature_b'); | ||||
| 
 | ||||
|     const { body: ascName } = await sortFeatures({ | ||||
|         sortBy: 'name', | ||||
| @ -351,4 +388,19 @@ test('should sort features', async () => { | ||||
|         ], | ||||
|         total: 3, | ||||
|     }); | ||||
| 
 | ||||
|     const { body: favoriteEnvironmentDescSort } = await sortFeatures({ | ||||
|         sortBy: 'environment:default', | ||||
|         sortOrder: 'desc', | ||||
|         favoritesFirst: 'true', | ||||
|     }); | ||||
| 
 | ||||
|     expect(favoriteEnvironmentDescSort).toMatchObject({ | ||||
|         features: [ | ||||
|             { name: 'my_feature_b' }, | ||||
|             { name: 'my_feature_c' }, | ||||
|             { name: 'my_feature_a' }, | ||||
|         ], | ||||
|         total: 3, | ||||
|     }); | ||||
| }); | ||||
|  | ||||
| @ -530,6 +530,7 @@ class FeatureStrategiesStore implements IFeatureStrategiesStore { | ||||
|         limit, | ||||
|         sortOrder, | ||||
|         sortBy, | ||||
|         favoritesFirst, | ||||
|     }: IFeatureSearchParams): Promise<{ | ||||
|         features: IFeatureOverview[]; | ||||
|         total: number; | ||||
| @ -706,6 +707,10 @@ class FeatureStrategiesStore implements IFeatureStrategiesStore { | ||||
|             lastSeenAt: 'env_last_seen_at', | ||||
|         }; | ||||
| 
 | ||||
|         if (favoritesFirst) { | ||||
|             query = query.orderBy('favorite', 'desc'); | ||||
|         } | ||||
| 
 | ||||
|         if (sortBy.startsWith('environment:')) { | ||||
|             const [, envName] = sortBy.split(':'); | ||||
|             query = query | ||||
|  | ||||
| @ -29,6 +29,7 @@ export interface IFeatureSearchParams { | ||||
|     tag?: string[][]; | ||||
|     status?: string[][]; | ||||
|     offset: number; | ||||
|     favoritesFirst?: boolean; | ||||
|     limit: number; | ||||
|     sortBy: string; | ||||
|     sortOrder: 'asc' | 'desc'; | ||||
|  | ||||
| @ -97,6 +97,16 @@ export const featureSearchQueryParameters = [ | ||||
|             'The sort order for the sortBy. By default it is det to "asc".', | ||||
|         in: 'query', | ||||
|     }, | ||||
|     { | ||||
|         name: 'favoritesFirst', | ||||
|         schema: { | ||||
|             type: 'string', | ||||
|             example: 'true', | ||||
|         }, | ||||
|         description: | ||||
|             'The flag to indicate if the favorite features should be returned first. By default it is set to false.', | ||||
|         in: 'query', | ||||
|     }, | ||||
| ] as const; | ||||
| 
 | ||||
| export type FeatureSearchQueryParameters = Partial< | ||||
|  | ||||
| @ -54,6 +54,12 @@ export interface IUnleashHttpAPI { | ||||
|         expectedResponseCode?: number, | ||||
|     ): supertest.Test; | ||||
| 
 | ||||
|     favoriteFeature( | ||||
|         feature: string, | ||||
|         project?: string, | ||||
|         expectedResponseCode?: number, | ||||
|     ): supertest.Test; | ||||
| 
 | ||||
|     getFeatures(name?: string, expectedResponseCode?: number): supertest.Test; | ||||
| 
 | ||||
|     getProjectFeatures( | ||||
| @ -239,6 +245,19 @@ function httpApis( | ||||
|                 ) | ||||
|                 .expect(expectedResponseCode); | ||||
|         }, | ||||
| 
 | ||||
|         favoriteFeature( | ||||
|             feature: string, | ||||
|             project = 'default', | ||||
|             expectedResponseCode = 200, | ||||
|         ): supertest.Test { | ||||
|             return request | ||||
|                 .post( | ||||
|                     `/api/admin/projects/${project}/features/${feature}/favorites`, | ||||
|                 ) | ||||
|                 .set('Content-Type', 'application/json') | ||||
|                 .expect(expectedResponseCode); | ||||
|         }, | ||||
|     }; | ||||
| } | ||||
| 
 | ||||
|  | ||||
		Loading…
	
		Reference in New Issue
	
	Block a user