Skip to content

Commit 6ec151b

Browse files
committed
Migrate editing to editingRequires, disabling editing on older clients
1 parent f5e4802 commit 6ec151b

File tree

15 files changed

+225
-21
lines changed

15 files changed

+225
-21
lines changed

src/RealtimeServer/scriptureforge/models/sf-project-test-data.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import merge from 'lodash/merge';
22
import { RecursivePartial } from '../../common/utils/type-utils';
33
import { CheckingAnswerExport } from './checking-config';
4-
import { SFProject, SFProjectProfile } from './sf-project';
4+
import { EditingRequires, SFProject, SFProjectProfile } from './sf-project';
55

66
function testProjectProfile(ordinal: number): SFProjectProfile {
77
return {
@@ -44,7 +44,8 @@ function testProjectProfile(ordinal: number): SFProjectProfile {
4444
biblicalTermsEnabled: false,
4545
hasRenderings: false
4646
},
47-
editable: true,
47+
editable: false,
48+
editingRequires: EditingRequires.ParatextEditingEnabled | EditingRequires.ViewModelBlankSupport,
4849
defaultFontSize: 12,
4950
defaultFont: 'Charis SIL',
5051
maxGeneratedUsersPerShareKey: 250

src/RealtimeServer/scriptureforge/models/sf-project.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ export interface SFProjectProfile extends Project {
4242
noteTags?: NoteTag[];
4343
sync: Sync;
4444
editable: boolean;
45+
editingRequires: EditingRequires;
4546
defaultFontSize?: number;
4647
defaultFont?: string;
4748
maxGeneratedUsersPerShareKey?: number;
@@ -66,3 +67,26 @@ export function isResource(project: SFProjectProfile): boolean {
6667
const resourceIdLength: number = DBL_RESOURCE_ID_LENGTH;
6768
return project.paratextId.length === resourceIdLength;
6869
}
70+
71+
/**
72+
* A bitwise-flag enumeration to represent what frontend features are required to edit this project's text documents.
73+
*
74+
* To add more required features, add as follows:
75+
*
76+
* FutureFeatureA = 1 << 2, // 4
77+
* FutureFeatureB = 1 << 3, // 8
78+
*
79+
* NOTE: Adding a new required feature and migrating editingRequires to include it will block older editors.
80+
* The new required featured should be added to the MaxSupportedEditingRequiresValue below.
81+
*/
82+
export enum EditingRequires {
83+
ParatextEditingEnabled = 1 << 0, // 1
84+
ViewModelBlankSupport = 1 << 1 // 2
85+
}
86+
87+
/**
88+
* This value is by the frontend to determine if a feature has been added
89+
* which should disable editing on the frontend until it is updated.
90+
*/
91+
export const MaxSupportedEditingRequiresValue: EditingRequires =
92+
EditingRequires.ParatextEditingEnabled | EditingRequires.ViewModelBlankSupport;

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

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -627,6 +627,55 @@ describe('SFProjectMigrations', () => {
627627
expect(projectDoc.data.translateConfig.draftConfig.additionalTrainingData).toBeUndefined();
628628
});
629629
});
630+
631+
describe('version 25', () => {
632+
it('migrates editable to editingRequires when true', async () => {
633+
const env = new TestEnvironment(24);
634+
const conn = env.server.connect();
635+
await createDoc(conn, SF_PROJECTS_COLLECTION, 'project01', {
636+
editable: true
637+
});
638+
let projectDoc = await fetchDoc(conn, SF_PROJECTS_COLLECTION, 'project01');
639+
expect(projectDoc.data.editable).toBe(true);
640+
641+
await env.server.migrateIfNecessary();
642+
643+
projectDoc = await fetchDoc(conn, SF_PROJECTS_COLLECTION, 'project01');
644+
expect(projectDoc.data.editable).toBe(false);
645+
expect(projectDoc.data.editingRequires).toBe(3);
646+
});
647+
648+
it('migrates editable to editingRequires when false', async () => {
649+
const env = new TestEnvironment(24);
650+
const conn = env.server.connect();
651+
await createDoc(conn, SF_PROJECTS_COLLECTION, 'project01', {
652+
editable: false
653+
});
654+
let projectDoc = await fetchDoc(conn, SF_PROJECTS_COLLECTION, 'project01');
655+
expect(projectDoc.data.editable).toBe(false);
656+
657+
await env.server.migrateIfNecessary();
658+
659+
projectDoc = await fetchDoc(conn, SF_PROJECTS_COLLECTION, 'project01');
660+
expect(projectDoc.data.editable).toBe(false);
661+
expect(projectDoc.data.editingRequires).toBe(2);
662+
});
663+
664+
it('does not remigrate editingRequires', async () => {
665+
const env = new TestEnvironment(24);
666+
const conn = env.server.connect();
667+
await createDoc(conn, SF_PROJECTS_COLLECTION, 'project01', { editingRequires: 6 });
668+
let projectDoc = await fetchDoc(conn, SF_PROJECTS_COLLECTION, 'project01');
669+
expect(projectDoc.data.editable).toBeUndefined();
670+
expect(projectDoc.data.editingRequires).toBe(6);
671+
672+
await env.server.migrateIfNecessary();
673+
674+
projectDoc = await fetchDoc(conn, SF_PROJECTS_COLLECTION, 'project01');
675+
expect(projectDoc.data.editable).toBeUndefined();
676+
expect(projectDoc.data.editingRequires).toBe(6);
677+
});
678+
});
630679
});
631680

632681
class TestEnvironment {

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

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { DocMigration, MigrationConstructor, monotonicallyIncreasingMigrationLis
44
import { Operation } from '../../common/models/project-rights';
55
import { submitMigrationOp } from '../../common/realtime-server';
66
import { NoteTag } from '../models/note-tag';
7+
import { EditingRequires } from '../models/sf-project';
78
import { SF_PROJECT_RIGHTS, SFProjectDomain } from '../models/sf-project-rights';
89
import { SFProjectRole } from '../models/sf-project-role';
910
import { TextInfoPermission } from '../models/text-info-permission';
@@ -453,6 +454,39 @@ class SFProjectMigration24 extends DocMigration {
453454
}
454455
}
455456

457+
class SFProjectMigration25 extends DocMigration {
458+
static readonly VERSION = 25;
459+
460+
async migrateDoc(doc: Doc): Promise<void> {
461+
const ops: Op[] = [];
462+
463+
if (doc.data.editingRequires == null) {
464+
const editable: boolean = doc.data.editable !== false;
465+
if (doc.data.editable == null) {
466+
ops.push({
467+
p: ['editable'],
468+
oi: false
469+
});
470+
} else if (doc.data.editable == true) {
471+
ops.push({
472+
p: ['editable'],
473+
od: true,
474+
oi: false
475+
});
476+
}
477+
478+
ops.push({
479+
p: ['editingRequires'],
480+
oi: (editable ? EditingRequires.ParatextEditingEnabled : 0) | EditingRequires.ViewModelBlankSupport
481+
});
482+
}
483+
484+
if (ops.length > 0) {
485+
await submitMigrationOp(SFProjectMigration25.VERSION, doc, ops);
486+
}
487+
}
488+
}
489+
456490
export const SF_PROJECT_MIGRATIONS: MigrationConstructor[] = monotonicallyIncreasingMigrationList([
457491
SFProjectMigration1,
458492
SFProjectMigration2,
@@ -477,5 +511,6 @@ export const SF_PROJECT_MIGRATIONS: MigrationConstructor[] = monotonicallyIncrea
477511
SFProjectMigration21,
478512
SFProjectMigration22,
479513
SFProjectMigration23,
480-
SFProjectMigration24
514+
SFProjectMigration24,
515+
SFProjectMigration25
481516
]);

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ const SF_PROJECT_PROFILE_FIELDS: ShareDB.ProjectionFields = {
2727
isRightToLeft: true,
2828
biblicalTermsConfig: true,
2929
editable: true,
30+
editingRequires: true,
3031
defaultFontSize: true,
3132
defaultFont: true,
3233
translateConfig: true,
@@ -513,6 +514,9 @@ export class SFProjectService extends ProjectService<SFProject> {
513514
editable: {
514515
bsonType: 'bool'
515516
},
517+
editingRequires: {
518+
bsonType: 'int'
519+
},
516520
defaultFontSize: {
517521
bsonType: 'int'
518522
},

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

Lines changed: 33 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { fakeAsync, TestBed, tick } from '@angular/core/testing';
22
import { Delta } from 'quill';
3+
import { EditingRequires } from 'realtime-server/lib/esm/scriptureforge/models/sf-project';
34
import { SFProjectRole } from 'realtime-server/lib/esm/scriptureforge/models/sf-project-role';
45
import { createTestProjectProfile } from 'realtime-server/lib/esm/scriptureforge/models/sf-project-test-data';
56
import { TextData } from 'realtime-server/lib/esm/scriptureforge/models/text-data';
@@ -79,7 +80,7 @@ describe('TextDocService', () => {
7980
it('should return true if the project and user are correctly configured', () => {
8081
const env = new TestEnvironment();
8182
const project = createTestProjectProfile({
82-
editable: true,
83+
editingRequires: EditingRequires.ParatextEditingEnabled | EditingRequires.ViewModelBlankSupport,
8384
sync: { dataInSync: true },
8485
texts: [
8586
{ bookNum: 1, chapters: [{ number: 1, isValid: true, permissions: { user01: TextInfoPermission.Write } }] }
@@ -105,7 +106,7 @@ describe('TextDocService', () => {
105106
it('should return false if user does not have general edit right', () => {
106107
const env = new TestEnvironment();
107108
const project = createTestProjectProfile({
108-
editable: true,
109+
editingRequires: EditingRequires.ParatextEditingEnabled | EditingRequires.ViewModelBlankSupport,
109110
sync: { dataInSync: true },
110111
texts: [
111112
{ bookNum: 1, chapters: [{ number: 1, isValid: true, permissions: { user01: TextInfoPermission.Write } }] }
@@ -121,7 +122,7 @@ describe('TextDocService', () => {
121122
it('should return false if user does not have chapter edit permission', () => {
122123
const env = new TestEnvironment();
123124
const project = createTestProjectProfile({
124-
editable: true,
125+
editingRequires: EditingRequires.ParatextEditingEnabled | EditingRequires.ViewModelBlankSupport,
125126
sync: { dataInSync: true },
126127
texts: [
127128
{ bookNum: 1, chapters: [{ number: 1, isValid: true, permissions: { user01: TextInfoPermission.Read } }] }
@@ -137,7 +138,7 @@ describe('TextDocService', () => {
137138
it('should return false if data is not in sync', () => {
138139
const env = new TestEnvironment();
139140
const project = createTestProjectProfile({
140-
editable: true,
141+
editingRequires: EditingRequires.ParatextEditingEnabled | EditingRequires.ViewModelBlankSupport,
141142
sync: { dataInSync: false },
142143
texts: [
143144
{ bookNum: 1, chapters: [{ number: 1, isValid: true, permissions: { user01: TextInfoPermission.Write } }] }
@@ -153,7 +154,7 @@ describe('TextDocService', () => {
153154
it('should return false if editing is disabled', () => {
154155
const env = new TestEnvironment();
155156
const project = createTestProjectProfile({
156-
editable: false,
157+
editingRequires: EditingRequires.ViewModelBlankSupport,
157158
sync: { dataInSync: true },
158159
texts: [
159160
{ bookNum: 1, chapters: [{ number: 1, isValid: true, permissions: { user01: TextInfoPermission.Write } }] }
@@ -169,7 +170,7 @@ describe('TextDocService', () => {
169170
it('should return true if all conditions are met', () => {
170171
const env = new TestEnvironment();
171172
const project = createTestProjectProfile({
172-
editable: true,
173+
editingRequires: EditingRequires.ParatextEditingEnabled | EditingRequires.ViewModelBlankSupport,
173174
sync: { dataInSync: true },
174175
texts: [
175176
{ bookNum: 1, chapters: [{ number: 1, isValid: true, permissions: { user01: TextInfoPermission.Write } }] }
@@ -241,7 +242,9 @@ describe('TextDocService', () => {
241242

242243
it('should return false if the project is editable', () => {
243244
const env = new TestEnvironment();
244-
const project = createTestProjectProfile({ editable: true });
245+
const project = createTestProjectProfile({
246+
editingRequires: EditingRequires.ParatextEditingEnabled | EditingRequires.ViewModelBlankSupport
247+
});
245248

246249
// SUT
247250
const actual: boolean = env.textDocService.isEditingDisabled(project);
@@ -250,7 +253,29 @@ describe('TextDocService', () => {
250253

251254
it('should return true if the project is not editable', () => {
252255
const env = new TestEnvironment();
253-
const project = createTestProjectProfile({ editable: false });
256+
const project = createTestProjectProfile({ editingRequires: EditingRequires.ViewModelBlankSupport });
257+
258+
// SUT
259+
const actual: boolean = env.textDocService.isEditingDisabled(project);
260+
expect(actual).toBe(true);
261+
});
262+
263+
it('should return true if the project is has been upgraded to a version beyond the supported version', () => {
264+
const env = new TestEnvironment();
265+
const project = createTestProjectProfile({
266+
editingRequires: Number.MAX_SAFE_INTEGER
267+
});
268+
269+
// SUT
270+
const actual: boolean = env.textDocService.isEditingDisabled(project);
271+
expect(actual).toBe(true);
272+
});
273+
274+
it('should return true if the project is has not been upgraded to view model support', () => {
275+
const env = new TestEnvironment();
276+
const project = createTestProjectProfile({
277+
editingRequires: EditingRequires.ParatextEditingEnabled
278+
});
254279

255280
// SUT
256281
const actual: boolean = env.textDocService.isEditingDisabled(project);

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

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
11
import { Injectable } from '@angular/core';
22
import { Delta } from 'quill';
33
import { Operation } from 'realtime-server/lib/esm/common/models/project-rights';
4-
import { SFProjectProfile } from 'realtime-server/lib/esm/scriptureforge/models/sf-project';
4+
import {
5+
EditingRequires,
6+
MaxSupportedEditingRequiresValue,
7+
SFProjectProfile
8+
} from 'realtime-server/lib/esm/scriptureforge/models/sf-project';
59
import { SF_PROJECT_RIGHTS, SFProjectDomain } from 'realtime-server/lib/esm/scriptureforge/models/sf-project-rights';
610
import { TextData } from 'realtime-server/lib/esm/scriptureforge/models/text-data';
711
import { Chapter, TextInfo } from 'realtime-server/lib/esm/scriptureforge/models/text-info';
@@ -103,14 +107,29 @@ export class TextDocService {
103107
return project?.sync?.dataInSync !== false;
104108
}
105109

110+
/**
111+
* Determines if an update is required to allow editing.
112+
*
113+
* @param {SFProjectProfile | undefined} project The project.
114+
* @returns {boolean} A value indicating whether the app must be updated.
115+
*/
116+
isUpdateRequired(project: SFProjectProfile | undefined): boolean {
117+
return (project?.editingRequires ?? 0) > MaxSupportedEditingRequiresValue;
118+
}
119+
106120
/**
107121
* Determines if editing is disabled for a project.
108122
*
109123
* @param {SFProjectProfile | undefined} project The project.
110124
* @returns {boolean} A value indicating whether editing is disabled for the project.
111125
*/
112126
isEditingDisabled(project: SFProjectProfile | undefined): boolean {
113-
return project?.editable === false;
127+
return (
128+
project != null &&
129+
(this.isUpdateRequired(project) ||
130+
(project.editingRequires & EditingRequires.ViewModelBlankSupport) !== EditingRequires.ViewModelBlankSupport ||
131+
(project.editingRequires & EditingRequires.ParatextEditingEnabled) !== EditingRequires.ParatextEditingEnabled)
132+
);
114133
}
115134

116135
/**

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,11 @@
142142
{{ t("project_data_out_of_sync") }}
143143
</app-notice>
144144
}
145+
@if (updateRequired && hasEditRight) {
146+
<app-notice mode="fill-dark" type="warning" icon="warning" class="update-required-warning">
147+
{{ t("update_required") }}
148+
</app-notice>
149+
}
145150
@if (target.areOpsCorrupted && hasEditRight) {
146151
<app-notice mode="fill-dark" type="error" icon="error" class="doc-corrupted-warning">
147152
{{ t("text_doc_corrupted") }}

0 commit comments

Comments
 (0)