[CST-4499] Version history - Changes to version history table

This commit is contained in:
Davide Negretti
2021-10-15 19:13:15 +02:00
parent 850970e204
commit e4d468b17c
10 changed files with 189 additions and 68 deletions

View File

@@ -59,6 +59,7 @@ export class ItemDataService extends DataService<Item> {
* Get the endpoint for browsing items
* (When options.sort.field is empty, the default field to browse by will be 'dc.date.issued')
* @param {FindListOptions} options
* @param linkPath
* @returns {Observable<string>}
*/
public getBrowseEndpoint(options: FindListOptions = {}, linkPath: string = this.linkPath): Observable<string> {
@@ -287,4 +288,13 @@ export class ItemDataService extends DataService<Item> {
switchMap((url: string) => this.halService.getEndpoint('bitstreams', `${url}/${itemId}`))
);
}
/**
* Invalidate the cache of the item
* @param itemUUID
*/
invalidateItemCache(itemUUID: string) {
this.requestService.setStaleByHrefSubstring('item/' + itemUUID);
}
}

View File

@@ -11,7 +11,7 @@ import { NotificationsService } from '../../shared/notifications/notifications.s
import { HttpClient } from '@angular/common/http';
import { DefaultChangeAnalyzer } from './default-change-analyzer.service';
import { FindListOptions } from './request.models';
import { Observable, of } from 'rxjs';
import { EMPTY, Observable } from 'rxjs';
import { dataService } from '../cache/builders/build-decorators';
import { VERSION } from '../shared/version.resource-type';
import { VersionHistory } from '../shared/version-history.model';
@@ -55,7 +55,7 @@ export class VersionDataService extends DataService<Version> {
getFirstSucceededRemoteDataPayload(),
switchMap((res) => res.versionhistory),
getFirstSucceededRemoteDataPayload(),
) : of(null);
) : EMPTY;
}
/**

View File

@@ -196,6 +196,10 @@ export class VersionHistoryDataService extends DataService<VersionHistory> {
);
}
/**
* Get the item of the latest version from any version in the version history
* @param version
*/
getVersionHistoryFromVersion$(version: Version): Observable<VersionHistory> {
return this.versionDataService.getHistoryIdFromVersion$(version).pipe(
take(1),
@@ -204,6 +208,10 @@ export class VersionHistoryDataService extends DataService<VersionHistory> {
);
}
/**
* Invalidate the cache of the version history
* @param versionHistoryID
*/
invalidateVersionHistoryCache(versionHistoryID: string) {
this.requestService.setStaleByHrefSubstring('versioning/versionhistories/' + versionHistoryID);
}

View File

@@ -9,7 +9,7 @@ import { DataService } from '../data/data.service';
import { RequestService } from '../data/request.service';
import { WorkflowItem } from './models/workflowitem.model';
import { HALEndpointService } from '../shared/hal-endpoint.service';
import { DeleteByIDRequest } from '../data/request.models';
import { DeleteByIDRequest, FindListOptions } from '../data/request.models';
import { NotificationsService } from '../../shared/notifications/notifications.service';
import { ObjectCacheService } from '../cache/object-cache.service';
import { DSOChangeAnalyzer } from '../data/dso-change-analyzer.service';
@@ -19,6 +19,9 @@ import { hasValue } from '../../shared/empty.util';
import { RemoteData } from '../data/remote-data';
import { NoContent } from '../shared/NoContent.model';
import { getFirstCompletedRemoteData } from '../shared/operators';
import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model';
import { WorkspaceItem } from './models/workspaceitem.model';
import { RequestParam } from '../cache/models/request-param.model';
/**
* A service that provides methods to make REST requests with workflow items endpoint.
@@ -27,6 +30,7 @@ import { getFirstCompletedRemoteData } from '../shared/operators';
@dataService(WorkflowItem.type)
export class WorkflowItemDataService extends DataService<WorkflowItem> {
protected linkPath = 'workflowitems';
protected searchByItemLinkPath = 'item';
protected responseMsToLive = 10 * 1000;
constructor(
@@ -86,4 +90,23 @@ export class WorkflowItemDataService extends DataService<WorkflowItem> {
return this.rdbService.buildFromRequestUUID(requestId);
}
/**
* Return the WorkflowItem object found through the UUID of an item
*
* @param uuid The uuid of the item
* @param useCachedVersionIfAvailable If this is true, the request will only be sent if there's
* no valid cached version. Defaults to true
* @param reRequestOnStale Whether or not the request should automatically be re-
* requested after the response becomes stale
* @param options The {@link FindListOptions} object
* @param linksToFollow List of {@link FollowLinkConfig} that indicate which {@link HALLink}s should be automatically resolved
*/
public findByItem(uuid: string, useCachedVersionIfAvailable = false, reRequestOnStale = true, options: FindListOptions = {}, ...linksToFollow: FollowLinkConfig<WorkspaceItem>[]): Observable<RemoteData<WorkspaceItem>> {
const findListOptions = new FindListOptions();
findListOptions.searchParams = [new RequestParam('uuid', encodeURIComponent(uuid))];
const href$ = this.getSearchByHref(this.searchByItemLinkPath, findListOptions, ...linksToFollow);
return this.findByHref(href$, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow);
}
}

View File

@@ -31,7 +31,7 @@ export class VersionedItemComponent extends ItemComponent {
private versionService: VersionDataService,
private itemVersionShared: ItemVersionsSharedService,
private router: Router,
private workspaceitemDataService: WorkspaceitemDataService,
private workspaceItemDataService: WorkspaceitemDataService,
private searchService: SearchService,
private itemService: ItemDataService,
) {
@@ -62,11 +62,11 @@ export class VersionedItemComponent extends ItemComponent {
getFirstCompletedRemoteData(),
// show success/failure notification
tap((res: RemoteData<Version>) => { this.itemVersionShared.notifyCreateNewVersion(res); }),
getFirstSucceededRemoteDataPayload<Version>(),
// get workspace item
getFirstSucceededRemoteDataPayload<Version>(),
switchMap((newVersion: Version) => this.itemService.findByHref(newVersion._links.item.href)),
getFirstSucceededRemoteDataPayload<Item>(),
switchMap((newVersionItem: Item) => this.workspaceitemDataService.findByItem(newVersionItem.uuid, true, false)),
switchMap((newVersionItem: Item) => this.workspaceItemDataService.findByItem(newVersionItem.uuid, true, false)),
getFirstSucceededRemoteDataPayload<WorkspaceItem>(),
).subscribe((wsItem) => {
const wsiId = wsItem.id;

View File

@@ -7,6 +7,8 @@ import { AuthService } from '../../../core/auth/auth.service';
import { NotificationsService } from '../../notifications/notifications.service';
import { TranslateService } from '@ngx-translate/core';
import { VersionHistoryDataService } from '../../../core/data/version-history-data.service';
import { WorkspaceitemDataService } from '../../../core/submission/workspaceitem-data.service';
import { WorkflowItemDataService } from '../../../core/submission/workflowitem-data.service';
describe('ItemVersionsSharedService', () => {
let service: ItemVersionsSharedService;
@@ -20,6 +22,8 @@ describe('ItemVersionsSharedService', () => {
{ provide: AuthService, useValue: {} },
{ provide: NotificationsService, useValue: {} },
{ provide: TranslateService, useValue: {} },
{ provide: WorkspaceitemDataService, useValue: {} },
{ provide: WorkflowItemDataService, useValue: {} },
],
});
service = TestBed.inject(ItemVersionsSharedService);

View File

@@ -19,26 +19,72 @@
<th scope="col" *ngIf="(hasEpersons$ | async)">{{"item.version.history.table.editor" | translate}}</th>
<th scope="col">{{"item.version.history.table.date" | translate}}</th>
<th scope="col">{{"item.version.history.table.summary" | translate}}</th>
<th scope="col" *ngIf="displayActions">{{"item.version.history.table.actions" | translate}}</th>
</tr>
</thead>
<tbody>
<tr *ngFor="let version of versions?.page" [id]="'version-row-' + version.id">
<td class="version-row-element-version">
<div>
<div class="left-column">
<a [routerLink]="getVersionRoute(version.id)">{{version.version}}</a>
<span *ngIf="version?.id === itemVersion?.id">*</span>
</div>
<div class="right-column">
<ng-container *ngVar="getDraftId(version?.item) as draftId$">
<button class="btn btn-outline-primary btn-sm version-row-element-edit" *ngIf="draftId$ | async"
(click)="editWorkspaceItem(draftId$)">
<i class="fas fa-pencil-alt fa-fw"></i> {{ "item.version.history.table.workspaceItem" | translate }}
</button>
</ng-container>
</div>
</div>
<!-- Get the ID of the workspace/workflow item (`undefined` if they don't exist).
Conditionals inside *ngVar are needed in order to avoid useless calls. -->
<ng-container *ngVar="((hasDraftVersion$ | async) ? getWorkspaceId(version?.item) : undefined) as workspaceId$">
<ng-container *ngVar=" ((workspaceId$ | async) ? undefined : getWorkflowId(version?.item)) as workflowId$">
<div class="left-column">
<span *ngIf="(workspaceId$ | async) || (workflowId$ | async); then versionNumberWithoutLink else versionNumberWithLink"></span>
<ng-template #versionNumberWithLink>
<a [routerLink]="getVersionRoute(version.id)">{{version.version}}</a>
</ng-template>
<ng-template #versionNumberWithoutLink>
{{version.version}}
</ng-template>
<span *ngIf="version?.id === itemVersion?.id">*</span>
<span *ngIf="workspaceId$ | async" class="text-light badge badge-primary ml-3">
{{ "item.version.history.table.workspaceItem" | translate }}
</span>
<span *ngIf="workflowId$ | async" class="text-light badge badge-info ml-3">
{{ "item.version.history.table.workflowItem" | translate }}
</span>
</div>
<div class="right-column">
<div class="btn-group edit-field" *ngIf="displayActions">
<!--EDIT WORKSPACE ITEM-->
<button class="btn btn-outline-primary btn-sm version-row-element-edit"
*ngIf="workspaceId$ | async"
(click)="editWorkspaceItem(workspaceId$)"
title="{{'item.version.history.table.action.editWorkspaceItem' | translate }}">
<i class="fas fa-pencil-alt fa-fw"></i>
</button>
<!--CREATE-->
<ng-container *ngIf="canCreateVersion$ | async">
<button class="btn btn-outline-primary btn-sm version-row-element-create"
[disabled]="isAnyBeingEdited() || (hasDraftVersion$ | async)"
(click)="createNewVersion(version)"
title="{{createVersionTitle$ | async | translate }}">
<i class="fas fa-code-branch fa-fw"></i>
</button>
</ng-container>
<!--DELETE-->
<ng-container *ngIf="canDeleteVersion$(version) | async">
<button class="btn btn-sm version-row-element-delete"
[ngClass]="isAnyBeingEdited() ? 'btn-outline-primary' : 'btn-outline-danger'"
[disabled]="isAnyBeingEdited()"
(click)="deleteVersion(version, version.id==itemVersion.id)"
title="{{'item.version.history.table.action.deleteVersion' | translate}}">
<i class="fas fa-trash fa-fw"></i>
</button>
</ng-container>
</div>
</div>
</ng-container>
</ng-container>
</td>
<td *ngIf="(hasEpersons$ | async)" class="version-row-element-editor">
<span *ngVar="(version?.eperson | async)?.payload as eperson">
@@ -49,14 +95,25 @@
{{version?.created | date : 'yyyy-MM-dd HH:mm:ss'}}
</td>
<td class="version-row-element-summary">
<ng-container *ngIf="isThisBeingEdited(version); then editSummary else showSummary"></ng-container>
<ng-template #showSummary>{{version?.summary}}</ng-template>
<ng-template #editSummary>
<input class="form-control" type="text" [(ngModel)]="versionBeingEditedSummary" (keyup.enter)="onSummarySubmit()"/>
</ng-template>
</td>
<td class="version-row-element-actions" *ngIf="displayActions">
<div class="btn-group edit-field">
<div class="float-left">
<ng-container *ngIf="isThisBeingEdited(version); then editSummary else showSummary"></ng-container>
<ng-template #showSummary>{{version?.summary}}</ng-template>
<ng-template #editSummary>
<input class="form-control" type="text" [(ngModel)]="versionBeingEditedSummary"
(keyup.enter)="onSummarySubmit()"/>
</ng-template>
</div>
<div class="float-right btn-group edit-field" *ngIf="displayActions">
<!--DISCARD EDIT -->
<ng-container *ngIf="(canEditVersion$(version) | async) && isThisBeingEdited(version)">
<button class="btn btn-sm"
[ngClass]="isThisBeingEdited(version) ? 'btn-outline-warning' : 'btn-outline-primary'"
(click)="disableSummaryEditing()"
title="{{'item.version.history.table.action.discardSummary' | translate}}">
<i class="fas fa-undo-alt fa-fw"></i>
</button>
</ng-container>
<!--EDIT / SAVE-->
<ng-container *ngIf="canEditVersion$(version) | async">
<button class="btn btn-outline-primary btn-sm version-row-element-edit"
@@ -73,36 +130,9 @@
<i class="fas fa-check fa-fw"></i>
</button>
</ng-container>
<!--CREATE-->
<ng-container *ngIf="canCreateVersion$ | async">
<button class="btn btn-outline-primary btn-sm version-row-element-create"
[disabled]="isAnyBeingEdited() || (hasDraftVersion$ | async)"
(click)="createNewVersion(version)"
title="{{createVersionTitle$ | async | translate }}">
<i class="fas fa-code-branch fa-fw"></i>
</button>
</ng-container>
<!--DELETE-->
<ng-container *ngIf="canDeleteVersion$(version) | async">
<button class="btn btn-sm version-row-element-delete"
[ngClass]="isAnyBeingEdited() ? 'btn-outline-primary' : 'btn-outline-danger'"
[disabled]="isAnyBeingEdited()"
(click)="deleteVersion(version, version.id==itemVersion.id)"
title="{{'item.version.history.table.action.deleteVersion' | translate}}">
<i class="fas fa-trash fa-fw"></i>
</button>
</ng-container>
<!--DISCARD EDIT -->
<ng-container *ngIf="canEditVersion$(version) | async">
<button class="btn btn-sm"
[ngClass]="isThisBeingEdited(version) ? 'btn-outline-warning' : 'btn-outline-primary'"
[disabled]="!isAnyBeingEdited()"
(click)="disableSummaryEditing()"
title="{{'item.version.history.table.action.discardSummary' | translate}}">
<i class="fas fa-undo-alt fa-fw"></i>
</button>
</ng-container>
</div>
</td>
</tr>
</tbody>

View File

@@ -22,6 +22,8 @@ import { NotificationsService } from '../../notifications/notifications.service'
import { NotificationsServiceStub } from '../../testing/notifications-service.stub';
import { AuthorizationDataService } from '../../../core/data/feature-authorization/authorization-data.service';
import { FeatureID } from '../../../core/data/feature-authorization/feature-id';
import { WorkspaceitemDataService } from '../../../core/submission/workspaceitem-data.service';
import { WorkflowItemDataService } from '../../../core/submission/workflowitem-data.service';
describe('ItemVersionsComponent', () => {
let component: ItemVersionsComponent;
@@ -29,9 +31,12 @@ describe('ItemVersionsComponent', () => {
let authenticationService: AuthService;
let authorizationService: AuthorizationDataService;
let versionHistoryService: VersionHistoryDataService;
let workspaceItemDataService: WorkspaceitemDataService;
let workflowItemDataService: WorkflowItemDataService;
const versionHistory = Object.assign(new VersionHistory(), {
id: '1'
id: '1',
draftVersion: false,
});
const version1 = Object.assign(new Version(), {
@@ -86,7 +91,7 @@ describe('ItemVersionsComponent', () => {
version2.item = createSuccessfulRemoteDataObject$(item2);
const versionHistoryServiceSpy = jasmine.createSpyObj('versionHistoryService', {
getVersions: createSuccessfulRemoteDataObject$(createPaginatedList(versions))
getVersions: createSuccessfulRemoteDataObject$(createPaginatedList(versions)),
});
const authenticationServiceSpy = jasmine.createSpyObj('authenticationService', {
isAuthenticated: observableOf(true),
@@ -94,6 +99,12 @@ describe('ItemVersionsComponent', () => {
}
);
const authorizationServiceSpy = jasmine.createSpyObj('authorizationService', ['isAuthorized']);
const workspaceItemDataServiceSpy = jasmine.createSpyObj('workspaceItemDataService', {
findByItem: of(undefined),
});
const workflowItemDataServiceSpy = jasmine.createSpyObj('workflowItemDataService', {
findByItem: of(undefined),
});
beforeEach(waitForAsync(() => {
@@ -110,6 +121,8 @@ describe('ItemVersionsComponent', () => {
{provide: VersionHistoryDataService, useValue: versionHistoryServiceSpy},
{provide: ItemDataService, useValue: {}},
{provide: VersionDataService, useValue: {}},
{provide: WorkspaceitemDataService, useValue: workspaceItemDataServiceSpy},
{provide: WorkflowItemDataService, useValue: workflowItemDataServiceSpy},
],
schemas: [NO_ERRORS_SCHEMA]
}).compileComponents();
@@ -117,6 +130,8 @@ describe('ItemVersionsComponent', () => {
versionHistoryService = TestBed.inject(VersionHistoryDataService);
authenticationService = TestBed.inject(AuthService);
authorizationService = TestBed.inject(AuthorizationDataService);
workspaceItemDataService = TestBed.inject(WorkspaceitemDataService);
workflowItemDataService = TestBed.inject(WorkflowItemDataService);
}));

View File

@@ -8,7 +8,7 @@ import {
combineLatest as observableCombineLatest,
Observable,
of,
Subscription
Subscription,
} from 'rxjs';
import { VersionHistory } from '../../../core/shared/version-history.model';
import {
@@ -47,6 +47,7 @@ import { FeatureID } from '../../../core/data/feature-authorization/feature-id';
import { ItemVersionsSharedService } from './item-versions-shared.service';
import { WorkspaceItem } from '../../../core/submission/models/workspaceitem.model';
import { WorkspaceitemDataService } from '../../../core/submission/workspaceitem-data.service';
import { WorkflowItemDataService } from '../../../core/submission/workflowitem-data.service';
@Component({
selector: 'ds-item-versions',
@@ -173,6 +174,7 @@ export class ItemVersionsComponent implements OnInit {
private itemVersionShared: ItemVersionsSharedService,
private authorizationService: AuthorizationDataService,
private workspaceItemDataService: WorkspaceitemDataService,
private workflowItemDataService: WorkflowItemDataService,
) {
}
@@ -333,19 +335,31 @@ export class ItemVersionsComponent implements OnInit {
of(summary),
version.item.pipe(getFirstSucceededRemoteDataPayload())
])),
mergeMap(([summary, item]: [string, Item]) => this.itemVersionShared.createNewVersionAndNotify(item, summary)),
map((newVersionRD: RemoteData<Version>) => {
mergeMap(([summary, item]: [string, Item]) => this.versionHistoryService.createVersion(item._links.self.href, summary)),
// show success/failure notification
tap((newVersionRD: RemoteData<Version>) => {
this.itemVersionShared.notifyCreateNewVersion(newVersionRD);
if (newVersionRD.hasSucceeded) {
const versionHistory$ = this.versionService.getHistoryFromVersion$(version).pipe(
tap((res: VersionHistory) => {
this.versionHistoryService.invalidateVersionHistoryCache(res.id);
tap((versionHistory: VersionHistory) => {
this.itemService.invalidateItemCache(this.item.uuid);
this.versionHistoryService.invalidateVersionHistoryCache(versionHistory.id);
}),
);
this.getAllVersions(versionHistory$);
}
}),
take(1),
).subscribe();
// get workspace item
getFirstSucceededRemoteDataPayload<Version>(),
switchMap((newVersion: Version) => this.itemService.findByHref(newVersion._links.item.href)),
getFirstSucceededRemoteDataPayload<Item>(),
switchMap((newVersionItem: Item) => this.workspaceItemDataService.findByItem(newVersionItem.uuid, true, false)),
getFirstSucceededRemoteDataPayload<WorkspaceItem>(),
).subscribe((wsItem) => {
const wsiId = wsItem.id;
const route = 'workspaceitems/' + wsiId + '/edit';
this.router.navigateByUrl(route);
});
}
/**
@@ -386,7 +400,7 @@ export class ItemVersionsComponent implements OnInit {
* Get the ID of the workspace item, if present, otherwise return undefined
* @param versionItem the item for which retrieve the workspace item id
*/
getDraftId(versionItem): Observable<string> {
getWorkspaceId(versionItem): Observable<string> {
return versionItem.pipe(
getFirstSucceededRemoteDataPayload(),
map((item: Item) => item.uuid),
@@ -396,6 +410,20 @@ export class ItemVersionsComponent implements OnInit {
);
}
/**
* Get the ID of the workflow item, if present, otherwise return undefined
* @param versionItem the item for which retrieve the workspace item id
*/
getWorkflowId(versionItem): Observable<string> {
return versionItem.pipe(
getFirstSucceededRemoteDataPayload(),
map((item: Item) => item.uuid),
switchMap((itemUuid: string) => this.workflowItemDataService.findByItem(itemUuid, true)),
getFirstCompletedRemoteData<WorkspaceItem>(),
map((res: RemoteData<WorkspaceItem>) => res?.payload?.id ),
);
}
/**
* redirect to the edit page of the workspace item
* @param id$ the id of the workspace item

View File

@@ -1983,8 +1983,11 @@
"item.version.history.table.workspaceItem": "Workspace item",
"item.version.history.table.workflowItem": "Workflow item",
"item.version.history.table.actions": "Action",
"item.version.history.table.action.editWorkspaceItem": "Edit workspace item",
"item.version.history.table.action.editSummary": "Edit summary",