Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
1e9fbee
[DURACOM-453] add edit item menu and data service
FrancescoMolinaro Feb 13, 2026
a3c14c5
[DURACOM-453] finalize improvement for submission and edit mode
FrancescoMolinaro Feb 16, 2026
e0bb5f9
Merge branch 'task/main/DURACOM-444' into task/main/DURACOM-453
FrancescoMolinaro Feb 16, 2026
664e44f
Merge branch 'task/main/DURACOM-426' into task/main/DURACOM-453
FrancescoMolinaro Feb 16, 2026
2edc805
Merge remote-tracking branch 'gitHub/main' into task/main/DURACOM-453
FrancescoMolinaro Feb 16, 2026
850a973
[DURACOM-453] add metadata security and finilize edit mode
FrancescoMolinaro Feb 17, 2026
87cfa6e
[DURACOM-453] fix observable issue, add full projection
FrancescoMolinaro Feb 19, 2026
ab0885e
[DURACOM-453] add shared submission config and labels
FrancescoMolinaro Feb 20, 2026
c352534
[DURACOM-453] port metadata security update, init and administrate
FrancescoMolinaro Mar 10, 2026
31cafeb
[DURACOM-453] port error parsing in edit mode
FrancescoMolinaro Mar 11, 2026
8c656fe
Merge remote-tracking branch 'gitHub/main' into task/main/DURACOM-453
FrancescoMolinaro Mar 16, 2026
6369166
[DURACOM-453] remove collection form required from fields, fix metada…
FrancescoMolinaro Mar 17, 2026
b383541
[DURACOM-453] fix collection form metadata name
FrancescoMolinaro Mar 17, 2026
cb4bc13
[DURACOM-453] fix security level patch update, fix style in admin tab…
FrancescoMolinaro Mar 24, 2026
db7bd44
[DURACOM-453] handle errors on missing security settings endpoint
FrancescoMolinaro Mar 25, 2026
12047a5
Merge remote-tracking branch 'gitHub/main' into task/main/DURACOM-453
FrancescoMolinaro Mar 27, 2026
1faf25a
[DURACOM-453] fix missing security config in tests
FrancescoMolinaro Mar 27, 2026
7746789
Merge remote-tracking branch 'gitHub/main' into task/main/DURACOM-453
FrancescoMolinaro Mar 27, 2026
386078e
[DURACOM-453] refactor, hide *-edit forms, fix cache issue, add type …
FrancescoMolinaro Mar 30, 2026
8982db7
[DURACOM-453] add back button to footer, add breadcrumb in edit page,…
FrancescoMolinaro Mar 30, 2026
a84e2d3
[DURACOM-453] fix issue with missing collection step in edit mode, re…
FrancescoMolinaro Mar 31, 2026
2a8b2c5
Merge remote-tracking branch 'gitHub/main' into task/main/DURACOM-453
FrancescoMolinaro Mar 31, 2026
7ebc02f
Merge remote-tracking branch 'gitHub/main' into task/main/DURACOM-453
FrancescoMolinaro Mar 31, 2026
c46c5a6
[DURACOM-453] fix lint
FrancescoMolinaro Mar 31, 2026
bca2d61
[DURACOM-453] remove opened property, fix cache issue for bitstreams,…
FrancescoMolinaro Apr 1, 2026
c2c8abe
[DURACOM-453] correct other workspace config to use came case
FrancescoMolinaro Apr 1, 2026
e9705f0
[DURACOM-453] prevent possible issue with spy order on submission test
FrancescoMolinaro Apr 1, 2026
ac2d4de
[DURACOM-453] fix error handling in menu provider, update labels
FrancescoMolinaro Apr 2, 2026
1b5114c
Merge remote-tracking branch '4science_gitHub/task/main/DURACOM-453' …
FrancescoMolinaro Apr 2, 2026
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
15 changes: 14 additions & 1 deletion config/config.example.yml
Original file line number Diff line number Diff line change
Expand Up @@ -405,6 +405,20 @@ homePage:
item:
edit:
undoTimeout: 10000 # 10 seconds
# Defines the security levels available for metadata fields.
# 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.
# Levels are displayed as toggle buttons in the metadata edit UI.
security:
levels:
- value: 0 # Public — visible to everyone
icon: fa fa-globe
color: green
- value: 1 # Registered users — visible to authenticated users only
icon: fa fa-key
color: orange
- value: 2 # Administrators only — restricted to admin users
icon: fa fa-lock
color: red
# Show the item access status label in items lists
showAccessStatuses: false
bitstream:
Expand All @@ -419,7 +433,6 @@ item:
# Metdadata list to be displayed for entities without a specific configuration
fallbackMetdataList:
- dc.description.abstract
- dc.description.note
# Configuration for each entity type
entityDataConfig:
- entityType: Person
Expand Down
6 changes: 6 additions & 0 deletions src/app/app-routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import { ACCESS_CONTROL_MODULE_PATH } from './access-control/access-control-rout
import { NOTIFICATIONS_MODULE_PATH } from './admin/admin-routing-paths';
import {
ADMIN_MODULE_PATH,
EDIT_ITEM_PATH,
FORGOT_PASSWORD_PATH,
HEALTH_PAGE_PATH,
PROFILE_MODULE_PATH,
Expand Down Expand Up @@ -280,6 +281,11 @@ export const APP_ROUTES: Route[] = [
.then((m) => m.ROUTES),
canActivate: [authenticatedGuard],
},
{
path: EDIT_ITEM_PATH,
loadChildren: () => import('./edit-item/edit-item-routes').then((m) => m.ROUTES),
canActivate: [endUserAgreementCurrentUserGuard],
},
{
path: 'external-login/:token',
loadChildren: () => import('./external-login-page/external-login-routes').then((m) => m.ROUTES),
Expand Down
4 changes: 4 additions & 0 deletions src/app/app.menus.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import { DSpaceObjectEditMenuProvider } from './shared/menu/providers/dso-edit.m
import { DsoOptionMenuProvider } from './shared/menu/providers/dso-option.menu';
import { EditMenuProvider } from './shared/menu/providers/edit.menu';
import { EditCMSMetadataMenuProvider } from './shared/menu/providers/edit-cms-metadata.menu';
import { EditItemMenuProvider } from './shared/menu/providers/edit-item-details.menu';
import { EditUserAgreementMenuProvider } from './shared/menu/providers/edit-user-agreement.menu';
import { ExportMenuProvider } from './shared/menu/providers/export.menu';
import { HealthMenuProvider } from './shared/menu/providers/health.menu';
Expand Down Expand Up @@ -82,6 +83,9 @@ export const MENUS = buildMenuStructure({
],
[MenuID.DSO_EDIT]: [
DsoOptionMenuProvider.withSubs([
EditItemMenuProvider.onRoute(
MenuRoute.ITEM_PAGE,
),
SubscribeMenuProvider.onRoute(
MenuRoute.COMMUNITY_PAGE,
MenuRoute.COLLECTION_PAGE,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,27 +4,32 @@ import {
Component,
Input,
OnChanges,
OnDestroy,
OnInit,
SimpleChange,
SimpleChanges,
} from '@angular/core';
import { AuthService } from '@dspace/core/auth/auth.service';
import { ObjectCacheService } from '@dspace/core/cache/object-cache.service';
import { ConfigObject } from '@dspace/core/config/models/config.model';
import { SubmissionDefinitionModel } from '@dspace/core/config/models/config-submission-definition.model';
import { SubmissionDefinitionsConfigDataService } from '@dspace/core/config/submission-definitions-config-data.service';
import { CollectionDataService } from '@dspace/core/data/collection-data.service';
import { EntityTypeDataService } from '@dspace/core/data/entity-type-data.service';
import { RequestService } from '@dspace/core/data/request.service';
import { NotificationsService } from '@dspace/core/notification-system/notifications.service';
import { Collection } from '@dspace/core/shared/collection.model';
import { ItemType } from '@dspace/core/shared/item-relationships/item-type.model';
import { NONE_ENTITY_TYPE } from '@dspace/core/shared/item-relationships/item-type.resource-type';
import { MetadataValue } from '@dspace/core/shared/metadata.models';
import { getFirstSucceededRemoteListPayload } from '@dspace/core/shared/operators';
import {
hasNoValue,
hasValue,
isNotNull,
} from '@dspace/shared/utils/empty.util';
import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
import {
DynamicCheckboxModel,
DynamicFormControlModel,
DynamicFormOptionConfig,
DynamicFormService,
Expand All @@ -34,7 +39,13 @@ import {
TranslateModule,
TranslateService,
} from '@ngx-translate/core';
import { Observable } from 'rxjs';
import {
catchError,
forkJoin,
Observable,
of,
Subscription,
} from 'rxjs';

import { ComColFormComponent } from '../../shared/comcol/comcol-forms/comcol-form/comcol-form.component';
import { ComcolPageLogoComponent } from '../../shared/comcol/comcol-page-logo/comcol-page-logo.component';
Expand All @@ -44,6 +55,8 @@ import { VarDirective } from '../../shared/utils/var.directive';
import {
collectionFormEntityTypeSelectionConfig,
collectionFormModels,
collectionFormSharedWorkspaceCheckboxConfig,
collectionFormSubmissionDefinitionSelectionConfig,
} from './collection-form.models';

/**
Expand All @@ -62,7 +75,7 @@ import {
VarDirective,
],
})
export class CollectionFormComponent extends ComColFormComponent<Collection> implements OnInit, OnChanges {
export class CollectionFormComponent extends ComColFormComponent<Collection> implements OnInit, OnChanges, OnDestroy {
/**
* @type {Collection} A new collection when a collection is being created, an existing Input collection when a collection is being edited
*/
Expand All @@ -79,12 +92,27 @@ export class CollectionFormComponent extends ComColFormComponent<Collection> imp
*/
entityTypeSelection: DynamicSelectModel<string> = new DynamicSelectModel(collectionFormEntityTypeSelectionConfig);

/**
* The dynamic form field used for submission definition selection
* @type {DynamicSelectModel<string>}
*/
submissionDefinitionSelection: DynamicSelectModel<string> = new DynamicSelectModel(collectionFormSubmissionDefinitionSelectionConfig);

sharedWorkspaceChekbox: DynamicCheckboxModel = new DynamicCheckboxModel(collectionFormSharedWorkspaceCheckboxConfig);

/**
* The dynamic form fields used for creating/editing a collection
* @type {DynamicFormControlModel[]}
*/
formModel: DynamicFormControlModel[];

/**
* Subscription to unsubscribe on destroy
*
* @private
*/
private initSubscription: Subscription;

public constructor(protected formService: DynamicFormService,
protected translate: TranslateService,
protected notificationsService: NotificationsService,
Expand All @@ -94,6 +122,7 @@ export class CollectionFormComponent extends ComColFormComponent<Collection> imp
protected objectCache: ObjectCacheService,
protected entityTypeService: EntityTypeDataService,
protected chd: ChangeDetectorRef,
protected submissionDefinitionService: SubmissionDefinitionsConfigDataService,
protected modalService: NgbModal) {
super(formService, translate, notificationsService, authService, requestService, objectCache, modalService);
}
Expand All @@ -115,21 +144,45 @@ export class CollectionFormComponent extends ComColFormComponent<Collection> imp
}
}

/**
* Clean up eventual subscription when component gets destroyed
*/
ngOnDestroy() {
super.ngOnDestroy();
if (hasValue(this.initSubscription)) {
this.initSubscription.unsubscribe();
}
}

initializeForm() {
let currentRelationshipValue: MetadataValue[];
let currentDefinitionValue: MetadataValue[];
let currentSharedWorkspaceValue: MetadataValue[];
if (this.dso && this.dso.metadata) {
currentRelationshipValue = this.dso.metadata['dspace.entity.type'];
currentDefinitionValue = this.dso.metadata['dspace.submission.definition'];
currentSharedWorkspaceValue = this.dso.metadata['dspace.workspace.shared'];
}

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

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

entityTypes = entityTypes.filter((type: ItemType) => type.label !== NONE_ENTITY_TYPE);
entityTypes.forEach((type: ItemType, index: number) => {
// retrieve all entity types and submission definitions to populate the dropdowns selection
this.initSubscription = forkJoin({
entityTypes: entities$,
definitions: definitions$,
}).subscribe(({ entityTypes, definitions }: {entityTypes: ItemType[]; definitions: ConfigObject[]}) => {
Comment thread
alexandrevryghem marked this conversation as resolved.
const sortedEntityTypes = entityTypes
.sort((a, b) => a.label.localeCompare(b.label));

sortedEntityTypes.forEach((type: ItemType, index: number) => {
this.entityTypeSelection.add({
disabled: false,
label: type.label,
Expand All @@ -141,9 +194,26 @@ export class CollectionFormComponent extends ComColFormComponent<Collection> imp
}
});

this.formModel = entityTypes.length === 0 ? collectionFormModels : [...collectionFormModels, this.entityTypeSelection];
definitions.filter(def => !def.id.includes('-edit')).forEach((definition: SubmissionDefinitionModel, index: number) => {
this.submissionDefinitionSelection.add({
disabled: false,
label: definition.name,
value: definition.name,
} as DynamicFormOptionConfig<string>);
if (currentDefinitionValue && currentDefinitionValue.length > 0 && currentDefinitionValue[0].value === definition.name) {
this.submissionDefinitionSelection.select(index);
}
});

this.formModel = entityTypes.length === 0 ?
[...collectionFormModels, this.submissionDefinitionSelection, this.sharedWorkspaceChekbox] :
[...collectionFormModels, this.entityTypeSelection, this.submissionDefinitionSelection, this.sharedWorkspaceChekbox];

super.ngOnInit();

if (currentSharedWorkspaceValue && currentSharedWorkspaceValue.length > 0) {
this.sharedWorkspaceChekbox.value = currentSharedWorkspaceValue[0].value === 'true';
}
this.chd.detectChanges();
});

Expand Down
20 changes: 20 additions & 0 deletions src/app/collection-page/collection-form/collection-form.models.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import {
DynamicCheckboxModelConfig,
DynamicFormControlModel,
DynamicInputModel,
DynamicSelectModelConfig,
Expand All @@ -11,6 +12,25 @@ export const collectionFormEntityTypeSelectionConfig: DynamicSelectModelConfig<s
id: 'entityType',
name: 'dspace.entity.type',
disabled: false,
errorMessages: {
required: 'collection.form.errors.entityType.required',
},
};

export const collectionFormSubmissionDefinitionSelectionConfig: DynamicSelectModelConfig<string> = {
id: 'submissionDefinition',
name: 'dspace.submission.definition',
disabled: false,
errorMessages: {
required: 'collection.form.errors.submissionDefinition.required',
},
};


export const collectionFormSharedWorkspaceCheckboxConfig: DynamicCheckboxModelConfig = {
id: 'sharedWorkspace',
name: 'dspace.workspace.shared',
disabled: false,
};

/**
Expand Down
62 changes: 62 additions & 0 deletions src/app/core/breadcrumbs/edit-item-breadcrumb.resolver.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import {
EnvironmentInjector,
runInInjectionContext,
} from '@angular/core';
import { TestBed } from '@angular/core/testing';
import { editItemBreadcrumbResolver } from '@dspace/core/breadcrumbs/edit-item-breadcrumb.resolver';
import { Item } from '@dspace/core/shared/item.model';
import { getTestScheduler } from 'jasmine-marbles';
import { of } from 'rxjs';

import { ItemDataService } from '../data/item-data.service';
import { createSuccessfulRemoteDataObject$ } from '../utilities/remote-data.utils';
import { DSOBreadcrumbsService } from './dso-breadcrumbs.service';

describe('editItemBreadcrumbResolver', () => {
describe('resolve', () => {
let resolver: any;
let dsoBreadcrumbService: any;
let itemDataService: any;
let testItem: Item;
let uuid: string;
let breadcrumbUrl: string;
let currentUrl: string;

beforeEach(() => {
uuid = '1234-65487-12354-1235';
breadcrumbUrl = `/items/${uuid}`;
currentUrl = `${breadcrumbUrl}/edit`;
testItem = Object.assign(new Item(), {
uuid: uuid,
type: 'item',
});

itemDataService = {
findById: () => createSuccessfulRemoteDataObject$(testItem),
};

dsoBreadcrumbService = {
getRepresentativeName: () => testItem.uuid,
getBreadcrumbs: () => of({ provider: dsoBreadcrumbService, key: testItem, url: breadcrumbUrl }),
};

TestBed.configureTestingModule({
providers: [
{ provide: DSOBreadcrumbsService, useValue: dsoBreadcrumbService },
{ provide: ItemDataService, useValue: itemDataService },
],
});

resolver = editItemBreadcrumbResolver;
});

it('should resolve a breadcrumb config for the correct uuid', () => {
const injector = TestBed.inject(EnvironmentInjector);
const resolvedConfig = runInInjectionContext(injector, () =>
resolver({ params: { id: testItem.uuid + ':FULL' } } as any, { url: currentUrl } as any),
);
const expectedConfig = { provider: dsoBreadcrumbService, key: testItem, url: breadcrumbUrl };
getTestScheduler().expectObservable(resolvedConfig).toBe('(a|)', { a: expectedConfig });
});
});
});
27 changes: 27 additions & 0 deletions src/app/core/breadcrumbs/edit-item-breadcrumb.resolver.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import {
ActivatedRouteSnapshot,
ResolveFn,
RouterStateSnapshot,
} from '@angular/router';
import { itemBreadcrumbResolver } from '@dspace/core/breadcrumbs/item-breadcrumb.resolver';
import { Observable } from 'rxjs';

import { Item } from '../shared/item.model';
import { BreadcrumbConfig } from './models/breadcrumb-config.model';

/**
* The resolve function that resolves the BreadcrumbConfig object for an Item in edit mode
*/
export const editItemBreadcrumbResolver: ResolveFn<BreadcrumbConfig<Item>> = (
route: ActivatedRouteSnapshot,
state: RouterStateSnapshot,
): Observable<BreadcrumbConfig<Item>> => {
const routeWithCorrectId = Object.assign(route, {
params: {
...route.params,
id: route.params.id.split(':')[0],
},
});

return itemBreadcrumbResolver(routeWithCorrectId, state) as Observable<BreadcrumbConfig<Item>>;
};
1 change: 1 addition & 0 deletions src/app/core/cache/builders/remote-data-build.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -339,6 +339,7 @@ export class RemoteDataBuildService {
response.errorMessage,
payload,
response.statusCode,
response.errors,
);
}),
);
Expand Down
Loading
Loading