This repository contains three interconnected applications:
- Frontend (Angular) that runs in a browser, at src/SIL.XForge.Scripture/ClientApp
- Backend (dotnet) that runs on a server, at src/SIL.XForge and src/SIL.XForge.Scripture
- RealtimeServer (Node) that runs on a server, at src/RealtimeServer
- Data models must be defined in all 3 applications and stay in sync.
- An example Backend data model can be seen at src/SIL.XForge.Scripture/Models/SFProject.cs
- Frontend and RealtimeServer both use the same data model files. An example can be seen at src/RealtimeServer/scriptureforge/models/sf-project.ts
- Frontend uses src/SIL.XForge.Scripture/ClientApp/src/xforge-common/realtime.service.ts to read and write data. This Realtime document data is stored in IndexedDB and is also automatically kept up to date with the data in RealtimeServer when Frontend has an Internet connection. If Frontend is offline, it continues reading and writing realtime documents using realtime.service.ts, and later data is synced with RealtimeService when Frontend regains an Internet connection.
- Realtime documents should not be modified directly. Instead, submit ops to modify the realtime documents.
- RealtimeServer uses ShareDB to merge ops from multiple Frontend clients. Frontend clients synchronize with RealtimeServer and present a live view of the changes from other clients as they work.
- Frontend also uses JSON-RPC to communicate with Backend, such as seen on Backend at src/SIL.XForge.Scripture/Controllers/SFProjectsRpcController.cs.
- User permissions should be checked in all of Frontend, Backend, and RealtimeServer, using the rights service whenever possible, rather than checking the user role. See project-rights.ts, sf-project-rights.ts, and SFProjectRights.cs.
- Follow existing patterns for validation schemas.
- Most Frontend tasks should work on a mobile phone. In other words, on a device with a narrow and short screen.
- Most editing and reviewing tasks should work while offline. Changing some settings may require being online.
- Feature flags (feature-flag.service.ts) can control functionality rollout.
- Error handling with ErrorReportingService
- Keep related files together in feature folders
- Follow existing naming conventions
- Follow MVVM design, where domain objects and business logic are in Models, templates represent information to the user in Views, and ViewModels transform and bridge data between Models and Views.
- Component templates should be in separate .html files, rather than specified inline in the component decorator.
- Component template stylesheets should be in separate .scss files, rather than specified inline in the component decorator.
- Avoid hard-coding colors in SCSS files when styling components. Instead, use existing CSS variables or create an Angular Material theme file and import it into src/SIL.XForge.Scripture/ClientApp/src/material-styles.scss
- Use TranslocoModule for translations
- Put UI strings in checking_en.json if ANY user might see them
- Only put strings in non_checking_en.json if community checkers will NEVER see them
- Localizations that a Community Checker user might see should be created or edited in src/SIL.XForge.Scripture/ClientApp/src/assets/i18n/checking_en.json. Only localizations that a Community Checker user will not see can be created or edited in src/SIL.XForge.Scripture/ClientApp/src/assets/i18n/non_checking_en.json.
- Even if something is a system-wide feature that isn't specific to community checking functionality, it should still be placed in checking_en.json if a community checking user would POSSIBLY see it.
- Avoid making up and using hard-coded or new color values. Where feasible, use color values from Material Design. For example,
--mat-sys-surfaceand--mat-sys-on-primary. If you can't get close to what you want from Material Design colors, you can use a defined color in_variables.scssormaterial-styles.scss. Follow patterns in existing_foo-theme.scssfiles.
- Use Sentence case for user interface elements, per Material Design. Do not use Title Case for user interface elements. For example, use "Project settings" rather than "Project Settings".
- Write unit tests for new components and services
- Follow existing patterns for mocking dependencies
- Use TestEnvironment pattern from existing tests. Use the TestEnvironment class pattern rather than using a
beforeEach. Do not put helper functions outside of TestEnvironment classes; helper functions or setup functions should be in the TestEnvironment class. - Test both online and offline scenarios
- Test permission boundaries
- When a TestEnvironment class is being given many optional arguments, prefer using object destructuring with default values, and the argument type definition specified inline rather than as an interface. For example,
Example using
class TestEnvironment { constructor({ someRequired, someOptional = 'abc', }: { someRequired: string; someOptional?: string; }) { ... } }
= {}when all items are optional:class TestEnvironment { constructor({ someOptional = 'abc', }: { someOptional?: string; } = {}) { ... } }
- Do not remove comments already in the code if they are still relevant.
- Do not insert new comments into the code where method calls already make it clear.
- Do not add method comments unless the method would be unclear to an experienced developer.
- Do put comments into the code to make it more clear what is going on if it would not be obvious to an experienced developer.
- Do put comments into the code if the intent is not clear from the code.
- All classes and interfaces should have a comment to briefly explain why it is there and what its purpose is in the overall system, even if it seems obvious.
- Please do not fail to add a comment to any classes or interfaces that are created. All classes and interfaces should have a comment.
- When doing null checks, prefer using
== nullor!= nullrather than writingif (value)orif (!value)to better express intent. If guarding for empty strings or other falsy values is necessary, prefer to explicitly check for those as well, such asif (value == null || value === '')rather than justif (!value). - Specify types when declaring variables, arguments, and for function return types, if it's not completely obvious. For example, don't write
const projectId = this.determineProjectId();orconst buildEvents = eventsSorted.filter(...);orconst buildEvent = buildEvents[0];. Instead, writeconst projectId: string | undefined = this.determineProjectId();andconst buildEvents: EventMetric[] = eventsSorted.filter(...);andconst buildEvent: EventMetric | undefined = buildEvents[0];. - Prefer to use
nullto express a deliberate absence of a value, andundefinedto express a value that has not been set yet. - Observables (including subclasses such as Subject or BehaviorSubject) should have names that end with a
$. - Do not use the
!non-null assertion operator. For example, do not writefoo!.baz(). If you need to dereferencefoo, first prove to the type system thatfoois not null either by using a type guard or checking for null. You may use the!non-null assertion operator in spec test files. - Do not use the
astype assertion operator. For example, do not writeconst foo: SomeType = someValue as SomeType. If you need to treatsomeValueasSomeType, first prove to the type system thatsomeValueis of typeSomeTypesuch as by using a type guard. You may use theastype assertion operator in spec test files. - Do not use object property shorthand when creating objects. For example, do not write
const obj = { foo, bar };. Instead, writeconst obj = { foo: foo, bar: bar };. - Do not reorder existing fields and methods to comply with this, but when creating new fields and methods in TypeScript classes, use this order:
- public static fields
- @Input, @Output, and @ViewChild fields
- public instance fields
- non-public static and instance fields
- constructor
- getters and setters
- ngOnInit
- public static and instance methods
- non-public static and instance methods
- Use
@if {}syntax rather than*ngIfsyntax.
- Do not use the
!null-forgiving operator. Instead, check for null and cause a specific outcome if it is null, or make use of theNotNullWhenattribute. You may use the!null-forgiving operator in test files.
- All code that you write should be able to pass eslint linting tests for TypeScript, or csharpier for C#.
- Don't merely write code for the local context, but make changes that are good overall considering the architecture of the application and structure of the files and classes.
- It is better to write code that is verbose and understandable than terse and concise.
- It is better to explicitly check for and handle problems, or prevent problems from happening, than to assume problems will not happen.
- Corner-cases happen. They should be handled in code.
- Please don't change existing code without good justification. Existing code largely works and changing it will cause work for code review. Leave existing code as is when possible.
- Avoid magic numbers where not obvious. Use a named constant for the value instead, which can be defined right before usage.
- Don't write useless comments. For example, for field
translationEngineId, comment "The translation engine ID." adds no additional information than the name of the field already says.
- Pay attention to available types and type guards in src/SIL.XForge.Scripture/ClientApp/src/type-utils.ts.
- If you run frontend tests, run them in the
src/SIL.XForge.Scripture/ClientAppdirectory with a command such asnpm run test:headless -- --watch=false --include '**/text.component.spec.ts' --include '**/settings.component.spec.ts' - If you need to run all frontend tests, you can run them in the
src/SIL.XForge.Scripture/ClientAppdirectory with commandnpm run test:headless -- --watch=false - If you run backend dotnet tests, run them in the repository root directory with a command such as
dotnet test.