Skip to content

Commit 6f5815f

Browse files
SF-2472 Show Paratext users who have not joined the project (#3517)
1 parent de136f1 commit 6f5815f

File tree

17 files changed

+685
-450
lines changed

17 files changed

+685
-450
lines changed

src/RealtimeServer/scriptureforge/models/paratext-user-profile.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,5 @@ export interface ParatextUserProfile {
22
username: string;
33
opaqueUserId: string;
44
sfUserId?: string;
5+
role?: string;
56
}

src/RealtimeServer/scriptureforge/services/sf-project-service.spec.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -62,8 +62,13 @@ class TestEnvironment {
6262
readonly db: ShareDBMingo;
6363
readonly mockedSchemaVersionRepository = mock(SchemaVersionRepository);
6464
readonly paratextUsers: ParatextUserProfile[] = [
65-
{ sfUserId: 'projectAdmin', username: 'ptprojectAdmin', opaqueUserId: 'opaqueprojectAdmin' },
66-
{ sfUserId: 'translator', username: 'pttranslator', opaqueUserId: 'opaquetranslator' }
65+
{
66+
sfUserId: 'projectAdmin',
67+
username: 'ptprojectAdmin',
68+
opaqueUserId: 'opaqueprojectAdmin',
69+
role: 'pt_administrator'
70+
},
71+
{ sfUserId: 'translator', username: 'pttranslator', opaqueUserId: 'opaquetranslator', role: 'pt_translator' }
6772
];
6873

6974
constructor() {

src/RealtimeServer/scriptureforge/services/sf-project-service.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -568,6 +568,9 @@ export class SFProjectService extends ProjectService<SFProject> {
568568
},
569569
sfUserId: {
570570
bsonType: 'string'
571+
},
572+
role: {
573+
bsonType: 'string'
571574
}
572575
},
573576
additionalProperties: false

src/SIL.XForge.Scripture/ClientApp/src/app/shared/share/share-control.component.ts

Lines changed: 3 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,9 @@
1-
import {
2-
ChangeDetectorRef,
3-
Component,
4-
DestroyRef,
5-
ElementRef,
6-
EventEmitter,
7-
Input,
8-
Output,
9-
ViewChild
10-
} from '@angular/core';
1+
import { Component, DestroyRef, ElementRef, EventEmitter, Input, Output, ViewChild } from '@angular/core';
112
import { FormControl, FormGroup, FormGroupDirective, Validators } from '@angular/forms';
123
import { Operation } from 'realtime-server/lib/esm/common/models/project-rights';
134
import { SF_PROJECT_RIGHTS, SFProjectDomain } from 'realtime-server/lib/esm/scriptureforge/models/sf-project-rights';
145
import { SFProjectRole } from 'realtime-server/lib/esm/scriptureforge/models/sf-project-role';
15-
import { BehaviorSubject, combineLatest } from 'rxjs';
6+
import { BehaviorSubject, combineLatest, startWith } from 'rxjs';
167
import { CommandError } from 'xforge-common/command.service';
178
import { I18nService } from 'xforge-common/i18n.service';
189
import { NoticeService } from 'xforge-common/notice.service';
@@ -58,7 +49,6 @@ export class ShareControlComponent extends ShareBaseComponent {
5849
private readonly noticeService: NoticeService,
5950
private readonly projectService: SFProjectService,
6051
private readonly onlineStatusService: OnlineStatusService,
61-
private readonly changeDetector: ChangeDetectorRef,
6252
userService: UserService,
6353
private destroyRef: DestroyRef
6454
) {
@@ -80,7 +70,7 @@ export class ShareControlComponent extends ShareBaseComponent {
8070
.pipe(quietTakeUntilDestroyed(this.destroyRef, { logWarnings: false }))
8171
.subscribe(() => this.updateFormEnabledStateAndLinkSharingKey());
8272
});
83-
combineLatest([this.onlineStatusService.onlineStatus$, this.roleControl.valueChanges])
73+
combineLatest([this.onlineStatusService.onlineStatus$, this.roleControl.valueChanges.pipe(startWith(null))])
8474
.pipe(quietTakeUntilDestroyed(this.destroyRef))
8575
.subscribe(() => this.updateFormEnabledStateAndLinkSharingKey());
8676
}
@@ -201,8 +191,6 @@ export class ShareControlComponent extends ShareBaseComponent {
201191
this.sendInviteForm.enable({ emitEvent: false });
202192
} else {
203193
this.sendInviteForm.disable({ emitEvent: false });
204-
// Workaround for angular/angular#17793 (ExpressionChangedAfterItHasBeenCheckedError after form disabled)
205-
this.changeDetector.detectChanges();
206194
}
207195
}
208196
}

src/SIL.XForge.Scripture/ClientApp/src/app/shared/test-utils.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,7 @@ export function getEmptyChapterDoc(id: TextDocId): TextData {
112112
export function paratextUsersFromRoles(userRoles: { [id: string]: string }): ParatextUserProfile[] {
113113
return Object.keys(userRoles)
114114
.filter(u => isParatextRole(userRoles[u]))
115-
.map(u => ({ sfUserId: u, username: `pt${u}`, opaqueUserId: `opaque${u}` }));
115+
.map(u => ({ sfUserId: u, username: `pt${u}`, opaqueUserId: `opaque${u}`, role: userRoles[u] }));
116116
}
117117

118118
// Function to create a mock MediaStream with an audio track

src/SIL.XForge.Scripture/ClientApp/src/app/users/collaborators/collaborators.component.html

Lines changed: 113 additions & 123 deletions
Original file line numberDiff line numberDiff line change
@@ -11,137 +11,127 @@
1111
</div>
1212
</app-notice>
1313

14-
<div class="users-controls">
15-
<!-- The tab group component sets the currentTabIndex which filters the list of users.
16-
This is a non-standard way to use the component and causes a slight UI glitch where the
17-
tab text jumps a few pixels when navigating between tabs. -->
18-
<div class="tab-selector">
19-
<mat-tab-group [mat-stretch-tabs]="false" (selectedIndexChange)="currentTabIndex = $event">
20-
<mat-tab [label]="t('all')"></mat-tab>
21-
<mat-tab [label]="t('paratext_members')"></mat-tab>
22-
<mat-tab [label]="t('project_guests')"></mat-tab>
23-
</mat-tab-group>
24-
</div>
25-
<mat-form-field [formGroup]="filterForm" appearance="outline" id="project-user-filter">
26-
<mat-label>{{ t("filter_users") }}</mat-label>
27-
<input matInput formControlName="filter" (keyup)="updateSearchTerm($event.target)" />
28-
</mat-form-field>
29-
</div>
3014
@if (!isLoading) {
3115
<div>
32-
@if (filteredLength > 0) {
33-
<div>
34-
<table mat-table fxFill id="project-users-table" [dataSource]="rowsToDisplay">
35-
<ng-container matColumnDef="avatar">
36-
<td mat-cell *matCellDef="let userRow; let i = index">
37-
@if (!userRow.isInvitee) {
38-
<div>
39-
<app-avatar [user]="userRow.user" [size]="32"></app-avatar>
40-
</div>
41-
}
42-
</td>
43-
</ng-container>
44-
<ng-container matColumnDef="name">
45-
<td mat-cell *matCellDef="let userRow">
46-
@if (!userRow.inviteeStatus) {
47-
<div class="display-name-label">
48-
{{ userRow.user?.displayName }}
49-
@if (isCurrentUser(userRow)) {
50-
<b class="current-user-label">&nbsp;{{ t("me") }}</b>
51-
}
52-
</div>
53-
} @else {
54-
<div
55-
[innerHtml]="
56-
userRow.inviteeStatus.expired
57-
? i18n.translateAndInsertTags('collaborators.invitation_expired', {
58-
email: userRow.user?.email
59-
})
60-
: i18n.translateAndInsertTags('collaborators.awaiting_response_from', {
61-
email: userRow.user?.email
62-
})
63-
"
64-
></div>
65-
}
66-
<div class="hide-gt-sm">
67-
<em>{{ userRow.role ? i18n.localizeRole(userRow.role) : "" }}</em>
68-
</div>
69-
</td>
70-
</ng-container>
71-
<ng-container matColumnDef="info">
72-
<td mat-cell *matCellDef="let userRow">
73-
@if (hasParatextRole(userRow)) {
74-
<div>
75-
<img src="/assets/images/logo-pt9.png" alt="Paratext Logo" class="paratext-logo" />
76-
</div>
77-
}
78-
</td>
79-
</ng-container>
80-
<ng-container matColumnDef="questions_permission">
81-
<td mat-cell *matCellDef="let userRow">
82-
@if (userRow.allowCreatingQuestions) {
83-
<div [matTooltip]="t('allow_add_edit_questions')">
84-
<mat-icon>post_add</mat-icon>
85-
</div>
86-
}
87-
</td>
88-
</ng-container>
89-
<ng-container matColumnDef="audio_permission">
90-
<td mat-cell *matCellDef="let userRow">
91-
@if (userRow.canManageAudio) {
92-
<div [matTooltip]="t('allow_manage_audio')">
93-
<mat-icon class="shift-left material-icons-outlined">audio_file</mat-icon>
16+
@for (userList of projectUsers; track userList.userType) {
17+
<h2>
18+
{{ userList.userType === "paratext" ? t("paratext_members") : t("project_guests") }}
19+
</h2>
20+
@if (userList.rows.length > 0) {
21+
<div>
22+
<table mat-table fxFill id="{{ userList.userType }}" [dataSource]="userList.rows">
23+
<ng-container matColumnDef="avatar">
24+
<td mat-cell *matCellDef="let userRow; let i = index">
25+
@if (userRow.inviteeStatus == null && !userRow.paratextMemberNotConnected) {
26+
<div>
27+
<app-avatar [user]="userRow.user" [size]="32"></app-avatar>
28+
</div>
29+
}
30+
</td>
31+
</ng-container>
32+
<ng-container matColumnDef="name">
33+
<td mat-cell *matCellDef="let userRow">
34+
@if (!userRow.inviteeStatus) {
35+
<div class="display-name-label">
36+
<div>
37+
{{ userRow.user?.displayName }}
38+
@if (isCurrentUser(userRow)) {
39+
&nbsp;<b class="current-user-label">
40+
{{ t("me") }}
41+
</b>
42+
}
43+
</div>
44+
@if (userRow.paratextMemberNotConnected) {
45+
<i>{{ t("paratext_member_not_connected") }}</i>
46+
}
47+
</div>
48+
} @else {
49+
<div
50+
[innerHtml]="
51+
userRow.inviteeStatus.expired
52+
? i18n.translateAndInsertTags('collaborators.invitation_expired', {
53+
email: userRow.user?.email
54+
})
55+
: i18n.translateAndInsertTags('collaborators.awaiting_response_from', {
56+
email: userRow.user?.email
57+
})
58+
"
59+
></div>
60+
}
61+
<div class="hide-gt-sm">
62+
<em>{{ userRow.role ? i18n.localizeRole(userRow.role) : t("role_unknown") }}</em>
9463
</div>
95-
}
96-
</td>
97-
</ng-container>
98-
<ng-container matColumnDef="role">
99-
<td class="hide-lt-sm" mat-cell *matCellDef="let userRow">
100-
<em>{{ userRow.role ? i18n.localizeRole(userRow.role) : "" }}</em>
101-
</td>
102-
</ng-container>
103-
<ng-container matColumnDef="more">
104-
<td mat-cell *matCellDef="let userRow">
105-
<button mat-icon-button class="user-more-menu" [matMenuTriggerFor]="userOptions">
106-
<mat-icon>more_vert</mat-icon>
107-
</button>
108-
<mat-menu #userOptions="matMenu" class="user-options">
109-
@if (!userRow.inviteeStatus && !isCurrentUser(userRow)) {
110-
<button
111-
mat-menu-item
112-
class="remove-user"
113-
(click)="removeProjectUserClicked(userRow)"
114-
[disabled]="!isAppOnline"
115-
>
116-
{{ t("remove_from_project") }}
64+
</td>
65+
</ng-container>
66+
<ng-container matColumnDef="questions_permission">
67+
<td mat-cell *matCellDef="let userRow">
68+
@if (userRow.allowCreatingQuestions) {
69+
<div [matTooltip]="t('allow_add_edit_questions')">
70+
<mat-icon>post_add</mat-icon>
71+
</div>
72+
}
73+
</td>
74+
</ng-container>
75+
<ng-container matColumnDef="audio_permission">
76+
<td mat-cell *matCellDef="let userRow">
77+
@if (userRow.canManageAudio) {
78+
<div [matTooltip]="t('allow_manage_audio')">
79+
<mat-icon class="shift-left material-icons-outlined">audio_file</mat-icon>
80+
</div>
81+
}
82+
</td>
83+
</ng-container>
84+
<ng-container matColumnDef="role">
85+
<td class="hide-lt-sm" mat-cell *matCellDef="let userRow">
86+
<em>{{ userRow.role ? i18n.localizeRole(userRow.role) : t("role_unknown") }}</em>
87+
</td>
88+
</ng-container>
89+
<ng-container matColumnDef="more">
90+
<td mat-cell *matCellDef="let userRow">
91+
@if (!userRow.paratextMemberNotConnected) {
92+
<button mat-icon-button class="user-more-menu" [matMenuTriggerFor]="userOptions">
93+
<mat-icon>more_vert</mat-icon>
11794
</button>
118-
} @else if (userRow.inviteeStatus) {
95+
}
96+
<mat-menu #userOptions="matMenu" class="user-options">
97+
@if (!userRow.inviteeStatus && !isCurrentUser(userRow)) {
98+
<button
99+
mat-menu-item
100+
class="remove-user"
101+
(click)="removeProjectUserClicked(userRow)"
102+
[disabled]="!isAppOnline"
103+
>
104+
{{ t("remove_from_project") }}
105+
</button>
106+
} @else if (userRow.inviteeStatus) {
107+
<button
108+
mat-menu-item
109+
class="cancel-invite"
110+
(click)="uninviteProjectUser(userRow.user.email)"
111+
[disabled]="!isAppOnline"
112+
>
113+
{{ t("cancel_invite") }}
114+
</button>
115+
}
119116
<button
120117
mat-menu-item
121-
class="cancel-invite"
122-
(click)="uninviteProjectUser(userRow.user.email)"
123-
[disabled]="!isAppOnline"
118+
(click)="openRolesDialog(userRow)"
119+
[disabled]="isAdmin(userRow.role) || userRow.inviteeStatus"
120+
data-test-id="edit-roles-and-permissions"
124121
>
125-
{{ t("cancel_invite") }}
122+
{{ t("edit_roles_and_permissions") }}
126123
</button>
127-
}
128-
<button
129-
mat-menu-item
130-
(click)="openRolesDialog(userRow)"
131-
[disabled]="isAdmin(userRow.role) || userRow.inviteeStatus"
132-
data-test-id="edit-roles-and-permissions"
133-
>
134-
{{ t("edit_roles_and_permissions") }}
135-
</button>
136-
</mat-menu>
137-
</td>
138-
</ng-container>
139-
<tr mat-row *matRowDef="let userRow; columns: tableColumns"></tr>
140-
</table>
141-
</div>
142-
}
143-
@if (filteredLength === 0) {
144-
<mat-hint class="no-users-label">{{ t("no_users_found") }}</mat-hint>
124+
</mat-menu>
125+
</td>
126+
</ng-container>
127+
<tr mat-row *matRowDef="let userRow; columns: tableColumns"></tr>
128+
</table>
129+
</div>
130+
} @else {
131+
<mat-hint class="no-users-label" id="{{ `no-users-${userList.userType}`}}">{{
132+
t("no_users_found")
133+
}}</mat-hint>
134+
}
145135
}
146136
</div>
147137
}

src/SIL.XForge.Scripture/ClientApp/src/app/users/collaborators/collaborators.component.scss

Lines changed: 1 addition & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,6 @@ h3 {
1414
bottom: 10px;
1515
}
1616

17-
.paratext-logo {
18-
width: 24px;
19-
height: 24px;
20-
}
21-
2217
// Add bottom border to last row that was removed in Material v15+
2318
.mat-mdc-row:last-child .mat-mdc-cell {
2419
border-bottom: 1px solid rgba(0, 0, 0, 0.12);
@@ -27,6 +22,7 @@ h3 {
2722
// Padding only for the start of first column and around the 'role' column
2823
.mat-mdc-cell {
2924
padding-inline-end: 0;
25+
min-width: 32px;
3026

3127
&:not(:first-of-type) {
3228
padding-inline-start: 0;
@@ -48,10 +44,6 @@ h3 {
4844
align-items: center;
4945
}
5046

51-
.tab-selector {
52-
flex-grow: 1;
53-
}
54-
5547
.mat-column-avatar {
5648
width: 65px !important;
5749
}
@@ -88,8 +80,3 @@ h3 {
8880
justify-content: space-between;
8981
align-items: center;
9082
}
91-
92-
// prevent undesirable flicker effect when changing tabs
93-
:host ::ng-deep .mat-mdc-tab-body-wrapper {
94-
display: none;
95-
}

0 commit comments

Comments
 (0)