Skip to content

Commit ea85961

Browse files
DawnkaiMaciej Kleban
authored andcommitted
[CRIS-Merger] Port CMS metadata edit feature from CRIS
1 parent f8b8ac0 commit ea85961

67 files changed

Lines changed: 2199 additions & 94 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
@if (editMode | async) {
2+
<ng-container *ngTemplateOutlet="editMetadataTemplate"></ng-container>
3+
}
4+
@else {
5+
<ng-container *ngTemplateOutlet="selectMetadataTemplate"></ng-container>
6+
}
7+
8+
<ng-template #selectMetadataTemplate>
9+
<div class="container">
10+
<h3 class="mb-4">{{'menu.section.cms.edit.metadata.head' | translate}}</h3>
11+
<div class="input-group">
12+
<select class="form-control col-md-4 mr-3 mb-2" aria-label="Select metadata" [(ngModel)]="selectedMetadata">
13+
<option [ngValue]="undefined" disabled selected>{{'admin.edit-cms-metadata.select-metadata' | translate}}</option>
14+
@for (md of metadataList; track $index) {
15+
<option [value]="md">{{md}}</option>
16+
}
17+
</select>
18+
<span class="input-group-btn">
19+
<button id="edit-metadata-btn" class="btn btn-primary" (click)="editSelectedMetadata()" [dsBtnDisabled]="!selectedMetadata">
20+
<i class="fas fa-edit mr-1"></i>
21+
{{'admin.edit-cms-metadata.edit-button' | translate}}
22+
</button>
23+
</span>
24+
</div>
25+
</div>
26+
</ng-template>
27+
28+
29+
<ng-template #editMetadataTemplate>
30+
<div class="container">
31+
<h2 class=" mb-5">{{'admin.edit-cms-metadata.title' | translate}} '{{selectedMetadata}}'</h2>
32+
<div class="row">
33+
<div class="col-md">
34+
@for (lang of languageList; track $index) {
35+
<div class="form-group">
36+
<label>{{languageLabel(lang)}}</label>
37+
<textarea class="col-md-12 m-2" [ngModel]="selectedMetadataValues.get(lang)" (ngModelChange)="selectedMetadataValues.set(lang, $event)" rows="10"></textarea>
38+
</div>
39+
}
40+
</div>
41+
</div>
42+
<ng-container *ngTemplateOutlet="editMetadataButtonsTemplate"></ng-container>
43+
</div>
44+
</ng-template>
45+
46+
47+
<ng-template #editMetadataButtonsTemplate>
48+
<div class="row">
49+
<div class="col-md">
50+
<button id="save-metadata-btn" class="btn btn-primary float-right m-2" (click)="saveMetadata()">
51+
<span>
52+
<i class="fas fa-save mr-1"></i>
53+
{{ 'admin.edit-cms-metadata.save-button' | translate }}
54+
</span>
55+
</button>
56+
<button id="back-metadata-btn" class="btn btn-outline-secondary float-right m-2" (click)="back()">
57+
<span>
58+
<i class="fas fa-arrow-left mr-1"></i>
59+
{{ 'admin.edit-cms-metadata.back-button' | translate }}
60+
</span>
61+
</button>
62+
</div>
63+
</div>
64+
</ng-template>

src/app/admin/admin-edit-cms-metadata/admin-edit-cms-metadata.component.scss

Whitespace-only changes.
Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
import { NO_ERRORS_SCHEMA } from '@angular/core';
2+
import {
3+
ComponentFixture,
4+
TestBed,
5+
waitForAsync,
6+
} from '@angular/core/testing';
7+
import { FormsModule } from '@angular/forms';
8+
import { By } from '@angular/platform-browser';
9+
import {
10+
TranslateLoader,
11+
TranslateModule,
12+
} from '@ngx-translate/core';
13+
import { of } from 'rxjs';
14+
15+
import { environment } from '../../../environments/environment.test';
16+
import { SiteDataService } from '../../core/data/site-data.service';
17+
import { Site } from '../../core/shared/site.model';
18+
import { AdminEditCmsMetadataComponent } from './admin-edit-cms-metadata.component';
19+
import { TranslateLoaderMock } from '@dspace/core/testing/translate-loader.mock';
20+
import { NotificationsServiceStub } from '@dspace/core/testing/notifications-service.stub';
21+
import { NotificationsService } from '@dspace/core/notification-system/notifications.service';
22+
23+
describe('AdminEditCmsMetadataComponent', () => {
24+
25+
let component: AdminEditCmsMetadataComponent;
26+
let fixture: ComponentFixture<AdminEditCmsMetadataComponent>;
27+
const site = Object.assign(new Site(), {
28+
metadata: { },
29+
});
30+
31+
const siteServiceStub = jasmine.createSpyObj('SiteDataService', {
32+
find: jasmine.createSpy('find'),
33+
patch: jasmine.createSpy('patch'),
34+
});
35+
36+
const metadataValueMap = new Map([
37+
['en', ''],
38+
['de', ''],
39+
['cs', ''],
40+
['nl', ''],
41+
['pt', ''],
42+
['fr', ''],
43+
['lv', ''],
44+
['bn', ''],
45+
['el', ''],
46+
]);
47+
beforeEach(waitForAsync(() => {
48+
TestBed.configureTestingModule({
49+
imports: [
50+
FormsModule,
51+
TranslateModule.forRoot({
52+
loader: {
53+
provide: TranslateLoader,
54+
useClass: TranslateLoaderMock,
55+
},
56+
}),
57+
AdminEditCmsMetadataComponent,
58+
],
59+
providers: [
60+
{ provide: NotificationsService, useValue: NotificationsServiceStub },
61+
{ provide: SiteDataService, useValue: siteServiceStub },
62+
],
63+
schemas: [NO_ERRORS_SCHEMA],
64+
}).compileComponents();
65+
}));
66+
67+
beforeEach(() => {
68+
fixture = TestBed.createComponent(AdminEditCmsMetadataComponent);
69+
component = fixture.componentInstance;
70+
siteServiceStub.find.and.returnValue(of(site));
71+
siteServiceStub.patch.and.returnValue(of(site));
72+
});
73+
74+
describe('', () => {
75+
76+
beforeEach(() => {
77+
// component.editMode = false;
78+
fixture.detectChanges();
79+
});
80+
it('should create', () => {
81+
expect(component).toBeTruthy();
82+
});
83+
84+
it('should show metadata cms list correctly', () => {
85+
const metadataListLength = environment.cms.metadataList.length;
86+
const selectMetadata = fixture.debugElement.query(By.css('select'));
87+
expect(selectMetadata.children).toHaveSize(metadataListLength + 1);
88+
});
89+
90+
});
91+
92+
describe('when the edit button is clicked', () => {
93+
beforeEach(() => {
94+
spyOn(component, 'editSelectedMetadata');
95+
component.selectedMetadata = 'metadata';
96+
fixture.detectChanges();
97+
});
98+
it('should call selectMetadataToEdit', () => {
99+
const editButton = fixture.debugElement.query(By.css('#edit-metadata-btn'));
100+
editButton.nativeElement.click();
101+
expect(component.editSelectedMetadata).toHaveBeenCalled();
102+
});
103+
});
104+
105+
describe('after the button edit is clicked', () => {
106+
107+
beforeEach(() => {
108+
component.selectedMetadata = environment.cms.metadataList[0];
109+
component.selectedMetadataValues = metadataValueMap;
110+
component.editMode.next(true);
111+
fixture.detectChanges();
112+
});
113+
114+
it('should render textareas of the languages', () => {
115+
const languagesLength = environment.languages.filter((l) => l.active).length;
116+
const textareas = fixture.debugElement.queryAll(By.css('textarea'));
117+
console.log(textareas.length, languagesLength);
118+
expect(textareas).toHaveSize(languagesLength);
119+
});
120+
121+
describe('after the button save is clicked', () => {
122+
123+
it('should call method edit', () => {
124+
spyOn(component, 'saveMetadata');
125+
const saveButton = fixture.debugElement.query(By.css('#save-metadata-btn'));
126+
saveButton.nativeElement.click();
127+
expect(component.saveMetadata).toHaveBeenCalled();
128+
});
129+
130+
it('should call method patch of service', () => {
131+
component.selectedMetadata = environment.cms.metadataList[0];
132+
const saveButton = fixture.debugElement.query(By.css('#save-metadata-btn'));
133+
saveButton.nativeElement.click();
134+
const operations = [];
135+
operations.push({
136+
op: 'replace',
137+
path: '/metadata/' + component.selectedMetadata,
138+
value: {
139+
value: component.selectedMetadataValues.get(environment.languages[0].code),
140+
language: environment.languages[0].code,
141+
},
142+
});
143+
component.selectedMetadataValues.forEach((value, key) => {
144+
if (key !== environment.languages[0].code) {
145+
operations.push({
146+
op: 'add',
147+
path: '/metadata/' + component.selectedMetadata,
148+
value: {
149+
value: value,
150+
language: key,
151+
},
152+
});
153+
}
154+
});
155+
expect(siteServiceStub.patch).toHaveBeenCalledWith(site, operations);
156+
});
157+
158+
});
159+
});
160+
});
Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
import {
2+
AsyncPipe,
3+
NgForOf,
4+
NgIf,
5+
NgTemplateOutlet,
6+
} from '@angular/common';
7+
import {
8+
Component,
9+
OnInit,
10+
} from '@angular/core';
11+
import { FormsModule } from '@angular/forms';
12+
import {
13+
TranslateModule,
14+
TranslateService,
15+
} from '@ngx-translate/core';
16+
import { Operation } from 'fast-json-patch';
17+
import { BehaviorSubject } from 'rxjs';
18+
import { BtnDisabledDirective } from 'src/app/shared/btn-disabled.directive';
19+
20+
import { environment } from '../../../environments/environment';
21+
import { SiteDataService } from '../../core/data/site-data.service';
22+
import { getFirstCompletedRemoteData } from '../../core/shared/operators';
23+
import { Site } from '../../core/shared/site.model';
24+
import { NotificationsService } from '@dspace/core/notification-system/notifications.service';
25+
26+
/**
27+
* Component representing the page to edit cms metadata for site.
28+
*/
29+
@Component({
30+
selector: 'ds-edit-homepage-metadata',
31+
templateUrl: './admin-edit-cms-metadata.component.html',
32+
styleUrls: ['./admin-edit-cms-metadata.component.scss'],
33+
imports: [
34+
FormsModule,
35+
TranslateModule,
36+
AsyncPipe,
37+
NgTemplateOutlet,
38+
BtnDisabledDirective,
39+
],
40+
standalone: true,
41+
})
42+
export class AdminEditCmsMetadataComponent implements OnInit {
43+
/**
44+
* default value of the select options
45+
*/
46+
selectedMetadata: string;
47+
/**
48+
* default true to show the select options
49+
*/
50+
editMode: BehaviorSubject<boolean> = new BehaviorSubject(false);
51+
/**
52+
* The map between language codes available and their label
53+
*/
54+
languageMap: Map<string, string> = new Map();
55+
/**
56+
* The list of languages available
57+
*/
58+
languageList: string[] = [];
59+
/**
60+
* key value pair map with language and value of metadata
61+
*/
62+
selectedMetadataValues: Map<string, string> = new Map();
63+
/**
64+
* the owner object of the metadataList
65+
*/
66+
site: Site;
67+
/**
68+
* list of the metadata to be edited by the user
69+
*/
70+
metadataList: string[] = [];
71+
72+
constructor(
73+
private siteService: SiteDataService,
74+
private notificationsService: NotificationsService,
75+
private translateService: TranslateService,
76+
) {
77+
}
78+
79+
ngOnInit(): void {
80+
environment.languages.filter((language) => language.active).forEach((language) => {
81+
this.languageMap.set(language.code, language.label);
82+
this.languageList.push(language.code);
83+
});
84+
environment.cms.metadataList.forEach((md) => {
85+
this.metadataList.push(md);
86+
});
87+
this.siteService.find().subscribe((site) => {
88+
this.site = site;
89+
});
90+
}
91+
92+
/**
93+
* Save metadata values
94+
*/
95+
saveMetadata() {
96+
const operations = this.getOperationsToEditText();
97+
98+
this.siteService.patch(this.site, operations).pipe(getFirstCompletedRemoteData())
99+
.subscribe((restResponse) => {
100+
if (restResponse.hasSucceeded) {
101+
this.site = restResponse.payload;
102+
this.notificationsService.success(this.translateService.get('admin.edit-cms-metadata.success'));
103+
this.selectedMetadata = undefined;
104+
this.editMode.next(false);
105+
} else {
106+
this.notificationsService.error(this.translateService.get('admin.edit-cms-metadata.error'));
107+
}
108+
this.siteService.setStale();
109+
this.siteService.find().subscribe((site) => {
110+
this.site = site;
111+
});
112+
});
113+
}
114+
115+
/**
116+
* Reset metadata selection and go back to the select options
117+
*/
118+
back() {
119+
this.selectedMetadata = undefined;
120+
this.editMode.next(false);
121+
}
122+
123+
/**
124+
* Get the label for a language key using language map
125+
* @param key Key of the language to get the label for
126+
* @returns Label of the language if found, otherwise the key itself
127+
*/
128+
languageLabel(key: string) {
129+
return this.languageMap.get(key) ?? key;
130+
}
131+
132+
/**
133+
* Start editing selected metadata
134+
*/
135+
editSelectedMetadata() {
136+
if (this.selectedMetadata) {
137+
this.languageList.forEach((languageCode: string) => {
138+
const text = this.site.firstMetadataValue(this.selectedMetadata, { language: languageCode });
139+
this.selectedMetadataValues.set(languageCode, text);
140+
});
141+
}
142+
this.editMode.next(true);
143+
}
144+
145+
/**
146+
* Create a list of operations to send to backend to edit selected metadata
147+
* @returns List of operations to send to backend to edit selected metadata
148+
*/
149+
private getOperationsToEditText(): Operation[] {
150+
const entries = Array.from(this.selectedMetadataValues.entries());
151+
152+
// First entry should form a 'replace' operation, then the rest should be an 'add' operation
153+
return entries.map(([language, text], index) => ({
154+
op: index === 0 ? 'replace' : 'add',
155+
path: `/metadata/${this.selectedMetadata}`,
156+
value: {
157+
value: text ?? '',
158+
language: language,
159+
},
160+
}));
161+
}
162+
}

0 commit comments

Comments
 (0)