Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,5 @@ export interface ParatextUserProfile {
username: string;
opaqueUserId: string;
sfUserId?: string;
role?: string;
}
Original file line number Diff line number Diff line change
Expand Up @@ -62,8 +62,13 @@ class TestEnvironment {
readonly db: ShareDBMingo;
readonly mockedSchemaVersionRepository = mock(SchemaVersionRepository);
readonly paratextUsers: ParatextUserProfile[] = [
{ sfUserId: 'projectAdmin', username: 'ptprojectAdmin', opaqueUserId: 'opaqueprojectAdmin' },
{ sfUserId: 'translator', username: 'pttranslator', opaqueUserId: 'opaquetranslator' }
{
sfUserId: 'projectAdmin',
username: 'ptprojectAdmin',
opaqueUserId: 'opaqueprojectAdmin',
role: 'pt_administrator'
},
{ sfUserId: 'translator', username: 'pttranslator', opaqueUserId: 'opaquetranslator', role: 'pt_translator' }
];

constructor() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -568,6 +568,9 @@ export class SFProjectService extends ProjectService<SFProject> {
},
sfUserId: {
bsonType: 'string'
},
role: {
bsonType: 'string'
}
},
additionalProperties: false
Expand Down
Original file line number Diff line number Diff line change
@@ -1,18 +1,9 @@
import {
ChangeDetectorRef,
Component,
DestroyRef,
ElementRef,
EventEmitter,
Input,
Output,
ViewChild
} from '@angular/core';
import { Component, DestroyRef, ElementRef, EventEmitter, Input, Output, ViewChild } from '@angular/core';
import { FormControl, FormGroup, FormGroupDirective, Validators } from '@angular/forms';
import { Operation } from 'realtime-server/lib/esm/common/models/project-rights';
import { SF_PROJECT_RIGHTS, SFProjectDomain } from 'realtime-server/lib/esm/scriptureforge/models/sf-project-rights';
import { SFProjectRole } from 'realtime-server/lib/esm/scriptureforge/models/sf-project-role';
import { BehaviorSubject, combineLatest } from 'rxjs';
import { BehaviorSubject, combineLatest, startWith } from 'rxjs';
import { CommandError } from 'xforge-common/command.service';
import { I18nService } from 'xforge-common/i18n.service';
import { NoticeService } from 'xforge-common/notice.service';
Expand Down Expand Up @@ -58,7 +49,6 @@ export class ShareControlComponent extends ShareBaseComponent {
private readonly noticeService: NoticeService,
private readonly projectService: SFProjectService,
private readonly onlineStatusService: OnlineStatusService,
private readonly changeDetector: ChangeDetectorRef,
userService: UserService,
private destroyRef: DestroyRef
) {
Expand All @@ -80,7 +70,7 @@ export class ShareControlComponent extends ShareBaseComponent {
.pipe(quietTakeUntilDestroyed(this.destroyRef, { logWarnings: false }))
.subscribe(() => this.updateFormEnabledStateAndLinkSharingKey());
});
combineLatest([this.onlineStatusService.onlineStatus$, this.roleControl.valueChanges])
combineLatest([this.onlineStatusService.onlineStatus$, this.roleControl.valueChanges.pipe(startWith(null))])
.pipe(quietTakeUntilDestroyed(this.destroyRef))
.subscribe(() => this.updateFormEnabledStateAndLinkSharingKey());
}
Expand Down Expand Up @@ -201,8 +191,6 @@ export class ShareControlComponent extends ShareBaseComponent {
this.sendInviteForm.enable({ emitEvent: false });
} else {
this.sendInviteForm.disable({ emitEvent: false });
// Workaround for angular/angular#17793 (ExpressionChangedAfterItHasBeenCheckedError after form disabled)
this.changeDetector.detectChanges();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ export function getEmptyChapterDoc(id: TextDocId): TextData {
export function paratextUsersFromRoles(userRoles: { [id: string]: string }): ParatextUserProfile[] {
return Object.keys(userRoles)
.filter(u => isParatextRole(userRoles[u]))
.map(u => ({ sfUserId: u, username: `pt${u}`, opaqueUserId: `opaque${u}` }));
.map(u => ({ sfUserId: u, username: `pt${u}`, opaqueUserId: `opaque${u}`, role: userRoles[u] }));
}

// Function to create a mock MediaStream with an audio track
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,137 +11,127 @@
</div>
</app-notice>

<div class="users-controls">
<!-- The tab group component sets the currentTabIndex which filters the list of users.
This is a non-standard way to use the component and causes a slight UI glitch where the
tab text jumps a few pixels when navigating between tabs. -->
<div class="tab-selector">
<mat-tab-group [mat-stretch-tabs]="false" (selectedIndexChange)="currentTabIndex = $event">
<mat-tab [label]="t('all')"></mat-tab>
<mat-tab [label]="t('paratext_members')"></mat-tab>
<mat-tab [label]="t('project_guests')"></mat-tab>
</mat-tab-group>
</div>
<mat-form-field [formGroup]="filterForm" appearance="outline" id="project-user-filter">
<mat-label>{{ t("filter_users") }}</mat-label>
<input matInput formControlName="filter" (keyup)="updateSearchTerm($event.target)" />
</mat-form-field>
</div>
@if (!isLoading) {
<div>
@if (filteredLength > 0) {
<div>
<table mat-table fxFill id="project-users-table" [dataSource]="rowsToDisplay">
<ng-container matColumnDef="avatar">
<td mat-cell *matCellDef="let userRow; let i = index">
@if (!userRow.isInvitee) {
<div>
<app-avatar [user]="userRow.user" [size]="32"></app-avatar>
</div>
}
</td>
</ng-container>
<ng-container matColumnDef="name">
<td mat-cell *matCellDef="let userRow">
@if (!userRow.inviteeStatus) {
<div class="display-name-label">
{{ userRow.user?.displayName }}
@if (isCurrentUser(userRow)) {
<b class="current-user-label">&nbsp;{{ t("me") }}</b>
}
</div>
} @else {
<div
[innerHtml]="
userRow.inviteeStatus.expired
? i18n.translateAndInsertTags('collaborators.invitation_expired', {
email: userRow.user?.email
})
: i18n.translateAndInsertTags('collaborators.awaiting_response_from', {
email: userRow.user?.email
})
"
></div>
}
<div class="hide-gt-sm">
<em>{{ userRow.role ? i18n.localizeRole(userRow.role) : "" }}</em>
</div>
</td>
</ng-container>
<ng-container matColumnDef="info">
<td mat-cell *matCellDef="let userRow">
@if (hasParatextRole(userRow)) {
<div>
<img src="/assets/images/logo-pt9.png" alt="Paratext Logo" class="paratext-logo" />
</div>
}
</td>
</ng-container>
<ng-container matColumnDef="questions_permission">
<td mat-cell *matCellDef="let userRow">
@if (userRow.allowCreatingQuestions) {
<div [matTooltip]="t('allow_add_edit_questions')">
<mat-icon>post_add</mat-icon>
</div>
}
</td>
</ng-container>
<ng-container matColumnDef="audio_permission">
<td mat-cell *matCellDef="let userRow">
@if (userRow.canManageAudio) {
<div [matTooltip]="t('allow_manage_audio')">
<mat-icon class="shift-left material-icons-outlined">audio_file</mat-icon>
@for (userList of projectUsers; track userList.userType) {
<h2>
{{ userList.userType === "paratext" ? t("paratext_members") : t("project_guests") }}
</h2>
@if (userList.rows.length > 0) {
<div>
<table mat-table fxFill id="{{ userList.userType }}" [dataSource]="userList.rows">
<ng-container matColumnDef="avatar">
<td mat-cell *matCellDef="let userRow; let i = index">
@if (userRow.inviteeStatus == null && !userRow.paratextMemberNotConnected) {
<div>
<app-avatar [user]="userRow.user" [size]="32"></app-avatar>
</div>
}
</td>
</ng-container>
<ng-container matColumnDef="name">
<td mat-cell *matCellDef="let userRow">
@if (!userRow.inviteeStatus) {
<div class="display-name-label">
<div>
{{ userRow.user?.displayName }}
@if (isCurrentUser(userRow)) {
&nbsp;<b class="current-user-label">
{{ t("me") }}
</b>
}
</div>
@if (userRow.paratextMemberNotConnected) {
<i>{{ t("paratext_member_not_connected") }}</i>
}
</div>
} @else {
<div
[innerHtml]="
userRow.inviteeStatus.expired
? i18n.translateAndInsertTags('collaborators.invitation_expired', {
email: userRow.user?.email
})
: i18n.translateAndInsertTags('collaborators.awaiting_response_from', {
email: userRow.user?.email
})
"
></div>
}
<div class="hide-gt-sm">
<em>{{ userRow.role ? i18n.localizeRole(userRow.role) : t("role_unknown") }}</em>
</div>
}
</td>
</ng-container>
<ng-container matColumnDef="role">
<td class="hide-lt-sm" mat-cell *matCellDef="let userRow">
<em>{{ userRow.role ? i18n.localizeRole(userRow.role) : "" }}</em>
</td>
</ng-container>
<ng-container matColumnDef="more">
<td mat-cell *matCellDef="let userRow">
<button mat-icon-button class="user-more-menu" [matMenuTriggerFor]="userOptions">
<mat-icon>more_vert</mat-icon>
</button>
<mat-menu #userOptions="matMenu" class="user-options">
@if (!userRow.inviteeStatus && !isCurrentUser(userRow)) {
<button
mat-menu-item
class="remove-user"
(click)="removeProjectUserClicked(userRow)"
[disabled]="!isAppOnline"
>
{{ t("remove_from_project") }}
</td>
</ng-container>
<ng-container matColumnDef="questions_permission">
<td mat-cell *matCellDef="let userRow">
@if (userRow.allowCreatingQuestions) {
<div [matTooltip]="t('allow_add_edit_questions')">
<mat-icon>post_add</mat-icon>
</div>
}
</td>
</ng-container>
<ng-container matColumnDef="audio_permission">
<td mat-cell *matCellDef="let userRow">
@if (userRow.canManageAudio) {
<div [matTooltip]="t('allow_manage_audio')">
<mat-icon class="shift-left material-icons-outlined">audio_file</mat-icon>
</div>
}
</td>
</ng-container>
<ng-container matColumnDef="role">
<td class="hide-lt-sm" mat-cell *matCellDef="let userRow">
<em>{{ userRow.role ? i18n.localizeRole(userRow.role) : t("role_unknown") }}</em>
</td>
</ng-container>
<ng-container matColumnDef="more">
<td mat-cell *matCellDef="let userRow">
@if (!userRow.paratextMemberNotConnected) {
<button mat-icon-button class="user-more-menu" [matMenuTriggerFor]="userOptions">
<mat-icon>more_vert</mat-icon>
</button>
} @else if (userRow.inviteeStatus) {
}
<mat-menu #userOptions="matMenu" class="user-options">
@if (!userRow.inviteeStatus && !isCurrentUser(userRow)) {
<button
mat-menu-item
class="remove-user"
(click)="removeProjectUserClicked(userRow)"
[disabled]="!isAppOnline"
>
{{ t("remove_from_project") }}
</button>
} @else if (userRow.inviteeStatus) {
<button
mat-menu-item
class="cancel-invite"
(click)="uninviteProjectUser(userRow.user.email)"
[disabled]="!isAppOnline"
>
{{ t("cancel_invite") }}
</button>
}
<button
mat-menu-item
class="cancel-invite"
(click)="uninviteProjectUser(userRow.user.email)"
[disabled]="!isAppOnline"
(click)="openRolesDialog(userRow)"
[disabled]="isAdmin(userRow.role) || userRow.inviteeStatus"
data-test-id="edit-roles-and-permissions"
>
{{ t("cancel_invite") }}
{{ t("edit_roles_and_permissions") }}
</button>
}
<button
mat-menu-item
(click)="openRolesDialog(userRow)"
[disabled]="isAdmin(userRow.role) || userRow.inviteeStatus"
data-test-id="edit-roles-and-permissions"
>
{{ t("edit_roles_and_permissions") }}
</button>
</mat-menu>
</td>
</ng-container>
<tr mat-row *matRowDef="let userRow; columns: tableColumns"></tr>
</table>
</div>
}
@if (filteredLength === 0) {
<mat-hint class="no-users-label">{{ t("no_users_found") }}</mat-hint>
</mat-menu>
</td>
</ng-container>
<tr mat-row *matRowDef="let userRow; columns: tableColumns"></tr>
</table>
</div>
} @else {
<mat-hint class="no-users-label" id="{{ `no-users-${userList.userType}`}}">{{
t("no_users_found")
}}</mat-hint>
}
}
</div>
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,6 @@ h3 {
bottom: 10px;
}

.paratext-logo {
width: 24px;
height: 24px;
}

// Add bottom border to last row that was removed in Material v15+
.mat-mdc-row:last-child .mat-mdc-cell {
border-bottom: 1px solid rgba(0, 0, 0, 0.12);
Expand All @@ -27,6 +22,7 @@ h3 {
// Padding only for the start of first column and around the 'role' column
.mat-mdc-cell {
padding-inline-end: 0;
min-width: 32px;

&:not(:first-of-type) {
padding-inline-start: 0;
Expand All @@ -48,10 +44,6 @@ h3 {
align-items: center;
}

.tab-selector {
flex-grow: 1;
}

.mat-column-avatar {
width: 65px !important;
}
Expand Down Expand Up @@ -88,8 +80,3 @@ h3 {
justify-content: space-between;
align-items: center;
}

// prevent undesirable flicker effect when changing tabs
:host ::ng-deep .mat-mdc-tab-body-wrapper {
display: none;
}
Loading
Loading