[ACS-11973] Fix: context menu disappears during bulk upload (#5236)

* [11973] Fix: context menu disappears during bulk upload

* sonar & copilot fix

* [ACS-11973] cr fix
This commit is contained in:
Mykyta Maliarchuk
2026-06-25 10:24:17 +02:00
committed by GitHub
parent c9e3338ccf
commit b2149441f0
11 changed files with 151 additions and 36 deletions
@@ -31,7 +31,7 @@ import { AppTestingModule } from '../../testing/app-testing.module';
import { AppService, ContentApiService } from '@alfresco/aca-shared';
import { getTitleElementText } from '../../testing/test-utils';
import { MatSnackBarModule } from '@angular/material/snack-bar';
import { testHeader } from '../../testing/document-base-page-utils';
import { testHeader, testUploadEvents } from '../../testing/document-base-page-utils';
describe('FavoritesComponent', () => {
let fixture: ComponentFixture<FavoritesComponent>;
@@ -143,4 +143,9 @@ describe('FavoritesComponent', () => {
});
testHeader(FavoritesComponent);
testUploadEvents(
() => component,
() => fixture
);
});
@@ -80,8 +80,8 @@ export class FavoritesComponent extends PageComponent implements OnInit {
super.ngOnInit();
this.subscriptions = this.subscriptions.concat([
this.uploadService.fileUploadComplete.pipe(debounceTime(300)).subscribe(() => this.reload()),
this.uploadService.fileUploadDeleted.pipe(debounceTime(300)).subscribe(() => this.reload())
this.uploadService.fileUploadComplete.pipe(debounceTime(300)).subscribe(() => this.reloadWithoutResettingSelection()),
this.uploadService.fileUploadDeleted.pipe(debounceTime(300)).subscribe(() => this.reloadWithoutResettingSelection())
]);
this.columns = this.extensions.documentListPresets.favorites;
@@ -25,7 +25,14 @@
import { TestBed, fakeAsync, tick, ComponentFixture } from '@angular/core/testing';
import { NO_ERRORS_SCHEMA, SimpleChange, SimpleChanges } from '@angular/core';
import { Router, ActivatedRoute, convertToParamMap, ParamMap } from '@angular/router';
import { DocumentListService, FilterSearch, SearchHeaderQueryBuilderService, UploadService } from '@alfresco/adf-content-services';
import {
DocumentListService,
FileUploadCompleteEvent,
FileUploadDeleteEvent,
FilterSearch,
SearchHeaderQueryBuilderService,
UploadService
} from '@alfresco/adf-content-services';
import { NodeActionsService } from '../../services/node-actions.service';
import { FilesComponent } from './files.component';
import { AppTestingModule } from '../../testing/app-testing.module';
@@ -243,6 +250,7 @@ describe('FilesComponent', () => {
describe('refresh on events', () => {
beforeEach(() => {
spyOn(component, 'reload');
spyOn(component, 'reloadWithoutResettingSelection');
fixture.detectChanges();
spyOn(component.documentList, 'loadFolder').and.callFake(() => {});
@@ -288,32 +296,34 @@ describe('FilesComponent', () => {
expect(component.reload).not.toHaveBeenCalled();
});
it('should call refresh on fileUploadComplete event if parent node match', fakeAsync(() => {
const file: any = { file: { options: { parentId: 'parentId' } } };
component.node = { id: 'parentId' } as any;
it('should call reloadWithoutResettingSelection on fileUploadComplete event if parent node match', fakeAsync(() => {
const file = { file: { options: { parentId: 'parentId' } } } as FileUploadCompleteEvent;
component.node = { id: 'parentId' } as Node;
uploadService.fileUploadComplete.next(file);
tick(500);
expect(component.reload).toHaveBeenCalled();
expect(component.reload).not.toHaveBeenCalled();
expect(component.reloadWithoutResettingSelection).toHaveBeenCalled();
}));
it('should not call reload on fileUploadComplete event if file parent folder already displayed', fakeAsync(() => {
spyOn(component.documentList.data, 'getRows').and.returnValue([{ node: { entry: { isFolder: true, name: 'files' } } }] as any);
const file: any = { file: { options: { parentId: 'parentId', path: '/files' } } };
component.node = { id: 'parentId' } as any;
const file = { file: { options: { parentId: 'parentId', path: '/files' } } } as FileUploadCompleteEvent;
component.node = { id: 'parentId' } as Node;
uploadService.fileUploadComplete.next(file);
tick(500);
expect(component.reload).not.toHaveBeenCalled();
expect(component.reloadWithoutResettingSelection).not.toHaveBeenCalled();
}));
it('should not call refresh on fileUploadComplete event if parent mismatch', fakeAsync(() => {
const file: any = { file: { options: { parentId: 'otherId' } } };
component.node = { id: 'parentId' } as any;
const file = { file: { options: { parentId: 'otherId' } } } as FileUploadCompleteEvent;
component.node = { id: 'parentId' } as Node;
uploadService.fileUploadComplete.next(file);
@@ -322,26 +332,58 @@ describe('FilesComponent', () => {
expect(component.reload).not.toHaveBeenCalled();
}));
it('should call refresh on fileUploadDeleted event if parent node match', fakeAsync(() => {
const file: any = { file: { options: { parentId: 'parentId' } } };
component.node = { id: 'parentId' } as any;
uploadService.fileUploadDeleted.next(file);
tick(500);
expect(component.reload).toHaveBeenCalled();
}));
it('should not call refresh on fileUploadDeleted event if parent mismatch', fakeAsync(() => {
const file: any = { file: { options: { parentId: 'otherId' } } };
component.node = { id: 'parentId' } as any;
it('should call reloadWithoutResettingSelection on fileUploadDeleted event when folder parent uploaded to current folder', fakeAsync(() => {
const file = { file: { options: { parentId: 'parentId' } } } as FileUploadDeleteEvent;
component.node = { id: 'parentId' } as Node;
uploadService.fileUploadDeleted.next(file);
tick(500);
expect(component.reload).not.toHaveBeenCalled();
expect(component.reloadWithoutResettingSelection).toHaveBeenCalled();
}));
it('should not call refresh on fileUploadDeleted event if parent mismatch', fakeAsync(() => {
const file = { file: { options: { parentId: 'otherId' } } } as FileUploadDeleteEvent;
component.node = { id: 'parentId' } as Node;
uploadService.fileUploadDeleted.next(file);
tick(500);
expect(component.reload).not.toHaveBeenCalled();
}));
it('should call reloadWithoutResettingSelection when uploaded file belongs to current folder', fakeAsync(() => {
const file = { file: { data: { entry: { parentId: 'folder-id' } }, options: {} } } as FileUploadCompleteEvent;
component.node = { id: 'folder-id' } as Node;
uploadService.fileUploadComplete.next(file);
tick(500);
expect(component.reload).not.toHaveBeenCalled();
expect(component.reloadWithoutResettingSelection).toHaveBeenCalled();
}));
it('should NOT call reloadWithoutResettingSelection when uploaded file belongs to a different folder', fakeAsync(() => {
const file = { file: { data: { entry: { parentId: 'other-folder-id' } }, options: {} } } as FileUploadCompleteEvent;
component.node = { id: 'folder-id' } as Node;
uploadService.fileUploadComplete.next(file);
tick(500);
expect(component.reloadWithoutResettingSelection).not.toHaveBeenCalled();
}));
it('should call reloadWithoutResettingSelection on fileUploadDeleted when file belongs to current folder', fakeAsync(() => {
const file = { file: { data: { entry: { parentId: 'folder-id' } }, options: {} } } as FileUploadDeleteEvent;
component.node = { id: 'folder-id' } as Node;
uploadService.fileUploadDeleted.next(file);
tick(500);
expect(component.reloadWithoutResettingSelection).toHaveBeenCalled();
}));
});
@@ -271,7 +271,7 @@ export class FilesComponent extends PageComponent implements OnInit, OnDestroy {
// check root and child nodes
if (node?.entry?.parentId === this.getParentNodeId()) {
this.reload(this.selectedNode);
this.reloadWithoutResettingSelection();
return;
}
@@ -303,7 +303,7 @@ export class FilesComponent extends PageComponent implements OnInit, OnDestroy {
if (alreadyDisplayedParentFolder) {
return;
}
this.reload(this.selectedNode);
this.reloadWithoutResettingSelection();
}
onContentAdded(nodes: NodeEntry[]) {
@@ -31,7 +31,7 @@ import { NodePaging, SearchApi } from '@alfresco/js-api';
import { of } from 'rxjs';
import { getTitleElementText } from '../../testing/test-utils';
import { MatSnackBarModule } from '@angular/material/snack-bar';
import { testHeader } from '../../testing/document-base-page-utils';
import { testHeader, testUploadEvents } from '../../testing/document-base-page-utils';
describe('RecentFilesComponent', () => {
let fixture: ComponentFixture<RecentFilesComponent>;
@@ -113,4 +113,9 @@ describe('RecentFilesComponent', () => {
});
testHeader(RecentFilesComponent);
testUploadEvents(
() => component,
() => fixture
);
});
@@ -77,8 +77,8 @@ export class RecentFilesComponent extends PageComponent implements OnInit {
super.ngOnInit();
this.subscriptions = this.subscriptions.concat([
this.uploadService.fileUploadComplete.pipe(debounceTime(300)).subscribe(() => this.reload()),
this.uploadService.fileUploadDeleted.pipe(debounceTime(300)).subscribe(() => this.reload())
this.uploadService.fileUploadComplete.pipe(debounceTime(300)).subscribe(() => this.reloadWithoutResettingSelection()),
this.uploadService.fileUploadDeleted.pipe(debounceTime(300)).subscribe(() => this.reloadWithoutResettingSelection())
]);
this.columns = this.extensions.documentListPresets.recent || [];
@@ -33,7 +33,7 @@ import { AppService } from '@alfresco/aca-shared';
import { getTitleElementText } from '../../testing/test-utils';
import { ActivatedRoute, NavigationStart, Router } from '@angular/router';
import { MatSnackBarModule } from '@angular/material/snack-bar';
import { testHeader } from '../../testing/document-base-page-utils';
import { testHeader, testUploadEvents } from '../../testing/document-base-page-utils';
describe('SharedFilesComponent', () => {
let fixture: ComponentFixture<SharedFilesComponent>;
@@ -108,4 +108,9 @@ describe('SharedFilesComponent', () => {
});
testHeader(SharedFilesComponent);
testUploadEvents(
() => component,
() => fixture
);
});
@@ -81,8 +81,8 @@ export class SharedFilesComponent extends PageComponent implements OnInit {
this.subscriptions = this.subscriptions.concat([
this.appHookService.linksUnshared.pipe(debounceTime(300)).subscribe(() => this.reload()),
this.uploadService.fileUploadComplete.pipe(debounceTime(300)).subscribe(() => this.reload()),
this.uploadService.fileUploadDeleted.pipe(debounceTime(300)).subscribe(() => this.reload())
this.uploadService.fileUploadComplete.pipe(debounceTime(300)).subscribe(() => this.reloadWithoutResettingSelection()),
this.uploadService.fileUploadDeleted.pipe(debounceTime(300)).subscribe(() => this.reloadWithoutResettingSelection())
]);
this.columns = this.extensions.documentListPresets.shared || [];
@@ -23,11 +23,18 @@
*/
import { BehaviorSubject, Subject } from 'rxjs';
import { AgentService, SearchAiInputState, SearchAiService } from '@alfresco/adf-content-services';
import {
AgentService,
FileUploadCompleteEvent,
FileUploadDeleteEvent,
SearchAiInputState,
SearchAiService,
UploadService
} from '@alfresco/adf-content-services';
import { DebugElement, Type } from '@angular/core';
import { By } from '@angular/platform-browser';
import { SearchAiInputContainerComponent } from '../components/knowledge-retrieval/search-ai/search-ai-input-container/search-ai-input-container.component';
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { ComponentFixture, fakeAsync, TestBed, tick } from '@angular/core/testing';
import { PageComponent } from '@alfresco/aca-shared';
import { Agent } from '@alfresco/js-api/typings';
@@ -101,3 +108,30 @@ export const testHeader = <T extends PageComponent>(component: Type<T>, checkHea
}
});
};
export const testUploadEvents = <T extends PageComponent>(getComponent: () => T, getFixture: () => ComponentFixture<T>) => {
describe('upload events', () => {
beforeEach(() => {
const component: PageComponent = getComponent();
spyOn(component, 'reload');
spyOn(component, 'reloadWithoutResettingSelection');
getFixture().detectChanges();
});
it('should call reloadWithoutResettingSelection and not reload on fileUploadComplete', fakeAsync(() => {
TestBed.inject(UploadService).fileUploadComplete.next({} as FileUploadCompleteEvent);
tick(300);
const component = getComponent();
expect(component.reloadWithoutResettingSelection).toHaveBeenCalled();
expect(component.reload).not.toHaveBeenCalled();
}));
it('should call reloadWithoutResettingSelection and not reload on fileUploadDeleted', fakeAsync(() => {
TestBed.inject(UploadService).fileUploadDeleted.next({} as FileUploadDeleteEvent);
tick(300);
const component = getComponent();
expect(component.reloadWithoutResettingSelection).toHaveBeenCalled();
expect(component.reload).not.toHaveBeenCalled();
}));
});
};
@@ -226,6 +226,13 @@ export abstract class PageComponent implements OnInit, OnDestroy, OnChanges {
}
}
reloadWithoutResettingSelection(): void {
if (this.isOutletPreviewUrl()) {
return;
}
this.documentListService.reloadSilently();
}
trackByActionId(_: number, action: ContentActionRef) {
return action.id;
}
@@ -157,6 +157,23 @@ describe('PageComponent', () => {
expect(store.dispatch['calls'].mostRecent().args[0]).toEqual(new SetSelectedNodesAction([node]));
});
it('should call documentListService.reloadSilently()', () => {
spyOn(documentListService, 'reloadSilently');
component.reloadWithoutResettingSelection();
expect(documentListService.reloadSilently).toHaveBeenCalledWith();
});
it('should not call documentListService.reloadSilently() when url contains viewer outlet', () => {
window.history.pushState({}, null, `${locationHref}#test(viewer:view)`);
spyOn(documentListService, 'reloadSilently');
component.reloadWithoutResettingSelection();
expect(documentListService.reloadSilently).not.toHaveBeenCalled();
});
it('should call ViewNodeAction on showPreview for selected node', () => {
spyOn(store, 'dispatch');
const node = {