Hide buttons depending on user role

This commit is contained in:
Adina Țeudan 2020-10-27 21:56:41 +02:00 committed by Timo Bejan
parent 019e50c071
commit d23f5264c1
13 changed files with 373 additions and 290 deletions

View File

@ -59,6 +59,7 @@ import { AssignOwnerDialogComponent } from './dialogs/assign-owner-dialog/assign
import { MatDatepickerModule } from '@angular/material/datepicker'; import { MatDatepickerModule } from '@angular/material/datepicker';
import { MatNativeDateModule } from '@angular/material/core'; import { MatNativeDateModule } from '@angular/material/core';
import { MatInputModule } from '@angular/material/input'; import { MatInputModule } from '@angular/material/input';
import { ProjectMemberGuard } from './auth/project-member-guard.service';
export function HttpLoaderFactory(httpClient: HttpClient) { export function HttpLoaderFactory(httpClient: HttpClient) {
return new TranslateHttpLoader(httpClient, '/assets/i18n/', '.json'); return new TranslateHttpLoader(httpClient, '/assets/i18n/', '.json');
@ -131,7 +132,12 @@ export function HttpLoaderFactory(httpClient: HttpClient) {
component: ProjectOverviewScreenComponent, component: ProjectOverviewScreenComponent,
canActivate: [CompositeRouteGuard], canActivate: [CompositeRouteGuard],
data: { data: {
routeGuards: [AuthGuard, RedRoleGuard, AppStateGuard] routeGuards: [
AuthGuard,
RedRoleGuard,
ProjectMemberGuard,
AppStateGuard
]
} }
}, },
{ {
@ -139,7 +145,12 @@ export function HttpLoaderFactory(httpClient: HttpClient) {
component: FilePreviewScreenComponent, component: FilePreviewScreenComponent,
canActivate: [CompositeRouteGuard], canActivate: [CompositeRouteGuard],
data: { data: {
routeGuards: [AuthGuard, RedRoleGuard, AppStateGuard] routeGuards: [
AuthGuard,
RedRoleGuard,
ProjectMemberGuard,
AppStateGuard
]
} }
} }
] ]

View File

@ -0,0 +1,53 @@
import { Injectable } from '@angular/core';
import { ActivatedRouteSnapshot, CanActivate, Router, RouterStateSnapshot } from '@angular/router';
import { UserService } from '../user/user.service';
import { AppLoadStateService } from '../utils/app-load-state.service';
import { Observable } from 'rxjs';
import { AppStateService } from '../state/app-state.service';
import { NotificationService, NotificationType } from '../notification/notification.service';
import { TranslateService } from '@ngx-translate/core';
@Injectable({
providedIn: 'root'
})
export class ProjectMemberGuard implements CanActivate {
constructor(
private readonly _router: Router,
private readonly _appLoadStateService: AppLoadStateService,
private readonly _appStateService: AppStateService,
private readonly _userService: UserService,
private readonly _notificationService: NotificationService,
private readonly _translateService: TranslateService
) {}
canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean> {
return new Observable((obs) => {
if (this._userService.isManager(this._userService.user)) {
obs.next(true);
obs.complete();
return;
}
const projectId = route.params.projectId;
this._appStateService.loadAllProjects().then(() => {
const isProjectMember = this._appStateService.allProjects
.find((pw) => pw.project.projectId === projectId)
?.project.memberIds.includes(this._userService.user.id);
if (!isProjectMember) {
this._router.navigate(['ui', 'projects']);
this._appLoadStateService.pushLoadingEvent(false);
this._notificationService.showToastNotification(
this._translateService.instant('project-member-guard.access-denied.label'),
null,
NotificationType.ERROR
);
obs.next(false);
obs.complete();
} else {
obs.next(true);
obs.complete();
}
});
});
}
}

View File

@ -231,7 +231,7 @@ export class DialogService {
return ref; return ref;
} }
public openAssignFileOwnerDialog( public openAssignFileReviewerDialog(
$event: MouseEvent, $event: MouseEvent,
file: FileStatus, file: FileStatus,
cb?: Function cb?: Function

View File

@ -12,13 +12,21 @@
<div class="flex-1 actions-container"> <div class="flex-1 actions-container">
<div class="actions-row"> <div class="actions-row">
<button mat-icon-button (click)="openDeleteFileDialog($event)"> <button
mat-icon-button
(click)="openDeleteFileDialog($event)"
*ngIf="userService.isManager(this.user) || appStateService.isActiveProjectOwner"
>
<mat-icon svgIcon="red:trash"></mat-icon> <mat-icon svgIcon="red:trash"></mat-icon>
</button> </button>
<button mat-icon-button> <button mat-icon-button>
<mat-icon svgIcon="red:report"></mat-icon> <mat-icon svgIcon="red:report"></mat-icon>
</button> </button>
<button mat-icon-button (click)="openAssignFileOwnerDialog($event)"> <button
mat-icon-button
(click)="openAssignFileReviewerDialog($event)"
*ngIf="appStateService.isActiveProjectMember"
>
<mat-icon svgIcon="red:assign"></mat-icon> <mat-icon svgIcon="red:assign"></mat-icon>
</button> </button>
<button mat-icon-button (click)="reanalyseFile($event)"> <button mat-icon-button (click)="reanalyseFile($event)">
@ -30,6 +38,7 @@
</div> </div>
<button <button
*ngIf="userService.isManager(user) || appStateService.isActiveProjectOwner"
color="primary" color="primary"
mat-flat-button mat-flat-button
class="arrow-button" class="arrow-button"

View File

@ -1,33 +1,20 @@
import { import {ChangeDetectorRef, Component, ElementRef, HostListener, NgZone, OnInit, ViewChild} from '@angular/core';
ChangeDetectorRef, import {ActivatedRoute, Router} from '@angular/router';
Component, import {ManualRedactionEntry, ReanalysisControllerService} from '@redaction/red-ui-http';
ElementRef, import {AppStateService} from '../../../state/app-state.service';
HostListener, import {Annotations, WebViewerInstance} from '@pdftron/webviewer';
NgZone, import {PdfViewerComponent} from '../pdf-viewer/pdf-viewer.component';
OnInit, import {AnnotationUtils} from '../../../utils/annotation-utils';
ViewChild, import {UserService} from '../../../user/user.service';
EventEmitter import {debounce} from '../../../utils/debounce';
} from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router';
import {
DictionaryControllerService,
ManualRedactionEntry,
ReanalysisControllerService
} from '@redaction/red-ui-http';
import { AppStateService } from '../../../state/app-state.service';
import { Annotations, WebViewerInstance } from '@pdftron/webviewer';
import { PdfViewerComponent } from '../pdf-viewer/pdf-viewer.component';
import { AnnotationUtils } from '../../../utils/annotation-utils';
import { UserService } from '../../../user/user.service';
import { debounce } from '../../../utils/debounce';
import scrollIntoView from 'scroll-into-view-if-needed'; import scrollIntoView from 'scroll-into-view-if-needed';
import { AnnotationFilters } from '../../../utils/types'; import {AnnotationFilters} from '../../../utils/types';
import { FiltersService } from '../service/filters.service'; import {FiltersService} from '../service/filters.service';
import { FileDownloadService } from '../service/file-download.service'; import {FileDownloadService} from '../service/file-download.service';
import { saveAs } from 'file-saver'; import {saveAs} from 'file-saver';
import { FileType } from '../model/file-type'; import {FileType} from '../model/file-type';
import { DialogService } from '../../../dialogs/dialog.service'; import {DialogService} from '../../../dialogs/dialog.service';
import { MatDialogRef, MatDialogState } from '@angular/material/dialog'; import {MatDialogRef, MatDialogState} from '@angular/material/dialog';
@Component({ @Component({
selector: 'redaction-file-preview-screen', selector: 'redaction-file-preview-screen',
@ -57,12 +44,11 @@ export class FilePreviewScreenComponent implements OnInit {
constructor( constructor(
public readonly appStateService: AppStateService, public readonly appStateService: AppStateService,
public readonly userService: UserService,
private readonly _changeDetectorRef: ChangeDetectorRef, private readonly _changeDetectorRef: ChangeDetectorRef,
private readonly _activatedRoute: ActivatedRoute, private readonly _activatedRoute: ActivatedRoute,
private readonly _dialogService: DialogService, private readonly _dialogService: DialogService,
private readonly _router: Router, private readonly _router: Router,
private readonly _dictionaryControllerService: DictionaryControllerService,
private readonly _userService: UserService,
private readonly _fileDownloadService: FileDownloadService, private readonly _fileDownloadService: FileDownloadService,
private readonly _reanalysisControllerService: ReanalysisControllerService, private readonly _reanalysisControllerService: ReanalysisControllerService,
private readonly _filtersService: FiltersService, private readonly _filtersService: FiltersService,
@ -76,7 +62,7 @@ export class FilePreviewScreenComponent implements OnInit {
} }
public get user() { public get user() {
return this._userService.user; return this.userService.user;
} }
public filterKeys(key?: string) { public filterKeys(key?: string) {
@ -145,9 +131,9 @@ export class FilePreviewScreenComponent implements OnInit {
); );
} }
public openAssignFileOwnerDialog($event: MouseEvent) { public openAssignFileReviewerDialog($event: MouseEvent) {
const file = this.appStateService.getFileById(this.projectId, this.fileId); const file = this.appStateService.getFileById(this.projectId, this.fileId);
this._dialogRef = this._dialogService.openAssignFileOwnerDialog($event, file); this._dialogRef = this._dialogService.openAssignFileReviewerDialog($event, file);
} }
public get activeViewer() { public get activeViewer() {

View File

@ -22,11 +22,12 @@
</div> </div>
<button <button
(click)="openAddProjectDialog()" (click)="openAddProjectDialog()"
*ngIf="userService.isManager(user)"
class="add-project-btn"
color="primary" color="primary"
mat-flat-button mat-flat-button
class="add-project-btn"
> >
<mat-icon svgIcon="red:plus"> </mat-icon> <mat-icon svgIcon="red:plus"></mat-icon>
<span translate="project-listing.add-new.label"></span> <span translate="project-listing.add-new.label"></span>
</button> </button>
</div> </div>
@ -73,18 +74,19 @@
</div> </div>
<div <div
class="no-data"
*ngIf="appStateService.allProjects?.length === 0" *ngIf="appStateService.allProjects?.length === 0"
class="no-data"
translate="project-listing.no-projects.label" translate="project-listing.no-projects.label"
></div> ></div>
<div <div
class="table-item pointer"
[routerLink]="'/ui/projects/' + pw.project.projectId"
*ngFor=" *ngFor="
let pw of appStateService.allProjects let pw of appStateService.allProjects
| sortBy: sortingOption.order:sortingOption.column | sortBy: sortingOption.order:sortingOption.column
" "
[routerLink]="[canOpenProject(pw) ? '/ui/projects/' + pw.project.projectId : []]"
class="table-item"
[class.pointer]="canOpenProject(pw)"
> >
<div> <div>
<div class="table-item-title table-item-title--large"> <div class="table-item-title table-item-title--large">
@ -122,26 +124,28 @@
<div class="action-buttons"> <div class="action-buttons">
<button <button
mat-icon-button
color="accent"
(click)="openDeleteProjectDialog($event, pw.project)" (click)="openDeleteProjectDialog($event, pw.project)"
[matTooltip]="'project-listing.delete.action.label' | translate" [matTooltip]="'project-listing.delete.action.label' | translate"
*ngIf="userService.isManager(user)"
color="accent"
mat-icon-button
> >
<mat-icon svgIcon="red:trash"></mat-icon> <mat-icon svgIcon="red:trash"></mat-icon>
</button> </button>
<button <button
mat-icon-button mat-icon-button
color="accent"
(click)="downloadRedactionReport($event, pw.project)" (click)="downloadRedactionReport($event, pw.project)"
[matTooltip]="'project-listing.report.action.label' | translate" [matTooltip]="'project-listing.report.action.label' | translate"
color="accent"
> >
<mat-icon svgIcon="red:report"></mat-icon> <mat-icon svgIcon="red:report"></mat-icon>
</button> </button>
<button <button
color="accent"
(click)="openAssignProjectOwnerDialog($event, pw.project)" (click)="openAssignProjectOwnerDialog($event, pw.project)"
mat-icon-button
[matTooltip]="'project-listing.assign.action.label' | translate" [matTooltip]="'project-listing.assign.action.label' | translate"
*ngIf="userService.isManager(user)"
color="accent"
mat-icon-button
> >
<mat-icon svgIcon="red:assign"></mat-icon> <mat-icon svgIcon="red:assign"></mat-icon>
</button> </button>

View File

@ -1,12 +1,11 @@
import { Component, OnInit } from '@angular/core'; import {Component, OnInit} from '@angular/core';
import { FileUploadControllerService, Project } from '@redaction/red-ui-http'; import {Project} from '@redaction/red-ui-http';
import { AppStateService, ProjectWrapper } from '../../state/app-state.service'; import {AppStateService, ProjectWrapper} from '../../state/app-state.service';
import { UserService } from '../../user/user.service'; import {UserService} from '../../user/user.service';
import { DoughnutChartConfig } from '../../components/simple-doughnut-chart/simple-doughnut-chart.component'; import {DoughnutChartConfig} from '../../components/simple-doughnut-chart/simple-doughnut-chart.component';
import { SortingOption } from '../../utils/types'; import {SortingOption} from '../../utils/types';
import { groupBy } from '../../utils/functions'; import {groupBy} from '../../utils/functions';
import { DialogService } from '../../dialogs/dialog.service'; import {DialogService} from '../../dialogs/dialog.service';
import { download } from '../../utils/file-download-utils';
@Component({ @Component({
selector: 'redaction-project-listing-screen', selector: 'redaction-project-listing-screen',
@ -28,8 +27,7 @@ export class ProjectListingScreenComponent implements OnInit {
constructor( constructor(
public readonly appStateService: AppStateService, public readonly appStateService: AppStateService,
private readonly _fileUploadControllerService: FileUploadControllerService, public readonly userService: UserService,
private readonly _userService: UserService,
private readonly _dialogService: DialogService private readonly _dialogService: DialogService
) {} ) {}
@ -54,7 +52,7 @@ export class ProjectListingScreenComponent implements OnInit {
} }
public get user() { public get user() {
return this._userService.user; return this.userService.user;
} }
public get totalPages() { public get totalPages() {
@ -84,6 +82,10 @@ export class ProjectListingScreenComponent implements OnInit {
return 1; return 1;
} }
public canOpenProject(pw: ProjectWrapper): boolean {
return this.userService.isManager(this.user) || pw.project.memberIds.includes(this.user.id);
}
public openAddProjectDialog(): void { public openAddProjectDialog(): void {
this._dialogService.openAddProjectDialog(() => { this._dialogService.openAddProjectDialog(() => {
this._calculateData(); this._calculateData();

View File

@ -194,6 +194,9 @@
<button <button
(click)="openDeleteFileDialog($event, fileStatus)" (click)="openDeleteFileDialog($event, fileStatus)"
color="accent" color="accent"
*ngIf="
userService.isManager(user) || appStateService.isActiveProjectOwner
"
mat-icon-button mat-icon-button
[matTooltip]="'project-overview.delete.action.label' | translate" [matTooltip]="'project-overview.delete.action.label' | translate"
> >
@ -208,8 +211,9 @@
<mat-icon svgIcon="red:report"></mat-icon> <mat-icon svgIcon="red:report"></mat-icon>
</button> </button>
<button <button
(click)="openAssignFileOwnerDialog($event, fileStatus)" (click)="openAssignFileReviewerDialog($event, fileStatus)"
color="accent" color="accent"
*ngIf="appStateService.isActiveProjectMember"
mat-icon-button mat-icon-button
[matTooltip]="'project-overview.assign.action.label' | translate" [matTooltip]="'project-overview.assign.action.label' | translate"
> >
@ -231,10 +235,18 @@
<div class="project-details-container right-fixed-container"> <div class="project-details-container right-fixed-container">
<div class="actions-row"> <div class="actions-row">
<button mat-icon-button (click)="openDeleteProjectDialog($event)"> <button
mat-icon-button
*ngIf="userService.isManager(user)"
(click)="openDeleteProjectDialog($event)"
>
<mat-icon svgIcon="red:trash"></mat-icon> <mat-icon svgIcon="red:trash"></mat-icon>
</button> </button>
<button mat-icon-button (click)="openEditProjectDialog($event)"> <button
mat-icon-button
*ngIf="userService.isManager(user)"
(click)="openEditProjectDialog($event)"
>
<mat-icon svgIcon="red:edit"></mat-icon> <mat-icon svgIcon="red:edit"></mat-icon>
</button> </button>
<button mat-icon-button (click)="downloadRedactionReport($event)"> <button mat-icon-button (click)="downloadRedactionReport($event)">
@ -292,7 +304,11 @@
<div class="member" *ngIf="overflowCount"> <div class="member" *ngIf="overflowCount">
<div class="oval large white-dark">+{{ overflowCount }}</div> <div class="oval large white-dark">+{{ overflowCount }}</div>
</div> </div>
<div class="member pointer" (click)="openAssignProjectMembersDialog()"> <div
class="member pointer"
(click)="openAssignProjectMembersDialog()"
*ngIf="userService.isManager(user)"
>
<div class="oval red-white large">+</div> <div class="oval red-white large">+</div>
</div> </div>
</div> </div>

View File

@ -1,23 +1,22 @@
import { Component, OnDestroy, OnInit } from '@angular/core'; import {Component, OnDestroy, OnInit} from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router'; import {ActivatedRoute, Router} from '@angular/router';
import { import {
FileStatus, FileStatus,
FileUploadControllerService, FileUploadControllerService,
ReanalysisControllerService, ReanalysisControllerService,
StatusControllerService StatusControllerService
} from '@redaction/red-ui-http'; } from '@redaction/red-ui-http';
import { NotificationService } from '../../notification/notification.service'; import {NotificationService} from '../../notification/notification.service';
import { AppStateService } from '../../state/app-state.service'; import {AppStateService} from '../../state/app-state.service';
import { FileDropOverlayService } from '../../upload/file-drop/service/file-drop-overlay.service'; import {FileDropOverlayService} from '../../upload/file-drop/service/file-drop-overlay.service';
import { FileUploadModel } from '../../upload/model/file-upload.model'; import {FileUploadModel} from '../../upload/model/file-upload.model';
import { FileUploadService } from '../../upload/file-upload.service'; import {FileUploadService} from '../../upload/file-upload.service';
import { UploadStatusOverlayService } from '../../upload/upload-status-dialog/service/upload-status-overlay.service'; import {UploadStatusOverlayService} from '../../upload/upload-status-dialog/service/upload-status-overlay.service';
import { UserService } from '../../user/user.service'; import {UserService} from '../../user/user.service';
import { SortingOption } from '../../utils/types'; import {SortingOption} from '../../utils/types';
import { DoughnutChartConfig } from '../../components/simple-doughnut-chart/simple-doughnut-chart.component'; import {DoughnutChartConfig} from '../../components/simple-doughnut-chart/simple-doughnut-chart.component';
import { groupBy } from '../../utils/functions'; import {groupBy} from '../../utils/functions';
import { DialogService } from '../../dialogs/dialog.service'; import {DialogService} from '../../dialogs/dialog.service';
import { download } from '../../utils/file-download-utils';
@Component({ @Component({
selector: 'redaction-project-overview-screen', selector: 'redaction-project-overview-screen',
@ -28,8 +27,8 @@ export class ProjectOverviewScreenComponent implements OnInit, OnDestroy {
private _selectedFileIds: string[] = []; private _selectedFileIds: string[] = [];
public sortingOptions: SortingOption[] = [ public sortingOptions: SortingOption[] = [
{ label: 'project-overview.sorting.recent.label', order: 'desc', column: 'lastUpdated' }, {label: 'project-overview.sorting.recent.label', order: 'desc', column: 'lastUpdated'},
{ label: 'project-overview.sorting.oldest.label', order: 'asc', column: 'lastUpdated' }, {label: 'project-overview.sorting.oldest.label', order: 'asc', column: 'lastUpdated'},
{ {
label: 'project-overview.sorting.alphabetically.label', label: 'project-overview.sorting.alphabetically.label',
order: 'asc', order: 'asc',
@ -51,6 +50,7 @@ export class ProjectOverviewScreenComponent implements OnInit, OnDestroy {
constructor( constructor(
public readonly appStateService: AppStateService, public readonly appStateService: AppStateService,
public readonly userService: UserService,
private readonly _activatedRoute: ActivatedRoute, private readonly _activatedRoute: ActivatedRoute,
private readonly _fileUploadControllerService: FileUploadControllerService, private readonly _fileUploadControllerService: FileUploadControllerService,
private readonly _statusControllerService: StatusControllerService, private readonly _statusControllerService: StatusControllerService,
@ -60,7 +60,6 @@ export class ProjectOverviewScreenComponent implements OnInit, OnDestroy {
private readonly _uploadStatusOverlayService: UploadStatusOverlayService, private readonly _uploadStatusOverlayService: UploadStatusOverlayService,
private readonly _reanalysisControllerService: ReanalysisControllerService, private readonly _reanalysisControllerService: ReanalysisControllerService,
private readonly _router: Router, private readonly _router: Router,
private readonly _userService: UserService,
private readonly _fileDropOverlayService: FileDropOverlayService private readonly _fileDropOverlayService: FileDropOverlayService
) { ) {
this._activatedRoute.params.subscribe((params) => { this._activatedRoute.params.subscribe((params) => {
@ -86,7 +85,7 @@ export class ProjectOverviewScreenComponent implements OnInit, OnDestroy {
} }
public get user() { public get user() {
return this._userService.user; return this.userService.user;
} }
public get displayMembers() { public get displayMembers() {
@ -99,8 +98,8 @@ export class ProjectOverviewScreenComponent implements OnInit, OnDestroy {
: 0; : 0;
} }
private _getFileStatus() { private _reloadProjects() {
this.appStateService.reloadActiveProjectFiles().then(() => { this.appStateService.loadAllProjects().then(() => {
this._calculateChartConfig(); this._calculateChartConfig();
}); });
} }
@ -109,7 +108,7 @@ export class ProjectOverviewScreenComponent implements OnInit, OnDestroy {
const groups = groupBy(this.appStateService.activeProject.files, 'status'); const groups = groupBy(this.appStateService.activeProject.files, 'status');
this.documentsChartData = []; this.documentsChartData = [];
for (const key of Object.keys(groups)) { for (const key of Object.keys(groups)) {
this.documentsChartData.push({ value: groups[key].length, color: key, label: key }); this.documentsChartData.push({value: groups[key].length, color: key, label: key});
} }
} }
@ -184,13 +183,13 @@ export class ProjectOverviewScreenComponent implements OnInit, OnDestroy {
public openAssignProjectMembersDialog() { public openAssignProjectMembersDialog() {
this._dialogService.openAssignProjectMembersAndOwnerDialog(null, this.activeProject, () => { this._dialogService.openAssignProjectMembersAndOwnerDialog(null, this.activeProject, () => {
this._getFileStatus(); this._reloadProjects();
}); });
} }
public openAssignFileOwnerDialog($event: MouseEvent, file: FileStatus) { public openAssignFileReviewerDialog($event: MouseEvent, file: FileStatus) {
this._dialogService.openAssignFileOwnerDialog($event, file, () => { this._dialogService.openAssignFileReviewerDialog($event, file, () => {
this._getFileStatus(); this._reloadProjects();
}); });
} }
@ -199,7 +198,7 @@ export class ProjectOverviewScreenComponent implements OnInit, OnDestroy {
this._reanalysisControllerService this._reanalysisControllerService
.reanalyzeFile(this.appStateService.activeProject.project.projectId, fileStatus.fileId) .reanalyzeFile(this.appStateService.activeProject.project.projectId, fileStatus.fileId)
.subscribe(() => { .subscribe(() => {
this._getFileStatus(); this._reloadProjects();
}); });
} }

View File

@ -90,9 +90,7 @@ export class AppStateService {
} }
get isActiveProjectMember() { get isActiveProjectMember() {
return ( return this._appState.activeProject?.project?.memberIds?.includes(this._userService.userId);
this._appState.activeProject?.project?.memberIds?.indexOf(this._userService.userId) >= 0
);
} }
get dictionaryData() { get dictionaryData() {

View File

@ -99,11 +99,11 @@ export class UserService {
: undefined; : undefined;
} }
isManager(user: User) { isManager(user: User): boolean {
return user.roles.indexOf('RED_MANAGER') >= 0; return user.roles.indexOf('RED_MANAGER') >= 0;
} }
isUser(user: User) { isUser(user: User): boolean {
return user.roles.indexOf('RED_USER') >= 0; return user.roles.indexOf('RED_USER') >= 0;
} }

View File

@ -527,6 +527,11 @@
} }
} }
}, },
"project-member-guard": {
"access-denied": {
"label": "You are not allowed to access that page."
}
},
"unassigned": "Unassigned", "unassigned": "Unassigned",
"under-review": "Under review", "under-review": "Under review",
"under-approval": "Under approval", "under-approval": "Under approval",

0
package-lock.json generated
View File