From 820c6a0c6c7c1181d0b45b21e40df0326e077146 Mon Sep 17 00:00:00 2001 From: Nathaniel Paulus Date: Wed, 7 Feb 2024 00:57:18 -0500 Subject: [PATCH] SF-2508 Add projects page --- .../ClientApp/src/app/app-routing.module.ts | 6 +- .../ClientApp/src/app/app.component.html | 26 +++-- .../ClientApp/src/app/app.component.scss | 28 +++++ .../ClientApp/src/app/app.component.ts | 42 ++++--- .../ClientApp/src/app/app.module.ts | 6 +- ...navigation-project-selector.component.html | 19 ---- ...navigation-project-selector.component.scss | 35 ------ ...igation-project-selector.component.spec.ts | 103 ------------------ .../navigation-project-selector.component.ts | 45 -------- .../page-not-found.component.html | 2 +- .../src/app/start/start.component.html | 40 +++++-- .../src/app/start/start.component.scss | 32 ++++++ .../src/app/start/start.component.spec.ts | 10 +- .../src/app/start/start.component.ts | 79 +++++++++----- .../src/assets/i18n/checking_en.json | 5 +- .../xforge-common/user-projects.service.ts | 4 +- 16 files changed, 190 insertions(+), 292 deletions(-) delete mode 100644 src/SIL.XForge.Scripture/ClientApp/src/app/navigation-project-selector/navigation-project-selector.component.html delete mode 100644 src/SIL.XForge.Scripture/ClientApp/src/app/navigation-project-selector/navigation-project-selector.component.scss delete mode 100644 src/SIL.XForge.Scripture/ClientApp/src/app/navigation-project-selector/navigation-project-selector.component.spec.ts delete mode 100644 src/SIL.XForge.Scripture/ClientApp/src/app/navigation-project-selector/navigation-project-selector.component.ts diff --git a/src/SIL.XForge.Scripture/ClientApp/src/app/app-routing.module.ts b/src/SIL.XForge.Scripture/ClientApp/src/app/app-routing.module.ts index 28f90f51aba..1340448c0fe 100644 --- a/src/SIL.XForge.Scripture/ClientApp/src/app/app-routing.module.ts +++ b/src/SIL.XForge.Scripture/ClientApp/src/app/app-routing.module.ts @@ -8,12 +8,12 @@ import { ProjectComponent } from './project/project.component'; import { SettingsComponent } from './settings/settings.component'; import { PageNotFoundComponent } from './shared/page-not-found/page-not-found.component'; import { SettingsAuthGuard, SyncAuthGuard } from './shared/project-router.guard'; -import { StartComponent } from './start/start.component'; +import { MyProjectsComponent } from './start/start.component'; import { SyncComponent } from './sync/sync.component'; import { JoinComponent } from './join/join.component'; const routes: Routes = [ - { path: 'callback/auth0', component: StartComponent, canActivate: [AuthGuard] }, + { path: 'callback/auth0', component: MyProjectsComponent, canActivate: [AuthGuard] }, { path: 'connect-project', component: ConnectProjectComponent, canActivate: [AuthGuard] }, { path: 'login', redirectTo: 'projects', pathMatch: 'full' }, { path: 'join/:shareKey', component: JoinComponent }, @@ -21,7 +21,7 @@ const routes: Routes = [ { path: 'projects/:projectId/settings', component: SettingsComponent, canActivate: [SettingsAuthGuard] }, { path: 'projects/:projectId/sync', component: SyncComponent, canActivate: [SyncAuthGuard] }, { path: 'projects/:projectId', component: ProjectComponent, canActivate: [AuthGuard] }, - { path: 'projects', component: StartComponent, canActivate: [AuthGuard] }, + { path: 'projects', component: MyProjectsComponent, canActivate: [AuthGuard] }, { path: 'system-administration', component: SystemAdministrationComponent, canActivate: [SystemAdminAuthGuard] }, { path: '**', component: PageNotFoundComponent } ]; diff --git a/src/SIL.XForge.Scripture/ClientApp/src/app/app.component.html b/src/SIL.XForge.Scripture/ClientApp/src/app/app.component.html index bb985511a67..e8ed0f489b3 100644 --- a/src/SIL.XForge.Scripture/ClientApp/src/app/app.component.html +++ b/src/SIL.XForge.Scripture/ClientApp/src/app/app.component.html @@ -10,11 +10,12 @@ - - - + + + {{ selectedProjectDoc?.data?.name }} + {{ selectedProjectDoc?.data?.shortName }} - + - +
cloud {{ t("online") }} @@ -109,11 +116,6 @@ [opened]="isExpanded || isDrawerPermanent" (closed)="drawerCollapsed()" > - diff --git a/src/SIL.XForge.Scripture/ClientApp/src/app/app.component.scss b/src/SIL.XForge.Scripture/ClientApp/src/app/app.component.scss index 452694b0846..151dee72c6c 100644 --- a/src/SIL.XForge.Scripture/ClientApp/src/app/app.component.scss +++ b/src/SIL.XForge.Scripture/ClientApp/src/app/app.component.scss @@ -211,3 +211,31 @@ header { flex-direction: column; width: 255px; } + +.logo { + display: flex; + align-items: center; + justify-content: center; +} + +.project-name-wrapper { + display: flex; + flex-direction: column; + line-height: 1em; + max-width: 20em; + min-width: 0; + margin-inline-start: 4px; + + .project-name { + font-size: 0.8em; + } + + .project-short-name { + font-size: 0.6em; + } + + > * { + overflow: hidden; + text-overflow: ellipsis; + } +} diff --git a/src/SIL.XForge.Scripture/ClientApp/src/app/app.component.ts b/src/SIL.XForge.Scripture/ClientApp/src/app/app.component.ts index 48834e63e0b..0907361af8d 100644 --- a/src/SIL.XForge.Scripture/ClientApp/src/app/app.component.ts +++ b/src/SIL.XForge.Scripture/ClientApp/src/app/app.component.ts @@ -7,9 +7,9 @@ import { SystemRole } from 'realtime-server/lib/esm/common/models/system-role'; import { AuthType, User, getAuthType } from 'realtime-server/lib/esm/common/models/user'; import { SFProjectRole } from 'realtime-server/lib/esm/scriptureforge/models/sf-project-role'; import { TextInfo } from 'realtime-server/lib/esm/scriptureforge/models/text-info'; -import { filter, map } from 'rxjs/operators'; +import { filter, map, startWith } from 'rxjs/operators'; import { ActivatedProjectService } from 'xforge-common/activated-project.service'; -import { Subscription, combineLatest } from 'rxjs'; +import { Subscription, combineLatest, Observable } from 'rxjs'; import { AuthService } from 'xforge-common/auth.service'; import { DataLoadingComponent } from 'xforge-common/data-loading-component'; import { DialogService } from 'xforge-common/dialog.service'; @@ -19,7 +19,6 @@ import { FeatureFlagService } from 'xforge-common/feature-flags/feature-flag.ser import { FeatureFlagsDialogComponent } from 'xforge-common/feature-flags/feature-flags-dialog.component'; import { FileService } from 'xforge-common/file.service'; import { I18nService } from 'xforge-common/i18n.service'; -import { LocationService } from 'xforge-common/location.service'; import { UserDoc } from 'xforge-common/models/user-doc'; import { NoticeService } from 'xforge-common/notice.service'; import { OnlineStatusService } from 'xforge-common/online-status.service'; @@ -31,6 +30,7 @@ import { import { SFUserProjectsService } from 'xforge-common/user-projects.service'; import { UserService } from 'xforge-common/user.service'; import { issuesEmailTemplate, supportedBrowser } from 'xforge-common/utils'; +import { filterNullish } from 'xforge-common/util/rxjs-util'; import versionData from '../../../version.json'; import { environment } from '../environments/environment'; import { SFProjectProfileDoc } from './core/models/sf-project-profile-doc'; @@ -53,7 +53,6 @@ export class AppComponent extends DataLoadingComponent implements OnInit, OnDest isExpanded: boolean = false; versionNumberClickCount = 0; - projectDocs?: SFProjectProfileDoc[]; hasUpdate: boolean = false; private currentUserDoc?: UserDoc; @@ -66,7 +65,6 @@ export class AppComponent extends DataLoadingComponent implements OnInit, OnDest constructor( private readonly router: Router, private readonly authService: AuthService, - private readonly locationService: LocationService, private readonly userService: UserService, private readonly projectService: SFProjectService, private readonly dialogService: DialogService, @@ -159,8 +157,11 @@ export class AppComponent extends DataLoadingComponent implements OnInit, OnDest } } - get isLoggedIn(): Promise { - return this.authService.isLoggedIn; + get homeUrl$(): Observable { + return this.authService.loggedInState$.pipe( + map(state => (state.loggedIn ? '/projects' : '/')), + startWith('/') + ); } get isAppLoading(): boolean { @@ -183,7 +184,7 @@ export class AppComponent extends DataLoadingComponent implements OnInit, OnDest } get selectedProjectDoc(): SFProjectProfileDoc | undefined { - return this._selectedProjectDoc; + return this.activatedProjectService.projectDoc; } get selectedProjectId(): string | undefined { @@ -191,7 +192,7 @@ export class AppComponent extends DataLoadingComponent implements OnInit, OnDest } get isProjectSelected(): boolean { - return this.selectedProjectId != null; + return this.activatedProjectService.projectId != null; } get selectedProjectRole(): SFProjectRole | undefined { @@ -231,13 +232,18 @@ export class AppComponent extends DataLoadingComponent implements OnInit, OnDest const projectDocs$ = this.userProjectsService.projectDocs$; + const selectedProjectDoc$ = projectDocs$.pipe( + map(projectDocs => { + const projectId = this.activatedProjectService.projectId; + return projectId == null ? undefined : projectDocs.find(p => p.id === projectId); + }), + filterNullish() + ); + // select the current project this.subscribe( - combineLatest([projectDocs$, this.activatedProjectService.projectId$]), - async ([projectDocs, projectId]) => { - this.projectDocs = projectDocs; - const selectedProjectDoc = projectId == null ? undefined : this.projectDocs.find(p => p.id === projectId); - + combineLatest([selectedProjectDoc$, this.activatedProjectService.projectId$]), + async ([selectedProjectDoc, projectId]) => { if (this.selectedProjectDeleteSub != null) { this.selectedProjectDeleteSub.unsubscribe(); this.selectedProjectDeleteSub = undefined; @@ -333,14 +339,6 @@ export class AppComponent extends DataLoadingComponent implements OnInit, OnDest this.authService.logOut(); } - async goHome(): Promise { - if (await this.isLoggedIn) { - this.router.navigateByUrl('/projects'); - } else { - this.locationService.go('/'); - } - } - projectChanged(value: string): void { if (value === CONNECT_PROJECT_OPTION) { if (!this.isDrawerPermanent) { diff --git a/src/SIL.XForge.Scripture/ClientApp/src/app/app.module.ts b/src/SIL.XForge.Scripture/ClientApp/src/app/app.module.ts index a635805def5..02c7e5ca905 100644 --- a/src/SIL.XForge.Scripture/ClientApp/src/app/app.module.ts +++ b/src/SIL.XForge.Scripture/ClientApp/src/app/app.module.ts @@ -27,7 +27,6 @@ import { CheckingModule } from './checking/checking.module'; import { ConnectProjectComponent } from './connect-project/connect-project.component'; import { CoreModule } from './core/core.module'; import { JoinComponent } from './join/join.component'; -import { NavigationProjectSelectorComponent } from './navigation-project-selector/navigation-project-selector.component'; import { ProjectSelectComponent } from './project-select/project-select.component'; import { ProjectComponent } from './project/project.component'; import { ScriptureChooserDialogComponent } from './scripture-chooser-dialog/scripture-chooser-dialog.component'; @@ -35,7 +34,7 @@ import { DeleteProjectDialogComponent } from './settings/delete-project-dialog/d import { SettingsComponent } from './settings/settings.component'; import { SharedModule } from './shared/shared.module'; import { TextNoteDialogComponent } from './shared/text/text-note-dialog/text-note-dialog.component'; -import { StartComponent } from './start/start.component'; +import { MyProjectsComponent } from './start/start.component'; import { SyncProgressComponent } from './sync/sync-progress/sync-progress.component'; import { SyncComponent } from './sync/sync.component'; import { TranslateModule } from './translate/translate.module'; @@ -50,7 +49,7 @@ import { NavigationComponent } from './navigation/navigation.component'; DeleteProjectDialogComponent, ProjectComponent, SettingsComponent, - StartComponent, + MyProjectsComponent, SyncComponent, ScriptureChooserDialogComponent, SupportedBrowsersDialogComponent, @@ -79,7 +78,6 @@ import { NavigationComponent } from './navigation/navigation.component'; TranslocoModule, AppRoutingModule, SharedModule, - NavigationProjectSelectorComponent, AvatarComponent ], providers: [ diff --git a/src/SIL.XForge.Scripture/ClientApp/src/app/navigation-project-selector/navigation-project-selector.component.html b/src/SIL.XForge.Scripture/ClientApp/src/app/navigation-project-selector/navigation-project-selector.component.html deleted file mode 100644 index af32fb166fd..00000000000 --- a/src/SIL.XForge.Scripture/ClientApp/src/app/navigation-project-selector/navigation-project-selector.component.html +++ /dev/null @@ -1,19 +0,0 @@ - - - - - {{ projectLabel(projectDoc) }} - - - - add - {{ t("connect_project") }} - - - - diff --git a/src/SIL.XForge.Scripture/ClientApp/src/app/navigation-project-selector/navigation-project-selector.component.scss b/src/SIL.XForge.Scripture/ClientApp/src/app/navigation-project-selector/navigation-project-selector.component.scss deleted file mode 100644 index c7859a595fa..00000000000 --- a/src/SIL.XForge.Scripture/ClientApp/src/app/navigation-project-selector/navigation-project-selector.component.scss +++ /dev/null @@ -1,35 +0,0 @@ -// The select overlay container is positioned outside of the component -::ng-deep .app-navigation-project-selector-panel.mdc-menu-surface.mat-mdc-select-panel { - // Top app bar height is 56px, this project selector's height is 56px, and we want 32px of space at the bottom. - // 56 + 56 + 32 = 144 - max-height: calc(100vh - 144px); - - // .project-option added to increase CSS rule specificity - mat-option.project-option { - .mdc-list-item__primary-text { - // Fix overflow in Firefox when a project name uses the Khmer script. As of 2022-11-11, the DBL resource - // KHSV05 is one example that will cause overflow, which looks particularly bad in RTL mode because it - // overflows to the left in a menu that is laid out in LTR. - overflow-wrap: anywhere; - // Material wants to right-align everything when any parent element has dir="rtl". Prevent that. - margin-left: 0; - } - } -} - -:host ::ng-deep { - .mat-mdc-text-field-wrapper { - border-radius: 0; - } - - // Hide the hint/error spacing as it isn't required - .mat-mdc-form-field-subscript-wrapper { - display: none; - } -} - -// :host is here only to increase CSS rule specificity -:host mat-form-field { - display: block; - text-align: left; -} diff --git a/src/SIL.XForge.Scripture/ClientApp/src/app/navigation-project-selector/navigation-project-selector.component.spec.ts b/src/SIL.XForge.Scripture/ClientApp/src/app/navigation-project-selector/navigation-project-selector.component.spec.ts deleted file mode 100644 index 6236264786b..00000000000 --- a/src/SIL.XForge.Scripture/ClientApp/src/app/navigation-project-selector/navigation-project-selector.component.spec.ts +++ /dev/null @@ -1,103 +0,0 @@ -import { Component, DebugElement } from '@angular/core'; -import { ComponentFixture, fakeAsync, TestBed, tick } from '@angular/core/testing'; -import { By } from '@angular/platform-browser'; -import { NoopAnimationsModule } from '@angular/platform-browser/animations'; -import { createTestProjectProfile } from 'realtime-server/lib/esm/scriptureforge/models/sf-project-test-data'; -import { mock } from 'ts-mockito'; -import { FeatureFlagService } from 'xforge-common/feature-flags/feature-flag.service'; -import { OnlineStatusService } from 'xforge-common/online-status.service'; -import { TestOnlineStatusModule } from 'xforge-common/test-online-status.module'; -import { TestOnlineStatusService } from 'xforge-common/test-online-status.service'; -import { configureTestingModule, TestTranslocoModule } from 'xforge-common/test-utils'; -import { UICommonModule } from 'xforge-common/ui-common.module'; -import { SFProjectProfileDoc } from '../core/models/sf-project-profile-doc'; -import { NavigationProjectSelectorComponent } from './navigation-project-selector.component'; - -const mockFeatureFlagService = mock(FeatureFlagService); - -describe('NavigationProjectSelectorComponent', () => { - configureTestingModule(() => ({ - providers: [ - { provide: OnlineStatusService, useClass: TestOnlineStatusService }, - { provide: FeatureFlagService, useMock: mockFeatureFlagService } - ], - imports: [ - NavigationProjectSelectorComponent, - UICommonModule, - NoopAnimationsModule, - TestTranslocoModule, - TestOnlineStatusModule.forRoot() - ] - })); - - it('emits event when project changes', fakeAsync(() => { - const template = ``; - const env = new TestEnvironment(template); - - env.click(env.select); - const options = env.options; - expect(options.length).toEqual(3); - env.click(options[0]); - expect(env.component.changed).toEqual('project01'); - env.click(options[2]); - expect(env.component.changed).toEqual('*connect-project*'); - env.wait(); - })); -}); - -@Component({ selector: 'app-host', template: '' }) -class HostComponent { - changed?: string; - projectDocs: Partial[] = [ - { - id: 'project01', - data: createTestProjectProfile({ - name: 'Project 01', - shortName: 'PR1', - paratextId: '' - }) - }, - { - id: 'project02', - data: createTestProjectProfile({ - name: 'Project 02', - shortName: 'PR2', - paratextId: '' - }) - } - ]; -} - -class TestEnvironment { - readonly component: HostComponent; - readonly fixture: ComponentFixture; - - constructor(template: string) { - TestBed.configureTestingModule({ - declarations: [HostComponent], - imports: [NavigationProjectSelectorComponent, UICommonModule, NoopAnimationsModule, TestTranslocoModule] - }); - TestBed.overrideComponent(HostComponent, { set: { template: template } }); - this.fixture = TestBed.createComponent(HostComponent); - this.component = this.fixture.componentInstance; - this.wait(); - } - - get select(): DebugElement { - return this.fixture.debugElement.query(By.css('mat-select')); - } - - get options(): DebugElement[] { - return this.fixture.debugElement.queryAll(By.css('mat-option')); - } - - click(element: DebugElement): void { - element.nativeElement.click(); - this.fixture.detectChanges(); - } - - wait(): void { - tick(); - this.fixture.detectChanges(); - } -} diff --git a/src/SIL.XForge.Scripture/ClientApp/src/app/navigation-project-selector/navigation-project-selector.component.ts b/src/SIL.XForge.Scripture/ClientApp/src/app/navigation-project-selector/navigation-project-selector.component.ts deleted file mode 100644 index 92c2c12b0dd..00000000000 --- a/src/SIL.XForge.Scripture/ClientApp/src/app/navigation-project-selector/navigation-project-selector.component.ts +++ /dev/null @@ -1,45 +0,0 @@ -import { Component, EventEmitter, Input, Output } from '@angular/core'; -import { I18nService } from 'xforge-common/i18n.service'; -import { OnlineStatusService } from 'xforge-common/online-status.service'; -import { TranslocoModule } from '@ngneat/transloco'; -import { CommonModule } from '@angular/common'; -import { MatDividerModule } from '@angular/material/divider'; -import { MatSelectModule } from '@angular/material/select'; -import { MatIconModule } from '@angular/material/icon'; -import { projectLabel } from '../shared/utils'; -import { SFProjectProfileDoc } from '../core/models/sf-project-profile-doc'; -import { SelectableProject } from '../core/paratext.service'; - -@Component({ - standalone: true, - selector: 'app-navigation-project-selector', - templateUrl: './navigation-project-selector.component.html', - styleUrls: ['./navigation-project-selector.component.scss'], - imports: [TranslocoModule, CommonModule, MatDividerModule, MatSelectModule, MatIconModule] -}) -export class NavigationProjectSelectorComponent { - @Output() changed: EventEmitter = new EventEmitter(); - @Input() projectDocs?: SFProjectProfileDoc[]; - @Input() selected?: SFProjectProfileDoc; - constructor(readonly i18n: I18nService, private readonly onlineStatusService: OnlineStatusService) {} - - get hasProjects(): boolean { - return this.projectDocs != null && this.projectDocs.length > 0; - } - - get isOnline(): boolean { - return this.onlineStatusService.isOnline; - } - - get selectedProjectId(): string | undefined { - return this.selected?.id; - } - - projectLabel(doc: SFProjectProfileDoc): string { - return projectLabel({ - name: doc.data?.name, - shortName: doc.data?.shortName, - paratextId: doc.data?.paratextId - } as SelectableProject); - } -} diff --git a/src/SIL.XForge.Scripture/ClientApp/src/app/shared/page-not-found/page-not-found.component.html b/src/SIL.XForge.Scripture/ClientApp/src/app/shared/page-not-found/page-not-found.component.html index 7a46540582e..b7a85a83767 100644 --- a/src/SIL.XForge.Scripture/ClientApp/src/app/shared/page-not-found/page-not-found.component.html +++ b/src/SIL.XForge.Scripture/ClientApp/src/app/shared/page-not-found/page-not-found.component.html @@ -3,5 +3,5 @@

error_outline{{ t("page_not_found") }}

{{ t("redirecting_in_moments") }}


- {{ t("project_home") }} + {{ t("my_projects") }} diff --git a/src/SIL.XForge.Scripture/ClientApp/src/app/start/start.component.html b/src/SIL.XForge.Scripture/ClientApp/src/app/start/start.component.html index d42370f6134..693d60a39fc 100644 --- a/src/SIL.XForge.Scripture/ClientApp/src/app/start/start.component.html +++ b/src/SIL.XForge.Scripture/ClientApp/src/app/start/start.component.html @@ -1,10 +1,36 @@ -
-

{{ t("not_connected_to_any_projects") }}

-

- -

+
+

Connected projects

+ +
+ {{ projectDoc.data?.shortName }} - {{ projectDoc.data?.name }} + + {{ projectDoc.data?.checkingConfig?.checkingEnabled ? "Drafting • Community checking" : "Drafting" }} + +
+ Open +
+ +

DBL resources

+ +
+ {{ projectDoc.data?.shortName }} - {{ projectDoc.data?.name }} + DBL resource +
+ Open +
+ +

{{ userProjectCount === 0 ? "Connect a project to get started" : "Not connected" }}

+ + +
+ {{ project.shortName }} - {{ project.name }} + This project cannot be connected +
+ + {{ project.isConnected ? "Join" : "Connect" }} + +
+
diff --git a/src/SIL.XForge.Scripture/ClientApp/src/app/start/start.component.scss b/src/SIL.XForge.Scripture/ClientApp/src/app/start/start.component.scss index 2ee4f0c750b..d829c8fb7d8 100644 --- a/src/SIL.XForge.Scripture/ClientApp/src/app/start/start.component.scss +++ b/src/SIL.XForge.Scripture/ClientApp/src/app/start/start.component.scss @@ -3,3 +3,35 @@ button { padding: 0 0.5em; } } + +.wrapper { + max-width: 50em; + margin: 0 auto; + display: flex; + flex-direction: column; + row-gap: 1em; +} + +mat-card { + display: flex; + justify-content: space-between; + align-items: flex-end; + + > div { + display: flex; + flex-direction: column; + row-gap: 8px; + } +} + +.helper-text { + color: #777; +} + +.project-name { + font-size: 1.25em; +} + +.active-project { + background-color: #b6d7a8; +} diff --git a/src/SIL.XForge.Scripture/ClientApp/src/app/start/start.component.spec.ts b/src/SIL.XForge.Scripture/ClientApp/src/app/start/start.component.spec.ts index 33dfedfa35b..f369b011d3f 100644 --- a/src/SIL.XForge.Scripture/ClientApp/src/app/start/start.component.spec.ts +++ b/src/SIL.XForge.Scripture/ClientApp/src/app/start/start.component.spec.ts @@ -12,7 +12,7 @@ import { configureTestingModule, TestTranslocoModule } from 'xforge-common/test- import { UICommonModule } from 'xforge-common/ui-common.module'; import { UserService } from 'xforge-common/user.service'; import { SF_TYPE_REGISTRY } from '../core/models/sf-type-registry'; -import { StartComponent } from './start.component'; +import { MyProjectsComponent } from './start.component'; const mockedRouter = mock(Router); const mockedActivatedRoute = mock(ActivatedRoute); @@ -21,7 +21,7 @@ const mockedUserService = mock(UserService); describe('StartComponent', () => { configureTestingModule(() => ({ - declarations: [StartComponent], + declarations: [MyProjectsComponent], imports: [UICommonModule, RouterTestingModule, TestTranslocoModule, TestRealtimeModule.forRoot(SF_TYPE_REGISTRY)], providers: [ { provide: Router, useMock: mockedRouter }, @@ -87,8 +87,8 @@ describe('StartComponent', () => { }); class TestEnvironment { - readonly component: StartComponent; - readonly fixture: ComponentFixture; + readonly component: MyProjectsComponent; + readonly fixture: ComponentFixture; private readonly realtimeService: TestRealtimeService = TestBed.inject(TestRealtimeService); @@ -97,7 +97,7 @@ class TestEnvironment { this.realtimeService.subscribe(UserDoc.COLLECTION, 'user01') ); - this.fixture = TestBed.createComponent(StartComponent); + this.fixture = TestBed.createComponent(MyProjectsComponent); this.component = this.fixture.componentInstance; } diff --git a/src/SIL.XForge.Scripture/ClientApp/src/app/start/start.component.ts b/src/SIL.XForge.Scripture/ClientApp/src/app/start/start.component.ts index 52d62bfb610..d0aeae81211 100644 --- a/src/SIL.XForge.Scripture/ClientApp/src/app/start/start.component.ts +++ b/src/SIL.XForge.Scripture/ClientApp/src/app/start/start.component.ts @@ -1,56 +1,75 @@ -import { Component, OnInit } from '@angular/core'; -import { ActivatedRoute, Router } from '@angular/router'; +import { Component } from '@angular/core'; import { UserDoc } from 'xforge-common/models/user-doc'; import { NoticeService } from 'xforge-common/notice.service'; import { SubscriptionDisposable } from 'xforge-common/subscription-disposable'; +import { SFUserProjectsService } from 'xforge-common/user-projects.service'; import { UserService } from 'xforge-common/user.service'; import { environment } from '../../environments/environment'; - -export function selectValidProject(userDoc: UserDoc, currentProjectId?: string): string | undefined { - if (userDoc.data == null) { - return; - } - - let projectId: string | undefined; - const site = userDoc.data.sites[environment.siteId]; - if (currentProjectId != null && site.projects.includes(currentProjectId)) { - projectId = currentProjectId; - } else if (site.projects.length > 0) { - projectId = site.projects[0]; - } - return projectId; -} +import { ParatextProject } from '../core/models/paratext-project'; +import { SFProjectProfileDoc } from '../core/models/sf-project-profile-doc'; +import { ParatextService } from '../core/paratext.service'; @Component({ selector: 'app-start', templateUrl: './start.component.html', styleUrls: ['./start.component.scss'] }) -export class StartComponent extends SubscriptionDisposable implements OnInit { +export class MyProjectsComponent extends SubscriptionDisposable { + private allParatextProjects: ParatextProject[] | undefined; + + connectedProjects: SFProjectProfileDoc[] | undefined; + connectedResources: SFProjectProfileDoc[] | undefined; + nonJoinedParatextProjects: ParatextProject[] | undefined; + user: UserDoc | undefined; + constructor( - private readonly router: Router, - private readonly route: ActivatedRoute, private readonly noticeService: NoticeService, + private readonly userProjectsService: SFUserProjectsService, + private readonly paratextService: ParatextService, private readonly userService: UserService ) { super(); + + this.subscribe(this.userProjectsService.projectDocs$, projects => { + this.connectedProjects = projects.filter(project => !this.isResource(project)); + this.connectedResources = projects.filter(project => this.isResource(project)); + }); + + this.loadParatextProjects(); + this.loadUser(); } get isAppLoading(): boolean { return this.noticeService.isAppLoading; } - async ngOnInit(): Promise { - const userDoc = await this.userService.getCurrentUser(); - this.subscribe(userDoc.remoteChanges$, () => this.navigateToProject(userDoc)); - this.navigateToProject(userDoc); + isLastSelectedProject(project: SFProjectProfileDoc): boolean { + return project.id === this.user?.data?.sites[environment.siteId].currentProjectId; + } + + get userProjectCount(): number { + return this.user?.data?.sites[environment.siteId].projects.length ?? 0; + } + + private async loadParatextProjects(): Promise { + this.allParatextProjects = ((await this.paratextService.getProjects()) ?? []).filter( + project => !project.isConnected + ); + this.updateNonJoinedParatextProjects(); + } + + private async loadUser(): Promise { + this.user = await this.userService.getCurrentUser(); + this.updateNonJoinedParatextProjects(); + } + + private updateNonJoinedParatextProjects(): void { + this.nonJoinedParatextProjects = this.allParatextProjects?.filter(project => { + return !this.user?.data?.sites[environment.siteId].projects.includes(project.projectId as any); + }); } - private navigateToProject(userDoc: UserDoc): void { - const currentProjectId: string | undefined = this.userService.currentProjectId(userDoc); - const projectId = selectValidProject(userDoc, currentProjectId); - if (projectId != null) { - this.router.navigate(['./', projectId], { relativeTo: this.route, replaceUrl: true }); - } + private isResource(project: SFProjectProfileDoc): boolean { + return project.data?.paratextId.length === 16; } } diff --git a/src/SIL.XForge.Scripture/ClientApp/src/assets/i18n/checking_en.json b/src/SIL.XForge.Scripture/ClientApp/src/assets/i18n/checking_en.json index 6940fbfdeaa..ed6b824d38d 100644 --- a/src/SIL.XForge.Scripture/ClientApp/src/assets/i18n/checking_en.json +++ b/src/SIL.XForge.Scripture/ClientApp/src/assets/i18n/checking_en.json @@ -13,6 +13,7 @@ "logged_in_as": "Logged in as", "manual": "Manual", "my_progress": "My progress", + "my_projects": "My projects", "offline": "Offline", "online": "Online", "open_source_licenses": "Open source licenses", @@ -21,7 +22,6 @@ "password_reset_email_sent": "We've just sent you an email to reset your password.", "product_version": "Product version: v{{ version }}", "project_has_been_deleted": "The project has been deleted or is no longer accessible.", - "project_home": "Project home", "questions_answers": "Questions & answers", "redirecting_in_moments": "Redirecting in a few moments...", "refresh": "Refresh", @@ -355,9 +355,6 @@ "cancel": "Cancel", "close": "Close" }, - "navigation-project_selector": { - "connect_project": "Connect project" - }, "scripture_chooser_dialog": { "choose_book": "Choose book", "choose_chapter": "Choose chapter", diff --git a/src/SIL.XForge.Scripture/ClientApp/src/xforge-common/user-projects.service.ts b/src/SIL.XForge.Scripture/ClientApp/src/xforge-common/user-projects.service.ts index ed5ed52d4d0..a8def0a98a4 100644 --- a/src/SIL.XForge.Scripture/ClientApp/src/xforge-common/user-projects.service.ts +++ b/src/SIL.XForge.Scripture/ClientApp/src/xforge-common/user-projects.service.ts @@ -1,5 +1,5 @@ import { Injectable } from '@angular/core'; -import { Observable, Subject } from 'rxjs'; +import { BehaviorSubject, Observable } from 'rxjs'; import { SFProjectService } from '../app/core/sf-project.service'; import { compareProjectsForSorting } from '../app/shared/utils'; import { SFProjectProfileDoc } from '../app/core/models/sf-project-profile-doc'; @@ -14,7 +14,7 @@ import { UserService } from './user.service'; }) export class SFUserProjectsService extends SubscriptionDisposable { private projectDocs: Map = new Map(); - private _projectDocs$ = new Subject(); + private _projectDocs$ = new BehaviorSubject([]); constructor( private readonly userService: UserService,