Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Metadata Editor: implement notifications when saving a record #855

Merged
merged 11 commits into from
Apr 19, 2024
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -248,6 +248,7 @@ Libraries are organized in the following fashion:
- `feature-editor` for logic and components related to editing metadata
- `feature-map` for logic and components related to interactive maps
- `feature-search` for logic and components related to searching through the catalog
- `feature-notifications` for notifications systems

> Note: these libraries provide "smart components" which are communicating with each other using a NgRx store.
> They rely on presentation components and as such hold very little HTML or CSS code.
Expand Down
2 changes: 1 addition & 1 deletion apps/datafeeder/tsconfig.app.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"compilerOptions": {
"outDir": "../../dist/out-tsc",
"types": [],
"target": "ES2022",
"target": "es2022",
"useDefineForClassFields": false
},
"files": ["src/main.ts", "src/polyfills.ts"],
Expand Down
2 changes: 1 addition & 1 deletion apps/datahub/tsconfig.app.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"compilerOptions": {
"outDir": "../../dist/out-tsc",
"types": [],
"target": "ES2022",
"target": "es2022",
"useDefineForClassFields": false
},
"files": ["src/main.ts", "src/polyfills.ts"],
Expand Down
2 changes: 1 addition & 1 deletion apps/demo/tsconfig.app.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"compilerOptions": {
"outDir": "../../dist/out-tsc",
"types": [],
"target": "ES2022",
"target": "es2022",
"useDefineForClassFields": false
},
"files": ["src/main.ts", "src/polyfills.ts"],
Expand Down
2 changes: 1 addition & 1 deletion apps/map-viewer/tsconfig.app.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"compilerOptions": {
"outDir": "../../dist/out-tsc",
"types": [],
"target": "ES2022",
"target": "es2022",
"useDefineForClassFields": false
},
"files": ["src/main.ts", "src/polyfills.ts"],
Expand Down
2 changes: 1 addition & 1 deletion apps/metadata-converter/tsconfig.app.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"compilerOptions": {
"outDir": "../../dist/out-tsc",
"types": [],
"target": "ES2022",
"target": "es2022",
"useDefineForClassFields": false
},
"files": ["src/main.ts", "src/polyfills.ts"],
Expand Down
2 changes: 1 addition & 1 deletion apps/metadata-editor/src/app/app.component.html
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
<div gnUiSearchRouterContainer="editor">
<div gnUiSearchRouterContainer="editor" class="h-full">
<router-outlet></router-outlet>
</div>
7 changes: 3 additions & 4 deletions apps/metadata-editor/src/app/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,19 +20,17 @@ import { AppComponent } from './app.component'
import { appRoutes } from './app.routes'
import { FeatureAuthModule } from '@geonetwork-ui/feature/auth'
import { BrowserModule } from '@angular/platform-browser'
import { BrowserAnimationsModule } from '@angular/platform-browser/animations'
import { provideAnimations } from '@angular/platform-browser/animations'
import { extModules } from './build-specifics'
import { DashboardPageComponent } from './dashboard/dashboard-page.component'
import { EditorRouterService } from './router.service'
import { provideRepositoryUrl } from '@geonetwork-ui/api/repository'
import { provideGn4 } from '@geonetwork-ui/api/repository'
import { provideGn4, provideRepositoryUrl } from '@geonetwork-ui/api/repository'
import { FeatureEditorModule } from '@geonetwork-ui/feature/editor'

@NgModule({
declarations: [AppComponent],
imports: [
BrowserModule,
BrowserAnimationsModule,
StoreModule.forRoot(
{},
{
Expand Down Expand Up @@ -62,6 +60,7 @@ import { FeatureEditorModule } from '@geonetwork-ui/feature/editor'
provideRepositoryUrl(() => getGlobalConfig().GN4_API_URL),
importProvidersFrom(EffectsModule.forRoot()),
provideGn4(),
provideAnimations(),
],
bootstrap: [AppComponent],
})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,12 @@
<header class="shrink-0 border-b border-blue-300">
<md-editor-search-header></md-editor-search-header>
</header>
<router-outlet></router-outlet>
<div class="relative">
<div class="absolute top-0 left-0 w-2/3 z-10 pointer-events-none">
<gn-ui-notifications-container></gn-ui-notifications-container>
</div>
<router-outlet></router-outlet>
</div>
</div>
</div>
</div>
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,19 @@ import { ChangeDetectionStrategy, Component } from '@angular/core'
import { RouterOutlet } from '@angular/router'
import { SidebarComponent } from './sidebar/sidebar.component'
import { SearchHeaderComponent } from './search-header/search-header.component'
import { NotificationsContainerComponent } from '@geonetwork-ui/feature/notifications'

@Component({
selector: 'md-editor-dashboard',
templateUrl: './dashboard-page.component.html',
styleUrls: ['./dashboard-page.component.css'],
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [RouterOutlet, SidebarComponent, SearchHeaderComponent],
imports: [
RouterOutlet,
SidebarComponent,
SearchHeaderComponent,
NotificationsContainerComponent,
],
standalone: true,
})
export class DashboardPageComponent {}
59 changes: 58 additions & 1 deletion apps/metadata-editor/src/app/edit-record.resolver.spec.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,75 @@
import { TestBed } from '@angular/core/testing'
import { EditRecordResolver } from './edit-record.resolver'
import { HttpClientTestingModule } from '@angular/common/http/testing'
import { NotificationsService } from '@geonetwork-ui/feature/notifications'
import { of, throwError } from 'rxjs'
import { DATASET_RECORDS } from '@geonetwork-ui/common/fixtures'
import { EditorService } from '@geonetwork-ui/feature/editor'
import { ActivatedRouteSnapshot, convertToParamMap } from '@angular/router'
import { CatalogRecord } from '@geonetwork-ui/common/domain/model/record'
import { TranslateModule } from '@ngx-translate/core'

class NotificationsServiceMock {
showNotification = jest.fn()
}
class EditorServiceMock {
loadRecordByUuid = jest.fn(() => of(DATASET_RECORDS[0]))
}

const activatedRoute = {
paramMap: convertToParamMap({ id: DATASET_RECORDS[0].uniqueIdentifier }),
} as ActivatedRouteSnapshot

describe('EditRecordResolver', () => {
let resolver: EditRecordResolver
let editorService: EditorService
let notificationsService: NotificationsService
let record: CatalogRecord

beforeEach(() => {
TestBed.configureTestingModule({
imports: [HttpClientTestingModule],
imports: [HttpClientTestingModule, TranslateModule.forRoot()],
providers: [
{ provide: NotificationsService, useClass: NotificationsServiceMock },
{ provide: EditorService, useClass: EditorServiceMock },
],
})
resolver = TestBed.inject(EditRecordResolver)
editorService = TestBed.inject(EditorService)
notificationsService = TestBed.inject(NotificationsService)
})

it('should be created', () => {
expect(resolver).toBeTruthy()
})

describe('load record success', () => {
beforeEach(() => {
record = undefined
resolver.resolve(activatedRoute, null).subscribe((r) => (record = r))
})
it('should load record by uuid', () => {
expect(record).toBe(DATASET_RECORDS[0])
})
})

describe('load record failure', () => {
beforeEach(() => {
editorService.loadRecordByUuid = () =>
throwError(() => new Error('oopsie'))
record = undefined
resolver.resolve(activatedRoute, null).subscribe((r) => (record = r))
})
it('should not emit anything', () => {
expect(record).toBeUndefined()
})
it('should show error notification', () => {
expect(notificationsService.showNotification).toHaveBeenCalledWith({
type: 'error',
title: 'editor.record.loadError.title',
text: 'editor.record.loadError.body oopsie',
closeMessage: 'editor.record.loadError.closeMessage',
})
})
})
})
26 changes: 23 additions & 3 deletions apps/metadata-editor/src/app/edit-record.resolver.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,39 @@
import { Injectable } from '@angular/core'
import { ActivatedRouteSnapshot, RouterStateSnapshot } from '@angular/router'
import { Observable } from 'rxjs'
import { catchError, EMPTY, Observable } from 'rxjs'
import { EditorService } from '@geonetwork-ui/feature/editor'
import { CatalogRecord } from '@geonetwork-ui/common/domain/model/record'
import { NotificationsService } from '@geonetwork-ui/feature/notifications'
import { TranslateService } from '@ngx-translate/core'

@Injectable({
providedIn: 'root',
})
export class EditRecordResolver {
constructor(private editorService: EditorService) {}
constructor(
private editorService: EditorService,
private notificationsService: NotificationsService,
private translateService: TranslateService
) {}

resolve(
route: ActivatedRouteSnapshot,
state: RouterStateSnapshot
): Observable<CatalogRecord> {
return this.editorService.loadRecordByUuid(route.paramMap.get('uuid'))
return this.editorService.loadRecordByUuid(route.paramMap.get('uuid')).pipe(
catchError((error) => {
this.notificationsService.showNotification({
type: 'error',
title: this.translateService.instant('editor.record.loadError.title'),
text: `${this.translateService.instant(
'editor.record.loadError.body'
)} ${error.message}`,
closeMessage: this.translateService.instant(
'editor.record.loadError.closeMessage'
),
})
return EMPTY
})
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
<gn-ui-button type="light">
<mat-icon class="material-symbols-outlined">undo</mat-icon>
</gn-ui-button>
<div class="flex-grow text-center">Save status</div>
<div class="grow text-center">Save status</div>
<gn-ui-button type="light">
<mat-icon class="material-symbols-outlined">help</mat-icon>
</gn-ui-button>
Expand Down
7 changes: 5 additions & 2 deletions apps/metadata-editor/src/app/edit/edit-page.component.html
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
<div class="flex flex-col h-full">
<div class="w-full h-auto flex-shrink-0">
<div class="w-full h-auto shrink-0">
<md-editor-top-toolbar></md-editor-top-toolbar>
</div>
<div class="flex-grow overflow-auto">
<div class="grow overflow-auto relative">
<div class="absolute top-0 left-0 w-2/3 z-10 pointer-events-none">
<gn-ui-notifications-container></gn-ui-notifications-container>
</div>
<gn-ui-record-form></gn-ui-record-form>
</div>
</div>
42 changes: 41 additions & 1 deletion apps/metadata-editor/src/app/edit/edit-page.component.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ import { ActivatedRoute } from '@angular/router'
import { EditorFacade } from '@geonetwork-ui/feature/editor'
import { NO_ERRORS_SCHEMA } from '@angular/core'
import { DATASET_RECORDS } from '@geonetwork-ui/common/fixtures'
import { Subject } from 'rxjs'
import { NotificationsService } from '@geonetwork-ui/feature/notifications'
import { TranslateModule } from '@ngx-translate/core'

const getRoute = () => ({
snapshot: {
Expand All @@ -15,16 +18,22 @@ const getRoute = () => ({

class EditorFacadeMock {
openRecord = jest.fn()
saveError$ = new Subject<string>()
saveSuccess$ = new Subject()
}
class NotificationsServiceMock {
showNotification = jest.fn()
}

describe('EditPageComponent', () => {
let component: EditPageComponent
let fixture: ComponentFixture<EditPageComponent>
let facade: EditorFacade
let notificationsService: NotificationsService

beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [EditPageComponent],
imports: [EditPageComponent, TranslateModule.forRoot()],
schemas: [NO_ERRORS_SCHEMA],
providers: [
{
Expand All @@ -35,10 +44,15 @@ describe('EditPageComponent', () => {
provide: EditorFacade,
useClass: EditorFacadeMock,
},
{
provide: NotificationsService,
useClass: NotificationsServiceMock,
},
],
}).compileComponents()

facade = TestBed.inject(EditorFacade)
notificationsService = TestBed.inject(NotificationsService)
fixture = TestBed.createComponent(EditPageComponent)
component = fixture.componentInstance
fixture.detectChanges()
Expand All @@ -53,4 +67,30 @@ describe('EditPageComponent', () => {
expect(facade.openRecord).toHaveBeenCalledWith(DATASET_RECORDS[0])
})
})

describe('publish error', () => {
it('shows notification', () => {
;(facade.saveError$ as any).next('oopsie')
expect(notificationsService.showNotification).toHaveBeenCalledWith({
type: 'error',
title: 'editor.record.publishError.title',
text: 'editor.record.publishError.body oopsie',
closeMessage: 'editor.record.publishError.closeMessage',
})
})
})

describe('publish success', () => {
it('shows notification', () => {
;(facade.saveSuccess$ as any).next()
expect(notificationsService.showNotification).toHaveBeenCalledWith(
{
type: 'success',
title: 'editor.record.publishSuccess.title',
text: 'editor.record.publishSuccess.body',
},
2500
)
})
})
})
Loading
Loading