Skip to content

Commit 0a8c62d

Browse files
committed
SF-3613 Allow old drafts to open draft tab without formatting options
1 parent 0c0df6f commit 0a8c62d

File tree

7 files changed

+90
-36
lines changed

7 files changed

+90
-36
lines changed

src/SIL.XForge.Scripture/ClientApp/src/app/translate/draft-generation/draft-history-list/draft-history-entry/draft-history-entry.component.spec.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ describe('DraftHistoryEntryComponent', () => {
8383
when(mockedTrainingDataService.queryTrainingDataAsync(anything(), anything())).thenResolve(
8484
instance(trainingDataQuery)
8585
);
86-
when(mockedDraftOptionsService.areFormattingOptionsAvailableButUnselected()).thenReturn(true);
86+
when(mockedDraftOptionsService.areFormattingOptionsAvailableButUnselected(anything())).thenReturn(true);
8787
fixture = TestBed.createComponent(DraftHistoryEntryComponent);
8888
component = fixture.componentInstance;
8989
fixture.detectChanges();
@@ -168,7 +168,7 @@ describe('DraftHistoryEntryComponent', () => {
168168
}));
169169

170170
it('should show the USFM format option when the project is the latest draft', fakeAsync(() => {
171-
when(mockedDraftOptionsService.areFormattingOptionsAvailableButUnselected()).thenReturn(false);
171+
when(mockedDraftOptionsService.areFormattingOptionsAvailableButUnselected(anything())).thenReturn(false);
172172
when(mockedDraftOptionsService.areFormattingOptionsSupportedForBuild(anything())).thenReturn(true);
173173
const user = 'user-display-name';
174174
const date = dateAfterFormattingSupported;
@@ -241,7 +241,7 @@ describe('DraftHistoryEntryComponent', () => {
241241
}));
242242

243243
it('should handle builds with additional info referencing a deleted user', fakeAsync(() => {
244-
when(mockedDraftOptionsService.areFormattingOptionsAvailableButUnselected()).thenReturn(false);
244+
when(mockedDraftOptionsService.areFormattingOptionsAvailableButUnselected(anything())).thenReturn(false);
245245
when(mockedDraftOptionsService.areFormattingOptionsSupportedForBuild(anything())).thenReturn(true);
246246
when(mockedI18nService.formatDate(anything())).thenReturn('formatted-date');
247247
when(mockedI18nService.formatAndLocalizeScriptureRange('GEN')).thenReturn('Genesis');
@@ -353,7 +353,7 @@ describe('DraftHistoryEntryComponent', () => {
353353
});
354354

355355
it('should show set draft format UI', fakeAsync(() => {
356-
when(mockedDraftOptionsService.areFormattingOptionsAvailableButUnselected()).thenReturn(false);
356+
when(mockedDraftOptionsService.areFormattingOptionsAvailableButUnselected(anything())).thenReturn(false);
357357
when(mockedDraftOptionsService.areFormattingOptionsSupportedForBuild(anything())).thenReturn(true);
358358
const date = dateAfterFormattingSupported;
359359
component.entry = {

src/SIL.XForge.Scripture/ClientApp/src/app/translate/draft-generation/draft-options.service.spec.ts

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,16 @@ describe('DraftOptionsService', () => {
4545
return doc;
4646
}
4747

48+
function buildDtoWithDate(date: Date): BuildDto {
49+
return {
50+
additionalInfo: {
51+
dateFinished: date.toJSON()
52+
}
53+
} as BuildDto;
54+
}
55+
56+
const SUPPORTED_BUILD_ENTRY: BuildDto = buildDtoWithDate(new Date(FORMATTING_OPTIONS_SUPPORTED_DATE.getTime() + 1));
57+
4858
const PROJECT_DOC_BOTH_FORMATS: SFProjectProfileDoc = buildProjectDoc({
4959
paragraphFormat: ParagraphBreakFormat.BestGuess,
5060
quoteFormat: QuoteFormat.Normalized
@@ -83,23 +93,28 @@ describe('DraftOptionsService', () => {
8393
describe('areFormattingOptionsAvailableButUnselected', () => {
8494
it('returns true when flag enabled and both options missing', () => {
8595
when(mockedActivatedProject.projectDoc).thenReturn(PROJECT_DOC_EMPTY_USFM);
86-
expect(service.areFormattingOptionsAvailableButUnselected()).toBe(true);
96+
expect(service.areFormattingOptionsAvailableButUnselected(SUPPORTED_BUILD_ENTRY)).toBe(true);
8797
});
8898

8999
it('returns true when flag enabled and one option missing', () => {
90100
when(mockedActivatedProject.projectDoc).thenReturn(PROJECT_DOC_QUOTE_ONLY);
91-
expect(service.areFormattingOptionsAvailableButUnselected()).toBe(true);
101+
expect(service.areFormattingOptionsAvailableButUnselected(SUPPORTED_BUILD_ENTRY)).toBe(true);
92102
});
93103

94104
it('returns false when flag enabled and both options set', () => {
95105
when(mockedActivatedProject.projectDoc).thenReturn(PROJECT_DOC_BOTH_FORMATS);
96-
expect(service.areFormattingOptionsAvailableButUnselected()).toBe(false);
106+
expect(service.areFormattingOptionsAvailableButUnselected(SUPPORTED_BUILD_ENTRY)).toBe(false);
97107
});
98108

99109
it('returns false when flag disabled', () => {
100110
when(mockedFeatureFlagService.usfmFormat).thenReturn(createTestFeatureFlag(false));
101111
when(mockedActivatedProject.projectDoc).thenReturn(PROJECT_DOC_EMPTY_USFM);
102-
expect(service.areFormattingOptionsAvailableButUnselected()).toBe(false);
112+
expect(service.areFormattingOptionsAvailableButUnselected(SUPPORTED_BUILD_ENTRY)).toBe(false);
113+
});
114+
115+
it('returns false when build entry unavailable', () => {
116+
when(mockedActivatedProject.projectDoc).thenReturn(PROJECT_DOC_EMPTY_USFM);
117+
expect(service.areFormattingOptionsAvailableButUnselected(undefined)).toBe(false);
103118
});
104119
});
105120

@@ -109,7 +124,7 @@ describe('DraftOptionsService', () => {
109124
if (date == null) {
110125
return { additionalInfo: {} } as BuildDto;
111126
}
112-
return { additionalInfo: { dateFinished: date.toJSON() } } as BuildDto;
127+
return buildDtoWithDate(date);
113128
}
114129

115130
it('returns true when flag enabled and date after supported date', () => {

src/SIL.XForge.Scripture/ClientApp/src/app/translate/draft-generation/draft-options.service.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -23,12 +23,12 @@ export class DraftOptionsService {
2323
);
2424
}
2525

26-
areFormattingOptionsAvailableButUnselected(): boolean {
27-
return (
28-
this.featureFlags.usfmFormat.enabled &&
29-
(this.activatedProjectService.projectDoc?.data?.translateConfig.draftConfig.usfmConfig?.paragraphFormat == null ||
30-
this.activatedProjectService.projectDoc?.data?.translateConfig.draftConfig.usfmConfig?.quoteFormat == null)
31-
);
26+
areFormattingOptionsAvailableButUnselected(draftBuild: BuildDto | undefined): boolean {
27+
const usfmConfig = this.activatedProjectService.projectDoc?.data?.translateConfig.draftConfig.usfmConfig;
28+
const optionsSelected = usfmConfig?.paragraphFormat != null && usfmConfig?.quoteFormat != null;
29+
30+
const available = this.featureFlags.usfmFormat.enabled && this.areFormattingOptionsSupportedForBuild(draftBuild);
31+
return available && !optionsSelected;
3232
}
3333

3434
areFormattingOptionsSupportedForBuild(entry: BuildDto | undefined): boolean {

src/SIL.XForge.Scripture/ClientApp/src/app/translate/editor/editor.component.spec.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4111,8 +4111,8 @@ describe('EditorComponent', () => {
41114111
Object.defineProperty(env.component, 'showSource', { get: () => true });
41124112
});
41134113
env.setupProject({ translateConfig: { draftConfig: {} } });
4114-
// Formatting options not selected, so draft tab should not be shown
4115-
when(mockedDraftOptionsService.areFormattingOptionsAvailableButUnselected()).thenReturn(true);
4114+
// Formatting options not selected, so draft tab should be blocked
4115+
when(mockedDraftOptionsService.areFormattingOptionsAvailableButUnselected(anything())).thenReturn(true);
41164116
when(mockedPermissionsService.canAccessDrafts(anything(), anything())).thenReturn(true);
41174117
env.wait();
41184118
env.routeWithParams({ projectId: 'project01', bookId: 'LUK', chapter: '1' });
@@ -4903,6 +4903,8 @@ class TestEnvironment {
49034903
).thenReturn(of([]));
49044904
when(mockedDraftGenerationService.getGeneratedDraftHistory(anything(), anything(), anything())).thenReturn(of([]));
49054905
when(mockedDraftGenerationService.draftExists(anything(), anything(), anything())).thenReturn(of(true));
4906+
when(mockedDraftGenerationService.getLastCompletedBuild(anything())).thenReturn(of({} as BuildDto));
4907+
when(mockedDraftOptionsService.areFormattingOptionsAvailableButUnselected(anything())).thenReturn(false);
49064908
when(mockedPermissionsService.isUserOnProject(anything())).thenResolve(true);
49074909
when(mockedFeatureFlagService.newDraftHistory).thenReturn(createTestFeatureFlag(false));
49084910
when(mockedLynxWorkspaceService.rawInsightSource$).thenReturn(of([]));

src/SIL.XForge.Scripture/ClientApp/src/app/translate/editor/editor.component.ts

Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1490,20 +1490,25 @@ export class EditorComponent extends DataLoadingComponent implements OnDestroy,
14901490
this.projectDoc,
14911491
this.userService.currentUserId
14921492
);
1493-
if (
1494-
((hasDraft && !draftApplied) || urlDraftActive) &&
1495-
canViewDrafts &&
1496-
!this.draftOptionsService.areFormattingOptionsAvailableButUnselected()
1497-
) {
1493+
1494+
const hasUnappliedDraft: boolean = hasDraft && !draftApplied;
1495+
const draftShouldBeVisible: boolean = hasUnappliedDraft || urlDraftActive;
1496+
1497+
let draftBuild: BuildDto | undefined;
1498+
// Only try to fetch the draft build if existing information indicates we should show the draft
1499+
if (draftShouldBeVisible && canViewDrafts && this.projectId != null) {
1500+
draftBuild = await firstValueFrom(this.draftGenerationService.getLastCompletedBuild(this.projectId));
1501+
}
1502+
1503+
const isBlockedByFormattingOptions: boolean =
1504+
this.draftOptionsService.areFormattingOptionsAvailableButUnselected(draftBuild);
1505+
1506+
if (draftShouldBeVisible && canViewDrafts && draftBuild != null && !isBlockedByFormattingOptions) {
14981507
// URL may indicate to select the 'draft' tab (such as when coming from generate draft page)
14991508
const groupIdToAddTo: EditorTabGroupType = this.showSource ? 'source' : 'target';
15001509

15011510
// Add to 'source' (or 'target' if showSource is false) tab group if no existing draft tab
15021511
if (existingDraftTab == null) {
1503-
const draftBuild: BuildDto | undefined = await firstValueFrom(
1504-
this.draftGenerationService.getLastCompletedBuild(this.projectId!)
1505-
);
1506-
15071512
this.tabState.addTab(
15081513
groupIdToAddTo,
15091514
this.editorTabFactory.createTab('draft', {

src/SIL.XForge.Scripture/ClientApp/src/app/translate/editor/tabs/editor-tab-menu.service.spec.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import { SFProjectProfileDoc } from '../../../core/models/sf-project-profile-doc
1616
import { SF_TYPE_REGISTRY } from '../../../core/models/sf-type-registry';
1717
import { PermissionsService } from '../../../core/permissions.service';
1818
import { TabStateService } from '../../../shared/sf-tab-group';
19+
import { DraftGenerationService } from '../../draft-generation/draft-generation.service';
1920
import { DraftOptionsService } from '../../draft-generation/draft-options.service';
2021
import { EditorTabMenuService } from './editor-tab-menu.service';
2122
import { EditorTabInfo } from './editor-tabs.types';
@@ -26,6 +27,7 @@ const tabStateMock: TabStateService<any, any> = mock(TabStateService);
2627
const mockUserService = mock(UserService);
2728
const mockPermissionsService = mock(PermissionsService);
2829
const mockDraftOptionsService = mock(DraftOptionsService);
30+
const mockDraftGenerationService = mock(DraftGenerationService);
2931

3032
describe('EditorTabMenuService', () => {
3133
configureTestingModule(() => ({
@@ -37,7 +39,8 @@ describe('EditorTabMenuService', () => {
3739
{ provide: UserService, useMock: mockUserService },
3840
{ provide: PermissionsService, useMock: mockPermissionsService },
3941
{ provide: OnlineStatusService, useClass: TestOnlineStatusService },
40-
{ provide: DraftOptionsService, useMock: mockDraftOptionsService }
42+
{ provide: DraftOptionsService, useMock: mockDraftOptionsService },
43+
{ provide: DraftGenerationService, useMock: mockDraftGenerationService }
4144
]
4245
}));
4346

@@ -253,7 +256,7 @@ describe('EditorTabMenuService', () => {
253256
it('should not show draft menu item when draft formatting (usfmConfig) is not set', done => {
254257
const env = new TestEnvironment();
255258
// Simulate formatting options available but still unselected, so draft tab should be hidden
256-
when(mockDraftOptionsService.areFormattingOptionsAvailableButUnselected()).thenReturn(true);
259+
when(mockDraftOptionsService.areFormattingOptionsAvailableButUnselected(anything())).thenReturn(true);
257260
env.setExistingTabs([]);
258261
service['canShowHistory'] = () => true;
259262
service['canShowResource'] = () => true;
@@ -307,6 +310,10 @@ class TestEnvironment {
307310
constructor(explicitProjectDoc?: SFProjectProfileDoc) {
308311
const projectDoc: SFProjectProfileDoc = explicitProjectDoc ?? this.projectDoc;
309312
when(activatedProjectMock.projectDoc$).thenReturn(of(projectDoc));
313+
when(activatedProjectMock.projectId$).thenReturn(of(projectDoc.id));
314+
when(tabStateMock.tabs$).thenReturn(of([] as EditorTabInfo[]));
315+
when(mockDraftGenerationService.getLastCompletedBuild(anything())).thenReturn(of(undefined));
316+
when(mockDraftOptionsService.areFormattingOptionsAvailableButUnselected(anything())).thenReturn(false);
310317
when(mockUserService.currentUserId).thenReturn('user01');
311318
when(mockPermissionsService.canAccessDrafts(anything(), anything())).thenReturn(true);
312319
service = TestBed.inject(EditorTabMenuService);

src/SIL.XForge.Scripture/ClientApp/src/app/translate/editor/tabs/editor-tab-menu.service.ts

Lines changed: 33 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,18 @@ import { SFProjectProfileDoc } from '../../../core/models/sf-project-profile-doc
1616
import { ParatextService } from '../../../core/paratext.service';
1717
import { PermissionsService } from '../../../core/permissions.service';
1818
import { SFProjectService } from '../../../core/sf-project.service';
19+
import { BuildDto } from '../../../machine-api/build-dto';
1920
import { TabMenuItem, TabMenuService, TabStateService } from '../../../shared/sf-tab-group';
21+
import { FlatTabInfo } from '../../../shared/sf-tab-group/tab-state/tab-state.service';
22+
import { DraftGenerationService } from '../../draft-generation/draft-generation.service';
2023
import { DraftOptionsService } from '../../draft-generation/draft-options.service';
2124
import { EditorTabInfo } from './editor-tabs.types';
25+
2226
@Injectable()
2327
export class EditorTabMenuService implements TabMenuService<EditorTabGroupType> {
24-
private readonly menuItems$: Observable<TabMenuItem[]> = this.initMenuItems();
28+
private readonly menuItems$: Observable<TabMenuItem[]>;
29+
// TODO: Detect when a new draft build is available so we can update the latest build
30+
private readonly latestDraftBuild$: Observable<BuildDto | undefined>;
2531

2632
constructor(
2733
private readonly destroyRef: DestroyRef,
@@ -31,30 +37,49 @@ export class EditorTabMenuService implements TabMenuService<EditorTabGroupType>
3137
private readonly tabState: TabStateService<EditorTabGroupType, EditorTabInfo>,
3238
private readonly permissionsService: PermissionsService,
3339
private readonly i18n: I18nService,
34-
private readonly draftOptionsService: DraftOptionsService
35-
) {}
40+
private readonly draftOptionsService: DraftOptionsService,
41+
private readonly draftGenerationService: DraftGenerationService
42+
) {
43+
this.latestDraftBuild$ = this.initLatestDraftBuild();
44+
this.menuItems$ = this.initMenuItems();
45+
}
3646

3747
getMenuItems(): Observable<TabMenuItem[]> {
3848
// Return the same menu items for all tab groups
3949
return this.menuItems$;
4050
}
4151

52+
private initLatestDraftBuild(): Observable<BuildDto | undefined> {
53+
return this.activatedProject.projectId$.pipe(
54+
switchMap(projectId => {
55+
if (projectId == null) {
56+
return of(undefined);
57+
}
58+
59+
const build$ = this.draftGenerationService.getLastCompletedBuild(projectId);
60+
return build$ == null ? of(undefined) : build$;
61+
})
62+
);
63+
}
64+
4265
private initMenuItems(): Observable<TabMenuItem[]> {
4366
return combineLatest([
4467
this.activatedProject.projectDoc$.pipe(filterNullish()),
45-
this.onlineStatus.onlineStatus$
68+
this.onlineStatus.onlineStatus$,
69+
this.latestDraftBuild$
4670
]).pipe(
4771
quietTakeUntilDestroyed(this.destroyRef),
48-
switchMap(([projectDoc, isOnline]) => {
49-
return combineLatest([of(projectDoc), of(isOnline), this.tabState.tabs$]);
72+
switchMap(([projectDoc, isOnline, latestDraftBuild]) => {
73+
const tabs$: Observable<FlatTabInfo<EditorTabGroupType, EditorTabInfo>[]> = this.tabState.tabs$ ?? of([]);
74+
return combineLatest([of(projectDoc), of(isOnline), tabs$, of(latestDraftBuild)]);
5075
}),
51-
switchMap(([projectDoc, isOnline, existingTabs]) => {
76+
switchMap(([projectDoc, isOnline, existingTabs, latestDraftBuild]) => {
5277
const showDraft =
5378
isOnline &&
5479
projectDoc.data != null &&
5580
SFProjectService.hasDraft(projectDoc.data) &&
5681
this.permissionsService.canAccessDrafts(projectDoc, this.userService.currentUserId) &&
57-
!this.draftOptionsService.areFormattingOptionsAvailableButUnselected();
82+
!this.draftOptionsService.areFormattingOptionsAvailableButUnselected(latestDraftBuild);
5883
const items: Observable<TabMenuItem>[] = [];
5984

6085
for (const tabType of editorTabTypes) {

0 commit comments

Comments
 (0)