Skip to content

Commit

Permalink
SF-2441 Focus draft tab on draft nav (#2486)
Browse files Browse the repository at this point in the history
* initial commit

* code review changes
  • Loading branch information
siltomato authored May 24, 2024
1 parent 2655dcb commit d4621d0
Show file tree
Hide file tree
Showing 5 changed files with 141 additions and 18 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,40 @@ describe('TabStateService', () => {
});
});

describe('getFirstTabOfTypeIndex', () => {
it('should return the group and index of the first tab of the given type', () => {
const groupId1: string = 'group1';
const groupId2: string = 'group2';
const tabs1: TabInfo<string>[] = [
{ type: 'type-a', headerText: 'Header 1', closeable: true, movable: true },
{ type: 'type-b', headerText: 'Header 2', closeable: true, movable: true }
];
const tabs2: TabInfo<string>[] = [
{ type: 'type-b', headerText: 'Header 2', closeable: true, movable: true },
{ type: 'type-a', headerText: 'Header 1', closeable: true, movable: true }
];
service['groups'].set(groupId1, new TabGroup<string, any>(groupId1, tabs1));
service['groups'].set(groupId2, new TabGroup<string, any>(groupId2, tabs2));

expect(service.getFirstTabOfTypeIndex('type-a')).toEqual({ groupId: groupId1, index: 0 });
expect(service.getFirstTabOfTypeIndex('type-a', groupId2)).toEqual({ groupId: groupId2, index: 1 });
});

it('should return undefined if no tab of the given type is found', () => {
const groupId1: string = 'group1';
const groupId2: string = 'group2';
const tabs: TabInfo<string>[] = [
{ type: 'type-a', headerText: 'Header 1', closeable: true, movable: true },
{ type: 'type-b', headerText: 'Header 2', closeable: true, movable: true }
];
service['groups'].set(groupId1, new TabGroup<string, any>(groupId1, tabs));
service['groups'].set(groupId2, new TabGroup<string, any>(groupId2, tabs));

const result = service.getFirstTabOfTypeIndex('type-c');
expect(result).toEqual(undefined);
});
});

describe('tab actions', () => {
it('should add a tab', () => {
const groupId: string = 'source';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -101,8 +101,31 @@ export class TabStateService<TGroupId extends string, T extends TabInfo<string>>
this.tabGroupsSource$.next(this.groups);
}

getFirstTabOfTypeIndex(groupId: TGroupId, type: string): number | undefined {
return this.groups.get(groupId)?.tabs?.findIndex(t => t.type === type);
/**
* Returns the group and index of the first tab of the given type. If no tab is found, returns undefined.
* @param type The type of the tab to find.
* @param groupId The group to search in. If not provided, searches all groups.
*/
getFirstTabOfTypeIndex(type: string, groupId?: TGroupId): { groupId: TGroupId; index: number } | undefined {
if (groupId == null) {
for (const [groupId, group] of this.groups) {
const index = group.tabs.findIndex(tab => tab.type === type);

if (index !== -1) {
return { groupId, index };
}
}

return undefined;
}

const index: number | undefined = this.groups.get(groupId)?.tabs?.findIndex(t => t.type === type);

if (index == null || index === -1) {
return undefined;
}

return { groupId, index };
}

hasTab(groupId: TGroupId, type: string): boolean {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
<ng-container *transloco="let t; read: 'draft_preview_books'">
@for (book of booksWithDrafts$ | async; track book.bookNumber) {
<a mat-stroked-button [routerLink]="linkForBookAndChapter(book.bookNumber, book.firstChapterWithDraft)">
<a
mat-stroked-button
[routerLink]="linkForBookAndChapter(book.bookNumber, book.firstChapterWithDraft)"
[queryParams]="{ 'draft-active': true }"
>
{{ bookNumberToName(book.bookNumber) }}
</a>
} @empty { <strong>{{ t("no_books_have_drafts") }}</strong> }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import { AngularSplitModule } from 'angular-split';
import { merge } from 'lodash-es';
import cloneDeep from 'lodash-es/cloneDeep';
import { CookieService } from 'ngx-cookie-service';
import { TranslocoMarkupModule } from 'ngx-transloco-markup';
import Quill, { DeltaOperation, DeltaStatic, RangeStatic, Sources, StringMap } from 'quill';
import { User } from 'realtime-server/lib/esm/common/models/user';
import { createTestUser } from 'realtime-server/lib/esm/common/models/user-test-data';
Expand Down Expand Up @@ -59,7 +60,7 @@ import { TextType } from 'realtime-server/lib/esm/scriptureforge/models/text-dat
import { TextInfoPermission } from 'realtime-server/lib/esm/scriptureforge/models/text-info-permission';
import { fromVerseRef } from 'realtime-server/lib/esm/scriptureforge/models/verse-ref-data';
import * as RichText from 'rich-text';
import { BehaviorSubject, defer, Observable, of, Subject } from 'rxjs';
import { BehaviorSubject, defer, Observable, of, Subject, take } from 'rxjs';
import { anything, capture, deepEqual, instance, mock, resetCalls, verify, when } from 'ts-mockito';
import { ActivatedProjectService } from 'xforge-common/activated-project.service';
import { AuthService } from 'xforge-common/auth.service';
Expand All @@ -76,7 +77,6 @@ import { TestRealtimeService } from 'xforge-common/test-realtime.service';
import { configureTestingModule, TestTranslocoModule } from 'xforge-common/test-utils';
import { UICommonModule } from 'xforge-common/ui-common.module';
import { UserService } from 'xforge-common/user.service';
import { TranslocoMarkupModule } from 'ngx-transloco-markup';
import { BiblicalTermDoc } from '../../core/models/biblical-term-doc';
import { NoteThreadDoc } from '../../core/models/note-thread-doc';
import { SFProjectDoc } from '../../core/models/sf-project-doc';
Expand All @@ -97,14 +97,14 @@ import { XmlUtils } from '../../shared/utils';
import { BiblicalTermsComponent } from '../biblical-terms/biblical-terms.component';
import { DraftGenerationService } from '../draft-generation/draft-generation.service';
import { TrainingProgressComponent } from '../training-progress/training-progress.component';
import { EditorDraftComponent } from './editor-draft/editor-draft.component';
import { HistoryChooserComponent } from './editor-history/history-chooser/history-chooser.component';
import { EditorComponent, UPDATE_SUGGESTIONS_TIMEOUT } from './editor.component';
import { NoteDialogComponent, NoteDialogData, NoteDialogResult } from './note-dialog/note-dialog.component';
import { SuggestionsComponent } from './suggestions.component';
import { EditorTabFactoryService } from './tabs/editor-tab-factory.service';
import { EditorTabMenuService } from './tabs/editor-tab-menu.service';
import { ACTIVE_EDIT_TIMEOUT } from './translate-metrics-session';
import { EditorDraftComponent } from './editor-draft/editor-draft.component';

const mockedAuthService = mock(AuthService);
const mockedSFProjectService = mock(SFProjectService);
Expand Down Expand Up @@ -3767,7 +3767,10 @@ describe('EditorComponent', () => {
tick(100);
expect(spyCreateTab).toHaveBeenCalledWith('project', { headerText: targetLabel });
}));
});

describe('updateAutoDraftTabVisibility', () => {
beforeEach(() => {});
it('should add auto draft tab when available', fakeAsync(() => {
const env = new TestEnvironment();
env.wait();
Expand Down Expand Up @@ -3797,6 +3800,46 @@ describe('EditorComponent', () => {

env.dispose();
}));

it('should not add draft tab if draft exists and draft tab is already present', fakeAsync(() => {
const env = new TestEnvironment();
env.wait();

env.component.tabState.addTab('target', env.tabFactory.createTab('draft'));
const addTab = spyOn(env.component.tabState, 'addTab');

env.routeWithParams({ projectId: 'project01', bookId: 'LUK', chapter: '1' });
env.wait();

expect(addTab).not.toHaveBeenCalled();
env.dispose();
}));

it('should select the draft tab if url query param is set', fakeAsync(() => {
const env = new TestEnvironment();
when(mockedActivatedRoute.snapshot).thenReturn({ queryParams: { 'draft-active': 'true' } } as any);
env.wait();
env.routeWithParams({ projectId: 'project01', bookId: 'LUK', chapter: '1' });
env.wait();

env.component.tabState.tabs$.pipe(take(1)).subscribe(tabs => {
expect(tabs.find(tab => tab.type === 'draft')?.isSelected).toBe(true);
env.dispose();
});
}));

it('should not select the draft tab if url query param is not set', fakeAsync(() => {
const env = new TestEnvironment();
when(mockedActivatedRoute.snapshot).thenReturn({ queryParams: {} } as any);
env.wait();
env.routeWithParams({ projectId: 'project01', bookId: 'LUK', chapter: '1' });
env.wait();

env.component.tabState.tabs$.pipe(take(1)).subscribe(tabs => {
expect(tabs.find(tab => tab.type === 'draft')?.isSelected).toBe(false);
env.dispose();
});
}));
});
});

Expand Down Expand Up @@ -3998,6 +4041,7 @@ class TestEnvironment {
this.addEmptyTextDoc(new TextDocId('project01', 43, 1, 'target'));

when(mockedActivatedRoute.params).thenReturn(this.params$);
when(mockedActivatedRoute.snapshot).thenReturn({ queryParams: {} } as any);
this.setupUsers();
this.setCurrentUser('user01');
when(mockedTranslationEngineService.createTranslationEngine('project01')).thenReturn(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ import { SF_PROJECT_RIGHTS, SFProjectDomain } from 'realtime-server/lib/esm/scri
import { SFProjectRole } from 'realtime-server/lib/esm/scriptureforge/models/sf-project-role';
import { TextAnchor } from 'realtime-server/lib/esm/scriptureforge/models/text-anchor';
import { TextType } from 'realtime-server/lib/esm/scriptureforge/models/text-data';
import { TextInfo } from 'realtime-server/lib/esm/scriptureforge/models/text-info';
import { Chapter, TextInfo } from 'realtime-server/lib/esm/scriptureforge/models/text-info';
import { TextInfoPermission } from 'realtime-server/lib/esm/scriptureforge/models/text-info-permission';
import { fromVerseRef } from 'realtime-server/lib/esm/scriptureforge/models/verse-ref-data';
import { DeltaOperation } from 'rich-text';
Expand Down Expand Up @@ -93,7 +93,6 @@ import { UserService } from 'xforge-common/user.service';
import { filterNullish } from 'xforge-common/util/rxjs-util';
import { getLinkHTML, issuesEmailTemplate, objectId } from 'xforge-common/utils';
import { XFValidators } from 'xforge-common/xfvalidators';
import { Chapter } from 'realtime-server/lib/esm/scriptureforge/models/text-info';
import { environment } from '../../../environments/environment';
import { defaultNoteThreadIcon, NoteThreadDoc, NoteThreadIcon } from '../../core/models/note-thread-doc';
import { SFProjectDoc } from '../../core/models/sf-project-doc';
Expand Down Expand Up @@ -1307,19 +1306,38 @@ export class EditorComponent extends DataLoadingComponent implements OnDestroy,
}

private updateAutoDraftTabVisibility(): void {
const sourceTabGroup = this.tabState.getTabGroup('source');
if (sourceTabGroup == null) {
return;
}

const chapter: Chapter | undefined = this.text?.chapters.find(c => c.number === this._chapter);
const hasDraft: boolean = chapter?.hasDraft ?? false;
const hasDraftTab: boolean = this.tabState.hasTab('source', 'draft');
const existingDraftTab: { groupId: EditorTabGroupType; index: number } | undefined =
this.tabState.getFirstTabOfTypeIndex('draft');

if (hasDraft && !hasDraftTab) {
sourceTabGroup.addTab(this.editorTabFactory.createTab('draft'), false);
} else if (!hasDraft && hasDraftTab) {
sourceTabGroup.removeTab(this.tabState.getFirstTabOfTypeIndex('source', 'draft')!);
if (hasDraft) {
// URL may indicate to select the 'draft' tab (such as when coming from generate draft page)
const urlDraftActive: boolean = this.activatedRoute.snapshot.queryParams['draft-active'] === 'true';

// Add to 'source' tab group if no draft tab
if (existingDraftTab == null) {
this.tabState.addTab('source', this.editorTabFactory.createTab('draft'), urlDraftActive);
}

if (urlDraftActive) {
// Remove 'draft-active' query string from url when another tab from 'source' is selected
this.tabState.tabs$
.pipe(
filter(tabs => tabs.some(tab => tab.groupId === 'source' && tab.type !== 'draft' && tab.isSelected)),
take(1)
)
.subscribe(() => {
this.router.navigate([], {
queryParams: { 'draft-active': null },
queryParamsHandling: 'merge',
replaceUrl: true
});
});
}
} else if (existingDraftTab != null) {
// No draft for chapter, so remove the draft tab
this.tabState.removeTab(existingDraftTab.groupId, existingDraftTab.index);
}
}

Expand Down

0 comments on commit d4621d0

Please sign in to comment.