mirror of
https://github.com/Unleash/unleash.git
synced 2025-04-19 01:17:18 +02: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
src
lib
features
feature-search
feature-toggle
openapi/spec
test/e2e/helpers
@ -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