Skip to content

Commit e3b661b

Browse files
minotticJunjiequansnyk-botdependabot[bot]github-actions[bot]
authored
RELEASE 2025/28/10 (#2080)
## Description RELEASE 2025/28/10 ## Summary by Sourcery Implement advanced filtering for proposals and datasets by introducing NgRx-based add/remove filter actions, dynamic facet count selectors, and configurable filter behaviors, refactor the AppConfigService to support override merging, update filter components to simplify multiselect and shared filters, and extend tests to cover the new filter logic. New Features: - Add NgRx actions and reducer handlers for adding, removing, and clearing proposal filters with multiSelect, checkbox, and dateRange support - Introduce selectors for full facet parameters and per-key filter state, and integrate dynamic facet count retrieval by key - Update AppConfigService to support allowConfigOverrides flag and custom merging of base and override configurations - Enable default filter settings from app config and immediate filter application for checkbox and multiselect in dataset filters Enhancements: - Refactor mergeConfig in AppConfigService to use mergeWith for array replacement and remove deprecated loadAndMerge - Simplify MultiSelectFilterComponent by managing id-label mapping internally and removing external getFacetId util - Improve SharedFilterComponent filtering logic to normalize labels and faceted checkbox helper methods Build: - Bump @scicatproject/scicat-sdk-ts-angular, luxon, and mathjs dependencies Tests: - Expand unit tests for AppConfigService to verify override behavior, and add specs for proposal filter actions, reducers, selectors, and side-bar filter component - Update dataset and proposal effects and reducer tests to reflect new filter actions and selectors --------- Signed-off-by: dependabot[bot] <[email protected]> Co-authored-by: Jay <[email protected]> Co-authored-by: snyk-bot <[email protected]> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: Omkar Zade <[email protected]>
1 parent 58e159c commit e3b661b

39 files changed

+5135
-1772
lines changed

.github/CODEOWNERS

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
* @SciCatProject/reviewers

package-lock.json

Lines changed: 4469 additions & 1491 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -41,15 +41,15 @@
4141
"@ngrx/store": "^19.1.0",
4242
"@ngx-translate/core": "^17.0.0",
4343
"@ngxmc/datetime-picker": "^19.3.1",
44-
"@scicatproject/scicat-sdk-ts-angular": "^4.24.0",
44+
"@scicatproject/scicat-sdk-ts-angular": "^4.25.0",
4545
"autolinker": "^4.0.0",
4646
"deep-equal": "^2.0.5",
4747
"exceljs": "^4.4.0",
4848
"file-saver": "^2.0.5",
4949
"filesize": "^11.0.1",
5050
"lodash-es": "^4.17.21",
51-
"luxon": "^3.3.0",
52-
"mathjs": "^14.0.0",
51+
"luxon": "^3.7.2",
52+
"mathjs": "^15.0.0",
5353
"ngx-cookie-service": "^19.1.2",
5454
"ngx-json-viewer": "^3",
5555
"ngx-linky": "^4.0.0",

src/app/app-config.service.spec.ts

Lines changed: 60 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -285,27 +285,45 @@ describe("AppConfigService", () => {
285285
accessTokenPrefix: "",
286286
lbBaseURL: "http://127.0.0.1:3000",
287287
gettingStarted: null,
288+
defaultMainPage: {
289+
nonAuthenticatedUser: "DATASETS",
290+
authenticatedUser: "DATASETS",
291+
},
288292
mainMenu: { nonAuthenticatedUser: { datasets: true } },
293+
dateFormat: "yyyy-MM-dd HH:mm",
294+
oAuth2Endpoints: [
295+
{
296+
authURL: "abcd",
297+
},
298+
],
289299
},
290-
"/assets/config.0.json": { accessTokenPrefix: "Bearer " },
291300
"/assets/config.override.json": {
301+
accessTokenPrefix: "Bearer ",
292302
gettingStarted: "aGettingStarted",
293303
addDatasetEnabled: true,
294304
mainMenu: { nonAuthenticatedUser: { files: true } },
305+
oAuth2Endpoints: [],
295306
},
296-
"/assets/config.override.0.json": { siteTitle: "Test title" },
297307
};
298308

299309
const mergedConfig = {
300310
accessTokenPrefix: "Bearer ",
301311
lbBaseURL: "http://127.0.0.1:3000",
302312
gettingStarted: "aGettingStarted",
303313
addDatasetEnabled: true,
304-
siteTitle: "Test title",
314+
defaultMainPage: {
315+
nonAuthenticatedUser: "DATASETS",
316+
authenticatedUser: "DATASETS",
317+
},
305318
mainMenu: { nonAuthenticatedUser: { datasets: true, files: true } },
319+
oAuth2Endpoints: [],
320+
dateFormat: "yyyy-MM-dd HH:mm",
306321
};
307322

308-
const mockHttpGet = (backendError = false) => {
323+
const mockHttpGet = (
324+
configOverrideEnabled: boolean,
325+
backendError = false,
326+
) => {
309327
spyOn(service["http"], "get").and.callFake(
310328
(url: string): Observable<any> => {
311329
if (url === "/api/v3/admin/config") {
@@ -316,6 +334,11 @@ describe("AppConfigService", () => {
316334
}
317335
return of(mergedConfig);
318336
}
337+
if (url === "/assets/config.json")
338+
return of({
339+
...(mockConfigResponses[url] || {}),
340+
allowConfigOverrides: configOverrideEnabled,
341+
});
319342
return of(mockConfigResponses[url] || {});
320343
},
321344
);
@@ -330,28 +353,42 @@ describe("AppConfigService", () => {
330353
expect(config).toEqual(appConfig);
331354
});
332355

333-
it("should merge multiple config JSONs", async () => {
334-
mockHttpGet();
335-
const config = await service["mergeConfig"]();
336-
expect(config).toEqual(mergedConfig);
356+
[true, false].forEach((configOverrideEnabled) => {
357+
it(`should merge ${configOverrideEnabled} multiple config JSONs`, async () => {
358+
mockHttpGet(configOverrideEnabled);
359+
const config = await service["mergeConfig"]();
360+
expect(config).toEqual({
361+
...(configOverrideEnabled
362+
? mergedConfig
363+
: mockConfigResponses["/assets/config.json"]),
364+
allowConfigOverrides: configOverrideEnabled,
365+
});
366+
});
337367
});
338368

339-
it("should return the merged appConfig", async () => {
340-
mockHttpGet(true);
341-
await service.loadAppConfig();
369+
[true, false].forEach((configOverrideEnabled) => {
370+
it(`should return the merged ${configOverrideEnabled} appConfig`, async () => {
371+
mockHttpGet(configOverrideEnabled, true);
372+
await service.loadAppConfig();
342373

343-
expect(service["appConfig"]).toEqual(
344-
jasmine.objectContaining(mergedConfig),
345-
);
346-
expect(service["http"].get).toHaveBeenCalledWith("/api/v3/admin/config");
347-
expect(service["http"].get).toHaveBeenCalledWith("/assets/config.json");
348-
expect(service["http"].get).toHaveBeenCalledWith("/assets/config.0.json");
349-
expect(service["http"].get).toHaveBeenCalledWith(
350-
"/assets/config.override.json",
351-
);
352-
expect(service["http"].get).toHaveBeenCalledWith(
353-
"/assets/config.override.0.json",
354-
);
374+
expect(service["appConfig"]).toEqual({
375+
...(configOverrideEnabled
376+
? mergedConfig
377+
: mockConfigResponses["/assets/config.json"]),
378+
allowConfigOverrides: configOverrideEnabled,
379+
});
380+
expect(service["http"].get).toHaveBeenCalledTimes(
381+
configOverrideEnabled ? 3 : 2,
382+
);
383+
expect(service["http"].get).toHaveBeenCalledWith(
384+
"/api/v3/admin/config",
385+
);
386+
expect(service["http"].get).toHaveBeenCalledWith("/assets/config.json");
387+
if (configOverrideEnabled)
388+
expect(service["http"].get).toHaveBeenCalledWith(
389+
"/assets/config.override.json",
390+
);
391+
});
355392
});
356393
});
357394
});

src/app/app-config.service.ts

Lines changed: 34 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { HttpClient } from "@angular/common/http";
22
import { Injectable } from "@angular/core";
3-
import { merge } from "lodash-es";
4-
import { firstValueFrom, forkJoin, of } from "rxjs";
3+
import { mergeWith } from "lodash-es";
4+
import { firstValueFrom, of } from "rxjs";
55
import { catchError, timeout } from "rxjs/operators";
66
import {
77
DatasetDetailComponentConfig,
@@ -65,6 +65,7 @@ export class MainMenuConfiguration {
6565
}
6666

6767
export interface AppConfigInterface {
68+
allowConfigOverrides?: boolean;
6869
skipSciCatLoginPageEnabled?: boolean;
6970
accessTokenPrefix: string;
7071
addDatasetEnabled: boolean;
@@ -160,33 +161,41 @@ function isMainPageConfiguration(obj: any): obj is MainPageConfiguration {
160161
})
161162
export class AppConfigService {
162163
private appConfig: object = {};
163-
private configsSize = 4;
164164

165165
constructor(private http: HttpClient) {}
166166

167-
private async loadAndMerge(files: string[]): Promise<object> {
168-
const requests = files.map((f) =>
169-
this.http.get(`/assets/${f}`).pipe(catchError(() => of({}))),
170-
);
171-
const configs = await firstValueFrom(forkJoin(requests));
172-
return configs.reduce((acc, cfg) => merge(acc, cfg), {});
173-
}
174-
175167
private async mergeConfig(): Promise<object> {
176-
const normalConfigFiles = Array.from(
177-
{ length: this.configsSize },
178-
(_, i) => `config.${i}.json`,
168+
const config = await firstValueFrom(
169+
this.http.get<Partial<AppConfigInterface>>("/assets/config.json").pipe(
170+
catchError(() => {
171+
console.error("No config provided.");
172+
return of({} as Partial<AppConfigInterface>);
173+
}),
174+
),
179175
);
180-
const overrideConfigFiles = Array.from(
181-
{ length: this.configsSize },
182-
(_, i) => `config.override.${i}.json`,
176+
let configOverrideRequest: Partial<AppConfigInterface> = {};
177+
if (config?.allowConfigOverrides) {
178+
configOverrideRequest = await firstValueFrom(
179+
this.http
180+
.get<Partial<AppConfigInterface>>("/assets/config.override.json")
181+
.pipe(
182+
catchError(() => {
183+
console.error(
184+
"allowConfigOverrides set to true but no config.override provided.",
185+
);
186+
return of({} as Partial<AppConfigInterface>);
187+
}),
188+
),
189+
);
190+
}
191+
// Custom merge to replace arrays instead of merging them
192+
return mergeWith(
193+
{},
194+
config ?? {},
195+
configOverrideRequest ?? {},
196+
(objVal, srcVal) =>
197+
Array.isArray(objVal) && Array.isArray(srcVal) ? srcVal : undefined,
183198
);
184-
return await this.loadAndMerge([
185-
"config.json",
186-
...normalConfigFiles,
187-
"config.override.json",
188-
...overrideConfigFiles,
189-
]);
190199
}
191200

192201
async loadAppConfig(): Promise<void> {
@@ -198,12 +207,8 @@ export class AppConfigService {
198207
this.appConfig = Object.assign({}, this.appConfig, config);
199208
} catch (err) {
200209
console.log("No config available in backend, trying with local config.");
201-
try {
202-
const config = await this.mergeConfig();
203-
this.appConfig = Object.assign({}, this.appConfig, config);
204-
} catch (err) {
205-
console.error("No config provided.");
206-
}
210+
const config = await this.mergeConfig();
211+
this.appConfig = Object.assign({}, this.appConfig, config);
207212
}
208213

209214
const config: AppConfigInterface = this.appConfig as AppConfigInterface;

src/app/datasets/dataset-lifecycle/dataset-lifecycle.component.spec.ts

Lines changed: 26 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,10 @@ import {
55
waitForAsync,
66
} from "@angular/core/testing";
77

8-
import { DatasetLifecycleComponent } from "./dataset-lifecycle.component";
8+
import {
9+
DatasetLifecycleComponent,
10+
HistoryWithProperties,
11+
} from "./dataset-lifecycle.component";
912

1013
import { NO_ERRORS_SCHEMA } from "@angular/core";
1114
import { PipesModule } from "shared/pipes/pipes.module";
@@ -121,31 +124,28 @@ describe("DatasetLifecycleComponent", () => {
121124
expect(parsedHistoryItems.length).toEqual(0);
122125
});
123126

124-
// it("should parse dataset.history into a HistoryItem array if dataset is defined", () => {
125-
// const keywords = ["test", "parse"];
126-
// const dataset = createMock<
127-
// OutputDatasetObsoleteDto & { history: HistoryClass[] }
128-
// >({ ...mockDataset });
129-
// // TODO: Check the types here and see if we need the keywords at all or not as it doesn't exist on the HistoryClass.
130-
// dataset.history = [
131-
// {
132-
// id: "testId",
133-
// keywords,
134-
// updatedBy: "Test User",
135-
// updatedAt: new Date().toISOString(),
136-
// },
137-
// ] as unknown as HistoryClass[];
138-
139-
// component.dataset = dataset;
140-
// const parsedHistoryItems = component["parseHistoryItems"]();
141-
142-
// expect(parsedHistoryItems.length).toEqual(1);
143-
// parsedHistoryItems.forEach((item) => {
144-
// expect(Object.keys(item).includes("id")).toEqual(false);
145-
// expect(item.property).toEqual("keywords");
146-
// expect(item.value).toEqual(keywords);
147-
// });
148-
// });
127+
it("should parse dataset.history into a HistoryItem array if dataset is defined", () => {
128+
const keywords = ["test", "parse"];
129+
const dataset = createMock<OutputDatasetObsoleteDto>({ ...mockDataset });
130+
dataset.history = [
131+
{
132+
id: "testId",
133+
keywords,
134+
updatedBy: "Test User",
135+
updatedAt: new Date().toISOString(),
136+
},
137+
] as HistoryWithProperties[];
138+
139+
component.dataset = dataset;
140+
const parsedHistoryItems = component["parseHistoryItems"]();
141+
142+
expect(parsedHistoryItems.length).toEqual(1);
143+
parsedHistoryItems.forEach((item) => {
144+
expect("id" in item).toBe(false);
145+
expect(item.property).toEqual("keywords");
146+
expect(item.value).toEqual(keywords);
147+
});
148+
});
149149
});
150150

151151
describe("#downloadCsv()", () => {

src/app/datasets/dataset-lifecycle/dataset-lifecycle.component.ts

Lines changed: 18 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { Component, OnInit, OnChanges, SimpleChange } from "@angular/core";
22
import {
3-
DatasetClass,
3+
HistoryClass,
44
OutputDatasetObsoleteDto,
55
} from "@scicatproject/scicat-sdk-ts-angular";
66
import {
@@ -25,6 +25,9 @@ export interface HistoryItem {
2525
[key: string]: any;
2626
}
2727

28+
// unfortunately the index signature in HistoryClass is not exported by the openapi sdk generator
29+
export type HistoryWithProperties = HistoryClass & { [key: string]: unknown };
30+
2831
@Component({
2932
selector: "dataset-lifecycle",
3033
templateUrl: "./dataset-lifecycle.component.html",
@@ -63,22 +66,20 @@ export class DatasetLifecycleComponent implements OnInit, OnChanges {
6366
) {}
6467

6568
private parseHistoryItems(): HistoryItem[] {
66-
// TODO: This should be checked because something is wrong with the types
67-
// TODO: The following code is commented out and should be refactored because the history is no longer part of the dataset object anymore due to this PR from Scicat BE: "feat: implements history for many entities #1939"
68-
// const dataset = this.dataset as DatasetClass;
69-
// if (dataset && dataset.history) {
70-
// const history = dataset.history.map(
71-
// ({ updatedAt, updatedBy, id, ...properties }) =>
72-
// Object.keys(properties).map((property) => ({
73-
// property,
74-
// value: properties[property],
75-
// updatedBy: updatedBy.replace("ldap.", ""),
76-
// updatedAt: this.datePipe.transform(updatedAt, "yyyy-MM-dd HH:mm"),
77-
// })),
78-
// );
79-
// // flatten and reverse array before return
80-
// return [].concat(...history).reverse();
81-
// }
69+
const dataset = this.dataset;
70+
if (dataset && dataset.history) {
71+
const history = (dataset.history as HistoryWithProperties[]).map(
72+
({ updatedAt, updatedBy, id, _id, ...properties }) =>
73+
Object.keys(properties).map((property) => ({
74+
property,
75+
value: properties[property],
76+
updatedBy: updatedBy.replace("ldap.", ""),
77+
updatedAt: this.datePipe.transform(updatedAt),
78+
})),
79+
);
80+
// flatten array before return
81+
return [].concat(...history);
82+
}
8283
return [];
8384
}
8485

src/app/datasets/datasets-filter/datasets-filter.component.html

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
(textChange)="setFilter(filter.key, $event)"
2929
(dateRangeChange)="setDateFilter(filter.key, $event)"
3030
(selectionChange)="selectionChange($event)"
31+
(checkBoxChange)="setFilter(filter.key, $event)"
3132
(numericRangeChange)="numericRangeChange(filter.key, $event)"
3233
></shared-filter>
3334
</ng-container>
@@ -229,7 +230,10 @@
229230
(input)="updateConditionUnit(i, $event)"
230231
[matAutocomplete]="rhsUnits"
231232
/>
232-
<mat-autocomplete #rhsUnits="matAutocomplete" (optionSelected)="updateConditionUnit(i, $event)">
233+
<mat-autocomplete
234+
#rhsUnits="matAutocomplete"
235+
(optionSelected)="updateConditionUnit(i, $event)"
236+
>
233237
<mat-option
234238
*ngFor="let unit of getUnits(conditionConfig.condition.lhs)"
235239
[value]="unit"

0 commit comments

Comments
 (0)