Skip to content

Commit 781bef5

Browse files
authored
Merge pull request DSpace#5145 from 4Science/task/main/DURACOM-453
[DSpace-CRIS] Administrative Edit of archived items via submission form and security configuration for metadata visibility (Frontend)
2 parents b20b47c + 1b5114c commit 781bef5

File tree

150 files changed

+4738
-955
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

150 files changed

+4738
-955
lines changed

config/config.example.yml

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -405,6 +405,20 @@ homePage:
405405
item:
406406
edit:
407407
undoTimeout: 10000 # 10 seconds
408+
# Defines the security levels available for metadata fields.
409+
# Each level maps to a numeric value stored on the metadata field in the backend and controls visibility of that field based on the user's role.
410+
# Levels are displayed as toggle buttons in the metadata edit UI.
411+
security:
412+
levels:
413+
- value: 0 # Public — visible to everyone
414+
icon: fa fa-globe
415+
color: green
416+
- value: 1 # Registered users — visible to authenticated users only
417+
icon: fa fa-key
418+
color: orange
419+
- value: 2 # Administrators only — restricted to admin users
420+
icon: fa fa-lock
421+
color: red
408422
# Show the item access status label in items lists
409423
showAccessStatuses: false
410424
bitstream:
@@ -419,7 +433,6 @@ item:
419433
# Metdadata list to be displayed for entities without a specific configuration
420434
fallbackMetdataList:
421435
- dc.description.abstract
422-
- dc.description.note
423436
# Configuration for each entity type
424437
entityDataConfig:
425438
- entityType: Person

src/app/app-routes.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import { ACCESS_CONTROL_MODULE_PATH } from './access-control/access-control-rout
2828
import { NOTIFICATIONS_MODULE_PATH } from './admin/admin-routing-paths';
2929
import {
3030
ADMIN_MODULE_PATH,
31+
EDIT_ITEM_PATH,
3132
FORGOT_PASSWORD_PATH,
3233
HEALTH_PAGE_PATH,
3334
PROFILE_MODULE_PATH,
@@ -280,6 +281,11 @@ export const APP_ROUTES: Route[] = [
280281
.then((m) => m.ROUTES),
281282
canActivate: [authenticatedGuard],
282283
},
284+
{
285+
path: EDIT_ITEM_PATH,
286+
loadChildren: () => import('./edit-item/edit-item-routes').then((m) => m.ROUTES),
287+
canActivate: [endUserAgreementCurrentUserGuard],
288+
},
283289
{
284290
path: 'external-login/:token',
285291
loadChildren: () => import('./external-login-page/external-login-routes').then((m) => m.ROUTES),

src/app/app.menus.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import { DSpaceObjectEditMenuProvider } from './shared/menu/providers/dso-edit.m
2222
import { DsoOptionMenuProvider } from './shared/menu/providers/dso-option.menu';
2323
import { EditMenuProvider } from './shared/menu/providers/edit.menu';
2424
import { EditCMSMetadataMenuProvider } from './shared/menu/providers/edit-cms-metadata.menu';
25+
import { EditItemMenuProvider } from './shared/menu/providers/edit-item-details.menu';
2526
import { EditUserAgreementMenuProvider } from './shared/menu/providers/edit-user-agreement.menu';
2627
import { ExportMenuProvider } from './shared/menu/providers/export.menu';
2728
import { HealthMenuProvider } from './shared/menu/providers/health.menu';
@@ -82,6 +83,9 @@ export const MENUS = buildMenuStructure({
8283
],
8384
[MenuID.DSO_EDIT]: [
8485
DsoOptionMenuProvider.withSubs([
86+
EditItemMenuProvider.onRoute(
87+
MenuRoute.ITEM_PAGE,
88+
),
8589
SubscribeMenuProvider.onRoute(
8690
MenuRoute.COMMUNITY_PAGE,
8791
MenuRoute.COLLECTION_PAGE,

src/app/collection-page/collection-form/collection-form.component.ts

Lines changed: 78 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,27 +4,32 @@ import {
44
Component,
55
Input,
66
OnChanges,
7+
OnDestroy,
78
OnInit,
89
SimpleChange,
910
SimpleChanges,
1011
} from '@angular/core';
1112
import { AuthService } from '@dspace/core/auth/auth.service';
1213
import { ObjectCacheService } from '@dspace/core/cache/object-cache.service';
14+
import { ConfigObject } from '@dspace/core/config/models/config.model';
15+
import { SubmissionDefinitionModel } from '@dspace/core/config/models/config-submission-definition.model';
16+
import { SubmissionDefinitionsConfigDataService } from '@dspace/core/config/submission-definitions-config-data.service';
1317
import { CollectionDataService } from '@dspace/core/data/collection-data.service';
1418
import { EntityTypeDataService } from '@dspace/core/data/entity-type-data.service';
1519
import { RequestService } from '@dspace/core/data/request.service';
1620
import { NotificationsService } from '@dspace/core/notification-system/notifications.service';
1721
import { Collection } from '@dspace/core/shared/collection.model';
1822
import { ItemType } from '@dspace/core/shared/item-relationships/item-type.model';
19-
import { NONE_ENTITY_TYPE } from '@dspace/core/shared/item-relationships/item-type.resource-type';
2023
import { MetadataValue } from '@dspace/core/shared/metadata.models';
2124
import { getFirstSucceededRemoteListPayload } from '@dspace/core/shared/operators';
2225
import {
2326
hasNoValue,
27+
hasValue,
2428
isNotNull,
2529
} from '@dspace/shared/utils/empty.util';
2630
import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
2731
import {
32+
DynamicCheckboxModel,
2833
DynamicFormControlModel,
2934
DynamicFormOptionConfig,
3035
DynamicFormService,
@@ -34,7 +39,13 @@ import {
3439
TranslateModule,
3540
TranslateService,
3641
} from '@ngx-translate/core';
37-
import { Observable } from 'rxjs';
42+
import {
43+
catchError,
44+
forkJoin,
45+
Observable,
46+
of,
47+
Subscription,
48+
} from 'rxjs';
3849

3950
import { ComColFormComponent } from '../../shared/comcol/comcol-forms/comcol-form/comcol-form.component';
4051
import { ComcolPageLogoComponent } from '../../shared/comcol/comcol-page-logo/comcol-page-logo.component';
@@ -44,6 +55,8 @@ import { VarDirective } from '../../shared/utils/var.directive';
4455
import {
4556
collectionFormEntityTypeSelectionConfig,
4657
collectionFormModels,
58+
collectionFormSharedWorkspaceCheckboxConfig,
59+
collectionFormSubmissionDefinitionSelectionConfig,
4760
} from './collection-form.models';
4861

4962
/**
@@ -62,7 +75,7 @@ import {
6275
VarDirective,
6376
],
6477
})
65-
export class CollectionFormComponent extends ComColFormComponent<Collection> implements OnInit, OnChanges {
78+
export class CollectionFormComponent extends ComColFormComponent<Collection> implements OnInit, OnChanges, OnDestroy {
6679
/**
6780
* @type {Collection} A new collection when a collection is being created, an existing Input collection when a collection is being edited
6881
*/
@@ -79,12 +92,27 @@ export class CollectionFormComponent extends ComColFormComponent<Collection> imp
7992
*/
8093
entityTypeSelection: DynamicSelectModel<string> = new DynamicSelectModel(collectionFormEntityTypeSelectionConfig);
8194

95+
/**
96+
* The dynamic form field used for submission definition selection
97+
* @type {DynamicSelectModel<string>}
98+
*/
99+
submissionDefinitionSelection: DynamicSelectModel<string> = new DynamicSelectModel(collectionFormSubmissionDefinitionSelectionConfig);
100+
101+
sharedWorkspaceChekbox: DynamicCheckboxModel = new DynamicCheckboxModel(collectionFormSharedWorkspaceCheckboxConfig);
102+
82103
/**
83104
* The dynamic form fields used for creating/editing a collection
84105
* @type {DynamicFormControlModel[]}
85106
*/
86107
formModel: DynamicFormControlModel[];
87108

109+
/**
110+
* Subscription to unsubscribe on destroy
111+
*
112+
* @private
113+
*/
114+
private initSubscription: Subscription;
115+
88116
public constructor(protected formService: DynamicFormService,
89117
protected translate: TranslateService,
90118
protected notificationsService: NotificationsService,
@@ -94,6 +122,7 @@ export class CollectionFormComponent extends ComColFormComponent<Collection> imp
94122
protected objectCache: ObjectCacheService,
95123
protected entityTypeService: EntityTypeDataService,
96124
protected chd: ChangeDetectorRef,
125+
protected submissionDefinitionService: SubmissionDefinitionsConfigDataService,
97126
protected modalService: NgbModal) {
98127
super(formService, translate, notificationsService, authService, requestService, objectCache, modalService);
99128
}
@@ -115,21 +144,45 @@ export class CollectionFormComponent extends ComColFormComponent<Collection> imp
115144
}
116145
}
117146

147+
/**
148+
* Clean up eventual subscription when component gets destroyed
149+
*/
150+
ngOnDestroy() {
151+
super.ngOnDestroy();
152+
if (hasValue(this.initSubscription)) {
153+
this.initSubscription.unsubscribe();
154+
}
155+
}
156+
118157
initializeForm() {
119158
let currentRelationshipValue: MetadataValue[];
159+
let currentDefinitionValue: MetadataValue[];
160+
let currentSharedWorkspaceValue: MetadataValue[];
120161
if (this.dso && this.dso.metadata) {
121162
currentRelationshipValue = this.dso.metadata['dspace.entity.type'];
163+
currentDefinitionValue = this.dso.metadata['dspace.submission.definition'];
164+
currentSharedWorkspaceValue = this.dso.metadata['dspace.workspace.shared'];
122165
}
123166

124167
const entities$: Observable<ItemType[]> = this.entityTypeService.findAll({ elementsPerPage: 100, currentPage: 1 }).pipe(
125168
getFirstSucceededRemoteListPayload(),
126169
);
127170

128-
// retrieve all entity types to populate the dropdowns selection
129-
entities$.subscribe((entityTypes: ItemType[]) => {
171+
const definitions$: Observable<ConfigObject[]> = this.submissionDefinitionService
172+
.findAll({ elementsPerPage: 100, currentPage: 1 }).pipe(
173+
getFirstSucceededRemoteListPayload(),
174+
catchError(() => of([])),
175+
);
130176

131-
entityTypes = entityTypes.filter((type: ItemType) => type.label !== NONE_ENTITY_TYPE);
132-
entityTypes.forEach((type: ItemType, index: number) => {
177+
// retrieve all entity types and submission definitions to populate the dropdowns selection
178+
this.initSubscription = forkJoin({
179+
entityTypes: entities$,
180+
definitions: definitions$,
181+
}).subscribe(({ entityTypes, definitions }: {entityTypes: ItemType[]; definitions: ConfigObject[]}) => {
182+
const sortedEntityTypes = entityTypes
183+
.sort((a, b) => a.label.localeCompare(b.label));
184+
185+
sortedEntityTypes.forEach((type: ItemType, index: number) => {
133186
this.entityTypeSelection.add({
134187
disabled: false,
135188
label: type.label,
@@ -141,9 +194,26 @@ export class CollectionFormComponent extends ComColFormComponent<Collection> imp
141194
}
142195
});
143196

144-
this.formModel = entityTypes.length === 0 ? collectionFormModels : [...collectionFormModels, this.entityTypeSelection];
197+
definitions.filter(def => !def.id.includes('-edit')).forEach((definition: SubmissionDefinitionModel, index: number) => {
198+
this.submissionDefinitionSelection.add({
199+
disabled: false,
200+
label: definition.name,
201+
value: definition.name,
202+
} as DynamicFormOptionConfig<string>);
203+
if (currentDefinitionValue && currentDefinitionValue.length > 0 && currentDefinitionValue[0].value === definition.name) {
204+
this.submissionDefinitionSelection.select(index);
205+
}
206+
});
207+
208+
this.formModel = entityTypes.length === 0 ?
209+
[...collectionFormModels, this.submissionDefinitionSelection, this.sharedWorkspaceChekbox] :
210+
[...collectionFormModels, this.entityTypeSelection, this.submissionDefinitionSelection, this.sharedWorkspaceChekbox];
145211

146212
super.ngOnInit();
213+
214+
if (currentSharedWorkspaceValue && currentSharedWorkspaceValue.length > 0) {
215+
this.sharedWorkspaceChekbox.value = currentSharedWorkspaceValue[0].value === 'true';
216+
}
147217
this.chd.detectChanges();
148218
});
149219

src/app/collection-page/collection-form/collection-form.models.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import {
2+
DynamicCheckboxModelConfig,
23
DynamicFormControlModel,
34
DynamicInputModel,
45
DynamicSelectModelConfig,
@@ -11,6 +12,25 @@ export const collectionFormEntityTypeSelectionConfig: DynamicSelectModelConfig<s
1112
id: 'entityType',
1213
name: 'dspace.entity.type',
1314
disabled: false,
15+
errorMessages: {
16+
required: 'collection.form.errors.entityType.required',
17+
},
18+
};
19+
20+
export const collectionFormSubmissionDefinitionSelectionConfig: DynamicSelectModelConfig<string> = {
21+
id: 'submissionDefinition',
22+
name: 'dspace.submission.definition',
23+
disabled: false,
24+
errorMessages: {
25+
required: 'collection.form.errors.submissionDefinition.required',
26+
},
27+
};
28+
29+
30+
export const collectionFormSharedWorkspaceCheckboxConfig: DynamicCheckboxModelConfig = {
31+
id: 'sharedWorkspace',
32+
name: 'dspace.workspace.shared',
33+
disabled: false,
1434
};
1535

1636
/**
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import {
2+
EnvironmentInjector,
3+
runInInjectionContext,
4+
} from '@angular/core';
5+
import { TestBed } from '@angular/core/testing';
6+
import { editItemBreadcrumbResolver } from '@dspace/core/breadcrumbs/edit-item-breadcrumb.resolver';
7+
import { Item } from '@dspace/core/shared/item.model';
8+
import { getTestScheduler } from 'jasmine-marbles';
9+
import { of } from 'rxjs';
10+
11+
import { ItemDataService } from '../data/item-data.service';
12+
import { createSuccessfulRemoteDataObject$ } from '../utilities/remote-data.utils';
13+
import { DSOBreadcrumbsService } from './dso-breadcrumbs.service';
14+
15+
describe('editItemBreadcrumbResolver', () => {
16+
describe('resolve', () => {
17+
let resolver: any;
18+
let dsoBreadcrumbService: any;
19+
let itemDataService: any;
20+
let testItem: Item;
21+
let uuid: string;
22+
let breadcrumbUrl: string;
23+
let currentUrl: string;
24+
25+
beforeEach(() => {
26+
uuid = '1234-65487-12354-1235';
27+
breadcrumbUrl = `/items/${uuid}`;
28+
currentUrl = `${breadcrumbUrl}/edit`;
29+
testItem = Object.assign(new Item(), {
30+
uuid: uuid,
31+
type: 'item',
32+
});
33+
34+
itemDataService = {
35+
findById: () => createSuccessfulRemoteDataObject$(testItem),
36+
};
37+
38+
dsoBreadcrumbService = {
39+
getRepresentativeName: () => testItem.uuid,
40+
getBreadcrumbs: () => of({ provider: dsoBreadcrumbService, key: testItem, url: breadcrumbUrl }),
41+
};
42+
43+
TestBed.configureTestingModule({
44+
providers: [
45+
{ provide: DSOBreadcrumbsService, useValue: dsoBreadcrumbService },
46+
{ provide: ItemDataService, useValue: itemDataService },
47+
],
48+
});
49+
50+
resolver = editItemBreadcrumbResolver;
51+
});
52+
53+
it('should resolve a breadcrumb config for the correct uuid', () => {
54+
const injector = TestBed.inject(EnvironmentInjector);
55+
const resolvedConfig = runInInjectionContext(injector, () =>
56+
resolver({ params: { id: testItem.uuid + ':FULL' } } as any, { url: currentUrl } as any),
57+
);
58+
const expectedConfig = { provider: dsoBreadcrumbService, key: testItem, url: breadcrumbUrl };
59+
getTestScheduler().expectObservable(resolvedConfig).toBe('(a|)', { a: expectedConfig });
60+
});
61+
});
62+
});
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import {
2+
ActivatedRouteSnapshot,
3+
ResolveFn,
4+
RouterStateSnapshot,
5+
} from '@angular/router';
6+
import { itemBreadcrumbResolver } from '@dspace/core/breadcrumbs/item-breadcrumb.resolver';
7+
import { Observable } from 'rxjs';
8+
9+
import { Item } from '../shared/item.model';
10+
import { BreadcrumbConfig } from './models/breadcrumb-config.model';
11+
12+
/**
13+
* The resolve function that resolves the BreadcrumbConfig object for an Item in edit mode
14+
*/
15+
export const editItemBreadcrumbResolver: ResolveFn<BreadcrumbConfig<Item>> = (
16+
route: ActivatedRouteSnapshot,
17+
state: RouterStateSnapshot,
18+
): Observable<BreadcrumbConfig<Item>> => {
19+
const routeWithCorrectId = Object.assign(route, {
20+
params: {
21+
...route.params,
22+
id: route.params.id.split(':')[0],
23+
},
24+
});
25+
26+
return itemBreadcrumbResolver(routeWithCorrectId, state) as Observable<BreadcrumbConfig<Item>>;
27+
};

src/app/core/cache/builders/remote-data-build.service.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -339,6 +339,7 @@ export class RemoteDataBuildService {
339339
response.errorMessage,
340340
payload,
341341
response.statusCode,
342+
response.errors,
342343
);
343344
}),
344345
);

0 commit comments

Comments
 (0)