Skip to content

Commit 74448ac

Browse files
authored
[ACS-10035] Ensure ADW handles no PUT for preferences API method in ACS below 25.x v. [PoC] (#4750)
* [ACS-10035]: adds evaluator and helper functions * [ACS-10035]: disables navbar savedSearch for non-supported versions * [ACS-10035]: extends app extension service to allow manual rule evaluation; clean up * [ACS-10035]: introduces pipe as an alternative option for compatibility check * [ACS-10035]: disables save search feature if is not supported * [ACS-10035]: adds test for new method * [ACS-10035]: sonarQube issue * [10035]: adds unit tests for evaluators and helper fns * [ACS-10035]: fixes failed test * [ACS-10035]: fixes naming * [ACS-10035]: fixes race condition issue on direct page refresh * [ACS-10035]: fixes import * [ACS-10035]: sonarQube issues * [ACS-10035]: fixes sonarQube with fake versions * [ACS-10035]: fixes tests * [ACS-10035]: improves pipe logic stream * [ACS-10035]: fixes sonarQube; adjusts tests * [ACS-10035]: adds documentation * [ACS-10035]: exposes isFeatureSupportedInCurrentAcs from aca-content lib * [ACS-10035]: minor fixes * [ACS-10035]: typo fix
1 parent 9395d98 commit 74448ac

File tree

13 files changed

+260
-16
lines changed

13 files changed

+260
-16
lines changed

docs/extending/rules-list.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,4 +76,10 @@ or not.
7676
| 1.7.0 | app.navigation.isPreview | Current page is **Preview**. |
7777
| 5.1.1 | app.navigation.isDetails | User is currently on the **Folder Details** page. |
7878

79+
#### ACS Versions compatibility Rules/Evaluators
7980

81+
Rules/Evaluators created for specific features in ADW to be checked if supported in current ACS version. Evaluators are created using **createVersionRule** helper function locking specific version number into the rule.
82+
83+
| Version | Key | Description |
84+
|---------|---------------------------------|---------------------------------------------------------------------------|
85+
| 8.1.0 | isSavedSearchAvailable | Checks whether current ACS version supports PUT method in Preferences API |

projects/aca-content/assets/app.extensions.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -212,7 +212,10 @@
212212
"items": [
213213
{
214214
"id": "app.search.navbar",
215-
"component": "app.search.navbar"
215+
"component": "app.search.navbar",
216+
"rules": {
217+
"visible": "isSavedSearchAvailable"
218+
}
216219
}
217220
]
218221
}

projects/aca-content/src/lib/aca-content.module.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,7 @@ import { SaveSearchSidenavComponent } from './components/search/search-save/side
134134
isSmartFolder: rules.isSmartFolder,
135135
isMultiSelection: rules.isMultiselection,
136136
canPrintFile: rules.canPrintFile,
137+
isSavedSearchAvailable: rules.isSavedSearchAvailable,
137138

138139
'app.selection.canDelete': rules.canDeleteSelection,
139140
'app.selection.canDownload': rules.canDownloadSelection,

projects/aca-content/src/lib/components/search/search-results/search-results.component.html

Lines changed: 13 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -26,17 +26,18 @@
2626
<div class="aca-content__advanced-filters--header">
2727
<p>{{ 'APP.BROWSE.SEARCH.ADVANCED_FILTERS' | translate }}</p>
2828
<div class="aca-content__advanced-filters--header--action-buttons">
29-
<button
30-
*ngIf="initialSavedSearch !== undefined else saveSearchButton"
31-
mat-button
32-
[disabled]="!encodedQuery"
33-
class="aca-content__save-search-action"
34-
title="{{ 'APP.BROWSE.SEARCH.SAVE_SEARCH.ACTION_BUTTON' | translate }}"
35-
[attr.aria-label]="'APP.BROWSE.SEARCH.SAVE_SEARCH.ACTION_BUTTON' | translate "
36-
[matMenuTriggerFor]="saveSearchOptionsMenu">
37-
{{ 'APP.BROWSE.SEARCH.SAVE_SEARCH.ACTION_BUTTON' | translate }}
38-
<mat-icon iconPositionEnd>keyboard_arrow_down</mat-icon>
39-
</button>
29+
@if('isSavedSearchAvailable' | isFeatureSupportedInCurrentAcs | async) {
30+
<button
31+
*ngIf="initialSavedSearch !== undefined else saveSearchButton"
32+
mat-button
33+
[disabled]="!encodedQuery"
34+
class="aca-content__save-search-action"
35+
title="{{ 'APP.BROWSE.SEARCH.SAVE_SEARCH.ACTION_BUTTON' | translate }}"
36+
[attr.aria-label]="'APP.BROWSE.SEARCH.SAVE_SEARCH.ACTION_BUTTON' | translate "
37+
[matMenuTriggerFor]="saveSearchOptionsMenu">
38+
{{ 'APP.BROWSE.SEARCH.SAVE_SEARCH.ACTION_BUTTON' | translate }}
39+
<mat-icon iconPositionEnd>keyboard_arrow_down</mat-icon>
40+
</button>
4041
<mat-menu #saveSearchOptionsMenu="matMenu">
4142
<button
4243
mat-menu-item
@@ -66,6 +67,7 @@
6667
{{ 'APP.BROWSE.SEARCH.SAVE_SEARCH.ACTION_BUTTON' | translate }}
6768
</button>
6869
</ng-template>
70+
}
6971
<button
7072
mat-button
7173
adf-reset-search

projects/aca-content/src/lib/components/search/search-results/search-results.component.spec.ts

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,13 +24,14 @@
2424

2525
import { ComponentFixture, fakeAsync, TestBed, tick } from '@angular/core/testing';
2626
import { SearchResultsComponent } from './search-results.component';
27+
import { Pipe, PipeTransform } from '@angular/core';
2728
import { AppConfigService, NotificationService, TranslationService } from '@alfresco/adf-core';
2829
import { Store } from '@ngrx/store';
2930
import { NavigateToFolder } from '@alfresco/aca-shared/store';
3031
import { Pagination, SearchRequest } from '@alfresco/js-api';
3132
import { SavedSearchesService, SearchQueryBuilderService } from '@alfresco/adf-content-services';
3233
import { ActivatedRoute, Event, NavigationStart, Params, Router } from '@angular/router';
33-
import { BehaviorSubject, of, Subject, throwError } from 'rxjs';
34+
import { BehaviorSubject, Observable, of, Subject, throwError } from 'rxjs';
3435
import { AppTestingModule } from '../../../testing/app-testing.module';
3536
import { AppService } from '@alfresco/aca-shared';
3637
import { MatSnackBarModule, MatSnackBarRef } from '@angular/material/snack-bar';
@@ -42,6 +43,13 @@ import { NoopAnimationsModule } from '@angular/platform-browser/animations';
4243
import { MatMenuModule } from '@angular/material/menu';
4344
import { MatMenuHarness } from '@angular/material/menu/testing';
4445

46+
@Pipe({ name: 'isFeatureSupportedInCurrentAcs' })
47+
class MockIsFeatureSupportedInCurrentAcsPipe implements PipeTransform {
48+
transform(): Observable<boolean> {
49+
return of(true);
50+
}
51+
}
52+
4553
describe('SearchComponent', () => {
4654
let component: SearchResultsComponent;
4755
let fixture: ComponentFixture<SearchResultsComponent>;
@@ -112,6 +120,12 @@ describe('SearchComponent', () => {
112120
]
113121
});
114122

123+
TestBed.overrideComponent(SearchResultsComponent, {
124+
add: {
125+
imports: [MockIsFeatureSupportedInCurrentAcsPipe]
126+
}
127+
});
128+
115129
config = TestBed.inject(AppConfigService);
116130
store = TestBed.inject(Store);
117131
queryBuilder = TestBed.inject(SearchQueryBuilderService);

projects/aca-content/src/lib/components/search/search-results/search-results.component.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,7 @@ import { SaveSearchDirective } from '../search-save/directive/save-search.direct
8888
import { combineLatest, of } from 'rxjs';
8989
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
9090
import { MatMenuModule } from '@angular/material/menu';
91+
import { IsFeatureSupportedInCurrentAcsPipe } from '../../../pipes/is-feature-supported.pipe';
9192

9293
@Component({
9394
imports: [
@@ -121,7 +122,8 @@ import { MatMenuModule } from '@angular/material/menu';
121122
ViewerToolbarComponent,
122123
BulkActionsDropdownComponent,
123124
SearchAiInputContainerComponent,
124-
SaveSearchDirective
125+
SaveSearchDirective,
126+
IsFeatureSupportedInCurrentAcsPipe
125127
],
126128
selector: 'aca-search-results',
127129
templateUrl: './search-results.component.html',
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
/*!
2+
* Copyright © 2005-2025 Hyland Software, Inc. and its affiliates. All rights reserved.
3+
*
4+
* Alfresco Example Content Application
5+
*
6+
* This file is part of the Alfresco Example Content Application.
7+
* If the software was purchased under a paid Alfresco license, the terms of
8+
* the paid license agreement will prevail. Otherwise, the software is
9+
* provided under the following open source license terms:
10+
*
11+
* The Alfresco Example Content Application is free software: you can redistribute it and/or modify
12+
* it under the terms of the GNU Lesser General Public License as published by
13+
* the Free Software Foundation, either version 3 of the License, or
14+
* (at your option) any later version.
15+
*
16+
* The Alfresco Example Content Application is distributed in the hope that it will be useful,
17+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
18+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
19+
* GNU Lesser General Public License for more details.
20+
*
21+
* You should have received a copy of the GNU Lesser General Public License
22+
* from Hyland Software. If not, see <http://www.gnu.org/licenses/>.
23+
*/
24+
25+
import { of } from 'rxjs';
26+
import { IsFeatureSupportedInCurrentAcsPipe } from './is-feature-supported.pipe';
27+
import { TestBed } from '@angular/core/testing';
28+
import { AppExtensionService } from '@alfresco/aca-shared';
29+
import { AppStore } from '@alfresco/aca-shared/store';
30+
import { Store } from '@ngrx/store';
31+
32+
describe('IsFeatureSupportedInCurrentAcsPipe', () => {
33+
let serviceSpy: jasmine.SpyObj<AppExtensionService>;
34+
let storeSpy: jasmine.SpyObj<Store<AppStore>>;
35+
let pipe: IsFeatureSupportedInCurrentAcsPipe;
36+
37+
beforeEach(() => {
38+
serviceSpy = jasmine.createSpyObj('AppExtensionService', ['isFeatureSupported']);
39+
storeSpy = jasmine.createSpyObj('Store', ['dispatch', 'select']);
40+
TestBed.configureTestingModule({
41+
providers: [IsFeatureSupportedInCurrentAcsPipe, { provide: AppExtensionService, useValue: serviceSpy }, { provide: Store, useValue: storeSpy }]
42+
});
43+
pipe = TestBed.inject(IsFeatureSupportedInCurrentAcsPipe);
44+
});
45+
46+
it('should call isFeatureSupported in AppExtensionService', (done) => {
47+
serviceSpy.isFeatureSupported.and.returnValue(false);
48+
storeSpy.select.and.returnValue(of('7.4.0'));
49+
pipe.transform('someFeature').subscribe((result) => {
50+
expect(result).toBe(false);
51+
expect(serviceSpy.isFeatureSupported).toHaveBeenCalledWith('someFeature');
52+
done();
53+
});
54+
});
55+
});
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
/*!
2+
* Copyright © 2005-2025 Hyland Software, Inc. and its affiliates. All rights reserved.
3+
*
4+
* Alfresco Example Content Application
5+
*
6+
* This file is part of the Alfresco Example Content Application.
7+
* If the software was purchased under a paid Alfresco license, the terms of
8+
* the paid license agreement will prevail. Otherwise, the software is
9+
* provided under the following open source license terms:
10+
*
11+
* The Alfresco Example Content Application is free software: you can redistribute it and/or modify
12+
* it under the terms of the GNU Lesser General Public License as published by
13+
* the Free Software Foundation, either version 3 of the License, or
14+
* (at your option) any later version.
15+
*
16+
* The Alfresco Example Content Application is distributed in the hope that it will be useful,
17+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
18+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
19+
* GNU Lesser General Public License for more details.
20+
*
21+
* You should have received a copy of the GNU Lesser General Public License
22+
* from Hyland Software. If not, see <http://www.gnu.org/licenses/>.
23+
*/
24+
25+
import { AppExtensionService } from '@alfresco/aca-shared';
26+
import { Pipe, PipeTransform } from '@angular/core';
27+
import { AppStore, getRepositoryStatus } from '@alfresco/aca-shared/store';
28+
import { Store } from '@ngrx/store';
29+
import { map, Observable } from 'rxjs';
30+
31+
@Pipe({
32+
name: 'isFeatureSupportedInCurrentAcs'
33+
})
34+
export class IsFeatureSupportedInCurrentAcsPipe implements PipeTransform {
35+
constructor(
36+
private readonly appExtensionsService: AppExtensionService,
37+
private readonly store: Store<AppStore>
38+
) {}
39+
40+
transform(evaluatorId: string): Observable<boolean> {
41+
return this.store.select(getRepositoryStatus).pipe(map(() => this.appExtensionsService.isFeatureSupported(evaluatorId)));
42+
}
43+
}

projects/aca-content/src/public-api.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,3 +33,4 @@ export * from './lib/services/content-url.service';
3333
export * from './lib/services/content-management.service';
3434
export * from './lib/components/info-drawer/comments-tab/external-node-permission-comments-tab.service';
3535
export * from './lib/utils/aca-search-utils';
36+
export * from './lib/pipes/is-feature-supported.pipe';

projects/aca-shared/rules/src/app.rules.spec.ts

Lines changed: 67 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,10 +23,10 @@
2323
*/
2424

2525
import * as app from './app.rules';
26-
import { getFileExtension } from './app.rules';
26+
import { createVersionRule, getFileExtension, isSavedSearchAvailable } from './app.rules';
2727
import { TestRuleContext } from './test-rule-context';
2828
import { NodeEntry, RepositoryInfo, StatusInfo } from '@alfresco/js-api';
29-
import { ProfileState } from '@alfresco/adf-extensions';
29+
import { ProfileState, RuleContext } from '@alfresco/adf-extensions';
3030
import { AppConfigService } from '@alfresco/adf-core';
3131

3232
describe('app.evaluators', () => {
@@ -1198,6 +1198,71 @@ describe('app.evaluators', () => {
11981198
});
11991199
});
12001200

1201+
describe('Versions compatibility', () => {
1202+
function makeContext(versionDisplay?: string): RuleContext {
1203+
return {
1204+
repository: {
1205+
version: versionDisplay ? { display: versionDisplay } : undefined
1206+
}
1207+
} as RuleContext;
1208+
}
1209+
1210+
describe('isSavedSearchAvailable', () => {
1211+
it('should return true if ACS version is equal to minimal version', () => {
1212+
expect(isSavedSearchAvailable(makeContext('25.1.0'))).toBe(true);
1213+
});
1214+
1215+
it('should return true if ACS version is greater than minimal version', () => {
1216+
expect(isSavedSearchAvailable(makeContext('25.2.0'))).toBe(true);
1217+
expect(isSavedSearchAvailable(makeContext('26.0.0'))).toBe(true);
1218+
});
1219+
1220+
it('should return false if ACS version is less than minimal version', () => {
1221+
expect(isSavedSearchAvailable(makeContext('24.4.0'))).toBe(false);
1222+
expect(isSavedSearchAvailable(makeContext('25.0.9'))).toBe(false);
1223+
});
1224+
1225+
it('should return false if ACS version is missing', () => {
1226+
expect(isSavedSearchAvailable(makeContext())).toBe(false);
1227+
expect(isSavedSearchAvailable({ repository: {} } as any)).toBe(false);
1228+
});
1229+
});
1230+
1231+
describe('createVersionRule', () => {
1232+
it('should return true if version is equal to minimal version', () => {
1233+
const rule = createVersionRule('25.1.0');
1234+
expect(rule(makeContext('25.1.0'))).toBe(true);
1235+
});
1236+
1237+
it('should return true if version is greater than minimal version', () => {
1238+
const rule = createVersionRule('25.1.0');
1239+
expect(rule(makeContext('25.2.0'))).toBe(true);
1240+
expect(rule(makeContext('26.0.0'))).toBe(true);
1241+
expect(rule(makeContext('25.1.1'))).toBe(true);
1242+
});
1243+
1244+
it('should return false if version is less than minimal version', () => {
1245+
const rule = createVersionRule('25.1.0');
1246+
expect(rule(makeContext('25.0.9'))).toBe(false);
1247+
expect(rule(makeContext('24.9.0'))).toBe(false);
1248+
});
1249+
1250+
it('should return false if version is missing', () => {
1251+
const rule = createVersionRule('25.1.0');
1252+
expect(rule(makeContext())).toBe(false);
1253+
expect(rule({ repository: {} } as any)).toBe(false);
1254+
});
1255+
1256+
it('should handle versions with different number of segments', () => {
1257+
const rule = createVersionRule('25.1.0');
1258+
expect(rule(makeContext('25.1'))).toBe(true);
1259+
expect(rule(makeContext('25.1.1'))).toBe(true);
1260+
expect(rule(makeContext('25.1.0.1-beta'))).toBe(true);
1261+
expect(rule(makeContext('25.0.1.1-rc'))).toBe(false);
1262+
});
1263+
});
1264+
});
1265+
12011266
function createTestContext(): TestRuleContext {
12021267
const context = new TestRuleContext();
12031268
context.repository = {

0 commit comments

Comments
 (0)