Assign team members dialog

This commit is contained in:
Adina Țeudan 2020-12-04 02:01:51 +02:00
parent 31a1b8eaf3
commit fc7a5b4373
23 changed files with 317 additions and 120 deletions

View File

@ -23,7 +23,7 @@ import { AddEditProjectDialogComponent } from './dialogs/add-edit-project-dialog
import { MatDialogModule } from '@angular/material/dialog';
import { MatSnackBarModule } from '@angular/material/snack-bar';
import { MatTooltipModule } from '@angular/material/tooltip';
import { ConfirmationDialogComponent } from './common/confirmation-dialog/confirmation-dialog.component';
import { ConfirmationDialogComponent } from './dialogs/confirmation-dialog/confirmation-dialog.component';
import { FilePreviewScreenComponent } from './screens/file/file-preview-screen/file-preview-screen.component';
import { PdfViewerComponent } from './screens/file/pdf-viewer/pdf-viewer.component';
import { MatTabsModule } from '@angular/material/tabs';

View File

@ -1,15 +1,25 @@
<div class="flex members-container">
<div *ngFor="let userId of displayedMembers" class="member">
<div class="flex container" #container>
<div
*ngFor="let userId of displayedMembers"
class="member"
[class.large-spacing]="largeSpacing"
[class.can-remove]="canRemove"
(click)="canRemove && remove.emit(userId)"
>
<redaction-initials-avatar [userId]="userId" size="large" color="gray"></redaction-initials-avatar>
<div class="remove">
<mat-icon svgIcon="red:close"></mat-icon>
</div>
</div>
<div *ngIf="overflowCount && !expandedTeam" class="member pointer">
<div *ngIf="overflowCount && !expandedTeam" class="member pointer" [class.large-spacing]="largeSpacing">
<div class="oval large white-dark" (click)="toggleExpandedTeam()">+{{ overflowCount }}</div>
</div>
<redaction-circle-button
class="member"
[class.large-spacing]="largeSpacing"
(action)="openAssignProjectMembersDialog.emit()"
icon="red:plus"
*ngIf="permissionsService.isManager()"
*ngIf="permissionsService.isManager() && canAdd"
type="primary"
[small]="true"
tooltip="project-details.assign-members"

View File

@ -1,12 +1,48 @@
@import '../../../assets/styles/red-variables';
.members-container {
.container {
flex-wrap: wrap;
margin-top: 4px;
.member {
position: relative;
margin-top: 4px;
margin-right: 2px;
&.large-spacing {
margin-top: 8px;
margin-right: 12px;
}
.remove {
display: none;
}
&.can-remove {
cursor: pointer;
}
&.can-remove:hover {
.remove {
width: 16px;
height: 16px;
border-radius: 50%;
background-color: $accent;
color: $white;
position: absolute;
right: -8px;
bottom: -2px;
line-height: 6px;
display: flex;
justify-content: center;
align-items: center;
mat-icon {
width: 6px;
height: 7px;
}
}
}
}
}

View File

@ -1,4 +1,4 @@
import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
import { Component, ElementRef, EventEmitter, Input, OnInit, Output, ViewChild } from '@angular/core';
import { PermissionsService } from '../../common/service/permissions.service';
@Component({
@ -8,7 +8,13 @@ import { PermissionsService } from '../../common/service/permissions.service';
})
export class TeamMembersComponent implements OnInit {
@Input() public memberIds: string[];
@Input() public canAdd = true;
@Input() public largeSpacing = false;
@Input() public canRemove = false;
@Output() public openAssignProjectMembersDialog = new EventEmitter();
@Output() public remove = new EventEmitter<string>();
@ViewChild('container', { static: true }) container: ElementRef;
public expandedTeam = false;
@ -16,8 +22,15 @@ export class TeamMembersComponent implements OnInit {
ngOnInit(): void {}
public get maxTeamMembersBeforeExpand(): number {
const width = this.container.nativeElement.offsetWidth;
// 32px element width + margin right (2px or 12px)
const elementWidth = this.largeSpacing ? 46 : 34;
return Math.floor(width / elementWidth) - (this.canAdd ? 1 : 0);
}
public get displayedMembers(): string[] {
return this.expandedTeam || !this.overflowCount ? this.memberIds : this.memberIds.slice(0, 7);
return this.expandedTeam || !this.overflowCount ? this.memberIds : this.memberIds.slice(0, this.maxTeamMembersBeforeExpand - 1);
}
public toggleExpandedTeam() {
@ -25,6 +38,6 @@ export class TeamMembersComponent implements OnInit {
}
public get overflowCount() {
return this.memberIds.length > 8 ? this.memberIds.length - 7 : 0;
return this.memberIds.length > this.maxTeamMembersBeforeExpand ? this.memberIds.length - (this.maxTeamMembersBeforeExpand - 1) : 0;
}
}

View File

@ -44,7 +44,5 @@
</div>
</form>
<button (click)="dialogRef.close()" class="dialog-close" mat-icon-button>
<mat-icon svgIcon="red:close"></mat-icon>
</button>
<redaction-circle-button icon="red:close" mat-dialog-close class="dialog-close"></redaction-circle-button>
</section>

View File

@ -1,48 +1,57 @@
<section class="dialog">
<div
[translate]="'assign-' + data.type + '-owner.dialog.title'"
class="dialog-header heading-l"
></div>
<div [translate]="'assign-' + data.type + '-owner.dialog.title'" class="dialog-header heading-l"></div>
<form (submit)="saveUsers()" [formGroup]="usersForm">
<div class="dialog-content">
<div class="red-input-group">
<mat-form-field floatLabel="always">
<mat-label>{{
'assign-' + data.type + '-owner.dialog.single-user' | translate
}}</mat-label>
<mat-label>{{ 'assign-' + data.type + '-owner.dialog.single-user' | translate }}</mat-label>
<mat-select formControlName="singleUser">
<mat-option
*ngFor="let userId of singleUsersSelectOptions"
[value]="userId"
>
{{ userService.getNameForId(userId) }}
</mat-option>
</mat-select>
</mat-form-field>
</div>
<div class="red-input-group">
<mat-form-field *ngIf="data.type === 'project'" floatLabel="always">
<mat-label>{{
'assign-' + data.type + '-owner.dialog.multi-user' | translate
}}</mat-label>
<mat-select formControlName="userList" multiple="true">
<mat-option *ngFor="let userId of multiUsersSelectOptions" [value]="userId">
<mat-option *ngFor="let userId of singleUsersSelectOptions" [value]="userId">
{{ userService.getNameForId(userId) }}
</mat-option>
</mat-select>
</mat-form-field>
</div>
<ng-container *ngIf="data.type === 'project'">
<div class="all-caps-label" [translate]="'assign-' + data.type + '-owner.dialog.members'"></div>
<redaction-team-members
[memberIds]="selectedUserList"
[canAdd]="false"
[largeSpacing]="true"
[canRemove]="true"
(remove)="toggleSelected($event)"
></redaction-team-members>
<form [formGroup]="searchForm">
<div class="red-input-group search-container">
<input
[placeholder]="'assign-' + data.type + '-owner.dialog.search' | translate"
formControlName="query"
name="query"
type="text"
class="with-icon mt-0"
/>
<mat-icon class="icon-right" svgIcon="red:search"></mat-icon>
</div>
</form>
<div class="members-list">
<div *ngFor="let userId of multiUsersSelectOptions" [class.selected]="isMemberSelected(userId)" (click)="toggleSelected(userId)">
<redaction-initials-avatar [userId]="userId" [withName]="true" size="large"></redaction-initials-avatar>
<mat-icon svgIcon="red:check-alt" class="add"></mat-icon>
<mat-icon svgIcon="red:close" class="remove"></mat-icon>
</div>
</div>
</ng-container>
</div>
<div class="dialog-actions">
<button color="primary" mat-flat-button type="submit" [disabled]="!usersForm.valid">
<button color="primary" mat-flat-button type="submit" [disabled]="!usersForm.valid || !changed" class="red-button">
{{ 'assign-' + data.type + '-owner.dialog.save' | translate }}
</button>
<div class="all-caps-label pointer" [translate]="'assign-' + data.type + '-owner.dialog.cancel'" mat-dialog-close></div>
</div>
</form>
<button (click)="dialogRef.close()" class="dialog-close" mat-icon-button>
<mat-icon svgIcon="red:close"></mat-icon>
</button>
<redaction-circle-button icon="red:close" mat-dialog-close class="dialog-close"></redaction-circle-button>
</section>

View File

@ -1,3 +1,67 @@
.red-input-group {
max-width: 200px;
@import '../../../assets/styles/red-mixins';
.search-container {
width: 560px;
margin-top: 16px;
}
.members-list {
max-height: 220px;
height: 220px;
margin-top: 16px;
overflow-y: hidden;
width: 587px;
&:hover {
overflow-y: auto;
@include scroll-bar;
}
> div {
margin-bottom: 2px;
padding: 3px 5px;
border-radius: 4px;
cursor: pointer;
position: relative;
transition: background-color ease-in-out 0.1s;
width: 560px;
box-sizing: border-box;
mat-icon {
display: none;
position: absolute;
right: 13px;
top: 12px;
width: 14px;
height: 14px;
}
&.selected {
background-color: $green-2;
mat-icon.add {
display: initial;
}
}
&:hover {
background-color: $grey-2;
&.selected {
mat-icon.remove {
display: initial;
}
mat-icon.add {
display: none;
}
}
&:not(.selected) {
mat-icon.add {
display: initial;
}
}
}
}
}

View File

@ -1,15 +1,16 @@
import { Component, Inject } from '@angular/core';
import { Project, ProjectControllerService, StatusControllerService } from '@redaction/red-ui-http';
import { ProjectControllerService, StatusControllerService } from '@redaction/red-ui-http';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { AppStateService } from '../../state/app-state.service';
import { UserService } from '../../user/user.service';
import { NotificationService, NotificationType } from '../../notification/notification.service';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { FileStatusWrapper } from '../../screens/file/model/file-status.wrapper';
import { ProjectWrapper } from '../../state/model/project.wrapper';
class DialogData {
type: 'file' | 'project';
project?: Project;
project?: ProjectWrapper;
files?: FileStatusWrapper[];
}
@ -19,7 +20,8 @@ class DialogData {
styleUrls: ['./assign-owner-dialog.component.scss']
})
export class AssignOwnerDialogComponent {
usersForm: FormGroup;
public usersForm: FormGroup;
public searchForm: FormGroup;
constructor(
public readonly userService: UserService,
@ -39,7 +41,10 @@ export class AssignOwnerDialogComponent {
const project = this.data.project;
this.usersForm = this._formBuilder.group({
singleUser: [project?.ownerId, Validators.required],
userList: [project?.memberIds]
userList: [[...project?.memberIds]]
});
this.searchForm = this._formBuilder.group({
query: ['']
});
}
@ -57,22 +62,30 @@ export class AssignOwnerDialogComponent {
}
}
public get selectedSingleUser(): string {
return this.usersForm.get('singleUser').value;
}
public get selectedUserList(): string[] {
return this.usersForm.get('userList').value;
}
async saveUsers() {
try {
if (this.data.type === 'project') {
const ownerId = this.usersForm.get('singleUser').value;
const memberIds = this.usersForm.get('userList').value;
const project = Object.assign({}, this.data.project);
project.memberIds = memberIds;
project.ownerId = ownerId;
await this._appStateService.addOrUpdateProject(project);
const ownerId = this.selectedSingleUser;
const memberIds = this.selectedUserList;
const pw = Object.assign({}, this.data.project);
pw.project.memberIds = memberIds;
pw.project.ownerId = ownerId;
await this._appStateService.addOrUpdateProject(pw.project);
this._notificationService.showToastNotification(
'Successfully assigned ' + this.userService.getNameForId(ownerId) + ' to project: ' + project.projectName
'Successfully assigned ' + this.userService.getNameForId(ownerId) + ' to project: ' + pw.project.projectName
);
}
if (this.data.type === 'file') {
const reviewerId = this.usersForm.get('singleUser').value;
const reviewerId = this.selectedSingleUser;
const promises = this.data.files.map((file) =>
this._statusControllerService.assignProjectOwner(this._appStateService.activeProjectId, file.fileId, reviewerId).toPromise()
@ -95,10 +108,57 @@ export class AssignOwnerDialogComponent {
}
get singleUsersSelectOptions() {
return this.data.type === 'file' ? this._appStateService.activeProject.project.memberIds : this.userService.managerUsers.map((m) => m.userId);
return this.data.type === 'file' ? this._appStateService.activeProject.memberIds : this.userService.managerUsers.map((m) => m.userId);
}
get multiUsersSelectOptions() {
return this.userService.eligibleUsers.map((m) => m.userId);
const searchQuery = this.searchForm.get('query').value;
return this.userService.eligibleUsers
.filter((user) => this.userService.getNameForId(user.userId).toLowerCase().includes(searchQuery.toLowerCase()))
.map((user) => user.userId);
}
public isMemberSelected(userId: string): boolean {
return this.selectedUserList.indexOf(userId) !== -1;
}
public toggleSelected(userId: string) {
if (this.isMemberSelected(userId)) {
const idx = this.selectedUserList.indexOf(userId);
this.selectedUserList.splice(idx, 1);
} else {
this.selectedUserList.push(userId);
}
}
public get changed(): boolean {
if (this.data.type === 'project') {
if (this.data.project.ownerId !== this.selectedSingleUser) {
return true;
}
const initial = this.data.project.memberIds.sort();
const current = this.selectedUserList.sort();
if (initial.length !== current.length) {
return true;
}
for (let idx = 0; idx < initial.length; ++idx) {
if (initial[idx] !== current[idx]) {
return true;
}
}
} else if (this.data.type === 'file') {
const reviewerId = this.selectedSingleUser;
for (const file of this.data.files) {
if (file.currentReviewer !== reviewerId) {
return true;
}
}
}
return false;
}
}

View File

@ -6,15 +6,13 @@
</div>
<div class="dialog-actions">
<button (click)="confirm()" color="primary" mat-flat-button>
<button (click)="confirm()" color="primary" mat-flat-button class="red-button">
{{ confirmationDialogInput.confirmationText | translate }}
</button>
<button (click)="deny()" color="primary" mat-flat-button>
<button (click)="deny()" color="primary" mat-flat-button class="red-button">
{{ confirmationDialogInput.denyText | translate }}
</button>
</div>
<button (click)="dialogRef.close()" class="dialog-close" mat-icon-button>
<mat-icon svgIcon="red:close"></mat-icon>
</button>
<redaction-circle-button icon="red:close" mat-dialog-close class="dialog-close"></redaction-circle-button>
</section>

View File

@ -11,8 +11,7 @@ export class ConfirmationDialogInput {
constructor(options: ConfirmationDialogInput) {
this.title = options.title || ConfirmationDialogInput.default().title;
this.question = options.question || ConfirmationDialogInput.default().question;
this.confirmationText =
options.confirmationText || ConfirmationDialogInput.default().confirmationText;
this.confirmationText = options.confirmationText || ConfirmationDialogInput.default().confirmationText;
this.denyText = options.denyText || ConfirmationDialogInput.default().denyText;
}

View File

@ -9,7 +9,7 @@ import {
TypeValue,
DictionaryControllerService
} from '@redaction/red-ui-http';
import { ConfirmationDialogComponent, ConfirmationDialogInput } from '../common/confirmation-dialog/confirmation-dialog.component';
import { ConfirmationDialogComponent, ConfirmationDialogInput } from './confirmation-dialog/confirmation-dialog.component';
import { NotificationService, NotificationType } from '../notification/notification.service';
import { TranslateService } from '@ngx-translate/core';
import { AppStateService } from '../state/app-state.service';
@ -23,7 +23,7 @@ import { ProjectWrapper } from '../state/model/project.wrapper';
import { AddEditDictionaryDialogComponent } from '../screens/admin/dictionary-listing-screen/add-edit-dictionary-dialog/add-edit-dictionary-dialog.component';
const dialogConfig = {
width: '600px',
width: '662px',
maxWidth: '90vw',
autoFocus: false
};

View File

@ -10,42 +10,23 @@
{{ 'project-overview.file-listing.file-entry.status' | translate: fileStatus }}
</div>
<div class="detail-row">
{{
'project-overview.file-listing.file-entry.number-of-pages'
| translate: fileStatus
}}
{{ 'project-overview.file-listing.file-entry.number-of-pages' | translate: fileStatus }}
</div>
<div class="detail-row">
{{
'project-overview.file-listing.file-entry.number-of-analyses'
| translate: fileStatus
}}
{{ 'project-overview.file-listing.file-entry.number-of-analyses' | translate: fileStatus }}
</div>
<div class="detail-row">
{{
'project-overview.file-listing.file-entry.added'
| translate: { added: fileStatus.added | date: 'short' }
}}
{{ 'project-overview.file-listing.file-entry.added' | translate: { added: fileStatus.added | date: 'short' } }}
</div>
<div *ngIf="fileStatus.lastUpdated" class="detail-row">
{{
'project-overview.file-listing.file-entry.last-updated'
| translate: { lastUpdated: fileStatus.lastUpdated | date: 'short' }
}}
{{ 'project-overview.file-listing.file-entry.last-updated' | translate: { lastUpdated: fileStatus.lastUpdated | date: 'short' } }}
</div>
</div>
</div>
<div class="dialog-actions">
<button
(click)="downloadRedactionReport()"
color="primary"
mat-flat-button
translate="file-details.dialog.actions.download-redaction-report"
></button>
<button (click)="downloadRedactionReport()" color="primary" mat-flat-button translate="file-details.dialog.actions.download-redaction-report"></button>
</div>
<button (click)="dialogRef.close()" class="dialog-close" mat-icon-button>
<mat-icon svgIcon="red:close"></mat-icon>
</button>
<redaction-circle-button icon="red:close" mat-dialog-close class="dialog-close"></redaction-circle-button>
</section>

View File

@ -50,7 +50,6 @@
<button color="primary" mat-flat-button [disabled]="!redactionForm.valid" translate="manual-annotation.dialog.actions.save" type="submit"></button>
</div>
</form>
<button (click)="dialogRef.close()" class="dialog-close" mat-icon-button>
<mat-icon svgIcon="red:close"></mat-icon>
</button>
<redaction-circle-button icon="red:close" mat-dialog-close class="dialog-close"></redaction-circle-button>
</section>

View File

@ -23,7 +23,7 @@
></redaction-filter>
<form [formGroup]="searchForm">
<div class="red-input-group">
<input [placeholder]="'project-listing.search' | translate" formControlName="query" name="query" type="text" class="with-icon" />
<input [placeholder]="'project-listing.search' | translate" formControlName="query" name="query" type="text" class="with-icon mt-0" />
<mat-icon class="icon-right" svgIcon="red:search"></mat-icon>
</div>
</form>

View File

@ -3,6 +3,13 @@
.header-wrapper {
display: flex;
flex-direction: row;
position: relative;
redaction-circle-button {
position: absolute;
top: -5px;
left: 277px;
}
}
.legend {

View File

@ -24,7 +24,7 @@
></redaction-filter>
<form [formGroup]="searchForm">
<div class="red-input-group">
<input [placeholder]="'project-overview.search' | translate" formControlName="query" name="query" type="text" class="with-icon" />
<input [placeholder]="'project-overview.search' | translate" formControlName="query" name="query" type="text" class="with-icon mt-0" />
<mat-icon class="icon-right" svgIcon="red:search"></mat-icon>
</div>
</form>

View File

@ -68,14 +68,18 @@ redaction-table-col-name::ng-deep {
}
.left-container {
width: calc(100vw - 350px);
width: calc(100vw - 358px);
}
.right-container {
display: flex;
width: 350px;
min-width: 350px;
width: 358px;
min-width: 358px;
padding: 16px 16px 16px 24px;
redaction-project-details {
width: 100%;
}
}
.reanalyse-link {

View File

@ -344,15 +344,19 @@
"dialog": {
"single-user": "Reviewer",
"title": "Manage File Reviewer",
"save": "Save"
"save": "Save",
"cancel": "Cancel"
}
},
"assign-project-owner": {
"dialog": {
"single-user": "Owner",
"multi-user": "Review Team",
"title": "Manage Project Owner and Review Team",
"save": "Save"
"title": "Manage Project Team",
"members": "Members",
"save": "Save Changes",
"cancel": "Cancel",
"search": "Search..."
}
},
"project-member-guard": {

View File

@ -12,10 +12,24 @@
.mat-button-wrapper {
display: flex;
align-items: center;
line-height: 34px;
gap: 6px;
}
}
.cdk-program-focused .mat-button-focus-overlay {
opacity: 0 !important;
}
.mat-flat-button.mat-primary.red-button {
padding: 0 14px;
transition: background-color 0.2s;
&:not(.mat-button-disabled):hover {
background-color: $red-2;
}
}
redaction-icon-button,
redaction-chevron-button,
redaction-circle-button {

View File

@ -1,5 +1,9 @@
@import './red-variables';
.mat-dialog-container {
border-radius: 8px;
color: $grey-1;
padding: 0 !important;
border-radius: 8px !important;
}
.dialog {
@ -8,31 +12,28 @@
.dialog-close {
position: absolute;
top: 0;
right: -10px;
mat-icon {
width: 14px;
height: 14px;
}
top: 16px;
right: 16px;
}
.dialog-header {
padding-top: 12px;
padding-bottom: 12px;
padding: 32px 32px 16px 32px;
}
.dialog-content {
padding-top: 12px;
padding-bottom: 12px;
padding: 0 32px;
}
.dialog-actions {
padding-top: 12px;
height: 81px;
box-sizing: border-box;
border-top: 1px solid $separator;
padding: 0 32px;
align-items: center;
display: flex;
> * {
margin-right: 16px;
}
padding-bottom: 40px;
}
}

View File

@ -36,10 +36,6 @@ body {
.red-input-group {
width: 250px;
input {
margin-top: 0;
}
}
}
}
@ -145,6 +141,10 @@ body {
flex: 2;
}
.mt-0 {
margin-top: 0 !important;
}
.mt-5 {
margin-top: 5px;
}

View File

@ -18,9 +18,9 @@ a {
}
.heading-l {
font-size: 18px;
font-size: 20px;
font-weight: 600;
line-height: 22px;
line-height: 24px;
}
.heading {