diff --git a/src/lib/features/feature-search/feature-search-store.ts b/src/lib/features/feature-search/feature-search-store.ts index 4b0e32fd20..1973c8f698 100644 --- a/src/lib/features/feature-search/feature-search-store.ts +++ b/src/lib/features/feature-search/feature-search-store.ts @@ -19,8 +19,8 @@ import { } from '../feature-toggle/types/feature-toggle-strategies-store-type'; import FeatureStrategiesStore from '../feature-toggle/feature-toggle-strategies-store'; -const sortEnvironments = (overview: IFeatureOverview) => { - return Object.values(overview).map((data: IFeatureOverview) => ({ +const sortEnvironments = (overview: IFeatureOverview[]) => { + return overview.map((data: IFeatureOverview) => ({ ...data, environments: data.environments .filter((f) => f.name) @@ -296,34 +296,16 @@ class FeatureSearchStore implements IFeatureSearchStore { }; } - getAggregatedSearchData(rows): IFeatureOverview { - return rows.reduce((acc, row) => { - if (acc[row.feature_name] !== undefined) { - const environmentExists = acc[ - row.feature_name - ].environments.some( - (existingEnvironment) => - existingEnvironment.name === row.environment, - ); - if (!environmentExists) { - acc[row.feature_name].environments.push( - FeatureSearchStore.getEnvironment(row), - ); - } + getAggregatedSearchData(rows): IFeatureOverview[] { + const entriesMap: Map = new Map(); + const orderedEntries: IFeatureOverview[] = []; - const segmentExists = acc[row.feature_name].segments.includes( - row.segment_name, - ); + rows.forEach((row) => { + let entry = entriesMap.get(row.feature_name); - if (row.segment_name && !segmentExists) { - acc[row.feature_name].segments.push(row.segment_name); - } - - if (this.isNewTag(acc[row.feature_name], row)) { - this.addTag(acc[row.feature_name], row); - } - } else { - acc[row.feature_name] = { + if (!entry) { + // Create a new entry + entry = { type: row.type, description: row.description, project: row.project, @@ -333,24 +315,41 @@ class FeatureSearchStore implements IFeatureSearchStore { stale: row.stale, impressionData: row.impression_data, lastSeenAt: row.last_seen_at, - environments: [FeatureSearchStore.getEnvironment(row)], + environments: [], segments: row.segment_name ? [row.segment_name] : [], }; + entriesMap.set(row.feature_name, entry); + orderedEntries.push(entry); + } - if (this.isNewTag(acc[row.feature_name], row)) { - this.addTag(acc[row.feature_name], row); - } + // Add environment if not already present + if (!entry.environments.some((e) => e.name === row.environment)) { + entry.environments.push(FeatureSearchStore.getEnvironment(row)); } - const featureRow = acc[row.feature_name]; + + // Add segment if not already present if ( - featureRow.lastSeenAt === undefined || - new Date(row.env_last_seen_at) > - new Date(featureRow.last_seen_at) + row.segment_name && + !entry.segments.includes(row.segment_name) ) { - featureRow.lastSeenAt = row.env_last_seen_at; + entry.segments.push(row.segment_name); } - return acc; - }, {}); + + // Add tag if new + if (this.isNewTag(entry, row)) { + this.addTag(entry, row); + } + + // Update lastSeenAt if more recent + if ( + !entry.lastSeenAt || + new Date(row.env_last_seen_at) > new Date(entry.lastSeenAt) + ) { + entry.lastSeenAt = row.env_last_seen_at; + } + }); + + return orderedEntries; } private addTag( @@ -369,13 +368,16 @@ class FeatureSearchStore implements IFeatureSearchStore { }; } + private isTagRow(row: Record): boolean { + return row.tag_type && row.tag_value; + } + private isNewTag( featureToggle: Record, row: Record, ): boolean { return ( - row.tag_type && - row.tag_value && + this.isTagRow(row) && !featureToggle.tags?.some( (tag) => tag.type === row.tag_type && tag.value === row.tag_value, diff --git a/src/lib/features/feature-search/feature.search.e2e.test.ts b/src/lib/features/feature-search/feature.search.e2e.test.ts index 2066ac6628..de72f3b96c 100644 --- a/src/lib/features/feature-search/feature.search.e2e.test.ts +++ b/src/lib/features/feature-search/feature.search.e2e.test.ts @@ -477,6 +477,31 @@ test('should sort features', async () => { total: 3, }); }); + +test('should sort features when feature names are numbers', async () => { + await app.createFeature('my_feature_a'); + await app.createFeature('my_feature_c'); + await app.createFeature('my_feature_b'); + await app.createFeature('1234'); + await app.favoriteFeature('my_feature_b'); + + const { body: favoriteSortByName } = await sortFeatures({ + sortBy: 'name', + sortOrder: 'asc', + favoritesFirst: 'true', + }); + + expect(favoriteSortByName).toMatchObject({ + features: [ + { name: 'my_feature_b' }, + { name: '1234' }, + { name: 'my_feature_a' }, + { name: 'my_feature_c' }, + ], + total: 4, + }); +}); + test('should paginate correctly when using tags', async () => { await app.createFeature('my_feature_a'); await app.createFeature('my_feature_b'); diff --git a/src/lib/types/model.ts b/src/lib/types/model.ts index 5e0c46da42..444a031885 100644 --- a/src/lib/types/model.ts +++ b/src/lib/types/model.ts @@ -205,6 +205,11 @@ export interface IEnvironmentOverview extends IEnvironmentBase { export interface IFeatureOverview { name: string; + description: string; + project: string; + favorite: boolean; + impressionData: boolean; + segments: string[]; type: string; stale: boolean; createdAt: Date;