From b81828840428ca21e3cf6819c6beea8aca1540e0 Mon Sep 17 00:00:00 2001 From: Vijeinath Tissaveerasingham Date: Tue, 20 Sep 2022 11:39:50 +0200 Subject: [PATCH] feat(lists): read mode project member (DEV-1343) (#825) * chore: change typo in comment * chore: change typo in comment * chore: clean up * chore: clean up * refactor(project): enable lists tab * feat(list): Showing list for project member * feat(list-item): hide crud operations for project member * feat(list): display list section for project member * test(list-item): fix unit tests * test(list-item-form): fix unit tests * test(list-item-component): fix lint errors * fix(list-item): correct gap * refactor(list-item): delete member --- .../list-item-form.component.html | 2 +- .../list-item-form.component.scss | 2 +- .../list-item-form.component.spec.ts | 57 +++++++++++- .../list-item-form.component.ts | 33 ++++++- .../list/list-item/list-item.component.html | 20 ++--- .../list/list-item/list-item.component.scss | 5 ++ .../list-item/list-item.component.spec.ts | 58 +++++++++++- .../list/list-item/list-item.component.ts | 31 ++++++- src/app/project/list/list.component.html | 90 ++++++++++--------- src/app/project/list/list.component.ts | 27 +++++- src/app/project/project.component.html | 8 +- src/app/project/project.component.ts | 4 +- .../workspace/results/results.component.ts | 2 +- 13 files changed, 270 insertions(+), 69 deletions(-) diff --git a/src/app/project/list/list-item-form/list-item-form.component.html b/src/app/project/list/list-item-form/list-item-form.component.html index 0642f835c7..7d54c47bd0 100644 --- a/src/app/project/list/list-item-form/list-item-form.component.html +++ b/src/app/project/list/list-item-form/list-item-form.component.html @@ -1,5 +1,5 @@ -
+
diff --git a/src/app/project/list/list-item-form/list-item-form.component.scss b/src/app/project/list/list-item-form/list-item-form.component.scss index 18594bec0f..efcbf8e385 100644 --- a/src/app/project/list/list-item-form/list-item-form.component.scss +++ b/src/app/project/list/list-item-form/list-item-form.component.scss @@ -13,7 +13,7 @@ } .add-node-btn, .edit-node-btn, -.progress-indicator +.progress-indicator, .update-success-btn { margin: 0 0 0 8px; } diff --git a/src/app/project/list/list-item-form/list-item-form.component.spec.ts b/src/app/project/list/list-item-form/list-item-form.component.spec.ts index 41f2aa5d46..bb2f420f9d 100644 --- a/src/app/project/list/list-item-form/list-item-form.component.spec.ts +++ b/src/app/project/list/list-item-form/list-item-form.component.spec.ts @@ -10,7 +10,13 @@ import { MatDialogHarness } from '@angular/material/dialog/testing'; import { MatIconModule } from '@angular/material/icon'; import { MatSnackBarModule } from '@angular/material/snack-bar'; import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; -import { ApiResponseData, DeleteListNodeResponse, ListsEndpointAdmin, StringLiteral } from '@dasch-swiss/dsp-js'; +import { + ApiResponseData, + DeleteListNodeResponse, + ListsEndpointAdmin, MockProjects, + ProjectResponse, ReadProject, + StringLiteral +} from '@dasch-swiss/dsp-js'; import { TranslateModule } from '@ngx-translate/core'; import { of } from 'rxjs'; import { AjaxResponse } from 'rxjs/ajax'; @@ -20,6 +26,8 @@ import { DialogComponent } from 'src/app/main/dialog/dialog.component'; import { StringifyStringLiteralPipe } from 'src/app/main/pipes/string-transformation/stringify-string-literal.pipe'; import { TruncatePipe } from 'src/app/main/pipes/string-transformation/truncate.pipe'; import { ListItemFormComponent, ListNodeOperation } from './list-item-form.component'; +import { Session, SessionService } from '../../../main/services/session.service'; +import { CacheService } from '../../../main/cache/cache.service'; /** * test host component to simulate parent component. @@ -78,6 +86,10 @@ describe('ListItemFormComponent', () => { } }; + const sessionServiceSpy = jasmine.createSpyObj('SessionService', ['getSession']); + + const cacheServiceSpy = jasmine.createSpyObj('CacheService', ['get']); + TestBed.configureTestingModule({ declarations: [ ListItemFormComponent, @@ -108,12 +120,55 @@ describe('ListItemFormComponent', () => { provide: MatDialogRef, useValue: {} }, + { + provide: SessionService, + useValue: sessionServiceSpy + }, + { + provide: CacheService, + useValue: cacheServiceSpy + } ] }) .compileComponents(); })); beforeEach(() => { + // mock session service + const sessionSpy = TestBed.inject(SessionService); + + (sessionSpy as jasmine.SpyObj).getSession.and.callFake( + () => { + const session: Session = { + id: 12345, + user: { + name: 'username', + jwt: 'myToken', + lang: 'en', + sysAdmin: true, + projectAdmin: [] + } + }; + + return session; + } + ); + + // mock cache service + const cacheSpy = TestBed.inject(CacheService); + + (cacheSpy as jasmine.SpyObj).get.and.callFake( + () => { + const response: ProjectResponse = new ProjectResponse(); + + const mockProjects = MockProjects.mockProjects(); + + response.project = mockProjects.body.projects[0]; + + return of(response.project as ReadProject); + } + ); + testHostFixture = TestBed.createComponent(TestHostComponent); testHostComponent = testHostFixture.componentInstance; testHostFixture.detectChanges(); diff --git a/src/app/project/list/list-item-form/list-item-form.component.ts b/src/app/project/list/list-item-form/list-item-form.component.ts index f12728113c..36a121379d 100644 --- a/src/app/project/list/list-item-form/list-item-form.component.ts +++ b/src/app/project/list/list-item-form/list-item-form.component.ts @@ -11,11 +11,14 @@ import { ListInfoResponse, ListNode, ListNodeInfoResponse, + ReadProject, StringLiteral } from '@dasch-swiss/dsp-js'; import { DspApiConnectionToken } from 'src/app/main/declarations/dsp-api-tokens'; import { DialogComponent } from 'src/app/main/dialog/dialog.component'; import { ErrorHandlerService } from 'src/app/main/services/error-handler.service'; +import { Session, SessionService } from '../../../main/services/session.service'; +import { CacheService } from '../../../main/cache/cache.service'; export class ListNodeOperation { operation: 'create' | 'insert' | 'update' | 'delete' | 'reposition'; @@ -90,6 +93,11 @@ export class ListItemFormComponent implements OnInit { @Output() refreshParent: EventEmitter = new EventEmitter(); + // permissions of logged-in user + session: Session; + sysAdmin = false; + projectAdmin = false; + loading: boolean; initComponent: boolean; @@ -101,10 +109,29 @@ export class ListItemFormComponent implements OnInit { constructor( @Inject(DspApiConnectionToken) private _dspApiConnection: KnoraApiConnection, private _errorHandler: ErrorHandlerService, - private _dialog: MatDialog + private _dialog: MatDialog, + private _session: SessionService, + private _cache: CacheService, ) { } ngOnInit() { + // get information about the logged-in user + this.session = this._session.getSession(); + + // is the logged-in user system admin? + this.sysAdmin = this.session.user.sysAdmin; + + // get the project data from cache + this._cache.get(this.projectCode).subscribe( + (response: ReadProject) => { + + // is logged-in user projectAdmin? + this.projectAdmin = this.sysAdmin ? this.sysAdmin : this.session.user.projectAdmin.some(e => e === response.id); + }, + (error: ApiResponseError) => { + this._errorHandler.showMessage(error); + } + ); this.initComponent = true; @@ -199,7 +226,9 @@ export class ListItemFormComponent implements OnInit { * show action bubble with various CRUD buttons when hovered over. */ mouseEnter() { - this.showActionBubble = true; + if (this.projectAdmin) { + this.showActionBubble = true; + } } /** diff --git a/src/app/project/list/list-item/list-item.component.html b/src/app/project/list/list-item/list-item.component.html index c1a931c4a0..6cb92e240d 100644 --- a/src/app/project/list/list-item/list-item.component.html +++ b/src/app/project/list/list-item/list-item.component.html @@ -2,11 +2,13 @@
- +
+ +
- - - -
+ +
- diff --git a/src/app/project/list/list-item/list-item.component.scss b/src/app/project/list/list-item/list-item.component.scss index df4a2d6d62..bcec8013aa 100644 --- a/src/app/project/list/list-item/list-item.component.scss +++ b/src/app/project/list/list-item/list-item.component.scss @@ -3,6 +3,11 @@ display: flex; flex-wrap: wrap; width: 640px; + + .expand-close-container { + width: 40px; + height: 40px; + } } .child-node { diff --git a/src/app/project/list/list-item/list-item.component.spec.ts b/src/app/project/list/list-item/list-item.component.spec.ts index 9997b44b29..98b644c8a5 100644 --- a/src/app/project/list/list-item/list-item.component.spec.ts +++ b/src/app/project/list/list-item/list-item.component.spec.ts @@ -4,12 +4,22 @@ import { MatDialogModule } from '@angular/material/dialog'; import { MatIconModule } from '@angular/material/icon'; import { MatSnackBarModule } from '@angular/material/snack-bar'; import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; -import { ApiResponseData, ListNode, ListNodeInfo, ListResponse, ListsEndpointAdmin, RepositionChildNodeResponse } from '@dasch-swiss/dsp-js'; +import { + ApiResponseData, + ListNode, + ListNodeInfo, + ListResponse, + ListsEndpointAdmin, MockProjects, + ProjectResponse, ReadProject, + RepositionChildNodeResponse +} from '@dasch-swiss/dsp-js'; import { of } from 'rxjs'; import { AjaxResponse } from 'rxjs/ajax'; import { DspApiConnectionToken } from 'src/app/main/declarations/dsp-api-tokens'; import { ListNodeOperation } from '../list-item-form/list-item-form.component'; import { ListItemComponent } from './list-item.component'; +import { Session, SessionService } from '../../../main/services/session.service'; +import { CacheService } from '../../../main/cache/cache.service'; /** * test host component to simulate parent component. @@ -71,6 +81,10 @@ describe('ListItemComponent', () => { } }; + const sessionServiceSpy = jasmine.createSpyObj('SessionService', ['getSession']); + + const cacheServiceSpy = jasmine.createSpyObj('CacheService', ['get']); + TestBed.configureTestingModule({ declarations: [ ListItemComponent, @@ -87,6 +101,14 @@ describe('ListItemComponent', () => { { provide: DspApiConnectionToken, useValue: listsEndpointSpyObj + }, + { + provide: SessionService, + useValue: sessionServiceSpy + }, + { + provide: CacheService, + useValue: cacheServiceSpy } ], schemas: [CUSTOM_ELEMENTS_SCHEMA] @@ -95,6 +117,40 @@ describe('ListItemComponent', () => { })); beforeEach(() => { + // mock session service + const sessionSpy = TestBed.inject(SessionService); + + (sessionSpy as jasmine.SpyObj).getSession.and.callFake( + () => { + const session: Session = { + id: 12345, + user: { + name: 'username', + jwt: 'myToken', + lang: 'en', + sysAdmin: true, + projectAdmin: [] + } + }; + + return session; + } + ); + + // mock cache service + const cacheSpy = TestBed.inject(CacheService); + + (cacheSpy as jasmine.SpyObj).get.and.callFake( + () => { + const response: ProjectResponse = new ProjectResponse(); + + const mockProjects = MockProjects.mockProjects(); + + response.project = mockProjects.body.projects[0]; + + return of(response.project as ReadProject); + } + ); const dspConnSpy = TestBed.inject(DspApiConnectionToken); diff --git a/src/app/project/list/list-item/list-item.component.ts b/src/app/project/list/list-item/list-item.component.ts index 7697735afd..479be12d39 100644 --- a/src/app/project/list/list-item/list-item.component.ts +++ b/src/app/project/list/list-item/list-item.component.ts @@ -6,12 +6,15 @@ import { ListChildNodeResponse, ListNode, ListResponse, + ReadProject, RepositionChildNodeRequest, - RepositionChildNodeResponse + RepositionChildNodeResponse, } from '@dasch-swiss/dsp-js'; import { DspApiConnectionToken } from 'src/app/main/declarations/dsp-api-tokens'; import { ErrorHandlerService } from 'src/app/main/services/error-handler.service'; import { ListNodeOperation } from '../list-item-form/list-item-form.component'; +import { Session, SessionService } from '../../../main/services/session.service'; +import { CacheService } from '../../../main/cache/cache.service'; @Component({ selector: 'app-list-item', @@ -36,14 +39,38 @@ export class ListItemComponent implements OnInit { @Output() refreshChildren: EventEmitter = new EventEmitter(); + // permissions of logged-in user + session: Session; + sysAdmin = false; + projectAdmin = false; + expandedNode: string; constructor( @Inject(DspApiConnectionToken) private _dspApiConnection: KnoraApiConnection, - private _errorHandler: ErrorHandlerService + private _errorHandler: ErrorHandlerService, + private _session: SessionService, + private _cache: CacheService, ) { } ngOnInit() { + // get information about the logged-in user + this.session = this._session.getSession(); + + // is the logged-in user system admin? + this.sysAdmin = this.session.user.sysAdmin; + + // get the project data from cache + this._cache.get(this.projectCode).subscribe( + (response: ReadProject) => { + // is logged-in user projectAdmin? + this.projectAdmin = this.sysAdmin ? this.sysAdmin : this.session.user.projectAdmin.some(e => e === response.id); + }, + (error: ApiResponseError) => { + this._errorHandler.showMessage(error); + } + ); + // in case of parent node: run the following request to get the entire list if (!this.childNode) { this._dspApiConnection.admin.listsEndpoint.getList(this.parentIri).subscribe( diff --git a/src/app/project/list/list.component.html b/src/app/project/list/list.component.html index 02a0d428f7..9f35b160ec 100644 --- a/src/app/project/list/list.component.html +++ b/src/app/project/list/list.component.html @@ -1,26 +1,29 @@ -
- +
- -
-
-

Lists are reusable data

-
+ +
+
+ + +
+
+

Lists are reusable data

+
-
-

+
+

Project has {{lists.length | i18nPlural: itemPluralMapping['list']}} - + It seems there's no list defined yet -

+

- + - +
@@ -32,7 +35,7 @@

+ [matTooltip]="list.comments | appStringifyStringLiteral" matTooltipPosition="below"> {{list.labels | appStringifyStringLiteral:'all'}} @@ -43,57 +46,58 @@

- + - + -

+
-
+
- -
+ +
- - -

{{list.labels | appStringifyStringLiteral}}

- -

- {{list.comments | appStringifyStringLiteral | appTruncate:64}}

- + + +

{{list.labels | appStringifyStringLiteral}}

+ +

+ {{list.comments | appStringifyStringLiteral | appTruncate:64}}

+ - +

List configuration

-
-
+
+
- - + + -
- -
+
+
-
- +
+ +
+
diff --git a/src/app/project/list/list.component.ts b/src/app/project/list/list.component.ts index 8819b3a0c5..b71422b76d 100644 --- a/src/app/project/list/list.component.ts +++ b/src/app/project/list/list.component.ts @@ -12,7 +12,8 @@ import { ListNodeInfo, ListsResponse, ReadProject, - StringLiteral + StringLiteral, + UserResponse } from '@dasch-swiss/dsp-js'; import { AppGlobal } from 'src/app/app-global'; import { AppInitService } from 'src/app/app-init.service'; @@ -29,7 +30,7 @@ import { Session, SessionService } from 'src/app/main/services/session.service'; }) export class ListComponent implements OnInit { - // loading for progess indicator + // loading for progress indicator loading: boolean; loadList: boolean; @@ -37,6 +38,7 @@ export class ListComponent implements OnInit { session: Session; sysAdmin = false; projectAdmin = false; + projectMember = undefined; // project shortcode; as identifier in project cache service projectCode: string; @@ -142,6 +144,27 @@ export class ListComponent implements OnInit { // is logged-in user projectAdmin? this.projectAdmin = this.sysAdmin ? this.sysAdmin : this.session.user.projectAdmin.some(e => e === this.project.id); + // or at least project member? + if (!this.projectAdmin) { + this._dspApiConnection.admin.usersEndpoint.getUserByUsername(this.session.user.name).subscribe( + (res: ApiResponseData) => { + const usersProjects = res.body.user.projects; + if (usersProjects.length === 0) { + // the user is not part of any project + this.projectMember = false; + } else { + // check if the user is member of the current project + this.projectMember = usersProjects.some(p => p.shortcode === this.projectCode); + } + }, + (error: ApiResponseError) => { + this._errorHandler.showMessage(error); + } + ); + } else { + this.projectMember = this.projectAdmin; + } + this.initList(); this.listForm = this._fb.group({ diff --git a/src/app/project/project.component.html b/src/app/project/project.component.html index 5b578b2035..53f15a20c1 100644 --- a/src/app/project/project.component.html +++ b/src/app/project/project.component.html @@ -5,7 +5,7 @@ class="navigation tab-bar margin-from-top" animationDuration="0ms" [color]="color"> + [disabled]="!projectAdmin && !first && i !== TAB_DATA_MODEL && i !== TAB_LISTS" [class.deactivated]="!project?.status"> {{link.icon}} {{link.label}} @@ -83,14 +83,14 @@ - +
- + add_circle_outline

Add a new list

@@ -146,4 +146,4 @@

Settings

- \ No newline at end of file + diff --git a/src/app/project/project.component.ts b/src/app/project/project.component.ts index 71ebfd995f..a16cf96354 100644 --- a/src/app/project/project.component.ts +++ b/src/app/project/project.component.ts @@ -26,8 +26,10 @@ import { OntologyService } from './ontology/ontology.service'; styleUrls: ['./project.component.scss'] }) export class ProjectComponent implements OnInit { + readonly TAB_DATA_MODEL = 3; + readonly TAB_LISTS = 4; - // loading for progess indicator + // loading for progress indicator loading: boolean; // error in case of wrong project code error: boolean; diff --git a/src/app/workspace/results/results.component.ts b/src/app/workspace/results/results.component.ts index ac22c566d0..cba79f9b94 100644 --- a/src/app/workspace/results/results.component.ts +++ b/src/app/workspace/results/results.component.ts @@ -6,7 +6,7 @@ import { FilteredResources, SearchParams } from './list-view/list-view.component export interface SplitSize { gutterNum: number; sizes: Array; -}; +} @Component({ selector: 'app-results',