Commit 7110d1b5 authored by Maxym Borodenko's avatar Maxym Borodenko Committed by Matija Obreza

Images: use FilteredSlice for image browsing

parent 12c58d14
import Slice from '@genesys/client/model/Slice';
import { SortDirection } from '@genesys/client/model/Page';
class FilteredSlice<T> extends Slice<T> {
public filterCode?: string;
public filter?: any;
public static reSort(sliced: FilteredSlice<any>, property?: string, direction?: SortDirection): Partial<FilteredSlice<any>> {
const resorted: Partial<FilteredSlice<any>> = {
filterCode: sliced ? sliced.filterCode : undefined,
offset: 0, // Request first page
sort: [{
property: property ? property : undefined,
direction: direction ? direction : undefined,
}],
};
console.log(`reSort for prop=${property} dir=${direction}`, resorted);
return resorted;
}
public static merge(oldSliced: FilteredSlice<any>, newSliced: FilteredSlice<any>): FilteredSlice<any> {
if (!oldSliced) {
return newSliced;
}
if (!newSliced) {
return oldSliced;
}
const { filterCode, filter, ...other } = newSliced; // other helps with properties of subclasses
return {
...other,
...Slice.merge(oldSliced, newSliced),
filterCode: filterCode === '' ? '' : filterCode || oldSliced.filterCode,
filter: filter || oldSliced.filter,
};
}
}
class SuggestionSlice<T> extends FilteredSlice<T> {
public suggestions: any;
}
export { FilteredSlice as default, SuggestionSlice };
import { ISort, SortDirection } from '@genesys/client/model/Page';
class Slice<T> {
public content: T[];
public totalElements: number;
public offset: number;
public sliceSize: number;
public sort: ISort[];
public static nextSlice(sliced?: Slice<any>): IOffsetRequest {
const nextSlice: IOffsetRequest = {
offset: sliced && (sliced.offset || sliced.offset === 0) ? sliced.offset + sliced.sliceSize : 0,
size: undefined,
direction: sliced && sliced.sort && sliced.sort[0].direction || undefined,
properties: sliced && sliced.sort && sliced.sort[0].property ? [sliced.sort[0].property] : undefined,
};
// console.log('Current/Next', sliced, nextSlice);
return nextSlice;
}
public static reSort(paged: Slice<any>, property?: string, direction?: SortDirection): Partial<Slice<any>> {
const resorted: Partial<Slice<any>> = {
offset: 0, // resort needs to request the first slice
sort: [{
property: property ? property : undefined,
direction: direction ? direction : undefined,
}],
};
console.log(`Resort for prop=${property} dir={direction}`, resorted);
return resorted;
}
public static merge(oldSliced: Slice<any>, newSliced: Slice<any>): Slice<any> {
if (!oldSliced) {
return newSliced;
}
return newSliced ? {
content: newSliced.content && newSliced.offset === 0 ? newSliced.content : (newSliced.content ? [...oldSliced.content, ...newSliced.content] : oldSliced.content),
totalElements: newSliced.totalElements === 0 ? newSliced.totalElements : newSliced.totalElements || oldSliced.totalElements,
offset: newSliced.offset === 0 ? newSliced.offset : newSliced.offset || oldSliced.offset,
sliceSize: newSliced.content ? newSliced.content.length : oldSliced.sliceSize,
sort: newSliced.sort || oldSliced.sort,
} : oldSliced;
}
}
export interface IOffsetRequest {
offset?: number; // offset
size?: number; // requested slice size
direction?: SortDirection; // direction
properties?: string[]; // sort by properties
}
export default Slice;
......@@ -8,6 +8,8 @@ import AccessionDetails from '@genesys/client/model/accession/AccessionDetails';
import AccessionFilter from '@genesys/client/model/accession/AccessionFilter';
import AccessionMapInfo from '@genesys/client/model/accession/AccessionMapInfo';
import FilteredPage, { IPageRequest, SuggestionsPage } from '@genesys/client/model/FilteredPage';
import { SuggestionSlice } from '@genesys/client/model/FilteredSlice';
import { IOffsetRequest } from '@genesys/client/model/Slice';
import {AccessionRef} from '@genesys/client/model/accession/AccessionRef';
import AccessionAuditLog from '@genesys/client/model/accession/AccessionAuditLog';
import LabelValue from '@genesys/client/model/LabelValue';
......@@ -312,7 +314,7 @@ class AccessionService {
const qs = QueryString.stringify({
f: typeof filter === 'string' ? filter : undefined,
p: page.page || undefined,
p: page.page || 0,
l: page.size || undefined,
d: page.direction ? page.direction : undefined,
s: page.properties || undefined,
......@@ -333,6 +335,31 @@ class AccessionService {
});
}
public static withImagesSliced(filter: string | AccessionFilter, slice?: IOffsetRequest, xhrConfig?: any): Promise<SuggestionSlice<AccessionDetails>> {
const qs = QueryString.stringify({
f: typeof filter === 'string' ? filter : undefined,
o: slice.offset || 0,
l: slice.size || undefined,
d: slice.direction ? slice.direction : undefined,
s: slice.properties || undefined,
}, {});
const apiUrl = URL_IMAGES + (qs ? `?${qs}` : '');
// console.log(`Fetching from ${apiUrl}`);
const content = { data: typeof filter === 'string' ? null : { ...filter } };
return axiosBackend.request({
...xhrConfig,
url: apiUrl,
method: 'POST',
...content,
}).then(({ data }) => data as SuggestionSlice<AccessionDetails>)
.then((data) => {
dereferenceReferences2(data.content, AccessionDetails.DEREFERENCES);
return data;
});
}
/**
* listSuggestions at /api/v1/acn/list-suggestions
*
......
......@@ -5,6 +5,8 @@ import * as _ from 'lodash';
import navigateTo from 'actions/navigation';
import {filterCodeToUrl} from 'actions/filterCode';
import FilteredPage, { IPageRequest } from '@genesys/client/model/FilteredPage';
import FilteredSlice from '@genesys/client/model/FilteredSlice';
import Slice, {IOffsetRequest} from '@genesys/client/model/Slice';
import Accession from '@genesys/client/model/accession/Accession';
import AccessionFilter from '@genesys/client/model/accession/AccessionFilter';
import AccessionMapInfo from '@genesys/client/model/accession/AccessionMapInfo';
......@@ -39,6 +41,7 @@ const receiveAccessionMapInfo = (apiCall: ApiCall<any>) => ({
const apiListAccessions = createApiCaller(AccessionService.list, APPEND_ACCESSIONS);
const apiListAccessionSugestions = createApiCaller(AccessionService.listSuggestions, APPEND_ACCESSIONS_WITH_SUGGESTIONS);
const apiListAccessionWithImages = createApiCaller(AccessionService.withImages, APPEND_ACCESSIONS_WITH_IMAGES);
const apiListAccessionWithImagesSliced = createApiCaller(AccessionService.withImagesSliced, APPEND_ACCESSIONS_WITH_IMAGES);
const apiAccessionsOverview = createApiCaller(AccessionService.listOverview, RECEIVE_ACCESSION_OVERVIEW);
const apiAccessionsMapInfo = createApiCaller(AccessionService.mapInfo, RECEIVE_ACCESSION_MAPINFO);
......@@ -108,12 +111,35 @@ export const applyImagesFilters = (filters: string | AccessionFilter, page: IPag
});
};
export const applySlicedImagesFilters = (filters: string | AccessionFilter, page: IOffsetRequest = { offset: 0 }) => (dispatch, getState) => {
console.log('Applying new filter', filters);
if (typeof filters === 'object' && filters.images === null) {
filters.images = true;
} else if (filters === '') {
filters = {images: true};
}
dispatch(showSnackbar('common.snackbar.applyingFilters'));
dispatch(navigateTo('/a/images'));
return dispatch(apiListAccessionWithImagesSliced(filters, page))
.then((paged) => {
dispatch(updateRouteWithFilterCode(paged.filterCode, '/a/images'));
dispatch(showSnackbar('common.snackbar.filtersApplied'));
});
};
export const loadMoreAccessionsWithImages = (paged?: FilteredPage<Accession>) => (dispatch) => {
console.log('Load more accessions', paged);
return dispatch(apiListAccessionWithImages(paged ? paged.filterCode : '', Page.nextPage(paged)))
.then((paged) => dispatch(updateRouteWithFilterCode(paged.filterCode, '/a/images')));
};
export const loadMoreAccessionsWithImagesSliced = (sliced?: FilteredSlice<Accession>) => (dispatch) => {
console.log('Load more accessions', sliced);
return dispatch(apiListAccessionWithImagesSliced(sliced ? sliced.filterCode : '', Slice.nextSlice(sliced)))
.then((sliced2) => dispatch(updateRouteWithFilterCode(sliced2.filterCode, '/a/images')));
};
export const listAccessionsByUuid = (uuidList: string[]) => (dispatch) => {
return dispatch(apiListAccessionByUuid(uuidList));
};
......
......@@ -12,6 +12,7 @@ import {
} from 'accessions/constants';
import FilteredPage from '@genesys/client/model/FilteredPage';
import FilteredSlice from '@genesys/client/model/FilteredSlice';
import Accession from '@genesys/client/model/accession/Accession';
import AccessionMapInfo from '@genesys/client/model/accession/AccessionMapInfo';
import AccessionOverview from '@genesys/client/model/accession/AccessionOverview';
......@@ -27,7 +28,7 @@ const INITIAL_STATE: {
paged: ApiCall<FilteredPage<Accession>>,
suggestions: any,
overview: ApiCall<AccessionOverview>,
images: ApiCall<FilteredPage<AccessionDetails>>,
images: ApiCall<FilteredSlice<AccessionDetails>>,
mapInfo: ApiCall<AccessionMapInfo>,
mapLayers: MapLayer[],
defaultLayer: {
......@@ -127,7 +128,7 @@ const publicAccessions = (state = INITIAL_STATE, action: IReducerAction) => {
loading,
error,
timestamp,
data: FilteredPage.merge(state.images && state.images.data, data),
data: FilteredSlice.merge(state.images && state.images.data, data),
} },
// mapInfo: { $set: null },
// overview: { $set: null },
......
......@@ -6,23 +6,23 @@ import withStyles from '@material-ui/core/styles/withStyles';
// actions
import {
applyImagesFilters,
loadMoreAccessionsWithImages,
applySlicedImagesFilters,
loadMoreAccessionsWithImagesSliced,
updateRouteWithFilterCode,
} from 'accessions/actions/public';
// model
import ApiCall from '@genesys/client/model/ApiCall';
import AccessionDetails from '@genesys/client/model/accession/AccessionDetails';
import AccessionFilter from '@genesys/client/model/accession/AccessionFilter';
import { IPageRequest } from '@genesys/client/model/Page';
import { SuggestionsPage } from '@genesys/client/model/FilteredPage';
import { SuggestionSlice } from '@genesys/client/model/FilteredSlice';
import { IOffsetRequest } from '@genesys/client/model/Slice';
// UI
import ContentHeader from 'ui/common/heading/ContentHeader';
import PageTitle from 'ui/common/PageTitle';
import PageLayout, { PageContents } from 'ui/layout/PageLayout';
import Tabs, { Tab } from 'ui/common/Tabs';
import PagedLoader from 'ui/common/PagedLoader';
import SlicedLoader from 'ui/common/SlicedLoader';
import ErrorMessage from 'ui/common/error/ErrorMessage';
import Loading from 'ui/common/Loading';
......@@ -44,11 +44,11 @@ interface IImageBrowsePageProps extends React.ClassAttributes<any> {
t: any;
classes: any;
apiCall: ApiCall<SuggestionsPage<AccessionDetails>>;
apiCall: ApiCall<SuggestionSlice<AccessionDetails>>;
filterCode: string;
currentTab: string;
loadMoreData: (paged?: SuggestionsPage<any>) => any;
applyImagesFilters: (filters: string | AccessionFilter, page?: IPageRequest) => void;
loadMoreData: (sliced?: SuggestionSlice<AccessionDetails>) => any;
applyImagesFilters: (filters: string | AccessionFilter, page?: IOffsetRequest) => void;
updateRouteWithFilterCode: (filterCode: string, path: string) => void;
repositoryThumbnailUrl: (image: RepositoryImage, width: number, height: number, first?: boolean) => any;
}
......@@ -164,26 +164,26 @@ class ImageBrowsePage extends React.Component<IImageBrowsePageProps> {
return applyImagesFilters(filters);
}
private loadMore = (page) => {
private loadMore = (slice: SuggestionSlice<AccessionDetails>) => {
const { loadMoreData } = this.props;
return loadMoreData(page);
return loadMoreData(slice);
}
public render() {
const { apiCall, filterCode, currentTab, t } = this.props;
const { data: paged, loading, error } = apiCall || { data: undefined, loading: true, error: undefined };
const { data: sliced, loading, error } = apiCall || { data: undefined, loading: true, error: undefined };
// if (paged && paged.filter) {
// delete paged.filter.images;
// }
const suggestionTerms = new Map();
if (paged && paged.suggestions) {
Object.keys(paged.suggestions).forEach((key) => {
const overviewEl = paged.suggestions[key];
if (sliced && sliced.suggestions) {
Object.keys(sliced.suggestions).forEach((key) => {
const overviewEl = sliced.suggestions[key];
const terms = new Map();
overviewEl.terms.forEach((term) => terms.set(term.term, term.count));
terms.set('missing', paged.suggestions[key].missing);
terms.set('missing', sliced.suggestions[key].missing);
suggestionTerms.set(key, terms);
});
}
......@@ -193,8 +193,8 @@ class ImageBrowsePage extends React.Component<IImageBrowsePageProps> {
return(
<PageLayout sidebar={
<AccessionFilters
initialValues={ paged && paged.filter || {} }
filterCode={ paged && paged.filterCode || '' }
initialValues={ sliced && sliced.filter || {} }
filterCode={ sliced && sliced.filterCode || '' }
onSubmit={ this.myApplyFilters }
terms={ suggestionTerms }
/>
......@@ -212,17 +212,17 @@ class ImageBrowsePage extends React.Component<IImageBrowsePageProps> {
<ScrollToTopOnMount/>
<PrettyFilters
prefix="accessions"
filterObj={ paged && paged.filter || {} }
filterObj={ sliced && sliced.filter || {} }
onSubmit={ this.myApplyFilters }
displayName="accessions.common.modelName"
amount={ paged && paged.totalElements }
amount={ sliced && sliced.totalElements }
/>
<PageContents className="pt-1rem">
{ loading && <Loading /> }
{ error && <ErrorMessage error={ error }/> }
<GridContainer>
<PagedLoader
paged={ paged }
<SlicedLoader
sliced={ sliced }
loadMore={ this.loadMore }
itemRenderer={ this.renderImageGallery }
/>
......@@ -241,8 +241,8 @@ const mapStateToProps = (state, ownProps) => ({
});
const mapDispatchToProps = (dispatch) => bindActionCreators({
applyImagesFilters,
loadMoreData: loadMoreAccessionsWithImages,
applyImagesFilters: applySlicedImagesFilters,
loadMoreData: loadMoreAccessionsWithImagesSliced,
updateRouteWithFilterCode,
repositoryThumbnailUrl,
}, dispatch);
......
import * as React from 'react';
import {log} from '@genesys/client/utilities/debug';
import VisibilitySensor from 'react-visibility-sensor';
import Loading from 'ui/common/Loading';
import {ScrollToTopOnMount} from 'ui/common/page/scrollers';
import Slice from '@genesys/client/model/Slice';
interface IProps<T> {
sliced: Slice<T>;
roughItemHeight?: number; // px of height of element, defaults to 50px, determines when loadNextPage is called
itemRenderer: (item: T, index?: number) => any;
loadingIndicator?: any;
loadMore: (sliced?: Slice<T>) => any;
}
export default class SlicedLoader<T> extends React.Component<IProps<T>, any> {
private endOfListVisibilityChange = (isVisible: boolean): void => {
const { sliced, loadMore } = this.props;
// log(`Visibility ${isVisible}`);
if (sliced && isVisible) {
// we should load some stuff
if (sliced && ! this.isLast(sliced)) {
log('Calling for next offset', sliced.offset + sliced.content.length);
loadMore(sliced);
}
}
}
private isLast = (sliced: Slice<any>): boolean => {
return sliced.content.length === sliced.totalElements;
}
public render() {
const { sliced, itemRenderer, loadingIndicator, roughItemHeight } = this.props;
if (! sliced || ! sliced.content || sliced.content.length === 0) {
return null;
}
console.log(`Rendering ${sliced.content.length} items`);
const visibilityOffset = (roughItemHeight && roughItemHeight || 20) * (sliced.sliceSize * .2);
const myLoadingIndicator = loadingIndicator || <Loading id="paged_loading_indicator" />;
// log(`Visibility offset bottom: ${(roughItemHeight && roughItemHeight || 20)} * ${paged.size} * .2 = ${-visibilityOffset}`);
const result = [
<ScrollToTopOnMount key="PAGED_LOADER_SCROLL" />,
...sliced.content.map((item: T, index) => itemRenderer(item, index)),
(
<div key="pagedLoaderLastItem">
{ ! this.isLast(sliced) ? myLoadingIndicator : null }
<VisibilitySensor delayedCall scrollCheck scrollThrottle={ 75 } intervalCheck intervalDelay={ 500 } offset={ { bottom: -visibilityOffset } } onChange={ this.endOfListVisibilityChange }>
{ () => <div>&nbsp;</div> }
</VisibilitySensor>
</div>
),
];
// log('Done rendering');
return result;
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment