Skip to content

Commit 2ec0029

Browse files
SF-3633 Add draft import wizard (#3638)
Co-authored-by: Nateowami <nathaniel_paulus@sil.org>
1 parent ecdcda0 commit 2ec0029

File tree

77 files changed

+3012
-2149
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

77 files changed

+3012
-2149
lines changed

src/RealtimeServer/common/index.ts

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -210,7 +210,8 @@ export = {
210210
collection: string,
211211
id: string,
212212
data: any,
213-
typeName: OTType
213+
typeName: OTType,
214+
source: string | undefined
214215
): void => {
215216
if (server == null) {
216217
callback(new Error('Server not started.'));
@@ -221,7 +222,17 @@ export = {
221222
callback(new Error('Connection not found.'));
222223
return;
223224
}
224-
doc.create(data, typeName, err => callback(err, createSnapshot(doc)));
225+
const options: any = {};
226+
doc.submitSource = source != null;
227+
if (source != null) {
228+
options.source = source;
229+
}
230+
doc.create(data, typeName, options, err => {
231+
if (source != null) {
232+
doc.submitSource = false;
233+
}
234+
callback(err, createSnapshot(doc));
235+
});
225236
},
226237

227238
fetchDoc: (callback: InteropCallback, handle: number, collection: string, id: string): void => {

src/RealtimeServer/common/utils/sharedb-utils.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,15 @@ export function docFetch(doc: Doc): Promise<void> {
1313
});
1414
}
1515

16-
export function docCreate(doc: Doc, data: any, type?: OTType): Promise<void> {
16+
export function docCreate(
17+
doc: Doc,
18+
data: any,
19+
type?: OTType,
20+
source: boolean | any | undefined = undefined
21+
): Promise<void> {
22+
const options: ShareDBSourceOptions = source != null ? { source } : {};
1723
return new Promise<void>((resolve, reject) => {
18-
doc.create(data, type, err => {
24+
doc.create(data, type, options, err => {
1925
if (err != null) {
2026
reject(err);
2127
} else {

src/RealtimeServer/common/utils/test-utils.ts

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,19 @@ export async function hasDoc(conn: Connection, collection: string, id: string):
2020
return doc.data != null;
2121
}
2222

23-
export function createDoc<T>(conn: Connection, collection: string, id: string, data: T, type?: OTType): Promise<void> {
24-
return docCreate(conn.get(collection, id), data, type);
23+
export function createDoc<T>(
24+
conn: Connection,
25+
collection: string,
26+
id: string,
27+
data: T,
28+
type?: OTType,
29+
source: boolean | any | undefined = undefined
30+
): Promise<void> {
31+
const doc = conn.get(collection, id);
32+
if (source != null) {
33+
doc.submitSource = true;
34+
}
35+
return docCreate(doc, data, type, source);
2536
}
2637

2738
export async function submitOp(

src/RealtimeServer/scriptureforge/services/text-service.spec.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,22 @@ describe('TextService', () => {
7575
});
7676
});
7777
});
78+
79+
it('writes the op source to the database on create', async () => {
80+
const env = new TestEnvironment();
81+
await env.createData();
82+
83+
const conn = clientConnect(env.server, 'administrator');
84+
const id: string = getTextDocId('project01', 40, 2);
85+
const source: string = 'history';
86+
await createDoc<TextData>(conn, TEXTS_COLLECTION, id, new Delta(), 'rich-text', source);
87+
await new Promise<void>(resolve => {
88+
env.db.getOps(TEXTS_COLLECTION, id, 0, null, { metadata: true }, (_, ops) => {
89+
expect(ops[0].m.source).toBe(source);
90+
resolve();
91+
});
92+
});
93+
});
7894
});
7995

8096
class TestEnvironment {

src/SIL.XForge.Scripture/ClientApp/e2e/workflows/generate-draft.ts

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { expect } from 'npm:@playwright/test';
22
import { Locator, Page } from 'npm:playwright';
3-
import { preset, ScreenshotContext } from '../e2e-globals.ts';
3+
import { E2E_SYNC_DEFAULT_TIMEOUT, preset, ScreenshotContext } from '../e2e-globals.ts';
44
import {
55
enableDeveloperMode,
66
enableDraftingOnProjectAsServalAdmin,
@@ -203,21 +203,27 @@ export async function generateDraft(
203203
await user.click(page.getByRole('button', { name: 'Save' }));
204204

205205
// Preview and apply chapter 1
206-
await user.click(page.getByRole('radio', { name: bookToDraft }));
206+
await user.click(page.getByRole('button', { name: bookToDraft, exact: true }));
207207
await user.click(page.getByRole('button', { name: 'Add to project' }));
208208
await user.click(page.getByRole('button', { name: 'Overwrite chapter' }));
209209
await user.click(page.locator('app-tab-header').filter({ hasText: DRAFT_PROJECT_SHORT_NAME }));
210210

211211
// Go back to generate draft page and apply all chapters
212212
await user.click(page.getByRole('link', { name: 'Generate draft' }));
213-
await user.click(page.locator('app-draft-preview-books mat-button-toggle:last-child button'));
214-
await user.click(page.getByRole('menuitem', { name: 'Add to project' }));
215-
await user.check(page.getByRole('checkbox', { name: /I understand the draft will overwrite .* in .* project/ }));
216213
await user.click(page.getByRole('button', { name: 'Add to project' }));
217-
await expect(
218-
page.getByRole('heading', { name: `Successfully applied all chapters to ${bookToDraft}` })
219-
).toBeVisible();
220-
await user.click(page.getByRole('button', { name: 'Close' }));
214+
await user.click(page.getByRole('combobox', { name: 'Choose a project' }));
215+
await user.type(DRAFT_PROJECT_SHORT_NAME);
216+
await user.click(page.getByRole('option', { name: `${DRAFT_PROJECT_SHORT_NAME} -` }));
217+
await user.click(page.getByRole('button', { name: 'Next' }));
218+
await user.check(page.getByRole('checkbox', { name: /I understand that existing content will be overwritten/ }));
219+
await user.click(page.getByRole('button', { name: 'Import' }));
220+
await expect(page.getByText('Import complete', { exact: true })).toBeVisible();
221+
await user.click(page.getByRole('button', { name: 'Next' }));
222+
await user.click(page.locator('[data-test-id="step-7-sync"]'));
223+
await expect(page.getByText(`The draft has been imported into ${DRAFT_PROJECT_SHORT_NAME}`)).toBeVisible({
224+
timeout: E2E_SYNC_DEFAULT_TIMEOUT
225+
});
226+
await user.click(page.getByRole('button', { name: 'Done' }));
221227

222228
await screenshot(page, { pageName: 'generate_draft_add_to_project', ...context });
223229

src/SIL.XForge.Scripture/ClientApp/e2e/workflows/localized-screenshots.ts

Lines changed: 6 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -432,11 +432,11 @@ export async function localizedScreenshots(
432432
await user.click(page.getByRole('button', { name: 'Save' }));
433433

434434
await forEachLocale(async locale => {
435-
await user.hover(page.getByRole('radio').first(), defaultArrowLocation);
435+
await user.hover(page.getByRole('button', { name: 'Ruth', exact: true }), defaultArrowLocation);
436436
await screenshot(page, { ...context, pageName: 'draft_complete', locale });
437437
});
438438

439-
await page.getByRole('radio', { name: 'Ruth' }).first().click();
439+
await user.click(page.getByRole('button', { name: 'Ruth', exact: true }));
440440

441441
await expect(page.getByRole('button', { name: 'Add to project' })).toBeVisible({ timeout: 15_000 });
442442

@@ -461,19 +461,16 @@ export async function localizedScreenshots(
461461
await expect(page.getByText('The draft is ready')).toBeVisible();
462462

463463
await forEachLocale(async locale => {
464-
await page.getByRole('radio').nth(1).click();
465-
await user.hover(page.getByRole('menuitem').last(), defaultArrowLocation);
464+
await user.hover(page.getByRole('button', { name: 'Add to project' }), defaultArrowLocation);
466465
await screenshot(page, { ...context, pageName: 'import_book', locale });
467-
await page.keyboard.press('Escape');
468466
});
469467

470468
await forEachLocale(async locale => {
471-
await page.getByRole('radio').nth(1).click();
472-
await page.getByRole('menuitem').last().click();
469+
await user.click(page.getByRole('button', { name: 'Add to project' }));
470+
473471
await page.getByRole('combobox').fill('seedsp2');
474472
await page.getByRole('option', { name: 'seedsp2 - ' }).click();
475-
await page.getByRole('checkbox').check();
476-
await user.hover(page.getByRole('button').last(), defaultArrowLocation);
473+
await user.hover(page.getByRole('button', { name: 'next' }), defaultArrowLocation);
477474
await screenshotElements(
478475
page,
479476
[page.locator('mat-dialog-container')],

src/SIL.XForge.Scripture/ClientApp/src/app/core/project-notification.service.ts

Lines changed: 37 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
import { Injectable } from '@angular/core';
2-
import { AbortError, HubConnection, HubConnectionBuilder, IHttpConnectionOptions } from '@microsoft/signalr';
2+
import {
3+
AbortError,
4+
HubConnection,
5+
HubConnectionBuilder,
6+
HubConnectionState,
7+
IHttpConnectionOptions
8+
} from '@microsoft/signalr';
39
import { AuthService } from 'xforge-common/auth.service';
410
import { OnlineStatusService } from 'xforge-common/online-status.service';
511

@@ -11,6 +17,7 @@ export class ProjectNotificationService {
1117
private options: IHttpConnectionOptions = {
1218
accessTokenFactory: async () => (await this.authService.getAccessToken()) ?? ''
1319
};
20+
private openConnections: number = 0;
1421

1522
constructor(
1623
private authService: AuthService,
@@ -30,6 +37,10 @@ export class ProjectNotificationService {
3037
this.connection.off('notifyBuildProgress', handler);
3138
}
3239

40+
removeNotifyDraftApplyProgressHandler(handler: any): void {
41+
this.connection.off('notifyDraftApplyProgress', handler);
42+
}
43+
3344
removeNotifySyncProgressHandler(handler: any): void {
3445
this.connection.off('notifySyncProgress', handler);
3546
}
@@ -38,25 +49,39 @@ export class ProjectNotificationService {
3849
this.connection.on('notifyBuildProgress', handler);
3950
}
4051

52+
setNotifyDraftApplyProgressHandler(handler: any): void {
53+
this.connection.on('notifyDraftApplyProgress', handler);
54+
}
55+
4156
setNotifySyncProgressHandler(handler: any): void {
4257
this.connection.on('notifySyncProgress', handler);
4358
}
4459

4560
async start(): Promise<void> {
46-
await this.connection.start().catch(err => {
47-
// Suppress AbortErrors, as they are not caused by server error, but the SignalR connection state
48-
// These will be thrown if a user navigates away quickly after
49-
// starting the sync or the app loses internet connection
50-
if (err instanceof AbortError || !this.appOnline) {
51-
return;
52-
} else {
53-
throw err;
54-
}
55-
});
61+
this.openConnections++;
62+
if (
63+
this.connection.state !== HubConnectionState.Connected &&
64+
this.connection.state !== HubConnectionState.Connecting &&
65+
this.connection.state !== HubConnectionState.Reconnecting
66+
) {
67+
await this.connection.start().catch(err => {
68+
// Suppress AbortErrors, as they are not caused by server error, but the SignalR connection state
69+
// These will be thrown if a user navigates away quickly after
70+
// starting the sync or the app loses internet connection
71+
if (err instanceof AbortError || !this.appOnline) {
72+
return;
73+
} else {
74+
throw err;
75+
}
76+
});
77+
}
5678
}
5779

5880
async stop(): Promise<void> {
59-
await this.connection.stop();
81+
// Only stop the connection if this is the last open connection
82+
if (this.openConnections > 0 && --this.openConnections === 0) {
83+
await this.connection.stop();
84+
}
6085
}
6186

6287
async subscribeToProject(projectId: string): Promise<void> {

src/SIL.XForge.Scripture/ClientApp/src/app/core/sf-project.service.spec.ts

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -119,15 +119,6 @@ describe('SFProjectService', () => {
119119
}));
120120
});
121121

122-
describe('onlineAddChapters', () => {
123-
it('should invoke the command service', fakeAsync(async () => {
124-
const env = new TestEnvironment();
125-
await env.service.onlineAddChapters('project01', 1, [2, 3]);
126-
verify(mockedCommandService.onlineInvoke(anything(), 'addChapters', anything())).once();
127-
expect().nothing();
128-
}));
129-
});
130-
131122
describe('onlineSetDraftApplied', () => {
132123
it('should invoke the command service', fakeAsync(async () => {
133124
const env = new TestEnvironment();

src/SIL.XForge.Scripture/ClientApp/src/app/core/sf-project.service.ts

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -236,10 +236,6 @@ export class SFProjectService extends ProjectService<SFProject, SFProjectDoc> {
236236
});
237237
}
238238

239-
onlineAddChapters(projectId: string, book: number, chapters: number[]): Promise<void> {
240-
return this.onlineInvoke<void>('addChapters', { projectId, book, chapters });
241-
}
242-
243239
onlineUpdateSettings(id: string, settings: SFProjectSettings): Promise<void> {
244240
return this.onlineInvoke('updateSettings', { projectId: id, settings });
245241
}

src/SIL.XForge.Scripture/ClientApp/src/app/core/text-doc.service.spec.ts

Lines changed: 0 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -183,25 +183,6 @@ describe('TextDocService', () => {
183183
});
184184
});
185185

186-
describe('createTextDoc', () => {
187-
it('should throw error if text doc already exists', fakeAsync(() => {
188-
const env = new TestEnvironment();
189-
expect(() => {
190-
env.textDocService.createTextDoc(env.textDocId, getTextDoc(env.textDocId));
191-
tick();
192-
}).toThrowError();
193-
}));
194-
195-
it('creates the text doc if it does not already exist', fakeAsync(async () => {
196-
const env = new TestEnvironment();
197-
const textDocId = new TextDocId('project01', 40, 2);
198-
const textDoc = await env.textDocService.createTextDoc(textDocId, getTextDoc(textDocId));
199-
tick();
200-
201-
expect(textDoc.data).toBeDefined();
202-
}));
203-
});
204-
205186
describe('isDataInSync', () => {
206187
it('should return true if the project is undefined', () => {
207188
const env = new TestEnvironment();

0 commit comments

Comments
 (0)