Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(lists): read mode project member (DEV-1343) #825

Merged
merged 13 commits into from Sep 20, 2022
@@ -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
37 changes: 35 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,14 @@ export class ListItemFormComponent implements OnInit {

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

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

// project data
project: ReadProject;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think this variable is needed, you can use a const to temporarily store the project you get from the cache

Copy link
Collaborator Author

@Vijeinath Vijeinath Sep 20, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You are right, it is not needed elsewhere (ff8b5a8)


loading: boolean;

initComponent: boolean;
Expand All @@ -101,10 +112,30 @@ 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) => {
this.project = response;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can use a const here instead

Copy link
Collaborator Author

@Vijeinath Vijeinath Sep 20, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would go further and not even use const but pass the response directly because it is not needed elsewhere (ff8b5a8)


// is logged-in user projectAdmin?
this.projectAdmin = this.sysAdmin ? this.sysAdmin : this.session.user.projectAdmin.some(e => e === this.project.id);
},
(error: ApiResponseError) => {
this._errorHandler.showMessage(error);
}
);

this.initComponent = true;

Expand Down Expand Up @@ -199,7 +230,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
36 changes: 34 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,43 @@ export class ListItemComponent implements OnInit {

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

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

// project data
project: ReadProject;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same thing here as in the list-item-form component

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.


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) => {
this.project = response;

// is logged-in user projectAdmin?
this.projectAdmin = this.sysAdmin ? this.sysAdmin : this.session.user.projectAdmin.some(e => e === this.project.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