Skip to content

Commit

Permalink
feat: clone metadata in item
Browse files Browse the repository at this point in the history
  • Loading branch information
MMilosz committed May 27, 2024
1 parent 659052f commit c10e49f
Show file tree
Hide file tree
Showing 11 changed files with 344 additions and 1 deletion.
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@
<i class="fa fa-edit"></i><span *ngIf="!small" class="d-none d-sm-inline"> {{"admin.search.item.edit" | translate}}</span>
</a>

<a [ngClass]="{'btn-sm': small}" class="btn btn-secondary edit-link" [routerLink]="[getCloneRoute()]" [title]="'admin.search.item.clone' | translate">
<i class="fa fa-copy"></i><span *ngIf="!small" class="d-none d-sm-inline"> {{"admin.search.item.clone" | translate}}</span>
</a>

<a [ngClass]="{'btn-sm': small}" *ngIf="item && !item.isWithdrawn" class="btn btn-warning t withdraw-link" [routerLink]="[getWithdrawRoute()]" [title]="'admin.search.item.withdraw' | translate">
<i class="fa fa-ban"></i><span *ngIf="!small" class="d-none d-sm-inline"> {{"admin.search.item.withdraw" | translate}}</span>
</a>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { TranslateModule } from '@ngx-translate/core';
import { Item } from '../../../core/shared/item.model';
import { URLCombiner } from '../../../core/url-combiner/url-combiner';
import {
ITEM_EDIT_CLONE_PATH,
ITEM_EDIT_DELETE_PATH,
ITEM_EDIT_MOVE_PATH,
ITEM_EDIT_PRIVATE_PATH,
Expand Down Expand Up @@ -56,6 +57,9 @@ export class ItemAdminSearchResultActionsComponent {
return new URLCombiner(this.getEditRoute(), ITEM_EDIT_MOVE_PATH).toString();
}

getCloneRoute(): string {
return new URLCombiner(this.getEditRoute(), ITEM_EDIT_CLONE_PATH).toString();
}
/**
* Returns the path to the delete page of this item
*/
Expand Down
1 change: 1 addition & 0 deletions src/app/core/data/feature-authorization/feature-id.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ export enum FeatureID {
CanEditVersion = 'canEditVersion',
CanDeleteVersion = 'canDeleteVersion',
CanCreateVersion = 'canCreateVersion',
CanClone = CanCreateVersion,
CanViewUsageStatistics = 'canViewUsageStatistics',
CanSendFeedback = 'canSendFeedback',
CanClaimItem = 'canClaimItem',
Expand Down
37 changes: 37 additions & 0 deletions src/app/core/data/item-data.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -280,6 +280,18 @@ export abstract class BaseItemDataService extends IdentifiableDataService<Item>
);
}

/**
* Get the endpoint to clone the item
* @param itemId UUID of the source item
* @param collectionId UUID of the target collection
*/
public getCloneItemEndpoint(itemId: string, collectionId: string): Observable<string> {
return this.halService.getEndpoint('workspaceitems').pipe(
map((endpoint: string) => this.getIDHref(endpoint, itemId)),
map((endpoint: string) => `${endpoint}?owningCollection=${collectionId}`),

Check warning on line 291 in src/app/core/data/item-data.service.ts

View check run for this annotation

Codecov / codecov/patch

src/app/core/data/item-data.service.ts#L289-L291

Added lines #L289 - L291 were not covered by tests
);
}

/**
* Move the item to a different owning collection
* @param itemId
Expand Down Expand Up @@ -313,6 +325,31 @@ export abstract class BaseItemDataService extends IdentifiableDataService<Item>
return this.rdbService.buildFromRequestUUID(requestId);
}

/**
* Create a new item in a specified collection using properties from the existing item
* @param item a source item
* @param collectionId an UUID of a target collection
*/
public clone(item: Item, collectionId: string): Observable<RemoteData<any>> {
const options: HttpOptions = Object.create({});
let headers = new HttpHeaders();
headers = headers.append('Content-Type', 'text/uri-list');
options.headers = headers;

Check warning on line 337 in src/app/core/data/item-data.service.ts

View check run for this annotation

Codecov / codecov/patch

src/app/core/data/item-data.service.ts#L334-L337

Added lines #L334 - L337 were not covered by tests

const requestId = this.requestService.generateRequestId();
const href$ = this.getCloneItemEndpoint(item.id, collectionId);

Check warning on line 340 in src/app/core/data/item-data.service.ts

View check run for this annotation

Codecov / codecov/patch

src/app/core/data/item-data.service.ts#L339-L340

Added lines #L339 - L340 were not covered by tests

href$.pipe(
find((href: string) => hasValue(href)),

Check warning on line 343 in src/app/core/data/item-data.service.ts

View check run for this annotation

Codecov / codecov/patch

src/app/core/data/item-data.service.ts#L342-L343

Added lines #L342 - L343 were not covered by tests
map((href: string) => {
const request = new PostRequest(requestId, href, item._links.self.href, options);
this.requestService.send(request);

Check warning on line 346 in src/app/core/data/item-data.service.ts

View check run for this annotation

Codecov / codecov/patch

src/app/core/data/item-data.service.ts#L345-L346

Added lines #L345 - L346 were not covered by tests
}),
).subscribe();

return this.rdbService.buildFromRequestUUID(requestId);

Check warning on line 350 in src/app/core/data/item-data.service.ts

View check run for this annotation

Codecov / codecov/patch

src/app/core/data/item-data.service.ts#L350

Added line #L350 was not covered by tests
}

/**
* Import an external source entry into a collection
* @param externalSourceEntry
Expand Down
25 changes: 25 additions & 0 deletions src/app/core/submission/workspaceitem-data.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,31 @@ export class WorkspaceitemDataService extends IdentifiableDataService<WorkspaceI
return this.findByHref(href$, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow);
}

/**
* Create a new WorkspaceItem in a specified collection using properties from an existing item
* @param itemHref a source item
* @param collectionId an UUID of a target collection
*/
public cloneToCollection(itemHref: string, collectionId: string): Observable<RemoteData<WorkspaceItem>> {
const options: HttpOptions = Object.create({});
let headers = new HttpHeaders();
headers = headers.append('Content-Type', 'text/uri-list');
options.headers = headers;

Check warning on line 107 in src/app/core/submission/workspaceitem-data.service.ts

View check run for this annotation

Codecov / codecov/patch

src/app/core/submission/workspaceitem-data.service.ts#L104-L107

Added lines #L104 - L107 were not covered by tests

const requestId = this.requestService.generateRequestId();
const href$ = this.halService.getEndpoint(this.linkPath).pipe(map((href) => `${href}?owningCollection=${collectionId}`));

Check warning on line 110 in src/app/core/submission/workspaceitem-data.service.ts

View check run for this annotation

Codecov / codecov/patch

src/app/core/submission/workspaceitem-data.service.ts#L109-L110

Added lines #L109 - L110 were not covered by tests

href$.pipe(
find((href: string) => hasValue(href)),

Check warning on line 113 in src/app/core/submission/workspaceitem-data.service.ts

View check run for this annotation

Codecov / codecov/patch

src/app/core/submission/workspaceitem-data.service.ts#L112-L113

Added lines #L112 - L113 were not covered by tests
map((href: string) => {
const request = new PostRequest(requestId, href, itemHref, options);
this.requestService.send(request);

Check warning on line 116 in src/app/core/submission/workspaceitem-data.service.ts

View check run for this annotation

Codecov / codecov/patch

src/app/core/submission/workspaceitem-data.service.ts#L115-L116

Added lines #L115 - L116 were not covered by tests
}),
).subscribe();

return this.rdbService.buildFromRequestUUID(requestId);

Check warning on line 120 in src/app/core/submission/workspaceitem-data.service.ts

View check run for this annotation

Codecov / codecov/patch

src/app/core/submission/workspaceitem-data.service.ts#L120

Added line #L120 was not covered by tests
}

/**
* Import an external source entry into a collection
* @param externalSourceEntryHref
Expand Down
8 changes: 7 additions & 1 deletion src/app/item-page/edit-item-page/edit-item-page-routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { resourcePolicyTargetResolver } from '../../shared/resource-policies/res
import { EditItemPageComponent } from './edit-item-page.component';
import {
ITEM_EDIT_AUTHORIZATIONS_PATH,
ITEM_EDIT_CLONE_PATH,
ITEM_EDIT_DELETE_PATH,
ITEM_EDIT_MOVE_PATH,
ITEM_EDIT_PRIVATE_PATH,
Expand All @@ -23,6 +24,7 @@ import {
import { ItemAccessControlComponent } from './item-access-control/item-access-control.component';
import { ItemAuthorizationsComponent } from './item-authorizations/item-authorizations.component';
import { ItemBitstreamsComponent } from './item-bitstreams/item-bitstreams.component';
import { ItemCloneComponent } from './item-clone/item-clone.component';
import { ItemCollectionMapperComponent } from './item-collection-mapper/item-collection-mapper.component';
import { ItemCurateComponent } from './item-curate/item-curate.component';
import { ItemDeleteComponent } from './item-delete/item-delete.component';
Expand All @@ -46,7 +48,6 @@ import { ItemRelationshipsComponent } from './item-relationships/item-relationsh
import { ThemedItemStatusComponent } from './item-status/themed-item-status.component';
import { ItemVersionHistoryComponent } from './item-version-history/item-version-history.component';
import { ItemWithdrawComponent } from './item-withdraw/item-withdraw.component';

/**
* Routing module that handles the routing for the Edit Item page administrator functionality
*/
Expand Down Expand Up @@ -161,6 +162,11 @@ export const ROUTES: Route[] = [
component: ItemMoveComponent,
data: { title: 'item.edit.move.title' },
},
{
path: ITEM_EDIT_CLONE_PATH,
component: ItemCloneComponent,
data: { title: 'item.edit.clone.title' },
},
{
path: ITEM_EDIT_REGISTER_DOI_PATH,
component: ItemRegisterDoiComponent,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,6 @@ export const ITEM_EDIT_PRIVATE_PATH = 'private';
export const ITEM_EDIT_PUBLIC_PATH = 'public';
export const ITEM_EDIT_DELETE_PATH = 'delete';
export const ITEM_EDIT_MOVE_PATH = 'move';
export const ITEM_EDIT_CLONE_PATH = 'clone';
export const ITEM_EDIT_AUTHORIZATIONS_PATH = 'authorizations';
export const ITEM_EDIT_REGISTER_DOI_PATH = 'register-doi';
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
<div class="container">
<div class="row">
<div class="col-12">
<h1>{{'item.edit.clone.head' | translate: {id: (itemRD$ | async)?.payload?.handle} }}</h1>
<p>{{'item.edit.clone.description' | translate}}</p>
<div class="row">
<div class="col-12">
<div class="card mb-3">
<div class="card-header">{{'dso-selector.placeholder' | translate: { type: 'dso-selector.placeholder.type.collection' | translate } }}</div>
<div class="card-body">
<ds-authorized-collection-selector [types]="COLLECTIONS"
[entityType]="item.getRenderTypes[0]"
[currentDSOId]="selectedCollection ? selectedCollection.id : originalCollection.id"
(onSelect)="selectDso($event)">
</ds-authorized-collection-selector>
</div>
<div></div>
</div>
</div>
</div>
<!--<div class="row">
<div class="col-12">
<p>
<label for="inheritPoliciesCheckbox">
<ng-template #tooltipContent>
{{ 'item.edit.clone.inheritpolicies.tooltip' | translate }}
</ng-template>
<input type="checkbox" name="tc" [(ngModel)]="inheritPolicies" id="inheritPoliciesCheckbox" [ngbTooltip]="tooltipContent"
>
{{'item.edit.clone.inheritpolicies.checkbox' |translate}}
</label>
</p>
<p>
{{'item.edit.clone.inheritpolicies.description' | translate}}
</p>
</div>
</div>-->

<div class="button-row bottom">
<div class="float-right space-children-mr">
<button [routerLink]="[(itemPageRoute$ | async), 'edit']" class="btn btn-outline-secondary">
<i class="fas fa-arrow-left"></i> {{'item.edit.clone.cancel' | translate}}
</button>
<button class="btn btn-primary" [disabled]="!canClone" (click)="cloneToCollection()">
<span *ngIf="!processing">
<i class="fas fa-save"></i> {{'item.edit.clone.save-button' | translate}}
</span>
<span *ngIf="processing">
<i class="fas fa-circle-notch fa-spin"></i> {{'item.edit.clone.processing' | translate}}
</span>
</button>
<button class="btn btn-danger" [disabled]="!canSubmit" (click)="discard()">
<i class="fas fa-times"></i> {{"item.edit.clone.discard-button" | translate}}
</button>
</div>
</div>
</div>
</div>
</div>
175 changes: 175 additions & 0 deletions src/app/item-page/edit-item-page/item-clone/item-clone.component.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
import {
AsyncPipe,
NgIf,
} from '@angular/common';
import {
Component,
OnInit,
} from '@angular/core';
import {
FormsModule,
ReactiveFormsModule,
} from '@angular/forms';
import {
ActivatedRoute,
Router,
RouterLink,
} from '@angular/router';
import { NgbTooltipModule } from '@ng-bootstrap/ng-bootstrap';
import {
TranslateModule,
TranslateService,
} from '@ngx-translate/core';
import { Observable } from 'rxjs';
import {
map,
switchMap,
} from 'rxjs/operators';

import { DSONameService } from '../../../core/breadcrumbs/dso-name.service';
import { ItemDataService } from '../../../core/data/item-data.service';
import { RemoteData } from '../../../core/data/remote-data';
import { RequestService } from '../../../core/data/request.service';
import { Collection } from '../../../core/shared/collection.model';
import { DSpaceObjectType } from '../../../core/shared/dspace-object-type.model';
import { Item } from '../../../core/shared/item.model';
import {
getAllSucceededRemoteDataPayload,
getFirstCompletedRemoteData,
getFirstSucceededRemoteData,
getFirstSucceededRemoteDataPayload,
getRemoteDataPayload,
} from '../../../core/shared/operators';
import { SearchService } from '../../../core/shared/search/search.service';
import { WorkspaceItem } from '../../../core/submission/models/workspaceitem.model';
import { WorkspaceitemDataService } from '../../../core/submission/workspaceitem-data.service';
import { AuthorizedCollectionSelectorComponent } from '../../../shared/dso-selector/dso-selector/authorized-collection-selector/authorized-collection-selector.component';
import { NotificationsService } from '../../../shared/notifications/notifications.service';
import { getItemPageRoute } from '../../item-page-routing-paths';

@Component({
selector: 'ds-item-clone',
templateUrl: './item-clone.component.html',
imports: [
AsyncPipe,
AuthorizedCollectionSelectorComponent,
NgIf,
ReactiveFormsModule,
TranslateModule,
FormsModule,
NgbTooltipModule,
RouterLink,
],
standalone: true,
})
export class ItemCloneComponent implements OnInit {
/**
* TODO: Similarly to {@code ItemMoveComponent}, there is currently no backend support to change the
* owningCollection and inherit policies, hence the code that was commented out
*/

selectorType = DSpaceObjectType.COLLECTION;

inheritPolicies = false;
itemRD$: Observable<RemoteData<Item>>;
originalCollection: Collection;

selectedCollectionName: string;
selectedCollection: Collection;
canSubmit = false;

item: Item;
processing = false;

/**
* Route to the item's page
*/
itemPageRoute$: Observable<string>;

COLLECTIONS = [DSpaceObjectType.COLLECTION];

constructor(private route: ActivatedRoute,
private router: Router,
private notificationsService: NotificationsService,
private itemDataService: ItemDataService,
private workspaceItemDataService: WorkspaceitemDataService,
private searchService: SearchService,
private translateService: TranslateService,
private requestService: RequestService,
protected dsoNameService: DSONameService,
) {}

ngOnInit(): void {
this.itemRD$ = this.route.data.pipe(
map((data) => data.dso), getFirstSucceededRemoteData(),
) as Observable<RemoteData<Item>>;
this.itemPageRoute$ = this.itemRD$.pipe(
getAllSucceededRemoteDataPayload(),
map((item) => getItemPageRoute(item)),
);
this.itemRD$.subscribe((rd) => {
this.item = rd.payload;
},
);
this.itemRD$.pipe(
getFirstSucceededRemoteData(),
getRemoteDataPayload(),
switchMap((item) => item.owningCollection),
getFirstSucceededRemoteData(),
getRemoteDataPayload(),
).subscribe((collection) => {
this.originalCollection = collection;
});
}

/**
* Set the collection name and id based on the selected value
* @param data - obtained from the ds-input-suggestions component
*/
selectDso(data: any): void {
this.selectedCollection = data;
this.selectedCollectionName = this.dsoNameService.getName(data);
this.canSubmit = true;
}

/**
* @returns {string} the current URL
*/
getCurrentUrl() {
return this.router.url;
}

/**
* Moves the item to a new collection based on the selected collection
*/
cloneToCollection() {
this.processing = true;
const clone$ = this.workspaceItemDataService.cloneToCollection(this.item._links.self.href, this.selectedCollection.id)
.pipe(getFirstCompletedRemoteData());

clone$.subscribe((response: RemoteData<any>) => {
if (response.hasSucceeded) {
this.notificationsService.success(this.translateService.get('item.edit.clone.success'));
} else {
this.notificationsService.error(this.translateService.get('item.edit.clone.error'));
}
});

clone$.pipe(
getFirstSucceededRemoteDataPayload<WorkspaceItem>())
.subscribe((wsi) => {
this.processing = false;
this.router.navigate(['/workspaceitems', wsi.id, 'edit']);
});

}

discard(): void {
this.selectedCollection = null;
this.canSubmit = false;
}

get canClone(): boolean {
return this.canSubmit;
}
}
Loading

0 comments on commit c10e49f

Please sign in to comment.