Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Limit edit item modal results #2011

Open
wants to merge 8 commits into
base: main
Choose a base branch
from
Open
1 change: 1 addition & 0 deletions src/app/admin/admin-sidebar/admin-sidebar.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,7 @@ export class AdminSidebarComponent extends MenuComponent implements OnInit {
}
}
});
this.menuVisible = this.menuService.isMenuVisibleWithVisibleSections(this.menuID);
}

@HostListener('focusin')
Expand Down
4 changes: 3 additions & 1 deletion src/app/core/data/feature-authorization/feature-id.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,5 +29,7 @@ export enum FeatureID {
CanViewUsageStatistics = 'canViewUsageStatistics',
CanSendFeedback = 'canSendFeedback',
CanClaimItem = 'canClaimItem',
CanSynchronizeWithORCID = 'canSynchronizeWithORCID'
CanSynchronizeWithORCID = 'canSynchronizeWithORCID',
CanSubmit = 'canSubmit',
CanEditItem = 'canEditItem',
}
16 changes: 15 additions & 1 deletion src/app/core/data/item-data.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import { ObjectCacheService } from '../cache/object-cache.service';
import { RestResponse } from '../cache/response.models';
import { ExternalSourceEntry } from '../shared/external-source-entry.model';
import { ItemDataService } from './item-data.service';
import { DeleteRequest, PostRequest } from './request.models';
import { DeleteRequest, PostRequest, GetRequest } from './request.models';
import { RequestService } from './request.service';
import { getMockRemoteDataBuildService } from '../../shared/mocks/remote-data-build.service.mock';
import { CoreState } from '../core-state.model';
Expand Down Expand Up @@ -192,6 +192,20 @@ describe('ItemDataService', () => {
});
});

describe('findItemsWithEdit', () => {
beforeEach(() => {
service = initTestService();
});

it('should send a GET request', (done) => {
const result = service.findItemsWithEdit('', {});
result.subscribe(() => {
expect(requestService.send).toHaveBeenCalledWith(jasmine.any(GetRequest), true);
done();
});
});
});

describe('when cache is invalidated', () => {
beforeEach(() => {
service = initTestService();
Expand Down
34 changes: 33 additions & 1 deletion src/app/core/data/item-data.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,16 +46,19 @@ import { RestRequestMethod } from './rest-request-method';
import { CreateData, CreateDataImpl } from './base/create-data';
import { RequestParam } from '../cache/models/request-param.model';
import { dataService } from './base/data-service.decorator';
import { FollowLinkConfig } from 'src/app/shared/utils/follow-link-config.model';
import { SearchData, SearchDataImpl } from './base/search-data';

/**
* An abstract service for CRUD operations on Items
* Doesn't specify an endpoint because multiple endpoints support Item-like functionality (e.g. items, itemtemplates)
* Extend this class to implement data services for Items
*/
export abstract class BaseItemDataService extends IdentifiableDataService<Item> implements CreateData<Item>, PatchData<Item>, DeleteData<Item> {
export abstract class BaseItemDataService extends IdentifiableDataService<Item> implements CreateData<Item>, PatchData<Item>, DeleteData<Item>, SearchData<Item> {
private createData: CreateData<Item>;
private patchData: PatchData<Item>;
private deleteData: DeleteData<Item>;
private searchData: SearchData<Item>;

protected constructor(
protected linkPath,
Expand All @@ -74,6 +77,7 @@ export abstract class BaseItemDataService extends IdentifiableDataService<Item>
this.createData = new CreateDataImpl(this.linkPath, requestService, rdbService, objectCache, halService, notificationsService, this.responseMsToLive);
this.patchData = new PatchDataImpl<Item>(this.linkPath, requestService, rdbService, objectCache, halService, comparator, this.responseMsToLive, this.constructIdEndpoint);
this.deleteData = new DeleteDataImpl(this.linkPath, requestService, rdbService, objectCache, halService, notificationsService, this.responseMsToLive, this.constructIdEndpoint);
this.searchData = new SearchDataImpl(this.linkPath, requestService, rdbService, objectCache, halService, this.responseMsToLive);
}

/**
Expand Down Expand Up @@ -388,6 +392,34 @@ export abstract class BaseItemDataService extends IdentifiableDataService<Item>
return this.createData.create(object, ...params);
}

/**
* Make a new FindListRequest with given search method
*
* @param searchMethod The search method for the object
* @param options The [[FindListOptions]] object
* @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 linksToFollow List of {@link FollowLinkConfig} that indicate which
* {@link HALLink}s should be automatically resolved
* @return {Observable<RemoteData<PaginatedList<T>>}
* Return an observable that emits response from the server
*/
public searchBy(searchMethod: string, options?: FindListOptions, useCachedVersionIfAvailable?: boolean, reRequestOnStale?: boolean, ...linksToFollow: FollowLinkConfig<Item>[]): Observable<RemoteData<PaginatedList<Item>>> {
return this.searchData.searchBy(searchMethod, options, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow);
}

/**
* Find the list of items for which the current user has editing rights.
*
* @param linksToFollow List of {@link FollowLinkConfig} that indicate which
* {@link HALLink}s should be automatically resolved
*/
public findItemsWithEdit(query: string, options: FindListOptions, useCachedVersionIfAvailable = true, reRequestOnStale = true, ...linksToFollow: FollowLinkConfig<Item>[]): Observable<RemoteData<PaginatedList<Item>>> {
options = { ...options, searchParams: [new RequestParam('query', query)] };
return this.searchBy('findItemsWithEdit', options, useCachedVersionIfAvailable, reRequestOnStale );
}
}

/**
Expand Down
1 change: 0 additions & 1 deletion src/app/core/data/remote-data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -114,5 +114,4 @@ export class RemoteData<T> {
get hasNoContent(): boolean {
return this.statusCode === 204;
}

}
9 changes: 9 additions & 0 deletions src/app/core/shared/operators.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,15 @@ export const getFirstSucceededRemoteWithNotEmptyData = <T>() =>
(source: Observable<RemoteData<T>>): Observable<RemoteData<T>> =>
source.pipe(find((rd: RemoteData<T>) => rd.hasSucceeded && isNotEmpty(rd.payload)));

export const mapRemoteDataPayload = <T,U>(fn: (payload: T) => U) =>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we add a comment or TypeDocs to describe how/when to use this? See for example how "getFirstSucceededRemoteDataPayload" is described (and others below it.)

(source: Observable<RemoteData<T>>): Observable<RemoteData<U>> =>
source.pipe(map((rd: RemoteData<T>) => {
let {payload, ...rest} = rd;
return Object.assign(new RemoteData(null,null,null,null),
{...rest, payload: fn(payload)});
}
));

/**
* Get the first successful remotely retrieved object
*
Expand Down
159 changes: 123 additions & 36 deletions src/app/menu.resolver.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -142,29 +142,7 @@ describe('MenuResolver', () => {
});

describe('createAdminMenu$', () => {
it('should retrieve the menu by ID return an Observable that emits true as soon as it is created', () => {
(menuService as any).getMenu.and.returnValue(cold('--u--m', {
u: undefined,
m: MENU_STATE,
}));

expect(resolver.createAdminMenu$()).toBeObservable(cold('-----(t|)', BOOLEAN));
expect(menuService.getMenu).toHaveBeenCalledOnceWith(MenuID.ADMIN);
});

describe('for regular user', () => {
beforeEach(() => {
authorizationService.isAuthorized = createSpy('isAuthorized').and.callFake(() => {
return observableOf(false);
});
});

beforeEach((done) => {
resolver.createAdminMenu$().subscribe((_) => {
done();
});
});

const dontShowAdminSections = () => {
it('should not show site admin section', () => {
expect(menuService.addSection).toHaveBeenCalledWith(MenuID.ADMIN, jasmine.objectContaining({
id: 'admin_search', visible: false,
Expand All @@ -183,19 +161,6 @@ describe('MenuResolver', () => {
}));
});

it('should not show edit_community', () => {
expect(menuService.addSection).toHaveBeenCalledWith(MenuID.ADMIN, jasmine.objectContaining({
id: 'edit_community', visible: false,
}));

});

it('should not show edit_collection', () => {
expect(menuService.addSection).toHaveBeenCalledWith(MenuID.ADMIN, jasmine.objectContaining({
id: 'edit_collection', visible: false,
}));
});

it('should not show access control section', () => {
expect(menuService.addSection).toHaveBeenCalledWith(MenuID.ADMIN, jasmine.objectContaining({
id: 'access_control', visible: false,
Expand All @@ -222,6 +187,122 @@ describe('MenuResolver', () => {
id: 'export', visible: true,
}));
});
};

const dontShowNewSection = () => {
it('should not show the "New" section', () => {
expect(menuService.addSection).toHaveBeenCalledWith(MenuID.ADMIN, jasmine.objectContaining({
id: 'new_community', visible: false,
}));
expect(menuService.addSection).toHaveBeenCalledWith(MenuID.ADMIN, jasmine.objectContaining({
id: 'new_collection', visible: false,
}));
expect(menuService.addSection).toHaveBeenCalledWith(MenuID.ADMIN, jasmine.objectContaining({
id: 'new_item', visible: false,
}));
expect(menuService.addSection).toHaveBeenCalledWith(MenuID.ADMIN, jasmine.objectContaining({
id: 'new', visible: false,
}));
});
};

const dontShowEditSection = () => {
it('should not show the "Edit" section', () => {
expect(menuService.addSection).toHaveBeenCalledWith(MenuID.ADMIN, jasmine.objectContaining({
id: 'edit_community', visible: false,
}));
expect(menuService.addSection).toHaveBeenCalledWith(MenuID.ADMIN, jasmine.objectContaining({
id: 'edit_collection', visible: false,
}));
expect(menuService.addSection).toHaveBeenCalledWith(MenuID.ADMIN, jasmine.objectContaining({
id: 'edit_item', visible: false,
}));
expect(menuService.addSection).toHaveBeenCalledWith(MenuID.ADMIN, jasmine.objectContaining({
id: 'edit', visible: false,
}));
});
};

it('should retrieve the menu by ID return an Observable that emits true as soon as it is created', () => {
(menuService as any).getMenu.and.returnValue(cold('--u--m', {
u: undefined,
m: MENU_STATE,
}));

expect(resolver.createAdminMenu$()).toBeObservable(cold('-----(t|)', BOOLEAN));
expect(menuService.getMenu).toHaveBeenCalledOnceWith(MenuID.ADMIN);
});

describe('for regular user', () => {
beforeEach(() => {
authorizationService.isAuthorized = createSpy('isAuthorized').and.callFake((featureID) => {
return observableOf(false);
});
});

beforeEach((done) => {
resolver.createAdminMenu$().subscribe((_) => {
done();
});
});

dontShowAdminSections();
dontShowNewSection();
dontShowEditSection();
});

describe('regular user who can submit', () => {
beforeEach(() => {
authorizationService.isAuthorized = createSpy('isAuthorized')
.and.callFake((featureID: FeatureID) => {
return observableOf(featureID === FeatureID.CanSubmit);
});
});

beforeEach((done) => {
resolver.createAdminMenu$().subscribe((_) => {
done();
});
});

it('should show "New Item" section', () => {
expect(menuService.addSection).toHaveBeenCalledWith(MenuID.ADMIN, jasmine.objectContaining({
id: 'new_item', visible: true,
}));
expect(menuService.addSection).toHaveBeenCalledWith(MenuID.ADMIN, jasmine.objectContaining({
id: 'new', visible: true,
}));
});

dontShowAdminSections();
dontShowEditSection();
});

describe('regular user who can edit items', () => {
beforeEach(() => {
authorizationService.isAuthorized = createSpy('isAuthorized')
.and.callFake((featureID: FeatureID) => {
return observableOf(featureID === FeatureID.CanEditItem);
});
});

beforeEach((done) => {
resolver.createAdminMenu$().subscribe((_) => {
done();
});
});

it('should show "Edit Item" section', () => {
expect(menuService.addSection).toHaveBeenCalledWith(MenuID.ADMIN, jasmine.objectContaining({
id: 'edit_item', visible: true,
}));
expect(menuService.addSection).toHaveBeenCalledWith(MenuID.ADMIN, jasmine.objectContaining({
id: 'edit', visible: true,
}));
});

dontShowAdminSections();
dontShowNewSection();
});

describe('for site admin', () => {
Expand All @@ -237,6 +318,12 @@ describe('MenuResolver', () => {
});
});

it('should show new_process', () => {
expect(menuService.addSection).toHaveBeenCalledWith(MenuID.ADMIN, jasmine.objectContaining({
id: 'new_process', visible: true,
}));
});

it('should contain site admin section', () => {
expect(menuService.addSection).toHaveBeenCalledWith(MenuID.ADMIN, jasmine.objectContaining({
id: 'admin_search', visible: true,
Expand Down
Loading