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',
 | 
					                limit = '50',
 | 
				
			||||||
                sortOrder,
 | 
					                sortOrder,
 | 
				
			||||||
                sortBy,
 | 
					                sortBy,
 | 
				
			||||||
 | 
					                favoritesFirst,
 | 
				
			||||||
            } = req.query;
 | 
					            } = req.query;
 | 
				
			||||||
            const userId = req.user.id;
 | 
					            const userId = req.user.id;
 | 
				
			||||||
            const normalizedTag = tag?.map((tag) => tag.split(':'));
 | 
					            const normalizedTag = tag?.map((tag) => tag.split(':'));
 | 
				
			||||||
@ -97,6 +98,7 @@ export default class FeatureSearchController extends Controller {
 | 
				
			|||||||
            const normalizedSortBy: string = sortBy ? sortBy : 'createdAt';
 | 
					            const normalizedSortBy: string = sortBy ? sortBy : 'createdAt';
 | 
				
			||||||
            const normalizedSortOrder =
 | 
					            const normalizedSortOrder =
 | 
				
			||||||
                sortOrder === 'asc' || sortOrder === 'desc' ? sortOrder : 'asc';
 | 
					                sortOrder === 'asc' || sortOrder === 'desc' ? sortOrder : 'asc';
 | 
				
			||||||
 | 
					            const normalizedFavoritesFirst = favoritesFirst === 'true';
 | 
				
			||||||
            const { features, total } = await this.featureSearchService.search({
 | 
					            const { features, total } = await this.featureSearchService.search({
 | 
				
			||||||
                query,
 | 
					                query,
 | 
				
			||||||
                projectId,
 | 
					                projectId,
 | 
				
			||||||
@ -108,6 +110,7 @@ export default class FeatureSearchController extends Controller {
 | 
				
			|||||||
                limit: normalizedLimit,
 | 
					                limit: normalizedLimit,
 | 
				
			||||||
                sortBy: normalizedSortBy,
 | 
					                sortBy: normalizedSortBy,
 | 
				
			||||||
                sortOrder: normalizedSortOrder,
 | 
					                sortOrder: normalizedSortOrder,
 | 
				
			||||||
 | 
					                favoritesFirst: normalizedFavoritesFirst,
 | 
				
			||||||
            });
 | 
					            });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            res.json({ features, total });
 | 
					            res.json({ features, total });
 | 
				
			||||||
 | 
				
			|||||||
@ -1,6 +1,7 @@
 | 
				
			|||||||
import dbInit, { ITestDb } from '../../../test/e2e/helpers/database-init';
 | 
					import dbInit, { ITestDb } from '../../../test/e2e/helpers/database-init';
 | 
				
			||||||
import {
 | 
					import {
 | 
				
			||||||
    IUnleashTest,
 | 
					    IUnleashTest,
 | 
				
			||||||
 | 
					    setupAppWithAuth,
 | 
				
			||||||
    setupAppWithCustomConfig,
 | 
					    setupAppWithCustomConfig,
 | 
				
			||||||
} from '../../../test/e2e/helpers/test-helper';
 | 
					} from '../../../test/e2e/helpers/test-helper';
 | 
				
			||||||
import getLogger from '../../../test/fixtures/no-logger';
 | 
					import getLogger from '../../../test/fixtures/no-logger';
 | 
				
			||||||
@ -11,7 +12,7 @@ let db: ITestDb;
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
beforeAll(async () => {
 | 
					beforeAll(async () => {
 | 
				
			||||||
    db = await dbInit('feature_search', getLogger);
 | 
					    db = await dbInit('feature_search', getLogger);
 | 
				
			||||||
    app = await setupAppWithCustomConfig(
 | 
					    app = await setupAppWithAuth(
 | 
				
			||||||
        db.stores,
 | 
					        db.stores,
 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
            experimental: {
 | 
					            experimental: {
 | 
				
			||||||
@ -23,6 +24,13 @@ beforeAll(async () => {
 | 
				
			|||||||
        },
 | 
					        },
 | 
				
			||||||
        db.rawDatabase,
 | 
					        db.rawDatabase,
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    await app.request
 | 
				
			||||||
 | 
					        .post(`/auth/demo/login`)
 | 
				
			||||||
 | 
					        .send({
 | 
				
			||||||
 | 
					            email: 'user@getunleash.io',
 | 
				
			||||||
 | 
					        })
 | 
				
			||||||
 | 
					        .expect(200);
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
afterAll(async () => {
 | 
					afterAll(async () => {
 | 
				
			||||||
@ -48,12 +56,13 @@ const sortFeatures = async (
 | 
				
			|||||||
        sortBy = '',
 | 
					        sortBy = '',
 | 
				
			||||||
        sortOrder = '',
 | 
					        sortOrder = '',
 | 
				
			||||||
        projectId = 'default',
 | 
					        projectId = 'default',
 | 
				
			||||||
 | 
					        favoritesFirst = 'false',
 | 
				
			||||||
    }: FeatureSearchQueryParameters,
 | 
					    }: FeatureSearchQueryParameters,
 | 
				
			||||||
    expectedCode = 200,
 | 
					    expectedCode = 200,
 | 
				
			||||||
) => {
 | 
					) => {
 | 
				
			||||||
    return app.request
 | 
					    return app.request
 | 
				
			||||||
        .get(
 | 
					        .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);
 | 
					        .expect(expectedCode);
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
@ -149,8 +158,14 @@ test('should paginate with offset', async () => {
 | 
				
			|||||||
});
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
test('should filter features by type', async () => {
 | 
					test('should filter features by type', async () => {
 | 
				
			||||||
    await app.createFeature({ name: 'my_feature_a', type: 'release' });
 | 
					    await app.createFeature({
 | 
				
			||||||
    await app.createFeature({ name: 'my_feature_b', type: 'experimental' });
 | 
					        name: 'my_feature_a',
 | 
				
			||||||
 | 
					        type: 'release',
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					    await app.createFeature({
 | 
				
			||||||
 | 
					        name: 'my_feature_b',
 | 
				
			||||||
 | 
					        type: 'experimental',
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const { body } = await filterFeaturesByType([
 | 
					    const { body } = await filterFeaturesByType([
 | 
				
			||||||
        'experimental',
 | 
					        'experimental',
 | 
				
			||||||
@ -165,7 +180,10 @@ test('should filter features by type', async () => {
 | 
				
			|||||||
test('should filter features by tag', async () => {
 | 
					test('should filter features by tag', async () => {
 | 
				
			||||||
    await app.createFeature('my_feature_a');
 | 
					    await app.createFeature('my_feature_a');
 | 
				
			||||||
    await app.createFeature('my_feature_b');
 | 
					    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']);
 | 
					    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 () => {
 | 
					test('should filter by partial tag', async () => {
 | 
				
			||||||
    await app.createFeature('my_feature_a');
 | 
					    await app.createFeature('my_feature_a');
 | 
				
			||||||
    await app.createFeature('my_feature_b');
 | 
					    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']);
 | 
					    const { body } = await filterFeaturesByTag(['simple']);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -205,7 +226,10 @@ test('should filter by partial tag', async () => {
 | 
				
			|||||||
test('should search matching features by tag', async () => {
 | 
					test('should search matching features by tag', async () => {
 | 
				
			||||||
    await app.createFeature('my_feature_a');
 | 
					    await app.createFeature('my_feature_a');
 | 
				
			||||||
    await app.createFeature('my_feature_b');
 | 
					    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({
 | 
					    const { body: fullMatch } = await searchFeatures({
 | 
				
			||||||
        query: 'simple:my_tag',
 | 
					        query: 'simple:my_tag',
 | 
				
			||||||
@ -230,8 +254,14 @@ test('should search matching features by tag', async () => {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
test('should return all feature tags', async () => {
 | 
					test('should return all feature tags', async () => {
 | 
				
			||||||
    await app.createFeature('my_feature_a');
 | 
					    await app.createFeature('my_feature_a');
 | 
				
			||||||
    await app.addTag('my_feature_a', { type: 'simple', value: 'my_tag' });
 | 
					    await app.addTag('my_feature_a', {
 | 
				
			||||||
    await app.addTag('my_feature_a', { type: 'simple', value: 'second_tag' });
 | 
					        type: 'simple',
 | 
				
			||||||
 | 
					        value: 'my_tag',
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					    await app.addTag('my_feature_a', {
 | 
				
			||||||
 | 
					        type: 'simple',
 | 
				
			||||||
 | 
					        value: 'second_tag',
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const { body } = await searchFeatures({});
 | 
					    const { body } = await searchFeatures({});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -240,8 +270,14 @@ test('should return all feature tags', async () => {
 | 
				
			|||||||
            {
 | 
					            {
 | 
				
			||||||
                name: 'my_feature_a',
 | 
					                name: 'my_feature_a',
 | 
				
			||||||
                tags: [
 | 
					                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_c');
 | 
				
			||||||
    await app.createFeature('my_feature_b');
 | 
					    await app.createFeature('my_feature_b');
 | 
				
			||||||
    await app.enableFeature('my_feature_c', 'default');
 | 
					    await app.enableFeature('my_feature_c', 'default');
 | 
				
			||||||
 | 
					    await app.favoriteFeature('my_feature_b');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const { body: ascName } = await sortFeatures({
 | 
					    const { body: ascName } = await sortFeatures({
 | 
				
			||||||
        sortBy: 'name',
 | 
					        sortBy: 'name',
 | 
				
			||||||
@ -351,4 +388,19 @@ test('should sort features', async () => {
 | 
				
			|||||||
        ],
 | 
					        ],
 | 
				
			||||||
        total: 3,
 | 
					        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,
 | 
					        limit,
 | 
				
			||||||
        sortOrder,
 | 
					        sortOrder,
 | 
				
			||||||
        sortBy,
 | 
					        sortBy,
 | 
				
			||||||
 | 
					        favoritesFirst,
 | 
				
			||||||
    }: IFeatureSearchParams): Promise<{
 | 
					    }: IFeatureSearchParams): Promise<{
 | 
				
			||||||
        features: IFeatureOverview[];
 | 
					        features: IFeatureOverview[];
 | 
				
			||||||
        total: number;
 | 
					        total: number;
 | 
				
			||||||
@ -706,6 +707,10 @@ class FeatureStrategiesStore implements IFeatureStrategiesStore {
 | 
				
			|||||||
            lastSeenAt: 'env_last_seen_at',
 | 
					            lastSeenAt: 'env_last_seen_at',
 | 
				
			||||||
        };
 | 
					        };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if (favoritesFirst) {
 | 
				
			||||||
 | 
					            query = query.orderBy('favorite', 'desc');
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if (sortBy.startsWith('environment:')) {
 | 
					        if (sortBy.startsWith('environment:')) {
 | 
				
			||||||
            const [, envName] = sortBy.split(':');
 | 
					            const [, envName] = sortBy.split(':');
 | 
				
			||||||
            query = query
 | 
					            query = query
 | 
				
			||||||
 | 
				
			|||||||
@ -29,6 +29,7 @@ export interface IFeatureSearchParams {
 | 
				
			|||||||
    tag?: string[][];
 | 
					    tag?: string[][];
 | 
				
			||||||
    status?: string[][];
 | 
					    status?: string[][];
 | 
				
			||||||
    offset: number;
 | 
					    offset: number;
 | 
				
			||||||
 | 
					    favoritesFirst?: boolean;
 | 
				
			||||||
    limit: number;
 | 
					    limit: number;
 | 
				
			||||||
    sortBy: string;
 | 
					    sortBy: string;
 | 
				
			||||||
    sortOrder: 'asc' | 'desc';
 | 
					    sortOrder: 'asc' | 'desc';
 | 
				
			||||||
 | 
				
			|||||||
@ -97,6 +97,16 @@ export const featureSearchQueryParameters = [
 | 
				
			|||||||
            'The sort order for the sortBy. By default it is det to "asc".',
 | 
					            'The sort order for the sortBy. By default it is det to "asc".',
 | 
				
			||||||
        in: 'query',
 | 
					        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;
 | 
					] as const;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export type FeatureSearchQueryParameters = Partial<
 | 
					export type FeatureSearchQueryParameters = Partial<
 | 
				
			||||||
 | 
				
			|||||||
@ -54,6 +54,12 @@ export interface IUnleashHttpAPI {
 | 
				
			|||||||
        expectedResponseCode?: number,
 | 
					        expectedResponseCode?: number,
 | 
				
			||||||
    ): supertest.Test;
 | 
					    ): supertest.Test;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    favoriteFeature(
 | 
				
			||||||
 | 
					        feature: string,
 | 
				
			||||||
 | 
					        project?: string,
 | 
				
			||||||
 | 
					        expectedResponseCode?: number,
 | 
				
			||||||
 | 
					    ): supertest.Test;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    getFeatures(name?: string, expectedResponseCode?: number): supertest.Test;
 | 
					    getFeatures(name?: string, expectedResponseCode?: number): supertest.Test;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    getProjectFeatures(
 | 
					    getProjectFeatures(
 | 
				
			||||||
@ -239,6 +245,19 @@ function httpApis(
 | 
				
			|||||||
                )
 | 
					                )
 | 
				
			||||||
                .expect(expectedResponseCode);
 | 
					                .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