Skip to content

Commit

Permalink
feat(lists): read mode project member (DEV-1343) (#825)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
Vijeinath committed Sep 20, 2022
1 parent 0318a8d commit b818288
Show file tree
Hide file tree
Showing 13 changed files with 270 additions and 69 deletions.
@@ -1,5 +1,5 @@
<!-- add new node item -->
<div class="new-list-item medium-field" *ngIf="newNode">
<div class="new-list-item medium-field" *ngIf="newNode && projectAdmin">
<app-string-literal-input class="list-item-label" [placeholder]="placeholder" [value]="[]"
(dataChanged)="handleData($event)" [language]="language" (enter)="createChildNode()">
</app-string-literal-input>
Expand Down
Expand Up @@ -13,7 +13,7 @@
}
.add-node-btn,
.edit-node-btn,
.progress-indicator
.progress-indicator,
.update-success-btn {
margin: 0 0 0 8px;
}
Expand Down
Expand Up @@ -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';
Expand All @@ -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.
Expand Down Expand Up @@ -78,6 +86,10 @@ describe('ListItemFormComponent', () => {
}
};

const sessionServiceSpy = jasmine.createSpyObj('SessionService', ['getSession']);

const cacheServiceSpy = jasmine.createSpyObj('CacheService', ['get']);

TestBed.configureTestingModule({
declarations: [
ListItemFormComponent,
Expand Down Expand Up @@ -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<SessionService>).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<CacheService>).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();
Expand Down
33 changes: 31 additions & 2 deletions src/app/project/list/list-item-form/list-item-form.component.ts
Expand Up @@ -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';
Expand Down Expand Up @@ -90,6 +93,11 @@ export class ListItemFormComponent implements OnInit {

@Output() refreshParent: EventEmitter<ListNodeOperation> = new EventEmitter<ListNodeOperation>();

// permissions of logged-in user
session: Session;
sysAdmin = false;
projectAdmin = false;

loading: boolean;

initComponent: boolean;
Expand All @@ -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;

Expand Down Expand Up @@ -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;
}
}

/**
Expand Down
20 changes: 10 additions & 10 deletions src/app/project/list/list-item/list-item.component.html
Expand Up @@ -2,28 +2,28 @@
<div class="list-node" *ngFor="let node of list; let first = first; let last = last;">

<!-- button to expand / close node -->
<button type="button" mat-icon-button (click)="toggleChildren(node.id)" class="">
<mat-icon class="mat-icon-rtl-mirror">
{{showChildren(node.id) ? 'expand_more' : 'chevron_right'}}
</mat-icon>
</button>
<div class="expand-close-container">
<button *ngIf="projectAdmin || node.children.length !== 0" type="button" mat-icon-button (click)="toggleChildren(node.id)">
<mat-icon class="mat-icon-rtl-mirror">
{{showChildren(node.id) ? 'expand_more' : 'chevron_right'}}
</mat-icon>
</button>
</div>

<!-- existing node: show label in form; value is e.g. {{node.labels[0].value}} -->
<app-list-item-form [iri]="node.id" [language]="language" (refreshParent)="updateView($event, true)"
[projectIri]="projectIri" [projectCode]="projectCode" [projectStatus]="projectStatus" [labels]="node.labels"
[position]="node.position" [lastPosition]="last" [parentIri]="parentIri">
</app-list-item-form>


<!-- recursion: list with child nodes -->
<app-list-item class="child-node" *ngIf="node.id === expandedNode && node.children.length > 0"
[language]="language" [childNode]="true" [list]="node.children" [parentIri]="node.id"
[projectIri]="projectIri" [projectCode]="projectCode" [projectStatus]="projectStatus" (refreshChildren)="updateParentNodeChildren($event, node.position)">
</app-list-item>


<!-- in case of none children: show form to append new item -->
<div *ngIf="node.id === expandedNode && node.children.length === 0 && projectStatus" class="child-node">
<!-- in case of none children: show form to append new item -->
<div class="child-node" *ngIf="node.id === expandedNode && node.children.length === 0 && projectStatus && projectAdmin">
<!-- first child should have an empty list? yes -->
<app-list-item-form class="append-child-node" [parentIri]="node.id" [projectIri]="projectIri"
[projectCode]="projectCode" [language]="language" [newNode]="true"
Expand All @@ -32,7 +32,7 @@
</div>

<!-- form to append new item to parent node -->
<app-list-item-form class="list-node append-child-node" *ngIf="last && projectStatus" [parentIri]="parentIri"
<app-list-item-form class="list-node append-child-node" *ngIf="last && projectStatus && projectAdmin" [parentIri]="parentIri"
[projectIri]="projectIri" [projectCode]="projectCode" [language]="language"
[newNode]="true" (refreshParent)="updateView($event)">
</app-list-item-form>
Expand Down
5 changes: 5 additions & 0 deletions src/app/project/list/list-item/list-item.component.scss
Expand Up @@ -3,6 +3,11 @@
display: flex;
flex-wrap: wrap;
width: 640px;

.expand-close-container {
width: 40px;
height: 40px;
}
}

.child-node {
Expand Down
58 changes: 57 additions & 1 deletion src/app/project/list/list-item/list-item.component.spec.ts
Expand Up @@ -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.
Expand Down Expand Up @@ -71,6 +81,10 @@ describe('ListItemComponent', () => {
}
};

const sessionServiceSpy = jasmine.createSpyObj('SessionService', ['getSession']);

const cacheServiceSpy = jasmine.createSpyObj('CacheService', ['get']);

TestBed.configureTestingModule({
declarations: [
ListItemComponent,
Expand All @@ -87,6 +101,14 @@ describe('ListItemComponent', () => {
{
provide: DspApiConnectionToken,
useValue: listsEndpointSpyObj
},
{
provide: SessionService,
useValue: sessionServiceSpy
},
{
provide: CacheService,
useValue: cacheServiceSpy
}
],
schemas: [CUSTOM_ELEMENTS_SCHEMA]
Expand All @@ -95,6 +117,40 @@ describe('ListItemComponent', () => {
}));

beforeEach(() => {
// mock session service
const sessionSpy = TestBed.inject(SessionService);

(sessionSpy as jasmine.SpyObj<SessionService>).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<CacheService>).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);

Expand Down
31 changes: 29 additions & 2 deletions src/app/project/list/list-item/list-item.component.ts
Expand Up @@ -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',
Expand All @@ -36,14 +39,38 @@ export class ListItemComponent implements OnInit {

@Output() refreshChildren: EventEmitter<ListNode[]> = new EventEmitter<ListNode[]>();

// 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(
Expand Down

0 comments on commit b818288

Please sign in to comment.