mirror of
https://github.com/Alfresco/alfresco-content-app.git
synced 2026-09-09 18:02:54 +00:00
[ACS-12333][ACS-12335][ACS-12334] Add Checkout / Check-in Support (#5360)
Co-authored-by: Adam Świderski <adam.tomasz.swiderski@gmail.com>
This commit is contained in:
co-authored by
Adam Świderski
parent
ff7560b7fc
commit
c4f3e988c8
@@ -50,6 +50,7 @@ import { PreviewComponent } from '@alfresco/aca-content/viewer';
|
||||
import { ToggleEditOfflineComponent } from './components/toolbar/toggle-edit-offline/toggle-edit-offline.component';
|
||||
import { ToggleFavoriteLibraryComponent } from './components/toolbar/toggle-favorite-library/toggle-favorite-library.component';
|
||||
import { ToggleFavoriteComponent } from './components/toolbar/toggle-favorite/toggle-favorite.component';
|
||||
import { LockIconComponent } from './components/dl-custom-components/lock-icon/lock-icon.component';
|
||||
import { ToggleInfoDrawerComponent } from './components/toolbar/toggle-info-drawer/toggle-info-drawer.component';
|
||||
import { ToggleJoinLibraryButtonComponent } from './components/toolbar/toggle-join-library/toggle-join-library-button.component';
|
||||
import { ToggleJoinLibraryMenuComponent } from './components/toolbar/toggle-join-library/toggle-join-library-menu.component';
|
||||
@@ -124,7 +125,8 @@ import { IsFeatureSupportedInCurrentAcsPipe } from './pipes/is-feature-supported
|
||||
'app.user.menu': UserMenuComponent,
|
||||
'app.search.columns.name': SearchResultsRowComponent,
|
||||
'app.search.navbar': SaveSearchSidenavComponent,
|
||||
'app.knowledgeDiscovery.sidenav': KnowledgeDiscoverySidenavComponent
|
||||
'app.knowledgeDiscovery.sidenav': KnowledgeDiscoverySidenavComponent,
|
||||
'app.badge.lock': LockIconComponent
|
||||
},
|
||||
evaluators: {
|
||||
canToggleJoinLibrary: rules.canToggleJoinLibrary,
|
||||
@@ -155,7 +157,11 @@ import { IsFeatureSupportedInCurrentAcsPipe } from './pipes/is-feature-supported
|
||||
'app.selection.folder': rules.hasFolderSelected,
|
||||
'app.selection.folder.canUpdate': rules.canUpdateSelectedFolder,
|
||||
'app.selection.isCheckedOut': rules.isCheckedOut,
|
||||
'app.selection.isWorkingCopy': rules.isWorkingCopy,
|
||||
'app.selection.canCheckout': rules.canCheckout,
|
||||
'app.selection.canCancelCheckout': rules.canCancelCheckout,
|
||||
'app.selection.isNodeLink': rules.isNodeLink,
|
||||
'app.selection.isLockedOrWorkingCopy': rules.isLockedOrWorkingCopy,
|
||||
|
||||
'app.navigation.folder.canCreate': rules.canCreateFolder,
|
||||
'app.navigation.isTrashcan': rules.isTrashcan,
|
||||
|
||||
+128
@@ -0,0 +1,128 @@
|
||||
/*!
|
||||
* Copyright © 2005-2026 Hyland Software, Inc. and its affiliates. All rights reserved.
|
||||
*
|
||||
* Alfresco Example Content Application
|
||||
*
|
||||
* This file is part of the Alfresco Example Content Application.
|
||||
* If the software was purchased under a paid Alfresco license, the terms of
|
||||
* the paid license agreement will prevail. Otherwise, the software is
|
||||
* provided under the following open source license terms:
|
||||
*
|
||||
* The Alfresco Example Content Application is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Lesser General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* The Alfresco Example Content Application is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Lesser General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Lesser General Public License
|
||||
* from Hyland Software. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { LockIconComponent } from './lock-icon.component';
|
||||
import { NoopTranslateModule } from '@alfresco/adf-core';
|
||||
import { NodeEntry } from '@alfresco/js-api';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
|
||||
const lockOwner = { id: 'jdoe', displayName: 'Jane Doe' };
|
||||
const workingCopyOwner = { id: 'jsmith', displayName: 'John Smith' };
|
||||
|
||||
function makeNode(aspectNames: string[], properties: Record<string, unknown> = {}): { node: NodeEntry } {
|
||||
return {
|
||||
node: {
|
||||
entry: {
|
||||
isFile: true,
|
||||
id: 'node-id',
|
||||
aspectNames,
|
||||
properties
|
||||
}
|
||||
} as NodeEntry
|
||||
};
|
||||
}
|
||||
|
||||
describe('LockIconComponent', () => {
|
||||
let fixture: ComponentFixture<LockIconComponent>;
|
||||
let component: LockIconComponent;
|
||||
let translate: TranslateService;
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [NoopTranslateModule, LockIconComponent]
|
||||
});
|
||||
|
||||
fixture = TestBed.createComponent(LockIconComponent);
|
||||
component = fixture.componentInstance;
|
||||
translate = TestBed.inject(TranslateService);
|
||||
spyOn(translate, 'instant').and.callFake((key: string, params?: Record<string, string>) => `${key}:${params?.owner ?? ''}`);
|
||||
});
|
||||
|
||||
describe('working copy (cm:workingcopy aspect)', () => {
|
||||
beforeEach(() => {
|
||||
component.data = makeNode(['cm:workingcopy'], { 'cm:workingCopyOwner': workingCopyOwner });
|
||||
component.ngOnInit();
|
||||
});
|
||||
|
||||
it('should use WORKING_COPY_BADGE translation key', () => {
|
||||
expect(translate.instant).toHaveBeenCalledWith('APP.TOOLTIPS.WORKING_COPY_BADGE', { owner: workingCopyOwner.displayName });
|
||||
});
|
||||
|
||||
it('should set tooltip with working copy owner name', () => {
|
||||
expect(component.tooltip).toBe(`APP.TOOLTIPS.WORKING_COPY_BADGE:${workingCopyOwner.displayName}`);
|
||||
});
|
||||
});
|
||||
|
||||
describe('checked-out original (cm:checkedOut aspect)', () => {
|
||||
beforeEach(() => {
|
||||
component.data = makeNode(['cm:checkedOut'], { 'cm:lockOwner': lockOwner });
|
||||
component.ngOnInit();
|
||||
});
|
||||
|
||||
it('should use LOCK_BADGE translation key', () => {
|
||||
expect(translate.instant).toHaveBeenCalledWith('APP.TOOLTIPS.LOCK_BADGE', { owner: lockOwner.displayName });
|
||||
});
|
||||
|
||||
it('should set tooltip with lock owner name', () => {
|
||||
expect(component.tooltip).toBe(`APP.TOOLTIPS.LOCK_BADGE:${lockOwner.displayName}`);
|
||||
});
|
||||
});
|
||||
|
||||
describe('generic lock (no checkout aspects)', () => {
|
||||
beforeEach(() => {
|
||||
component.data = makeNode([], { 'cm:lockOwner': lockOwner });
|
||||
component.ngOnInit();
|
||||
});
|
||||
|
||||
it('should use LOCK_BADGE translation key', () => {
|
||||
expect(translate.instant).toHaveBeenCalledWith('APP.TOOLTIPS.LOCK_BADGE', { owner: lockOwner.displayName });
|
||||
});
|
||||
|
||||
it('should set tooltip with lock owner name', () => {
|
||||
expect(component.tooltip).toBe(`APP.TOOLTIPS.LOCK_BADGE:${lockOwner.displayName}`);
|
||||
});
|
||||
});
|
||||
|
||||
describe('fallback when owner has no displayName', () => {
|
||||
it('should fall back to owner id when displayName is absent', () => {
|
||||
component.data = makeNode([], { 'cm:lockOwner': { id: 'admin' } });
|
||||
component.ngOnInit();
|
||||
expect(translate.instant).toHaveBeenCalledWith('APP.TOOLTIPS.LOCK_BADGE', { owner: 'admin' });
|
||||
});
|
||||
|
||||
it('should use empty string when owner property is absent', () => {
|
||||
component.data = makeNode([], {});
|
||||
component.ngOnInit();
|
||||
expect(translate.instant).toHaveBeenCalledWith('APP.TOOLTIPS.LOCK_BADGE', { owner: '' });
|
||||
});
|
||||
});
|
||||
|
||||
describe('missing data', () => {
|
||||
it('should not throw when data is undefined', () => {
|
||||
component.data = undefined;
|
||||
expect(() => component.ngOnInit()).not.toThrow();
|
||||
});
|
||||
});
|
||||
});
|
||||
+55
@@ -0,0 +1,55 @@
|
||||
/*!
|
||||
* Copyright © 2005-2026 Hyland Software, Inc. and its affiliates. All rights reserved.
|
||||
*
|
||||
* Alfresco Example Content Application
|
||||
*
|
||||
* This file is part of the Alfresco Example Content Application.
|
||||
* If the software was purchased under a paid Alfresco license, the terms of
|
||||
* the paid license agreement will prevail. Otherwise, the software is
|
||||
* provided under the following open source license terms:
|
||||
*
|
||||
* The Alfresco Example Content Application is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Lesser General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* The Alfresco Example Content Application is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Lesser General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Lesser General Public License
|
||||
* from Hyland Software. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { Component, inject, Input, OnInit, ViewEncapsulation } from '@angular/core';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import { NodeEntry } from '@alfresco/js-api';
|
||||
import { MatIconModule } from '@angular/material/icon';
|
||||
|
||||
@Component({
|
||||
selector: 'aca-lock-icon',
|
||||
template: `<mat-icon class="adf-datatable-cell-badge" [title]="tooltip" aria-hidden="true">lock</mat-icon>`,
|
||||
imports: [MatIconModule],
|
||||
encapsulation: ViewEncapsulation.None
|
||||
})
|
||||
export class LockIconComponent implements OnInit {
|
||||
private readonly translate = inject(TranslateService);
|
||||
|
||||
@Input()
|
||||
data: { node: NodeEntry };
|
||||
|
||||
tooltip: string;
|
||||
|
||||
ngOnInit() {
|
||||
const entry = this.data?.node?.entry;
|
||||
const aspectNames = entry?.aspectNames ?? [];
|
||||
const props = entry?.properties ?? {};
|
||||
|
||||
// cspell:ignore workingcopy
|
||||
const isWorkingCopy = aspectNames.includes('cm:workingcopy');
|
||||
const ownerProp = isWorkingCopy ? props['cm:workingCopyOwner'] : props['cm:lockOwner'];
|
||||
const key = isWorkingCopy ? 'APP.TOOLTIPS.WORKING_COPY_BADGE' : 'APP.TOOLTIPS.LOCK_BADGE';
|
||||
this.tooltip = this.translate.instant(key, { owner: ownerProp?.displayName ?? ownerProp?.id ?? '' });
|
||||
}
|
||||
}
|
||||
+2
-2
@@ -15,9 +15,9 @@
|
||||
{{ displayText$ | async }}
|
||||
</span>
|
||||
|
||||
<ng-container *ngIf="isFile && isFileWriteLocked">
|
||||
@if (isFile && (isFileWriteLocked || isWorkingCopy)) {
|
||||
<aca-locked-by [node]="context.row.node" />
|
||||
</ng-container>
|
||||
}
|
||||
</div>
|
||||
<aca-datatable-cell-badges [node]="node" />
|
||||
</div>
|
||||
|
||||
+31
-6
@@ -23,14 +23,16 @@
|
||||
*/
|
||||
|
||||
import { CustomNameColumnComponent } from './name-column.component';
|
||||
import { DatatableCellBadgesComponent } from '../datatable-cell-badges/datatable-cell-badges.component';
|
||||
import { LockedByComponent } from '@alfresco/aca-shared';
|
||||
import { provideStore } from '@ngrx/store';
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { By } from '@angular/platform-browser';
|
||||
import { NoopTranslateModule, provideCoreAuth } from '@alfresco/adf-core';
|
||||
import { NoopTranslateModule, provideCoreAuth, UnitTestingUtils } from '@alfresco/adf-core';
|
||||
|
||||
describe('CustomNameColumnComponent', () => {
|
||||
let fixture: ComponentFixture<CustomNameColumnComponent>;
|
||||
let component: CustomNameColumnComponent;
|
||||
let testingUtils: UnitTestingUtils;
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
@@ -57,6 +59,7 @@ describe('CustomNameColumnComponent', () => {
|
||||
|
||||
fixture = TestBed.createComponent(CustomNameColumnComponent);
|
||||
component = fixture.componentInstance;
|
||||
testingUtils = new UnitTestingUtils(fixture.debugElement);
|
||||
});
|
||||
|
||||
it('should not render lock element if file is not locked', () => {
|
||||
@@ -76,7 +79,7 @@ describe('CustomNameColumnComponent', () => {
|
||||
component.ngOnInit();
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(fixture.debugElement.nativeElement.querySelector('aca-locked-by')).toBe(null);
|
||||
expect(testingUtils.getByDirective(LockedByComponent)).toBe(null);
|
||||
});
|
||||
|
||||
it('should not render lock element if node is not a file', () => {
|
||||
@@ -96,7 +99,7 @@ describe('CustomNameColumnComponent', () => {
|
||||
component.ngOnInit();
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(fixture.debugElement.nativeElement.querySelector('aca-locked-by')).toBe(null);
|
||||
expect(testingUtils.getByDirective(LockedByComponent)).toBe(null);
|
||||
});
|
||||
|
||||
it('should render lock element if file is locked', () => {
|
||||
@@ -117,7 +120,7 @@ describe('CustomNameColumnComponent', () => {
|
||||
component.ngOnInit();
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(fixture.debugElement.nativeElement.querySelector('aca-locked-by')).not.toBe(null);
|
||||
expect(testingUtils.getByDirective(LockedByComponent)).not.toBe(null);
|
||||
});
|
||||
|
||||
it('should call parent component onClick method', () => {
|
||||
@@ -138,8 +141,30 @@ describe('CustomNameColumnComponent', () => {
|
||||
});
|
||||
|
||||
it('should pass node to badge component', () => {
|
||||
const badgeElement = fixture.debugElement.query(By.css('aca-datatable-cell-badges'));
|
||||
const badgeElement = testingUtils.getByDirective(DatatableCellBadgesComponent);
|
||||
expect(badgeElement).not.toBe(null);
|
||||
expect(badgeElement.componentInstance.node).toBe(component.node);
|
||||
});
|
||||
|
||||
it('should render lock element for a working copy (cm:workingcopy aspect)', () => {
|
||||
component.context = {
|
||||
row: {
|
||||
node: {
|
||||
entry: {
|
||||
isFile: true,
|
||||
id: 'nodeId',
|
||||
name: 'working-copy.txt',
|
||||
aspectNames: ['cm:workingcopy'],
|
||||
properties: {}
|
||||
}
|
||||
},
|
||||
getValue: (key: string) => key
|
||||
}
|
||||
};
|
||||
|
||||
component.ngOnInit();
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(testingUtils.getByDirective(LockedByComponent)).not.toBe(null);
|
||||
});
|
||||
});
|
||||
|
||||
+5
-1
@@ -27,7 +27,7 @@ import { ChangeDetectorRef, Component, DestroyRef, inject, OnInit, ViewEncapsula
|
||||
import { Actions, ofType } from '@ngrx/effects';
|
||||
import { filter } from 'rxjs/operators';
|
||||
import { NodeActionTypes } from '@alfresco/aca-shared/store';
|
||||
import { isLocked, LockedByComponent } from '@alfresco/aca-shared';
|
||||
import { isLocked, isWorkingCopy, LockedByComponent } from '@alfresco/aca-shared';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { TranslatePipe } from '@ngx-translate/core';
|
||||
import { DatatableCellBadgesComponent } from '../datatable-cell-badges/datatable-cell-badges.component';
|
||||
@@ -46,6 +46,7 @@ import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
||||
export class CustomNameColumnComponent extends NameColumnComponent implements OnInit {
|
||||
isFile: boolean;
|
||||
isFileWriteLocked: boolean;
|
||||
isWorkingCopy: boolean;
|
||||
|
||||
private readonly destroy = inject(DestroyRef);
|
||||
private readonly cd = inject(ChangeDetectorRef);
|
||||
@@ -56,6 +57,7 @@ export class CustomNameColumnComponent extends NameColumnComponent implements On
|
||||
this.updateValue();
|
||||
this.isFile = this.node?.entry && !this.node.entry.isFolder;
|
||||
this.isFileWriteLocked = isLocked(this.node);
|
||||
this.isWorkingCopy = isWorkingCopy(this.node);
|
||||
|
||||
this.nodesService.nodeUpdated.pipe(takeUntilDestroyed(this.destroy)).subscribe((node: any) => {
|
||||
const row = this.context.row;
|
||||
@@ -72,6 +74,7 @@ export class CustomNameColumnComponent extends NameColumnComponent implements On
|
||||
|
||||
this.isFile = this.node?.entry && !this.node.entry.isFolder;
|
||||
this.isFileWriteLocked = isLocked(this.node);
|
||||
this.isWorkingCopy = isWorkingCopy(this.node);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -83,6 +86,7 @@ export class CustomNameColumnComponent extends NameColumnComponent implements On
|
||||
)
|
||||
.subscribe(() => {
|
||||
this.isFileWriteLocked = isLocked(this.node);
|
||||
this.isWorkingCopy = isWorkingCopy(this.node);
|
||||
this.cd.detectChanges();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -61,8 +61,8 @@ describe('RecentFilesComponent', () => {
|
||||
isFolder: false,
|
||||
createdAt: null,
|
||||
modifiedAt: null,
|
||||
modifiedByUser: null,
|
||||
createdByUser: null
|
||||
modifiedByUser: { id: '', displayName: '' },
|
||||
createdByUser: { id: '', displayName: '' }
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -74,8 +74,8 @@ describe('RecentFilesComponent', () => {
|
||||
isFolder: false,
|
||||
createdAt: null,
|
||||
modifiedAt: null,
|
||||
modifiedByUser: null,
|
||||
createdByUser: null
|
||||
modifiedByUser: { id: '', displayName: '' },
|
||||
createdByUser: { id: '', displayName: '' }
|
||||
}
|
||||
}
|
||||
],
|
||||
|
||||
+43
-63
@@ -27,11 +27,9 @@ import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { of } from 'rxjs';
|
||||
import { Store } from '@ngrx/store';
|
||||
import { NodeEntry } from '@alfresco/js-api';
|
||||
import { DownloadNodesAction, EditOfflineAction } from '@alfresco/aca-shared/store';
|
||||
import { CancelCheckoutNodeAction, CheckoutNodeAction } from '@alfresco/aca-shared/store';
|
||||
import { AppTestingModule } from '../../../testing/app-testing.module';
|
||||
import { AppExtensionService } from '@alfresco/aca-shared';
|
||||
import { NotificationService } from '@alfresco/adf-core';
|
||||
import { MatSnackBarModule } from '@angular/material/snack-bar';
|
||||
|
||||
describe('ToggleEditOfflineComponent', () => {
|
||||
let fixture: ComponentFixture<ToggleEditOfflineComponent>;
|
||||
@@ -40,7 +38,6 @@ describe('ToggleEditOfflineComponent', () => {
|
||||
let dispatchSpy: jasmine.Spy;
|
||||
let selectSpy: jasmine.Spy;
|
||||
let selection: any;
|
||||
let showErrorSpy: jasmine.Spy;
|
||||
|
||||
const extensionsMock = {
|
||||
updateSidebarActions: jasmine.createSpy('updateSidebarActions')
|
||||
@@ -48,7 +45,7 @@ describe('ToggleEditOfflineComponent', () => {
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [AppTestingModule, ToggleEditOfflineComponent, MatSnackBarModule],
|
||||
imports: [AppTestingModule, ToggleEditOfflineComponent],
|
||||
providers: [
|
||||
{
|
||||
provide: Store,
|
||||
@@ -66,88 +63,71 @@ describe('ToggleEditOfflineComponent', () => {
|
||||
|
||||
fixture = TestBed.createComponent(ToggleEditOfflineComponent);
|
||||
component = fixture.componentInstance;
|
||||
spyOn(component, 'unlockNode').and.returnValue(Promise.resolve(null));
|
||||
spyOn(component, 'lockNode').and.returnValue(Promise.resolve(null));
|
||||
store = TestBed.inject(Store);
|
||||
|
||||
store = TestBed.inject(Store);
|
||||
dispatchSpy = spyOn(store, 'dispatch');
|
||||
selectSpy = spyOn(store, 'select');
|
||||
|
||||
selection = { file: { entry: { name: 'test', properties: {}, isLocked: false } } };
|
||||
|
||||
const notificationService = TestBed.inject(NotificationService);
|
||||
showErrorSpy = spyOn(notificationService, 'showError');
|
||||
});
|
||||
|
||||
it('should initialized with data from store', () => {
|
||||
it('should initialize selection from store', () => {
|
||||
selectSpy.and.returnValue(of(selection));
|
||||
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(component.selection).toEqual(selection.file);
|
||||
});
|
||||
|
||||
it('should download content when node is locked', async () => {
|
||||
it('should dispatch CheckoutNodeAction when node is not locked', () => {
|
||||
selectSpy.and.returnValue(of(selection));
|
||||
fixture.detectChanges();
|
||||
|
||||
selection.file.entry.isLocked = false;
|
||||
await component.onClick();
|
||||
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(dispatchSpy.calls.argsFor(0)).toEqual([new DownloadNodesAction([selection.file as NodeEntry])]);
|
||||
});
|
||||
|
||||
it('should not download content if node is not locked', () => {
|
||||
selectSpy.and.returnValue(of(selection));
|
||||
|
||||
fixture.detectChanges();
|
||||
component.onClick();
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(dispatchSpy.calls.argsFor(0)).not.toEqual([new DownloadNodesAction([selection.file as NodeEntry])]);
|
||||
expect(dispatchSpy).toHaveBeenCalledWith(new CheckoutNodeAction(selection.file as NodeEntry));
|
||||
});
|
||||
|
||||
it('should dispatch EditOfflineAction action', async () => {
|
||||
selectSpy.and.returnValue(of(selection));
|
||||
|
||||
it('should dispatch CancelCheckoutNodeAction when node is locked', () => {
|
||||
selection.file.entry.isLocked = true;
|
||||
|
||||
fixture.detectChanges();
|
||||
await component.onClick();
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(dispatchSpy.calls.argsFor(0)).toEqual([new EditOfflineAction(selection.file as NodeEntry)]);
|
||||
});
|
||||
|
||||
it('should raise notification on lock error', () => {
|
||||
selectSpy.and.returnValue(of(selection));
|
||||
|
||||
fixture.detectChanges();
|
||||
component.onLockError();
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(showErrorSpy).toHaveBeenCalledWith('APP.MESSAGES.ERRORS.LOCK_NODE', null, { fileName: 'test' });
|
||||
});
|
||||
|
||||
it('should raise notification on unlock error', () => {
|
||||
selectSpy.and.returnValue(of(selection));
|
||||
|
||||
fixture.detectChanges();
|
||||
component.onUnlockError();
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(showErrorSpy).toHaveBeenCalledWith('APP.MESSAGES.ERRORS.UNLOCK_NODE', null, { fileName: 'test' });
|
||||
});
|
||||
|
||||
it('should call updateSidebarActions on click', async () => {
|
||||
selectSpy.and.returnValue(of(selection));
|
||||
fixture.detectChanges();
|
||||
|
||||
await component.onClick();
|
||||
component.onClick();
|
||||
|
||||
expect(dispatchSpy).toHaveBeenCalledWith(new CancelCheckoutNodeAction(selection.file as NodeEntry));
|
||||
});
|
||||
|
||||
it('should dispatch CancelCheckoutNodeAction when node is a working copy', () => {
|
||||
selection.file.entry.aspectNames = ['cm:workingcopy'];
|
||||
selectSpy.and.returnValue(of(selection));
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(extensionsMock.updateSidebarActions).toHaveBeenCalled();
|
||||
component.onClick();
|
||||
|
||||
expect(dispatchSpy).toHaveBeenCalledWith(new CancelCheckoutNodeAction(selection.file as NodeEntry));
|
||||
});
|
||||
|
||||
it('should set isNodeLocked to false and nodeTitle to EDIT_OFFLINE for a plain unlocked file', () => {
|
||||
selectSpy.and.returnValue(of(selection));
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(component.isNodeLocked).toBeFalse();
|
||||
expect(component.nodeTitle).toBe('APP.ACTIONS.EDIT_OFFLINE');
|
||||
});
|
||||
|
||||
it('should set isNodeLocked to true and nodeTitle to EDIT_OFFLINE_CANCEL for a locked file', () => {
|
||||
selection.file.entry.isLocked = true;
|
||||
selectSpy.and.returnValue(of(selection));
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(component.isNodeLocked).toBeTrue();
|
||||
expect(component.nodeTitle).toBe('APP.ACTIONS.EDIT_OFFLINE_CANCEL');
|
||||
});
|
||||
|
||||
it('should set isNodeLocked to true for a working copy', () => {
|
||||
selection.file.entry.aspectNames = ['cm:workingcopy'];
|
||||
selectSpy.and.returnValue(of(selection));
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(component.isNodeLocked).toBeTrue();
|
||||
});
|
||||
});
|
||||
|
||||
+11
-69
@@ -22,13 +22,11 @@
|
||||
* from Hyland Software. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { AppStore, DownloadNodesAction, EditOfflineAction, SetSelectedNodesAction, getAppSelection } from '@alfresco/aca-shared/store';
|
||||
import { NodeEntry, SharedLinkEntry, Node, NodesApi, LazyApi } from '@alfresco/js-api';
|
||||
import { AppStore, CancelCheckoutNodeAction, CheckoutNodeAction, getAppSelection } from '@alfresco/aca-shared/store';
|
||||
import { NodeEntry } from '@alfresco/js-api';
|
||||
import { Component, inject, OnInit, ViewChild, ViewEncapsulation } from '@angular/core';
|
||||
import { Store } from '@ngrx/store';
|
||||
import { AppExtensionService, isLocked } from '@alfresco/aca-shared';
|
||||
import { NotificationService } from '@alfresco/adf-core';
|
||||
import { AlfrescoApiService } from '@alfresco/adf-content-services';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { TranslatePipe } from '@ngx-translate/core';
|
||||
import { MatMenuItem, MatMenuModule } from '@angular/material/menu';
|
||||
@@ -48,17 +46,11 @@ import { MatIconModule } from '@angular/material/icon';
|
||||
})
|
||||
export class ToggleEditOfflineComponent implements OnInit {
|
||||
private readonly store = inject<Store<AppStore>>(Store);
|
||||
private readonly alfrescoApiService = inject(AlfrescoApiService);
|
||||
private readonly extensions = inject(AppExtensionService);
|
||||
|
||||
@ViewChild(MatMenuItem)
|
||||
menuItem: MatMenuItem;
|
||||
|
||||
private readonly notificationService = inject(NotificationService);
|
||||
|
||||
@LazyApi((self: ToggleEditOfflineComponent) => new NodesApi(self.alfrescoApiService.getInstance()))
|
||||
declare private readonly nodesApi: NodesApi;
|
||||
|
||||
selection: NodeEntry;
|
||||
nodeTitle = '';
|
||||
isNodeLocked = false;
|
||||
@@ -66,71 +58,21 @@ export class ToggleEditOfflineComponent implements OnInit {
|
||||
ngOnInit() {
|
||||
this.store.select(getAppSelection).subscribe(({ file }) => {
|
||||
this.selection = file;
|
||||
this.isNodeLocked = this.selection && isLocked(this.selection);
|
||||
this.isNodeLocked = this.selection && this.isCancelable(this.selection);
|
||||
this.nodeTitle = this.isNodeLocked ? 'APP.ACTIONS.EDIT_OFFLINE_CANCEL' : 'APP.ACTIONS.EDIT_OFFLINE';
|
||||
});
|
||||
}
|
||||
|
||||
async onClick() {
|
||||
await this.toggleLock(this.selection);
|
||||
onClick() {
|
||||
if (this.isCancelable(this.selection)) {
|
||||
this.store.dispatch(new CancelCheckoutNodeAction(this.selection));
|
||||
} else {
|
||||
this.store.dispatch(new CheckoutNodeAction(this.selection));
|
||||
}
|
||||
this.extensions.updateSidebarActions();
|
||||
}
|
||||
|
||||
private async toggleLock(node: NodeEntry | SharedLinkEntry) {
|
||||
const id = (node as SharedLinkEntry).entry.nodeId || node.entry.id;
|
||||
|
||||
if (isLocked(this.selection)) {
|
||||
try {
|
||||
const response = await this.unlockNode(id);
|
||||
|
||||
this.update(response?.entry);
|
||||
this.store.dispatch(new EditOfflineAction(this.selection));
|
||||
this.store.dispatch(new SetSelectedNodesAction([this.selection]));
|
||||
} catch {
|
||||
this.onUnlockError();
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
const response = await this.lockNode(id);
|
||||
|
||||
this.update(response?.entry);
|
||||
this.store.dispatch(new DownloadNodesAction([this.selection]));
|
||||
this.store.dispatch(new EditOfflineAction(this.selection));
|
||||
this.store.dispatch(new SetSelectedNodesAction([this.selection]));
|
||||
} catch {
|
||||
this.onLockError();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onLockError() {
|
||||
this.notificationService.showError('APP.MESSAGES.ERRORS.LOCK_NODE', null, { fileName: this.selection.entry.name });
|
||||
}
|
||||
|
||||
onUnlockError() {
|
||||
this.notificationService.showError('APP.MESSAGES.ERRORS.UNLOCK_NODE', null, { fileName: this.selection.entry.name });
|
||||
}
|
||||
|
||||
lockNode(nodeId: string) {
|
||||
return this.nodesApi.lockNode(nodeId, {
|
||||
type: 'ALLOW_OWNER_CHANGES',
|
||||
lifetime: 'PERSISTENT'
|
||||
});
|
||||
}
|
||||
|
||||
unlockNode(nodeId: string) {
|
||||
return this.nodesApi.unlockNode(nodeId);
|
||||
}
|
||||
|
||||
private update(data: Node) {
|
||||
if (data?.properties) {
|
||||
const properties = this.selection.entry.properties || {};
|
||||
|
||||
properties['cm:lockLifetime'] = data.properties['cm:lockLifetime'];
|
||||
properties['cm:lockOwner'] = data.properties['cm:lockOwner'];
|
||||
properties['cm:lockType'] = data.properties['cm:lockType'];
|
||||
|
||||
this.selection.entry.properties = properties;
|
||||
}
|
||||
private isCancelable(node: NodeEntry): boolean {
|
||||
return isLocked(node) || (node?.entry?.aspectNames ?? []).includes('cm:workingcopy');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -368,4 +368,49 @@ describe('DocumentListDirective', () => {
|
||||
expect(elementRefMock.nativeElement.querySelector).not.toHaveBeenCalled();
|
||||
expect(documentListMock.preselectNodes).toEqual([]);
|
||||
}));
|
||||
|
||||
describe('rowFilter for cm:checkedOut nodes', () => {
|
||||
beforeEach(() => {
|
||||
documentListMock.rowFilter = undefined;
|
||||
});
|
||||
|
||||
it('should set rowFilter on folder views (personal-files)', () => {
|
||||
mockRouter.url = '/personal-files';
|
||||
documentListDirective.ngOnInit();
|
||||
expect(documentListMock.rowFilter).toBeDefined();
|
||||
});
|
||||
|
||||
it('should not set rowFilter on /favorites', () => {
|
||||
mockRouter.url = '/favorites';
|
||||
documentListDirective.ngOnInit();
|
||||
expect(documentListMock.rowFilter).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should not set rowFilter on /shared', () => {
|
||||
mockRouter.url = '/shared';
|
||||
documentListDirective.ngOnInit();
|
||||
expect(documentListMock.rowFilter).toBeUndefined();
|
||||
});
|
||||
|
||||
it('rowFilter should return false for cm:checkedOut nodes', () => {
|
||||
mockRouter.url = '/personal-files';
|
||||
documentListDirective.ngOnInit();
|
||||
const result = documentListMock.rowFilter({ node: { entry: { aspectNames: ['cm:checkedOut'] } } });
|
||||
expect(result).toBeFalse();
|
||||
});
|
||||
|
||||
it('rowFilter should return true for nodes without cm:checkedOut', () => {
|
||||
mockRouter.url = '/personal-files';
|
||||
documentListDirective.ngOnInit();
|
||||
const result = documentListMock.rowFilter({ node: { entry: { aspectNames: ['cm:titled'] } } });
|
||||
expect(result).toBeTrue();
|
||||
});
|
||||
|
||||
it('rowFilter should return true when aspectNames is undefined', () => {
|
||||
mockRouter.url = '/personal-files';
|
||||
documentListDirective.ngOnInit();
|
||||
const result = documentListMock.rowFilter({ node: { entry: {} } });
|
||||
expect(result).toBeTrue();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -64,6 +64,13 @@ export class DocumentListDirective implements OnInit {
|
||||
ngOnInit() {
|
||||
this.documentList.stickyHeader = true;
|
||||
this.documentList.includeFields = this.documentList.currentFolderId === '-recent-' ? SEARCH_INCLUDE_FIELDS : INCLUDE_FIELDS;
|
||||
|
||||
const url = this.router.url;
|
||||
const isFolderView = !url.startsWith('/favorites') && !url.startsWith('/shared');
|
||||
if (isFolderView) {
|
||||
this.documentList.rowFilter = ({ node }) => !(node?.entry?.aspectNames ?? []).includes('cm:checkedOut');
|
||||
}
|
||||
|
||||
this.isLibrary =
|
||||
this.documentList.currentFolderId === '-mysites-' ||
|
||||
// workaround for custom node list
|
||||
|
||||
@@ -39,6 +39,7 @@ import {
|
||||
ShareNodeAction,
|
||||
ShowLoaderAction,
|
||||
UnlockWriteAction,
|
||||
ViewNodeAction,
|
||||
ViewNodeVersionAction
|
||||
} from '@alfresco/aca-shared/store';
|
||||
import { NodeEffects } from '../store/effects/node.effects';
|
||||
@@ -1625,8 +1626,9 @@ describe('ContentManagementService', () => {
|
||||
spyOnOpenUploadNewVersionDialog.and.returnValue(
|
||||
of({ action: NewVersionUploaderDataAction.upload, newVersion: mockNewVersion, currentVersion: fakeNode })
|
||||
);
|
||||
spyOn(documentListService, 'reload');
|
||||
contentManagementService.versionUpdateDialog(fakeNode, fakeFile);
|
||||
expect(spyOnDispatch).toHaveBeenCalledOnceWith(new UnlockWriteAction(mockNewVersion.value));
|
||||
expect(spyOnDispatch).toHaveBeenCalledWith(new UnlockWriteAction(mockNewVersion.value));
|
||||
});
|
||||
|
||||
it('should unlock node if is locked when uploading a file', () => {
|
||||
@@ -1636,6 +1638,53 @@ describe('ContentManagementService', () => {
|
||||
|
||||
expect(showErrorSpy).toHaveBeenCalledOnceWith(fakeError);
|
||||
});
|
||||
|
||||
it('should reload document list after upload', () => {
|
||||
const uploadData = {
|
||||
action: NewVersionUploaderDataAction.upload,
|
||||
newVersion: { value: { entry: { id: 'uploaded-id', properties: {} } } },
|
||||
currentVersion: fakeNode
|
||||
};
|
||||
spyOnOpenUploadNewVersionDialog.and.returnValue(of(uploadData));
|
||||
spyOn(documentListService, 'reload');
|
||||
|
||||
contentManagementService.versionUpdateDialog(fakeNode, fakeFile);
|
||||
|
||||
expect(documentListService.reload).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should dispatch RefreshPreviewAction for a regular (non-working-copy) upload', () => {
|
||||
const uploadedEntry = { id: 'uploaded-id', properties: {} };
|
||||
const uploadData = {
|
||||
action: NewVersionUploaderDataAction.upload,
|
||||
newVersion: { value: { entry: uploadedEntry } },
|
||||
currentVersion: fakeNode
|
||||
};
|
||||
spyOnOpenUploadNewVersionDialog.and.returnValue(of(uploadData));
|
||||
spyOn(documentListService, 'reload');
|
||||
|
||||
contentManagementService.versionUpdateDialog(fakeNode, fakeFile);
|
||||
|
||||
expect(spyOnDispatch).toHaveBeenCalledWith(new RefreshPreviewAction(uploadedEntry as Node));
|
||||
});
|
||||
|
||||
it('should dispatch ViewNodeAction when checking in a working copy from the viewer', () => {
|
||||
fakeNode.aspectNames = ['cm:workingcopy'];
|
||||
fakeNode.id = 'wc-id';
|
||||
const uploadedEntry = { id: 'original-id', properties: {} };
|
||||
const uploadData = {
|
||||
action: NewVersionUploaderDataAction.upload,
|
||||
newVersion: { value: { entry: uploadedEntry } },
|
||||
currentVersion: fakeNode
|
||||
};
|
||||
spyOnOpenUploadNewVersionDialog.and.returnValue(of(uploadData));
|
||||
spyOn(documentListService, 'reload');
|
||||
spyOnProperty(router, 'url', 'get').and.returnValue('/personal-files/preview/wc-id');
|
||||
|
||||
contentManagementService.versionUpdateDialog(fakeNode, fakeFile);
|
||||
|
||||
expect(spyOnDispatch).toHaveBeenCalledWith(new ViewNodeAction('original-id', { location: '/personal-files/preview/wc-id' }));
|
||||
});
|
||||
});
|
||||
|
||||
describe('manageVersions', () => {
|
||||
|
||||
@@ -35,6 +35,7 @@ import {
|
||||
SetSelectedNodesAction,
|
||||
ShowLoaderAction,
|
||||
UnlockWriteAction,
|
||||
ViewNodeAction,
|
||||
ViewNodeVersionAction
|
||||
} from '@alfresco/aca-shared/store';
|
||||
import {
|
||||
@@ -187,9 +188,21 @@ export class ContentManagementService {
|
||||
this.newVersionUploaderService.openUploadNewVersionDialog(newVersionUploaderDialogData, dialogConfig).subscribe(
|
||||
(data) => {
|
||||
if (data.action === NewVersionUploaderDataAction.upload) {
|
||||
if (data.newVersion.value.entry.properties['cm:lockType'] === 'WRITE_LOCK') {
|
||||
const uploadedEntry = data.newVersion.value.entry;
|
||||
|
||||
if (uploadedEntry.properties?.['cm:lockType'] === 'WRITE_LOCK') {
|
||||
this.store.dispatch(new UnlockWriteAction(data.newVersion.value));
|
||||
}
|
||||
|
||||
this.documentListService.reload();
|
||||
|
||||
const isWorkingCopy = (node.aspectNames ?? []).includes('cm:workingcopy');
|
||||
if (isWorkingCopy && uploadedEntry.id !== node.id && this.isInViewer()) {
|
||||
const location = this.activatedRoute.snapshot.queryParams['location'] || this.router.url;
|
||||
this.store.dispatch(new ViewNodeAction(uploadedEntry.id, { location }));
|
||||
} else {
|
||||
this.store.dispatch(new RefreshPreviewAction(uploadedEntry));
|
||||
}
|
||||
}
|
||||
},
|
||||
(error) => this.notificationService.showError(error)
|
||||
@@ -1285,4 +1298,9 @@ export class ContentManagementService {
|
||||
width: '700px'
|
||||
});
|
||||
}
|
||||
|
||||
private isInViewer(): boolean {
|
||||
const url = this.router.url;
|
||||
return url.includes('/preview/') || url.includes('viewer:view') || url.includes('/view/');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,11 +29,14 @@ import { provideEffects } from '@ngrx/effects';
|
||||
import { Store } from '@ngrx/store';
|
||||
import { ContentManagementService } from '../../services/content-management.service';
|
||||
import {
|
||||
CancelCheckoutNodeAction,
|
||||
CheckoutNodeAction,
|
||||
CopyNodesAction,
|
||||
CreateFolderAction,
|
||||
DeletedNodeInfo,
|
||||
DeleteNodesAction,
|
||||
EditFolderAction,
|
||||
EditOfflineAction,
|
||||
ExpandInfoDrawerAction,
|
||||
NodeInformationAction,
|
||||
FullscreenViewerAction,
|
||||
@@ -52,16 +55,18 @@ import {
|
||||
UndoDeleteNodesAction,
|
||||
UnlockWriteAction,
|
||||
UnshareNodesAction,
|
||||
ViewNodeAction,
|
||||
LinkNodesAction,
|
||||
LocateLinkedItemAction
|
||||
} from '@alfresco/aca-shared/store';
|
||||
import { RenditionService } from '@alfresco/adf-content-services';
|
||||
import { DocumentListService, RenditionService } from '@alfresco/adf-content-services';
|
||||
import { AppHookService } from '@alfresco/aca-shared';
|
||||
import { ViewerEffects } from './viewer.effects';
|
||||
import { ActivatedRoute, NavigationEnd, Router } from '@angular/router';
|
||||
import { of } from 'rxjs';
|
||||
import { MatDialogModule } from '@angular/material/dialog';
|
||||
import { MatSnackBarModule } from '@angular/material/snack-bar';
|
||||
import { NodeEntry, UserInfo } from '@alfresco/js-api';
|
||||
import { NodeEntry, SharedLinkEntry, UserInfo } from '@alfresco/js-api';
|
||||
import { Node } from '@alfresco/js-api/typings/src/api/content-rest-api/model/node';
|
||||
|
||||
describe('NodeEffects', () => {
|
||||
@@ -745,4 +750,103 @@ describe('NodeEffects', () => {
|
||||
expect(contentService.showNodeInformation).toHaveBeenCalledWith(node);
|
||||
}));
|
||||
});
|
||||
|
||||
describe('checkout$', () => {
|
||||
let documentListService: DocumentListService;
|
||||
let appHookService: AppHookService;
|
||||
|
||||
beforeEach(() => {
|
||||
documentListService = TestBed.inject(DocumentListService);
|
||||
appHookService = TestBed.inject(AppHookService);
|
||||
spyOn(documentListService, 'reload').and.stub();
|
||||
spyOn(appHookService.nodeToSelect$, 'next');
|
||||
});
|
||||
|
||||
it('should call checkout with the node id from payload', () => {
|
||||
const node = { entry: { id: 'node-id' } } as NodeEntry;
|
||||
spyOn(contentService, 'checkout').and.returnValue(of({ entry: { id: 'wc-id' } } as NodeEntry));
|
||||
|
||||
store.dispatch(new CheckoutNodeAction(node));
|
||||
|
||||
expect(contentService.checkout).toHaveBeenCalledWith('node-id');
|
||||
});
|
||||
|
||||
it('should prefer nodeId over id for shared link nodes', () => {
|
||||
const node = { entry: { nodeId: 'shared-node-id', id: 'link-id' } } as SharedLinkEntry;
|
||||
spyOn(contentService, 'checkout').and.returnValue(of({ entry: { id: 'wc-id' } } as NodeEntry));
|
||||
|
||||
store.dispatch(new CheckoutNodeAction(node as NodeEntry));
|
||||
|
||||
expect(contentService.checkout).toHaveBeenCalledWith('shared-node-id');
|
||||
});
|
||||
|
||||
it('should reload document list and dispatch EditOfflineAction on success', () => {
|
||||
const node = { entry: { id: 'node-id' } } as NodeEntry;
|
||||
const workingCopy = { entry: { id: 'wc-id' } } as NodeEntry;
|
||||
spyOn(contentService, 'checkout').and.returnValue(of(workingCopy));
|
||||
spyOn(store, 'dispatch').and.callThrough();
|
||||
|
||||
store.dispatch(new CheckoutNodeAction(node));
|
||||
|
||||
expect(documentListService.reload).toHaveBeenCalled();
|
||||
expect(appHookService.nodeToSelect$.next).toHaveBeenCalledWith(workingCopy);
|
||||
expect(store.dispatch).toHaveBeenCalledWith(jasmine.objectContaining({ ...new EditOfflineAction(node) }));
|
||||
});
|
||||
|
||||
it('should preselect original node and not open viewer on favorites/shared where working copy is not shown', () => {
|
||||
spyOnProperty(router, 'url', 'get').and.returnValue('/favorites');
|
||||
const node = { entry: { id: 'node-id' } } as NodeEntry;
|
||||
const workingCopy = { entry: { id: 'wc-id' } } as NodeEntry;
|
||||
spyOn(contentService, 'checkout').and.returnValue(of(workingCopy));
|
||||
spyOn(store, 'dispatch').and.callThrough();
|
||||
|
||||
store.dispatch(new CheckoutNodeAction(node));
|
||||
|
||||
expect(appHookService.nodeToSelect$.next).toHaveBeenCalledWith(node);
|
||||
expect(store.dispatch).not.toHaveBeenCalledWith(jasmine.objectContaining({ ...new ViewNodeAction(workingCopy.entry.id) }));
|
||||
});
|
||||
});
|
||||
|
||||
describe('cancelCheckout$', () => {
|
||||
let documentListService: DocumentListService;
|
||||
let appHookService: AppHookService;
|
||||
|
||||
beforeEach(() => {
|
||||
documentListService = TestBed.inject(DocumentListService);
|
||||
appHookService = TestBed.inject(AppHookService);
|
||||
spyOn(documentListService, 'reload').and.stub();
|
||||
spyOn(appHookService.nodeToSelect$, 'next');
|
||||
});
|
||||
|
||||
it('should call cancelCheckout with the node id from payload', () => {
|
||||
const node = { entry: { id: 'wc-id' } } as NodeEntry;
|
||||
spyOn(contentService, 'cancelCheckout').and.returnValue(of({ entry: { id: 'original-id' } } as NodeEntry));
|
||||
|
||||
store.dispatch(new CancelCheckoutNodeAction(node));
|
||||
|
||||
expect(contentService.cancelCheckout).toHaveBeenCalledWith('wc-id');
|
||||
});
|
||||
|
||||
it('should prefer nodeId over id for shared link nodes', () => {
|
||||
const node = { entry: { nodeId: 'shared-node-id', id: 'link-id' } } as SharedLinkEntry;
|
||||
spyOn(contentService, 'cancelCheckout').and.returnValue(of({ entry: { id: 'original-id' } } as NodeEntry));
|
||||
|
||||
store.dispatch(new CancelCheckoutNodeAction(node as NodeEntry));
|
||||
|
||||
expect(contentService.cancelCheckout).toHaveBeenCalledWith('shared-node-id');
|
||||
});
|
||||
|
||||
it('should reload document list and dispatch EditOfflineAction on success', () => {
|
||||
const node = { entry: { id: 'wc-id' } } as NodeEntry;
|
||||
const originalNode = { entry: { id: 'original-id' } } as NodeEntry;
|
||||
spyOn(contentService, 'cancelCheckout').and.returnValue(of(originalNode));
|
||||
spyOn(store, 'dispatch').and.callThrough();
|
||||
|
||||
store.dispatch(new CancelCheckoutNodeAction(node));
|
||||
|
||||
expect(documentListService.reload).toHaveBeenCalled();
|
||||
expect(appHookService.nodeToSelect$.next).toHaveBeenCalledWith(originalNode);
|
||||
expect(store.dispatch).toHaveBeenCalledWith(jasmine.objectContaining({ ...new EditOfflineAction(node) }));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -28,10 +28,14 @@ import { first, map, take } from 'rxjs/operators';
|
||||
import { Store } from '@ngrx/store';
|
||||
import {
|
||||
AppStore,
|
||||
CancelCheckoutNodeAction,
|
||||
CheckoutNodeAction,
|
||||
CopyNodesAction,
|
||||
CreateFolderAction,
|
||||
DeleteNodesAction,
|
||||
DownloadNodesAction,
|
||||
EditFolderAction,
|
||||
EditOfflineAction,
|
||||
ExpandInfoDrawerAction,
|
||||
getAppSelection,
|
||||
getCurrentFolder,
|
||||
@@ -54,13 +58,15 @@ import {
|
||||
ShareNodeAction,
|
||||
UndoDeleteNodesAction,
|
||||
UnlockWriteAction,
|
||||
UnshareNodesAction
|
||||
UnshareNodesAction,
|
||||
ViewNodeAction
|
||||
} from '@alfresco/aca-shared/store';
|
||||
import { ContentManagementService } from '../../services/content-management.service';
|
||||
import { RenditionService } from '@alfresco/adf-content-services';
|
||||
import { AppHookService } from '@alfresco/aca-shared';
|
||||
import { DocumentListService, RenditionService } from '@alfresco/adf-content-services';
|
||||
import { ActivatedRoute, NavigationEnd, Router } from '@angular/router';
|
||||
import { DomSanitizer } from '@angular/platform-browser';
|
||||
import { Node } from '@alfresco/js-api';
|
||||
import { Node, SharedLink } from '@alfresco/js-api';
|
||||
|
||||
@Injectable()
|
||||
export class NodeEffects {
|
||||
@@ -68,6 +74,8 @@ export class NodeEffects {
|
||||
private readonly actions$ = inject(Actions);
|
||||
private readonly router = inject(Router);
|
||||
private readonly contentService = inject(ContentManagementService);
|
||||
private readonly documentListService = inject(DocumentListService);
|
||||
private readonly appHookService = inject(AppHookService);
|
||||
private readonly renditionViewer = inject(RenditionService);
|
||||
private readonly activatedRoute = inject(ActivatedRoute);
|
||||
private readonly sanitizer = inject(DomSanitizer);
|
||||
@@ -454,6 +462,58 @@ export class NodeEffects {
|
||||
{ dispatch: false }
|
||||
);
|
||||
|
||||
checkout$ = createEffect(
|
||||
() =>
|
||||
this.actions$.pipe(
|
||||
ofType<CheckoutNodeAction>(NodeActionTypes.CheckoutNode),
|
||||
map((action) => {
|
||||
const nodeEntry = action.payload;
|
||||
const id = (nodeEntry.entry as SharedLink).nodeId ?? nodeEntry.entry.id;
|
||||
const inViewer = this.isInViewer();
|
||||
const viewerExtras = inViewer ? this.currentViewerExtras() : undefined;
|
||||
|
||||
this.contentService.checkout(id).subscribe({
|
||||
next: (workingCopy) => {
|
||||
const nodeToSelect = this.isWorkingCopyVisible() ? workingCopy : nodeEntry;
|
||||
this.appHookService.nodeToSelect$.next(nodeToSelect);
|
||||
this.documentListService.reload();
|
||||
this.store.dispatch(new DownloadNodesAction([nodeEntry]));
|
||||
this.store.dispatch(new EditOfflineAction(nodeEntry));
|
||||
if (inViewer && this.isWorkingCopyVisible()) {
|
||||
this.store.dispatch(new ViewNodeAction(workingCopy.entry.id, viewerExtras));
|
||||
}
|
||||
}
|
||||
});
|
||||
})
|
||||
),
|
||||
{ dispatch: false }
|
||||
);
|
||||
|
||||
cancelCheckout$ = createEffect(
|
||||
() =>
|
||||
this.actions$.pipe(
|
||||
ofType<CancelCheckoutNodeAction>(NodeActionTypes.CancelCheckoutNode),
|
||||
map((action) => {
|
||||
const nodeEntry = action.payload;
|
||||
const id = (nodeEntry.entry as SharedLink).nodeId ?? nodeEntry.entry.id;
|
||||
const inViewer = this.isInViewer();
|
||||
const viewerExtras = inViewer ? this.currentViewerExtras() : undefined;
|
||||
|
||||
this.contentService.cancelCheckout(id).subscribe({
|
||||
next: (originalNode) => {
|
||||
this.appHookService.nodeToSelect$.next(originalNode);
|
||||
this.documentListService.reload();
|
||||
this.store.dispatch(new EditOfflineAction(nodeEntry));
|
||||
if (inViewer) {
|
||||
this.store.dispatch(new ViewNodeAction(originalNode.entry.id, viewerExtras));
|
||||
}
|
||||
}
|
||||
});
|
||||
})
|
||||
),
|
||||
{ dispatch: false }
|
||||
);
|
||||
|
||||
aspectList$ = createEffect(
|
||||
() =>
|
||||
this.actions$.pipe(
|
||||
@@ -536,4 +596,26 @@ export class NodeEffects {
|
||||
const isRepository = location?.includes('/repository') || getNodeContentSource(entry?.path) === 'repository';
|
||||
return `${isRepository ? 'repository' : 'personal-files'}/details`;
|
||||
}
|
||||
|
||||
private isInViewer(): boolean {
|
||||
const url = this.router.url;
|
||||
return url.includes('/preview/') || url.includes('viewer:view') || url.includes('/view/');
|
||||
}
|
||||
|
||||
private isWorkingCopyVisible(): boolean {
|
||||
const url = this.router.url;
|
||||
return !url.startsWith('/favorites') && !url.startsWith('/shared');
|
||||
}
|
||||
|
||||
private currentViewerExtras() {
|
||||
const tree = this.router.parseUrl(this.router.url);
|
||||
const { location, path } = tree.queryParams;
|
||||
if (location) {
|
||||
return { location };
|
||||
}
|
||||
if (path) {
|
||||
return { path };
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user