TSK-1349: Workbasket access items unit tests (#1246)
* TSK-1349: remove html coverage to optimize test speed * TSK-1349: init workbasket access items testing env * TSK-1349: update workbasket access items unit tests
This commit is contained in:
parent
827703a176
commit
88b8486a49
|
|
@ -9,7 +9,8 @@ module.exports = {
|
||||||
testMatch: ['**/+(*.)+(spec).+(ts)'],
|
testMatch: ['**/+(*.)+(spec).+(ts)'],
|
||||||
setupFilesAfterEnv: ['<rootDir>/src/test.ts'],
|
setupFilesAfterEnv: ['<rootDir>/src/test.ts'],
|
||||||
collectCoverage: true,
|
collectCoverage: true,
|
||||||
coverageReporters: ['html', 'text'],
|
coverageReporters: ['text'],
|
||||||
|
// coverageReporters: ['html', 'text'],
|
||||||
coverageDirectory: 'coverage/taskana-web',
|
coverageDirectory: 'coverage/taskana-web',
|
||||||
moduleNameMapper: pathsToModuleNameMapper(compilerOptions.paths || {}, {
|
moduleNameMapper: pathsToModuleNameMapper(compilerOptions.paths || {}, {
|
||||||
prefix: '<rootDir>/'
|
prefix: '<rootDir>/'
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@
|
||||||
<button type="button" (click)="onSubmit()" [disabled]="action === 'COPY'" data-toggle="tooltip" title="Save" class="btn btn-default btn-primary">
|
<button type="button" (click)="onSubmit()" [disabled]="action === 'COPY'" data-toggle="tooltip" title="Save" class="btn btn-default btn-primary">
|
||||||
<span class="material-icons md-20">save</span>
|
<span class="material-icons md-20">save</span>
|
||||||
</button>
|
</button>
|
||||||
<button type="button" (click)="clear()" data-toggle="tooltip" title="Undo Changes" class="btn btn-default">
|
<button type="button" (click)="clear()" data-toggle="tooltip" title="Undo Changes" class="btn btn-default undo-button">
|
||||||
<span class="material-icons md-20 blue">undo</span>
|
<span class="material-icons md-20 blue">undo</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -115,7 +115,7 @@
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<!-- ADD ACCESS ITEM -->
|
<!-- ADD ACCESS ITEM -->
|
||||||
<button type="button" (click)="addAccessItem()" data-toggle="tooltip" title="Add new access" class="btn btn-default">
|
<button type="button" (click)="addAccessItem()" data-toggle="tooltip" title="Add new access" class="btn btn-default add-access-item">
|
||||||
<span class="material-icons md-20 green-blue">add</span>
|
<span class="material-icons md-20 green-blue">add</span>
|
||||||
<span>Add new access</span>
|
<span>Add new access</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,197 @@
|
||||||
|
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
import { WorkbasketAccessItemsComponent } from './workbasket-access-items.component';
|
||||||
|
import { Component, DebugElement, Input } from '@angular/core';
|
||||||
|
import { Actions, NgxsModule, ofActionDispatched, Store } from '@ngxs/store';
|
||||||
|
import { Observable, of } from 'rxjs';
|
||||||
|
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
|
||||||
|
import { TypeAheadComponent } from '../../../shared/components/type-ahead/type-ahead.component';
|
||||||
|
import { TypeaheadModule } from 'ngx-bootstrap';
|
||||||
|
import { SavingWorkbasketService } from '../../services/saving-workbaskets.service';
|
||||||
|
import { RequestInProgressService } from '../../../shared/services/request-in-progress/request-in-progress.service';
|
||||||
|
import { FormsValidatorService } from '../../../shared/services/forms-validator/forms-validator.service';
|
||||||
|
import { NotificationService } from '../../../shared/services/notifications/notification.service';
|
||||||
|
import { WorkbasketState } from '../../../shared/store/workbasket-store/workbasket.state';
|
||||||
|
import { EngineConfigurationState } from '../../../shared/store/engine-configuration-store/engine-configuration.state';
|
||||||
|
import { ClassificationCategoriesService } from '../../../shared/services/classification-categories/classification-categories.service';
|
||||||
|
import { HttpClientTestingModule } from '@angular/common/http/testing';
|
||||||
|
import { WorkbasketService } from '../../../shared/services/workbasket/workbasket.service';
|
||||||
|
import { DomainService } from '../../../shared/services/domain/domain.service';
|
||||||
|
import { RouterTestingModule } from '@angular/router/testing';
|
||||||
|
import { SelectedRouteService } from '../../../shared/services/selected-route/selected-route';
|
||||||
|
import { StartupService } from '../../../shared/services/startup/startup.service';
|
||||||
|
import { TaskanaEngineService } from '../../../shared/services/taskana-engine/taskana-engine.service';
|
||||||
|
import { WindowRefService } from '../../../shared/services/window/window.service';
|
||||||
|
import {
|
||||||
|
workbasketAccessItemsMock,
|
||||||
|
engineConfigurationMock,
|
||||||
|
selectedWorkbasketMock
|
||||||
|
} from '../../../shared/store/mock-data/mock-store';
|
||||||
|
import {
|
||||||
|
GetWorkbasketAccessItems,
|
||||||
|
UpdateWorkbasketAccessItems
|
||||||
|
} from '../../../shared/store/workbasket-store/workbasket.actions';
|
||||||
|
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
|
||||||
|
import { ACTION } from '../../../shared/models/action';
|
||||||
|
import { WorkbasketAccessItems } from '../../../shared/models/workbasket-access-items';
|
||||||
|
|
||||||
|
@Component({ selector: 'taskana-shared-spinner', template: '' })
|
||||||
|
class SpinnerStub {
|
||||||
|
@Input() isRunning: boolean;
|
||||||
|
@Input() positionClass: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const savingWorkbasketServiceSpy = jest.fn().mockImplementation(
|
||||||
|
(): Partial<SavingWorkbasketService> => ({
|
||||||
|
triggeredAccessItemsSaving: jest.fn().mockReturnValue(of(true))
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
const requestInProgressServiceSpy = jest.fn().mockImplementation(
|
||||||
|
(): Partial<RequestInProgressService> => ({
|
||||||
|
setRequestInProgress: jest.fn()
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
const showDialogFn = jest.fn().mockReturnValue(true);
|
||||||
|
const notificationServiceSpy = jest.fn().mockImplementation(
|
||||||
|
(): Partial<NotificationService> => ({
|
||||||
|
triggerError: showDialogFn,
|
||||||
|
showToast: showDialogFn
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
const validateFormInformationFn = jest.fn().mockImplementation((): Promise<any> => Promise.resolve(true));
|
||||||
|
const formValidatorServiceSpy = jest.fn().mockImplementation(
|
||||||
|
(): Partial<FormsValidatorService> => ({
|
||||||
|
isFieldValid: jest.fn().mockReturnValue(true),
|
||||||
|
validateInputOverflow: jest.fn(),
|
||||||
|
validateFormInformation: validateFormInformationFn,
|
||||||
|
get inputOverflowObservable(): Observable<Map<string, boolean>> {
|
||||||
|
return of(new Map<string, boolean>());
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
describe('WorkbasketAccessItemsComponent', () => {
|
||||||
|
let fixture: ComponentFixture<WorkbasketAccessItemsComponent>;
|
||||||
|
let debugElement: DebugElement;
|
||||||
|
let component: WorkbasketAccessItemsComponent;
|
||||||
|
let store: Store;
|
||||||
|
let actions$: Observable<any>;
|
||||||
|
|
||||||
|
beforeEach(async(() => {
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
imports: [
|
||||||
|
FormsModule,
|
||||||
|
ReactiveFormsModule,
|
||||||
|
TypeaheadModule.forRoot(),
|
||||||
|
NgxsModule.forRoot([WorkbasketState, EngineConfigurationState]),
|
||||||
|
HttpClientTestingModule,
|
||||||
|
RouterTestingModule.withRoutes([]),
|
||||||
|
BrowserAnimationsModule
|
||||||
|
],
|
||||||
|
declarations: [WorkbasketAccessItemsComponent, TypeAheadComponent, SpinnerStub],
|
||||||
|
providers: [
|
||||||
|
{ provide: SavingWorkbasketService, useClass: savingWorkbasketServiceSpy },
|
||||||
|
{ provide: RequestInProgressService, useClass: requestInProgressServiceSpy },
|
||||||
|
{ provide: FormsValidatorService, useClass: formValidatorServiceSpy },
|
||||||
|
{ provide: NotificationService, useClass: notificationServiceSpy },
|
||||||
|
ClassificationCategoriesService,
|
||||||
|
WorkbasketService,
|
||||||
|
DomainService,
|
||||||
|
SelectedRouteService,
|
||||||
|
StartupService,
|
||||||
|
TaskanaEngineService,
|
||||||
|
WindowRefService
|
||||||
|
]
|
||||||
|
}).compileComponents();
|
||||||
|
|
||||||
|
fixture = TestBed.createComponent(WorkbasketAccessItemsComponent);
|
||||||
|
debugElement = fixture.debugElement;
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
store = TestBed.inject(Store);
|
||||||
|
actions$ = TestBed.inject(Actions);
|
||||||
|
component.workbasket = { ...selectedWorkbasketMock };
|
||||||
|
component.accessItemsRepresentation = workbasketAccessItemsMock;
|
||||||
|
store.reset({
|
||||||
|
...store.snapshot(),
|
||||||
|
engineConfiguration: engineConfigurationMock,
|
||||||
|
workbasket: {
|
||||||
|
workbasketAccessItems: workbasketAccessItemsMock
|
||||||
|
}
|
||||||
|
});
|
||||||
|
fixture.detectChanges();
|
||||||
|
}));
|
||||||
|
|
||||||
|
afterEach(async(() => {
|
||||||
|
component.workbasket = { ...selectedWorkbasketMock };
|
||||||
|
}));
|
||||||
|
|
||||||
|
it('should create component', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should initialize when accessItems exist', async(() => {
|
||||||
|
component.action = ACTION.COPY;
|
||||||
|
let actionDispatched = false;
|
||||||
|
component.onSave = jest.fn().mockImplementation();
|
||||||
|
actions$.pipe(ofActionDispatched(GetWorkbasketAccessItems)).subscribe(() => (actionDispatched = true));
|
||||||
|
component.init();
|
||||||
|
expect(component.initialized).toBe(true);
|
||||||
|
expect(actionDispatched).toBe(true);
|
||||||
|
expect(component.onSave).toHaveBeenCalled();
|
||||||
|
}));
|
||||||
|
|
||||||
|
it("should discard initializing when accessItems don't exist", () => {
|
||||||
|
component.workbasket._links.accessItems = null;
|
||||||
|
component.init();
|
||||||
|
expect(component.initialized).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should add accessItems when add access item button is clicked', () => {
|
||||||
|
const addAccessItemButton = debugElement.nativeElement.querySelector('button.add-access-item');
|
||||||
|
const clearSpy = jest.spyOn(component, 'addAccessItem');
|
||||||
|
expect(addAccessItemButton.title).toMatch('Add new access');
|
||||||
|
|
||||||
|
addAccessItemButton.click();
|
||||||
|
expect(clearSpy).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should undo changes when undo button is clicked', () => {
|
||||||
|
const undoButton = debugElement.nativeElement.querySelector('button.undo-button');
|
||||||
|
const clearSpy = jest.spyOn(component, 'clear');
|
||||||
|
expect(undoButton.title).toMatch('Undo Changes');
|
||||||
|
|
||||||
|
undoButton.click();
|
||||||
|
expect(clearSpy).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should check all permissions when check all box is checked', () => {
|
||||||
|
const checkAllSpy = jest.spyOn(component, 'checkAll');
|
||||||
|
const checkAllButton = debugElement.nativeElement.querySelector('#checkbox-0-00');
|
||||||
|
expect(checkAllButton).toBeTruthy();
|
||||||
|
|
||||||
|
checkAllButton.click();
|
||||||
|
expect(checkAllSpy).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should dispatch UpdateWorkbasketAccessItems action when save button is triggered', () => {
|
||||||
|
component.accessItemsRepresentation._links.self.href = 'https://link.mock';
|
||||||
|
const onSaveSpy = jest.spyOn(component, 'onSave');
|
||||||
|
let actionDispatched = false;
|
||||||
|
actions$.pipe(ofActionDispatched(UpdateWorkbasketAccessItems)).subscribe(() => (actionDispatched = true));
|
||||||
|
component.onSave();
|
||||||
|
expect(onSaveSpy).toHaveBeenCalled();
|
||||||
|
expect(actionDispatched).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should set badge correctly', () => {
|
||||||
|
component.action = ACTION.READ;
|
||||||
|
component.setBadge();
|
||||||
|
expect(component.badgeMessage).toMatch('');
|
||||||
|
|
||||||
|
component.action = ACTION.COPY;
|
||||||
|
component.setBadge();
|
||||||
|
expect(component.badgeMessage).toMatch(`Copying workbasket: ${component.workbasket.key}`);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -78,7 +78,6 @@ export class WorkbasketAccessItemsComponent implements OnInit, OnChanges, OnDest
|
||||||
accessItemsRepresentation$: Observable<WorkbasketAccessItemsRepresentation>;
|
accessItemsRepresentation$: Observable<WorkbasketAccessItemsRepresentation>;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private workbasketService: WorkbasketService,
|
|
||||||
private savingWorkbaskets: SavingWorkbasketService,
|
private savingWorkbaskets: SavingWorkbasketService,
|
||||||
private requestInProgressService: RequestInProgressService,
|
private requestInProgressService: RequestInProgressService,
|
||||||
private formBuilder: FormBuilder,
|
private formBuilder: FormBuilder,
|
||||||
|
|
@ -111,7 +110,7 @@ export class WorkbasketAccessItemsComponent implements OnInit, OnChanges, OnDest
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnChanges(changes: SimpleChanges) {
|
ngOnChanges(changes?: SimpleChanges) {
|
||||||
if (!this.initialized && changes.active && changes.active.currentValue === 'accessItems') {
|
if (!this.initialized && changes.active && changes.active.currentValue === 'accessItems') {
|
||||||
this.init();
|
this.init();
|
||||||
}
|
}
|
||||||
|
|
@ -125,7 +124,7 @@ export class WorkbasketAccessItemsComponent implements OnInit, OnChanges, OnDest
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private init() {
|
init() {
|
||||||
if (!this.workbasket._links.accessItems) {
|
if (!this.workbasket._links.accessItems) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -243,7 +242,7 @@ export class WorkbasketAccessItemsComponent implements OnInit, OnChanges, OnDest
|
||||||
this.accessItemsGroups.controls[row].get('accessName').setValue(accessItem.name);
|
this.accessItemsGroups.controls[row].get('accessName').setValue(accessItem.name);
|
||||||
}
|
}
|
||||||
|
|
||||||
private onSave() {
|
onSave() {
|
||||||
this.requestInProgressService.setRequestInProgress(true);
|
this.requestInProgressService.setRequestInProgress(true);
|
||||||
this.store
|
this.store
|
||||||
.dispatch(
|
.dispatch(
|
||||||
|
|
@ -257,19 +256,19 @@ export class WorkbasketAccessItemsComponent implements OnInit, OnChanges, OnDest
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private setBadge() {
|
setBadge() {
|
||||||
if (this.action === ACTION.COPY) {
|
if (this.action === ACTION.COPY) {
|
||||||
this.badgeMessage = `Copying workbasket: ${this.workbasket.key}`;
|
this.badgeMessage = `Copying workbasket: ${this.workbasket.key}`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private cloneAccessItems(inputaccessItem): Array<WorkbasketAccessItems> {
|
cloneAccessItems(inputaccessItem): Array<WorkbasketAccessItems> {
|
||||||
return this.AccessItemsForm.value.accessItemsGroups.map((accessItems: WorkbasketAccessItems) => ({
|
return this.AccessItemsForm.value.accessItemsGroups.map((accessItems: WorkbasketAccessItems) => ({
|
||||||
...accessItems
|
...accessItems
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
private setWorkbasketIdForCopy(workbasketId: string) {
|
setWorkbasketIdForCopy(workbasketId: string) {
|
||||||
this.accessItemsGroups.value.forEach((element) => {
|
this.accessItemsGroups.value.forEach((element) => {
|
||||||
delete element.accessItemId;
|
delete element.accessItemId;
|
||||||
element.workbasketId = workbasketId;
|
element.workbasketId = workbasketId;
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,7 @@ export interface WorkbasketAccessItems {
|
||||||
permCustom10: boolean;
|
permCustom10: boolean;
|
||||||
permCustom11: boolean;
|
permCustom11: boolean;
|
||||||
permCustom12: boolean;
|
permCustom12: boolean;
|
||||||
_links: Links;
|
_links?: Links;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const customFieldCount: number = 12;
|
export const customFieldCount: number = 12;
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import { Workbasket } from '../../models/workbasket';
|
import { Workbasket } from '../../models/workbasket';
|
||||||
import { ICONTYPES } from '../../models/icon-types';
|
import { ICONTYPES } from '../../models/icon-types';
|
||||||
import { ACTION } from '../../models/action';
|
import { ACTION } from '../../models/action';
|
||||||
|
import { WorkbasketAccessItemsRepresentation } from '../../models/workbasket-access-items-representation';
|
||||||
|
|
||||||
export const classificationStateMock = {
|
export const classificationStateMock = {
|
||||||
selectedClassificationType: 'DOCUMENT',
|
selectedClassificationType: 'DOCUMENT',
|
||||||
|
|
@ -13,14 +14,6 @@ export const classificationStateMock = {
|
||||||
export const engineConfigurationMock = {
|
export const engineConfigurationMock = {
|
||||||
customisation: {
|
customisation: {
|
||||||
EN: {
|
EN: {
|
||||||
classifications: {
|
|
||||||
categories: {
|
|
||||||
EXTERNAL: 'assets/icons/categories/external.svg',
|
|
||||||
MANUAL: 'assets/icons/categories/manual.svg',
|
|
||||||
AUTOMATIC: 'assets/icons/categories/automatic.svg',
|
|
||||||
missing: 'assets/icons/categories/missing-icon.svg'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
workbaskets: {
|
workbaskets: {
|
||||||
information: {
|
information: {
|
||||||
owner: {
|
owner: {
|
||||||
|
|
@ -60,6 +53,32 @@ export const engineConfigurationMock = {
|
||||||
visible: false
|
visible: false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
classifications: {
|
||||||
|
information: {
|
||||||
|
custom1: {
|
||||||
|
field: 'Classification custom 1',
|
||||||
|
visible: true
|
||||||
|
},
|
||||||
|
custom3: {
|
||||||
|
field: '',
|
||||||
|
visible: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
categories: {
|
||||||
|
EXTERNAL: 'assets/icons/categories/external.svg',
|
||||||
|
MANUAL: 'assets/icons/categories/manual.svg',
|
||||||
|
AUTOMATIC: 'assets/icons/categories/automatic.svg',
|
||||||
|
PROCESS: 'assets/icons/categories/process.svg',
|
||||||
|
missing: 'assets/icons/categories/missing-icon.svg'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
tasks: {
|
||||||
|
information: {
|
||||||
|
owner: {
|
||||||
|
lookupField: true
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
@ -107,6 +126,44 @@ export const selectedWorkbasketMock: Workbasket = {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const workbasketAccessItemsMock: WorkbasketAccessItemsRepresentation = {
|
||||||
|
accessItems: [
|
||||||
|
{
|
||||||
|
accessItemId: 'WBI:000000000000000000000000000000000901',
|
||||||
|
workbasketId: 'WBI:000000000000000000000000000000000901',
|
||||||
|
workbasketKey: 'Sort002',
|
||||||
|
accessId: 'user-b-1',
|
||||||
|
accessName: 'Bern, Bernd',
|
||||||
|
permRead: true,
|
||||||
|
permOpen: true,
|
||||||
|
permAppend: true,
|
||||||
|
permTransfer: true,
|
||||||
|
permDistribute: true,
|
||||||
|
permCustom1: true,
|
||||||
|
permCustom2: true,
|
||||||
|
permCustom3: true,
|
||||||
|
permCustom4: true,
|
||||||
|
permCustom5: true,
|
||||||
|
permCustom6: true,
|
||||||
|
permCustom7: true,
|
||||||
|
permCustom8: true,
|
||||||
|
permCustom9: true,
|
||||||
|
permCustom10: true,
|
||||||
|
permCustom11: true,
|
||||||
|
permCustom12: true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
_links: {
|
||||||
|
self: {
|
||||||
|
href:
|
||||||
|
'http://localhost:8080/taskana/api/v1/workbaskets/WBI:000000000000000000000000000000000901/workbasketAccessItems'
|
||||||
|
},
|
||||||
|
workbasket: {
|
||||||
|
href: 'http://localhost:8080/taskana/api/v1/workbaskets/WBI:000000000000000000000000000000000901'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
export const workbasketReadStateMock = {
|
export const workbasketReadStateMock = {
|
||||||
selectedWorkbasket: selectedWorkbasketMock,
|
selectedWorkbasket: selectedWorkbasketMock,
|
||||||
paginatedWorkbasketsSummary: {
|
paginatedWorkbasketsSummary: {
|
||||||
|
|
@ -227,5 +284,6 @@ export const workbasketReadStateMock = {
|
||||||
number: 3
|
number: 3
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
action: ACTION.READ
|
action: ACTION.READ,
|
||||||
|
workbasketAccessItems: workbasketAccessItemsMock
|
||||||
};
|
};
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue