Skip to content

Commit

Permalink
Merge pull request #75 from ducktordanny/feat/changelog-view
Browse files Browse the repository at this point in the history
Feat: Changelog view
  • Loading branch information
ducktordanny authored Jan 29, 2024
2 parents 5b515d1 + cb90e41 commit cc8de28
Show file tree
Hide file tree
Showing 23 changed files with 231 additions and 35 deletions.
6 changes: 3 additions & 3 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ jobs:
- name: Setup node
uses: actions/setup-node@v3
with:
node-version: 16.x
node-version: 18.x

- name: Install dependencies
run: yarn
Expand All @@ -37,7 +37,7 @@ jobs:
- name: Setup node
uses: actions/setup-node@v3
with:
node-version: 16.x
node-version: 18.x
- name: Deploy to Vercel
run: npx vercel --token ${{secrets.VERCEL_TOKEN}} --prod
env:
Expand All @@ -56,7 +56,7 @@ jobs:
- name: Setup node
uses: actions/setup-node@v3
with:
node-version: 16.x
node-version: 18.x
- name: Deploy to Heroku
uses: akhileshns/heroku-deploy@v3.12.12
with:
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ jobs:
- name: Setup node
uses: actions/setup-node@v3
with:
node-version: 16.x
node-version: 18.x

- name: Install dependencies
run: yarn
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ Currently, there are two types of actions in the repo, one for running tests whe

[https://github.com/angular/angular/blob/main/CONTRIBUTING.md#-commit-message-format](https://github.com/angular/angular/blob/main/CONTRIBUTING.md#-commit-message-format)

### Changelog: [CHANGELOG.md](https://github.com/ducktordanny/opres.help/blob/master/CHANGELOG.md)
### Changelog: [CHANGELOG.md](https://github.com/ducktordanny/opres.help/blob/master/apps/backend/src/assets/CHANGELOG.md)

### Husky:

Expand Down
3 changes: 2 additions & 1 deletion apps/backend/src/app/app.module.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import {Module} from '@nestjs/common';

import {AssignmentProblemModule} from './controllers/assignment-problem/assignment-problem.module';
import {ChangelogModule} from './controllers/changelog/changelog.module';
import {TransportProblemModule} from './controllers/transport-problem/transport-problem.module';
import {TspModule} from './controllers/tsp/tsp.module';
import {AppController} from './app.controller';

@Module({
imports: [TransportProblemModule, AssignmentProblemModule, TspModule],
imports: [ChangelogModule, TransportProblemModule, AssignmentProblemModule, TspModule],
controllers: [AppController],
})
export class AppModule {}
15 changes: 15 additions & 0 deletions apps/backend/src/app/controllers/changelog/changelog.controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import {Controller, Get, Res} from '@nestjs/common';

import {Response} from 'express';

@Controller('changelog')
export class ChangelogController {
@Get()
public getChangelogContentInMD(@Res() res: Response) {
res.sendFile(__dirname + '/assets/CHANGELOG.md', {
headers: {
'Content-Type': 'text/markdown',
},
});
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import {Module} from '@nestjs/common';

import {ChangelogController} from './changelog.controller';

@Module({
controllers: [ChangelogController],
})
export class ChangelogModule {}
6 changes: 5 additions & 1 deletion CHANGELOG.md → apps/backend/src/assets/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
# Changelog

> All notable changes to this project will be documented in this file.
> All notable changes to this project will be documented and versioned here.
## [0.10.0] - 2024-01-29

- New page and endpoint for viewing Changelog in English

## [0.9.0] - 2024-01-26

Expand Down
10 changes: 6 additions & 4 deletions apps/backend/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,20 @@

import {Logger} from '@nestjs/common';
import {NestFactory} from '@nestjs/core';
import {NestExpressApplication} from '@nestjs/platform-express';

import {join} from 'path';

import {AppModule} from './app/app.module';

async function bootstrap() {
const app = await NestFactory.create(AppModule);
const app = await NestFactory.create<NestExpressApplication>(AppModule);
const globalPrefix = 'api';
app.useStaticAssets(join(__dirname, 'assets'));
app.setGlobalPrefix(globalPrefix);
const port = process.env.PORT || 3333;
await app.listen(port);
Logger.log(
`🚀 Application is running on: http://localhost:${port}/${globalPrefix}`,
);
Logger.log(`🚀 Application is running on: http://localhost:${port}/${globalPrefix}`);
}

bootstrap();
5 changes: 5 additions & 0 deletions apps/frontend/src/app/app.routing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,11 @@ const routes: Routes = [
path: 'home',
loadChildren: () => import('./pages/home/home.module').then((module) => module.HomeModule),
},
{
path: 'changelog',
loadChildren: () =>
import('./pages/changelog/changelog.module').then((module) => module.ChangelogPageModule),
},
{
path: 'public-api-docs',
pathMatch: 'full',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ export class LayoutComponent implements OnDestroy {
public mobileQuery: MediaQueryList;
public readonly ROUTES = [
{path: '/home', title: 'SIDEBAR_MENU.HOME'},
{path: '/changelog', title: 'SIDEBAR_MENU.CHANGELOG'},
{path: '/public-api-docs', title: 'SIDEBAR_MENU.API_DOCS'},
{
path: '/transport-problem',
Expand Down
24 changes: 24 additions & 0 deletions apps/frontend/src/app/pages/changelog/changelog.module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import {CommonModule} from '@angular/common';
import {HttpClientModule} from '@angular/common/http';
import {NgModule} from '@angular/core';
import {MatSnackBarModule} from '@angular/material/snack-bar';

import {MdPreviewModule} from '../../pipes/md-preview/md-preview.module';

import {ChangelogPageComponent} from './changelog.page';
import {ChangelogRouteModule} from './changelog.routing';
import {ChangelogService} from './changelog.service';

@NgModule({
declarations: [ChangelogPageComponent],
providers: [ChangelogService],
imports: [
ChangelogRouteModule,
CommonModule,
HttpClientModule,
MatSnackBarModule,
MdPreviewModule,
],
exports: [],
})
export class ChangelogPageModule {}
47 changes: 47 additions & 0 deletions apps/frontend/src/app/pages/changelog/changelog.page.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import {AfterViewInit, ChangeDetectionStrategy, Component} from '@angular/core';
import {MatSnackBar} from '@angular/material/snack-bar';

import {UntilDestroy, untilDestroyed} from '@ngneat/until-destroy';
import {TranslateService} from '@ngx-translate/core';
import {Observable} from 'rxjs';

import {ChangelogService} from './changelog.service';

@UntilDestroy()
@Component({
templateUrl: './changelog.template.html',
styles: [
`
section {
padding: 16px;
}
`,
],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ChangelogPageComponent implements AfterViewInit {
public changelogContent: Observable<string>;

constructor(
private changelogService: ChangelogService,
private snackBar: MatSnackBar,
private translateService: TranslateService,
) {
this.changelogContent = this.changelogService.getChangelogContent();
}

public ngAfterViewInit(): void {
this.showLanguageInfo();
this.translateService.onLangChange
.pipe(untilDestroyed(this))
.subscribe(() => this.showLanguageInfo());
}

public showLanguageInfo(): void {
const snackBarContent = this.translateService.instant('CHANGELOG.INFO');
const actionContent = this.translateService.instant('CLOSE');
this.snackBar.open(snackBarContent, actionContent, {
duration: 5_000,
});
}
}
12 changes: 12 additions & 0 deletions apps/frontend/src/app/pages/changelog/changelog.routing.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import {NgModule} from '@angular/core';
import {RouterModule, Routes} from '@angular/router';

import {ChangelogPageComponent} from './changelog.page';

const routes: Routes = [{path: '', component: ChangelogPageComponent}];

@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule],
})
export class ChangelogRouteModule {}
26 changes: 26 additions & 0 deletions apps/frontend/src/app/pages/changelog/changelog.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import {HttpClient, HttpHeaders} from '@angular/common/http';
import {Injectable} from '@angular/core';

import {Observable} from 'rxjs';
import {map} from 'rxjs/operators';

@Injectable()
export class ChangelogService {
constructor(private http: HttpClient) {}

public getChangelogContent(): Observable<string> {
const headers = new HttpHeaders();
headers.append('Access', 'text/markdown');
headers.append('Content-Type', 'text/markdown');

const request = this.http.get<ArrayBuffer>('/api/changelog', {
headers,
responseType: 'arraybuffer' as 'json',
});

return request.pipe(map(this.getContentFromArrayBuffer));
}

private getContentFromArrayBuffer = (arrbuffer: ArrayBuffer): string =>
new TextDecoder().decode(arrbuffer);
}
7 changes: 7 additions & 0 deletions apps/frontend/src/app/pages/changelog/changelog.template.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<section>
<p [innerHTML]="changelogContent | async | mdPreview"></p>
<a
href="https://github.com/ducktordanny/opres.help/blob/master/apps/backend/src/assets/CHANGELOG.md"
>See on GitHub.</a
>
</section>
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,14 @@ import {HttpClient, HttpClientModule} from '@angular/common/http';
import {TestBed} from '@angular/core/testing';
import {MatSnackBar, MatSnackBarModule} from '@angular/material/snack-bar';

import {
tableMinimumFirstResultMock,
tpDataFirstMock,
} from '@opres/shared/data/mocks';
import {tableMinimumFirstResultMock, tpDataFirstMock} from '@opres/shared/data/mocks';
import {Epsilon} from '@opres/shared/types';
import {TranslateModule, TranslateService} from '@ngx-translate/core';
import {last} from 'lodash';
import {of, throwError} from 'rxjs';

import {ErrorHandlerService} from '../../../services/error-handler.service';
import {
transportProblemCacheBuster$,
TransportProblemService,
} from '../transport-problem.service';
import {transportProblemCacheBuster$, TransportProblemService} from '../transport-problem.service';

describe('TransportProblemService', () => {
const epsilonMock: Epsilon = {value: 123};
Expand All @@ -26,15 +21,19 @@ describe('TransportProblemService', () => {
let transportProblemService: TransportProblemService;
let http: HttpClient;
let snackbar: MatSnackBar;
let translateService: TranslateService;

beforeEach(() => {
TestBed.configureTestingModule({
imports: [HttpClientModule, MatSnackBarModule],
providers: [TransportProblemService, HttpClient, ErrorHandlerService],
imports: [HttpClientModule, MatSnackBarModule, TranslateModule.forRoot()],
providers: [TransportProblemService, TranslateService, HttpClient, ErrorHandlerService],
});
transportProblemService = TestBed.inject(TransportProblemService);
http = TestBed.inject(HttpClient);
snackbar = TestBed.inject(MatSnackBar);
translateService = TestBed.inject(TranslateService);

jest.spyOn(translateService, 'instant').mockReturnValue('Close');
transportProblemCacheBuster$.next();
});

Expand All @@ -47,12 +46,10 @@ describe('TransportProblemService', () => {
.spyOn(transportProblemService, 'getEpsilonResult')
.mockReturnValue(of(epsilonMock));
const phaseMock = {steps: [], epsilon: epsilonMock};
transportProblemService
.getFullCalculationResult(tpDataFirstMock)
.subscribe((value) => {
expect(value.firstPhase).toEqual(phaseMock);
expect(value.secondPhase).toEqual(phaseMock);
});
transportProblemService.getFullCalculationResult(tpDataFirstMock).subscribe((value) => {
expect(value.firstPhase).toEqual(phaseMock);
expect(value.secondPhase).toEqual(phaseMock);
});
done();
expect(httpPostSpy).toHaveBeenCalledTimes(2);
expect(epsilonSpy).toHaveBeenCalledTimes(2);
Expand All @@ -72,9 +69,7 @@ describe('TransportProblemService', () => {
});

it('should check epsilon result', () => {
const httpPostSpy = jest
.spyOn(http, 'post')
.mockReturnValue(of(epsilonMock));
const httpPostSpy = jest.spyOn(http, 'post').mockReturnValue(of(epsilonMock));
transportProblemService
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
.getEpsilonResult(last(tableMinimumFirstResultMock)!.transportation)
Expand Down
9 changes: 9 additions & 0 deletions apps/frontend/src/app/pipes/md-preview/md-preview.module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import {NgModule} from '@angular/core';

import {MdPreviewPipe} from './md-preview.pipe';

@NgModule({
declarations: [MdPreviewPipe],
exports: [MdPreviewPipe],
})
export class MdPreviewModule {}
23 changes: 23 additions & 0 deletions apps/frontend/src/app/pipes/md-preview/md-preview.pipe.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import {Pipe, SecurityContext} from '@angular/core';
import {DomSanitizer} from '@angular/platform-browser';
import {PipeTransform} from '@nestjs/common';

import {marked} from 'marked';

@Pipe({
name: 'mdPreview',
})
export class MdPreviewPipe implements PipeTransform {
constructor(private sanitizer: DomSanitizer) {}

public transform(value: string | null): string {
if (value === null) {
console.error('mdPreview input value is null');
return '';
}
const rawPreview = marked.parse(value);
const preview = this.sanitizer.sanitize(SecurityContext.HTML, rawPreview);
if (preview === null) console.error('Cannot display HTML content because of security reasons.');
return preview ?? '';
}
}
9 changes: 5 additions & 4 deletions apps/frontend/src/app/services/error-handler.service.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,19 @@
import {Injectable} from '@angular/core';
import {MatSnackBar} from '@angular/material/snack-bar';

import {TranslateService} from '@ngx-translate/core';
import {EMPTY, Observable} from 'rxjs';

import {APIError} from '../types/api.type';

@Injectable()
export class ErrorHandlerService {
constructor(private snackbar: MatSnackBar) {}
constructor(private snackbar: MatSnackBar, private translateService: TranslateService) {}

public showError = (value: {error: APIError}): Observable<never> => {
const message =
value?.error?.message || value?.error?.error || 'Something went wrong';
this.snackbar.open(message, 'Close');
const message = value?.error?.message || value?.error?.error || 'Something went wrong';
const action = this.translateService.instant('CLOSE');
this.snackbar.open(message, action);
return EMPTY;
};
}
Loading

0 comments on commit cc8de28

Please sign in to comment.