Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 6 additions & 4 deletions .github/workflows/docker.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,9 @@ jobs:
tags_flavor: suffix=-dev
# As this is a "dev" image, its tags are all suffixed with "-dev". Otherwise, it uses the same
# tagging logic as the primary 'dspace/dspace-angular' image above.
# run_python_version_script: true
# python_version_script_dest: src/static-files/VERSION_D.html
# Generate deployed-version info served at /static/VERSION_D (issue #813)
run_python_version_script: true
python_version_script_dest: src/static-files/VERSION_D.html
secrets:
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
DOCKER_ACCESS_TOKEN: ${{ secrets.DOCKER_ACCESS_TOKEN }}
Expand All @@ -52,8 +53,9 @@ jobs:
build_id: dspace-angular
image_name: dataquest/dspace-angular
dockerfile_path: ./Dockerfile.dist
# run_python_version_script: true
# python_version_script_dest: src/static-files/VERSION_D.html
# Generate deployed-version info served at /static/VERSION_D (issue #813)
run_python_version_script: true
python_version_script_dest: src/static-files/VERSION_D.html
secrets:
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
DOCKER_ACCESS_TOKEN: ${{ secrets.DOCKER_ACCESS_TOKEN }}
Expand Down
5 changes: 5 additions & 0 deletions angular.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,11 @@
"aot": true,
"assets": [
"src/assets",
{
"glob": "VERSION_D.html",
"input": "src/static-files",
"output": "static-files"
},
"src/robots.txt"
],
"styles": [
Expand Down
38 changes: 38 additions & 0 deletions scripts/sourceversion.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import subprocess
import sys
from datetime import datetime, timezone

# when next editing this script, please introduce argparse.
# do not forget, it is called in BE by .github\workflows\reusable-docker-build.yml
# argparse must be introduced there.
# that action also calls BE version of this script, which is different (BE: scripts/sourceversion.py).
# It must also cooperate with argparse

# the idea is, that this will be different on each branch, but could be possibly passed by argv/argparse
RELEASE_TAG_BASE='none'

def get_time_in_timezone(zone: str = "Europe/Bratislava"):
try:
from zoneinfo import ZoneInfo
my_tz = ZoneInfo(zone)
except Exception as e:
my_tz = timezone.utc
return datetime.now(my_tz)


if __name__ == '__main__':
ts = get_time_in_timezone()
# we have html tags, since this script ends up creating VERSION_D.html
print(f"<h4>This info was generated on: <br> <strong> {ts.strftime('%Y-%m-%d %H:%M:%S %Z%z')} </strong> </h4>")

cmd = 'git log -1 --pretty=format:"<h4>Git hash: <br><strong> %H </strong> <br> Date of commit: <br> <strong> %ai </strong></h4>"'
subprocess.check_call(cmd, shell=True)

# when adding argparse, this should be a bit more obvious
link = sys.argv[1] + sys.argv[2]
print('<br> <h4>Build run: </h4> <a href="' + link + '"> ' + link + '</a> ')

link = "https://github.com/dataquest-dev/dspace-angular/releases/tag/" \
+ RELEASE_TAG_BASE + "-" + datetime.now().strftime('%Y.%m.') + sys.argv[2]

print('<br> <br> <h4>Release link: </h4><a href="' + link + '"> ' + link + '</a> (if it does not work, then this is not an official release instance) ')
6 changes: 6 additions & 0 deletions src/app/app-routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ import { ThemedPageErrorComponent } from './page-error/themed-page-error.compone
import { ThemedPageInternalServerErrorComponent } from './page-internal-server-error/themed-page-internal-server-error.component';
import { ThemedPageNotFoundComponent } from './pagenotfound/themed-pagenotfound.component';
import { PROCESS_MODULE_PATH } from './process-page/process-page-routing.paths';
import { STATIC_PAGE_PATH } from './static-page/static-page-routing-paths';
import { viewTrackerResolver } from './statistics/angulartics/dspace/view-tracker.resolver';
import { provideSubmissionState } from './submission/provide-submission-state';
import { SUGGESTION_MODULE_PATH } from './suggestions-page/suggestions-page-routing-paths';
Expand Down Expand Up @@ -289,6 +290,11 @@ export const APP_ROUTES: Route[] = [
.then((m) => m.ROUTES),
canActivate: [notAuthenticatedGuard],
},
{
path: STATIC_PAGE_PATH,
loadChildren: () => import('./static-page/static-page-routes')
.then((m) => m.ROUTES),
},
{ path: '**', pathMatch: 'full', component: ThemedPageNotFoundComponent, data: { title: '404.page-not-found' } },
],
},
Expand Down
85 changes: 85 additions & 0 deletions src/app/shared/html-content.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import { isPlatformBrowser } from '@angular/common';
import { HttpClient } from '@angular/common/http';
import {
Inject,
Injectable,
PLATFORM_ID,
} from '@angular/core';
import {
firstValueFrom,
of,
} from 'rxjs';
import {
catchError,
map,
} from 'rxjs/operators';

import { LocaleService } from '../core/locale/locale.service';
import {
HTML_SUFFIX,
STATIC_FILES_PROJECT_PATH,
} from '../static-page/static-page-routing-paths';

interface HtmlContentResult {
found: boolean;
body: string;
}

/**
* Service for loading static `.html` files stored in the `src/static-files` folder
* (registered as a build asset in `angular.json`). Used e.g. to render the
* deployed-version info at `/static/VERSION_D` (issue #813).
*/
@Injectable({
providedIn: 'root',
})
export class HtmlContentService {
constructor(
private http: HttpClient,
private localeService: LocaleService,
@Inject(PLATFORM_ID) private platformId: object,
) {}

private withSuffix(name: string): string {
return name.endsWith(HTML_SUFFIX) ? name : name + HTML_SUFFIX;
}

private fetch(url: string) {
return this.http.get(url, { responseType: 'text' }).pipe(
map((body): HtmlContentResult => ({ found: true, body })),
catchError(() => of<HtmlContentResult>({ found: false, body: '' })),
);
}

/**
* Load the html content for a file name, trying the current locale package first
* (`static-files/<lang>/<file>.html`) and falling back to the default package
* (`static-files/<file>.html`). Returns `undefined` when nothing was found.
*
* The files are fetched client-side only; during SSR this resolves to `undefined`
* and the content is loaded after hydration.
*/
async getHtmlContentByPathAndLocale(fileName: string): Promise<string | undefined> {
if (!isPlatformBrowser(this.platformId)) {
return undefined;
}

let language = await firstValueFrom(this.localeService.getCurrentLanguageCode());
// Default language `en` lives in the non-translated (root) package.
language = language === 'en' ? '' : language;

if (language) {
const localized = await firstValueFrom(
this.fetch(this.withSuffix(`${STATIC_FILES_PROJECT_PATH}/${language}/${fileName}`)),
);
if (localized.found) {
return localized.body;
}
}

const fallback = await firstValueFrom(
this.fetch(this.withSuffix(`${STATIC_FILES_PROJECT_PATH}/${fileName}`)),
);
return fallback.found ? fallback.body : undefined;
}
}
23 changes: 23 additions & 0 deletions src/app/shared/utils/clarin-safehtml.pipe.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import {
Pipe,
PipeTransform,
} from '@angular/core';
import {
DomSanitizer,
SafeHtml,
} from '@angular/platform-browser';

/**
* Pipe to keep html tags (e.g. `id`) when rendering a string via `[innerHTML]`.
*/
@Pipe({
name: 'dsSafeHtml',
standalone: true,
})
export class ClarinSafeHtmlPipe implements PipeTransform {
constructor(private sanitized: DomSanitizer) {}

transform(htmlString: string): SafeHtml {
return this.sanitized.bypassSecurityTrustHtml(htmlString ?? '');
}
}
10 changes: 10 additions & 0 deletions src/app/static-page/static-page-routes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { Route } from '@angular/router';

import { StaticPageComponent } from './static-page.component';

export const ROUTES: Route[] = [
{
path: ':id',
component: StaticPageComponent,
},
];
12 changes: 12 additions & 0 deletions src/app/static-page/static-page-routing-paths.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
/**
* Constants for the `/static` route.
*
* The static page component loads `.html` files bundled from `src/static-files`
* (registered as a build asset in `angular.json`). This is how the deployed-version
* info is served at `/static/VERSION_D` (issue #813).
*/
export const STATIC_PAGE_PATH = 'static';

export const STATIC_FILES_PROJECT_PATH = 'static-files';

export const HTML_SUFFIX = '.html';
28 changes: 28 additions & 0 deletions src/app/static-page/static-page.component.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
@if (contentState === 'loading') {
<div class="container text-center my-5">
<div class="spinner-border" role="status">
<span class="visually-hidden">{{ 'loading.default' | translate }}</span>
</div>
</div>
}

<!-- Show static page content when found -->
@if (contentState === 'found') {
<div class="container">
<div [innerHTML]="htmlContent | dsSafeHtml"></div>
</div>
}

<!-- Show 404 error when content not found -->
@if (contentState === 'not-found') {
<div class="container page-not-found">
<h1>404</h1>
<h2><small>{{ 'static-page.404.page-not-found' | translate }}</small></h2>
<br/>
<p>{{ 'static-page.404.help' | translate }}</p>
<br/>
<p class="text-center">
<a routerLink="/home" class="btn btn-primary">{{ 'static-page.404.link.home-page' | translate }}</a>
</p>
</div>
}
5 changes: 5 additions & 0 deletions src/app/static-page/static-page.component.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
.page-not-found {
text-align: center;
margin-top: 3rem;
margin-bottom: 3rem;
}
84 changes: 84 additions & 0 deletions src/app/static-page/static-page.component.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import {
ChangeDetectorRef,
Component,
OnInit,
} from '@angular/core';
import {
ActivatedRoute,
RouterLink,
} from '@angular/router';
import { TranslateModule } from '@ngx-translate/core';

import { ServerResponseService } from '../core/services/server-response.service';
import { HtmlContentService } from '../shared/html-content.service';
import { ClarinSafeHtmlPipe } from '../shared/utils/clarin-safehtml.pipe';

/**
* Component which loads and shows static files from the `src/static-files` folder.
* E.g. `<UI_URL>/static/VERSION_D` renders `static-files/VERSION_D.html`
* (the deployed-version info, issue #813).
*/
@Component({
selector: 'ds-static-page',
templateUrl: './static-page.component.html',
styleUrls: ['./static-page.component.scss'],
imports: [
ClarinSafeHtmlPipe,
RouterLink,
TranslateModule,
],
})
export class StaticPageComponent implements OnInit {
htmlContent = '';
contentState: 'loading' | 'found' | 'not-found' = 'loading';

constructor(
private htmlContentService: HtmlContentService,
private route: ActivatedRoute,
private responseService: ServerResponseService,
private changeDetector: ChangeDetectorRef,
) {}

async ngOnInit(): Promise<void> {
this.contentState = 'loading';
this.htmlContent = '';

const fileName = this.getHtmlFileName();
if (!fileName) {
this.markNotFound();
return;
}

try {
const content = await this.htmlContentService.getHtmlContentByPathAndLocale(fileName);
if (content !== undefined) {
this.htmlContent = content;
this.contentState = 'found';
this.changeDetector.detectChanges();
return;
}
} catch {
// fall through to not-found handling below
}

this.markNotFound();
}

private markNotFound(): void {
this.responseService.setNotFound();
this.contentState = 'not-found';
this.changeDetector.detectChanges();
}

/**
* Read the file name from the URL - `static/FILE_NAME`.
*/
private getHtmlFileName(): string | null {
const id = this.route.snapshot.paramMap.get('id');
if (!id) {
return null;
}
// Drop any trailing fragment, e.g. `VERSION_D#section`.
return id.split('#')[0];
}
}
9 changes: 9 additions & 0 deletions src/assets/i18n/cs.json5
Original file line number Diff line number Diff line change
Expand Up @@ -7532,6 +7532,15 @@
// "sorting.person.birthDate.DESC": "Birth Date Descending",
"sorting.person.birthDate.DESC": "Datum narození sestupně",

// "static-page.404.help": "The static page you requested does not exist. It may have been moved or deleted. You can use the button below to get back to the home page.",
"static-page.404.help": "Požadovaná statická stránka neexistuje. Mohla být přesunuta nebo smazána. Pomocí níže uvedeného tlačítka se můžete vrátit na domovskou stránku.",

// "static-page.404.link.home-page": "Take me to the home page",
"static-page.404.link.home-page": "Návrat na domovskou stránku",

// "static-page.404.page-not-found": "page not found",
"static-page.404.page-not-found": "stránka nebyla nalezena",

// "statistics.title": "Statistics",
"statistics.title": "Statistiky",

Expand Down
7 changes: 7 additions & 0 deletions src/assets/i18n/en.json5
Original file line number Diff line number Diff line change
Expand Up @@ -5012,6 +5012,13 @@

"sorting.person.birthDate.DESC": "Birth Date Descending",

"static-page.404.help": "The static page you requested does not exist. It may have been moved or deleted. You can use the button below to get back to the home page.",

"static-page.404.link.home-page": "Take me to the home page",

"static-page.404.page-not-found": "page not found",


"statistics.title": "Statistics",

"statistics.header": "Statistics for {{ scope }}",
Expand Down
7 changes: 7 additions & 0 deletions src/static-files/VERSION_D.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<!--
Placeholder for the deployed-version info (issue #813).
This file is regenerated at Docker/CI build time by scripts/sourceversion.py
and served at /static/VERSION_D. The committed placeholder only ensures the
src/static-files directory exists so the build-time redirection and the
angular.json asset copy succeed.
-->
Loading