[ACS-5281] Changed editable state of metadata content based on change o… (#3400)

* ACS-5281 Changed editable state of metadata content based on change of file lock state

* ACS-5281 Updated versions

* ACS-5281 Reverted change

* ACS-5281 Upgrade version

* ACS-5281 Small correction

* ACS-5281 Fixed e2e
This commit is contained in:
AleksanderSklorz
2023-08-27 10:00:35 +02:00
committed by GitHub
parent bc9c58176f
commit aec6852672
45 changed files with 455 additions and 364 deletions

View File

@@ -23,7 +23,7 @@
*/
import { Component, Input, ChangeDetectionStrategy, OnInit, ViewEncapsulation, HostListener, inject } from '@angular/core';
import { PathInfo, MinimalNodeEntity } from '@alfresco/js-api';
import { PathInfo, NodeEntry } from '@alfresco/js-api';
import { Observable, BehaviorSubject, of } from 'rxjs';
import { Store } from '@ngrx/store';
import { NavigateToParentFolder } from '@alfresco/aca-shared/store';
@@ -75,14 +75,14 @@ export class LocationLinkComponent implements OnInit {
goToLocation() {
if (this.context) {
const node: MinimalNodeEntity = this.context.row.node;
const node: NodeEntry = this.context.row.node;
this.store.dispatch(new NavigateToParentFolder(node));
}
}
ngOnInit() {
if (this.context) {
const node: MinimalNodeEntity = this.context.row.node;
const node: NodeEntry = this.context.row.node;
if (node && node.entry && node.entry.path) {
const path = node.entry.path;

View File

@@ -31,7 +31,7 @@ import {
PaginationDirective,
ToolbarComponent
} from '@alfresco/aca-shared';
import { MinimalNodeEntity, MinimalNodeEntryEntity, PathElementEntity, PathInfo } from '@alfresco/js-api';
import { NodeEntry, Node, PathElement, PathInfo } from '@alfresco/js-api';
import { Component, OnInit, ViewEncapsulation } from '@angular/core';
import { debounceTime, map } from 'rxjs/operators';
import { DocumentListPresetRef, ExtensionsModule } from '@alfresco/adf-extensions';
@@ -79,24 +79,24 @@ export class FavoritesComponent extends PageComponent implements OnInit {
this.columns = this.extensions.documentListPresets.favorites;
}
navigate(favorite: MinimalNodeEntryEntity) {
navigate(favorite: Node) {
const { isFolder, id } = favorite;
// TODO: rework as it will fail on non-English setups
const isSitePath = (path: PathInfo): boolean => path && path.elements && path.elements.some(({ name }: PathElementEntity) => name === 'Sites');
const isSitePath = (path: PathInfo): boolean => path && path.elements && path.elements.some(({ name }: PathElement) => name === 'Sites');
if (isFolder) {
this.contentApi
.getNode(id)
.pipe(map((node) => node.entry))
.subscribe(({ path }: MinimalNodeEntryEntity) => {
.subscribe(({ path }: Node) => {
const routeUrl = isSitePath(path) ? '/libraries' : '/personal-files';
this.router.navigate([routeUrl, id]);
});
}
}
onNodeDoubleClick(node: MinimalNodeEntity) {
onNodeDoubleClick(node: NodeEntry) {
if (node && node.entry) {
if (node.entry.isFolder) {
this.navigate(node.entry);

View File

@@ -32,7 +32,7 @@ import { AppTestingModule } from '../../testing/app-testing.module';
import { ContentApiService } from '@alfresco/aca-shared';
import { of, Subject, throwError } from 'rxjs';
import { By } from '@angular/platform-browser';
import { NodeEntry, NodePaging } from '@alfresco/js-api';
import { NodeEntry, NodePaging, Node } from '@alfresco/js-api';
describe('FilesComponent', () => {
let node;
@@ -424,7 +424,7 @@ describe('FilesComponent', () => {
it('should reset the pagination when navigating to a folder', () => {
const resetNewFolderPaginationSpy = spyOn(component.documentList, 'resetNewFolderPagination');
const fakeFolderNode = new NodeEntry({ entry: { id: 'fakeFolderNode', isFolder: true, isFile: false } });
const fakeFolderNode = new NodeEntry({ entry: { id: 'fakeFolderNode', isFolder: true, isFile: false } as Node });
component.navigateTo(fakeFolderNode);
expect(resetNewFolderPaginationSpy).toHaveBeenCalled();
@@ -432,7 +432,7 @@ describe('FilesComponent', () => {
it('should not reset the pagination when the node to navigate is not a folder', () => {
const resetNewFolderPaginationSpy = spyOn(component.documentList, 'resetNewFolderPagination');
const fakeFileNode = new NodeEntry({ entry: { id: 'fakeFileNode', isFolder: false, isFile: true } });
const fakeFileNode = new NodeEntry({ entry: { id: 'fakeFileNode', isFolder: false, isFile: true } as Node });
component.navigateTo(fakeFileNode);
expect(resetNewFolderPaginationSpy).not.toHaveBeenCalled();

View File

@@ -25,7 +25,7 @@
import { DataTableModule, PaginationModule, ShowHeaderMode } from '@alfresco/adf-core';
import { Component, OnDestroy, OnInit, ViewEncapsulation } from '@angular/core';
import { ActivatedRoute, Params } from '@angular/router';
import { MinimalNodeEntity, MinimalNodeEntryEntity, PathElement, PathElementEntity } from '@alfresco/js-api';
import { NodeEntry, Node, PathElement } from '@alfresco/js-api';
import { NodeActionsService } from '../../services/node-actions.service';
import {
ContentApiService,
@@ -72,7 +72,7 @@ import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
export class FilesComponent extends PageComponent implements OnInit, OnDestroy {
isValidPath = true;
isAdmin = false;
selectedNode: MinimalNodeEntity;
selectedNode: NodeEntry;
queryParams = null;
showLoader$ = this.store.select(showLoaderSelector);
@@ -186,7 +186,7 @@ export class FilesComponent extends PageComponent implements OnInit, OnDestroy {
this.store.dispatch(new UploadFileVersionAction(ev));
}
navigateTo(node: MinimalNodeEntity) {
navigateTo(node: NodeEntry) {
if (node && node.entry) {
this.selectedNode = node;
const { isFolder } = node.entry;
@@ -213,7 +213,7 @@ export class FilesComponent extends PageComponent implements OnInit, OnDestroy {
this.navigateTo((event as CustomEvent).detail?.node);
}
onBreadcrumbNavigate(route: PathElementEntity) {
onBreadcrumbNavigate(route: PathElement) {
this.documentList.resetNewFolderPagination();
// todo: review this approach once 5.2.3 is out
@@ -226,7 +226,7 @@ export class FilesComponent extends PageComponent implements OnInit, OnDestroy {
}
onFileUploadedEvent(event: FileUploadEvent) {
const node: MinimalNodeEntity = event.file.data;
const node: NodeEntry = event.file.data;
// check root and child nodes
if (node && node.entry && node.entry.parentId === this.getParentNodeId()) {
@@ -265,7 +265,7 @@ export class FilesComponent extends PageComponent implements OnInit, OnDestroy {
this.reload(this.selectedNode);
}
onContentCopied(nodes: MinimalNodeEntity[]) {
onContentCopied(nodes: NodeEntry[]) {
const newNode = nodes.find((node) => node && node.entry && node.entry.parentId === this.getParentNodeId());
if (newNode) {
this.reload(this.selectedNode);
@@ -273,7 +273,7 @@ export class FilesComponent extends PageComponent implements OnInit, OnDestroy {
}
// todo: review this approach once 5.2.3 is out
private async updateCurrentNode(node: MinimalNodeEntryEntity) {
private async updateCurrentNode(node: Node) {
this.nodePath = null;
if (node && node.path && node.path.elements) {
@@ -297,7 +297,7 @@ export class FilesComponent extends PageComponent implements OnInit, OnDestroy {
}
// todo: review this approach once 5.2.3 is out
private async normalizeSitePath(node: MinimalNodeEntryEntity) {
private async normalizeSitePath(node: Node) {
const elements = node.path.elements;
// remove 'Sites'
@@ -325,7 +325,7 @@ export class FilesComponent extends PageComponent implements OnInit, OnDestroy {
}
}
isSiteContainer(node: MinimalNodeEntryEntity): boolean {
isSiteContainer(node: Node): boolean {
if (node && node.aspectNames && node.aspectNames.length > 0) {
return node.aspectNames.indexOf('st:siteContainer') >= 0;
}

View File

@@ -23,7 +23,7 @@
*/
import { Component, Input, OnInit, ViewEncapsulation } from '@angular/core';
import { MinimalNodeEntryEntity } from '@alfresco/js-api';
import { Node } from '@alfresco/js-api';
import { NodePermissionService, isLocked } from '@alfresco/aca-shared';
import { MatCardModule } from '@angular/material/card';
import { NodeCommentsModule } from '@alfresco/adf-content-services';
@@ -37,7 +37,7 @@ import { NodeCommentsModule } from '@alfresco/adf-content-services';
})
export class CommentsTabComponent implements OnInit {
@Input()
node: MinimalNodeEntryEntity;
node: Node;
canUpdateNode = false;

View File

@@ -23,14 +23,16 @@
*/
import { MetadataTabComponent } from './metadata-tab.component';
import { MinimalNodeEntryEntity, Node } from '@alfresco/js-api';
import { Node } from '@alfresco/js-api';
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { AppTestingModule } from '../../../testing/app-testing.module';
import { AppConfigService } from '@alfresco/adf-core';
import { Store } from '@ngrx/store';
import { AppState, SetInfoDrawerMetadataAspectAction } from '@alfresco/aca-shared/store';
import { AppState, EditOfflineAction, SetInfoDrawerMetadataAspectAction } from '@alfresco/aca-shared/store';
import { By } from '@angular/platform-browser';
import { AppExtensionService, NodePermissionService } from '@alfresco/aca-shared';
import { Actions } from '@ngrx/effects';
import { Subject } from 'rxjs';
describe('MetadataTabComponent', () => {
let fixture: ComponentFixture<MetadataTabComponent>;
@@ -39,6 +41,7 @@ describe('MetadataTabComponent', () => {
let appConfig: AppConfigService;
let extensions: AppExtensionService;
let nodePermissionService: NodePermissionService;
let actions$: Subject<EditOfflineAction>;
const presets = {
default: {
@@ -47,11 +50,18 @@ describe('MetadataTabComponent', () => {
custom: []
};
beforeEach(() => {
actions$ = new Subject<EditOfflineAction>();
TestBed.configureTestingModule({
imports: [AppTestingModule, MetadataTabComponent]
imports: [AppTestingModule, MetadataTabComponent],
providers: [
{
provide: Actions,
useValue: actions$
}
]
});
nodePermissionService = TestBed.inject(NodePermissionService);
spyOn(nodePermissionService, 'check').and.callFake((source: MinimalNodeEntryEntity, permissions: string[]) => {
spyOn(nodePermissionService, 'check').and.callFake((source: Node, permissions: string[]) => {
return permissions.some((permission) => source.allowableOperations.includes(permission));
});
});
@@ -85,7 +95,7 @@ describe('MetadataTabComponent', () => {
});
});
describe('canUpdateNode()', () => {
describe('canUpdateNode', () => {
beforeEach(() => {
fixture = TestBed.createComponent(MetadataTabComponent);
component = fixture.componentInstance;
@@ -129,6 +139,113 @@ describe('MetadataTabComponent', () => {
component.ngOnInit();
expect(component.canUpdateNode).toBe(false);
});
describe('set by triggering EditOfflineAction', () => {
let editOfflineAction: EditOfflineAction;
beforeEach(() => {
component.node = {
id: 'some id',
allowableOperations: []
} as Node;
component.ngOnInit();
editOfflineAction = new EditOfflineAction({
entry: {
isLocked: false,
allowableOperations: ['update'],
id: component.node.id
} as Node
});
component.canUpdateNode = true;
});
it('should have set true if node is not locked and has update permission', () => {
component.canUpdateNode = false;
actions$.next(editOfflineAction);
expect(component.canUpdateNode).toBeTrue();
});
it('should not have set false if changed node has different id than original', () => {
editOfflineAction.payload.entry.id = 'some other id';
editOfflineAction.payload.entry.isLocked = true;
actions$.next(editOfflineAction);
expect(component.canUpdateNode).toBeTrue();
});
it('should have set false if node is locked', () => {
editOfflineAction.payload.entry.isLocked = true;
actions$.next(editOfflineAction);
expect(component.canUpdateNode).toBeFalse();
});
it('should have set false if node has no update permission', () => {
editOfflineAction.payload.entry.allowableOperations = ['other'];
actions$.next(editOfflineAction);
expect(component.canUpdateNode).toBeFalse();
});
it('should have set false if node has read only property', () => {
editOfflineAction.payload.entry.properties = {
'cm:lockType': 'WRITE_LOCK'
};
actions$.next(editOfflineAction);
expect(component.canUpdateNode).toBeFalse();
});
});
});
describe('editable', () => {
let editOfflineAction: EditOfflineAction;
beforeEach(() => {
fixture = TestBed.createComponent(MetadataTabComponent);
component = fixture.componentInstance;
component.node = {
id: 'some id',
allowableOperations: []
} as Node;
component.ngOnInit();
editOfflineAction = new EditOfflineAction({
entry: {
isLocked: false,
allowableOperations: ['update'],
id: component.node.id
} as Node
});
component.editable = true;
});
it('should not have set false if node is not locked and has update permission', () => {
actions$.next(editOfflineAction);
expect(component.editable).toBeTrue();
});
it('should not have set false if changed node has different id than original', () => {
editOfflineAction.payload.entry.id = 'some other id';
editOfflineAction.payload.entry.isLocked = true;
actions$.next(editOfflineAction);
expect(component.editable).toBeTrue();
});
it('should have set false if node is locked', () => {
editOfflineAction.payload.entry.isLocked = true;
actions$.next(editOfflineAction);
expect(component.editable).toBeFalse();
});
it('should have set false if node has no update permission', () => {
editOfflineAction.payload.entry.allowableOperations = ['other'];
actions$.next(editOfflineAction);
expect(component.editable).toBeFalse();
});
it('should have set false if node has read only property', () => {
editOfflineAction.payload.entry.properties = {
'cm:lockType': 'WRITE_LOCK'
};
actions$.next(editOfflineAction);
expect(component.editable).toBeFalse();
});
});
describe('displayAspect', () => {

View File

@@ -23,22 +23,29 @@
*/
import { Component, Input, ViewEncapsulation, OnInit, OnDestroy } from '@angular/core';
import { MinimalNodeEntryEntity } from '@alfresco/js-api';
import { Node } from '@alfresco/js-api';
import { NodePermissionService, isLocked, AppExtensionService } from '@alfresco/aca-shared';
import { AppStore, infoDrawerMetadataAspect } from '@alfresco/aca-shared/store';
import { AppStore, EditOfflineAction, infoDrawerMetadataAspect, NodeActionTypes } from '@alfresco/aca-shared/store';
import { AppConfigService, NotificationService } from '@alfresco/adf-core';
import { Observable, Subject } from 'rxjs';
import { Store } from '@ngrx/store';
import { ContentMetadataModule, ContentMetadataService } from '@alfresco/adf-content-services';
import { takeUntil } from 'rxjs/operators';
import { filter, takeUntil } from 'rxjs/operators';
import { CommonModule } from '@angular/common';
import { Actions, ofType } from '@ngrx/effects';
@Component({
standalone: true,
imports: [CommonModule, ContentMetadataModule],
selector: 'app-metadata-tab',
template: `
<adf-content-metadata-card [readOnly]="!canUpdateNode" [preset]="'custom'" [node]="node" [displayAspect]="displayAspect$ | async">
<adf-content-metadata-card
[readOnly]="!canUpdateNode"
[preset]="'custom'"
[node]="node"
[displayAspect]="displayAspect$ | async"
[(editable)]="editable"
>
</adf-content-metadata-card>
`,
encapsulation: ViewEncapsulation.None,
@@ -48,11 +55,11 @@ export class MetadataTabComponent implements OnInit, OnDestroy {
protected onDestroy$ = new Subject<boolean>();
@Input()
node: MinimalNodeEntryEntity;
node: Node;
displayAspect$: Observable<string>;
canUpdateNode = false;
editable = false;
constructor(
private permission: NodePermissionService,
@@ -60,7 +67,8 @@ export class MetadataTabComponent implements OnInit, OnDestroy {
private appConfig: AppConfigService,
private store: Store<AppStore>,
private notificationService: NotificationService,
private contentMetadataService: ContentMetadataService
private contentMetadataService: ContentMetadataService,
private actions$: Actions
) {
if (this.extensions.contentMetadata) {
this.appConfig.config['content-metadata'].presets = this.extensions.contentMetadata.presets;
@@ -72,13 +80,27 @@ export class MetadataTabComponent implements OnInit, OnDestroy {
this.contentMetadataService.error.pipe(takeUntil(this.onDestroy$)).subscribe((err: { message: string }) => {
this.notificationService.showError(err.message);
});
if (this.node && !isLocked({ entry: this.node })) {
this.canUpdateNode = this.permission.check(this.node, ['update']);
}
this.checkIfNodeIsUpdatable(this.node);
this.actions$
.pipe(
ofType<EditOfflineAction>(NodeActionTypes.EditOffline),
filter((updatedNode) => this.node.id === updatedNode.payload.entry.id),
takeUntil(this.onDestroy$)
)
.subscribe((updatedNode) => {
this.checkIfNodeIsUpdatable(updatedNode?.payload.entry);
if (!this.canUpdateNode) {
this.editable = false;
}
});
}
ngOnDestroy() {
this.onDestroy$.next(true);
this.onDestroy$.complete();
}
private checkIfNodeIsUpdatable(node: Node) {
this.canUpdateNode = node && !isLocked({ entry: node }) ? this.permission.check(node, ['update']) : false;
}
}

View File

@@ -23,7 +23,7 @@
*/
import { Component, Input, OnChanges, OnInit, ViewEncapsulation } from '@angular/core';
import { MinimalNodeEntryEntity } from '@alfresco/js-api';
import { Node } from '@alfresco/js-api';
import { CommonModule } from '@angular/common';
import { VersionManagerModule } from '@alfresco/adf-content-services';
import { AppConfigModule } from '@alfresco/adf-core';
@@ -55,7 +55,7 @@ import { TranslateModule } from '@ngx-translate/core';
})
export class VersionsTabComponent implements OnInit, OnChanges {
@Input()
node: MinimalNodeEntryEntity;
node: Node;
isFileSelected = false;

View File

@@ -23,7 +23,7 @@
*/
import { Component, OnInit, ViewEncapsulation } from '@angular/core';
import { MinimalNodeEntity } from '@alfresco/js-api';
import { NodeEntry } from '@alfresco/js-api';
import { debounceTime } from 'rxjs/operators';
import {
ContextActionsDirective,
@@ -78,7 +78,7 @@ export class RecentFilesComponent extends PageComponent implements OnInit {
this.columns = this.extensions.documentListPresets.recent || [];
}
onNodeDoubleClick(node: MinimalNodeEntity) {
onNodeDoubleClick(node: NodeEntry) {
if (node && node.entry) {
this.showPreview(node, { location: this.router.url });
}

View File

@@ -23,7 +23,7 @@
*/
import { Component, Input, OnInit, ViewEncapsulation, ChangeDetectionStrategy, OnDestroy } from '@angular/core';
import { MinimalNodeEntity } from '@alfresco/js-api';
import { NodeEntry } from '@alfresco/js-api';
import { ViewNodeAction, NavigateToFolder } from '@alfresco/aca-shared/store';
import { Store } from '@ngrx/store';
import { BehaviorSubject, Subject } from 'rxjs';
@@ -46,7 +46,7 @@ import { MatDialogModule } from '@angular/material/dialog';
host: { class: 'aca-search-results-row' }
})
export class SearchResultsRowComponent implements OnInit, OnDestroy {
private node: MinimalNodeEntity;
private node: NodeEntry;
private onDestroy$ = new Subject<boolean>();
@Input()

View File

@@ -23,7 +23,7 @@
*/
import { Component, OnInit, ViewEncapsulation } from '@angular/core';
import { MinimalNodeEntity, Pagination, ResultSetPaging } from '@alfresco/js-api';
import { NodeEntry, Pagination, ResultSetPaging } from '@alfresco/js-api';
import { ActivatedRoute, Params } from '@angular/router';
import { AlfrescoViewerModule, DocumentListModule, SearchModule, SearchQueryBuilderService } from '@alfresco/adf-content-services';
import {
@@ -265,7 +265,7 @@ export class SearchResultsComponent extends PageComponent implements OnInit {
return ['name', 'asc'];
}
onNodeDoubleClick(node: MinimalNodeEntity) {
onNodeDoubleClick(node: NodeEntry) {
if (node && node.entry) {
if (node.entry.isFolder) {
this.store.dispatch(new NavigateToFolder(node));

View File

@@ -24,7 +24,7 @@
import { Component, OnInit, ViewEncapsulation } from '@angular/core';
import { debounceTime } from 'rxjs/operators';
import { MinimalNodeEntity } from '@alfresco/js-api';
import { NodeEntry } from '@alfresco/js-api';
import {
AppHookService,
ContextActionsDirective,
@@ -80,7 +80,7 @@ export class SharedFilesComponent extends PageComponent implements OnInit {
this.columns = this.extensions.documentListPresets.shared || [];
}
preview(node: MinimalNodeEntity) {
preview(node: NodeEntry) {
this.showPreview(node, { location: this.router.url });
}

View File

@@ -23,7 +23,7 @@
*/
import { AppStore, DownloadNodesAction, EditOfflineAction, SnackbarErrorAction, getAppSelection } from '@alfresco/aca-shared/store';
import { MinimalNodeEntity, NodeEntry, SharedLinkEntry, Node, NodesApi } from '@alfresco/js-api';
import { NodeEntry, SharedLinkEntry, Node, NodesApi } from '@alfresco/js-api';
import { Component, OnInit, ViewEncapsulation } from '@angular/core';
import { Store } from '@ngrx/store';
import { isLocked } from '@alfresco/aca-shared';
@@ -55,7 +55,7 @@ import { MatIconModule } from '@angular/material/icon';
})
export class ToggleEditOfflineComponent implements OnInit {
private nodesApi: NodesApi;
selection: MinimalNodeEntity;
selection: NodeEntry;
nodeTitle = '';
isNodeLocked = false;