Skip to content

Commit 33d3e7f

Browse files
[O2B-1087] Set-up reusable OverviewPageModel (#1292)
* [O2B-1087] Set-up reusable OverviewPageModel * Fix linter * Removed unnecessary error throwing * Improve builder reset * Improve docblocks * Rename observableData builder map to apply
1 parent e04864e commit 33d3e7f

9 files changed

Lines changed: 300 additions & 104 deletions

File tree

lib/public/models/OverviewModel.js

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,12 @@
1111
* or submit itself to any jurisdiction.
1212
*/
1313

14+
import { Observable, RemoteData } from '/js/src/index.js';
15+
import { ObservableData } from '../utilities/ObservableData.js';
16+
import { PaginatedRemoteDataSource } from '../utilities/fetch/PaginatedRemoteDataSource.js';
17+
import { PaginationModel } from '../components/Pagination/PaginationModel.js';
18+
import { buildUrl } from '../utilities/fetch/buildUrl.js';
19+
1420
/**
1521
* Interface of a model representing an overview page state
1622
*
@@ -20,3 +26,120 @@
2026
/**
2127
* @property {PaginationModel} OverviewModel#pagination pagination model of the overview
2228
*/
29+
30+
/**
31+
* Base model for an overview page
32+
*
33+
* @template T the type of data displayed in the overview page
34+
*/
35+
export class OverviewPageModel extends Observable {
36+
/**
37+
* Constructor
38+
*/
39+
constructor() {
40+
super();
41+
42+
this._pagination = new PaginationModel();
43+
this._pagination.observe(() => this.load());
44+
this._pagination.itemsPerPageSelector$.observe(() => this.notify());
45+
46+
const dataSourceObservable = ObservableData.builder().initialValue(RemoteData.loading()).build();
47+
dataSourceObservable.observe(() => this.parseApiRemoteData(dataSourceObservable.getCurrent()));
48+
49+
this._dataSource = new PaginatedRemoteDataSource();
50+
this._dataSource.pipe(dataSourceObservable);
51+
52+
this._observableItems = ObservableData.builder().initialValue(RemoteData.loading()).build();
53+
this._observableItems.bubbleTo(this);
54+
}
55+
56+
/**
57+
* Return the root endpoint for the model to use to fetch data
58+
*
59+
* @return {string} the endpoint
60+
* @abstract
61+
*/
62+
getRootEndpoint() {
63+
throw new Error('Abstract function call');
64+
}
65+
66+
/**
67+
* Reset this model to its default
68+
*
69+
* @returns {void}
70+
*/
71+
reset() {
72+
this._observableItems.setCurrent(RemoteData.notAsked());
73+
this._pagination.reset();
74+
}
75+
76+
/**
77+
* Parse the API remote data to extract the list of items to display and update the pagination
78+
*
79+
* @param {RemoteData<{items: T[], totalCount: number}, *>} remoteData the API remote data
80+
* @return {void}
81+
*/
82+
parseApiRemoteData(remoteData) {
83+
/*
84+
* When fetching data, to avoid concurrency issues, save a flag stating if the fetched data should be concatenated with the current one
85+
* (infinite scroll) or if they should replace them
86+
*/
87+
const keepExisting = this._pagination.currentPage > 1 && this._pagination.isInfiniteScrollEnabled;
88+
89+
remoteData.match({
90+
NotAsked: () => this._observableItems.setCurrent(RemoteData.notAsked()),
91+
Loading: () => this._pagination.isInfiniteScrollEnabled ? null : this._observableItems.setCurrent(RemoteData.loading()),
92+
Success: ({ items, totalCount }) => {
93+
const concatenateWith = keepExisting ? this.items.match({
94+
Success: (payload) => payload,
95+
Other: () => [],
96+
}) : [];
97+
this._pagination.itemsCount = totalCount;
98+
this._observableItems.setCurrent(RemoteData.success([...concatenateWith, ...this.processItems(items)]));
99+
},
100+
Failure: (error) => this._observableItems.setCurrent(RemoteData.failure(error)),
101+
});
102+
}
103+
104+
/**
105+
* Apply a processing on each provided items and return the result
106+
*
107+
* @param {T[]} items the items to process
108+
* @return {T[]} The list of processed items
109+
*/
110+
processItems(items) {
111+
return items;
112+
}
113+
114+
/**
115+
* Fetch all the relevant items from the API
116+
*
117+
* @return {Promise<void>} void
118+
*/
119+
async load() {
120+
const params = {
121+
'page[offset]': this._pagination.firstItemOffset,
122+
'page[limit]': this._pagination.itemsPerPage,
123+
};
124+
125+
await this._dataSource.fetch(buildUrl(this.getRootEndpoint(), params));
126+
}
127+
128+
/**
129+
* Return the current items remote data
130+
*
131+
* @return {RemoteData<T[]>} the items
132+
*/
133+
get items() {
134+
return this._observableItems.getCurrent();
135+
}
136+
137+
/**
138+
* Returns the overview pagination
139+
*
140+
* @return {PaginationModel} the pagination
141+
*/
142+
get pagination() {
143+
return this._pagination;
144+
}
145+
}

lib/public/services/lhcFill/addStatisticsToLhcFill.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ export const addStatisticsToLhcFill = (lhcFill, statistics = null, override = fa
3333
}
3434

3535
for (const key in statistics) {
36-
if (override || !Object.prototype.hasOwnProperty.call(lhcFill, key)) {
36+
if (override || !Object.hasOwn(lhcFill, key)) {
3737
lhcFill[key] = statistics[key];
3838
}
3939
}

lib/public/utilities/ObservableData.js

Lines changed: 93 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,20 +27,111 @@ export class ObservableData extends Observable {
2727
this._current = initial;
2828
}
2929

30+
/**
31+
* Returns a builder to create a new observable data
32+
*
33+
* @return {ObservableDataBuilder} the builder
34+
*/
35+
static builder() {
36+
return new ObservableDataBuilder();
37+
}
38+
3039
/**
3140
* Returns the current value of the data
3241
* @return {T} the current value
3342
*/
34-
get current() {
43+
getCurrent() {
3544
return this._current;
3645
}
3746

3847
/**
3948
* Set the current value of the data and notify
4049
* @param {T} value the current value
50+
* @return {void}
4151
*/
42-
set current(value) {
52+
setCurrent(value) {
4353
this._current = value;
4454
this.notify();
4555
}
4656
}
57+
58+
/**
59+
* Observable data that applies a processing on every value that is pushed into it
60+
*/
61+
class ObservableDataWithProcessing extends ObservableData {
62+
/**
63+
* Constructor
64+
*
65+
* @param {initialValue} initialValue the initial value of the observable data
66+
* @param {function[]} processors the list of processing to apply to the values (processors must be chainable)
67+
*/
68+
constructor(initialValue, processors) {
69+
super(initialValue);
70+
71+
this._processors = processors;
72+
}
73+
74+
// eslint-disable-next-line valid-jsdoc
75+
/**
76+
* @inheritDoc
77+
*/
78+
setCurrent(value) {
79+
let processedValue = value;
80+
81+
for (const processor of this._processors) {
82+
processedValue = processor(processedValue);
83+
}
84+
85+
super.setCurrent(processedValue);
86+
}
87+
}
88+
89+
/**
90+
* Builder to create instance of observable data
91+
*
92+
* @template T
93+
*/
94+
class ObservableDataBuilder {
95+
/**
96+
* Constructor
97+
*/
98+
constructor() {
99+
this._initialValue = null;
100+
this._processors = [];
101+
}
102+
103+
/**
104+
* Set the initial value of the observable data
105+
*
106+
* @param {T} initialValue the initial value of the observable data
107+
* @return {this} the builder instance
108+
*/
109+
initialValue(initialValue) {
110+
this._initialValue = initialValue;
111+
return this;
112+
}
113+
114+
/**
115+
* Adds a processing function that will be applied to the observable data (map functions will be called in the same
116+
* order as they have been registered)
117+
*
118+
* @param {function} processor the function to apply to observable data
119+
* @return {this} the builder instance
120+
*/
121+
apply(processor) {
122+
this._processors.push(processor);
123+
return this;
124+
}
125+
126+
/**
127+
* Ends the building process and return the built observable data
128+
*
129+
* @return {ObservableData} the build observable data
130+
*/
131+
build() {
132+
const observableData = new ObservableDataWithProcessing(this._initialValue, this._processors);
133+
this._processors = [];
134+
this._initialValue = null;
135+
return observableData;
136+
}
137+
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
/**
2+
* @license
3+
* Copyright CERN and copyright holders of ALICE O2. This software is
4+
* distributed under the terms of the GNU General Public License v3 (GPL
5+
* Version 3), copied verbatim in the file "COPYING".
6+
*
7+
* See http://alice-o2.web.cern.ch/license for full licensing information.
8+
*
9+
* In applying this license CERN does not waive the privileges and immunities
10+
* granted to it by virtue of its status as an Intergovernmental Organization
11+
* or submit itself to any jurisdiction.
12+
*/
13+
import { RemoteDataSource } from './RemoteDataSource.js';
14+
import { getRemoteDataSlice } from './getRemoteDataSlice.js';
15+
16+
/**
17+
* Data source fetching paginated remote data
18+
*/
19+
export class PaginatedRemoteDataSource extends RemoteDataSource {
20+
// eslint-disable-next-line valid-jsdoc
21+
/**
22+
* @inheritDoc
23+
* @return {Promise<{data: *, totalCount: number}>}
24+
*/
25+
async getRemoteData(endpoint) {
26+
return getRemoteDataSlice(endpoint, { signal: this._abortController.signal });
27+
}
28+
}

lib/public/utilities/fetch/RemoteDataSource.js

Lines changed: 27 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -10,60 +10,71 @@
1010
* granted to it by virtue of its status as an Intergovernmental Organization
1111
* or submit itself to any jurisdiction.
1212
*/
13-
import { Observable, RemoteData } from '/js/src/index.js';
13+
import { RemoteData } from '/js/src/index.js';
1414
import { getRemoteData } from './getRemoteData.js';
15-
import { ObservableData } from '../ObservableData.js';
1615

1716
/**
1817
* Service class providing observable data fetching process
1918
*/
20-
export class RemoteDataSource extends Observable {
19+
export class RemoteDataSource {
2120
/**
2221
* Constructor
2322
*/
2423
constructor() {
25-
super();
26-
2724
/**
28-
* @type {ObservableData<RemoteData>}
25+
* @type {ObservableData<RemoteData> | null}
2926
* @private
3027
*/
31-
this._observableData = new ObservableData(RemoteData.notAsked());
28+
this._observableData = null;
3229
this._abortController = null;
3330
}
3431

32+
/**
33+
* Sets the observable data to which source's data should be pushed to
34+
*
35+
* @param {ObservableData} observableData the observable data to which data should be pushed
36+
* @return {void}
37+
*/
38+
pipe(observableData) {
39+
this._observableData = observableData;
40+
}
41+
3542
/**
3643
* Fetch the given endpoint to fill current data
3744
*
3845
* @param {string} endpoint the endpoint to fetch
3946
* @return {Promise<void>} resolves once the data fetching has ended
4047
*/
4148
async fetch(endpoint) {
42-
this._observableData.current = RemoteData.loading();
49+
if (!this._observableData) {
50+
return null;
51+
}
52+
53+
this._observableData.setCurrent(RemoteData.loading());
4354

4455
const abortController = new AbortController();
4556
try {
4657
if (this._abortController) {
4758
this._abortController.abort();
4859
}
4960
this._abortController = abortController;
50-
const { data } = await getRemoteData(endpoint, { signal: this._abortController.signal });
51-
this._observableData.current = RemoteData.success(data);
61+
const data = await this.getRemoteData(endpoint);
62+
this._observableData.setCurrent(RemoteData.success(data));
5263
} catch (error) {
5364
// Use local variable because the class member (this._abortController) may already have been override in another call
5465
if (!abortController.signal.aborted) {
55-
this._observableData.current = RemoteData.failure(error);
66+
this._observableData.setCurrent(RemoteData.failure(error));
5667
}
57-
throw error;
5868
}
5969
}
6070

6171
/**
62-
* Return the observable remote data provided by this source
72+
* Fetch the endpoint and return the result
6373
*
64-
* @return {ObservableData<RemoteData>} the observable remote data
74+
* @param {string} endpoint the endpoint to fetch
75+
* @return {Promise<*>} the result
6576
*/
66-
get observableData() {
67-
return this._observableData;
77+
async getRemoteData(endpoint) {
78+
return getRemoteData(endpoint, { signal: this._abortController.signal });
6879
}
6980
}

lib/public/views/LhcFills/LhcFills.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ export default class LhcFills extends Observable {
4242
* @returns {void}
4343
*/
4444
loadOverview() {
45-
this._overviewModel.fetchAllLhcFills();
45+
this._overviewModel.load();
4646
}
4747

4848
/**

0 commit comments

Comments
 (0)